UNPKG

dojo

Version:

Dojo core is a powerful, lightweight library that makes common tasks quicker and easier. Animate elements, manipulate the DOM, and query with easy CSS syntax, all without sacrificing performance.

566 lines (514 loc) 20.3 kB
define([/*===== "./_base/declare", =====*/ "./_base/lang", "./i18n", "./i18n!./cldr/nls/number", "./string", "./regexp"], function(/*===== declare, =====*/ lang, i18n, nlsNumber, dstring, dregexp){ // module: // dojo/number var number = { // summary: // localized formatting and parsing routines for Number }; lang.setObject("dojo.number", number); /*===== number.__FormatOptions = declare(null, { // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. Literal characters in patterns are not supported. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // places: Number? // fixed number of decimal places to show. This overrides any // information in the provided pattern. // round: Number? // 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1 // means do not round. // locale: String? // override the locale used to determine formatting rules // fractional: Boolean? // If false, show no decimal places, overriding places and pattern settings. }); =====*/ number.format = function(/*Number*/ value, /*number.__FormatOptions?*/ options){ // summary: // Format a Number as a String, using locale-specific settings // description: // Create a string from a Number using a known localized pattern. // Formatting patterns appropriate to the locale are chosen from the // [Common Locale Data Repository](http://unicode.org/cldr) as well as the appropriate symbols and // delimiters. // If value is Infinity, -Infinity, or is not a valid JavaScript number, return null. // value: // the number to be formatted options = lang.mixin({}, options || {}); var locale = i18n.normalizeLocale(options.locale), bundle = i18n.getLocalization("dojo.cldr", "number", locale); options.customs = bundle; var pattern = options.pattern || bundle[(options.type || "decimal") + "Format"]; if(isNaN(value) || Math.abs(value) == Infinity){ return null; } // null return number._applyPattern(value, pattern, options); // String }; //number._numberPatternRE = /(?:[#0]*,?)*[#0](?:\.0*#*)?/; // not precise, but good enough number._numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/; // not precise, but good enough number._applyPattern = function(/*Number*/ value, /*String*/ pattern, /*number.__FormatOptions?*/ options){ // summary: // Apply pattern to format value as a string using options. Gives no // consideration to local customs. // value: // the number to be formatted. // pattern: // a pattern string as described by // [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // options: number.__FormatOptions? // _applyPattern is usually called via `dojo/number.format()` which // populates an extra property in the options parameter, "customs". // The customs object specifies group and decimal parameters if set. //TODO: support escapes options = options || {}; var group = options.customs.group, decimal = options.customs.decimal, patternList = pattern.split(';'), positivePattern = patternList[0]; pattern = patternList[(value < 0) ? 1 : 0] || ("-" + positivePattern); //TODO: only test against unescaped if(pattern.indexOf('%') != -1){ value *= 100; }else if(pattern.indexOf('\u2030') != -1){ value *= 1000; // per mille }else if(pattern.indexOf('\u00a4') != -1){ group = options.customs.currencyGroup || group;//mixins instead? decimal = options.customs.currencyDecimal || decimal;// Should these be mixins instead? pattern = pattern.replace(/([\s\xa0\u202f]*)(\u00a4{1,3})([\s\xa0\u202f]*)/, function(match, before, target, after){ var prop = ["symbol", "currency", "displayName"][target.length-1], symbol = options[prop] || options.currency || ""; // if there is no symbol, also remove surrounding whitespaces if(!symbol){ return ""; } return before+symbol+after; }); }else if(pattern.indexOf('E') != -1){ throw new Error("exponential notation not supported"); } //TODO: support @ sig figs? var numberPatternRE = number._numberPatternRE; var numberPattern = positivePattern.match(numberPatternRE); if(!numberPattern){ throw new Error("unable to find a number expression in pattern: "+pattern); } if(options.fractional === false){ options.places = 0; } return pattern.replace(numberPatternRE, number._formatAbsolute(value, numberPattern[0], {decimal: decimal, group: group, places: options.places, round: options.round})); }; number.round = function(/*Number*/ value, /*Number?*/ places, /*Number?*/ increment){ // summary: // Rounds to the nearest value with the given number of decimal places, away from zero // description: // Rounds to the nearest value with the given number of decimal places, away from zero if equal. // Similar to Number.toFixed(), but compensates for browser quirks. Rounding can be done by // fractional increments also, such as the nearest quarter. // NOTE: Subject to floating point errors. See dojox/math/round for experimental workaround. // value: // The number to round // places: // The number of decimal places where rounding takes place. Defaults to 0 for whole rounding. // Must be non-negative. // increment: // Rounds next place to nearest value of increment/10. 10 by default. // example: // | >>> number.round(-0.5) // | -1 // | >>> number.round(162.295, 2) // | 162.29 // note floating point error. Should be 162.3 // | >>> number.round(10.71, 0, 2.5) // | 10.75 var factor = 10 / (increment || 10); return (factor * +value).toFixed(places) / factor; // Number }; if((0.9).toFixed() == 0){ // (isIE) toFixed() bug workaround: Rounding fails on IE when most significant digit // is just after the rounding place and is >=5 var round = number.round; number.round = function(v, p, m){ var d = Math.pow(10, -p || 0), a = Math.abs(v); if(!v || a >= d){ d = 0; }else{ a /= d; if(a < 0.5 || a >= 0.95){ d = 0; } } return round(v, p, m) + (v > 0 ? d : -d); }; // Use "doc hint" so the doc parser ignores this new definition of round(), and uses the one above. /*===== number.round = round; =====*/ } /*===== number.__FormatAbsoluteOptions = declare(null, { // decimal: String? // the decimal separator // group: String? // the group separator // places: Number|String? // number of decimal places. the range "n,m" will format to m places. // round: Number? // 5 rounds to nearest .5; 0 rounds to nearest whole (default). -1 // means don't round. }); =====*/ number._formatAbsolute = function(/*Number*/ value, /*String*/ pattern, /*number.__FormatAbsoluteOptions?*/ options){ // summary: // Apply numeric pattern to absolute value using options. Gives no // consideration to local customs. // value: // the number to be formatted, ignores sign // pattern: // the number portion of a pattern (e.g. `#,##0.00`) options = options || {}; if(options.places === true){options.places=0;} if(options.places === Infinity){options.places=6;} // avoid a loop; pick a limit var patternParts = pattern.split("."), comma = typeof options.places == "string" && options.places.indexOf(","), maxPlaces = options.places; if(comma){ maxPlaces = options.places.substring(comma + 1); }else if(!(maxPlaces >= 0)){ maxPlaces = (patternParts[1] || []).length; } if(!(options.round < 0)){ value = number.round(value, maxPlaces, options.round); } var valueParts = String(Math.abs(value)).split("."), fractional = valueParts[1] || ""; if(patternParts[1] || options.places){ if(comma){ options.places = options.places.substring(0, comma); } // Pad fractional with trailing zeros var pad = options.places !== undefined ? options.places : (patternParts[1] && patternParts[1].lastIndexOf("0") + 1); if(pad > fractional.length){ valueParts[1] = dstring.pad(fractional, pad, '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 var patternDigits = patternParts[0].replace(',', ''); pad = patternDigits.indexOf("0"); if(pad != -1){ pad = patternDigits.length - pad; if(pad > valueParts[0].length){ valueParts[0] = dstring.pad(valueParts[0], pad); } // Truncate whole if(patternDigits.indexOf("#") == -1){ valueParts[0] = valueParts[0].substr(valueParts[0].length - pad); } } // Add group separators var index = patternParts[0].lastIndexOf(','), groupSize, groupSize2; if(index != -1){ groupSize = patternParts[0].length - index - 1; var remainder = patternParts[0].substr(0, index); index = remainder.lastIndexOf(','); if(index != -1){ groupSize2 = remainder.length - index - 1; } } var pieces = []; for(var whole = valueParts[0]; whole;){ var off = whole.length - groupSize; pieces.push((off > 0) ? whole.substr(off) : whole); whole = (off > 0) ? whole.slice(0, off) : ""; if(groupSize2){ groupSize = groupSize2; groupSize2 = undefined; } } valueParts[0] = pieces.reverse().join(options.group || ","); return valueParts.join(options.decimal || "."); }; /*===== number.__RegexpOptions = declare(null, { // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // locale: String? // override the locale used to determine formatting rules // strict: Boolean? // strict parsing, false by default. Strict parsing requires input as produced by the format() method. // Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators // places: Number|String? // number of decimal places to accept: Infinity, a positive number, or // a range "n,m". Defined by pattern or Infinity if pattern not provided. }); =====*/ number.regexp = function(/*number.__RegexpOptions?*/ options){ // summary: // Builds the regular needed to parse a number // description: // Returns regular expression with positive and negative match, group // and decimal separators return number._parseInfo(options).regexp; // String }; number._parseInfo = function(/*Object?*/ options){ options = options || {}; var locale = i18n.normalizeLocale(options.locale), bundle = i18n.getLocalization("dojo.cldr", "number", locale), pattern = options.pattern || bundle[(options.type || "decimal") + "Format"], //TODO: memoize? group = bundle.group, decimal = bundle.decimal, factor = 1; if(pattern.indexOf('%') != -1){ factor /= 100; }else if(pattern.indexOf('\u2030') != -1){ factor /= 1000; // per mille }else{ var isCurrency = pattern.indexOf('\u00a4') != -1; if(isCurrency){ group = bundle.currencyGroup || group; decimal = bundle.currencyDecimal || decimal; } } //TODO: handle quoted escapes var patternList = pattern.split(';'); if(patternList.length == 1){ patternList.push("-" + patternList[0]); } var re = dregexp.buildGroupRE(patternList, function(pattern){ pattern = "(?:"+dregexp.escapeString(pattern, '.')+")"; return pattern.replace(number._numberPatternRE, function(format){ var flags = { signed: false, separator: options.strict ? group : [group,""], fractional: options.fractional, decimal: decimal, exponent: false }, parts = format.split('.'), places = options.places; // special condition for percent (factor != 1) // allow decimal places even if not specified in pattern if(parts.length == 1 && factor != 1){ parts[1] = "###"; } if(parts.length == 1 || places === 0){ flags.fractional = false; }else{ if(places === undefined){ places = options.pattern ? parts[1].lastIndexOf('0') + 1 : Infinity; } if(places && options.fractional == undefined){flags.fractional = true;} // required fractional, unless otherwise specified if(!options.places && (places < parts[1].length)){ places += "," + parts[1].length; } flags.places = places; } var groups = parts[0].split(','); if(groups.length > 1){ flags.groupSize = groups.pop().length; if(groups.length > 1){ flags.groupSize2 = groups.pop().length; } } return "("+number._realNumberRegexp(flags)+")"; }); }, true); if(isCurrency){ // substitute the currency symbol for the placeholder in the pattern re = re.replace(/([\s\xa0\u202f]*)(\u00a4{1,3})([\s\xa0\u202f]*)/g, function(match, before, target, after){ var prop = ["symbol", "currency", "displayName"][target.length-1], symbol = dregexp.escapeString(options[prop] || options.currency || ""); // if there is no symbol there is no need to take white-spaces into account. if(!symbol){ return ""; } before = before ? "[\\s\\xa0\\u202f]" : ""; after = after ? "[\\s\\xa0\\u202f]" : ""; if(!options.strict){ if(before){before += "*";} if(after){after += "*";} return "(?:"+before+symbol+after+")?"; } return before+symbol+after; }); } //TODO: substitute localized sign/percent/permille/etc.? // normalize whitespace and return return {regexp: re.replace(/[\xa0\u202f ]/g, "[\\s\\xa0\\u202f]"), group: group, decimal: decimal, factor: factor}; // Object }; /*===== number.__ParseOptions = declare(null, { // pattern: String? // override [formatting pattern](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // with this string. Default value is based on locale. Overriding this property will defeat // localization. Literal characters in patterns are not supported. // type: String? // choose a format type based on the locale from the following: // decimal, scientific (not yet supported), percent, currency. decimal by default. // locale: String? // override the locale used to determine formatting rules // strict: Boolean? // strict parsing, false by default. Strict parsing requires input as produced by the format() method. // Non-strict is more permissive, e.g. flexible on white space, omitting thousands separators // fractional: Boolean|Array? // Whether to include the fractional portion, where the number of decimal places are implied by pattern // or explicit 'places' parameter. The value [true,false] makes the fractional portion optional. }); =====*/ number.parse = function(/*String*/ expression, /*number.__ParseOptions?*/ options){ // summary: // Convert a properly formatted string to a primitive Number, using // locale-specific settings. // description: // Create a Number from a string using a known localized pattern. // Formatting patterns are chosen appropriate to the locale // and follow the syntax described by // [unicode.org TR35](http://www.unicode.org/reports/tr35/#Number_Format_Patterns) // Note that literal characters in patterns are not supported. // expression: // A string representation of a Number var info = number._parseInfo(options), results = (new RegExp("^"+info.regexp+"$")).exec(expression); if(!results){ return NaN; //NaN } var absoluteMatch = results[1]; // match for the positive expression if(!results[1]){ if(!results[2]){ return NaN; //NaN } // matched the negative pattern absoluteMatch =results[2]; info.factor *= -1; } // Transform it to something Javascript can parse as a number. Normalize // decimal point and strip out group separators or alternate forms of whitespace absoluteMatch = absoluteMatch. replace(new RegExp("["+info.group + "\\s\\xa0\\u202f"+"]", "g"), ""). replace(info.decimal, "."); // Adjust for negative sign, percent, etc. as necessary return absoluteMatch * info.factor; //Number }; /*===== number.__RealNumberRegexpFlags = declare(null, { // places: Number? // The integer number of decimal places or a range given as "n,m". If // not given, the decimal part is optional and the number of places is // unlimited. // decimal: String? // A string for the character used as the decimal point. Default // is ".". // fractional: Boolean|Array? // Whether decimal places are used. Can be true, false, or [true, // false]. Default is [true, false] which means optional. // exponent: Boolean|Array? // Express in exponential notation. Can be true, false, or [true, // false]. Default is [true, false], (i.e. will match if the // exponential part is present are not). // eSigned: Boolean|Array? // The leading plus-or-minus sign on the exponent. Can be true, // false, or [true, false]. Default is [true, false], (i.e. will // match if it is signed or unsigned). flags in regexp.integer can be // applied. }); =====*/ number._realNumberRegexp = function(/*__RealNumberRegexpFlags?*/ flags){ // summary: // Builds a regular expression to match a real number in exponential // notation // assign default values to missing parameters flags = flags || {}; //TODO: use mixin instead? if(!("places" in flags)){ flags.places = Infinity; } if(typeof flags.decimal != "string"){ flags.decimal = "."; } if(!("fractional" in flags) || /^0/.test(flags.places)){ flags.fractional = [true, false]; } if(!("exponent" in flags)){ flags.exponent = [true, false]; } if(!("eSigned" in flags)){ flags.eSigned = [true, false]; } var integerRE = number._integerRegexp(flags), decimalRE = dregexp.buildGroupRE(flags.fractional, function(q){ var re = ""; if(q && (flags.places!==0)){ re = "\\" + flags.decimal; if(flags.places == Infinity){ re = "(?:" + re + "\\d+)?"; }else{ re += "\\d{" + flags.places + "}"; } } return re; }, true ); var exponentRE = dregexp.buildGroupRE(flags.exponent, function(q){ if(q){ return "([eE]" + number._integerRegexp({ signed: flags.eSigned}) + ")"; } return ""; } ); var realRE = integerRE + decimalRE; // allow for decimals without integers, e.g. .25 if(decimalRE){realRE = "(?:(?:"+ realRE + ")|(?:" + decimalRE + "))";} return realRE + exponentRE; // String }; /*===== number.__IntegerRegexpFlags = declare(null, { // signed: Boolean? // The leading plus-or-minus sign. Can be true, false, or `[true,false]`. // Default is `[true, false]`, (i.e. will match if it is signed // or unsigned). // separator: String? // The character used as the thousands separator. Default is no // separator. For more than one symbol use an array, e.g. `[",", ""]`, // makes ',' optional. // groupSize: Number? // group size between separators // groupSize2: Number? // second grouping, where separators 2..n have a different interval than the first separator (for India) }); =====*/ number._integerRegexp = function(/*number.__IntegerRegexpFlags?*/ flags){ // summary: // Builds a regular expression that matches an integer // assign default values to missing parameters flags = flags || {}; if(!("signed" in flags)){ flags.signed = [true, false]; } if(!("separator" in flags)){ flags.separator = ""; }else if(!("groupSize" in flags)){ flags.groupSize = 3; } var signRE = dregexp.buildGroupRE(flags.signed, function(q){ return q ? "[-+]" : ""; }, true ); var numberRE = dregexp.buildGroupRE(flags.separator, function(sep){ if(!sep){ return "(?:\\d+)"; } sep = dregexp.escapeString(sep); if(sep == " "){ sep = "\\s"; } else if(sep == "\xa0"){ sep = "\\s\\xa0"; } else if(sep == "\u202f"){ sep = "\\s\\u202f"; } var grp = flags.groupSize, grp2 = flags.groupSize2; //TODO: should we continue to enforce that numbers with separators begin with 1-9? See #6933 if(grp2){ var grp2RE = "(?:0|[1-9]\\d{0," + (grp2-1) + "}(?:[" + sep + "]\\d{" + grp2 + "})*[" + sep + "]\\d{" + grp + "})"; return ((grp-grp2) > 0) ? "(?:" + grp2RE + "|(?:0|[1-9]\\d{0," + (grp-1) + "}))" : grp2RE; } return "(?:0|[1-9]\\d{0," + (grp-1) + "}(?:[" + sep + "]\\d{" + grp + "})*)"; }, true ); return signRE + numberRE; // String }; return number; });