Source file: /~heha/hsn/wsh.zip/Erfassungsliste.js

/* Excel-CSV-Export-Erfassungsliste einlesen und bearbeiten
 Eingabedaten liegen in CP1252 vor, nicht in UTF-8.
 Diese Datei ist in CP1252, bei UTF-8 funktionieren die Umlaute auf der Konsole nicht richtig.
 Eine BOM wird von cscript.exe nicht verdaut.
 Aufruf: cscript /Nologo Erfassungsliste.js < Erfassungsliste.csv > Erfassungsliste4.csv
 Wenn als Unicode-Text gespeichert (in Wahrheit Tab-separierte Werte in UTF-16):
 Aufruf: cscript /u /nologo Erfassungsliste.js < Erfassungsliste.txt > Erfassungsliste4.csv
 220720	erstellt
+220725	Tab statt Semikolon als weiterer Feldtrenner, für Excels Export-als-Text
+220725	3 neue Tabellenspalten: Typ, Messplatz,Fremdfirma, generiert aus Zwischenüberschriften
-220726	System-Listentrennzeichen (besserer Begriff: Feld- oder Zelltrenner) statt hartkodiertes ";" verwenden
	Einfach beide Feldtrenner ("," und ";") zu akzeptieren ist nicht sicher,
	weil Excel das jeweils andere Zeichen nicht escaped; man müsste eine umständliche
	Häufigkeits- oder Plausibilitätsanalyse machen
	Überraschenderweise escaped Excel den Feldtrenner (';') auch beim „Textexport“ = TSV-Export.
	Was Excel nicht tun /müsste/. Sowie den Tabulator beim CSV-Export, ebenfalls unlogisch.
	Hier ist dieses Verhalten von Vorteil, weil '\t' und ';' gleich behandelt werden können.
+220726	OneSAP-Nummer aus „sap.tsv“ in linke Spalte mergen
+220727	Variable Spaltenzuordnung beim  Erkennen der Zeilen
+220727	Titelzeile ohne Schlusspunkte zur Ladbarkeit in MS Access
+220730	Offensichtlich verrutschte Folgezeileneinträge in vorhergehende Zeile mergen
*/
var forExcel=true;

// Feldtrennzeichen von Registry lesen
var sh = new ActiveXObject("WScript.Shell");
var FS = sh.RegRead("HKEY_CURRENT_USER\\Control Panel\\International\\sList")||',';
var sDecimal = sh.RegRead("HKEY_CURRENT_USER\\Control Panel\\International\\sDecimal")||'.';	// Punkt oder Komma?
// Feldtrenner wie in awk, Semikolon bei deutschem Windows, Komma bei englischem Windows

//var ExcelApp = new ActiveXObject("Excel.Application");
//var ExcelSheet = new ActiveXObject("Excel.Sheet");
//ExcelSheet.Application.Visible = true;
//WScript.StdErr.WriteLine(ExcelSheet.ActiveSheet.Cells(1,1).Value);
//ExcelSheet.Application.Quit();

// Dateinamen von Kommandozeile lesen
var argv = WScript.Arguments.Unnamed;	// Arrayzugriff mit runden(!!) Klammern
var txt, stdout = WScript.StdOut;
var fso = new ActiveXObject("Scripting.FileSystemObject");
if (argv.length>=1) {	// Inputfilename
 WScript.StdErr.WriteLine("Eingabedatei: "+argv(0));
 var f = fso.OpenTextFile(argv(0),1,false,-1);	// -1 == TristateUseDefault, scheint die BOM zu prüfen
 txt = f.ReadAll();
 f.close();
 if (argv.length>=2) {	// Outputfilename
  stdout = fso.OpenTextFile(argv(1),2,true,-1);	// -1 schreibt UTF-16 mit BOM
 }
}else txt = WScript.StdIn.ReadAll();

// Zuordnungsliste OneSAP-Nummer ↔ Inventarnummer laden
// Diese zweispaltige TSV-Tabelle kann recht einfach aus den Mockup-Listen extrahiert werden
// und ist in ASCII (nicht in Unicode, ohne Umlaute, ohne Spaltentitel) vorzuhalten.
// Ist keine derartige Datei vorhanden, wird keine Spalte links in den Ausgabedaten hinzugefügt.
// TODO: Alle Dateien (*.sap.*) laden, Eineindeutigkeit prüfen und verwenden, falls erforderlich.
var filename="sap.tsv";
var onesap = fso.OpenTextFile(filename,1,false);
if (onesap) {
 var map={size:0};	// JScript3 hat kein eingebautes „new Map()“, Behelf über Objekt = assoziatives Array
 while (!onesap.AtEndOfStream) {
  var z=onesap.ReadLine().split('\t',2);	// links: OneSAP-Nummer, rechts: Inventarnummer
  if (map[z[1]]) {
   WScript.StdErr.WriteLine("Fehler in "+onesap.Name+": Doppelte Inventarnummer bei Zeile "+map[z[1]].line+" und "+onesap.Line+"!");
  }else{
   map[z[1]]={sap:z[0],file:filename,line:onesap.Line-1};
   map.size++;		// "size" kommt nicht als Inventarnummer vor
  }
 }
 onesap.close();
 onesap=map;
 WScript.StdErr.WriteLine("OneSAP-Nummern werden eingefügt: Vorgabe hat "+map.size+" Einträge.");
}
var len = txt.length;	// Anzahl Zeichen = Bytes
var quote='', eat='', col=0, field="";
var a=[], b;	// a = Zeilenvektor, b = (aktueller) Spaltenvektor
//var start=0, step=1;
//if (len>=2 && !(len&1) && txt.charCodeAt(0)==0xFF && txt.charCodeAt(1)==0xFE) {
// ReadAll macht die UTF-Erkennung nicht selbst. JScript verwendet intern ohnehin UTF-16 für seine Strings.
// Es sieht so aus, als ob JScript mit Nullen Probleme hat!
// WScript.StdErr.WriteLine("Dateiformat: UTF-16LE");
// start=2; step=2;
//}
WScript.StdErr.Write("Gelesen: "+len+" Zeichen, jetzt läuft der Parser ...");

for (var i=0; i<len; i++) {
 var c=txt.charAt(i);	// Möglicherweise sind reguläre Ausdrücke schneller, aber einzelzeichenweise geht's auch noch akzeptabel schnell: ca. 1000 Datenzeilen pro Sekunde
 if (c==eat) {eat=''; continue;}
 eat='';
 if (quote) {		// Wenn innerhalb „quoted string“
  if (c==quote) {	// Mögliches Ende
   if (i+1<len && txt[i+1]==quote) {	// doppelt = einfach literal (derzeit kein Testfall)
    ++i;
    field+=c;
   }else{		// einfach = Feldende
    quote='';
   }
  }else{
   field+=c;		// kann auch FS und '\n' und '\t' sein!
  }
 }else if (c=='\r' || c=='\n' || c==FS || c=='\t') {	// Zeilenende oder Feldende?
  field=field.replace(/^\s+|\s+$/g,"");	// Vor- und nachlaufenden Weißraum tilgen
  if (field) {		// Wenn dann noch etwas übrig geblieben ist …
   if (!b) b=[];	// Neues Zeilenarray erzeugen, wenn noch nichts gespeichert
   b[col]=field;	// JScript füllt nichtzusammenhängende Indizes mit undefined auf und aktualisiert b.length
  }
  field="";
  if (c=='\r' || c=='\n') {
   if (c=='\r') eat='\n';	// dieses Zeichen beim nächsten getchar() wegignorieren
   else if (c=='\n') eat='\r';
   a.push(b);		// Referenz kopieren = Array retten
   col=0;
   b=undefined;		// Diese Referenz tilgen
  }else ++col;
 }else if (c=='"') {
  quote=c;		// Flag = Endezeichen setzen und nicht übernehmen
 }else{
  field+=c;		// Jedes andere Zeichen literal nehmen
 }
}
if (b) WScript.StdErr.WriteLine("Am Ende der Daten fehlt Zeilenschaltung!");
WScript.StdErr.WriteLine(" fertig, Zeilenzahl: "+a.length);

function countArray(b) {	// zählt Nicht-Null-Elemente eines Arrays, hier: nicht-leere Strings
 var c = 0;
 for (var i=0; i<b.length; i++) if (b[i]) ++c;
 return c;
}
function firstIndex(b) {	// Index des ersten Nicht-Null-Elements eines Arrays, -1 wenn keins
 for (var i=0; i<b.length; i++) if (b[i]) return i;
 return -1;
}
// Filtern: Leere Spalten der Datenzeilen auffinden und Eineindeutigkeit der Inventarnummer prüfen
function isKopfzeile(b) {
 if (countArray(b)<4) return false;
 return b[firstIndex(b)].match(/^Invent/i);	// Erste nicht-leere Spalte enthält "Inventar" oder "inventory" oder so
}
// WAS eine Datenzeile ist lässt sich nicht ganz sicher finden!
// Davon ausgehend, dass die Kopfzeile bereits detektiert wurde, eine Zeile mit mindestens 4 besetzten Spalten.
var Extraspalten={"Typ":undefined,"Messplatz":undefined,"Fremdfirma":undefined,Datenzeilen:0};
function isDatenzeile(b) {
 var ret = countArray(b)>=4;
 if (ret) ++Extraspalten.Datenzeilen;	// wird benötigt um das Umschalten von Datenzeilen auf Zwischenüberschriften zu erkennen
 return ret;
}
// Eine Zwischenüberschrift sei etwas mit genau einer gefüllten Spalte
function isZwischenüberschrift(b) {
 var ret = countArray(b) == 1;
 if (ret && Extraspalten.Datenzeilen) {
  Extraspalten.Typ=undefined;
  Extraspalten.Messplatz=undefined;
  Extraspalten.Fremdfirma=undefined;
  Extraspalten.Datenzeilen=0;
 }
 return ret;
}
function findOneSap(invnr) {
}

var invnr={};		// Normalerweise Map, aber das gibt's nicht bei JS3. Hier wird die Zeilennumer der Definition zugewiesen.
var spaltenr=[];	// Zähler für Spalteneinträge
var okay=true;
for (var i=0; i<a.length; i++) {
 var b=a[i];
 if (b && !isKopfzeile(b) && isDatenzeile(b)) {
  for (var j=0; j<b.length; j++) if (b[j]) spaltenr[j] = (spaltenr[j]||0)+1;	// Einträge zählen (true zuweisen würde es auch tun)
  var k=b[firstIndex(b)];
  if (invnr[k]) {
   WScript.StdErr.WriteLine("Mehrfache Inventarnummer "+k+" aus Zeile "+invnr[k]+" in Zeile "+(i+1)+"!");
   okay=false;
  }else invnr[k]=i+1;	// Zeilennummer für Fehlermeldung zuweisen
 }
}
if (okay) WScript.StdErr.WriteLine("Okay: Keine mehrfachen Inventarnummern gefunden.");
WScript.StdErr.WriteLine("Spalten-Nutzung der (vermeintlichen) Datenzeilen:");
for (var i=0; i<spaltenr.length; i++) {
 WScript.StdErr.Write((spaltenr[i]||0)+" ");
}
WScript.StdErr.WriteLine();

// Aufeinanderfolgende Zeilen auf verrutschte Elemente prüfen und ggf. auf eine Zeile bringen
for (var i=0; i<a.length-1; i++) {
 var b=a[i], bb=a[i+1];
 if (b && bb) {	// Zwei aufeinanderfolgende, nicht-leere Zeilen?
  var l=Math.max(b.length,bb.length), candidate=true;
  for (j=0; j<l; j++) if (b[j] && bb[j]) {candidate=false; break;}	// Komplett disjunkte Spalten?
  if (candidate) {
   for (j=0; j<l; j++) if (bb[j]) b[j]=bb[j];	// Folgespaltenelemente in aktuelle übertragen
   a[++i]=undefined;	// Folgezeile löschen
  }
 }
}

// Datenzeilen zusammenschieben. Danach sind Datenzeilen leider nicht mehr als solche erkennbar.
var killed=0;
for (var i=0; i<a.length; i++) {
 var b=a[i];
 if (b) {
  if (isKopfzeile(b)) {	// Tabellenkopf zusammenschieben und aufhübschen
   var c=[];
   if (onesap) c.push("OneSAP");
   for (var j=0; j<b.length; j++) if (b[j]) c.push(b[j].replace(/\s+/g," ").replace(/- /g,"-").replace(/-(?=[a-z])/g,""));	// Nicht-leere Spalten kopieren
	// Das zweite replace() verbindet "Inventar- Nr." zu "Inventar-Nr.", das dritte "Prüf-turn." zu "Prüfturn.".
	// Könnte man auch global für die Datenzeilen machen.
   Extraspalten.SpalteStart=onesap?1:0;
   for (var j=0; j<spaltenr.length; j++) if (spaltenr[j]) ++Extraspalten.SpalteStart;
   if (c.length==Extraspalten.SpalteStart) WScript.StdErr.WriteLine("Okay: Kopfzeile hat gleiche Spaltenzahl ("+c.length+") wie Datenzeilen");
   else WScript.StdErr.WriteLine("Problem: Kopfzeile hat Spaltenzahl "+c.length+", Datenzeilen Spaltenzahl "+Extraspalten.SpalteStart+"!");
   if (Extraspalten.SpalteStart<c.length) Extraspalten.SpalteStart=c.length;	// Maximum beider
   c[Extraspalten.SpalteStart]="Typ";
   c[Extraspalten.SpalteStart+1]="Messplatz";
   c[Extraspalten.SpalteStart+2]="Fremdfirma";
   a[i]=c;	// Zeilenvektor ersetzen
  }else if (isDatenzeile(b)) {	// Datenzeile zusammenschieben
   var c=[];
   if (onesap) {
    var k=b[firstIndex(b)];
    var entry=onesap[k];	// Map-Zugriff
    if (entry) {
     c.push(entry.sap);
     entry.used=i+1;		// Zeilennummer vermerken
    }else c.push(undefined);
   }
   for (var j=0; j<b.length; j++) 
     if (spaltenr[j])
     c.push(b[j]?b[j].replace(/\s+/g," "):undefined);	// Nicht-leere Spalten kopieren, dabei Mehrfachleerzeichen tilgen, '\n' durch ' ' ersetzen
   c[Extraspalten.SpalteStart]=Extraspalten["Typ"];
   c[Extraspalten.SpalteStart+1]=Extraspalten["Messplatz"];
   c[Extraspalten.SpalteStart+2]=Extraspalten["Fremdfirma"];
   a[i]=c;	// Zeilenvektor ersetzen
  }else if (isZwischenüberschrift(b)) {
   var ü = b[firstIndex(b)];
   if (ü.match(/^Messplatz/)) Extraspalten["Messplatz"]=ü.replace(/^Messplatz\s?:?\s*/,"");
   else if (ü.match(/^Fremdfirma/)) Extraspalten["Fremdfirma"]=ü.replace(/^Fremdfirma\s?:?\s*/,"");
   else{
    if (Extraspalten["Typ"]) {
     WScript.StdErr.WriteLine("Zwischenüberschrift ohne Datenzeile in Zeile "+(i+1)+", vorherige Definition „"+Extraspalten["Typ"]+"“ verfällt!");
     killed++;
    }
    Extraspalten["Typ"]=ü;
   }
   a[i]=undefined;
  }else{
   WScript.StdErr.WriteLine("Zeile "+(i+1)+" gekillt, 1. Spalte = "+a[i][0]);
   a[i]=undefined;
   killed++;
  }
 }
}
if (Extraspalten["Typ"] && !Extraspalten.Datenzeilen) {
 WScript.StdErr.WriteLine("Zwischenüberschrift ohne Datenzeile am Ende, Definition „"+Extraspalten["Typ"]+"“ verfällt!");
 killed++;
}
if (killed) WScript.StdErr.WriteLine(killed+" Zeilen gekillt.");
// Ungenutzte SAP-Listeneinträge zählen
if (onesap) {
 var unusedSap=0;
 for (var e in onesap) if (e!="size" && !onesap[e].used) unusedSap++;
 if (unusedSap) {
  WScript.StdErr.WriteLine("Von der OneSAP-Zuordnungsliste "+(unusedSap==1?"wurde "+unusedSap+" Eintrag":"wurden "+unusedSap+" Einträge")+" nicht genutzt!"
	+(unusedSap<=10?" Liste:":""));
// Liste der ungenutzten Einträge ausgeben, wenn <=10. Im MSC-Stil (Zeilennummern in Klammern), GNU wäre Zeilennummer nach Doppelpunkt
// Die Ausgabe erfolgt unsortiert, je nach innerer Verarbeitungslogik von JScript.
  if (unusedSap<=10) for (var e in onesap) if (e!="size" && !onesap[e].used) {
   WScript.StdErr.WriteLine("\t"+onesap[e].file+"("+onesap[e].line+"): "+onesap[e].sap+"\t"+e);
  }
 }
}

// Ausgeben
WScript.StdErr.WriteLine("Ausgabedatei wird geschrieben ...");
var zahl=new RegExp("^-?\\d+("+sDecimal+"\\d+)?(e-?\\d+)?$","i");
for (var Kopf=true, i=0; i<a.length; i++) {
 var b=a[i];
 if (b) {	// Gekillte und Leerzeilen nicht ausgeben = kompakte Liste
  for (var j=0;;) {
   var k=b[j];
   if (k) {
    k=k.replace(/"/g,'""');	// Quotes verdoppeln (derzeit kein Testfall)
    if (forExcel) {
     k='"'+(k.match(zahl)?'\t':'')+k+'"';	// Mit dem Tabulator bei Ziffernfolge String-Import erzwingen
    }else{	// for Access
     if (Kopf) k=k.replace(/\.$/,'');	// Schlusspunkt am Spaltentitel entfernen
     k='"'+k+'"';
    }
    stdout.Write(k);
   }
   if (++j>=b.length) break;
   stdout.Write(FS);
  }
  stdout.WriteLine();
  Kopf=false;
 }
}
Detected encoding: ASCII (7 bit)9