source: Dev/trunk/src/client/dojo/number.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 19.5 KB
RevLine 
[483]1define([/*===== "./_base/declare", =====*/ "./_base/lang", "./i18n", "./i18n!./cldr/nls/number", "./string", "./regexp"],
2        function(/*===== declare, =====*/ lang, i18n, nlsNumber, dstring, dregexp){
3
4// module:
5//              dojo/number
6
7var number = {
8        // summary:
9        //              localized formatting and parsing routines for Number
10};
11lang.setObject("dojo.number", number);
12
13/*=====
14number.__FormatOptions = declare(null, {
15        // pattern: String?
16        //              override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
17        //              with this string.  Default value is based on locale.  Overriding this property will defeat
18        //              localization.  Literal characters in patterns are not supported.
19        // type: String?
20        //              choose a format type based on the locale from the following:
21        //              decimal, scientific (not yet supported), percent, currency. decimal by default.
22        // places: Number?
23        //              fixed number of decimal places to show.  This overrides any
24        //              information in the provided pattern.
25        // round: Number?
26        //              5 rounds to nearest .5; 0 rounds to nearest whole (default). -1
27        //              means do not round.
28        // locale: String?
29        //              override the locale used to determine formatting rules
30        // fractional: Boolean?
31        //              If false, show no decimal places, overriding places and pattern settings.
32});
33=====*/
34
35number.format = function(/*Number*/ value, /*number.__FormatOptions?*/ options){
36        // summary:
37        //              Format a Number as a String, using locale-specific settings
38        // description:
39        //              Create a string from a Number using a known localized pattern.
40        //              Formatting patterns appropriate to the locale are chosen from the
41        //              [Common Locale Data Repository](http://unicode.org/cldr) as well as the appropriate symbols and
42        //              delimiters.
43        //              If value is Infinity, -Infinity, or is not a valid JavaScript number, return null.
44        // value:
45        //              the number to be formatted
46
47        options = lang.mixin({}, options || {});
48        var locale = i18n.normalizeLocale(options.locale),
49                bundle = i18n.getLocalization("dojo.cldr", "number", locale);
50        options.customs = bundle;
51        var pattern = options.pattern || bundle[(options.type || "decimal") + "Format"];
52        if(isNaN(value) || Math.abs(value) == Infinity){ return null; } // null
53        return number._applyPattern(value, pattern, options); // String
54};
55
56//number._numberPatternRE = /(?:[#0]*,?)*[#0](?:\.0*#*)?/; // not precise, but good enough
57number._numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/; // not precise, but good enough
58
59number._applyPattern = function(/*Number*/ value, /*String*/ pattern, /*number.__FormatOptions?*/ options){
60        // summary:
61        //              Apply pattern to format value as a string using options. Gives no
62        //              consideration to local customs.
63        // value:
64        //              the number to be formatted.
65        // pattern:
66        //              a pattern string as described by
67        //              [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
68        // options: number.__FormatOptions?
69        //              _applyPattern is usually called via `dojo/number.format()` which
70        //              populates an extra property in the options parameter, "customs".
71        //              The customs object specifies group and decimal parameters if set.
72
73        //TODO: support escapes
74        options = options || {};
75        var group = options.customs.group,
76                decimal = options.customs.decimal,
77                patternList = pattern.split(';'),
78                positivePattern = patternList[0];
79        pattern = patternList[(value < 0) ? 1 : 0] || ("-" + positivePattern);
80
81        //TODO: only test against unescaped
82        if(pattern.indexOf('%') != -1){
83                value *= 100;
84        }else if(pattern.indexOf('\u2030') != -1){
85                value *= 1000; // per mille
86        }else if(pattern.indexOf('\u00a4') != -1){
87                group = options.customs.currencyGroup || group;//mixins instead?
88                decimal = options.customs.currencyDecimal || decimal;// Should these be mixins instead?
89                pattern = pattern.replace(/\u00a4{1,3}/, function(match){
90                        var prop = ["symbol", "currency", "displayName"][match.length-1];
91                        return options[prop] || options.currency || "";
92                });
93        }else if(pattern.indexOf('E') != -1){
94                throw new Error("exponential notation not supported");
95        }
96
97        //TODO: support @ sig figs?
98        var numberPatternRE = number._numberPatternRE;
99        var numberPattern = positivePattern.match(numberPatternRE);
100        if(!numberPattern){
101                throw new Error("unable to find a number expression in pattern: "+pattern);
102        }
103        if(options.fractional === false){ options.places = 0; }
104        return pattern.replace(numberPatternRE,
105                number._formatAbsolute(value, numberPattern[0], {decimal: decimal, group: group, places: options.places, round: options.round}));
106};
107
108number.round = function(/*Number*/ value, /*Number?*/ places, /*Number?*/ increment){
109        // summary:
110        //              Rounds to the nearest value with the given number of decimal places, away from zero
111        // description:
112        //              Rounds to the nearest value with the given number of decimal places, away from zero if equal.
113        //              Similar to Number.toFixed(), but compensates for browser quirks. Rounding can be done by
114        //              fractional increments also, such as the nearest quarter.
115        //              NOTE: Subject to floating point errors.  See dojox/math/round for experimental workaround.
116        // value:
117        //              The number to round
118        // places:
119        //              The number of decimal places where rounding takes place.  Defaults to 0 for whole rounding.
120        //              Must be non-negative.
121        // increment:
122        //              Rounds next place to nearest value of increment/10.  10 by default.
123        // example:
124        // |    >>> number.round(-0.5)
125        // |    -1
126        // |    >>> number.round(162.295, 2)
127        // |    162.29  // note floating point error.  Should be 162.3
128        // |    >>> number.round(10.71, 0, 2.5)
129        // |    10.75
130        var factor = 10 / (increment || 10);
131        return (factor * +value).toFixed(places) / factor; // Number
132};
133
134if((0.9).toFixed() == 0){
135        // (isIE) toFixed() bug workaround: Rounding fails on IE when most significant digit
136        // is just after the rounding place and is >=5
137        var round = number.round;
138        number.round = function(v, p, m){
139                var d = Math.pow(10, -p || 0), a = Math.abs(v);
140                if(!v || a >= d){
141                        d = 0;
142                }else{
143                        a /= d;
144                        if(a < 0.5 || a >= 0.95){
145                                d = 0;
146                        }
147                }
148                return round(v, p, m) + (v > 0 ? d : -d);
149        };
150
151        // Use "doc hint" so the doc parser ignores this new definition of round(), and uses the one above.
152        /*===== number.round = round; =====*/
153}
154
155/*=====
156number.__FormatAbsoluteOptions = declare(null, {
157        // decimal: String?
158        //              the decimal separator
159        // group: String?
160        //              the group separator
161        // places: Number|String?
162        //              number of decimal places.  the range "n,m" will format to m places.
163        // round: Number?
164        //              5 rounds to nearest .5; 0 rounds to nearest whole (default). -1
165        //              means don't round.
166});
167=====*/
168
169number._formatAbsolute = function(/*Number*/ value, /*String*/ pattern, /*number.__FormatAbsoluteOptions?*/ options){
170        // summary:
171        //              Apply numeric pattern to absolute value using options. Gives no
172        //              consideration to local customs.
173        // value:
174        //              the number to be formatted, ignores sign
175        // pattern:
176        //              the number portion of a pattern (e.g. `#,##0.00`)
177        options = options || {};
178        if(options.places === true){options.places=0;}
179        if(options.places === Infinity){options.places=6;} // avoid a loop; pick a limit
180
181        var patternParts = pattern.split("."),
182                comma = typeof options.places == "string" && options.places.indexOf(","),
183                maxPlaces = options.places;
184        if(comma){
185                maxPlaces = options.places.substring(comma + 1);
186        }else if(!(maxPlaces >= 0)){
187                maxPlaces = (patternParts[1] || []).length;
188        }
189        if(!(options.round < 0)){
190                value = number.round(value, maxPlaces, options.round);
191        }
192
193        var valueParts = String(Math.abs(value)).split("."),
194                fractional = valueParts[1] || "";
195        if(patternParts[1] || options.places){
196                if(comma){
197                        options.places = options.places.substring(0, comma);
198                }
199                // Pad fractional with trailing zeros
200                var pad = options.places !== undefined ? options.places : (patternParts[1] && patternParts[1].lastIndexOf("0") + 1);
201                if(pad > fractional.length){
202                        valueParts[1] = dstring.pad(fractional, pad, '0', true);
203                }
204
205                // Truncate fractional
206                if(maxPlaces < fractional.length){
207                        valueParts[1] = fractional.substr(0, maxPlaces);
208                }
209        }else{
210                if(valueParts[1]){ valueParts.pop(); }
211        }
212
213        // Pad whole with leading zeros
214        var patternDigits = patternParts[0].replace(',', '');
215        pad = patternDigits.indexOf("0");
216        if(pad != -1){
217                pad = patternDigits.length - pad;
218                if(pad > valueParts[0].length){
219                        valueParts[0] = dstring.pad(valueParts[0], pad);
220                }
221
222                // Truncate whole
223                if(patternDigits.indexOf("#") == -1){
224                        valueParts[0] = valueParts[0].substr(valueParts[0].length - pad);
225                }
226        }
227
228        // Add group separators
229        var index = patternParts[0].lastIndexOf(','),
230                groupSize, groupSize2;
231        if(index != -1){
232                groupSize = patternParts[0].length - index - 1;
233                var remainder = patternParts[0].substr(0, index);
234                index = remainder.lastIndexOf(',');
235                if(index != -1){
236                        groupSize2 = remainder.length - index - 1;
237                }
238        }
239        var pieces = [];
240        for(var whole = valueParts[0]; whole;){
241                var off = whole.length - groupSize;
242                pieces.push((off > 0) ? whole.substr(off) : whole);
243                whole = (off > 0) ? whole.slice(0, off) : "";
244                if(groupSize2){
245                        groupSize = groupSize2;
246                        delete groupSize2;
247                }
248        }
249        valueParts[0] = pieces.reverse().join(options.group || ",");
250
251        return valueParts.join(options.decimal || ".");
252};
253
254/*=====
255number.__RegexpOptions = declare(null, {
256        // pattern: String?
257        //              override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
258        //              with this string.  Default value is based on locale.  Overriding this property will defeat
259        //              localization.
260        // type: String?
261        //              choose a format type based on the locale from the following:
262        //              decimal, scientific (not yet supported), percent, currency. decimal by default.
263        // locale: String?
264        //              override the locale used to determine formatting rules
265        // strict: Boolean?
266        //              strict parsing, false by default.  Strict parsing requires input as produced by the format() method.
267        //              Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators
268        // places: Number|String?
269        //              number of decimal places to accept: Infinity, a positive number, or
270        //              a range "n,m".  Defined by pattern or Infinity if pattern not provided.
271});
272=====*/
273number.regexp = function(/*number.__RegexpOptions?*/ options){
274        // summary:
275        //              Builds the regular needed to parse a number
276        // description:
277        //              Returns regular expression with positive and negative match, group
278        //              and decimal separators
279        return number._parseInfo(options).regexp; // String
280};
281
282number._parseInfo = function(/*Object?*/ options){
283        options = options || {};
284        var locale = i18n.normalizeLocale(options.locale),
285                bundle = i18n.getLocalization("dojo.cldr", "number", locale),
286                pattern = options.pattern || bundle[(options.type || "decimal") + "Format"],
287//TODO: memoize?
288                group = bundle.group,
289                decimal = bundle.decimal,
290                factor = 1;
291
292        if(pattern.indexOf('%') != -1){
293                factor /= 100;
294        }else if(pattern.indexOf('\u2030') != -1){
295                factor /= 1000; // per mille
296        }else{
297                var isCurrency = pattern.indexOf('\u00a4') != -1;
298                if(isCurrency){
299                        group = bundle.currencyGroup || group;
300                        decimal = bundle.currencyDecimal || decimal;
301                }
302        }
303
304        //TODO: handle quoted escapes
305        var patternList = pattern.split(';');
306        if(patternList.length == 1){
307                patternList.push("-" + patternList[0]);
308        }
309
310        var re = dregexp.buildGroupRE(patternList, function(pattern){
311                pattern = "(?:"+dregexp.escapeString(pattern, '.')+")";
312                return pattern.replace(number._numberPatternRE, function(format){
313                        var flags = {
314                                signed: false,
315                                separator: options.strict ? group : [group,""],
316                                fractional: options.fractional,
317                                decimal: decimal,
318                                exponent: false
319                                },
320
321                                parts = format.split('.'),
322                                places = options.places;
323
324                        // special condition for percent (factor != 1)
325                        // allow decimal places even if not specified in pattern
326                        if(parts.length == 1 && factor != 1){
327                            parts[1] = "###";
328                        }
329                        if(parts.length == 1 || places === 0){
330                                flags.fractional = false;
331                        }else{
332                                if(places === undefined){ places = options.pattern ? parts[1].lastIndexOf('0') + 1 : Infinity; }
333                                if(places && options.fractional == undefined){flags.fractional = true;} // required fractional, unless otherwise specified
334                                if(!options.places && (places < parts[1].length)){ places += "," + parts[1].length; }
335                                flags.places = places;
336                        }
337                        var groups = parts[0].split(',');
338                        if(groups.length > 1){
339                                flags.groupSize = groups.pop().length;
340                                if(groups.length > 1){
341                                        flags.groupSize2 = groups.pop().length;
342                                }
343                        }
344                        return "("+number._realNumberRegexp(flags)+")";
345                });
346        }, true);
347
348        if(isCurrency){
349                // substitute the currency symbol for the placeholder in the pattern
350                re = re.replace(/([\s\xa0]*)(\u00a4{1,3})([\s\xa0]*)/g, function(match, before, target, after){
351                        var prop = ["symbol", "currency", "displayName"][target.length-1],
352                                symbol = dregexp.escapeString(options[prop] || options.currency || "");
353                        before = before ? "[\\s\\xa0]" : "";
354                        after = after ? "[\\s\\xa0]" : "";
355                        if(!options.strict){
356                                if(before){before += "*";}
357                                if(after){after += "*";}
358                                return "(?:"+before+symbol+after+")?";
359                        }
360                        return before+symbol+after;
361                });
362        }
363
364//TODO: substitute localized sign/percent/permille/etc.?
365
366        // normalize whitespace and return
367        return {regexp: re.replace(/[\xa0 ]/g, "[\\s\\xa0]"), group: group, decimal: decimal, factor: factor}; // Object
368};
369
370/*=====
371number.__ParseOptions = declare(null, {
372        // pattern: String?
373        //              override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
374        //              with this string.  Default value is based on locale.  Overriding this property will defeat
375        //              localization.  Literal characters in patterns are not supported.
376        // type: String?
377        //              choose a format type based on the locale from the following:
378        //              decimal, scientific (not yet supported), percent, currency. decimal by default.
379        // locale: String?
380        //              override the locale used to determine formatting rules
381        // strict: Boolean?
382        //              strict parsing, false by default.  Strict parsing requires input as produced by the format() method.
383        //              Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators
384        // fractional: Boolean|Array?
385        //              Whether to include the fractional portion, where the number of decimal places are implied by pattern
386        //              or explicit 'places' parameter.  The value [true,false] makes the fractional portion optional.
387});
388=====*/
389number.parse = function(/*String*/ expression, /*number.__ParseOptions?*/ options){
390        // summary:
391        //              Convert a properly formatted string to a primitive Number, using
392        //              locale-specific settings.
393        // description:
394        //              Create a Number from a string using a known localized pattern.
395        //              Formatting patterns are chosen appropriate to the locale
396        //              and follow the syntax described by
397        //              [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns)
398        //              Note that literal characters in patterns are not supported.
399        // expression:
400        //              A string representation of a Number
401        var info = number._parseInfo(options),
402                results = (new RegExp("^"+info.regexp+"$")).exec(expression);
403        if(!results){
404                return NaN; //NaN
405        }
406        var absoluteMatch = results[1]; // match for the positive expression
407        if(!results[1]){
408                if(!results[2]){
409                        return NaN; //NaN
410                }
411                // matched the negative pattern
412                absoluteMatch =results[2];
413                info.factor *= -1;
414        }
415
416        // Transform it to something Javascript can parse as a number.  Normalize
417        // decimal point and strip out group separators or alternate forms of whitespace
418        absoluteMatch = absoluteMatch.
419                replace(new RegExp("["+info.group + "\\s\\xa0"+"]", "g"), "").
420                replace(info.decimal, ".");
421        // Adjust for negative sign, percent, etc. as necessary
422        return absoluteMatch * info.factor; //Number
423};
424
425/*=====
426number.__RealNumberRegexpFlags = declare(null, {
427        // places: Number?
428        //              The integer number of decimal places or a range given as "n,m".  If
429        //              not given, the decimal part is optional and the number of places is
430        //              unlimited.
431        // decimal: String?
432        //              A string for the character used as the decimal point.  Default
433        //              is ".".
434        // fractional: Boolean|Array?
435        //              Whether decimal places are used.  Can be true, false, or [true,
436        //              false].  Default is [true, false] which means optional.
437        // exponent: Boolean|Array?
438        //              Express in exponential notation.  Can be true, false, or [true,
439        //              false]. Default is [true, false], (i.e. will match if the
440        //              exponential part is present are not).
441        // eSigned: Boolean|Array?
442        //              The leading plus-or-minus sign on the exponent.  Can be true,
443        //              false, or [true, false].  Default is [true, false], (i.e. will
444        //              match if it is signed or unsigned).  flags in regexp.integer can be
445        //              applied.
446});
447=====*/
448
449number._realNumberRegexp = function(/*__RealNumberRegexpFlags?*/ flags){
450        // summary:
451        //              Builds a regular expression to match a real number in exponential
452        //              notation
453
454        // assign default values to missing parameters
455        flags = flags || {};
456        //TODO: use mixin instead?
457        if(!("places" in flags)){ flags.places = Infinity; }
458        if(typeof flags.decimal != "string"){ flags.decimal = "."; }
459        if(!("fractional" in flags) || /^0/.test(flags.places)){ flags.fractional = [true, false]; }
460        if(!("exponent" in flags)){ flags.exponent = [true, false]; }
461        if(!("eSigned" in flags)){ flags.eSigned = [true, false]; }
462
463        var integerRE = number._integerRegexp(flags),
464                decimalRE = dregexp.buildGroupRE(flags.fractional,
465                function(q){
466                        var re = "";
467                        if(q && (flags.places!==0)){
468                                re = "\\" + flags.decimal;
469                                if(flags.places == Infinity){
470                                        re = "(?:" + re + "\\d+)?";
471                                }else{
472                                        re += "\\d{" + flags.places + "}";
473                                }
474                        }
475                        return re;
476                },
477                true
478        );
479
480        var exponentRE = dregexp.buildGroupRE(flags.exponent,
481                function(q){
482                        if(q){ return "([eE]" + number._integerRegexp({ signed: flags.eSigned}) + ")"; }
483                        return "";
484                }
485        );
486
487        var realRE = integerRE + decimalRE;
488        // allow for decimals without integers, e.g. .25
489        if(decimalRE){realRE = "(?:(?:"+ realRE + ")|(?:" + decimalRE + "))";}
490        return realRE + exponentRE; // String
491};
492
493/*=====
494number.__IntegerRegexpFlags = declare(null, {
495        // signed: Boolean?
496        //              The leading plus-or-minus sign. Can be true, false, or `[true,false]`.
497        //              Default is `[true, false]`, (i.e. will match if it is signed
498        //              or unsigned).
499        // separator: String?
500        //              The character used as the thousands separator. Default is no
501        //              separator. For more than one symbol use an array, e.g. `[",", ""]`,
502        //              makes ',' optional.
503        // groupSize: Number?
504        //              group size between separators
505        // groupSize2: Number?
506        //              second grouping, where separators 2..n have a different interval than the first separator (for India)
507});
508=====*/
509
510number._integerRegexp = function(/*number.__IntegerRegexpFlags?*/ flags){
511        // summary:
512        //              Builds a regular expression that matches an integer
513
514        // assign default values to missing parameters
515        flags = flags || {};
516        if(!("signed" in flags)){ flags.signed = [true, false]; }
517        if(!("separator" in flags)){
518                flags.separator = "";
519        }else if(!("groupSize" in flags)){
520                flags.groupSize = 3;
521        }
522
523        var signRE = dregexp.buildGroupRE(flags.signed,
524                function(q){ return q ? "[-+]" : ""; },
525                true
526        );
527
528        var numberRE = dregexp.buildGroupRE(flags.separator,
529                function(sep){
530                        if(!sep){
531                                return "(?:\\d+)";
532                        }
533
534                        sep = dregexp.escapeString(sep);
535                        if(sep == " "){ sep = "\\s"; }
536                        else if(sep == "\xa0"){ sep = "\\s\\xa0"; }
537
538                        var grp = flags.groupSize, grp2 = flags.groupSize2;
539                        //TODO: should we continue to enforce that numbers with separators begin with 1-9?  See #6933
540                        if(grp2){
541                                var grp2RE = "(?:0|[1-9]\\d{0," + (grp2-1) + "}(?:[" + sep + "]\\d{" + grp2 + "})*[" + sep + "]\\d{" + grp + "})";
542                                return ((grp-grp2) > 0) ? "(?:" + grp2RE + "|(?:0|[1-9]\\d{0," + (grp-1) + "}))" : grp2RE;
543                        }
544                        return "(?:0|[1-9]\\d{0," + (grp-1) + "}(?:[" + sep + "]\\d{" + grp + "})*)";
545                },
546                true
547        );
548
549        return signRE + numberRE; // String
550};
551
552return number;
553});
Note: See TracBrowser for help on using the repository browser.