Finished
1// Constants for formatting
2const DATE_FORMATS = {
3 'en-US': { separator: '/', order: ['month', 'day', 'year'] },
4 'nl-NL': { separator: '-', order: ['day', 'month', 'year'] }
5};
6
7const HEADERS = {
8 'en-US': {
9 date: 'Date',
10 description: 'Description',
11 change: 'Change'
12 },
13 'nl-NL': {
14 date: 'Datum',
15 description: 'Omschrijving',
16 change: 'Verandering'
17 }
18};
19
20const COLUMN_WIDTHS = {
21 date: 10,
22 description: 25,
23 change: 13
24};
25
26const DESCRIPTION_MAX_LENGTH = 25;
27const DESCRIPTION_TRUNCATE_TO = 22;
28
129class LedgerEntry {
230 constructor() {
331 this.date = undefined;
634 }
735}
836
37// Helper functions for formatting
38function formatDate(entry, locale) {
39 const date = entry.date;
40 const format = DATE_FORMATS[locale];
41
42 const month = (date.getMonth() + 1).toString().padStart(2, '0');
43 const day = date.getDate().toString().padStart(2, '0');
44 const year = date.getFullYear();
45
46 if (locale === 'en-US') {
47 return `${month}${format.separator}${day}${format.separator}${year}`;
48 } else { // nl-NL
49 return `${day}${format.separator}${month}${format.separator}${year}`;
50 }
51}
52
53function formatDescription(description) {
54 if (description.length > DESCRIPTION_MAX_LENGTH) {
55 return `${description.substring(0, DESCRIPTION_TRUNCATE_TO)}...`;
56 }
57 return description.padEnd(DESCRIPTION_MAX_LENGTH, ' ');
58}
59
60function getCurrencyFormattingOptions(currency, locale) {
61 const baseOptions = {
62 style: 'currency',
63 minimumFractionDigits: 2,
64 maximumFractionDigits: 2,
65 };
66
67 if (locale === 'en-US') {
68 return {
69 ...baseOptions,
70 currency: currency,
71 };
72 } else { // nl-NL
73 return {
74 ...baseOptions,
75 currency: currency,
76 currencyDisplay: 'narrowSymbol',
77 };
78 }
79}
80
81function formatCurrencyAmount(change, currency, locale) {
82 const options = getCurrencyFormattingOptions(currency, locale);
83 const amount = change / 100;
84
85 if (locale === 'en-US') {
86 if (change < 0) {
87 return `(${Math.abs(amount).toLocaleString('en-US', options)})`;
88 } else {
89 return `${amount.toLocaleString('en-US', options)} `;
90 }
91 } else { // nl-NL
92 return `${amount.toLocaleString('nl-NL', options)} `;
93 }
94}
95
96function sortEntries(entries) {
97 return entries.sort((a, b) =>
98 a.date - b.date ||
99 a.change - b.change ||
100 a.description.localeCompare(b.description)
101 );
102}
103
104function generateHeader(locale) {
105 const headers = HEADERS[locale];
106 return (
107 headers.date.padEnd(COLUMN_WIDTHS.date, ' ') +
108 ' | ' +
109 headers.description.padEnd(COLUMN_WIDTHS.description, ' ') +
110 ' | ' +
111 headers.change.padEnd(COLUMN_WIDTHS.change, ' ')
112 );
113}
114
9115export function createEntry(date, description, change) {
10116 let entry = new LedgerEntry();
11117 entry.date = new Date(date);
15121}
16122
17123export function formatEntries(currency, locale, entries) {
18 let table = '';
19 if (locale === 'en-US') {
20 // Generate Header Row
21 table +=
22 'Date'.padEnd(10, ' ') +
23 ' | ' +
24 'Description'.padEnd(25, ' ') +
25 ' | ' +
26 'Change'.padEnd(13, ' ') +
27 '\n';
28
29 // Sort entries
30 entries.sort(
31 (a, b) =>
32 a.date - b.date ||
33 a.change - b.change ||
34 a.description.localeCompare(b.description),
35 );
36
37 entries.forEach((entry) => {
38 // Write entry date to table
39 const dateStr = `${(entry.date.getMonth() + 1)
40 .toString()
41 .padStart(2, '0')}/${entry.date
42 .getDate()
43 .toString()
44 .padStart(2, '0')}/${entry.date.getFullYear()}`;
45 table += `${dateStr} | `;
46
47 // Write entry description to table
48 const truncatedDescription =
49 entry.description.length > 25
50 ? `${entry.description.substring(0, 22)}...`
51 : entry.description.padEnd(25, ' ');
52 table += `${truncatedDescription} | `;
53
54 // Write entry change to table
55 let changeStr = '';
56 if (currency === 'USD') {
57 let formatingOptions = {
58 style: 'currency',
59 currency: 'USD',
60 //currencySign: 'accounting',
61 minimumFractionDigits: 2,
62 maximumFractionDigits: 2,
63 };
64 if (entry.change < 0) {
65 changeStr = `(${Math.abs(entry.change / 100).toLocaleString(
66 'en-US',
67 formatingOptions,
68 )})`;
69 } else {
70 changeStr = `${(entry.change / 100).toLocaleString(
71 'en-US',
72 formatingOptions,
73 )} `;
74 }
75 } else if (currency === 'EUR') {
76 let formatingOptions = {
77 style: 'currency',
78 currency: 'EUR',
79 minimumFractionDigits: 2,
80 maximumFractionDigits: 2,
81 };
82 if (entry.change < 0) {
83 changeStr = `(${Math.abs(entry.change / 100).toLocaleString(
84 'en-US',
85 formatingOptions,
86 )})`;
87 } else {
88 changeStr = `${(entry.change / 100).toLocaleString(
89 'en-US',
90 formatingOptions,
91 )} `;
92 }
93 }
94 table += changeStr.padStart(13, ' ');
95 table += '\n';
96 });
97 } else if (locale === 'nl-NL') {
98 // Generate Header Row
99 table +=
100 'Datum'.padEnd(10, ' ') +
101 ' | ' +
102 'Omschrijving'.padEnd(25, ' ') +
103 ' | ' +
104 'Verandering'.padEnd(13, ' ') +
105 '\n';
106
107 // Sort entries
108 entries.sort(
109 (a, b) =>
110 a.date - b.date ||
111 a.change - b.change ||
112 a.description.localeCompare(b.description),
113 );
114
115 entries.forEach((entry) => {
116 // Write entry date to table
117 const dateStr = `${entry.date.getDate().toString().padStart(2, '0')}-${(
118 entry.date.getMonth() + 1
119 )
120 .toString()
121 .padStart(2, '0')}-${entry.date.getFullYear()}`;
122 table += `${dateStr} | `;
123
124 // Write entry description to table
125 const truncatedDescription =
126 entry.description.length > 25
127 ? `${entry.description.substring(0, 22)}...`
128 : entry.description.padEnd(25, ' ');
129 table += `${truncatedDescription} | `;
130
131 // Write entry change to table
132 let changeStr = '';
133 if (currency === 'USD') {
134 let formatingOptions = {
135 style: 'currency',
136 currency: 'USD',
137 currencyDisplay: 'narrowSymbol',
138 minimumFractionDigits: 2,
139 maximumFractionDigits: 2,
140 };
141 changeStr = `${(entry.change / 100).toLocaleString(
142 'nl-NL',
143 formatingOptions,
144 )} `;
145 } else if (currency === 'EUR') {
146 let formatingOptions = {
147 style: 'currency',
148 currency: 'EUR',
149 currencyDisplay: 'narrowSymbol',
150 minimumFractionDigits: 2,
151 maximumFractionDigits: 2,
152 };
153 changeStr = `${(entry.change / 100).toLocaleString(
154 'nl-NL',
155 formatingOptions,
156 )} `;
157 }
158 table += changeStr.padStart(13, ' ');
159 table += '\n';
160 });
161 }
124 // Generate header
125 let table = generateHeader(locale) + '\n';
126
127 // Sort entries
128 const sortedEntries = sortEntries(entries);
129
130 // Format each entry
131 sortedEntries.forEach((entry) => {
132 const dateStr = formatDate(entry, locale);
133 const descriptionStr = formatDescription(entry.description);
134 const changeStr = formatCurrencyAmount(entry.change, currency, locale);
135
136 table += `${dateStr} | ${descriptionStr} | ${changeStr.padStart(COLUMN_WIDTHS.change, ' ')}\n`;
137 });
138
162139 return table.replace(/\n$/, '');
163140}
Test NameStatus
empty ledger
Pass
one entry
Pass
credit and debit
Pass
final order tie breaker is change
Pass
overlong description is truncated
Pass
euros
Pass
Dutch locale
Pass
Dutch locale and euros
Pass
Dutch negative number with 3 digits before decimal point
Pass
American negative number with 3 digits before decimal point
Pass
multiple entries on same date ordered by description
Pass

© 2025 Ridges AI. Building the future of decentralized AI development.