@ekwo-ai/trial-balance 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karuna Co OÜ, trading as Ekwo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # `@ekwo-ai/trial-balance`
2
+
3
+ Reads a **trial balance written as CSV** — one row per account, a debit and a
4
+ credit, or one signed balance — into plain objects, in TypeScript, with no
5
+ dependencies.
6
+
7
+ It is the one thing every accounting package prints and every spreadsheet
8
+ saves, which makes it the way in for books that come from anywhere: the
9
+ balances of the day the new books open on.
10
+
11
+ ```ts
12
+ import { readTrialBalance } from '@ekwo-ai/trial-balance';
13
+
14
+ const books = readTrialBalance(bytesOrString);
15
+
16
+ books.opening; // [{ account: '512000', debit: '8400.00', credit: '0.00', contact: null, … }, …]
17
+ books.accounts; // [{ code: '512000', name: 'Bank', type: null }, …]
18
+ books.contacts; // the parties a receivable or a payable row names
19
+ books.violations; // [{ rule: 'unbalanced', message: '…', row: null }] when it does not balance
20
+ ```
21
+
22
+ ## The file
23
+
24
+ A header row, then one row per account. The columns are found by their name,
25
+ without regard to case, and without the `*` some exports put on a required
26
+ column:
27
+
28
+ | Column | Names accepted | |
29
+ |---|---|---|
30
+ | account | `account`, `account code`, `code` | required |
31
+ | name | `name`, `account name` | optional |
32
+ | debit, credit | `debit`, `credit` | both, or… |
33
+ | balance | `balance` | …one signed balance, positive in debit |
34
+ | contact | `contact`, `party` | optional: the customer or supplier of a receivable or payable row |
35
+ | contact name | `contact name`, `party name` | optional |
36
+ | label | `label`, `description` | optional |
37
+
38
+ - A row with a debit **and** a credit is **netted**: a trial balance prints the
39
+ totals of each side, and 100 in debit with 30 in credit opens at 70 in
40
+ debit. A row that nets to zero opens nothing and is left out.
41
+ - The **separator** is read from the header: the first of `,`, `;` and a tab it
42
+ holds outside quotes. Fields may be quoted as RFC 4180 says.
43
+ - The **decimal mark** is a point. A comma is read as one only in a file
44
+ separated by something else, where it cannot be a separator of thousands. No
45
+ thousands separator, no currency sign, no exponent: each is refused rather
46
+ than read one way or the other.
47
+ - The **encoding** is UTF-8, fatally. A file saved in Latin-1 is said to be so
48
+ (`{ encoding: 'iso-8859-1' }`); it is never guessed, because every sequence
49
+ of bytes is valid Latin-1.
50
+
51
+ ## What it returns
52
+
53
+ The shape declared in [`src/types.ts`](./src/types.ts) — accounts, contacts,
54
+ entries, an opening balance, violations — which is, field for field, the shape
55
+ every reader of an accounting export in this repository declares for itself.
56
+ A trial balance fills `opening` and leaves `entries` empty. It says no
57
+ currency and no date: the file has neither, and whatever takes the balance is
58
+ told the day it opens on.
59
+
60
+ What does not balance comes back in `violations`, and the rows come back as
61
+ written. What is not a trial balance at all — no account column, a debit
62
+ column beside a balance column, an amount that is not one — is thrown as a
63
+ `BooksFileError` with a `code`.
64
+
65
+ It knows no chart of accounts. The codes are the ones the file wrote; which
66
+ account of another chart each becomes is the importer's business. In
67
+ [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os) that is `ekwo import
68
+ trial-balance <file> --opening-date <first day of the year>`.
69
+
70
+ MIT.
package/dist/csv.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
3
+ * fields between double quotes may hold the separator, a line break and a
4
+ * doubled quote. Nothing else about the file is assumed.
5
+ *
6
+ * The separator is **read from the header**, which this format requires: the
7
+ * first of `,`, `;` and a tab that the header row contains outside quotes. A
8
+ * spreadsheet saves with a comma or a semicolon depending on the language of
9
+ * the machine, and the header is the one row whose content is known, so this
10
+ * is reading the file rather than guessing at it.
11
+ */
12
+ import type { Encoding } from './types.js';
13
+ export type Separator = ',' | ';' | '\t';
14
+ /** Bytes to text, in the encoding the caller said, refusing what is not that. */
15
+ export declare function decode(input: string | Uint8Array, encoding: Encoding, maxBytes: number): string;
16
+ /** The separator of a header line: the first of the three found outside quotes. */
17
+ export declare function separatorOf(headerLine: string): Separator;
18
+ /** Every row of the file, each an array of fields, with the row number it starts on. */
19
+ export declare function parseCsv(text: string, separator: Separator): {
20
+ row: number;
21
+ fields: string[];
22
+ }[];
23
+ /** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
24
+ export declare function headerKey(cell: string): string;
25
+ //# sourceMappingURL=csv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAEzC,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA0B/F;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAQzD;AAED,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,CAqDhG;AAED,mHAAmH;AACnH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
package/dist/csv.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
3
+ * fields between double quotes may hold the separator, a line break and a
4
+ * doubled quote. Nothing else about the file is assumed.
5
+ *
6
+ * The separator is **read from the header**, which this format requires: the
7
+ * first of `,`, `;` and a tab that the header row contains outside quotes. A
8
+ * spreadsheet saves with a comma or a semicolon depending on the language of
9
+ * the machine, and the header is the one row whose content is known, so this
10
+ * is reading the file rather than guessing at it.
11
+ */
12
+ import { BooksFileError } from './errors.js';
13
+ const BYTE_ORDER_MARK = 0xfeff;
14
+ /** Bytes to text, in the encoding the caller said, refusing what is not that. */
15
+ export function decode(input, encoding, maxBytes) {
16
+ let text;
17
+ if (typeof input === 'string') {
18
+ if (input.length > maxBytes)
19
+ throw new BooksFileError('too_large', `the file is longer than ${maxBytes} bytes`);
20
+ text = input;
21
+ }
22
+ else {
23
+ if (input.byteLength > maxBytes) {
24
+ throw new BooksFileError('too_large', `the file is ${input.byteLength} bytes, over the limit of ${maxBytes}`);
25
+ }
26
+ if (encoding === 'iso-8859-1') {
27
+ const parts = [];
28
+ for (let i = 0; i < input.length; i += 8192)
29
+ parts.push(String.fromCharCode(...input.subarray(i, i + 8192)));
30
+ text = parts.join('');
31
+ }
32
+ else {
33
+ try {
34
+ text = new TextDecoder('utf-8', { fatal: true }).decode(input);
35
+ }
36
+ catch {
37
+ throw new BooksFileError('unsupported_encoding', "the bytes are not UTF-8; if the file was saved in Latin-1, say so with { encoding: 'iso-8859-1' } — it is not guessed, because every sequence of bytes is valid Latin-1");
38
+ }
39
+ }
40
+ }
41
+ if (text.charCodeAt(0) === BYTE_ORDER_MARK)
42
+ text = text.slice(1);
43
+ return text;
44
+ }
45
+ /** The separator of a header line: the first of the three found outside quotes. */
46
+ export function separatorOf(headerLine) {
47
+ let quoted = false;
48
+ for (const char of headerLine) {
49
+ if (char === '"')
50
+ quoted = !quoted;
51
+ else if (!quoted && (char === ',' || char === ';' || char === '\t'))
52
+ return char;
53
+ }
54
+ // A header of one column has no separator; any will do, and a comma is it.
55
+ return ',';
56
+ }
57
+ /** Every row of the file, each an array of fields, with the row number it starts on. */
58
+ export function parseCsv(text, separator) {
59
+ const rows = [];
60
+ let fields = [];
61
+ let field = '';
62
+ let quoted = false;
63
+ let line = 1;
64
+ let startedOn = 1;
65
+ let fieldStarted = false;
66
+ const endRow = () => {
67
+ fields.push(field);
68
+ // A blank line is no row; a row of empty fields is kept and read.
69
+ if (!(fields.length === 1 && fields[0] === ''))
70
+ rows.push({ row: startedOn, fields });
71
+ fields = [];
72
+ field = '';
73
+ fieldStarted = false;
74
+ };
75
+ for (let i = 0; i < text.length; i += 1) {
76
+ const char = text[i];
77
+ if (quoted) {
78
+ if (char === '"') {
79
+ if (text[i + 1] === '"') {
80
+ field += '"';
81
+ i += 1;
82
+ }
83
+ else
84
+ quoted = false;
85
+ }
86
+ else {
87
+ if (char === '\n')
88
+ line += 1;
89
+ field += char;
90
+ }
91
+ continue;
92
+ }
93
+ if (char === '"' && !fieldStarted) {
94
+ quoted = true;
95
+ fieldStarted = true;
96
+ }
97
+ else if (char === separator) {
98
+ fields.push(field);
99
+ field = '';
100
+ fieldStarted = false;
101
+ }
102
+ else if (char === '\r' && text[i + 1] === '\n') {
103
+ // The \n that follows ends the row.
104
+ }
105
+ else if (char === '\n' || char === '\r') {
106
+ endRow();
107
+ line += 1;
108
+ startedOn = line;
109
+ }
110
+ else {
111
+ field += char;
112
+ fieldStarted = true;
113
+ }
114
+ }
115
+ if (quoted)
116
+ throw new BooksFileError('malformed_csv', 'a quoted field is never closed', startedOn);
117
+ if (field !== '' || fields.length > 0)
118
+ endRow();
119
+ return rows;
120
+ }
121
+ /** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
122
+ export function headerKey(cell) {
123
+ return cell.trim().replace(/^\*/, '').trim().toLowerCase();
124
+ }
125
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Amounts as the file wrote them, added without a float.
3
+ *
4
+ * An amount is held as a `bigint` of millionths: enough for any currency and
5
+ * any rate a spreadsheet prints, and exact. What leaves is a string with two
6
+ * decimals, and more when the file wrote more that are not zero — dropping a
7
+ * written zero is not rounding, dropping anything else would be, and is never
8
+ * done here.
9
+ */
10
+ /**
11
+ * The decimal marks a number may be written with. The comma is accepted only
12
+ * where it cannot be a separator of thousands — in a file whose fields are
13
+ * separated by something else — and a number never carries both marks.
14
+ */
15
+ export type DecimalMark = '.' | ',';
16
+ /**
17
+ * A written amount as millionths, or null when it is not a number this reader
18
+ * accepts: digits, one decimal mark, an optional leading minus or a pair of
19
+ * brackets for a negative. No thousands separator, no currency symbol, no
20
+ * exponent — each of those is refused rather than read one way or another.
21
+ */
22
+ export declare function parseDecimal(text: string, marks: readonly DecimalMark[]): bigint | null;
23
+ /** Millionths as a decimal string: signed, two decimals, more when they are written. */
24
+ export declare function formatDecimal(value: bigint): string;
25
+ //# sourceMappingURL=decimal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,GAAG,GAAG,CAAC;AAEpC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAuBvF;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMnD"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Amounts as the file wrote them, added without a float.
3
+ *
4
+ * An amount is held as a `bigint` of millionths: enough for any currency and
5
+ * any rate a spreadsheet prints, and exact. What leaves is a string with two
6
+ * decimals, and more when the file wrote more that are not zero — dropping a
7
+ * written zero is not rounding, dropping anything else would be, and is never
8
+ * done here.
9
+ */
10
+ const SCALE = 6;
11
+ const UNIT = 10n ** BigInt(SCALE);
12
+ /**
13
+ * A written amount as millionths, or null when it is not a number this reader
14
+ * accepts: digits, one decimal mark, an optional leading minus or a pair of
15
+ * brackets for a negative. No thousands separator, no currency symbol, no
16
+ * exponent — each of those is refused rather than read one way or another.
17
+ */
18
+ export function parseDecimal(text, marks) {
19
+ let value = text.trim();
20
+ if (value === '')
21
+ return null;
22
+ let negative = false;
23
+ if (value.startsWith('(') && value.endsWith(')')) {
24
+ negative = true;
25
+ value = value.slice(1, -1).trim();
26
+ }
27
+ if (value.startsWith('-')) {
28
+ if (negative)
29
+ return null;
30
+ negative = true;
31
+ value = value.slice(1);
32
+ }
33
+ else if (value.startsWith('+')) {
34
+ value = value.slice(1);
35
+ }
36
+ const pattern = marks.includes(',') ? /^([0-9]+)(?:[.,]([0-9]*))?$|^[.,]([0-9]+)$/ : /^([0-9]+)(?:\.([0-9]*))?$|^\.([0-9]+)$/;
37
+ const match = pattern.exec(value);
38
+ if (match === null)
39
+ return null;
40
+ const whole = match[1] ?? '0';
41
+ const fraction = match[2] ?? match[3] ?? '';
42
+ if (fraction.length > SCALE)
43
+ return null;
44
+ const units = BigInt(whole) * UNIT + BigInt(fraction.padEnd(SCALE, '0') || '0');
45
+ return negative ? -units : units;
46
+ }
47
+ /** Millionths as a decimal string: signed, two decimals, more when they are written. */
48
+ export function formatDecimal(value) {
49
+ const negative = value < 0n;
50
+ const digits = (negative ? -value : value).toString().padStart(SCALE + 1, '0');
51
+ const whole = digits.slice(0, -SCALE);
52
+ const fraction = digits.slice(-SCALE).replace(/0+$/, '').padEnd(2, '0');
53
+ return `${negative ? '-' : ''}${whole}.${fraction}`;
54
+ }
55
+ //# sourceMappingURL=decimal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,KAAK,GAAG,CAAC,CAAC;AAChB,MAAM,IAAI,GAAG,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;AASlC;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAA6B;IACtE,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACxB,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,QAAQ,GAAG,IAAI,CAAC;QAChB,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,IAAI,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC1B,QAAQ,GAAG,IAAI,CAAC;QAChB,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;SAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,4CAA4C,CAAC,CAAC,CAAC,wCAAwC,CAAC;IAC9H,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;IAC9B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;IAChF,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACnC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACxE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What makes a file unreadable, as opposed to books that read and do not add
3
+ * up. The second comes back in `violations`; the first is thrown, because a
4
+ * file with no header the reader recognises has no row to hang a violation on.
5
+ */
6
+ export type BooksFileErrorCode =
7
+ /** Larger than `maxBytes`. Refused before a single character is read. */
8
+ 'too_large'
9
+ /** Bytes that are not the encoding announced. */
10
+ | 'unsupported_encoding'
11
+ /** Nothing in the file, or a header and nothing under it. */
12
+ | 'empty_file'
13
+ /** A header that lacks a column the format needs, or names one twice. */
14
+ | 'missing_column'
15
+ /** A quote that is never closed. */
16
+ | 'malformed_csv'
17
+ /** A value the format does not allow where it is: a date, an amount. */
18
+ | 'invalid_value';
19
+ export declare class BooksFileError extends Error {
20
+ readonly code: BooksFileErrorCode;
21
+ /** The row the reader stopped on, from 1, when there is one. */
22
+ readonly row: number | null;
23
+ constructor(code: BooksFileErrorCode, message: string, row?: number | null);
24
+ }
25
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,WAAW;AACb,iDAAiD;GAC/C,sBAAsB;AACxB,6DAA6D;GAC3D,YAAY;AACd,yEAAyE;GACvE,gBAAgB;AAClB,oCAAoC;GAClC,eAAe;AACjB,wEAAwE;GACtE,eAAe,CAAC;AAEpB,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEhB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,GAAG,IAAW;CAMjF"}
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ export class BooksFileError extends Error {
2
+ code;
3
+ /** The row the reader stopped on, from 1, when there is one. */
4
+ row;
5
+ constructor(code, message, row = null) {
6
+ super(row === null ? message : `row ${row}: ${message}`);
7
+ this.name = 'BooksFileError';
8
+ this.code = code;
9
+ this.row = row;
10
+ }
11
+ }
12
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAqB;IAClC,gEAAgE;IACvD,GAAG,CAAgB;IAE5B,YAAY,IAAwB,EAAE,OAAe,EAAE,MAAqB,IAAI;QAC9E,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A trial balance, written as CSV: one row per account, a debit and a credit,
3
+ * or a signed balance. It is the one thing every accounting package can print
4
+ * and every spreadsheet can save, which is why it is the way in for books that
5
+ * come from anywhere: the balances of the day the new books open on.
6
+ *
7
+ * It depends on nothing, reads no database and knows no chart of accounts: the
8
+ * codes are the ones the file wrote, and saying which account of another chart
9
+ * each of them becomes is the importer's business, not this reader's.
10
+ */
11
+ import type { ImportedBooks, ReadOptions } from './types.js';
12
+ export * from './types.js';
13
+ export { BooksFileError, type BooksFileErrorCode } from './errors.js';
14
+ /**
15
+ * The columns this reader knows, each under the names it accepts in a header.
16
+ * Compared without regard to case, and without the `*` some exports put on a
17
+ * column that is required.
18
+ */
19
+ export declare const TRIAL_BALANCE_COLUMNS: {
20
+ readonly account: readonly ["account", "account code", "code"];
21
+ readonly name: readonly ["name", "account name"];
22
+ readonly debit: readonly ["debit"];
23
+ readonly credit: readonly ["credit"];
24
+ readonly balance: readonly ["balance"];
25
+ readonly contact: readonly ["contact", "party"];
26
+ readonly contactName: readonly ["contact name", "party name"];
27
+ readonly label: readonly ["label", "description"];
28
+ };
29
+ /**
30
+ * Reads the file. A row carries its account and either a debit and a credit —
31
+ * the totals a trial balance prints, which are netted: 100 in debit and 30 in
32
+ * credit open at 70 in debit — or one signed `balance`, positive in debit.
33
+ * A row that nets to zero opens nothing and is left out.
34
+ *
35
+ * A comma is read as the decimal mark only in a file whose fields are
36
+ * separated by something else, where it cannot be a separator of thousands.
37
+ */
38
+ export declare function readTrialBalance(input: string | Uint8Array, options?: ReadOptions): ImportedBooks;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAmB,aAAa,EAAiC,WAAW,EAAa,MAAM,YAAY,CAAC;AAExH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;CASxB,CAAC;AAIX;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAwGrG"}
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * A trial balance, written as CSV: one row per account, a debit and a credit,
3
+ * or a signed balance. It is the one thing every accounting package can print
4
+ * and every spreadsheet can save, which is why it is the way in for books that
5
+ * come from anywhere: the balances of the day the new books open on.
6
+ *
7
+ * It depends on nothing, reads no database and knows no chart of accounts: the
8
+ * codes are the ones the file wrote, and saying which account of another chart
9
+ * each of them becomes is the importer's business, not this reader's.
10
+ */
11
+ import { decode, headerKey, parseCsv, separatorOf } from './csv.js';
12
+ import { formatDecimal, parseDecimal } from './decimal.js';
13
+ import { BooksFileError } from './errors.js';
14
+ export * from './types.js';
15
+ export { BooksFileError } from './errors.js';
16
+ const DEFAULTS = { encoding: 'utf-8', maxBytes: 64 * 1024 * 1024 };
17
+ /**
18
+ * The columns this reader knows, each under the names it accepts in a header.
19
+ * Compared without regard to case, and without the `*` some exports put on a
20
+ * column that is required.
21
+ */
22
+ export const TRIAL_BALANCE_COLUMNS = {
23
+ account: ['account', 'account code', 'code'],
24
+ name: ['name', 'account name'],
25
+ debit: ['debit'],
26
+ credit: ['credit'],
27
+ balance: ['balance'],
28
+ contact: ['contact', 'party'],
29
+ contactName: ['contact name', 'party name'],
30
+ label: ['label', 'description'],
31
+ };
32
+ /**
33
+ * Reads the file. A row carries its account and either a debit and a credit —
34
+ * the totals a trial balance prints, which are netted: 100 in debit and 30 in
35
+ * credit open at 70 in debit — or one signed `balance`, positive in debit.
36
+ * A row that nets to zero opens nothing and is left out.
37
+ *
38
+ * A comma is read as the decimal mark only in a file whose fields are
39
+ * separated by something else, where it cannot be a separator of thousands.
40
+ */
41
+ export function readTrialBalance(input, options = {}) {
42
+ const text = decode(input, options.encoding ?? DEFAULTS.encoding, options.maxBytes ?? DEFAULTS.maxBytes);
43
+ const firstLine = text.split(/\r?\n/, 1)[0] ?? '';
44
+ const separator = separatorOf(firstLine);
45
+ const rows = parseCsv(text, separator);
46
+ const header = rows.shift();
47
+ if (header === undefined)
48
+ throw new BooksFileError('empty_file', 'the file is empty');
49
+ const at = locate(header.fields);
50
+ if (at.account === undefined) {
51
+ throw new BooksFileError('missing_column', `the header names no account column; it needs one of: ${TRIAL_BALANCE_COLUMNS.account.join(', ')}`, header.row);
52
+ }
53
+ const signed = at.balance !== undefined;
54
+ if (!signed && (at.debit === undefined || at.credit === undefined)) {
55
+ throw new BooksFileError('missing_column', 'the header needs a debit and a credit column, or one signed balance column', header.row);
56
+ }
57
+ if (signed && (at.debit !== undefined || at.credit !== undefined)) {
58
+ throw new BooksFileError('missing_column', 'the header has a balance column beside a debit or a credit one; keep one of the two ways, so that no amount is read twice', header.row);
59
+ }
60
+ if (rows.length === 0)
61
+ throw new BooksFileError('empty_file', 'the file has a header and no row under it', header.row);
62
+ const marks = separator === ',' ? ['.'] : ['.', ','];
63
+ const cell = (fields, column) => {
64
+ const index = at[column];
65
+ return index === undefined ? '' : (fields[index] ?? '').trim();
66
+ };
67
+ const amount = (fields, column, row) => {
68
+ const raw = cell(fields, column);
69
+ if (raw === '')
70
+ return 0n;
71
+ const value = parseDecimal(raw, marks);
72
+ if (value === null) {
73
+ throw new BooksFileError('invalid_value', `"${raw}" in the ${column} column is not an amount this reader accepts: digits and one decimal mark${separator === ',' ? ' (a point, in a comma-separated file)' : ''}, no thousands separator, no currency sign`, row);
74
+ }
75
+ return value;
76
+ };
77
+ const accounts = new Map();
78
+ const contacts = new Map();
79
+ const opening = [];
80
+ const violations = [];
81
+ let debits = 0n;
82
+ let credits = 0n;
83
+ for (const { row, fields } of rows) {
84
+ const account = cell(fields, 'account');
85
+ if (account === '') {
86
+ violations.push({ rule: 'account_missing', message: 'a row names no account and was left out', row });
87
+ continue;
88
+ }
89
+ const name = cell(fields, 'name');
90
+ if (!accounts.has(account))
91
+ accounts.set(account, { code: account, name: name === '' ? null : name, type: null });
92
+ const net = signed ? amount(fields, 'balance', row) : amount(fields, 'debit', row) - amount(fields, 'credit', row);
93
+ if (net === 0n)
94
+ continue;
95
+ const contact = cell(fields, 'contact');
96
+ if (contact !== '' && !contacts.has(contact)) {
97
+ const contactName = cell(fields, 'contactName');
98
+ contacts.set(contact, {
99
+ code: contact,
100
+ name: contactName === '' ? contact : contactName,
101
+ vatNumber: null,
102
+ registrationNumber: null,
103
+ email: null,
104
+ country: null,
105
+ });
106
+ }
107
+ const label = cell(fields, 'label');
108
+ opening.push({
109
+ account,
110
+ contact: contact === '' ? null : contact,
111
+ label: label === '' ? null : label,
112
+ debit: formatDecimal(net > 0n ? net : 0n),
113
+ credit: formatDecimal(net < 0n ? -net : 0n),
114
+ currency: null,
115
+ amountCurrency: null,
116
+ dueDate: null,
117
+ matching: null,
118
+ });
119
+ if (net > 0n)
120
+ debits += net;
121
+ else
122
+ credits -= net;
123
+ }
124
+ if (debits !== credits) {
125
+ violations.push({
126
+ rule: 'unbalanced',
127
+ message: `the balance has debit ${formatDecimal(debits)} and credit ${formatDecimal(credits)}; an opening entry has to balance`,
128
+ row: null,
129
+ });
130
+ }
131
+ return {
132
+ format: 'trial-balance',
133
+ currency: null,
134
+ accounts: [...accounts.values()],
135
+ contacts: [...contacts.values()],
136
+ entries: [],
137
+ opening,
138
+ violations,
139
+ };
140
+ }
141
+ function locate(header) {
142
+ const keys = header.map(headerKey);
143
+ const at = {};
144
+ for (const [column, names] of Object.entries(TRIAL_BALANCE_COLUMNS)) {
145
+ const found = keys.findIndex((key) => names.includes(key));
146
+ if (found !== -1)
147
+ at[column] = found;
148
+ }
149
+ return at;
150
+ }
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * What a set of books is once it has been read out of the software that kept
3
+ * them: accounts, parties, entries and an opening balance. The shape is
4
+ * declared here and imported from nowhere — it names no table and no ledger.
5
+ *
6
+ * It is, field for field, the shape every reader of an accounting export in
7
+ * this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
8
+ * this one), so whatever takes one takes the others. It is not a shared
9
+ * abstraction: none imports another, and a reader that has nothing to say for
10
+ * a part of it returns that part empty.
11
+ *
12
+ * Every amount is a **decimal string**, never negative: a line carries a debit
13
+ * or a credit, and the side is the sign. No figure passes through a `number`.
14
+ */
15
+ /** ISO 8601 calendar date, `YYYY-MM-DD`. */
16
+ export type IsoDate = string;
17
+ /** A fixed-point number, `1234.50`, never negative. */
18
+ export type Decimal = string;
19
+ /** One account of the chart the books were kept on, in the words of that software. */
20
+ export interface ImportedAccount {
21
+ code: string;
22
+ name: string | null;
23
+ /** What the source calls its kind, verbatim. Null when it says nothing. */
24
+ type: string | null;
25
+ }
26
+ /** A customer, a supplier or anybody else a line names. */
27
+ export interface ImportedContact {
28
+ /** How the source identifies the party on a line: its code, or its name where it has no code. */
29
+ code: string;
30
+ name: string;
31
+ vatNumber: string | null;
32
+ registrationNumber: string | null;
33
+ email: string | null;
34
+ /** ISO 3166-1 alpha-2, when the source writes one. Never inferred. */
35
+ country: string | null;
36
+ }
37
+ export interface ImportedLine {
38
+ /** The account code in the source's chart. */
39
+ account: string;
40
+ /** The `code` of an {@link ImportedContact}, or null. */
41
+ contact: string | null;
42
+ label: string | null;
43
+ debit: Decimal;
44
+ credit: Decimal;
45
+ /** A foreign currency the line was in, when the source says so. */
46
+ currency: string | null;
47
+ /** The amount in that currency, never negative. */
48
+ amountCurrency: Decimal | null;
49
+ dueDate: IsoDate | null;
50
+ /** The reconciliation mark the source put on the line, as written. */
51
+ matching: string | null;
52
+ }
53
+ export interface ImportedEntry {
54
+ /** The journal code in the source, or null when it has none. */
55
+ journal: string | null;
56
+ journalName: string | null;
57
+ /** The entry's number in the source. */
58
+ number: string | null;
59
+ date: IsoDate;
60
+ reference: string | null;
61
+ description: string | null;
62
+ lines: ImportedLine[];
63
+ /** The row of the file the entry starts on, from 1, for a message. */
64
+ row: number | null;
65
+ }
66
+ export interface Violation {
67
+ rule: string;
68
+ message: string;
69
+ /** The row of the file, from 1, or null for a rule about the whole file. */
70
+ row: number | null;
71
+ }
72
+ export interface ImportedBooks {
73
+ /** Which reader: `trial-balance` here. */
74
+ format: string;
75
+ /** The currency the file says its amounts are in. Null when it says none: never guessed. */
76
+ currency: string | null;
77
+ accounts: ImportedAccount[];
78
+ contacts: ImportedContact[];
79
+ entries: ImportedEntry[];
80
+ /** A trial balance: one line per account, to become the opening entry of a year. */
81
+ opening: ImportedLine[];
82
+ violations: Violation[];
83
+ }
84
+ export type Encoding = 'utf-8' | 'iso-8859-1';
85
+ export interface ReadOptions {
86
+ /**
87
+ * How bytes are decoded. Default `utf-8`, fatally: bytes that are not UTF-8
88
+ * are refused by name, never read as something else. A spreadsheet that
89
+ * saves in Latin-1 is said to be so, because every sequence of bytes is
90
+ * valid Latin-1 and a guess would always succeed.
91
+ */
92
+ encoding?: Encoding;
93
+ /** Refused beyond this many bytes, before anything is read. Default 64 MiB. */
94
+ maxBytes?: number;
95
+ }
96
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mDAAmD;IACnD,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wCAAwC;IACxC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,oFAAoF;IACpF,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a set of books is once it has been read out of the software that kept
3
+ * them: accounts, parties, entries and an opening balance. The shape is
4
+ * declared here and imported from nowhere — it names no table and no ledger.
5
+ *
6
+ * It is, field for field, the shape every reader of an accounting export in
7
+ * this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
8
+ * this one), so whatever takes one takes the others. It is not a shared
9
+ * abstraction: none imports another, and a reader that has nothing to say for
10
+ * a part of it returns that part empty.
11
+ *
12
+ * Every amount is a **decimal string**, never negative: a line carries a debit
13
+ * or a credit, and the side is the sign. No figure passes through a `number`.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ekwo-ai/trial-balance",
3
+ "version": "0.6.0",
4
+ "description": "Reads a trial balance written as CSV — account, debit, credit — into the plain shape an opening balance is made of, in TypeScript: amounts as decimal strings, and what does not add up. Zero dependencies.",
5
+ "keywords": [
6
+ "trial-balance",
7
+ "opening-balance",
8
+ "csv",
9
+ "accounting",
10
+ "bookkeeping",
11
+ "migration",
12
+ "import",
13
+ "parser"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Ekwo AI",
17
+ "homepage": "https://github.com/Ekwo-ai/ekwo-os/tree/main/packages/formats/trial-balance#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Ekwo-ai/ekwo-os.git",
21
+ "directory": "packages/formats/trial-balance"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Ekwo-ai/ekwo-os/issues"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "vitest run"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.9.0",
52
+ "vitest": "^3.2.0"
53
+ }
54
+ }