Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.language / mediawiki.language.numbers.js
blobd3671e4403f9f947f91b71bfe492ed607a31aad7
1 /*
2  * Number-related utilities for mediawiki.language.
3  */
4 ( function () {
6         /**
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.
10          *
11          * Example: Fill the string to length 10 with '+' characters on the right.
12          *
13          *     pad( 'blah', 10, '+', true ); // => 'blah++++++'
14          *
15          * @private
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
20          * @return {string}
21          */
22         function pad( text, size, ch, end ) {
23                 if ( !ch ) {
24                         ch = '0';
25                 }
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;
32         }
34         /**
35          * Apply numeric pattern to absolute value using options. Gives no
36          * consideration to local customs.
37          *
38          * Adapted from dojo/number library with thanks
39          * <http://dojotoolkit.org/reference-guide/1.8/dojo/number.html>
40          *
41          * @private
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
48          * @return {string}
49          */
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 ] || '',
55                         pieces = [];
56                 let groupSize = 0,
57                         groupSize2 = 0;
59                 options = options || {
60                         group: ',',
61                         decimal: '.'
62                 };
64                 if ( isNaN( value ) ) {
65                         return value;
66                 }
68                 let padLength;
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 );
75                         }
77                         // Truncate fractional
78                         if ( maxPlaces < fractional.length ) {
79                                 valueParts[ 1 ] = fractional.slice( 0, maxPlaces );
80                         }
81                 } else {
82                         if ( valueParts[ 1 ] ) {
83                                 valueParts.pop();
84                         }
85                 }
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 );
97                         }
99                         // Truncate whole
100                         if ( patternDigits.indexOf( '#' ) === -1 && padLength ) {
101                                 valueParts[ 0 ] = valueParts[ 0 ].slice( -padLength );
102                         }
103                 }
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;
114                         }
115                 }
117                 if (
118                         options.minimumGroupingDigits === null ||
119                         valueParts[ 0 ].length >= groupSize + options.minimumGroupingDigits
120                 ) {
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 ) : '';
126                                 if ( groupSize2 ) {
127                                         groupSize = groupSize2;
128                                         groupSize2 = null;
129                                 }
130                         }
131                         valueParts[ 0 ] = pieces.reverse().join( options.group );
132                 }
134                 return valueParts.join( options.decimal );
135         }
137         /**
138          * Apply pattern to format value as a string.
139          *
140          * Using patterns from [Unicode TR35](https://www.unicode.org/reports/tr35/#Number_Format_Patterns).
141          *
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`.
146          * @return {string}
147          * @private
148          */
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 );
165                 }
167                 return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[ 0 ], {
168                         minimumGroupingDigits: minimumGroupingDigits,
169                         decimal: decimal,
170                         group: group
171                 } ) );
172         }
174         /**
175          * Helper function to flip transformation tables.
176          *
177          * @param {...Object} Transformation tables
178          * @return {Object}
179          */
180         function flipTransform() {
181                 const flipped = {};
183                 // Ensure we strip thousand separators. This might be overwritten.
184                 flipped[ ',' ] = '';
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;
191                         }
192                 }
194                 return flipped;
195         }
197         Object.assign( mw.language, {
199                 /**
200                  * Converts a number using `getDigitTransformTable()`.
201                  *
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
206                  */
207                 convertNumber: function ( num, integer ) {
208                         // Quick shortcut for plain numbers
209                         if ( integer && parseInt( num, 10 ) === num ) {
210                                 return num;
211                         }
213                         // Load the transformation tables (can be empty)
214                         const digitTransformTable = mw.language.getDigitTransformTable();
215                         const separatorTransformTable = mw.language.getSeparatorTransformTable();
217                         let transformTable, numberString;
218                         if ( integer ) {
219                                 // Reverse the digit transformation tables if we are doing unformatting
220                                 transformTable = flipTransform( separatorTransformTable, digitTransformTable );
221                                 numberString = String( num );
222                         } else {
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;
227                                 }
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 );
236                         }
238                         let convertedNumber;
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 ] ];
244                                         } else {
245                                                 convertedNumber += numberString[ i ];
246                                         }
247                                 }
248                         } else {
249                                 convertedNumber = numberString;
250                         }
252                         if ( integer ) {
253                                 // Parse string to integer. This loses decimals!
254                                 convertedNumber = parseInt( convertedNumber, 10 );
255                         }
257                         return convertedNumber;
258                 },
260                 /**
261                  * Get the digit transform table for current UI language.
262                  *
263                  * @ignore
264                  * @return {Object|Array}
265                  */
266                 getDigitTransformTable: function () {
267                         return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
268                                 'digitTransformTable' ) || [];
269                 },
271                 /**
272                  * Get the separator transform table for current UI language.
273                  *
274                  * @ignore
275                  * @return {Object|Array}
276                  */
277                 getSeparatorTransformTable: function () {
278                         return mw.language.getData( mw.config.get( 'wgUserLanguage' ),
279                                 'separatorTransformTable' ) || [];
280                 }
282         } );
284 }() );