/** * 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; }