fix(input-amount): round up decimals in parseAmount based on currency

This commit is contained in:
qa46hx 2021-07-13 12:17:49 +02:00
parent 9c8113e304
commit 5fd9f1cd8e
5 changed files with 257 additions and 164 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/input-amount': patch
---
rounds up parseAmount to correct amount of decimals based on currency

View file

@ -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';

View file

@ -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);
}

View file

@ -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>`)

View file

@ -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);
});
});
});