number.js 19 KB

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