2 * Number-related utilities for mediawiki.language.
10 * Replicate a string 'n' times.
13 * @param {string} str The string to replicate
14 * @param {number} num Number of times to replicate the string
17 function replicate( str
, num
) {
20 if ( num
<= 0 || !str
) {
27 return buf
.join( '' );
31 * Pad a string to guarantee that it is at least `size` length by
32 * filling with the character `ch` at either the start or end of the
33 * string. Pads at the start, by default.
35 * Example: Fill the string to length 10 with '+' characters on the right.
37 * pad( 'blah', 10, '+', true ); // => 'blah++++++'
40 * @param {string} text The string to pad
41 * @param {number} size The length to pad to
42 * @param {string} [ch='0'] Character to pad with
43 * @param {boolean} [end=false] Adds padding at the end if true, otherwise pads at start
46 function pad( text
, size
, ch
, end
) {
54 padStr
= replicate( ch
, Math
.ceil( ( size
- out
.length
) / ch
.length
) );
56 return end
? out
+ padStr
: padStr
+ out
;
60 * Apply numeric pattern to absolute value using options. Gives no
61 * consideration to local customs.
63 * Adapted from dojo/number library with thanks
64 * <http://dojotoolkit.org/reference-guide/1.8/dojo/number.html>
67 * @param {number} value the number to be formatted, ignores sign
68 * @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`)
69 * @param {Object} [options] If provided, all option keys must be present:
70 * @param {string} options.decimal The decimal separator. Defaults to: `'.'`.
71 * @param {string} options.group The group separator. Defaults to: `','`.
72 * @param {number|null} options.minimumGroupingDigits
75 function commafyNumber( value
, pattern
, options
) {
82 patternParts
= pattern
.split( '.' ),
83 maxPlaces
= ( patternParts
[ 1 ] || [] ).length
,
84 valueParts
= String( Math
.abs( value
) ).split( '.' ),
85 fractional
= valueParts
[ 1 ] || '',
90 options
= options
|| {
95 if ( isNaN( value
) ) {
99 if ( patternParts
[ 1 ] ) {
100 // Pad fractional with trailing zeros
101 padLength
= ( patternParts
[ 1 ] && patternParts
[ 1 ].lastIndexOf( '0' ) + 1 );
103 if ( padLength
> fractional
.length
) {
104 valueParts
[ 1 ] = pad( fractional
, padLength
, '0', true );
107 // Truncate fractional
108 if ( maxPlaces
< fractional
.length
) {
109 valueParts
[ 1 ] = fractional
.slice( 0, maxPlaces
);
112 if ( valueParts
[ 1 ] ) {
117 // Pad whole with leading zeros
118 patternDigits
= patternParts
[ 0 ].replace( ',', '' );
120 padLength
= patternDigits
.indexOf( '0' );
122 if ( padLength
!== -1 ) {
123 padLength
= patternDigits
.length
- padLength
;
125 if ( padLength
> valueParts
[ 0 ].length
) {
126 valueParts
[ 0 ] = pad( valueParts
[ 0 ], padLength
);
130 if ( patternDigits
.indexOf( '#' ) === -1 ) {
131 valueParts
[ 0 ] = valueParts
[ 0 ].slice( valueParts
[ 0 ].length
- padLength
);
135 // Add group separators
136 index
= patternParts
[ 0 ].lastIndexOf( ',' );
138 if ( index
!== -1 ) {
139 groupSize
= patternParts
[ 0 ].length
- index
- 1;
140 remainder
= patternParts
[ 0 ].slice( 0, index
);
141 index
= remainder
.lastIndexOf( ',' );
142 if ( index
!== -1 ) {
143 groupSize2
= remainder
.length
- index
- 1;
148 options
.minimumGroupingDigits
=== null ||
149 valueParts
[ 0 ].length
>= groupSize
+ options
.minimumGroupingDigits
151 for ( whole
= valueParts
[ 0 ]; whole
; ) {
152 off
= groupSize
? whole
.length
- groupSize
: 0;
153 pieces
.push( ( off
> 0 ) ? whole
.slice( off
) : whole
);
154 whole
= ( off
> 0 ) ? whole
.slice( 0, off
) : '';
157 groupSize
= groupSize2
;
161 valueParts
[ 0 ] = pieces
.reverse().join( options
.group
);
164 return valueParts
.join( options
.decimal );
168 * Helper function to flip transformation tables.
170 * @param {...Object} Transformation tables
173 function flipTransform() {
174 var i
, key
, table
, flipped
= {};
176 // Ensure we strip thousand separators. This might be overwritten.
179 for ( i
= 0; i
< arguments
.length
; i
++ ) {
180 table
= arguments
[ i
];
181 for ( key
in table
) {
182 // The thousand separator should be deleted
183 flipped
[ table
[ key
] ] = key
=== ',' ? '' : key
;
190 $.extend( mw
.language
, {
193 * Converts a number using #getDigitTransformTable.
195 * @param {number} num Value to be converted
196 * @param {boolean} [integer=false] Whether to convert the return value to an integer
197 * @return {number|string} Formatted number
199 convertNumber: function ( num
, integer
) {
200 var transformTable
, digitTransformTable
, separatorTransformTable
,
201 i
, numberString
, convertedNumber
, pattern
, minimumGroupingDigits
;
203 // Quick shortcut for plain numbers
204 if ( integer
&& parseInt( num
, 10 ) === num
) {
208 // Load the transformation tables (can be empty)
209 digitTransformTable
= mw
.language
.getDigitTransformTable();
210 separatorTransformTable
= mw
.language
.getSeparatorTransformTable();
213 // Reverse the digit transformation tables if we are doing unformatting
214 transformTable
= flipTransform( separatorTransformTable
, digitTransformTable
);
215 numberString
= String( num
);
217 // This check being here means that digits can still be unformatted
218 // even if we do not produce them. This seems sane behavior.
219 if ( mw
.config
.get( 'wgTranslateNumerals' ) ) {
220 transformTable
= digitTransformTable
;
223 // Commaying is more complex, so we handle it here separately.
224 // When unformatting, we just use separatorTransformTable.
225 pattern
= mw
.language
.getData( mw
.config
.get( 'wgUserLanguage' ),
226 'digitGroupingPattern' ) || '#,##0.###';
227 minimumGroupingDigits
= mw
.language
.getData( mw
.config
.get( 'wgUserLanguage' ),
228 'minimumGroupingDigits' ) || null;
229 numberString
= mw
.language
.commafy( num
, pattern
, minimumGroupingDigits
);
232 if ( transformTable
) {
233 convertedNumber
= '';
234 for ( i
= 0; i
< numberString
.length
; i
++ ) {
235 if ( Object
.prototype.hasOwnProperty
.call( transformTable
, numberString
[ i
] ) ) {
236 convertedNumber
+= transformTable
[ numberString
[ i
] ];
238 convertedNumber
+= numberString
[ i
];
242 convertedNumber
= numberString
;
246 // Parse string to integer. This loses decimals!
247 convertedNumber
= parseInt( convertedNumber
, 10 );
250 return convertedNumber
;
254 * Get the digit transform table for current UI language.
256 * @return {Object|Array}
258 getDigitTransformTable: function () {
259 return mw
.language
.getData( mw
.config
.get( 'wgUserLanguage' ),
260 'digitTransformTable' ) || [];
264 * Get the separator transform table for current UI language.
266 * @return {Object|Array}
268 getSeparatorTransformTable: function () {
269 return mw
.language
.getData( mw
.config
.get( 'wgUserLanguage' ),
270 'separatorTransformTable' ) || [];
274 * Apply pattern to format value as a string.
276 * Using patterns from [Unicode TR35](https://www.unicode.org/reports/tr35/#Number_Format_Patterns).
278 * @param {number} value
279 * @param {string} pattern Pattern string as described by Unicode TR35
280 * @param {number|null} [minimumGroupingDigits=null]
281 * @throws {Error} If unable to find a number expression in `pattern`.
284 commafy: function ( value
, pattern
, minimumGroupingDigits
) {
286 transformTable
= mw
.language
.getSeparatorTransformTable(),
287 group
= transformTable
[ ',' ] || ',',
288 numberPatternRE
= /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
289 decimal = transformTable
[ '.' ] || '.',
290 patternList
= pattern
.split( ';' ),
291 positivePattern
= patternList
[ 0 ];
293 pattern
= patternList
[ ( value
< 0 ) ? 1 : 0 ] || ( '-' + positivePattern
);
294 numberPattern
= positivePattern
.match( numberPatternRE
);
295 minimumGroupingDigits
= minimumGroupingDigits
!== undefined ? minimumGroupingDigits
: null;
297 if ( !numberPattern
) {
298 throw new Error( 'unable to find a number expression in pattern: ' + pattern
);
301 return pattern
.replace( numberPatternRE
, commafyNumber( value
, numberPattern
[ 0 ], {
302 minimumGroupingDigits
: minimumGroupingDigits
,