Files
actual/packages/loot-core/src/shared/util.test.ts
Copilot 690e2d0871 Fix: Accept keyboard apostrophe (U+0027) in arithmetic parser for apostrophe-dot format (#6795)
* Initial plan

* Add test case for apostrophe-dot format bug

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Fix: Support keyboard apostrophe (U+0027) in arithmetic parser for apostrophe-dot format

The arithmetic parser was only recognizing the typographic apostrophe (U+2019)
that Intl.NumberFormat outputs, but not the regular apostrophe (U+0027) that
users type on their keyboards. This caused amounts like "12'345.67" to be
truncated to "12" when users typed them.

The fix adds U+0027 to the regex character class in arithmetic.ts so both
apostrophe characters are accepted.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add explicit character code verification to apostrophe-dot test

Use escape sequences (\u0027 and \u2019) to ensure the test explicitly
uses the correct apostrophe characters, and add assertions to verify
the character codes are as expected.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Fix lint

* Add comprehensive tests for apostrophe-dot format in arithmetic.test.ts

Added three new test cases to verify both apostrophe types work correctly:
1. Tests keyboard apostrophe (U+0027) with explicit character code verification
2. Tests typographic apostrophe (U+2019) with explicit character code verification
3. Tests arithmetic operations with both apostrophe types

This addresses the feedback to add tests for the new logic in arithmetic.ts.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Add release notes for PR #6795

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-28 16:58:33 +00:00

232 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
currencyToAmount,
getNumberFormat,
looselyParseAmount,
setNumberFormat,
stringToInteger,
titleFirst,
} from './util';
describe('utility functions', () => {
test('looseParseAmount works with basic numbers', () => {
// Parsing is currently limited to 1,2 decimal places or 5-9.
// Ignoring 3 places removes the possibility of improper parse
// of amounts without decimal amounts included.
expect(looselyParseAmount('3')).toBe(3);
expect(looselyParseAmount('3.4')).toBe(3.4);
expect(looselyParseAmount('3.45')).toBe(3.45);
// cant tell if this next case should be decimal or different format
// so we set as full numbers
expect(looselyParseAmount('3.456')).toBe(3456); // the expected failing case
expect(looselyParseAmount('3.4500')).toBe(3.45);
expect(looselyParseAmount('3.45000')).toBe(3.45);
expect(looselyParseAmount('3.450000')).toBe(3.45);
expect(looselyParseAmount('3.4500000')).toBe(3.45);
expect(looselyParseAmount('3.45000000')).toBe(3.45);
expect(looselyParseAmount('3.450000000')).toBe(3.45);
});
test('looseParseAmount works with alternate formats', () => {
expect(looselyParseAmount('3,45')).toBe(3.45);
expect(looselyParseAmount('3,456')).toBe(3456); //expected failing case
expect(looselyParseAmount('3,4500')).toBe(3.45);
expect(looselyParseAmount('3,45000')).toBe(3.45);
expect(looselyParseAmount('3,450000')).toBe(3.45);
expect(looselyParseAmount('3,4500000')).toBe(3.45);
expect(looselyParseAmount('3,45000000')).toBe(3.45);
expect(looselyParseAmount('3,450000000')).toBe(3.45);
expect(looselyParseAmount("3'456.78")).toBe(3456.78);
expect(looselyParseAmount("3'456.78000")).toBe(3456.78);
expect(looselyParseAmount('1,00,000.99')).toBe(100000.99);
expect(looselyParseAmount('1,00,000.99000')).toBe(100000.99);
});
test('looseParseAmount works with leading decimal characters', () => {
expect(looselyParseAmount('.45')).toBe(0.45);
expect(looselyParseAmount(',45')).toBe(0.45);
});
test('looseParseAmount works with negative numbers', () => {
expect(looselyParseAmount('-3')).toBe(-3);
expect(looselyParseAmount('-3.45')).toBe(-3.45);
expect(looselyParseAmount('-3,45')).toBe(-3.45);
// Unicode minus
expect(looselyParseAmount('3')).toBe(-3);
expect(looselyParseAmount('3.45')).toBe(-3.45);
expect(looselyParseAmount('3,45')).toBe(-3.45);
});
test('looseParseAmount works with parentheses (negative)', () => {
expect(looselyParseAmount('(3.45)')).toBe(-3.45);
expect(looselyParseAmount('(3)')).toBe(-3);
// Parentheses with Unicode minus
expect(looselyParseAmount('(3.45)')).toBe(-3.45);
expect(looselyParseAmount('(3)')).toBe(-3);
});
test('looseParseAmount ignores non-numeric characters', () => {
// This is strange behavior because it does not work for just
// `3_45_23` (it needs a decimal amount). This function should be
// thought through more.
expect(looselyParseAmount('3_45_23.10')).toBe(34523.1);
expect(looselyParseAmount('(1 500.99)')).toBe(-1500.99);
});
test('number formatting works with comma-dot format', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: false });
let formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe('1,234.56');
setNumberFormat({ format: 'comma-dot', hideFraction: true });
formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe('1,235');
});
test('number formatting works with comma-dot-in format', () => {
setNumberFormat({ format: 'comma-dot-in', hideFraction: false });
let formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234567.89'))).toBe('12,34,567.89');
setNumberFormat({ format: 'comma-dot-in', hideFraction: true });
formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234567.89'))).toBe('12,34,568');
});
test('number formatting works with dot-comma format', () => {
setNumberFormat({ format: 'dot-comma', hideFraction: false });
let formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe('1.234,56');
setNumberFormat({ format: 'dot-comma', hideFraction: true });
formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe('1.235');
});
test('number formatting works with space-comma format', () => {
setNumberFormat({ format: 'space-comma', hideFraction: false });
let formatter = getNumberFormat().formatter;
// grouping separator space char is a narrow non-breaking space (U+202F)
expect(formatter.format(Number('1234.56'))).toBe('1\u202F234,56');
setNumberFormat({ format: 'space-comma', hideFraction: true });
formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe('1\u202F235');
});
test('number formatting works with apostrophe-dot format', () => {
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
let formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe(`1\u2019234.56`);
setNumberFormat({ format: 'apostrophe-dot', hideFraction: true });
formatter = getNumberFormat().formatter;
expect(formatter.format(Number('1234.56'))).toBe(`1\u2019235`);
});
test('number formatting works with small negative numbers with 0 decimal places', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: true });
const formatter = getNumberFormat().formatter;
expect(formatter.format(Number('-0.1'))).toBe('0');
expect(formatter.format(Number('-0.5'))).toBe('-1');
expect(formatter.format(Number('-0.9'))).toBe('-1');
expect(formatter.format(Number('-1.2'))).toBe('-1');
});
test('currencyToAmount works with basic numbers', () => {
expect(currencyToAmount('3')).toBe(3);
expect(currencyToAmount('3.4')).toBe(3.4);
expect(currencyToAmount('3.45')).toBe(3.45);
expect(currencyToAmount('3.45060')).toBe(3.4506);
});
test('currencyToAmount works with varied formats', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: true });
expect(currencyToAmount('3,45')).toBe(3.45);
expect(currencyToAmount('3,456')).toBe(3456);
expect(currencyToAmount('3,45000')).toBe(345000);
expect(currencyToAmount("3'456.78")).toBe(3456.78);
expect(currencyToAmount("3'456.78000")).toBe(3456.78);
expect(currencyToAmount('1,00,000.99')).toBe(100000.99);
expect(currencyToAmount('1,00,000.99000')).toBe(100000.99);
});
test('currencyToAmount works with leading decimal characters', () => {
expect(currencyToAmount('.45')).toBe(0.45);
expect(currencyToAmount(',45')).toBe(0.45);
});
test('currencyToAmount works with negative numbers', () => {
expect(currencyToAmount('-3')).toBe(-3);
expect(currencyToAmount('-3.45')).toBe(-3.45);
expect(currencyToAmount('-3,45')).toBe(-3.45);
// Unicode minus
expect(currencyToAmount('3')).toBe(-3);
expect(currencyToAmount('3.45')).toBe(-3.45);
expect(currencyToAmount('3,45')).toBe(-3.45);
});
test('currencyToAmount works with non-fractional numbers', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: false });
expect(currencyToAmount('3.')).toBe(3);
expect(currencyToAmount('3,')).toBe(3);
expect(currencyToAmount('3,000')).toBe(3000);
expect(currencyToAmount('3,000.')).toBe(3000);
});
test('currencyToAmount works with hidden fractions', () => {
setNumberFormat({ format: 'comma-dot', hideFraction: true });
expect(currencyToAmount('3.45')).toBe(3.45);
expect(currencyToAmount('3.456')).toBe(3.456);
expect(currencyToAmount('3.4500')).toBe(3.45);
expect(currencyToAmount('3.')).toBe(3);
expect(currencyToAmount('3,')).toBe(3);
expect(currencyToAmount('3,000')).toBe(3000);
expect(currencyToAmount('3,000.')).toBe(3000);
});
test('currencyToAmount works with apostrophe-dot format', () => {
setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
// Test with regular apostrophe (U+0027) - what users type on keyboard
const keyboardApostrophe = '12\u0027345.67';
expect(keyboardApostrophe.charCodeAt(2)).toBe(0x0027); // Verify it's U+0027
expect(currencyToAmount(keyboardApostrophe)).toBe(12345.67);
expect(currencyToAmount('1\u0027234.56')).toBe(1234.56);
expect(currencyToAmount('1\u0027000.33')).toBe(1000.33);
expect(currencyToAmount('100\u0027000.99')).toBe(100000.99);
expect(currencyToAmount('1\u0027000\u0027000.50')).toBe(1000000.5);
// Test with right single quotation mark (U+2019) - what Intl.NumberFormat outputs
const intlApostrophe = '12\u2019345.67';
expect(intlApostrophe.charCodeAt(2)).toBe(0x2019); // Verify it's U+2019
expect(currencyToAmount(intlApostrophe)).toBe(12345.67);
expect(currencyToAmount('1\u2019234.56')).toBe(1234.56);
expect(currencyToAmount('1\u2019000.33')).toBe(1000.33);
});
test('currencyToAmount works with dot-comma', () => {
setNumberFormat({ format: 'dot-comma', hideFraction: false });
expect(currencyToAmount('3,45')).toBe(3.45);
expect(currencyToAmount('3,456')).toBe(3.456);
expect(currencyToAmount('3,4500')).toBe(3.45);
expect(currencyToAmount('3,')).toBe(3);
expect(currencyToAmount('3.')).toBe(3);
expect(currencyToAmount('3.000')).toBe(3000);
expect(currencyToAmount('3.000,')).toBe(3000);
});
test('titleFirst works with all inputs', () => {
expect(titleFirst('')).toBe('');
expect(titleFirst(undefined)).toBe('');
expect(titleFirst(null)).toBe('');
expect(titleFirst('a')).toBe('A');
expect(titleFirst('abc')).toBe('Abc');
});
test('stringToInteger works with negative numbers', () => {
expect(stringToInteger('-3')).toBe(-3);
// Unicode minus
expect(stringToInteger('3')).toBe(-3);
});
});