From 8af7462d3aab4cf925a1f7482070d1a83f121fab Mon Sep 17 00:00:00 2001 From: Santhosh Thottingal Date: Fri, 18 Jan 2013 21:04:54 +0530 Subject: [PATCH] Commafy support for convertNumber As per http://www.unicode.org/reports/tr35 Change-Id: I765298959ad10ea8561abfea487328c8ddeb39f0 --- .../ResourceLoaderLanguageDataModule.php | 33 ++- resources/Resources.php | 7 +- .../mediawiki.language/mediawiki.language.js | 44 +--- .../mediawiki.language.numbers.js | 243 ++++++++++++++++++ .../mediawiki/mediawiki.jqueryMsg.test.js | 195 ++++++++------ .../resources/mediawiki/mediawiki.test.js | 4 +- 6 files changed, 395 insertions(+), 131 deletions(-) create mode 100644 resources/mediawiki.language/mediawiki.language.numbers.js diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php index 9679a59b75..0f8e54ce95 100644 --- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php @@ -47,24 +47,35 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { return $this->language->getPluralRules(); } + /** + * Get the digit groupin Pattern for the site content language. + * + * @return array + */ + protected function getDigitGroupingPattern() { + return $this->language->digitGroupingPattern(); + } + /** * Get the digit transform table for the content language - * Seperator transform table also required here to convert - * the . and , sign to appropriate forms in content language. * * @return array */ protected function getDigitTransformTable() { - $digitTransformTable = $this->language->digitTransformTable(); - $separatorTransformTable = $this->language->separatorTransformTable(); - if ( $digitTransformTable ) { - array_merge( $digitTransformTable, (array)$separatorTransformTable ); - } else { - return $separatorTransformTable; - } - return $digitTransformTable; + return $this->language->digitTransformTable(); } + /** + * Get seperator transform table required for converting + * the . and , sign to appropriate forms in site content language. + * + * @return array + */ + protected function getSeparatorTransformTable() { + return $this->language->separatorTransformTable(); + } + + /** * Get all the dynamic data for the content language to an array * @@ -73,8 +84,10 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { protected function getData() { return array( 'digitTransformTable' => $this->getDigitTransformTable(), + 'separatorTransformTable' => $this->getSeparatorTransformTable(), 'grammarForms' => $this->getSiteLangGrammarForms(), 'pluralRules' => $this->getPluralRules(), + 'digitGroupingPattern' => $this->getDigitGroupingPattern(), ); } diff --git a/resources/Resources.php b/resources/Resources.php index 51bf05f896..c2df86db36 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -746,7 +746,10 @@ return array( /* MediaWiki Language */ 'mediawiki.language' => array( - 'scripts' => 'resources/mediawiki.language/mediawiki.language.js', + 'scripts' => array( + 'resources/mediawiki.language/mediawiki.language.js', + 'resources/mediawiki.language/mediawiki.language.numbers.js' + ), 'languageScripts' => array( 'bs' => 'resources/mediawiki.language/languages/bs.js', 'dsb' => 'resources/mediawiki.language/languages/dsb.js', @@ -764,7 +767,7 @@ return array( ), 'dependencies' => array( 'mediawiki.language.data', - 'mediawiki.cldr' + 'mediawiki.cldr', ), 'targets' => array( 'desktop', 'mobile' ), ), diff --git a/resources/mediawiki.language/mediawiki.language.js b/resources/mediawiki.language/mediawiki.language.js index f8af0a00f0..7f729bdc8e 100644 --- a/resources/mediawiki.language/mediawiki.language.js +++ b/resources/mediawiki.language/mediawiki.language.js @@ -74,46 +74,6 @@ var language = { return forms; }, - /** - * Converts a number using digitTransformTable. - * - * @param {Number} number Value to be converted - * @param {boolean} integer Convert the return value to an integer - * @return {Number|String} formatted number - */ - convertNumber: function ( num, integer ) { - var i, tmp, transformTable, numberString, convertedNumber; - - // Set the target Transform table: - transformTable = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'digitTransformTable' ); - - if ( !transformTable ) { - return num; - } - - // Check if the "restore" to Latin number flag is set: - if ( integer ) { - if ( parseInt( num, 10 ) === num ) { - return num; - } - tmp = []; - for ( i in transformTable ) { - tmp[ transformTable[ i ] ] = i; - } - transformTable = tmp; - } - numberString = '' + num; - convertedNumber = ''; - for ( i = 0; i < numberString.length; i++ ) { - if ( transformTable[ numberString[i] ] ) { - convertedNumber += transformTable[numberString[i]]; - } else { - convertedNumber += numberString[i]; - } - } - return integer ? parseInt( convertedNumber, 10 ) : convertedNumber; - }, - /** * Provides an alternative text depending on specified gender. * Usage {{gender:[gender|user object]|masculine|feminine|neutral}}. @@ -156,10 +116,8 @@ var language = { return grammarForms[form][word] || word; } return word; - }, + } - // Digit Transform Table, populated by language classes where applicable - digitTransformTable: mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'digitTransformTable' ) }; $.extend( mw.language, language ); diff --git a/resources/mediawiki.language/mediawiki.language.numbers.js b/resources/mediawiki.language/mediawiki.language.numbers.js new file mode 100644 index 0000000000..fada6ce159 --- /dev/null +++ b/resources/mediawiki.language/mediawiki.language.numbers.js @@ -0,0 +1,243 @@ +/* + * Number related utilities for mediawiki.language + */ +( function ( mw, $ ) { + + /** + * Pad a string to guarantee that it is at least `size` length by + * filling with the character `ch` at either the start or end of the + * string. Pads at the start, by default. + * example: + * Fill the string to length 10 with '+' characters on the right. Yields 'blah++++++'. + * pad('blah', 10, '+', true); + * + * @param {string} text The string to pad + * @param {Number} size To provide padding + * @param {string} ch Character to pad, defaults to '0' + * @param {Boolean} end Adds padding at the end if true, otherwise pads at start + * @return {string} + */ + function pad ( text, size, ch, end ) { + if ( !ch ) { + ch = '0'; + } + + var out = String( text ), + padStr = replicate( ch, Math.ceil( ( size - out.length ) / ch.length ) ); + + return end ? out + padStr : padStr + out; + } + + /** + * Efficiently replicate a string n times. + * + * @param {string} str The string to replicate + * @param {Number} num Number of times to replicate the string + * @return {string} + */ + function replicate ( str, num ) { + if ( num <= 0 || !str ) { + return ''; + } + + var buf = []; + while (num) { + buf.push( str ); + str += str; + } + return buf.join( '' ); + } + + /** + * Apply numeric pattern to absolute value using options. Gives no + * consideration to local customs. + * + * Adapted from dojo/number library with thanks + * http://dojotoolkit.org/reference-guide/1.8/dojo/number.html + * + * @param {Number} value the number to be formatted, ignores sign + * @param {string} pattern the number portion of a pattern (e.g. `#,##0.00`) + * @param {string} options.decimalThe decimal separator + * @param {string} options.group The group separator + * + * @return {string} + */ + function commafyNumber( value, pattern, options ) { + options = options || { + group: ',', + decimal: '.' + }; + + if ( isNaN( value) ) { + return value; + } + + var padLength, + patternDigits, + index, + whole, + off, + remainder, + patternParts = pattern.split( '.' ), + maxPlaces = ( patternParts[1] || [] ).length, + valueParts = String( Math.abs( value ) ).split( '.' ), + fractional = valueParts[1] || '', + groupSize = 0, + groupSize2 = 0, + pieces = []; + + if ( patternParts[1] ) { + // Pad fractional with trailing zeros + padLength = ( patternParts[1] && patternParts[1].lastIndexOf( '0' ) + 1 ); + + if ( padLength > fractional.length ) { + valueParts[1] = pad( fractional, padLength, '0', true ); + } + + // Truncate fractional + if ( maxPlaces < fractional.length ) { + valueParts[1] = fractional.substr( 0, maxPlaces ); + } + } else { + if ( valueParts[1] ) { + valueParts.pop(); + } + } + + // Pad whole with leading zeros + patternDigits = patternParts[0].replace( ',', '' ); + + padLength = patternDigits.indexOf( '0' ); + + if ( padLength !== -1 ) { + padLength = patternDigits.length - padLength; + + if ( padLength > valueParts[0].length ) { + valueParts[0] = pad( valueParts[0], padLength ); + } + + // Truncate whole + if ( patternDigits.indexOf( '#' ) === -1 ) { + valueParts[0] = valueParts[0].substr( valueParts[0].length - padLength ); + } + } + + // Add group separators + index = patternParts[0].lastIndexOf( ',' ); + + if ( index !== -1 ) { + groupSize = patternParts[0].length - index - 1; + remainder = patternParts[0].substr( 0, index ); + index = remainder.lastIndexOf( ',' ); + if ( index !== -1 ) { + groupSize2 = remainder.length - index - 1; + } + } + + for ( whole = valueParts[0]; whole; ) { + off = whole.length - groupSize; + + pieces.push( ( off > 0 ) ? whole.substr( off ) : whole ); + whole = ( off > 0 ) ? whole.slice( 0, off ) : ''; + + if ( groupSize2 ) { + groupSize = groupSize2; + } + } + valueParts[0] = pieces.reverse().join( options.group ); + + return valueParts.join( options.decimal ); + } + + $.extend( mw.language, { + + /** + * Converts a number using digitTransformTable. + * + * @param {Number} num Value to be converted + * @param {boolean} integer Convert the return value to an integer + * @return {Number|string} Formatted number + */ + convertNumber: function ( num, integer ) { + var i, tmp, transformTable, numberString, convertedNumber, pattern; + + pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ), + 'digitGroupingPattern' ) || '#,##0.###'; + + // Set the target transform table: + transformTable = mw.language.getDigitTransformTable(); + + if ( !transformTable ) { + return num; + } + + // Check if the 'restore' to Latin number flag is set: + if ( integer ) { + if ( parseInt( num, 10 ) === num ) { + return num; + } + tmp = []; + for ( i in transformTable ) { + tmp[ transformTable[ i ] ] = i; + } + transformTable = tmp; + numberString = num + ''; + } else { + numberString = mw.language.commafy( num, pattern ); + } + + convertedNumber = ''; + for ( i = 0; i < numberString.length; i++ ) { + if ( transformTable[ numberString[i] ] ) { + convertedNumber += transformTable[numberString[i]]; + } else { + convertedNumber += numberString[i]; + } + } + return integer ? parseInt( convertedNumber, 10 ) : convertedNumber; + }, + + getDigitTransformTable: function () { + return mw.language.getData( mw.config.get( 'wgUserLanguage' ), + 'digitTransformTable' ) || []; + }, + + getSeparatorTransformTable: function () { + return mw.language.getData( mw.config.get( 'wgUserLanguage' ), + 'separatorTransformTable' ) || []; + }, + + /** + * Apply pattern to format value as a string using as per + * unicode.org TR35 - http://www.unicode.org/reports/tr35/#Number_Format_Patterns. + * + * @param {Number} value + * @param {string} pattern Pattern string as described by Unicode TR35 + * @throws Error + * @returns {String} + */ + commafy: function ( value, pattern ) { + var numberPattern, + transformTable = mw.language.getSeparatorTransformTable(), + group = transformTable[','] || ',', + numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/, // not precise, but good enough + decimal = transformTable['.'] || '.', + patternList = pattern.split( ';' ), + positivePattern = patternList[0]; + + pattern = patternList[ ( value < 0 ) ? 1 : 0] || ( '-' + positivePattern ); + numberPattern = positivePattern.match( numberPatternRE ); + + if ( !numberPattern ) { + throw new Error( 'unable to find a number expression in pattern: ' + pattern ); + } + + return pattern.replace( numberPatternRE, commafyNumber( value, numberPattern[0], { + decimal: decimal, + group: group + } ) ); + } + + } ); + +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index c61365e271..7c10d63c04 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -337,7 +337,7 @@ assertBothModes( ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' ); mw.config.set( 'wgUserLanguage', 'en' ); - assertBothModes( ['formatnum-msg', '987654321.654321'], '987654321.654321', 'formatnum is resolved' ); + assertBothModes( ['formatnum-msg', '987654321.654321'], '987,654,321.654', 'formatnum is resolved' ); // Test non-{{ wikitext, where behavior differs @@ -475,80 +475,127 @@ mw.jqueryMsg.getMessageFunction = oldGMF; } ); - formatnumTests = [ - { - lang: 'en', - number: 987654321.654321, - result: '987654321.654321', - description: 'formatnum test for English, decimal seperator' - }, - { - lang: 'ar', - number: 987654321.654321, - result: '٩٨٧٦٥٤٣٢١٫٦٥٤٣٢١', - description: 'formatnum test for Arabic, with decimal seperator' - }, - { - lang: 'ar', - number: '٩٨٧٦٥٤٣٢١٫٦٥٤٣٢١', - result: 987654321, - integer: true, - description: 'formatnum test for Arabic, with decimal seperator, reverse' - }, - { - lang: 'ar', - number: -12.89, - result: '-١٢٫٨٩', - description: 'formatnum test for Arabic, negative number' - }, - { - lang: 'ar', - number: '-١٢٫٨٩', - result: -12, - integer: true, - description: 'formatnum test for Arabic, negative number, reverse' - }, - { - lang: 'nl', - number: 987654321.654321, - result: '987654321,654321', - description: 'formatnum test for Nederlands, decimal seperator' - }, - { - lang: 'nl', - number: -12.89, - result: '-12,89', - description: 'formatnum test for Nederlands, negative number' - }, - { - lang: 'nl', - number: 'invalidnumber', - result: 'invalidnumber', - description: 'formatnum test for Nederlands, invalid number' - } - ]; - - QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) { - mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); - $.each( formatnumTests, function ( i, test ) { - QUnit.stop(); - getMwLanguage( test.lang, function ( langClass ) { - QUnit.start(); - if ( !langClass ) { - assert.ok( false, 'Language "' + test.lang + '" failed to load' ); - return; - } - mw.messages.set( test.message ); - mw.config.set( 'wgUserLanguage', test.lang ); - var parser = new mw.jqueryMsg.parser( { language: langClass } ); - assert.equal( - parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg', - [ test.number ] ).html(), - test.result, - test.description - ); - } ); +formatnumTests = [ + { + lang: 'en', + number: 987654321.654321, + result: '987,654,321.654', + description: 'formatnum test for English, decimal seperator' + }, + { + lang: 'ar', + number: 987654321.654321, + result: '٩٨٧٬٦٥٤٬٣٢١٫٦٥٤', + description: 'formatnum test for Arabic, with decimal seperator' + }, + { + lang: 'ar', + number: '٩٨٧٦٥٤٣٢١٫٦٥٤٣٢١', + result: 987654321, + integer: true, + description: 'formatnum test for Arabic, with decimal seperator, reverse' + }, + { + lang: 'ar', + number: -12.89, + result: '-١٢٫٨٩', + description: 'formatnum test for Arabic, negative number' + }, + { + lang: 'ar', + number: '-١٢٫٨٩', + result: -12, + integer: true, + description: 'formatnum test for Arabic, negative number, reverse' + }, + { + lang: 'nl', + number: 987654321.654321, + result: '987.654.321,654', + description: 'formatnum test for Nederlands, decimal seperator' + }, + { + lang: 'nl', + number: -12.89, + result: '-12,89', + description: 'formatnum test for Nederlands, negative number' + }, + { + lang: 'nl', + number: '.89', + result: '0,89', + description: 'formatnum test for Nederlands' + }, + { + lang: 'nl', + number: 'invalidnumber', + result: 'invalidnumber', + description: 'formatnum test for Nederlands, invalid number' + }, + { + lang: 'ml', + number: '1000000000', + result: '1,00,00,00,000', + description: 'formatnum test for Malayalam' + }, + { + lang: 'ml', + number: '-1000000000', + result: '-1,00,00,00,000', + description: 'formatnum test for Malayalam, negative number' + }, + /* + * This will fail because of wrong pattern for ml in MW(different from CLDR) + { + lang: 'ml', + number: '1000000000.000', + result: '1,00,00,00,000.000', + description: 'formatnum test for Malayalam with decimal place' + }, + */ + { + lang: 'hi', + number: '123456789.123456789', + result: '१२,३४,५६,७८९', + description: 'formatnum test for Hindi' + }, + { + lang: 'hi', + number: '१२,३४,५६,७८९', + result: '१२,३४,५६,७८९', + description: 'formatnum test for Hindi, Devanagari digits passed' + }, + { + lang: 'hi', + number: '१२३४५६,७८९', + result: '123456', + integer: true, + description: 'formatnum test for Hindi, Devanagari digits passed to get integer value' + } +]; + +QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) { + mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' ); + mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); + $.each( formatnumTests, function ( i, test ) { + QUnit.stop(); + getMwLanguage( test.lang, function ( langClass ) { + QUnit.start(); + if ( !langClass ) { + assert.ok( false, 'Language "' + test.lang + '" failed to load' ); + return; + } + mw.messages.set(test.message ); + mw.config.set( 'wgUserLanguage', test.lang ) ; + var parser = new mw.jqueryMsg.parser( { language: langClass } ); + assert.equal( + parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg', + [ test.number ] ).html(), + test.result, + test.description + ); } ); } ); +} ); }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index 9b540a86ff..45c3c5a69c 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -182,7 +182,7 @@ assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' ); assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' ); - assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987654321.654321', 'formatnum is resolved' ); + assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987,654,321.654', 'formatnum is resolved' ); assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' ); assertMultipleFormats( ['int-msg'], ['text', 'parse', 'escaped'], 'Some Other Message', 'int is resolved' ); @@ -207,7 +207,7 @@ assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' ); - assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987654321.654321', 'formatnum is resolved' ); + assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987,654,321.654', 'formatnum is resolved' ); assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' ); } ); -- 2.20.1