Skip to content

Commit

Permalink
Support for account transaction CSV export as input
Browse files Browse the repository at this point in the history
  • Loading branch information
martinadamek committed Jan 6, 2024
1 parent 01cc3ba commit 54c925a
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 7 deletions.
95 changes: 95 additions & 0 deletions docs/csv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Account transactions CSV export format parsing

/* eslint-disable import/extensions */

import { stripCommasAndSpaces } from './util.js';

export function csv2csv(inputCSV) {
// Split the input CSV into lines
const lines = inputCSV.split('\n');
lines.shift();

// Process the CSV data
const processedLines = lines.map(line => {
const row = []
const parts = csvToArray(line);

if (parts.length > 0) {
const infoColumnParts = parts[13].split(' ');
let payee = null;
let memo = getCard(infoColumnParts);
if (memo !== null) {
payee = getPayee(infoColumnParts);
} else {
payee = parts[8];
memo = parts[13] + " | " + parts[12];
}

row.push(parts[1]);
row.push(stripCommasAndSpaces(payee));
row.push(stripCommasAndSpaces(memo));
row.push(parts[2].replace(/,/g, '.'));
}

return row;
});

// Convert the processed lines back to a CSV string
const processedCSV = processedLines.map(row => row.join(',')).join('\n');
return 'Date,Payee,Memo,Amount\n' + processedCSV;
}

function getCard(parts) {
if (isValidCardFormat(parts[0])) {
return parts[0];
}
return null;
}

function getPayee(parts) {
const payee = []
for (let i = parts.length - 1; i >= 0; i--) {
if (isAmountWithCurrency(parts[i]) || isTime(parts[i])) {
return payee.join(' ');
}
payee.unshift(parts[i]);
}
return null;
}

function isValidCardFormat(str) {
// Regex to match the pattern: starting with 6 digits, followed by 6 asterisks, and ending with 4 digits
const regex = /^\d{6}\*{6}\d{4}$/;
return regex.test(str);
}

function isAmountWithCurrency(str) {
const regex = /^\d+(\.\d{1,2})?[A-Z]{3}$/;
return regex.test(str);
}

function isTime(str) {
const regex = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/;
return regex.test(str);
}

// https://stackoverflow.com/a/8497474
function csvToArray(text) {
var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/;
var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
// Return NULL if input string is not well formed CSV string.
if (!re_valid.test(text)) return null;
var a = []; // Initialize array to receive values.
text.replace(re_value, // "Walk" the string using replace with callback.
function(m0, m1, m2, m3) {
// Remove backslash from \' in single quoted values.
if (m1 !== undefined) a.push(m1.replace(/\\'/g, "'"));
// Remove backslash from \" in double quoted values.
else if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"'));
else if (m3 !== undefined) a.push(m3);
return ''; // Return empty string.
});
// Handle special case of empty last value.
if (/,\s*$/.test(text)) a.push('');
return a;
};
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ <h1>Tatra banka + YNAB</h1>
Konverzia výpisu z <a href="https://www.tatrabanka.sk">Tatra banky</a> pre import do <a href="https://www.youneedabudget.com">YNAB</a>.<br>
Prebieha na vašom počítači. Na server <a href="https://github.com/qsmd/tb2ynab">nič neposiela</a>.
</p>
<label for="file-input" class="button button-large">Nahraj XML (camt.053 SK)</label>
<input type="file" id="file-input" style="display:none" accept="text/xml" />
<label for="file-input" class="button button-large">Nahraj XML/CSV</label>
<input type="file" id="file-input" style="display:none" accept="text/xml, text/csv" />
<p>CSV bude stiahnuté automaticky.</p>
</div>
</div>
Expand Down
25 changes: 20 additions & 5 deletions docs/tb2ynab.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,33 @@

// https://medium.com/@mattlag/es6-modules-getting-started-gotchas-2ad154f38e2e
import { xml2csv } from './camt053sk.js';
import { csv2csv } from './csv.js';

// FIXME poor man's error handling
function error(text) {
// eslint-disable-next-line no-console
console.error(text);
}

function parse(xmlText, fileName) {
const csv = xml2csv(xmlText);
function parse(text, file) {
let fileName;
let csv;
if (file.type == "text/xml") {
fileName = file.name.replace('.xml', '.ynab.csv');
csv = xml2csv(text)
} else if (file.type == "text/csv") {
fileName = file.name.replace('.csv', '.ynab.csv');
csv = csv2csv(text);
} else {
error('Incorrect file type');
return;
}

// https://code-maven.com/create-and-download-csv-with-javascript
const hiddenElement = document.createElement('a');
hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURI(csv)}`;
hiddenElement.target = '_blank';
hiddenElement.download = fileName.replace('.xml', '.csv');
hiddenElement.download = fileName;
hiddenElement.click();
}

Expand All @@ -27,12 +39,15 @@ function fileListener() {
if (allFiles.length === 0) { error('No file selected'); return; }

const file = allFiles[0];
if (file.type !== 'text/xml') { error('Incorrect file type'); return; }
if (file.type !== 'text/xml' && file.type !== 'text/csv' ) {
error('Incorrect file type');
return;
}
if (file.size > 2 * 1024 * 1024) { error('Exceeded size 2MB'); return; }

const reader = new FileReader();
reader.addEventListener('load', (e) => {
parse(e.target.result, file.name);
parse(e.target.result, file);
});
reader.addEventListener('error', () => { error('Failed to read file'); });
reader.readAsText(file);
Expand Down

0 comments on commit 54c925a

Please sign in to comment.