source: Dev/branches/rest-dojo-ui/client/dojo/number.js @ 263

Last change on this file since 263 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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