1
0

number-functions.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. /*
  2. * Copyright (C) 2006 Baron Schwartz <baron at sequent dot org>
  3. *
  4. * This program is free software; you can redistribute it and/or modify it
  5. * under the terms of the GNU Lesser General Public License as published by the
  6. * Free Software Foundation, version 2.1.
  7. *
  8. * This program is distributed in the hope that it will be useful, but WITHOUT
  9. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  10. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  11. * details.
  12. *
  13. * $Revision: 1.3 $
  14. */
  15. // Abbreviations: LODP = Left Of Decimal Point, RODP = Right Of Decimal Point
  16. Number.formatFunctions = {count:0};
  17. // Constants useful for controlling the format of numbers in special cases.
  18. Number.prototype.NaN = 'NaN';
  19. Number.prototype.posInfinity = 'Infinity';
  20. Number.prototype.negInfinity = '-Infinity';
  21. Number.prototype.numberFormat = function(format, context) {
  22. if (isNaN(this) ) {
  23. return Number.prototype.NaNstring;
  24. }
  25. else if (this == +Infinity ) {
  26. return Number.prototype.posInfinity;
  27. }
  28. else if ( this == -Infinity) {
  29. return Number.prototype.negInfinity;
  30. }
  31. else if (Number.formatFunctions[format] == null) {
  32. Number.createNewFormat(format);
  33. }
  34. return this[Number.formatFunctions[format]](context);
  35. }
  36. Number.createNewFormat = function(format) {
  37. var funcName = "format" + Number.formatFunctions.count++;
  38. Number.formatFunctions[format] = funcName;
  39. var code = "Number.prototype." + funcName + " = function(context){\n";
  40. // Decide whether the function is a terminal or a pos/neg/zero function
  41. var formats = format.split(";");
  42. switch (formats.length) {
  43. case 1:
  44. code += Number.createTerminalFormat(format);
  45. break;
  46. case 2:
  47. code += "return (this < 0) ? this.numberFormat(\""
  48. + String.escape(formats[1])
  49. + "\", 1) : this.numberFormat(\""
  50. + String.escape(formats[0])
  51. + "\", 2);";
  52. break;
  53. case 3:
  54. code += "return (this < 0) ? this.numberFormat(\""
  55. + String.escape(formats[1])
  56. + "\", 1) : ((this == 0) ? this.numberFormat(\""
  57. + String.escape(formats[2])
  58. + "\", 2) : this.numberFormat(\""
  59. + String.escape(formats[0])
  60. + "\", 3));";
  61. break;
  62. default:
  63. code += "throw 'Too many semicolons in format string';";
  64. break;
  65. }
  66. eval(code + "}");
  67. }
  68. Number.createTerminalFormat = function(format) {
  69. // If there is no work to do, just return the literal value
  70. if (format.length > 0 && format.search(/[0#?]/) == -1) {
  71. return "return '" + String.escape(format) + "';\n";
  72. }
  73. // Negative values are always displayed without a minus sign when section separators are used.
  74. var code = "var val = (context == null) ? new Number(this) : Math.abs(this);\n";
  75. var thousands = false;
  76. var lodp = format;
  77. var rodp = "";
  78. var ldigits = 0;
  79. var rdigits = 0;
  80. var scidigits = 0;
  81. var scishowsign = false;
  82. var sciletter = "";
  83. // Look for (and remove) scientific notation instructions, which can be anywhere
  84. m = format.match(/\..*(e)([+-]?)(0+)/i);
  85. if (m) {
  86. sciletter = m[1];
  87. scishowsign = (m[2] == "+");
  88. scidigits = m[3].length;
  89. format = format.replace(/(e)([+-]?)(0+)/i, "");
  90. }
  91. // Split around the decimal point
  92. var m = format.match(/^([^.]*)\.(.*)$/);
  93. if (m) {
  94. lodp = m[1].replace(/\./g, "");
  95. rodp = m[2].replace(/\./g, "");
  96. }
  97. // Look for %
  98. if (format.indexOf('%') >= 0) {
  99. code += "val *= 100;\n";
  100. }
  101. // Look for comma-scaling to the left of the decimal point
  102. m = lodp.match(/(,+)(?:$|[^0#?,])/);
  103. if (m) {
  104. code += "val /= " + Math.pow(1000, m[1].length) + "\n;";
  105. }
  106. // Look for comma-separators
  107. if (lodp.search(/[0#?],[0#?]/) >= 0) {
  108. thousands = true;
  109. }
  110. // Nuke any extraneous commas
  111. if ((m) || thousands) {
  112. lodp = lodp.replace(/,/g, "");
  113. }
  114. // Figure out how many digits to the l/r of the decimal place
  115. m = lodp.match(/0[0#?]*/);
  116. if (m) {
  117. ldigits = m[0].length;
  118. }
  119. m = rodp.match(/[0#?]*/);
  120. if (m) {
  121. rdigits = m[0].length;
  122. }
  123. // Scientific notation takes precedence over rounding etc
  124. if (scidigits > 0) {
  125. code += "var sci = Number.toScientific(val,"
  126. + ldigits + ", " + rdigits + ", " + scidigits + ", " + scishowsign + ");\n"
  127. + "var arr = [sci.l, sci.r];\n";
  128. }
  129. else {
  130. // If there is no decimal point, round to nearest integer, AWAY from zero
  131. if (format.indexOf('.') < 0) {
  132. code += "val = (val > 0) ? Math.ceil(val) : Math.floor(val);\n";
  133. }
  134. // Numbers are rounded to the correct number of digits to the right of the decimal
  135. code += "var arr = val.round(" + rdigits + ").toFixed(" + rdigits + ").split('.');\n";
  136. // There are at least "ldigits" digits to the left of the decimal, so add zeros if needed.
  137. code += "arr[0] = (val < 0 ? '-' : '') + String.leftPad((val < 0 ? arr[0].substring(1) : arr[0]), "
  138. + ldigits + ", '0');\n";
  139. }
  140. // Add thousands separators
  141. if (thousands) {
  142. code += "arr[0] = Number.addSeparators(arr[0]);\n";
  143. }
  144. // Insert the digits into the formatting string. On the LHS, extra digits are copied
  145. // into the result. On the RHS, rounding has chopped them off.
  146. code += "arr[0] = Number.injectIntoFormat(arr[0].reverse(), '"
  147. + String.escape(lodp.reverse()) + "', true).reverse();\n";
  148. if (rdigits > 0) {
  149. code += "arr[1] = Number.injectIntoFormat(arr[1], '" + String.escape(rodp) + "', false);\n";
  150. }
  151. if (scidigits > 0) {
  152. code += "arr[1] = arr[1].replace(/(\\d{" + rdigits + "})/, '$1" + sciletter + "' + sci.s);\n";
  153. }
  154. return code + "return arr.join('.');\n";
  155. }
  156. Number.toScientific = function(val, ldigits, rdigits, scidigits, showsign) {
  157. var result = {l:"", r:"", s:""};
  158. var ex = "";
  159. // Make ldigits + rdigits significant figures
  160. var before = Math.abs(val).toFixed(ldigits + rdigits + 1).trim('0');
  161. // Move the decimal point to the right of all digits we want to keep,
  162. // and round the resulting value off
  163. var after = Math.round(new Number(before.replace(".", "").replace(
  164. new RegExp("(\\d{" + (ldigits + rdigits) + "})(.*)"), "$1.$2"))).toFixed(0);
  165. // Place the decimal point in the new string
  166. if (after.length >= ldigits) {
  167. after = after.substring(0, ldigits) + "." + after.substring(ldigits);
  168. }
  169. else {
  170. after += '.';
  171. }
  172. // Find how much the decimal point moved. This is #places to LODP in the original
  173. // number, minus the #places in the new number. There are no left-padded zeroes in
  174. // the new number, so the calculation for it is simpler than for the old number.
  175. result.s = (before.indexOf(".") - before.search(/[1-9]/)) - after.indexOf(".");
  176. // The exponent is off by 1 when it gets moved to the left.
  177. if (result.s < 0) {
  178. result.s++;
  179. }
  180. // Split the value around the decimal point and pad the parts appropriately.
  181. result.l = (val < 0 ? '-' : '') + String.leftPad(after.substring(0, after.indexOf(".")), ldigits, "0");
  182. result.r = after.substring(after.indexOf(".") + 1);
  183. if (result.s < 0) {
  184. ex = "-";
  185. }
  186. else if (showsign) {
  187. ex = "+";
  188. }
  189. result.s = ex + String.leftPad(Math.abs(result.s).toFixed(0), scidigits, "0");
  190. return result;
  191. }
  192. Number.prototype.round = function(decimals) {
  193. if (decimals > 0) {
  194. var m = this.toFixed(decimals + 1).match(
  195. new RegExp("(-?\\d*)\.(\\d{" + decimals + "})(\\d)\\d*$"));
  196. if (m && m.length) {
  197. return new Number(m[1] + "." + String.leftPad(Math.round(m[2] + "." + m[3]), decimals, "0"));
  198. }
  199. }
  200. return this;
  201. }
  202. Number.injectIntoFormat = function(val, format, stuffExtras) {
  203. var i = 0;
  204. var j = 0;
  205. var result = "";
  206. var revneg = val.charAt(val.length - 1) == '-';
  207. if ( revneg ) {
  208. val = val.substring(0, val.length - 1);
  209. }
  210. while (i < format.length && j < val.length && format.substring(i).search(/[0#?]/) >= 0) {
  211. if (format.charAt(i).match(/[0#?]/)) {
  212. // It's a formatting character; copy the corresponding character
  213. // in the value to the result
  214. if (val.charAt(j) != '-') {
  215. result += val.charAt(j);
  216. }
  217. else {
  218. result += "0";
  219. }
  220. j++;
  221. }
  222. else {
  223. result += format.charAt(i);
  224. }
  225. ++i;
  226. }
  227. if ( revneg && j == val.length ) {
  228. result += '-';
  229. }
  230. if (j < val.length) {
  231. if (stuffExtras) {
  232. result += val.substring(j);
  233. }
  234. if ( revneg ) {
  235. result += '-';
  236. }
  237. }
  238. if (i < format.length) {
  239. result += format.substring(i);
  240. }
  241. return result.replace(/#/g, "").replace(/\?/g, " ");
  242. }
  243. Number.addSeparators = function(val) {
  244. return val.reverse().replace(/(\d{3})/g, "$1,").reverse().replace(/^(-)?,/, "$1");
  245. }
  246. String.prototype.reverse = function() {
  247. var res = "";
  248. for (var i = this.length; i > 0; --i) {
  249. res += this.charAt(i - 1);
  250. }
  251. return res;
  252. }
  253. String.prototype.trim = function(ch) {
  254. if (!ch) ch = ' ';
  255. return this.replace(new RegExp("^" + ch + "+|" + ch + "+$", "g"), "");
  256. }
  257. String.leftPad = function (val, size, ch) {
  258. var result = new String(val);
  259. if (ch == null) {
  260. ch = " ";
  261. }
  262. while (result.length < size) {
  263. result = ch + result;
  264. }
  265. return result;
  266. }
  267. String.escape = function(string) {
  268. return string.replace(/('|\\)/g, "\\$1");
  269. }