Merge "mediawiki.language: Implement non-digit-grouping of four-digit numbers"
[lhc/web/wiklou.git] / resources / src / mediawiki.language / mediawiki.language.numbers.js
1 /*
2 * Number-related utilities for mediawiki.language.
3 */
4 ( function ( mw, $ ) {
5 /**
6 * @class mw.language
7 */
8
9 /**
10 * Replicate a string 'n' times.
11 *
12 * @private
13 * @param {string} str The string to replicate
14 * @param {number} num Number of times to replicate the string
15 * @return {string}
16 */
17 function replicate( str, num ) {
18 var buf = [];
19
20 if ( num <= 0 || !str ) {
21 return '';
22 }
23
24 while ( num-- ) {
25 buf.push( str );
26 }
27 return buf.join( '' );
28 }
29
30 /**
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.
34 *
35 * Example: Fill the string to length 10 with '+' characters on the right.
36 *
37 * pad( 'blah', 10, '+', true ); // => 'blah++++++'
38 *
39 * @private
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
44 * @return {string}
45 */
46 function pad( text, size, ch, end ) {
47 var out, padStr;
48
49 if ( !ch ) {
50 ch = '0';
51 }
52
53 out = String( text );
54 padStr = replicate( ch, Math.ceil( ( size - out.length ) / ch.length ) );
55
56 return end ? out + padStr : padStr + out;
57 }
58
59 /**
60 * Apply numeric pattern to absolute value using options. Gives no
61 * consideration to local customs.
62 *
63 * Adapted from dojo/number library with thanks
64 * <http://dojotoolkit.org/reference-guide/1.8/dojo/number.html>
65 *
66 * @private
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
73 * @return {string}
74 */
75 function commafyNumber( value, pattern, options ) {
76 var padLength,
77 patternDigits,
78 index,
79 whole,
80 off,
81 remainder,
82 patternParts = pattern.split( '.' ),
83 maxPlaces = ( patternParts[ 1 ] || [] ).length,
84 valueParts = String( Math.abs( value ) ).split( '.' ),
85 fractional = valueParts[ 1 ] || '',
86 groupSize = 0,
87 groupSize2 = 0,
88 pieces = [];
89
90 options = options || {
91 group: ',',
92 decimal: '.'
93 };
94
95 if ( isNaN( value ) ) {
96 return value;
97 }
98
99 if ( patternParts[ 1 ] ) {
100 // Pad fractional with trailing zeros
101 padLength = ( patternParts[ 1 ] && patternParts[ 1 ].lastIndexOf( '0' ) + 1 );
102
103 if ( padLength > fractional.length ) {
104 valueParts[ 1 ] = pad( fractional, padLength, '0', true );
105 }
106
107 // Truncate fractional
108 if ( maxPlaces < fractional.length ) {
109 valueParts[ 1 ] = fractional.slice( 0, maxPlaces );
110 }
111 } else {
112 if ( valueParts[ 1 ] ) {
113 valueParts.pop();
114 }
115 }
116
117 // Pad whole with leading zeros
118 patternDigits = patternParts[ 0 ].replace( ',', '' );
119
120 padLength = patternDigits.indexOf( '0' );
121
122 if ( padLength !== -1 ) {
123 padLength = patternDigits.length - padLength;
124
125 if ( padLength > valueParts[ 0 ].length ) {
126 valueParts[ 0 ] = pad( valueParts[ 0 ], padLength );
127 }
128
129 // Truncate whole
130 if ( patternDigits.indexOf( '#' ) === -1 ) {
131 valueParts[ 0 ] = valueParts[ 0 ].slice( valueParts[ 0 ].length - padLength );
132 }
133 }
134
135 // Add group separators
136 index = patternParts[ 0 ].lastIndexOf( ',' );
137
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;
144 }
145 }
146
147 if (
148 options.minimumGroupingDigits === null ||
149 valueParts[ 0 ].length >= groupSize + options.minimumGroupingDigits
150 ) {
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 ) : '';
155
156 if ( groupSize2 ) {
157 groupSize = groupSize2;
158 groupSize2 = null;
159 }
160 }
161 valueParts[ 0 ] = pieces.reverse().join( options.group );
162 }
163
164 return valueParts.join( options.decimal );
165 }
166
167 /**
168 * Helper function to flip transformation tables.
169 *
170 * @param {...Object} Transformation tables
171 * @return {Object}
172 */
173 function flipTransform() {
174 var i, key, table, flipped = {};
175
176 // Ensure we strip thousand separators. This might be overwritten.
177 flipped[ ',' ] = '';
178
179 for ( i = 0; i < arguments.length; i++ ) {
180 table = arguments[ i ];
181 for ( key in table ) {
182 if ( table.hasOwnProperty( key ) ) {
183 // The thousand separator should be deleted
184 flipped[ table[ key ] ] = key === ',' ? '' : key;
185 }
186 }
187 }
188
189 return flipped;
190 }
191
192 $.extend( mw.language, {
193
194 /**
195 * Converts a number using #getDigitTransformTable.
196 *
197 * @param {number} num Value to be converted
198 * @param {boolean} [integer=false] Whether to convert the return value to an integer
199 * @return {number|string} Formatted number
200 */
201 convertNumber: function ( num, integer ) {
202 var transformTable, digitTransformTable, separatorTransformTable,
203 i, numberString, convertedNumber, pattern, minimumGroupingDigits;
204
205 // Quick shortcut for plain numbers
206 if ( integer && parseInt( num, 10 ) === num ) {
207 return num;
208 }
209
210 // Load the transformation tables (can be empty)
211 digitTransformTable = mw.language.getDigitTransformTable();
212 separatorTransformTable = mw.language.getSeparatorTransformTable();
213
214 if ( integer ) {
215 // Reverse the digit transformation tables if we are doing unformatting
216 transformTable = flipTransform( separatorTransformTable, digitTransformTable );
217 numberString = String( num );
218 } else {
219 // This check being here means that digits can still be unformatted
220 // even if we do not produce them. This seems sane behavior.
221 if ( mw.config.get( 'wgTranslateNumerals' ) ) {
222 transformTable = digitTransformTable;
223 }
224
225 // Commaying is more complex, so we handle it here separately.
226 // When unformatting, we just use separatorTransformTable.
227 pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
228 'digitGroupingPattern' ) || '#,##0.###';
229 minimumGroupingDigits = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
230 'minimumGroupingDigits' ) || null;
231 numberString = mw.language.commafy( num, pattern, minimumGroupingDigits );
232 }
233
234 if ( transformTable ) {
235 convertedNumber = '';
236 for ( i = 0; i < numberString.length; i++ ) {
237 if ( transformTable.hasOwnProperty( numberString[ i ] ) ) {
238 convertedNumber += transformTable[ numberString[ i ] ];
239 } else {
240 convertedNumber += numberString[ i ];
241 }
242 }
243 } else {
244 convertedNumber = numberString;
245 }
246
247 if ( integer ) {
248 // Parse string to integer. This loses decimals!
249 convertedNumber = parseInt( convertedNumber, 10 );
250 }
251
252 return convertedNumber;
253 },
254
255 /**
256 * Get the digit transform table for current UI language.
257 *
258 * @return {Object|Array}
259 */
260 getDigitTransformTable: function () {
261 return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
262 'digitTransformTable' ) || [];
263 },
264
265 /**
266 * Get the separator transform table for current UI language.
267 *
268 * @return {Object|Array}
269 */
270 getSeparatorTransformTable: function () {
271 return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
272 'separatorTransformTable' ) || [];
273 },
274
275 /**
276 * Apply pattern to format value as a string.
277 *
278 * Using patterns from [Unicode TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns).
279 *
280 * @param {number} value
281 * @param {string} pattern Pattern string as described by Unicode TR35
282 * @param {number|null} [minimumGroupingDigits=null]
283 * @throws {Error} If unable to find a number expression in `pattern`.
284 * @return {string}
285 */
286 commafy: function ( value, pattern, minimumGroupingDigits ) {
287 var numberPattern,
288 transformTable = mw.language.getSeparatorTransformTable(),
289 group = transformTable[ ',' ] || ',',
290 numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
291 decimal = transformTable[ '.' ] || '.',
292 patternList = pattern.split( ';' ),
293 positivePattern = patternList[ 0 ];
294
295 pattern = patternList[ ( value < 0 ) ? 1 : 0 ] || ( '-' + positivePattern );
296 numberPattern = positivePattern.match( numberPatternRE );
297 minimumGroupingDigits = minimumGroupingDigits !== undefined ? minimumGroupingDigits : null;
298
299 if ( !numberPattern ) {
300 throw new Error( 'unable to find a number expression in pattern: ' + pattern );
301 }
302
303 return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
304 minimumGroupingDigits: minimumGroupingDigits,
305 decimal: decimal,
306 group: group
307 } ) );
308 }
309
310 } );
311
312 }( mediaWiki, jQuery ) );