From 9648d418f27c19fa4a3fd1975eb94d8dbbecee08 Mon Sep 17 00:00:00 2001 From: qa46hx Date: Thu, 15 Jul 2021 09:01:05 +0200 Subject: [PATCH] feat(localize): move parseNumber to @lion/localize --- .changeset/dry-clocks-notice.md | 5 + packages/input-amount/index.js | 2 +- packages/input-amount/src/parsers.js | 153 +----------- packages/input-amount/test/parsers.test.js | 217 +++--------------- packages/localize/index.js | 7 +- packages/localize/src/number/parseNumber.js | 150 ++++++++++++ .../localize/test/number/parseNumber.test.js | 165 +++++++++++++ 7 files changed, 354 insertions(+), 345 deletions(-) create mode 100644 .changeset/dry-clocks-notice.md create mode 100644 packages/localize/src/number/parseNumber.js create mode 100644 packages/localize/test/number/parseNumber.test.js diff --git a/.changeset/dry-clocks-notice.md b/.changeset/dry-clocks-notice.md new file mode 100644 index 000000000..3827d95eb --- /dev/null +++ b/.changeset/dry-clocks-notice.md @@ -0,0 +1,5 @@ +--- +'@lion/localize': minor +--- + +added a parseNumber function diff --git a/packages/input-amount/index.js b/packages/input-amount/index.js index b8a6b1969..49392cafc 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, parseNumber } from './src/parsers.js'; +export { parseAmount } 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 192aa7580..acd219490 100644 --- a/packages/input-amount/src/parsers.js +++ b/packages/input-amount/src/parsers.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 */ -/** - * @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 * @param {number} value to be rounded up @@ -26,148 +18,7 @@ function round(value, decimals) { } /** - * 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; - } -} - -/** - * Uses formatNumber to parses a number string and returns the best possible javascript number. + * Uses `parseNumber()` 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 diff --git a/packages/input-amount/test/parsers.test.js b/packages/input-amount/test/parsers.test.js index 90e5c7f17..2f3c338f2 100644 --- a/packages/input-amount/test/parsers.test.js +++ b/packages/input-amount/test/parsers.test.js @@ -1,198 +1,35 @@ import { expect } from '@open-wc/testing'; import { localize } from '@lion/localize'; -import { parseAmount, parseNumber } from '../src/parsers.js'; +import { parseAmount } from '../src/parsers.js'; -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); - }); +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); }); - 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); - }); + it('with no currency keeps all decimals', async () => { + localize.locale = 'en-GB'; + expect(parseAmount('1.015')).to.equal(1.015); }); }); diff --git a/packages/localize/index.js b/packages/localize/index.js index f4790f07b..18520ebb8 100644 --- a/packages/localize/index.js +++ b/packages/localize/index.js @@ -4,13 +4,14 @@ export { getMonthNames } from './src/date/getMonthNames.js'; export { getWeekdayNames } from './src/date/getWeekdayNames.js'; export { normalizeDateTime } from './src/date/normalizeDateTime.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 { formatNumberToParts } from './src/number/formatNumberToParts.js'; export { getCurrencyName } from './src/number/getCurrencyName.js'; export { getDecimalSeparator } from './src/number/getDecimalSeparator.js'; export { getFractionDigits } from './src/number/getFractionDigits.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 { parseNumber } from './src/number/parseNumber.js'; diff --git a/packages/localize/src/number/parseNumber.js b/packages/localize/src/number/parseNumber.js new file mode 100644 index 000000000..7f882b67f --- /dev/null +++ b/packages/localize/src/number/parseNumber.js @@ -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; + } +} diff --git a/packages/localize/test/number/parseNumber.test.js b/packages/localize/test/number/parseNumber.test.js new file mode 100644 index 000000000..76d850670 --- /dev/null +++ b/packages/localize/test/number/parseNumber.test.js @@ -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); + }); +});