fix(input-amount): round up decimals in parseAmount based on currency
This commit is contained in:
parent
9c8113e304
commit
5fd9f1cd8e
5 changed files with 257 additions and 164 deletions
5
.changeset/curly-bees-drum.md
Normal file
5
.changeset/curly-bees-drum.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/input-amount': patch
|
||||
---
|
||||
|
||||
rounds up parseAmount to correct amount of decimals based on currency
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@ describe('<lion-input-amount>', () => {
|
|||
expect(el.parser).to.equal(parseAmount);
|
||||
});
|
||||
|
||||
it('sets correct amount of decimals', async () => {
|
||||
const el = /** @type {LionInputAmount} */ (
|
||||
await fixture(html`<lion-input-amount .modelValue=${100.123}></lion-input-amount>`)
|
||||
);
|
||||
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(`<lion-input-amount></lion-input-amount>`)
|
||||
|
|
|
|||
|
|
@ -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()', () => {
|
||||
describe('parsers', () => {
|
||||
describe('parseNumber()', () => {
|
||||
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);
|
||||
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(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);
|
||||
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(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(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(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(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(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(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(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);
|
||||
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(parseAmount('1.', { mode: 'pasted' })).to.equal(1);
|
||||
expect(parseAmount('1,', { mode: 'pasted' })).to.equal(1);
|
||||
expect(parseAmount('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 ', { 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(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(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(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(parseAmount('1 234', { mode: 'pasted' })).to.equal(1234);
|
||||
expect(parseNumber('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(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(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(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(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(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(parseAmount('123456,78', { mode: 'pasted' })).to.equal(123456.78);
|
||||
expect(parseAmount('123456.78', { mode: 'pasted' })).to.equal(123456.78);
|
||||
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(parseAmount('1.234.5')).to.equal(1234.5);
|
||||
expect(parseAmount('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(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(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(parseAmount('1.234.5678')).to.equal(1234.5678);
|
||||
expect(parseAmount('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(parseAmount('1.234.56789')).to.equal(1234.56789);
|
||||
expect(parseAmount('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('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);
|
||||
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(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);
|
||||
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(parseAmount('1.234.567')).to.equal(1234567);
|
||||
expect(parseAmount('1,234,567')).to.equal(1234567);
|
||||
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(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);
|
||||
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(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);
|
||||
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(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);
|
||||
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(parseAmount('A')).to.equal(undefined);
|
||||
expect(parseAmount('EUR')).to.equal(undefined);
|
||||
expect(parseAmount('EU R')).to.equal(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(parseAmount('')).to.equal(undefined);
|
||||
expect(parseNumber('')).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('parseAmount with locale set and length is more than four', () => {
|
||||
it('with locale set and length is more than four', () => {
|
||||
expect(
|
||||
parseAmount('6,000', {
|
||||
parseNumber('6,000', {
|
||||
locale: 'en-GB',
|
||||
}),
|
||||
).to.equal(6000);
|
||||
expect(
|
||||
parseAmount('6.000', {
|
||||
parseNumber('6.000', {
|
||||
locale: 'es-ES',
|
||||
}),
|
||||
).to.equal(6000);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('with no currency keeps all decimals', async () => {
|
||||
localize.locale = 'en-GB';
|
||||
expect(parseAmount('1.015')).to.equal(1.015);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue