Transforming CSV

Transforming CSV

CSV Mapping mit node.js

Node.js ist ein mächtiges Tool um strukturierte Daten von einer Form in die andere zu transferieren.
Anhand eines aktuellen Beispieles möchte ich dies hier einmal demonstrieren. Das Ziel ist, die Kontoauszüge einer P2P Krediteplatform in ein Format zu wandeln, welches von der Banking-App MoneyMoney verstanden werden kann.

Das Problem

Mintos (affiliate Link) ist eine der ältesten P2P-Kredit Platformen und liefert aktuell immer noch eine der besten Renditen. Ein großer Nachteil dieser Platform ist allerding die fehlende API.

Zum Nachhalten meiner Finanzen verwende ich schon seit Jahren die Mac Desktop-App MoneyMoney. Die App zeichnet sich durch einen stabilen Betrieb, einen guten Kundensupport und – last, but not least – eine umfangreiche Plugin-Sammlung aus.

Mein Ziel war es also, die Abrechnungen von Mintos in MoneyMoney zu umportieren. Neben dem Abrufen der Daten über eine API und dem Import via Plugin, bietet MoneyMoney auch den Import von Daten über eine CSV Datei an.

Mintos auf der anderen Seite erlaubt einen Export der Datensätze über das Web-UI. Leider sind beide Format nicht ganze passend. So muss vor jedem CSV Import, die CSV Struktur angepasst werden.

Mintos-Export:

Datum,Transaktions-Nr.:,Einzelheiten,Umsatz,Saldo,Währung,Zahlungsart
"2023-01-05 09:54:48",49904316-978-12-3bad9ff01f0889407b38baf825e75406-63b682483cc8d,"Darlehen Nr. 46413771-01 Hauptbetrag",0.14583741585112,0.45632686514,EUR,Tilgungszahlungen
"2023-01-05 09:54:48",49904316-978-12-426043ba191a5c35ac733482cafc47f7-63b682483cbb7,"Darlehen Nr. 46413771-01 Hauptbetrag",2.76978362379E-5,0.45635456297623,EUR,Tilgungszahlungen

Von MoneyMoney erwartetes Format:

Datum;Wertstellung;Kategorie;Name;Verwendungszweck;Konto;Bank;Betrag;Währung
05.01.2023;05.01.2023;Anlagen YTN - Mintos;49904316-978-12-426043ba191a5c35ac733482cafc47f7-63b682483cbb7;Darlehen Nr. 46413771-01 Hauptbetrag;;Mintos;0,00;EUR
05.01.2023;05.01.2023;Anlagen YTN - Mintos;49904316-978-12-3bad9ff01f0889407b38baf825e75406-63b682483cc8d;Darlehen Nr. 46413771-01 Hauptbetrag;;Mintos;0,15;EUR

Wir erstellen uns also ein kleines node.js script, welches die Daten aus dem Mintos CSV einliest, entsprechend umformatiert und mit der richtigen MoneyMoney Kategorie versieht und geben das ganze als neue CSV Datei aus.

Das Setup

Da wir CSV lesen und schreiben wollen, sind dies unsere einzigen Abhängigkeiten. Die node typische package.json sieht wie folgt aus:

{
  "name": "mintos-parser",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "csv-parser": "^3.0.0",
    "fast-csv": "^4.3.6"
  }
}

Datenverarbeitung

Schauen wir uns nun den groben Ablauf an.
Das script erwartet die Eingangsdaten in der Datei data.csv – das lässt sich ggf. auch über einen CLI Parameter steuern, aktuell nenne ich die Datei einfach immer passen um.

const result = [];

fs.createReadStream("./data.csv")
    .pipe(csvParser())
    .on("data", (data) => {
        // hier werden die Daten umgewandelt und in das result array geschrieben
    })
    .on("end", () => {
        // Hier wird das array in die neue CSV Datei geschrieben.
        writeCSV(result.reverse());
    });

Der csvParser liest die CSV Zeile für Zeile aus und liefert uns jeweils im data Event eine key->value Struktur, welche wir passend umwandeln und in ein Array schreiben.
Beim end Event ist die Input Datei zu Ende und wir speichern das Array wieder in eine CSV Datei. writeCSV sieht wie folgt aus:

const ws = fs.createWriteStream("out.csv");

const writeCSV = (data) => {
    fastcsv
        .write(data, {
            headers: ["Datum", "Wertstellung", "Kategorie", "Name", "Verwendungszweck", "Konto", "Bank", "Betrag", "Währung"],
            quotes: true,
            delimiter: ';',
        })
        .pipe(ws);
}

Kommen wir nun zum interessantern Teil, in dem wir die Eingabedaten parsen und umwandeln:

// e.g. 'Mintos' or e.g. 'Anlagen YTN - Mintos' for an hierarchy
const category = "Mintos";

const incomingPaymentMarker = "Eingehende Zahlungen vom Bankkonto";

…
.on("data", (data) => {
        Object.keys(data)
        .forEach(key => {
            if (key === "Transaktions-Nr.:") {
                data["Name"] = data[key];
            }
            if (key === "Einzelheiten") {
                data["Verwendungszweck"] = data[key];
            }
            if (key === "Umsatz") {
                var v = parseFloat(data[key]);
                console.log(data[key], "=>", v);
                data["Betrag"] = numberFormat(v);
            }
            if (key === "Datum") {
                if(data[key]) {
                    var d = new Date(data[key]);
                    data["Datum"] = dateFormat(d);
                    data["Wertstellung"] = dateFormat(d);
                }
            }
        });
        data["Bank"] = "Mintos";
        if (data["Verwendungszweck"] === incomingPaymentMarker) {
            data["Kategorie"] = "Umbuchung";
        } else {
            data["Kategorie"] = category;
        }
        if(data["Datum"]) {
            result.push(data);
        }
    })
…

Hier passieren mehrere Dinge:
Zum einen werden die unterschiedlichen Werte je nach Key umkopiert (z.B. „Einzelheiten“ zu „Verwendungszweck“). Wir iterieren jeweils über die einzelnen Keys des data Objects, um unsere key->value pairs zu bekommen. Da die Werte jeweils nur Strings sind, müssen wir für die Daten und die Beträge die Values noch einmal in den passenden Typ parsen und für MoneyMoney formatieren. Das passiert über die beiden Funktionen dateFormat und numberFormat:

const numberFormat =
    newIntl.NumberFormat("de-DE",
    {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
    }).format;

const dateFormat =
    newIntl.DateTimeFormat("de-DE", 
    { day: "2-digit", month: "2-digit", year: "numeric" }).format;

Wir möchten die einzelnen Einträge jeweils einer der beiden Kategorien („Umbuchung“ oder „Mintos“) zuordnen.
„Umbuchung“ sind jeweils Beträge, die von einem anderen Konto abgegangen sind. Diese lassen sich anhand des incomingPaymentMarker erkennen.

Fazit

Mit 70 Zeilen Code lässt sich ein einfaches Mapping durchführen.
Bei größeren Dateien, lässt sich das Schreiben in die Ausgabe-Datei auch jeweils im data Event durchführen, das wäre dann auch für die Speicherverbrauch zuträglicher.

Die kompletten Sourcen sind in diesem Github Repository zu finden.

Philipp Haußleiter

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert