ldml-number
Version:
Formats numbers using the Unicode formatting standard, LDML.
217 lines (185 loc) • 6.8 kB
JavaScript
const formatInt = require( './formatint' );
const makelocale = require( './locale' );
const roundhalfeven = require( './roundhalfeven' );
const splitSign = require( './splitsign' );
const unquote = require( './unquote' );
const padright = require( './padright' );
const reSubpatt = /^((?:'[^']*'|[^';]+)*)(?:;(.*))?$/;
const reChunker = /^((?:'[^']*'|[^0-9@#.,])*)([0-9@#.,E+]+)(.*)$/;
const reNumbits = /^([^E.]*)(?:\.([^E]*))?(?:E(\+?)(.*))?$/;
const e5h = makelocale();
function numfmt ( pattern, locale = 'en' ) {
// resolve default pattern for locale if no pattern was provided
if ( !pattern ) {
pattern = '#,##0.###;-#,##0.###';
}
// localizable things
// be liberal accepting - and _
const [ lang, subLang ] = locale.split( /[_-]/ );
const l10n = numfmt.locale[ locale ] ||
numfmt.locale[ `${ lang }-${ subLang }` ] ||
numfmt.locale[ `${ lang }_${ subLang }` ] ||
numfmt.locale[ lang ] || {};
const cGroup = l10n.thousands_separator || e5h.thousands_separator;
const cDecimal = l10n.decimal_separator || e5h.decimal_separator;
const cPlus = l10n.positive_sign || e5h.positive_sign;
const cMinus = l10n.negative_sign || e5h.negative_sign;
const cExp = l10n.exponent_symbol || e5h.exponent_symbol;
const cInf = l10n.infinity_symbol || e5h.infinity_symbol;
const cNaN = l10n.nan_symbol || e5h.nan_symbol;
const p = function ( n ) {
const isNeg = ( n < 0 ) * 1;
let f = '';
let i = '';
let v;
let e;
n *= p.scale;
// == normal formatting ==
if ( !isFinite( n ) ) {
i = isNaN( n ) ? cNaN : cInf;
}
else if ( p.exponent ) {
v = Math.abs( n );
e = ( v ) ? Math.floor( Math.log( v ) / Math.LN10 ) : 0;
if ( p.int_min === p.int_max ) {
// Minimum number of integer digits
e -= ( p.int_min - 1 );
}
else if ( p.int_max && isFinite( p.int_max ) ) {
// Exponent grouping
e = Math.floor( e / p.int_max ) * p.int_max;
}
v = ( e < 0 ) ? v * Math.pow( 10, -e ) : v / Math.pow( 10, e );
[i, f] = splitSign( v, p.frac_min + p.int_min, p.frac_max + p.int_max, p.pad );
return p.prefix[0] +
( isNeg ? cMinus : '' ) +
( i + ( f ? cDecimal + f : '' ) ) +
cExp +
( ( e < 0 ) ? cMinus : ( p.exp_plus ) ? cPlus : '' ) +
formatInt( Math.abs( e ), p.exp_min, Infinity, p.pad ) +
p.suffix[0];
}
else if ( p.significance ) {
[i, f] = splitSign( n, p.sig_min, p.sig_max, p.pad );
}
else {
if ( p.frac_min === p.frac_max && !p.frac_min ) {
v = Math.round( Math.abs( n ) );
}
else {
v = Math.floor( Math.abs( n ) );
}
if ( p.int_max !== Infinity ) {
// is is possible to add a max digits to non-sci patterns?
// we should parse this as infinite and allow user to set int_max
}
i = formatInt( v, p.int_min, Infinity, p.pad );
if ( n % 1 ) {
// have fraction
f = padright( String( numfmt.round( n, p.frac_max ) ).split( '.' )[ 1 ] || '', p.frac_min, p.pad );
}
else {
// no fraction -- just add some zeros
f = padright( f, p.frac_min, '0' );
}
}
if ( isFinite( n ) && p.grouping ) {
let ret = '';
let ipos = i.length;
const gsize = p.group_sec;
if ( ipos > p.group_pri ) {
ret = cGroup + i.substr( ipos -= p.group_pri, p.group_pri ) + ret;
}
while ( ipos > gsize ) {
ret = cGroup + i.substr( ipos -= gsize, gsize ) + ret;
}
i = ipos ? i.substr( 0, ipos ) + ret : ret;
}
return p.prefix[ isNeg ] + i + ( f ? cDecimal + f : '' ) + p.suffix[ isNeg ];
};
const s = reSubpatt.exec( pattern );
const posBits = reChunker.exec( s[1] );
const number = posBits[2] || '';
const negBits = s[2] ? reChunker.exec( s[2] ) : null;
const numBits = reNumbits.exec( number );
const integer = numBits[1] || '';
const fraction = numBits[2] || '';
p.pattern = pattern;
p.significance = number.indexOf( '@' ) >= 0;
p.exponent = number.indexOf( 'E' ) >= 0;
p.grouping = number.indexOf( ',' ) >= 0;
p.exp_plus = !!numBits[3]; // show exponent positive mark
if ( /\d(?=.*#)/.test( integer ) ) {
throw new Error( `Nonsensical number pattern: ${ integer }` );
}
if ( /#(?=.*\d)/.test( fraction ) ) {
throw new Error( `Nonsensical number pattern: ${ fraction }` );
}
if ( p.exponent && p.grouping ) {
// "Exponential patterns may not contain grouping separators"
throw new Error( 'Exponential patterns must not contain ","' );
}
if ( p.significance && number.indexOf( '.' ) >= 0 ) {
// "If a pattern uses significant digits, it may not contain a decimal separator [...]"
throw new Error( `Significant digit patterns must not contain ".": ${ pattern }` );
}
if ( p.significance && integer.indexOf( '0' ) >= 0 ) {
// "If a pattern uses significant digits, it may not contain [...] the '0' pattern character."
throw new Error( `Significant digit patterns must not contain "0": ${ pattern }` );
}
// parse min/max digit counts
p.int_max = ( !p.exponent && !p.significance ) ? Infinity : integer.replace( /[,]/g, '' ).length;
p.int_min = ( integer.length < 1 ) ? 0 : integer.replace( /[,#]/g, '' ).length || 1;
p.frac_max = fraction.replace( /[,]/g, '' ).length;
p.frac_min = fraction.replace( /[,#]/g, '' ).length;
p.prefix = [
unquote( posBits[1] ),
unquote( negBits ? negBits[1] : '-' + posBits[1] )
];
p.suffix = [
unquote( posBits[3] ),
unquote( negBits ? negBits[3] : posBits[3] )
];
p.pad = '0';
const clean = pattern.replace( /'([^']*)'/g, '' );
p.scale = /%/.test( clean ) ? 100 : 1;
if ( p.significance ) {
const sigBits = /(@+)([^.E]*)/.exec( number );
p.sig_min = sigBits[1].length;
p.sig_max = p.sig_min + sigBits[2].length;
}
else {
p.sig_min = 1;
p.sig_max = Infinity;
}
if ( p.grouping ) {
const s = ( integer || '' ).split( ',' );
const sl = s.length;
if ( sl === 2 ) {
p.group_pri = p.group_sec = s[1].length;
}
else if ( sl > 2 ) {
p.group_pri = s[ sl - 1 ].length;
p.group_sec = s[ sl - 2 ].length;
}
}
else {
p.group_pri = 0;
p.group_sec = 0;
}
if ( p.exponent ) {
// The number of digit characters after the exponent character gives the
// minimum exponent digit count. There is no maximum.
p.exp_min = numBits[4].length;
if ( p.significance ) {
p.int_min = p.int_max = 1;
p.frac_min = p.sig_min;
p.frac_max = p.sig_max;
}
}
return p;
}
// export the interface
numfmt.round = roundhalfeven;
numfmt.locale = makelocale;
module.exports = numfmt;