Files
boc/sie-import/parser.mjs
T

198 lines
4.8 KiB
JavaScript
Raw Normal View History

/**
* SIE4 Parser
* Parses SIE4 accounting file format into structured objects.
*/
/**
* Tokenize a SIE line remainder (after the directive keyword).
* Handles:
* - Quoted strings "like this"
* - Object lists {key val ...} → replaced with placeholder '{}'
* - Regular whitespace-separated tokens
*/
function tokenize(str) {
const tokens = [];
let i = 0;
const s = str.trim();
while (i < s.length) {
// Skip whitespace
if (s[i] === ' ' || s[i] === '\t') { i++; continue; }
// Quoted string
if (s[i] === '"') {
let j = i + 1;
while (j < s.length && s[j] !== '"') j++;
tokens.push(s.slice(i + 1, j));
i = j + 1;
continue;
}
// Object list { ... } — treat whole block as one placeholder token
if (s[i] === '{') {
let depth = 0;
let j = i;
while (j < s.length) {
if (s[j] === '{') depth++;
else if (s[j] === '}') { depth--; if (depth === 0) break; }
j++;
}
tokens.push('{}');
i = j + 1;
continue;
}
// Regular token
let j = i;
while (j < s.length && s[j] !== ' ' && s[j] !== '\t' && s[j] !== '"') j++;
if (j > i) tokens.push(s.slice(i, j));
i = j;
}
return tokens;
}
/**
* Parse SIE4 file content (string) into a structured object.
* @param {string} content Raw SIE4 text (UTF-8 or already decoded)
* @returns {object}
*/
export function parseSIE4(content) {
const lines = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
const result = {
company: '',
org_nr: '',
fiscal_year: null,
accounts: [],
opening_balances: [],
closing_balances: [],
journal_entries: [],
};
let currentVer = null;
let inVerBlock = false;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
// Block delimiters
if (line === '{') {
inVerBlock = true;
continue;
}
if (line === '}') {
if (currentVer) {
result.journal_entries.push(currentVer);
currentVer = null;
}
inVerBlock = false;
continue;
}
if (!line.startsWith('#')) continue;
// Split directive from rest
const spaceIdx = line.indexOf(' ');
const directive = (spaceIdx >= 0 ? line.slice(0, spaceIdx) : line).toUpperCase();
const rest = spaceIdx >= 0 ? line.slice(spaceIdx + 1) : '';
const tokens = tokenize(rest);
switch (directive) {
case '#FNAMN':
result.company = tokens[0] || '';
break;
case '#ORGNR':
result.org_nr = tokens[0] || '';
break;
case '#RAR': {
// #RAR year_offset from_date to_date
const yearOffset = parseInt(tokens[0], 10);
if (yearOffset === 0) {
result.fiscal_year = {
from: tokens[1] || '',
to: tokens[2] || '',
};
}
break;
}
case '#KONTO':
// #KONTO account_nr account_name
result.accounts.push({
number: tokens[0] || '',
name: tokens[1] || '',
});
break;
case '#IB':
// #IB year_offset account amount [quantity]
result.opening_balances.push({
year_offset: parseInt(tokens[0], 10) || 0,
account: tokens[1] || '',
amount: parseFloat(tokens[2]) || 0,
});
break;
case '#UB':
// #UB year_offset account amount [quantity]
result.closing_balances.push({
year_offset: parseInt(tokens[0], 10) || 0,
account: tokens[1] || '',
amount: parseFloat(tokens[2]) || 0,
});
break;
case '#VER':
// #VER series number date "description" [reg_date] [reg_by]
currentVer = {
series: tokens[0] || '',
number: tokens[1] || '',
date: tokens[2] || '',
description: tokens[3] || '',
transactions: [],
};
break;
case '#TRANS':
// #TRANS account {object_list} amount [date] ["description"] [quantity]
// tokens: [account, {}, amount, date?, description?]
if (currentVer) {
currentVer.transactions.push({
account: tokens[0] || '',
amount: parseFloat(tokens[2]) || 0,
date: tokens[3] || currentVer.date,
description: tokens[4] || '',
});
}
break;
// Ignored directives (metadata only)
case '#FLAGGA':
case '#PROGRAM':
case '#FORMAT':
case '#GEN':
case '#SIETYP':
case '#VALUTA':
case '#TAXAR':
case '#OMFATTN':
case '#KPTYP':
case '#ENHET':
break;
default:
break;
}
}
// If file ended without closing brace for last VER (malformed), still capture it
if (currentVer) {
result.journal_entries.push(currentVer);
}
return result;
}