Files
prep-step/PrepStep/DistintaBaseImporter.cs
T
davideandClaude Sonnet 5 13798ad47c Add explicit working folder; rename project from CadSearchGui to PrepStep
Working folder (phase 1): CSV/raw/step/step_finali all used to live split
across two different implicit locations (CSV in the "repo root" found by
walking up for CLAUDE.md, raw/step next to the input distinta file) with
neither shown to the user. That repo-root lookup also silently breaks once
the app runs as a standalone published exe on a machine with no repo/
CLAUDE.md at all. Replaced both with one explicit "cartella di lavoro",
auto-suggested (last used, remembered the same way as the DB path) and
changeable from phase 1, with no dependency on any repo marker file.

Rename: CadSearchGui was the working name from before the app had a
product identity; renamed folders, csproj files, namespace and the
AppData settings folder to PrepStep throughout, matching the exe name
already in use. Product-facing name stays "Cevolani StepPrep".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:18:43 +02:00

125 lines
5.3 KiB
C#

using System.Text;
using System.Xml.Linq;
using ExcelDataReader;
namespace PrepStep;
/// <summary>
/// Converte il file "Distinta Base" esportato dal gestionale (SpreadsheetML
/// XML con estensione .xls/.xml, oppure .xls/.xlsx binario vero) nelle sole
/// colonne Componente/Descrizione in CSV.
/// </summary>
static class DistintaBaseImporter
{
private static readonly XNamespace Ss = "urn:schemas-microsoft-com:office:spreadsheet";
private const string NomeFoglio = "Distinta Base";
// Il gestionale esporta la distinta come "DBP_<codice assieme generale>_<timestamp>.xls":
// il foglio elenca solo i componenti SOTTO l'assieme, non l'assieme generale in sé, quindi
// va aggiunto a parte al CSV leggendo questo codice dal nome del file di input.
private static readonly System.Text.RegularExpressions.Regex NomeFileDistintaRegex =
new(@"^DBP_(?<codice>\d+)_\d+$", System.Text.RegularExpressions.RegexOptions.Compiled);
static DistintaBaseImporter()
{
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
public static (string PercorsoCsv, List<(string Componente, string Descrizione)> Righe) ConvertiInCsv(
string percorsoInput, string? cartellaOutput = null)
{
var righeGrezze = EImmagineSpreadsheetMl(percorsoInput)
? LeggiRigheSpreadsheetMl(percorsoInput)
: LeggiRigheExcelBinario(percorsoInput);
var righe = righeGrezze.Skip(1) // salta l'intestazione
.Where(r => r.Componente.Length > 0 || r.Descrizione.Length > 0)
.ToList();
var matchGenerale = NomeFileDistintaRegex.Match(Path.GetFileNameWithoutExtension(percorsoInput));
if (matchGenerale.Success)
{
string codiceGenerale = matchGenerale.Groups["codice"].Value;
if (!righe.Any(r => string.Equals(r.Componente, codiceGenerale, StringComparison.OrdinalIgnoreCase)))
righe.Insert(0, (codiceGenerale, "Assieme generale"));
}
var percorsoCsv = Path.Combine(
cartellaOutput ?? Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!,
Path.GetFileNameWithoutExtension(percorsoInput) + ".csv");
using var writer = new StreamWriter(percorsoCsv, false, Encoding.UTF8);
writer.WriteLine("Componente;Descrizione");
foreach (var (componente, descrizione) in righe)
{
writer.WriteLine($"{EscapeCsv(componente)};{EscapeCsv(descrizione)}");
}
return (percorsoCsv, righe);
}
/// <summary>I file XML "SpreadsheetML" iniziano con l'intestazione XML testuale;
/// i file .xls/.xlsx binari veri iniziano invece con una firma binaria (OLE2 o ZIP).</summary>
private static bool EImmagineSpreadsheetMl(string percorso)
{
Span<byte> primiByte = stackalloc byte[5];
using var stream = File.OpenRead(percorso);
var letti = stream.Read(primiByte);
return letti >= 5 && Encoding.ASCII.GetString(primiByte) == "<?xml";
}
private static List<(string Componente, string Descrizione)> LeggiRigheSpreadsheetMl(string percorsoInput)
{
var doc = XDocument.Load(percorsoInput);
var foglio = doc.Descendants(Ss + "Worksheet")
.FirstOrDefault(w => (string?)w.Attribute(Ss + "Name") == NomeFoglio)
?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file.");
var righeXml = foglio.Descendants(Ss + "Row").ToList();
if (righeXml.Count == 0)
throw new InvalidDataException($"Il foglio '{NomeFoglio}' non contiene righe.");
return righeXml.Select(LeggiColonneBeCSpreadsheetMl).ToList();
}
private static (string Componente, string Descrizione) LeggiColonneBeCSpreadsheetMl(XElement riga)
{
string b = "", c = "";
int colonna = 0;
foreach (var cella in riga.Elements(Ss + "Cell"))
{
// ss:Index salta le celle vuote iniziali: senza attributo la colonna è quella precedente + 1
var indice = cella.Attribute(Ss + "Index");
colonna = indice != null ? int.Parse(indice.Value) : colonna + 1;
if (colonna == 2) b = cella.Element(Ss + "Data")?.Value ?? "";
else if (colonna == 3) c = cella.Element(Ss + "Data")?.Value ?? "";
}
return (b, c);
}
private static List<(string Componente, string Descrizione)> LeggiRigheExcelBinario(string percorsoInput)
{
using var stream = File.Open(percorsoInput, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = ExcelReaderFactory.CreateReader(stream);
var dataSet = reader.AsDataSet();
var tabella = dataSet.Tables[NomeFoglio]
?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file.");
var righe = new List<(string Componente, string Descrizione)>();
foreach (System.Data.DataRow riga in tabella.Rows)
{
var b = riga.Table.Columns.Count > 1 ? riga[1]?.ToString() ?? "" : "";
var c = riga.Table.Columns.Count > 2 ? riga[2]?.ToString() ?? "" : "";
righe.Add((b, c));
}
return righe;
}
private static string EscapeCsv(string valore)
{
if (valore.Contains(';') || valore.Contains('"') || valore.Contains('\n'))
return $"\"{valore.Replace("\"", "\"\"")}\"";
return valore;
}
}