1
0
mirror of https://tt-rss.org/git/tt-rss.git synced 2024-06-27 12:05:06 +02:00
ttrss/lib/dojo/number.js

585 lines
21 KiB
JavaScript

/*
Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
if(!dojo._hasResource["dojo.number"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.number"] = true;
dojo.provide("dojo.number");
dojo.require("dojo.i18n");
dojo.requireLocalization("dojo.cldr", "number", null, "ROOT,ar,ca,cs,da,de,el,en,en-au,en-gb,es,fi,fr,he,hu,it,ja,ko,nb,nl,pl,pt,pt-pt,ru,sk,sl,sv,th,tr,zh");
dojo.require("dojo.string");
dojo.require("dojo.regexp");
/*=====
dojo.number = {
// summary: localized formatting and parsing routines for Number
}
dojo.number.__FormatOptions = function(){
// 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.
this.pattern = pattern;
this.type = type;
this.places = places;
this.round = round;
this.locale = locale;
this.fractional = fractional;
}
=====*/
dojo.number.format = function(/*Number*/value, /*dojo.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 = dojo.mixin({}, options || {});
var locale = dojo.i18n.normalizeLocale(options.locale),
bundle = dojo.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 dojo.number._applyPattern(value, pattern, options); // String
};
//dojo.number._numberPatternRE = /(?:[#0]*,?)*[#0](?:\.0*#*)?/; // not precise, but good enough
dojo.number._numberPatternRE = /[#0,]*[#0](?:\.0*#*)?/; // not precise, but good enough
dojo.number._applyPattern = function(/*Number*/value, /*String*/pattern, /*dojo.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: dojo.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(/\u00a4{1,3}/, function(match){
var prop = ["symbol", "currency", "displayName"][match.length-1];
return options[prop] || options.currency || "";
});
}else if(pattern.indexOf('E') != -1){
throw new Error("exponential notation not supported");
}
//TODO: support @ sig figs?
var numberPatternRE = dojo.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,
dojo.number._formatAbsolute(value, numberPattern[0], {decimal: decimal, group: group, places: options.places, round: options.round}));
}
dojo.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:
// >>> dojo.number.round(-0.5)
// -1
// >>> dojo.number.round(162.295, 2)
// 162.29 // note floating point error. Should be 162.3
// >>> dojo.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
(function(){
var round = dojo.number.round;
dojo.number.round = function(v, p, m){
var d = Math.pow(10, -p || 0), a = Math.abs(v);
if(!v || a >= d || a * Math.pow(10, p + 1) < 5){
d = 0;
}
return round(v, p, m) + (v > 0 ? d : -d);
}
})();
}
/*=====
dojo.number.__FormatAbsoluteOptions = function(){
// 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.
this.decimal = decimal;
this.group = group;
this.places = places;
this.round = round;
}
=====*/
dojo.number._formatAbsolute = function(/*Number*/value, /*String*/pattern, /*dojo.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 = dojo.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] = dojo.string.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] = dojo.string.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;
delete groupSize2;
}
}
valueParts[0] = pieces.reverse().join(options.group || ",");
return valueParts.join(options.decimal || ".");
};
/*=====
dojo.number.__RegexpOptions = function(){
// 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.
this.pattern = pattern;
this.type = type;
this.locale = locale;
this.strict = strict;
this.places = places;
}
=====*/
dojo.number.regexp = function(/*dojo.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 dojo.number._parseInfo(options).regexp; // String
}
dojo.number._parseInfo = function(/*Object?*/options){
options = options || {};
var locale = dojo.i18n.normalizeLocale(options.locale),
bundle = dojo.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 = dojo.regexp.buildGroupRE(patternList, function(pattern){
pattern = "(?:"+dojo.regexp.escapeString(pattern, '.')+")";
return pattern.replace(dojo.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 "("+dojo.number._realNumberRegexp(flags)+")";
});
}, true);
if(isCurrency){
// substitute the currency symbol for the placeholder in the pattern
re = re.replace(/([\s\xa0]*)(\u00a4{1,3})([\s\xa0]*)/g, function(match, before, target, after){
var prop = ["symbol", "currency", "displayName"][target.length-1],
symbol = dojo.regexp.escapeString(options[prop] || options.currency || "");
before = before ? "[\\s\\xa0]" : "";
after = after ? "[\\s\\xa0]" : "";
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 ]/g, "[\\s\\xa0]"), group: group, decimal: decimal, factor: factor}; // Object
}
/*=====
dojo.number.__ParseOptions = function(){
// 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.
this.pattern = pattern;
this.type = type;
this.locale = locale;
this.strict = strict;
this.fractional = fractional;
}
=====*/
dojo.number.parse = function(/*String*/expression, /*dojo.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 = dojo.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"+"]", "g"), "").
replace(info.decimal, ".");
// Adjust for negative sign, percent, etc. as necessary
return absoluteMatch * info.factor; //Number
};
/*=====
dojo.number.__RealNumberRegexpFlags = function(){
// 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.
this.places = places;
this.decimal = decimal;
this.fractional = fractional;
this.exponent = exponent;
this.eSigned = eSigned;
}
=====*/
dojo.number._realNumberRegexp = function(/*dojo.number.__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 = dojo.number._integerRegexp(flags),
decimalRE = dojo.regexp.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 = dojo.regexp.buildGroupRE(flags.exponent,
function(q){
if(q){ return "([eE]" + dojo.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
};
/*=====
dojo.number.__IntegerRegexpFlags = function(){
// 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)
this.signed = signed;
this.separator = separator;
this.groupSize = groupSize;
this.groupSize2 = groupSize2;
}
=====*/
dojo.number._integerRegexp = function(/*dojo.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 = dojo.regexp.buildGroupRE(flags.signed,
function(q){ return q ? "[-+]" : ""; },
true
);
var numberRE = dojo.regexp.buildGroupRE(flags.separator,
function(sep){
if(!sep){
return "(?:\\d+)";
}
sep = dojo.regexp.escapeString(sep);
if(sep == " "){ sep = "\\s"; }
else if(sep == "\xa0"){ sep = "\\s\\xa0"; }
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
}
}