diff --git a/.changeset/curly-bees-drum.md b/.changeset/curly-bees-drum.md new file mode 100644 index 000000000..32088ef6d --- /dev/null +++ b/.changeset/curly-bees-drum.md @@ -0,0 +1,5 @@ +--- +'@lion/input-amount': patch +--- + +rounds up parseAmount to correct amount of decimals based on currency diff --git a/packages/input-amount/index.js b/packages/input-amount/index.js index 49392cafc..b8a6b1969 100644 --- a/packages/input-amount/index.js +++ b/packages/input-amount/index.js @@ -1,4 +1,4 @@ export { LionInputAmount } from './src/LionInputAmount.js'; export { formatAmount } from './src/formatters.js'; -export { parseAmount } from './src/parsers.js'; +export { parseAmount, parseNumber } from './src/parsers.js'; export { preprocessAmount } from './src/preprocessors.js'; diff --git a/packages/input-amount/src/parsers.js b/packages/input-amount/src/parsers.js index 35f0ee8ec..192aa7580 100644 --- a/packages/input-amount/src/parsers.js +++ b/packages/input-amount/src/parsers.js @@ -1,4 +1,8 @@ -import { getDecimalSeparator } from '@lion/localize'; +import { getDecimalSeparator, getFractionDigits } from '@lion/localize'; + +/** + * @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions + */ /** * @param {string} value to evaluate @@ -8,6 +12,19 @@ function isDecimalSeparator(value) { return value === '.' || value === ','; } +/** + * Rounding problem can be avoided by using numbers represented in exponential notation + * @param {number} value to be rounded up + * @param {number | undefined} decimals amount of decimals to keep + * @return {number} new value with rounded up decimals + */ +function round(value, decimals) { + if (typeof decimals === 'undefined') { + return Number(value); + } + return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); +} + /** * Determines the best possible parsing mode. * @@ -112,16 +129,16 @@ function parseHeuristic(value) { * - 'heuristic': result depends on considering separators * * @example - * parseAmount('1.234.567'); // method: unparseable => 1234567 - * parseAmount('1.234'); // method: withLocale => depending on locale 1234 or 1.234 - * parseAmount('1.234,56'); // method: heuristic => 1234.56 - * parseAmount('1 234.56'); // method: heuristic => 1234.56 - * parseAmount('1,234.56'); // method: heuristic => 1234.56 + * parseNumber('1.234.567'); // method: unparseable => 1234567 + * parseNumber('1.234'); // method: withLocale => depending on locale 1234 or 1.234 + * parseNumber('1.234,56'); // method: heuristic => 1234.56 + * parseNumber('1 234.56'); // method: heuristic => 1234.56 + * parseNumber('1,234.56'); // method: heuristic => 1234.56 * * @param {string} value Number to be parsed * @param {object} [options] Locale Options */ -export function parseAmount(value, options) { +export function parseNumber(value, options) { const containsNumbers = value.match(/\d/g); if (!containsNumbers) { return undefined; @@ -148,3 +165,33 @@ export function parseAmount(value, options) { return 0; } } + +/** + * Uses formatNumber to parses a number string and returns the best possible javascript number. + * Rounds up the number with the correct amount of decimals according to the currency. + * + * @example + * parseAmount('1,234.56', {currency: 'EUR'}); => 1234.56 + * parseAmount('1,234.56', {currency: 'JPY'}); => 1235 + * parseAmount('1,234.56', {currency: 'JOD'}); => 1234.560 + * + * @param {string} value Number to be parsed + * @param {FormatOptions} [givenOptions] Locale Options + */ +export function parseAmount(value, givenOptions) { + const number = parseNumber(value, givenOptions); + + if (typeof number !== 'number') { + return number; + } + + /** @type {FormatOptions} */ + const options = { + ...givenOptions, + }; + + if (options.currency && typeof options.maximumFractionDigits === 'undefined') { + options.maximumFractionDigits = getFractionDigits(options.currency); + } + return round(number, options.maximumFractionDigits); +} diff --git a/packages/input-amount/test/lion-input-amount.test.js b/packages/input-amount/test/lion-input-amount.test.js index 14ff583d9..0fef018a5 100644 --- a/packages/input-amount/test/lion-input-amount.test.js +++ b/packages/input-amount/test/lion-input-amount.test.js @@ -77,6 +77,14 @@ describe('', () => { expect(el.parser).to.equal(parseAmount); }); + it('sets correct amount of decimals', async () => { + const el = /** @type {LionInputAmount} */ ( + await fixture(html``) + ); + const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el)); + expect(_inputNode.value).to.equal('100.12'); + }); + it('sets inputmode attribute to decimal', async () => { const el = /** @type {LionInputAmount} */ ( await fixture(``) diff --git a/packages/input-amount/test/parsers.test.js b/packages/input-amount/test/parsers.test.js index bc3396939..90e5c7f17 100644 --- a/packages/input-amount/test/parsers.test.js +++ b/packages/input-amount/test/parsers.test.js @@ -1,165 +1,198 @@ import { expect } from '@open-wc/testing'; import { localize } from '@lion/localize'; -import { parseAmount } from '../src/parsers.js'; +import { parseAmount, parseNumber } from '../src/parsers.js'; -describe('parseAmount()', () => { - it('parses integers', () => { - expect(parseAmount('1')).to.equal(1); - expect(parseAmount('12')).to.equal(12); - expect(parseAmount('123')).to.equal(123); - expect(parseAmount('1234')).to.equal(1234); - expect(parseAmount('12345')).to.equal(12345); - expect(parseAmount('123456')).to.equal(123456); - expect(parseAmount('1234567')).to.equal(1234567); - expect(parseAmount('12345678')).to.equal(12345678); - expect(parseAmount('123456789')).to.equal(123456789); +describe('parsers', () => { + describe('parseNumber()', () => { + it('parses integers', () => { + expect(parseNumber('1')).to.equal(1); + expect(parseNumber('12')).to.equal(12); + expect(parseNumber('123')).to.equal(123); + expect(parseNumber('1234')).to.equal(1234); + expect(parseNumber('12345')).to.equal(12345); + expect(parseNumber('123456')).to.equal(123456); + expect(parseNumber('1234567')).to.equal(1234567); + expect(parseNumber('12345678')).to.equal(12345678); + expect(parseNumber('123456789')).to.equal(123456789); + }); + + it('detects separators heuristically when there are 2 different ones e.g. 1,234.5', () => { + expect(parseNumber('1,234.5')).to.equal(1234.5); + expect(parseNumber('1.234,5')).to.equal(1234.5); + expect(parseNumber('1 234.5')).to.equal(1234.5); + expect(parseNumber('1 234,5')).to.equal(1234.5); + + expect(parseNumber('1,234.56')).to.equal(1234.56); + expect(parseNumber('1.234,56')).to.equal(1234.56); + expect(parseNumber('1 234.56')).to.equal(1234.56); + expect(parseNumber('1 234,56')).to.equal(1234.56); + + expect(parseNumber('1,234.567')).to.equal(1234.567); + expect(parseNumber('1.234,567')).to.equal(1234.567); + expect(parseNumber('1 234.567')).to.equal(1234.567); + expect(parseNumber('1 234,567')).to.equal(1234.567); + + expect(parseNumber('1,234.5678')).to.equal(1234.5678); + expect(parseNumber('1.234,5678')).to.equal(1234.5678); + expect(parseNumber('1 234.5678')).to.equal(1234.5678); + expect(parseNumber('1 234,5678')).to.equal(1234.5678); + + expect(parseNumber('1,234.56789')).to.equal(1234.56789); + expect(parseNumber('1.234,56789')).to.equal(1234.56789); + expect(parseNumber('1 234.56789')).to.equal(1234.56789); + expect(parseNumber('1 234,56789')).to.equal(1234.56789); + }); + + it('detects separators heuristically when there is only one and "pasted" mode used e.g. 123456,78', () => { + expect(parseNumber('1.', { mode: 'pasted' })).to.equal(1); + expect(parseNumber('1,', { mode: 'pasted' })).to.equal(1); + expect(parseNumber('1 ', { mode: 'pasted' })).to.equal(1); + + expect(parseNumber('1.2', { mode: 'pasted' })).to.equal(1.2); + expect(parseNumber('1,2', { mode: 'pasted' })).to.equal(1.2); + expect(parseNumber('1 2', { mode: 'pasted' })).to.equal(12); + + expect(parseNumber('1.23', { mode: 'pasted' })).to.equal(1.23); + expect(parseNumber('1,23', { mode: 'pasted' })).to.equal(1.23); + expect(parseNumber('1 23', { mode: 'pasted' })).to.equal(123); + + expect(parseNumber('1 234', { mode: 'pasted' })).to.equal(1234); + + expect(parseNumber('1.2345', { mode: 'pasted' })).to.equal(1.2345); + expect(parseNumber('1,2345', { mode: 'pasted' })).to.equal(1.2345); + expect(parseNumber('1 2345', { mode: 'pasted' })).to.equal(12345); + + expect(parseNumber('1.23456', { mode: 'pasted' })).to.equal(1.23456); + expect(parseNumber('1,23456', { mode: 'pasted' })).to.equal(1.23456); + expect(parseNumber('1 23456', { mode: 'pasted' })).to.equal(123456); + + expect(parseNumber('1.234567', { mode: 'pasted' })).to.equal(1.234567); + expect(parseNumber('1,234567', { mode: 'pasted' })).to.equal(1.234567); + expect(parseNumber('1 234567', { mode: 'pasted' })).to.equal(1234567); + + expect(parseNumber('123456,78', { mode: 'pasted' })).to.equal(123456.78); + expect(parseNumber('123456.78', { mode: 'pasted' })).to.equal(123456.78); + }); + + it('detects separators heuristically when there are 2 same ones e.g. 1.234.56', () => { + expect(parseNumber('1.234.5')).to.equal(1234.5); + expect(parseNumber('1,234,5')).to.equal(1234.5); + + expect(parseNumber('1.234.56')).to.equal(1234.56); + expect(parseNumber('1,234,56')).to.equal(1234.56); + expect(parseNumber('1 234 56')).to.equal(123456); + + expect(parseNumber('1.234.5678')).to.equal(1234.5678); + expect(parseNumber('1,234,5678')).to.equal(1234.5678); + + expect(parseNumber('1.234.56789')).to.equal(1234.56789); + expect(parseNumber('1,234,56789')).to.equal(1234.56789); + }); + + it('uses locale to parse amount if there is only one separator e.g. 1.234', () => { + localize.locale = 'en-GB'; + expect(parseNumber('12.34')).to.equal(12.34); + expect(parseNumber('12,34')).to.equal(1234); + expect(parseNumber('1.234')).to.equal(1.234); + expect(parseNumber('1,234')).to.equal(1234); + + localize.locale = 'nl-NL'; + expect(parseNumber('12.34')).to.equal(1234); + expect(parseNumber('12,34')).to.equal(12.34); + expect(parseNumber('1.234')).to.equal(1234); + expect(parseNumber('1,234')).to.equal(1.234); + }); + + it('returns numbers only if it can not be interpreted e.g. 1.234.567', () => { + // impossible to interpret unambiguously even with locale knowledge + expect(parseNumber('1.234.567')).to.equal(1234567); + expect(parseNumber('1,234,567')).to.equal(1234567); + }); + + it('keeps only last separator for "broken" numbers like 1.23,4', () => { + expect(parseNumber('1.23,4')).to.equal(123.4); + expect(parseNumber('1,23.4')).to.equal(123.4); + expect(parseNumber('1 23,4')).to.equal(123.4); + expect(parseNumber('1 23.4')).to.equal(123.4); + }); + + it('parses negative numbers', () => { + expect(parseNumber('-0')).to.equal(0); + expect(parseNumber('-1')).to.equal(-1); + expect(parseNumber('-1234')).to.equal(-1234); + expect(parseNumber('-1.234,5')).to.equal(-1234.5); + expect(parseNumber('-1,234.5')).to.equal(-1234.5); + expect(parseNumber('-1.234,5678')).to.equal(-1234.5678); + expect(parseNumber('-1,234.5678')).to.equal(-1234.5678); + }); + + it('ignores all non-number symbols (including currency)', () => { + expect(parseNumber('€ 1,234.56')).to.equal(1234.56); + expect(parseNumber('€ -1,234.56')).to.equal(-1234.56); + expect(parseNumber('-€ 1,234.56')).to.equal(-1234.56); + expect(parseNumber('1,234.56 €')).to.equal(1234.56); + expect(parseNumber('-1,234.56 €')).to.equal(-1234.56); + expect(parseNumber('EUR 1,234.56')).to.equal(1234.56); + expect(parseNumber('EUR -1,234.56')).to.equal(-1234.56); + expect(parseNumber('-EUR 1,234.56')).to.equal(-1234.56); + expect(parseNumber('1,234.56 EUR')).to.equal(1234.56); + expect(parseNumber('-1,234.56 EUR')).to.equal(-1234.56); + expect(parseNumber('Number is 1,234.56')).to.equal(1234.56); + }); + + it('ignores non-number characters and returns undefined', () => { + expect(parseNumber('A')).to.equal(undefined); + expect(parseNumber('EUR')).to.equal(undefined); + expect(parseNumber('EU R')).to.equal(undefined); + }); + + it('returns undefined when value is empty string', () => { + expect(parseNumber('')).to.equal(undefined); + }); + + it('with locale set and length is more than four', () => { + expect( + parseNumber('6,000', { + locale: 'en-GB', + }), + ).to.equal(6000); + expect( + parseNumber('6.000', { + locale: 'es-ES', + }), + ).to.equal(6000); + }); }); - it('detects separators heuristically when there are 2 different ones e.g. 1,234.5', () => { - expect(parseAmount('1,234.5')).to.equal(1234.5); - expect(parseAmount('1.234,5')).to.equal(1234.5); - expect(parseAmount('1 234.5')).to.equal(1234.5); - expect(parseAmount('1 234,5')).to.equal(1234.5); + describe('parseAmount()', async () => { + it('with currency set to correct amount of decimals', async () => { + localize.locale = 'en-GB'; + expect( + parseAmount('1.015', { + currency: 'EUR', + }), + ).to.equal(1.02); + expect( + parseAmount('5.555', { + currency: 'EUR', + }), + ).to.equal(5.56); + expect( + parseAmount('100.1235', { + currency: 'JPY', + }), + ).to.equal(100); + expect( + parseAmount('100.1235', { + currency: 'JOD', + }), + ).to.equal(100.124); + }); - expect(parseAmount('1,234.56')).to.equal(1234.56); - expect(parseAmount('1.234,56')).to.equal(1234.56); - expect(parseAmount('1 234.56')).to.equal(1234.56); - expect(parseAmount('1 234,56')).to.equal(1234.56); - - expect(parseAmount('1,234.567')).to.equal(1234.567); - expect(parseAmount('1.234,567')).to.equal(1234.567); - expect(parseAmount('1 234.567')).to.equal(1234.567); - expect(parseAmount('1 234,567')).to.equal(1234.567); - - expect(parseAmount('1,234.5678')).to.equal(1234.5678); - expect(parseAmount('1.234,5678')).to.equal(1234.5678); - expect(parseAmount('1 234.5678')).to.equal(1234.5678); - expect(parseAmount('1 234,5678')).to.equal(1234.5678); - - expect(parseAmount('1,234.56789')).to.equal(1234.56789); - expect(parseAmount('1.234,56789')).to.equal(1234.56789); - expect(parseAmount('1 234.56789')).to.equal(1234.56789); - expect(parseAmount('1 234,56789')).to.equal(1234.56789); - }); - - it('detects separators heuristically when there is only one and "pasted" mode used e.g. 123456,78', () => { - expect(parseAmount('1.', { mode: 'pasted' })).to.equal(1); - expect(parseAmount('1,', { mode: 'pasted' })).to.equal(1); - expect(parseAmount('1 ', { mode: 'pasted' })).to.equal(1); - - expect(parseAmount('1.2', { mode: 'pasted' })).to.equal(1.2); - expect(parseAmount('1,2', { mode: 'pasted' })).to.equal(1.2); - expect(parseAmount('1 2', { mode: 'pasted' })).to.equal(12); - - expect(parseAmount('1.23', { mode: 'pasted' })).to.equal(1.23); - expect(parseAmount('1,23', { mode: 'pasted' })).to.equal(1.23); - expect(parseAmount('1 23', { mode: 'pasted' })).to.equal(123); - - expect(parseAmount('1 234', { mode: 'pasted' })).to.equal(1234); - - expect(parseAmount('1.2345', { mode: 'pasted' })).to.equal(1.2345); - expect(parseAmount('1,2345', { mode: 'pasted' })).to.equal(1.2345); - expect(parseAmount('1 2345', { mode: 'pasted' })).to.equal(12345); - - expect(parseAmount('1.23456', { mode: 'pasted' })).to.equal(1.23456); - expect(parseAmount('1,23456', { mode: 'pasted' })).to.equal(1.23456); - expect(parseAmount('1 23456', { mode: 'pasted' })).to.equal(123456); - - expect(parseAmount('1.234567', { mode: 'pasted' })).to.equal(1.234567); - expect(parseAmount('1,234567', { mode: 'pasted' })).to.equal(1.234567); - expect(parseAmount('1 234567', { mode: 'pasted' })).to.equal(1234567); - - expect(parseAmount('123456,78', { mode: 'pasted' })).to.equal(123456.78); - expect(parseAmount('123456.78', { mode: 'pasted' })).to.equal(123456.78); - }); - - it('detects separators heuristically when there are 2 same ones e.g. 1.234.56', () => { - expect(parseAmount('1.234.5')).to.equal(1234.5); - expect(parseAmount('1,234,5')).to.equal(1234.5); - - expect(parseAmount('1.234.56')).to.equal(1234.56); - expect(parseAmount('1,234,56')).to.equal(1234.56); - expect(parseAmount('1 234 56')).to.equal(123456); - - expect(parseAmount('1.234.5678')).to.equal(1234.5678); - expect(parseAmount('1,234,5678')).to.equal(1234.5678); - - expect(parseAmount('1.234.56789')).to.equal(1234.56789); - expect(parseAmount('1,234,56789')).to.equal(1234.56789); - }); - - it('uses locale to parse amount if there is only one separator e.g. 1.234', () => { - localize.locale = 'en-GB'; - expect(parseAmount('12.34')).to.equal(12.34); - expect(parseAmount('12,34')).to.equal(1234); - expect(parseAmount('1.234')).to.equal(1.234); - expect(parseAmount('1,234')).to.equal(1234); - - localize.locale = 'nl-NL'; - expect(parseAmount('12.34')).to.equal(1234); - expect(parseAmount('12,34')).to.equal(12.34); - expect(parseAmount('1.234')).to.equal(1234); - expect(parseAmount('1,234')).to.equal(1.234); - }); - - it('returns numbers only if it can not be interpreted e.g. 1.234.567', () => { - // impossible to interpret unambiguously even with locale knowledge - expect(parseAmount('1.234.567')).to.equal(1234567); - expect(parseAmount('1,234,567')).to.equal(1234567); - }); - - it('keeps only last separator for "broken" numbers like 1.23,4', () => { - expect(parseAmount('1.23,4')).to.equal(123.4); - expect(parseAmount('1,23.4')).to.equal(123.4); - expect(parseAmount('1 23,4')).to.equal(123.4); - expect(parseAmount('1 23.4')).to.equal(123.4); - }); - - it('parses negative numbers', () => { - expect(parseAmount('-0')).to.equal(0); - expect(parseAmount('-1')).to.equal(-1); - expect(parseAmount('-1234')).to.equal(-1234); - expect(parseAmount('-1.234,5')).to.equal(-1234.5); - expect(parseAmount('-1,234.5')).to.equal(-1234.5); - expect(parseAmount('-1.234,5678')).to.equal(-1234.5678); - expect(parseAmount('-1,234.5678')).to.equal(-1234.5678); - }); - - it('ignores all non-number symbols (including currency)', () => { - expect(parseAmount('€ 1,234.56')).to.equal(1234.56); - expect(parseAmount('€ -1,234.56')).to.equal(-1234.56); - expect(parseAmount('-€ 1,234.56')).to.equal(-1234.56); - expect(parseAmount('1,234.56 €')).to.equal(1234.56); - expect(parseAmount('-1,234.56 €')).to.equal(-1234.56); - expect(parseAmount('EUR 1,234.56')).to.equal(1234.56); - expect(parseAmount('EUR -1,234.56')).to.equal(-1234.56); - expect(parseAmount('-EUR 1,234.56')).to.equal(-1234.56); - expect(parseAmount('1,234.56 EUR')).to.equal(1234.56); - expect(parseAmount('-1,234.56 EUR')).to.equal(-1234.56); - expect(parseAmount('Number is 1,234.56')).to.equal(1234.56); - }); - - it('ignores non-number characters and returns undefined', () => { - expect(parseAmount('A')).to.equal(undefined); - expect(parseAmount('EUR')).to.equal(undefined); - expect(parseAmount('EU R')).to.equal(undefined); - }); - - it('returns undefined when value is empty string', () => { - expect(parseAmount('')).to.equal(undefined); - }); - - it('parseAmount with locale set and length is more than four', () => { - expect( - parseAmount('6,000', { - locale: 'en-GB', - }), - ).to.equal(6000); - expect( - parseAmount('6.000', { - locale: 'es-ES', - }), - ).to.equal(6000); + it('with no currency keeps all decimals', async () => { + localize.locale = 'en-GB'; + expect(parseAmount('1.015')).to.equal(1.015); + }); }); });