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