Added release notes for 'ContentHandler::runLegacyHooks' removal
[mediawiki.git] / resources / src / mediawiki.language / mediawiki.language.numbers.js
blob83277cb00f3a8c97eae88d3c9ea19da9dc0a2d2c
1 /*
2  * Number-related utilities for mediawiki.language.
3  */
4 ( function ( mw, $ ) {
5         /**
6          * @class mw.language
7          */
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 = [];
20                 if ( num <= 0 || !str ) {
21                         return '';
22                 }
24                 while ( num-- ) {
25                         buf.push( str );
26                 }
27                 return buf.join( '' );
28         }
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;
49                 if ( !ch ) {
50                         ch = '0';
51                 }
53                 out = String( text );
54                 padStr = replicate( ch, Math.ceil( ( size - out.length ) / ch.length ) );
56                 return end ? out + padStr : padStr + out;
57         }
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, both 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          * @return {string}
73          */
74         function commafyNumber( value, pattern, options ) {
75                 var padLength,
76                         patternDigits,
77                         index,
78                         whole,
79                         off,
80                         remainder,
81                         patternParts = pattern.split( '.' ),
82                         maxPlaces = ( patternParts[ 1 ] || [] ).length,
83                         valueParts = String( Math.abs( value ) ).split( '.' ),
84                         fractional = valueParts[ 1 ] || '',
85                         groupSize = 0,
86                         groupSize2 = 0,
87                         pieces = [];
89                 options = options || {
90                         group: ',',
91                         decimal: '.'
92                 };
94                 if ( isNaN( value ) ) {
95                         return value;
96                 }
98                 if ( patternParts[ 1 ] ) {
99                         // Pad fractional with trailing zeros
100                         padLength = ( patternParts[ 1 ] && patternParts[ 1 ].lastIndexOf( '0' ) + 1 );
102                         if ( padLength > fractional.length ) {
103                                 valueParts[ 1 ] = pad( fractional, padLength, '0', true );
104                         }
106                         // Truncate fractional
107                         if ( maxPlaces < fractional.length ) {
108                                 valueParts[ 1 ] = fractional.slice( 0, maxPlaces );
109                         }
110                 } else {
111                         if ( valueParts[ 1 ] ) {
112                                 valueParts.pop();
113                         }
114                 }
116                 // Pad whole with leading zeros
117                 patternDigits = patternParts[ 0 ].replace( ',', '' );
119                 padLength = patternDigits.indexOf( '0' );
121                 if ( padLength !== -1 ) {
122                         padLength = patternDigits.length - padLength;
124                         if ( padLength > valueParts[ 0 ].length ) {
125                                 valueParts[ 0 ] = pad( valueParts[ 0 ], padLength );
126                         }
128                         // Truncate whole
129                         if ( patternDigits.indexOf( '#' ) === -1 ) {
130                                 valueParts[ 0 ] = valueParts[ 0 ].slice( valueParts[ 0 ].length - padLength );
131                         }
132                 }
134                 // Add group separators
135                 index = patternParts[ 0 ].lastIndexOf( ',' );
137                 if ( index !== -1 ) {
138                         groupSize = patternParts[ 0 ].length - index - 1;
139                         remainder = patternParts[ 0 ].slice( 0, index );
140                         index = remainder.lastIndexOf( ',' );
141                         if ( index !== -1 ) {
142                                 groupSize2 = remainder.length - index - 1;
143                         }
144                 }
146                 for ( whole = valueParts[ 0 ]; whole; ) {
147                         off = groupSize ? whole.length - groupSize : 0;
148                         pieces.push( ( off > 0 ) ? whole.slice( off ) : whole );
149                         whole = ( off > 0 ) ? whole.slice( 0, off ) : '';
151                         if ( groupSize2 ) {
152                                 groupSize = groupSize2;
153                                 groupSize2 = null;
154                         }
155                 }
156                 valueParts[ 0 ] = pieces.reverse().join( options.group );
158                 return valueParts.join( options.decimal );
159         }
161         /**
162          * Helper function to flip transformation tables.
163          *
164          * @param {...Object} Transformation tables
165          * @return {Object}
166          */
167         function flipTransform() {
168                 var i, key, table, flipped = {};
170                 // Ensure we strip thousand separators. This might be overwritten.
171                 flipped[ ',' ] = '';
173                 for ( i = 0; i < arguments.length; i++ ) {
174                         table = arguments[ i ];
175                         for ( key in table ) {
176                                 if ( table.hasOwnProperty( key ) ) {
177                                         // The thousand separator should be deleted
178                                         flipped[ table[ key ] ] = key === ',' ? '' : key;
179                                 }
180                         }
181                 }
183                 return flipped;
184         }
186         $.extend( mw.language, {
188                 /**
189                  * Converts a number using #getDigitTransformTable.
190                  *
191                  * @param {number} num Value to be converted
192                  * @param {boolean} [integer=false] Whether to convert the return value to an integer
193                  * @return {number|string} Formatted number
194                  */
195                 convertNumber: function ( num, integer ) {
196                         var transformTable, digitTransformTable, separatorTransformTable,
197                                 i, numberString, convertedNumber, pattern;
199                         // Quick shortcut for plain numbers
200                         if ( integer && parseInt( num, 10 ) === num ) {
201                                 return num;
202                         }
204                         // Load the transformation tables (can be empty)
205                         digitTransformTable = mw.language.getDigitTransformTable();
206                         separatorTransformTable = mw.language.getSeparatorTransformTable();
208                         if ( integer ) {
209                                 // Reverse the digit transformation tables if we are doing unformatting
210                                 transformTable = flipTransform( separatorTransformTable, digitTransformTable );
211                                 numberString = String( num );
212                         } else {
213                                 // This check being here means that digits can still be unformatted
214                                 // even if we do not produce them. This seems sane behavior.
215                                 if ( mw.config.get( 'wgTranslateNumerals' ) ) {
216                                         transformTable = digitTransformTable;
217                                 }
219                                 // Commaying is more complex, so we handle it here separately.
220                                 // When unformatting, we just use separatorTransformTable.
221                                 pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ),
222                                         'digitGroupingPattern' ) || '#,##0.###';
223                                 numberString = mw.language.commafy( num, pattern );
224                         }
226                         if ( transformTable ) {
227                                 convertedNumber = '';
228                                 for ( i = 0; i < numberString.length; i++ ) {
229                                         if ( transformTable.hasOwnProperty( numberString[ i ] ) ) {
230                                                 convertedNumber += transformTable[ numberString[ i ] ];
231                                         } else {
232                                                 convertedNumber += numberString[ i ];
233                                         }
234                                 }
235                         } else {
236                                 convertedNumber = numberString;
237                         }
239                         if ( integer ) {
240                                 // Parse string to integer. This loses decimals!
241                                 convertedNumber = parseInt( convertedNumber, 10 );
242                         }
244                         return convertedNumber;
245                 },
247                 /**
248                  * Get the digit transform table for current UI language.
249                  *
250                  * @return {Object|Array}
251                  */
252                 getDigitTransformTable: function () {
253                         return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
254                                 'digitTransformTable' ) || [];
255                 },
257                 /**
258                  * Get the separator transform table for current UI language.
259                  *
260                  * @return {Object|Array}
261                  */
262                 getSeparatorTransformTable: function () {
263                         return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
264                                 'separatorTransformTable' ) || [];
265                 },
267                 /**
268                  * Apply pattern to format value as a string.
269                  *
270                  * Using patterns from [Unicode TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns).
271                  *
272                  * @param {number} value
273                  * @param {string} pattern Pattern string as described by Unicode TR35
274                  * @throws {Error} If unable to find a number expression in `pattern`.
275                  * @return {string}
276                  */
277                 commafy: function ( value, pattern ) {
278                         var numberPattern,
279                                 transformTable = mw.language.getSeparatorTransformTable(),
280                                 group = transformTable[ ',' ] || ',',
281                                 numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough
282                                 decimal = transformTable[ '.' ] || '.',
283                                 patternList = pattern.split( ';' ),
284                                 positivePattern = patternList[ 0 ];
286                         pattern = patternList[ ( value < 0 ) ? 1 : 0 ] || ( '-' + positivePattern );
287                         numberPattern = positivePattern.match( numberPatternRE );
289                         if ( !numberPattern ) {
290                                 throw new Error( 'unable to find a number expression in pattern: ' + pattern );
291                         }
293                         return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
294                                 decimal: decimal,
295                                 group: group
296                         } ) );
297                 }
299         } );
301 }( mediaWiki, jQuery ) );