import { addLineNumbers, chunks, fillWithEmpty, removeRepeatingSpace, } from "./utils"; class Account { constructor( public number: string, public initial_balance: number, public closing_balance: number, public currency: string ) {} } class Transaction { constructor( public acc_date: Date, public op_date: Date, public description: string, public title: string, public person: string, public account_number: string, public amount: number, public balance_after: number ) {} public static counter = (function* () { let c = 1; while (true) { yield c.toString().padStart(11, "0"); c++; } })(); formatTitle() { return addLineNumbers( fillWithEmpty(chunks(this.title.trim(), 27), 9), 20 ).join("\n"); } formatPerson() { return addLineNumbers( fillWithEmpty( chunks(removeRepeatingSpace(this.person).trim(), 27), 2 ).slice(0, 2), 32 ).join("\n"); } toMT940() { const prefixes = { incoming: "150", outcoming: "169" }; const prefix = this.amount > 0 ? prefixes.incoming : prefixes.outcoming; return `:61:${mtDate(this.acc_date)}${mtDate(this.op_date).slice( 2 )}C${mtAmount(this.amount)}S${prefix}${Transaction.counter.next().value} :86:${prefix} :86:${prefix}~00B${prefix}${this.description.slice(0, 23)} ${this.formatTitle()} ~29${this.account_number} ~30${this.account_number.slice(2, 10)} ~31${this.account_number.slice(10)} ${this.formatPerson()} ~34${prefix} ~38PL${this.account_number} ~62 ~63`; } } class Range { constructor(public date_start: Date, public date_end: Date) {} } abstract class CSVParser { abstract read_csv(content_utf8: string): { account: Account; range: Range; transactions: Transaction[]; }; } class mBankParser extends CSVParser { read_csv(content_utf8: string): { account: Account; range: Range; transactions: Transaction[]; } { const lines = content_utf8.split("\r\n").map((line) => line.split(";")); let last = 0; while (!lines[last][0].startsWith("Niniejszy dokument sporzÄ…dzono")) { last++; } const account = new Account( lines[20][0], this.parseAmount(lines[35][1]), this.parseAmount(lines[last - 2][7]), lines[18][0] ); const range = new Range( this.parseDate(lines[14][0]), this.parseDate(lines[14][1]) ); const transactions = []; for (let i = 38; i <= last - 5; i++) { const line = lines[i]; if (line.length != 9) { throw new Error( "Wrong amount of columns! maybe a semicolon got stuck in a transaction description?" ); } const date_acc = new Date(line[0]); const date_op = new Date(line[1]); const [description, title, person, account_number] = line .slice(2) .map(this.trimString); const [amount, balance_after] = line .slice(6) .map((s) => this.parseAmount(s)); transactions.push( new Transaction( date_acc, date_op, description, title, person, account_number, amount, balance_after ) ); } return { account, range, transactions }; } parseAmount(s: string): number { return parseFloat(s.replace(/[^0-9,-]/g, "").replace(",", ".")); } parseDate(s: string): Date { const [day, month, year] = s.split(".").map((s) => parseInt(s)); const d = new Date(); d.setHours(0); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); d.setDate(day); d.setMonth(month - 1); d.setFullYear(year); return d; } trimString(s: string): string { //removes redundant quotes at the beginning or end of the transaction return s.replaceAll(/(^["']|["']$)/g, ""); } } function mtDate(d: Date) { return `${d.getFullYear() - 2000}${(d.getMonth() + 1) .toString() .padStart(2, "0")}${d.getDate().toString().padStart(2, "0")}`; } function mtAmount(n: number) { return Math.abs(n).toString().replace(".", ","); } export function convert(csv_utf8: string): { output: string; range: Range } { const result = new mBankParser().read_csv(csv_utf8); const { account, transactions, range } = result; const string = `:20:MT940 :25:/PL${account.number.replaceAll(/[^0-9]/g, "")} :28C:${mtDate(range.date_start)} :60F:D${mtDate(range.date_start)}${account.currency}${mtAmount( account.initial_balance )} ${transactions.map((t) => t.toMT940()).join("\n")} :62F:D${mtDate(range.date_end)}${account.currency}${mtAmount( account.closing_balance )}`; return { output: string, range }; }