Recognize commercial parts with alphabetic names in .asm scanning
Pro-E numeric codes cover most components, but parts downloaded as STEP from online catalogs (rings, bushings, knobs...) get alphabetic Creo names with no predictable shape. Add a second extraction path that tokenizes the binary generically and validates candidates against the known codes in cad_index.db, applying the same placement/occurrence rule used for bare numeric codes. Numeric extraction is unchanged, so missing-from-index reporting for renamed/moved Pro-E codes still works. Also load all known prt/asm codes once per scan instead of one query per candidate, in both RisolviAlberoComponenti and VerificaCompletezzaAssiemi. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c75dd21551
commit
8a8a7d0417
7 files changed
+363
-106
No files matched your search
@@ -105,9 +105,12 @@ disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
|||||||
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
|
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
|
||||||
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
|
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
|
||||||
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
||||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`); `Analyze.cs`
|
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
||||||
dumpa il contesto testo/hex attorno a un pattern in un file binario, per investigare a mano
|
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
|
||||||
nuovi falsi positivi prima di toccare la regex.
|
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
|
||||||
|
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
|
||||||
|
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
|
||||||
|
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
|
||||||
|
|
||||||
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
|
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
|
||||||
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class AssemblyComponentScannerTests
|
|||||||
"Componente ID 1 (R12--_-_U6592<UNI6592>.PRT) x Componente ID 2 (R12--_-_U6592<UNI6592>.PRT)");
|
"Componente ID 1 (R12--_-_U6592<UNI6592>.PRT) x Componente ID 2 (R12--_-_U6592<UNI6592>.PRT)");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "UNI6592" });
|
||||||
Assert.Contains("UNI6592", codici);
|
Assert.Contains("UNI6592", codici);
|
||||||
Assert.DoesNotContain("R12--_-_U6592<UNI6592>", codici);
|
Assert.DoesNotContain("R12--_-_U6592<UNI6592>", codici);
|
||||||
}
|
}
|
||||||
@@ -103,4 +103,49 @@ public class AssemblyComponentScannerTests
|
|||||||
public void File_inesistente_lancia_eccezione_IO() =>
|
public void File_inesistente_lancia_eccezione_IO() =>
|
||||||
Assert.Throws<DirectoryNotFoundException>(() =>
|
Assert.Throws<DirectoryNotFoundException>(() =>
|
||||||
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Riconosce_parte_commerciale_con_nome_alfabetico_se_nel_db()
|
||||||
|
{
|
||||||
|
// Visto su un caso reale: le parti commerciali scaricate come STEP (es. anelli,
|
||||||
|
// boccole) hanno nomi Creo alfabetici, non un codice numerico Pro-E - si
|
||||||
|
// riconoscono solo se il nome esiste davvero come codice indicizzato.
|
||||||
|
string percorso = ScriviAsmFinto(
|
||||||
|
"blah Componente ID 11 (ANELLO_ESTERNO-1-SOLID1.PRT) blah RUMORE_CASUALE blah");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codiciNoti = new[] { "ANELLO_ESTERNO-1-SOLID1" };
|
||||||
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||||
|
Assert.Contains("ANELLO_ESTERNO-1-SOLID1", codici);
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scarta_token_alfabetico_nel_db_ma_senza_placement()
|
||||||
|
{
|
||||||
|
// Un token che coincide per puro caso con un codice DB corto/generico (es. una
|
||||||
|
// keyword interna di Creo) non basta: senza un vero placement resta rumore.
|
||||||
|
string percorso = ScriviAsmFinto("start_part blah blah start_part");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codiciNoti = new[] { "start_part" };
|
||||||
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||||
|
Assert.DoesNotContain("start_part", codici);
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ignora_token_alfabetici_non_presenti_nel_db()
|
||||||
|
{
|
||||||
|
string percorso = ScriviAsmFinto(
|
||||||
|
"blah Componente ID 1 (QUALCOSA_DI_INESISTENTE-1.PRT) blah");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "ALTRO_CODICE" });
|
||||||
|
Assert.DoesNotContain("QUALCOSA_DI_INESISTENTE-1", codici);
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,59 +6,86 @@ namespace CadSearchGui;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
|
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
|
||||||
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
|
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
|
||||||
/// come stringhe ASCII leggibili al suo interno: si scansiona il file come testo
|
/// come stringhe ASCII leggibili al suo interno.
|
||||||
/// cercando pattern che assomigliano a un codice (stesso stile usato nei nomi file,
|
|
||||||
/// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli
|
|
||||||
/// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non
|
|
||||||
/// corrispondono a nessun codice reale).
|
|
||||||
///
|
///
|
||||||
/// Verificato su un caso reale confrontando con un report BOM generato da Creo: i
|
/// Due famiglie di codici, due strategie:
|
||||||
|
///
|
||||||
|
/// 1) La maggioranza (>90%) sono codici numerici Pro-E del gestionale, con una forma
|
||||||
|
/// nota (stesso stile usato nei nomi file, vedi Indexer.FilenameRegex): si riconoscono
|
||||||
|
/// da soli, senza bisogno del DB, e possono quindi essere segnalati come "referenziati
|
||||||
|
/// ma non trovati nell'indice" quando un codice risulta rinominato/spostato - il
|
||||||
|
/// chiamante fa la ricerca nel DB e riporta i mancanti.
|
||||||
|
///
|
||||||
|
/// 2) Una minoranza sono parti commerciali scaricate come STEP da cataloghi online
|
||||||
|
/// (viti, anelli, boccole...), che in Creo hanno nomi alfabetici senza una forma
|
||||||
|
/// riconoscibile a priori (es. "ANELLO_ESTERNO-1-SOLID1", non un numero). Per questi
|
||||||
|
/// non esiste un pattern da indovinare: si tokenizza in modo generico (qualunque run
|
||||||
|
/// di lettere/cifre/underscore/trattino) e si lascia decidere al DB, già caricato dal
|
||||||
|
/// chiamante, se il token è un codice vero - altrimenti il rumore binario sarebbe
|
||||||
|
/// ingestibile. Un token così "convalidato" è per costruzione già nell'indice, quindi
|
||||||
|
/// non contribuisce mai ai "non trovati": per le parti commerciali mancanti dall'indice
|
||||||
|
/// non c'è modo di distinguerle dal rumore, restano semplicemente invisibili come lo
|
||||||
|
/// erano prima.
|
||||||
|
///
|
||||||
|
/// Verificato su un caso reale confrontando con l'albero componenti di Creo: i
|
||||||
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
|
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
|
||||||
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
|
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
|
||||||
/// una sola volta. Si scartano quindi i candidati con una sola occorrenza.
|
/// una sola volta. Si scartano quindi, per entrambe le famiglie, i candidati "nudi" (senza
|
||||||
|
/// un suffisso di revisione/variante o una forma family-table nota) che non abbiano anche
|
||||||
|
/// un vero placement testuale nel binario.
|
||||||
///
|
///
|
||||||
/// Falso positivo noto ed escluso: gli ID interni di viste salvate, nel formato
|
/// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel
|
||||||
/// "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
/// formato "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
||||||
/// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md.
|
/// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md.
|
||||||
///
|
///
|
||||||
/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
|
/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
|
||||||
/// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>",
|
/// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>",
|
||||||
/// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il
|
/// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il
|
||||||
/// file reale indicizzato è il generico "uni6592.prt.<versione>"/
|
/// file reale indicizzato è il generico "uni6592.prt.<versione>"/
|
||||||
/// "cusci_acbb7.prt.<versione>", quindi va estratto solo il contenuto tra "<" e
|
/// "cusci_acbb7.prt.<versione>". Con la tokenizzazione generica "UNI6592"/
|
||||||
/// ">". Vedi docs/formato-asm.md.
|
/// "CUSCI_ACBB7" vengono catturati da soli: "<" e ">" non fanno parte del charset del
|
||||||
|
/// token e ne delimitano naturalmente i confini. Vedi docs/formato-asm.md.
|
||||||
///
|
///
|
||||||
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
|
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
|
||||||
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es.
|
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es.
|
||||||
/// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100").
|
/// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100").
|
||||||
///
|
///
|
||||||
/// Un codice "nudo" (senza suffisso "_..." e senza bracket UNI) ripetuto più volte non
|
/// Un codice "nudo" (senza suffisso "_..." e senza forma family-table nota) ripetuto più
|
||||||
/// basta: può trattarsi del riferimento al nome di un altro assieme che lo contiene (non
|
/// volte non basta: può trattarsi del riferimento al nome di un altro assieme che lo
|
||||||
/// noto a priori, quindi non escludibile per confronto diretto come il proprio codice) o
|
/// contiene (non noto a priori, quindi non escludibile per confronto diretto come il
|
||||||
/// di un residuo di feature soppresse/superate (vedi docs/formato-asm.md). Per questi si
|
/// proprio codice) o di un residuo di feature soppresse/superate, oppure - per la
|
||||||
/// richiede anche un vero placement, in una delle quattro forme osservate nel binario:
|
/// tokenizzazione generica - di puro rumore binario che coincide con un codice DB corto o
|
||||||
/// "Componente ID N (<codice>...)"/"Component id N (<codice>...)" (la stringa
|
/// generico (vedi docs/formato-asm.md). Per questi si richiede anche un vero placement, in
|
||||||
/// cambia con la localizzazione Creo del file); il codice ripetuto due volte di seguito
|
/// una delle quattro forme osservate nel binario: "Componente ID N (<codice>...)"/
|
||||||
/// separato da un singolo byte NUL ("<codice>\0<codice>\0", osservato per
|
/// "Component id N (<codice>...)" (la stringa cambia con la localizzazione Creo del
|
||||||
/// componenti di family table non-UNI, es. cuscinetti "CUSCI_..."); il campo
|
/// file); il codice ripetuto due volte di seguito separato da un singolo byte NUL
|
||||||
/// "family_name\0<codice>\0" (osservato una sola volta per file, su un sotto-assieme
|
/// ("<codice>\0<codice>\0", osservato per componenti di family table non-UNI,
|
||||||
/// referenziato come family table generica invece che con un placement testuale esplicito);
|
/// es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0" (osservato una
|
||||||
/// oppure il codice seguito immediatamente da un bracket "<codice><GENERICO>" -
|
/// sola volta per file, su un sotto-assieme referenziato come family table generica invece
|
||||||
/// l'istanza di una family table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>",
|
/// che con un placement testuale esplicito); oppure il codice seguito immediatamente da un
|
||||||
/// un cuscinetto): il codice dell'istanza non è mai indicizzato a sé (solo il generico tra
|
/// bracket "<codice><GENERICO>" - l'istanza di una family table non-UNI vista
|
||||||
/// "<" e ">" lo è), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel
|
/// nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il codice dell'istanza
|
||||||
/// DB invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
|
/// non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è), ma questo
|
||||||
/// scartato come "non in indice", ma in modo tracciabile. Non si applica alle varianti con
|
/// segnale lo fa comunque sopravvivere fino alla ricerca nel DB invece di scomparire in
|
||||||
/// suffisso o bracket UNI: in alcuni file il placement riporta solo il nome
|
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
|
||||||
/// istanza/generico senza il dettaglio del suffisso o del bracket, che vive altrove nei
|
/// ma in modo tracciabile.
|
||||||
/// metadati - lì resta necessaria la sola euristica delle occorrenze ripetute.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
static class AssemblyComponentScanner
|
static class AssemblyComponentScanner
|
||||||
{
|
{
|
||||||
private static readonly Regex CodiceRegex = new(
|
private static readonly Regex CodiceNumericoRegex = new(
|
||||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
|
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
||||||
RegexOptions.Compiled);
|
RegexOptions.Compiled);
|
||||||
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
|
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Nessuna forma ipotizzata a mano per le parti commerciali: qualunque run di
|
||||||
|
// lettere/cifre/underscore/trattino è un candidato, validato poi contro il DB.
|
||||||
|
private static readonly Regex TokenGenericoRegex = new(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Suffisso di revisione/variante (es. "_C01", "_D100", "_8188") o prefisso di family
|
||||||
|
// table nota (UNI/CUSCI_): segnale forte abbastanza da non richiedere un placement
|
||||||
|
// esplicito, come per i codici numerici nudi con lo stesso suffisso.
|
||||||
|
private static readonly Regex SuffissoRegex = new(@"_[A-Za-z]?\d{2,4}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
private const int OccorrenzeMinime = 2;
|
private const int OccorrenzeMinime = 2;
|
||||||
|
|
||||||
/// <summary>Codice radice (parte prima del primo "_") di un codice componente. Un
|
/// <summary>Codice radice (parte prima del primo "_") di un codice componente. Un
|
||||||
@@ -70,15 +97,13 @@ static class AssemblyComponentScanner
|
|||||||
/// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario.</summary>
|
/// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario.</summary>
|
||||||
public static string CodiceRadice(string codice) => codice.Split('_')[0];
|
public static string CodiceRadice(string codice) => codice.Split('_')[0];
|
||||||
|
|
||||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
/// <param name="codiciNoti">Tutti i codici prt/asm noti all'indice (caricati una sola
|
||||||
|
/// volta dal chiamante, non per-file): usati per validare i token generici della
|
||||||
|
/// famiglia 2 (parti commerciali). Non serve per la famiglia 1 (codici numerici), che
|
||||||
|
/// si riconosce dalla forma senza bisogno del DB.</param>
|
||||||
|
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string>? codiciNoti = null)
|
||||||
{
|
{
|
||||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
||||||
var occorrenze = new Dictionary<string, int>();
|
|
||||||
foreach (Match m in CodiceRegex.Matches(testo))
|
|
||||||
{
|
|
||||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
|
||||||
}
|
|
||||||
var candidati = occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
|
||||||
|
|
||||||
bool HaPlacement(string codice) =>
|
bool HaPlacement(string codice) =>
|
||||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||||
@@ -86,7 +111,40 @@ static class AssemblyComponentScanner
|
|||||||
|| testo.Contains("family_name\0" + codice + "\0")
|
|| testo.Contains("family_name\0" + codice + "\0")
|
||||||
|| testo.Contains(codice + "<");
|
|| testo.Contains(codice + "<");
|
||||||
|
|
||||||
candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c));
|
var risultato = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
return candidati;
|
|
||||||
|
var occorrenzeNumeriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (Match m in CodiceNumericoRegex.Matches(testo))
|
||||||
|
occorrenzeNumeriche[m.Value] = occorrenzeNumeriche.GetValueOrDefault(m.Value) + 1;
|
||||||
|
foreach (var (codice, occorrenze) in occorrenzeNumeriche)
|
||||||
|
{
|
||||||
|
if (occorrenze < OccorrenzeMinime)
|
||||||
|
continue;
|
||||||
|
if (CodiceNudoRegex.IsMatch(codice) && !HaPlacement(codice))
|
||||||
|
continue;
|
||||||
|
risultato.Add(codice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codiciNoti is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var occorrenzeGeneriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (Match m in TokenGenericoRegex.Matches(testo))
|
||||||
|
{
|
||||||
|
if (codiciNoti.Contains(m.Value))
|
||||||
|
occorrenzeGeneriche[m.Value] = occorrenzeGeneriche.GetValueOrDefault(m.Value) + 1;
|
||||||
|
}
|
||||||
|
foreach (var (codice, occorrenze) in occorrenzeGeneriche)
|
||||||
|
{
|
||||||
|
if (risultato.Contains(codice))
|
||||||
|
continue;
|
||||||
|
bool formaNota = SuffissoRegex.IsMatch(codice)
|
||||||
|
|| codice.StartsWith("UNI", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| codice.StartsWith("CUSCI_", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (formaNota ? occorrenze >= OccorrenzeMinime : HaPlacement(codice))
|
||||||
|
risultato.Add(codice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return risultato;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
-58
@@ -662,6 +662,31 @@ public partial class Form1 : Form
|
|||||||
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
||||||
|
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
|
||||||
|
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
|
||||||
|
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
|
||||||
|
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
|
||||||
|
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
|
||||||
|
{
|
||||||
|
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||||
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
string codice = reader.GetString(0);
|
||||||
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||||
|
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||||
|
if (!lista.Any(x => x.Tipo == tipo))
|
||||||
|
lista.Add((tipo, reader.GetString(2)));
|
||||||
|
}
|
||||||
|
return codiciNoti;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
||||||
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
||||||
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
||||||
@@ -681,42 +706,19 @@ public partial class Form1 : Form
|
|||||||
if (daScansionare.Count == 0 || _dbPath is null)
|
if (daScansionare.Count == 0 || _dbPath is null)
|
||||||
return (trovati, codiciNonTrovati);
|
return (trovati, codiciNonTrovati);
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||||
connection.Open();
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
|
|
||||||
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
|
||||||
+ "ORDER BY tipo_file, data_modifica DESC";
|
|
||||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
||||||
|
|
||||||
int processati = 0;
|
int processati = 0;
|
||||||
while (daScansionare.Count > 0)
|
while (daScansionare.Count > 0)
|
||||||
{
|
{
|
||||||
var asm = daScansionare.Dequeue();
|
var asm = daScansionare.Dequeue();
|
||||||
processati++;
|
processati++;
|
||||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
|
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||||
{
|
{
|
||||||
if (!codiciVisti.Add(codice))
|
if (!codiciVisti.Add(codice))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
pCodice.Value = codice;
|
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||||
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla
|
|
||||||
// share): ordinato per data_modifica DESC, si tiene solo il più recente per tipo.
|
|
||||||
var componenti = new List<(string Tipo, string Percorso)>();
|
|
||||||
var tipiVisti = new HashSet<string>();
|
|
||||||
using (var reader = command.ExecuteReader())
|
|
||||||
{
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
string tipoLower = reader.GetString(0).ToLowerInvariant();
|
|
||||||
if (tipiVisti.Add(tipoLower))
|
|
||||||
{
|
|
||||||
componenti.Add((tipoLower, reader.GetString(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componenti.Count == 0)
|
|
||||||
{
|
{
|
||||||
codiciNonTrovati.Add(codice);
|
codiciNonTrovati.Add(codice);
|
||||||
continue;
|
continue;
|
||||||
@@ -790,39 +792,11 @@ public partial class Form1 : Form
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||||
connection.Open();
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
|
|
||||||
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
|
||||||
+ "ORDER BY tipo_file, data_modifica DESC";
|
|
||||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
||||||
|
|
||||||
var cacheComponenti = new Dictionary<string, List<(string Tipo, string Percorso)>>();
|
|
||||||
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
||||||
|
|
||||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
|
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
|
||||||
{
|
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
|
||||||
if (cacheComponenti.TryGetValue(codice, out var giaTrovati))
|
|
||||||
return giaTrovati;
|
|
||||||
|
|
||||||
pCodice.Value = codice;
|
|
||||||
var risultato = new List<(string Tipo, string Percorso)>();
|
|
||||||
var tipiVisti = new HashSet<string>();
|
|
||||||
using (var reader = command.ExecuteReader())
|
|
||||||
{
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
string tipo = reader.GetString(0).ToLowerInvariant();
|
|
||||||
if (tipiVisti.Add(tipo))
|
|
||||||
{
|
|
||||||
risultato.Add((tipo, reader.GetString(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cacheComponenti[codice] = risultato;
|
|
||||||
return risultato;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool AssiemeCompleto(string percorsoAsmIniziale)
|
bool AssiemeCompleto(string percorsoAsmIniziale)
|
||||||
{
|
{
|
||||||
@@ -843,7 +817,7 @@ public partial class Form1 : Form
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
|
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
|||||||
+48
-2
@@ -179,6 +179,52 @@ table il placement può mancare del dettaglio, quindi resta necessaria la
|
|||||||
sola euristica delle occorrenze ripetute - meno precisa, ma l'unica
|
sola euristica delle occorrenze ripetute - meno precisa, ma l'unica
|
||||||
applicabile senza rischiare falsi negativi su casi come questi.
|
applicabile senza rischiare falsi negativi su casi come questi.
|
||||||
|
|
||||||
|
## Parti commerciali con nome alfabetico (validazione via DB)
|
||||||
|
|
||||||
|
Oltre il 90% dei codici indicizzati sono numerici Pro-E (regex sopra), ma una minoranza
|
||||||
|
sono parti commerciali scaricate come STEP da cataloghi online (anelli, boccole, guarnizioni,
|
||||||
|
manopole...) che in Creo hanno un nome **alfabetico**, assegnato dal file STEP di origine, non
|
||||||
|
un codice del gestionale — es. `NOME_PARTE-1-SOLID1`, senza nessuna cifra riconoscibile. Non
|
||||||
|
esiste un pattern comune da ipotizzare a priori per questi nomi: variano da fornitore a
|
||||||
|
fornitore.
|
||||||
|
|
||||||
|
Per questa famiglia l'estrazione si inverte: invece di restringere la regex a una forma nota
|
||||||
|
(come per i codici numerici), si tokenizza il binario in modo generico —
|
||||||
|
|
||||||
|
```
|
||||||
|
regex: [A-Za-z0-9_-]{3,}
|
||||||
|
```
|
||||||
|
|
||||||
|
— e si lascia decidere al DB (`cad_index.db`, già caricato in memoria una sola volta dal
|
||||||
|
chiamante, non per-file) se il token è un codice reale. Senza questo filtro il rumore binario
|
||||||
|
sarebbe ingestibile: su un assieme con poche migliaia di stringhe, la tokenizzazione generica
|
||||||
|
produce tipicamente qualche migliaio di candidati, di cui solo una manciata sopravvive al
|
||||||
|
controllo DB.
|
||||||
|
|
||||||
|
L'essere nel DB da solo non basta, per lo stesso motivo dei codici numerici "nudi": codici
|
||||||
|
corti o generici (es. una keyword interna di Creo come `start_part`, o un codice indicizzato
|
||||||
|
per errore/caso limite) possono comparire per puro rumore. Si applica quindi la stessa regola
|
||||||
|
di validazione della famiglia numerica:
|
||||||
|
|
||||||
|
- un token con un suffisso di revisione/variante (`_[A-Za-z]?\d{2,4}` in coda, es. `_C01`,
|
||||||
|
`_D100`) o un prefisso di family table nota (`UNI`, `CUSCI_`) è considerato "forma nota":
|
||||||
|
basta che occorra almeno 2 volte nel file;
|
||||||
|
- un token "nudo" (nessuno dei due segnali) richiede invece un vero placement testuale, come
|
||||||
|
per i codici numerici nudi (stesse quattro forme viste sopra).
|
||||||
|
|
||||||
|
Una parte commerciale referenziata ma **non ancora indicizzata** non è distinguibile dal
|
||||||
|
rumore con questo metodo: resta invisibile, come lo era prima che questa famiglia venisse
|
||||||
|
gestita. La famiglia numerica invece resta rilevabile anche quando manca dall'indice
|
||||||
|
(rinominata/spostata), perché si riconosce dalla forma senza bisogno del DB — è per questo
|
||||||
|
che le due famiglie restano due passaggi distinti invece di un unico tokenizer generico
|
||||||
|
sempre validato contro il DB: unificarle farebbe perdere la segnalazione dei codici numerici
|
||||||
|
mancanti (vedi "Riferimenti fantasma" sopra).
|
||||||
|
|
||||||
|
`AssemblyComponentScanner.EstraiCodiciReferenziati` accetta quindi un parametro opzionale
|
||||||
|
`codiciNoti` (l'insieme di tutti i codici prt/asm noti all'indice) usato solo per questa
|
||||||
|
seconda famiglia; se omesso, si estraggono solo i codici numerici. `scripts/BomViaDb.cs`
|
||||||
|
è il prototipo standalone usato per validare l'approccio prima di portarlo nella GUI.
|
||||||
|
|
||||||
## Componenti normalizzati (family table UNI, CUSCI, ...)
|
## Componenti normalizzati (family table UNI, CUSCI, ...)
|
||||||
|
|
||||||
I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
|
I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
|
||||||
@@ -225,8 +271,8 @@ livello) serve una BFS:
|
|||||||
Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
|
Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
|
||||||
duplicata in due punti del codice per scopi diversi:
|
duplicata in due punti del codice per scopi diversi:
|
||||||
|
|
||||||
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza.
|
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato.
|
||||||
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usato per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
|
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
|
||||||
|
|
||||||
Nota: le query contro `files_ultima_versione` filtrano solo per
|
Nota: le query contro `files_ultima_versione` filtrano solo per
|
||||||
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
|
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#:package Microsoft.Data.Sqlite@10.0.12
|
||||||
|
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||||
|
//
|
||||||
|
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo
|
||||||
|
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
||||||
|
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
||||||
|
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
||||||
|
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
||||||
|
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
||||||
|
// quindi si richiede comunque il placement esplicito nel file, come in
|
||||||
|
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
if (args.Length < 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Uso: dotnet run BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string asmIniziale = args[0];
|
||||||
|
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||||
|
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||||
|
|
||||||
|
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
|
||||||
|
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
|
||||||
|
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
|
||||||
|
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
|
||||||
|
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
|
||||||
|
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
|
||||||
|
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
|
||||||
|
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
||||||
|
// candidato estratto dal binario diventa O(1) invece di una query per file.
|
||||||
|
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
using (var command = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||||
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
string codice = reader.GetString(0);
|
||||||
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||||
|
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||||
|
if (!lista.Any(x => x.Tipo == tipo))
|
||||||
|
lista.Add((tipo, reader.GetString(2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||||
|
|
||||||
|
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
|
||||||
|
// la sola presenza del codice nel DB non basta, perché codici corti o generici
|
||||||
|
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
|
||||||
|
// componente vero è sempre referenziato in una di queste forme (vedi
|
||||||
|
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
|
||||||
|
// il codice ripetuto due volte separato da un byte NUL, il campo
|
||||||
|
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
|
||||||
|
bool HaPlacement(string testo, string codice) =>
|
||||||
|
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||||
|
|| testo.Contains(codice + "\0" + codice + "\0")
|
||||||
|
|| testo.Contains("family_name\0" + codice + "\0")
|
||||||
|
|| testo.Contains(codice + "<");
|
||||||
|
|
||||||
|
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||||
|
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
||||||
|
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
|
||||||
|
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
|
||||||
|
var codiceRadice = codiceProprio.Split('_')[0];
|
||||||
|
|
||||||
|
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||||
|
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||||
|
|
||||||
|
while (daScansionare.Count > 0)
|
||||||
|
{
|
||||||
|
var asm = daScansionare.Dequeue();
|
||||||
|
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
||||||
|
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var candidato in candidati)
|
||||||
|
{
|
||||||
|
if (!codiciVisti.Add(candidato))
|
||||||
|
continue;
|
||||||
|
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
||||||
|
{
|
||||||
|
scartatiNonInIndice.Add(candidato);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!HaPlacement(testo, candidato))
|
||||||
|
{
|
||||||
|
scartatiSenzaPlacement.Add(candidato);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
componentiTrovati.Add(candidato);
|
||||||
|
foreach (var (tipo, percorso) in componenti)
|
||||||
|
if (tipo == "asm")
|
||||||
|
daScansionare.Enqueue(percorso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllLines(outPath, componentiTrovati);
|
||||||
|
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||||
|
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
||||||
|
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
||||||
|
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
||||||
|
return 0;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#:package Microsoft.Data.Sqlite@10.0.12
|
||||||
|
// dotnet run scripts/Query.cs -- <cad_index.db> <sql>
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection($"Data Source={args[0]};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = args[1];
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
int righe = 0;
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
righe++;
|
||||||
|
var valori = Enumerable.Range(0, reader.FieldCount).Select(i => reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString());
|
||||||
|
Console.WriteLine(string.Join(" | ", valori));
|
||||||
|
}
|
||||||
|
Console.WriteLine($"({righe} righe)");
|
||||||
|
return 0;
|
||||||
Reference in new issue
Block a user