2 * Number-related utilities for mediawiki.language.
7 * Pad a string to guarantee that it is at least `size` length by
8 * filling with the character `ch` at either the start or end of the
9 * string. Pads at the start, by default.
11 * Example: Fill the string to length 10 with '+' characters on the right.
13 * pad( 'blah', 10, '+', true ); // => 'blah++++++'
16 * @param {string} text The string to pad
17 * @param {number} size The length to pad to
18 * @param {string} [ch='0'] Character to pad with
19 * @param {boolean} [end=false] Adds padding at the end if true, otherwise pads at start
22 function pad( text, size, ch, end ) {
27 const out = String( text );
28 const count = Math.ceil( ( size - out.length ) / ch.length );
29 const padStr = ch.repeat( Math.max( 0, count ) );
31 return end ? out + padStr : padStr + out;
35 * Apply numeric pattern to absolute value using options. Gives no
36 * consideration to local customs.
38 * Adapted from dojo/number library with thanks
39 * <http://dojotoolkit.org/reference-guide/1.8/dojo/number.html>
42 * @param {number} value the number to be formatted, ignores sign
43 * @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`)
44 * @param {Object} [options] If provided, all option keys must be present:
45 * @param {string} options.decimal The decimal separator. Defaults to: `'.'`.
46 * @param {string} options.group The group separator. Defaults to: `','`.
47 * @param {number|null} options.minimumGroupingDigits
50 function commafyNumber( value, pattern, options ) {
51 const patternParts = pattern.split( '.' ),
52 maxPlaces = ( patternParts[ 1 ] || [] ).length,
53 valueParts = String( Math.abs( value ) ).split( '.' ),
54 fractional = valueParts[ 1 ] || '',
59 options = options || {
64 if ( isNaN( value ) ) {
69 if ( patternParts[ 1 ] ) {
70 // Pad fractional with trailing zeros
71 padLength = ( patternParts[ 1 ] && patternParts[ 1 ].lastIndexOf( '0' ) + 1 );
73 if ( padLength > fractional.length ) {
74 valueParts[ 1 ] = pad( fractional, padLength, '0', true );
77 // Truncate fractional
78 if ( maxPlaces < fractional.length ) {
79 valueParts[ 1 ] = fractional.slice( 0, maxPlaces );
82 if ( valueParts[ 1 ] ) {
87 // Pad whole with leading zeros
88 const patternDigits = patternParts[ 0 ].replace( ',', '' );
90 padLength = patternDigits.indexOf( '0' );
92 if ( padLength !== -1 ) {
93 padLength = patternDigits.length - padLength;
95 if ( padLength > valueParts[ 0 ].length ) {
96 valueParts[ 0 ] = pad( valueParts[ 0 ], padLength );
100 if ( patternDigits.indexOf( '#' ) === -1 && padLength ) {
101 valueParts[ 0 ] = valueParts[ 0 ].slice( -padLength );
105 // Add group separators
106 let index = patternParts[ 0 ].lastIndexOf( ',' );
108 if ( index !== -1 ) {
109 groupSize = patternParts[ 0 ].length - index - 1;
110 const remainder = patternParts[ 0 ].slice( 0, index );
111 index = remainder.lastIndexOf( ',' );
112 if ( index !== -1 ) {
113 groupSize2 = remainder.length - index - 1;
118 options.minimumGroupingDigits === null ||
119 valueParts[ 0 ].length >= groupSize + options.minimumGroupingDigits
121 for ( let whole = valueParts[ 0 ]; whole; ) {
122 const off = groupSize ? whole.length - groupSize : 0;
123 pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
124 whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
127 groupSize = groupSize2;
131 valueParts[ 0 ] = pieces.reverse().join( options.group );
134 return valueParts.join( options.decimal );
138 * Apply pattern to format value as a string.
140 * Using patterns from [Unicode TR35](https://www.unicode.org/reports/tr35/#Number_Format_Patterns).
142 * @param {number} value
143 * @param {string} pattern Pattern string as described by Unicode TR35
144 * @param {number|null} [minimumGroupingDigits=null]
145 * @throws {Error} If unable to find a number expression in `pattern`.
149 function commafyInternal( value, pattern, minimumGroupingDigits ) {
150 const transformTable = mw.language.getSeparatorTransformTable(),
151 group = transformTable[ ',' ] || ',',
153 numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
154 decimal = transformTable[ '.' ] || '.',
155 patternList = pattern.split( ';' ),
156 positivePattern = patternList[ 0 ];
158 pattern = patternList[ ( value < 0 ) ? 1 : 0 ] || ( '-' + positivePattern );
159 const numberPattern = positivePattern.match( numberPatternRE );
161 minimumGroupingDigits = minimumGroupingDigits !== undefined ? minimumGroupingDigits : null;
163 if ( !numberPattern ) {
164 throw new Error( 'unable to find a number expression in pattern: ' + pattern );
167 return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
168 minimumGroupingDigits: minimumGroupingDigits,
175 * Helper function to flip transformation tables.
177 * @param {...Object} Transformation tables
180 function flipTransform() {
183 // Ensure we strip thousand separators. This might be overwritten.
186 for ( let i = 0; i < arguments.length; i++ ) {
187 const table = arguments[ i ];
188 for ( const key in table ) {
189 // The thousand separator should be deleted
190 flipped[ table[ key ] ] = key === ',' ? '' : key;
197 Object.assign( mw.language, {
200 * Converts a number using `getDigitTransformTable()`.
202 * @memberof mw.language
203 * @param {number} num Value to be converted
204 * @param {boolean} [integer=false] Whether to convert the return value to an integer
205 * @return {number|string} Formatted number
207 convertNumber: function ( num, integer ) {
208 // Quick shortcut for plain numbers
209 if ( integer && parseInt( num, 10 ) === num ) {
213 // Load the transformation tables (can be empty)
214 const digitTransformTable = mw.language.getDigitTransformTable();
215 const separatorTransformTable = mw.language.getSeparatorTransformTable();
217 let transformTable, numberString;
219 // Reverse the digit transformation tables if we are doing unformatting
220 transformTable = flipTransform( separatorTransformTable, digitTransformTable );
221 numberString = String( num );
223 // This check being here means that digits can still be unformatted
224 // even if we do not produce them.
225 if ( mw.config.get( 'wgTranslateNumerals' ) ) {
226 transformTable = digitTransformTable;
229 // Commaying is more complex, so we handle it here separately.
230 // When unformatting, we just use separatorTransformTable.
231 const pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
232 'digitGroupingPattern' ) || '#,##0.###';
233 const minimumGroupingDigits = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
234 'minimumGroupingDigits' ) || null;
235 numberString = commafyInternal( num, pattern, minimumGroupingDigits );
239 if ( transformTable ) {
240 convertedNumber = '';
241 for ( let i = 0; i < numberString.length; i++ ) {
242 if ( Object.prototype.hasOwnProperty.call( transformTable, numberString[ i ] ) ) {
243 convertedNumber += transformTable[ numberString[ i ] ];
245 convertedNumber += numberString[ i ];
249 convertedNumber = numberString;
253 // Parse string to integer. This loses decimals!
254 convertedNumber = parseInt( convertedNumber, 10 );
257 return convertedNumber;
261 * Get the digit transform table for current UI language.
264 * @return {Object|Array}
266 getDigitTransformTable: function () {
267 return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
268 'digitTransformTable' ) || [];
272 * Get the separator transform table for current UI language.
275 * @return {Object|Array}
277 getSeparatorTransformTable: function () {
278 return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
279 'separatorTransformTable' ) || [];