You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
20 KiB
565 lines
20 KiB
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; |
|
});
|
|
|