@lion/ui
Version:
A package of extendable web components
162 lines (150 loc) • 6.36 kB
JavaScript
import { emptyStringWhenNumberNan } from './utils/emptyStringWhenNumberNan.js';
import { getSeparatorsFromNumber } from './getSeparatorsFromNumber.js';
import { getDecimalSeparator } from './getDecimalSeparator.js';
import { getGroupSeparator } from './getGroupSeparator.js';
import { getLocale } from '../utils/getLocale.js';
import { normalizeIntl } from './utils/normalize-format-number-to-parts/normalizeIntl.js';
import { normalSpaces } from './utils/normalSpaces.js';
/**
* Round the number based on the options
*
* @param {number} number
* @param {string} roundMode
* @throws {Error} roundMode can only be round|floor|ceiling
* @returns {number}
*/
export function roundNumber(number, roundMode) {
switch (roundMode) {
case 'floor':
return Math.floor(number);
case 'ceiling':
return Math.ceil(number);
case 'round':
return Math.round(number);
default:
throw new Error('roundMode can only be round|floor|ceiling');
}
}
/**
* Splits a number up in parts for integer, fraction, group, literal, decimal and currency.
*
* @typedef {import('../../types/LocalizeMixinTypes.js').FormatNumberPart} FormatNumberPart
* @param {number} number Number to split up
* @param {import('../../types/LocalizeMixinTypes.js').FormatNumberOptions} [options] Intl options are available extended by roundMode,returnIfNaN
* @returns {string | FormatNumberPart[]} Array with parts or (an empty string or returnIfNaN if not a number)
*/
export function formatNumberToParts(number, options = {}) {
let parsedNumber = typeof number === 'string' ? parseFloat(number) : number;
const computedLocale = getLocale(options && options.locale);
// when parsedNumber is not a number we should return an empty string or returnIfNaN
if (Number.isNaN(parsedNumber)) {
return emptyStringWhenNumberNan(options && options.returnIfNaN);
}
// If roundMode is given the number is rounded based upon the mode
if (options && options.roundMode) {
parsedNumber = roundNumber(number, options.roundMode);
}
let formattedParts = [];
const formattedNumber = Intl.NumberFormat(computedLocale, options).format(parsedNumber);
const { decimalSeparator, groupSeparator } = getSeparatorsFromNumber(
parsedNumber,
formattedNumber,
options,
);
// eslint-disable-next-line no-irregular-whitespace
const regexCurrency = /[.,\s0-9 _ ]/;
const regexMinusSign = /[-]/; // U+002D, Hyphen-Minus, -
const regexNum = /[0-9]/;
const regexSpace = /[\s]/;
let currency = '';
let numberPart = '';
let fraction = false;
let isGroup = false;
const group = getGroupSeparator(computedLocale, options);
const decimal = getDecimalSeparator(computedLocale, options);
if (decimalSeparator && groupSeparator && group === decimal) {
throw new Error(`Decimal and group (thousand) separator are the same character: '${group}'.
This can happen due to both props being specified as the same, or one of the props being the same as the other one from default locale.
Please specify .groupSeparator / .decimalSeparator on the formatOptions object to be different.`);
}
for (let i = 0; i < formattedNumber.length; i += 1) {
// detect minusSign
if (regexMinusSign.test(formattedNumber[i])) {
formattedParts.push({ type: 'minusSign', value: '−' /* U+2212, 'Minus-Sign', − */ });
}
// detect numbers
if (regexNum.test(formattedNumber[i])) {
numberPart += formattedNumber[i];
}
// detect currency (symbol or code)
if (!regexCurrency.test(formattedNumber[i]) && !regexMinusSign.test(formattedNumber[i])) {
currency += formattedNumber[i];
}
// push when another character then currency
if (regexCurrency.test(formattedNumber[i]) && currency) {
formattedParts.push({ type: 'currency', value: currency });
currency = '';
}
// group sep must be lead by / followed by a number
if (
formattedNumber[i] === groupSeparator &&
formattedNumber[i - 1].match(regexNum) &&
formattedNumber[i + 1].match(regexNum)
) {
// Write number grouping
if (numberPart) {
formattedParts.push({ type: 'integer', value: numberPart });
numberPart = '';
}
formattedParts.push({ type: 'group', value: group });
isGroup = true;
}
if (formattedNumber[i] === decimalSeparator) {
// Write number grouping
if (numberPart) {
formattedParts.push({ type: 'integer', value: numberPart });
numberPart = '';
}
formattedParts.push({ type: 'decimal', value: decimal });
fraction = true;
}
// detect literals (empty spaces) or space group separator
if (regexSpace.test(formattedNumber[i])) {
const hasNumberPart = !!numberPart;
// Write number grouping
if (numberPart && !fraction) {
formattedParts.push({ type: 'integer', value: numberPart });
numberPart = '';
} else if (numberPart) {
formattedParts.push({ type: 'fraction', value: numberPart });
numberPart = '';
}
// If space equals the group separator it gets type group
if (normalSpaces(formattedNumber[i]) === group && hasNumberPart && !fraction) {
formattedParts.push({ type: 'group', value: formattedNumber[i] });
// if we already pushed it as a group separator, don't add it as a literal on top..
} else if (!isGroup) {
formattedParts.push({ type: 'literal', value: formattedNumber[i] });
}
}
isGroup = false;
// Numbers after the decimal sign are fractions, write the last
// fractions at the end of the number
if (fraction === true && i === formattedNumber.length - 1) {
// write last number part
if (numberPart) {
formattedParts.push({ type: 'fraction', value: numberPart });
}
// If there are no fractions but we reached the end write the number part as integer
} else if (i === formattedNumber.length - 1 && numberPart) {
formattedParts.push({ type: 'integer', value: numberPart });
}
// push currency on end of loop
if (i === formattedNumber.length - 1 && currency) {
formattedParts.push({ type: 'currency', value: currency });
currency = '';
}
}
formattedParts = normalizeIntl(formattedParts, options, computedLocale);
return formattedParts;
}