feat(localize): move parseNumber to @lion/localize
This commit is contained in:
parent
5fd9f1cd8e
commit
9648d418f2
7 changed files with 354 additions and 345 deletions
5
.changeset/dry-clocks-notice.md
Normal file
5
.changeset/dry-clocks-notice.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/localize': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
added a parseNumber function
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { LionInputAmount } from './src/LionInputAmount.js';
|
export { LionInputAmount } from './src/LionInputAmount.js';
|
||||||
export { formatAmount } from './src/formatters.js';
|
export { formatAmount } from './src/formatters.js';
|
||||||
export { parseAmount, parseNumber } from './src/parsers.js';
|
export { parseAmount } from './src/parsers.js';
|
||||||
export { preprocessAmount } from './src/preprocessors.js';
|
export { preprocessAmount } from './src/preprocessors.js';
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
import { getDecimalSeparator, getFractionDigits } from '@lion/localize';
|
import { parseNumber, getFractionDigits } from '@lion/localize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
|
* @typedef {import('@lion/localize/types/LocalizeMixinTypes').FormatNumberOptions} FormatOptions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} value to evaluate
|
|
||||||
* @return {boolean} true if value equal . or ,
|
|
||||||
*/
|
|
||||||
function isDecimalSeparator(value) {
|
|
||||||
return value === '.' || value === ',';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rounding problem can be avoided by using numbers represented in exponential notation
|
* Rounding problem can be avoided by using numbers represented in exponential notation
|
||||||
* @param {number} value to be rounded up
|
* @param {number} value to be rounded up
|
||||||
|
|
@ -26,148 +18,7 @@ function round(value, decimals) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the best possible parsing mode.
|
* Uses `parseNumber()` to parses a number string and returns the best possible javascript number.
|
||||||
*
|
|
||||||
* - If there is only one separator (withLocale)
|
|
||||||
* - 1,23 => xxx1.23 (heuristic)
|
|
||||||
* - else parse mode depends mostly on the last 4 chars
|
|
||||||
* - 1234 => xxx1234 (heuristic)
|
|
||||||
* - [space]123 => xxx123 (heuristic)
|
|
||||||
* - ,123 => unclear
|
|
||||||
* - if 1.000,123 (we find a different separator) => 1000.123 (heuristic)
|
|
||||||
* - if 1,000,123 (we find only same separators) => 1000123 (unparseable)
|
|
||||||
* - if 100,123 (we find no more separators) => unclear
|
|
||||||
* - if en => 100123 (withLocale)
|
|
||||||
* - if nl => 100.123 (withLocale)
|
|
||||||
*
|
|
||||||
* See also {@link parseAmount}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* getParseMode('1.234') => 'withLocale'
|
|
||||||
*
|
|
||||||
* @param {string} value Clean number (only [0-9 ,.]) to be parsed
|
|
||||||
* @return {string} unparseable|withLocale|heuristic
|
|
||||||
*/
|
|
||||||
function getParseMode(value, { mode = 'auto' } = {}) {
|
|
||||||
const separators = value.match(/[., ]/g);
|
|
||||||
|
|
||||||
if (mode === 'auto' && separators && separators.length === 1) {
|
|
||||||
return 'withLocale';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.length > 4) {
|
|
||||||
const charAtLastSeparatorPosition = value[value.length - 4];
|
|
||||||
if (isDecimalSeparator(charAtLastSeparatorPosition)) {
|
|
||||||
const firstPart = value.substring(0, value.length - 4);
|
|
||||||
const otherSeparators = firstPart.match(/[., ]/g);
|
|
||||||
if (otherSeparators) {
|
|
||||||
const lastSeparator = charAtLastSeparatorPosition;
|
|
||||||
return otherSeparators.indexOf(lastSeparator) === -1 ? 'heuristic' : 'unparseable';
|
|
||||||
}
|
|
||||||
return 'withLocale';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'heuristic';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses numbers by considering the locale.
|
|
||||||
* Useful for numbers with an ending pair of 3 number chars as in this case you can not be
|
|
||||||
* certain if it is a group or comma separator. e.g. 1.234; 1,234; 1234.567;
|
|
||||||
* Taking into consideration the locale we make the best possible assumption.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* parseWithLocale('1.234', { locale: 'en-GB' }) => 1.234
|
|
||||||
* parseWithLocale('1,234', { locale: 'en-GB' }) => 1234
|
|
||||||
*
|
|
||||||
* @param {string} value Number to be parsed
|
|
||||||
* @param {Object} options Locale Options
|
|
||||||
* @param {string} [options.locale]
|
|
||||||
*/
|
|
||||||
function parseWithLocale(value, options) {
|
|
||||||
const locale = options && options.locale ? options.locale : undefined;
|
|
||||||
const separator = getDecimalSeparator(locale);
|
|
||||||
const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
|
|
||||||
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
|
|
||||||
if (separator === ',') {
|
|
||||||
numberAndLocaleSeparator = numberAndLocaleSeparator?.replace(',', '.');
|
|
||||||
}
|
|
||||||
if (!numberAndLocaleSeparator) {
|
|
||||||
return NaN;
|
|
||||||
}
|
|
||||||
return parseFloat(numberAndLocaleSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses numbers by considering all separators.
|
|
||||||
* It only keeps the last separator and uses it as decimal separator.
|
|
||||||
*
|
|
||||||
* Warning: This function works only with numbers that can be heuristically parsed.
|
|
||||||
*
|
|
||||||
* @param {string} value Number that can be heuristically parsed
|
|
||||||
* @return {number} parsed javascript number
|
|
||||||
*/
|
|
||||||
function parseHeuristic(value) {
|
|
||||||
if (value.match(/[0-9., ]/g)) {
|
|
||||||
// 1. put placeholder at decimal separator
|
|
||||||
const numberString = value
|
|
||||||
.replace(/(,|\.)([^,|.]*)$/g, '_decSep_$2')
|
|
||||||
.replace(/(,|\.| )/g, '') // 2. remove all thousand separators
|
|
||||||
.replace(/_decSep_/, '.'); // 3. restore decimal separator
|
|
||||||
return parseFloat(numberString);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a number string and returns the best possible javascript number.
|
|
||||||
* For edge cases it may use locale to give the best possible assumption.
|
|
||||||
*
|
|
||||||
* It has 3 "methods" of returning numbers
|
|
||||||
* - 'unparseable': becomes just numbers
|
|
||||||
* - 'withLocale': result depends on given or global locale
|
|
||||||
* - 'heuristic': result depends on considering separators
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* 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 parseNumber(value, options) {
|
|
||||||
const containsNumbers = value.match(/\d/g);
|
|
||||||
if (!containsNumbers) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const matchedInput = value.match(/[0-9,.\- ]/g);
|
|
||||||
if (!matchedInput) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const cleanedInput = matchedInput.join('');
|
|
||||||
const parseMode = getParseMode(cleanedInput, options);
|
|
||||||
switch (parseMode) {
|
|
||||||
case 'unparseable': {
|
|
||||||
const cleanedInputMatchStr = cleanedInput.match(/[0-9]/g)?.join('');
|
|
||||||
if (!cleanedInputMatchStr) {
|
|
||||||
return NaN;
|
|
||||||
}
|
|
||||||
return parseFloat(cleanedInputMatchStr);
|
|
||||||
}
|
|
||||||
case 'withLocale':
|
|
||||||
return parseWithLocale(cleanedInput, options || {});
|
|
||||||
case 'heuristic':
|
|
||||||
return parseHeuristic(cleanedInput);
|
|
||||||
default:
|
|
||||||
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.
|
* Rounds up the number with the correct amount of decimals according to the currency.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
|
|
|
||||||
|
|
@ -1,198 +1,35 @@
|
||||||
import { expect } from '@open-wc/testing';
|
import { expect } from '@open-wc/testing';
|
||||||
import { localize } from '@lion/localize';
|
import { localize } from '@lion/localize';
|
||||||
|
|
||||||
import { parseAmount, parseNumber } from '../src/parsers.js';
|
import { parseAmount } from '../src/parsers.js';
|
||||||
|
|
||||||
describe('parsers', () => {
|
describe('parseAmount()', async () => {
|
||||||
describe('parseNumber()', () => {
|
it('with currency set to correct amount of decimals', async () => {
|
||||||
it('parses integers', () => {
|
localize.locale = 'en-GB';
|
||||||
expect(parseNumber('1')).to.equal(1);
|
expect(
|
||||||
expect(parseNumber('12')).to.equal(12);
|
parseAmount('1.015', {
|
||||||
expect(parseNumber('123')).to.equal(123);
|
currency: 'EUR',
|
||||||
expect(parseNumber('1234')).to.equal(1234);
|
}),
|
||||||
expect(parseNumber('12345')).to.equal(12345);
|
).to.equal(1.02);
|
||||||
expect(parseNumber('123456')).to.equal(123456);
|
expect(
|
||||||
expect(parseNumber('1234567')).to.equal(1234567);
|
parseAmount('5.555', {
|
||||||
expect(parseNumber('12345678')).to.equal(12345678);
|
currency: 'EUR',
|
||||||
expect(parseNumber('123456789')).to.equal(123456789);
|
}),
|
||||||
});
|
).to.equal(5.56);
|
||||||
|
expect(
|
||||||
it('detects separators heuristically when there are 2 different ones e.g. 1,234.5', () => {
|
parseAmount('100.1235', {
|
||||||
expect(parseNumber('1,234.5')).to.equal(1234.5);
|
currency: 'JPY',
|
||||||
expect(parseNumber('1.234,5')).to.equal(1234.5);
|
}),
|
||||||
expect(parseNumber('1 234.5')).to.equal(1234.5);
|
).to.equal(100);
|
||||||
expect(parseNumber('1 234,5')).to.equal(1234.5);
|
expect(
|
||||||
|
parseAmount('100.1235', {
|
||||||
expect(parseNumber('1,234.56')).to.equal(1234.56);
|
currency: 'JOD',
|
||||||
expect(parseNumber('1.234,56')).to.equal(1234.56);
|
}),
|
||||||
expect(parseNumber('1 234.56')).to.equal(1234.56);
|
).to.equal(100.124);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseAmount()', async () => {
|
it('with no currency keeps all decimals', async () => {
|
||||||
it('with currency set to correct amount of decimals', async () => {
|
localize.locale = 'en-GB';
|
||||||
localize.locale = 'en-GB';
|
expect(parseAmount('1.015')).to.equal(1.015);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ export { getMonthNames } from './src/date/getMonthNames.js';
|
||||||
export { getWeekdayNames } from './src/date/getWeekdayNames.js';
|
export { getWeekdayNames } from './src/date/getWeekdayNames.js';
|
||||||
export { normalizeDateTime } from './src/date/normalizeDateTime.js';
|
export { normalizeDateTime } from './src/date/normalizeDateTime.js';
|
||||||
export { parseDate } from './src/date/parseDate.js';
|
export { parseDate } from './src/date/parseDate.js';
|
||||||
|
export { localize, setLocalize } from './src/localize.js';
|
||||||
|
export { LocalizeManager } from './src/LocalizeManager.js';
|
||||||
|
export { LocalizeMixin } from './src/LocalizeMixin.js';
|
||||||
export { formatNumber } from './src/number/formatNumber.js';
|
export { formatNumber } from './src/number/formatNumber.js';
|
||||||
export { formatNumberToParts } from './src/number/formatNumberToParts.js';
|
export { formatNumberToParts } from './src/number/formatNumberToParts.js';
|
||||||
export { getCurrencyName } from './src/number/getCurrencyName.js';
|
export { getCurrencyName } from './src/number/getCurrencyName.js';
|
||||||
export { getDecimalSeparator } from './src/number/getDecimalSeparator.js';
|
export { getDecimalSeparator } from './src/number/getDecimalSeparator.js';
|
||||||
export { getFractionDigits } from './src/number/getFractionDigits.js';
|
export { getFractionDigits } from './src/number/getFractionDigits.js';
|
||||||
export { getGroupSeparator } from './src/number/getGroupSeparator.js';
|
export { getGroupSeparator } from './src/number/getGroupSeparator.js';
|
||||||
export { localize, setLocalize } from './src/localize.js';
|
|
||||||
export { LocalizeManager } from './src/LocalizeManager.js';
|
|
||||||
export { LocalizeMixin } from './src/LocalizeMixin.js';
|
|
||||||
export { normalizeCurrencyLabel } from './src/number/normalizeCurrencyLabel.js';
|
export { normalizeCurrencyLabel } from './src/number/normalizeCurrencyLabel.js';
|
||||||
|
export { parseNumber } from './src/number/parseNumber.js';
|
||||||
|
|
|
||||||
150
packages/localize/src/number/parseNumber.js
Normal file
150
packages/localize/src/number/parseNumber.js
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { getDecimalSeparator } from './getDecimalSeparator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value to evaluate
|
||||||
|
* @return {boolean} true if value equal . or ,
|
||||||
|
*/
|
||||||
|
function isDecimalSeparator(value) {
|
||||||
|
return value === '.' || value === ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the best possible parsing mode.
|
||||||
|
*
|
||||||
|
* - If there is only one separator (withLocale)
|
||||||
|
* - 1,23 => xxx1.23 (heuristic)
|
||||||
|
* - else parse mode depends mostly on the last 4 chars
|
||||||
|
* - 1234 => xxx1234 (heuristic)
|
||||||
|
* - [space]123 => xxx123 (heuristic)
|
||||||
|
* - ,123 => unclear
|
||||||
|
* - if 1.000,123 (we find a different separator) => 1000.123 (heuristic)
|
||||||
|
* - if 1,000,123 (we find only same separators) => 1000123 (unparseable)
|
||||||
|
* - if 100,123 (we find no more separators) => unclear
|
||||||
|
* - if en => 100123 (withLocale)
|
||||||
|
* - if nl => 100.123 (withLocale)
|
||||||
|
*
|
||||||
|
* See also {@link parseAmount}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getParseMode('1.234') => 'withLocale'
|
||||||
|
*
|
||||||
|
* @param {string} value Clean number (only [0-9 ,.]) to be parsed
|
||||||
|
* @return {string} unparseable|withLocale|heuristic
|
||||||
|
*/
|
||||||
|
function getParseMode(value, { mode = 'auto' } = {}) {
|
||||||
|
const separators = value.match(/[., ]/g);
|
||||||
|
|
||||||
|
if (mode === 'auto' && separators && separators.length === 1) {
|
||||||
|
return 'withLocale';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length > 4) {
|
||||||
|
const charAtLastSeparatorPosition = value[value.length - 4];
|
||||||
|
if (isDecimalSeparator(charAtLastSeparatorPosition)) {
|
||||||
|
const firstPart = value.substring(0, value.length - 4);
|
||||||
|
const otherSeparators = firstPart.match(/[., ]/g);
|
||||||
|
if (otherSeparators) {
|
||||||
|
const lastSeparator = charAtLastSeparatorPosition;
|
||||||
|
return otherSeparators.indexOf(lastSeparator) === -1 ? 'heuristic' : 'unparseable';
|
||||||
|
}
|
||||||
|
return 'withLocale';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'heuristic';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses numbers by considering the locale.
|
||||||
|
* Useful for numbers with an ending pair of 3 number chars as in this case you can not be
|
||||||
|
* certain if it is a group or comma separator. e.g. 1.234; 1,234; 1234.567;
|
||||||
|
* Taking into consideration the locale we make the best possible assumption.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseWithLocale('1.234', { locale: 'en-GB' }) => 1.234
|
||||||
|
* parseWithLocale('1,234', { locale: 'en-GB' }) => 1234
|
||||||
|
*
|
||||||
|
* @param {string} value Number to be parsed
|
||||||
|
* @param {Object} options Locale Options
|
||||||
|
* @param {string} [options.locale]
|
||||||
|
*/
|
||||||
|
function parseWithLocale(value, options) {
|
||||||
|
const locale = options && options.locale ? options.locale : undefined;
|
||||||
|
const separator = getDecimalSeparator(locale);
|
||||||
|
const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
|
||||||
|
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
|
||||||
|
if (separator === ',') {
|
||||||
|
numberAndLocaleSeparator = numberAndLocaleSeparator?.replace(',', '.');
|
||||||
|
}
|
||||||
|
if (!numberAndLocaleSeparator) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
return parseFloat(numberAndLocaleSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses numbers by considering all separators.
|
||||||
|
* It only keeps the last separator and uses it as decimal separator.
|
||||||
|
*
|
||||||
|
* Warning: This function works only with numbers that can be heuristically parsed.
|
||||||
|
*
|
||||||
|
* @param {string} value Number that can be heuristically parsed
|
||||||
|
* @return {number} parsed javascript number
|
||||||
|
*/
|
||||||
|
function parseHeuristic(value) {
|
||||||
|
if (value.match(/[0-9., ]/g)) {
|
||||||
|
// 1. put placeholder at decimal separator
|
||||||
|
const numberString = value
|
||||||
|
.replace(/(,|\.)([^,|.]*)$/g, '_decSep_$2')
|
||||||
|
.replace(/(,|\.| )/g, '') // 2. remove all thousand separators
|
||||||
|
.replace(/_decSep_/, '.'); // 3. restore decimal separator
|
||||||
|
return parseFloat(numberString);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a number string and returns the best possible javascript number.
|
||||||
|
* For edge cases it may use locale to give the best possible assumption.
|
||||||
|
*
|
||||||
|
* It has 3 "methods" of returning numbers
|
||||||
|
* - 'unparseable': becomes just numbers
|
||||||
|
* - 'withLocale': result depends on given or global locale
|
||||||
|
* - 'heuristic': result depends on considering separators
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 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 parseNumber(value, options) {
|
||||||
|
const containsNumbers = value.match(/\d/g);
|
||||||
|
if (!containsNumbers) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const matchedInput = value.match(/[0-9,.\- ]/g);
|
||||||
|
if (!matchedInput) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const cleanedInput = matchedInput.join('');
|
||||||
|
const parseMode = getParseMode(cleanedInput, options);
|
||||||
|
switch (parseMode) {
|
||||||
|
case 'unparseable': {
|
||||||
|
const cleanedInputMatchStr = cleanedInput.match(/[0-9]/g)?.join('');
|
||||||
|
if (!cleanedInputMatchStr) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
return parseFloat(cleanedInputMatchStr);
|
||||||
|
}
|
||||||
|
case 'withLocale':
|
||||||
|
return parseWithLocale(cleanedInput, options || {});
|
||||||
|
case 'heuristic':
|
||||||
|
return parseHeuristic(cleanedInput);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
165
packages/localize/test/number/parseNumber.test.js
Normal file
165
packages/localize/test/number/parseNumber.test.js
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { expect } from '@open-wc/testing';
|
||||||
|
import { localize } from '@lion/localize';
|
||||||
|
|
||||||
|
import { parseNumber } from '../../src/number/parseNumber.js';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue