Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit c512845

Browse files
WIP: basic working code - needs optmizing
1 parent 0b5ecc6 commit c512845

File tree

2 files changed

+183
-72
lines changed

2 files changed

+183
-72
lines changed

src/ng/filter/filters.js

+160-65
Original file line numberDiff line numberDiff line change
@@ -139,93 +139,188 @@ function numberFilter($locale) {
139139
};
140140
}
141141

142+
var isValid = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
143+
function parse(numStr) {
144+
var parsedNumber = {};
145+
var exponent, i, j, zeros;
146+
147+
// Decimal point?
148+
if ((exponent = numStr.indexOf('.')) > -1) {
149+
numStr = numStr.replace('.', '');
150+
}
151+
// Exponential form?
152+
if ((i = numStr.search(/e/i)) > 0) {
153+
// Determine exponent.
154+
if (exponent < 0) {
155+
exponent = i;
156+
}
157+
exponent += +numStr.slice(i + 1);
158+
numStr = numStr.substring(0, i);
159+
} else if (exponent < 0) {
160+
// Integer.
161+
exponent = numStr.length;
162+
}
163+
// Determine leading zeros.
164+
i = 0;
165+
while (numStr.charAt(i) == '0') i++;
166+
167+
if (i == (zeros = numStr.length)) {
168+
// Zero.
169+
parsedNumber.digits = [parsedNumber.exponent = 0];
170+
} else {
171+
// Determine trailing zeros.
172+
do {
173+
zeros--;
174+
} while (numStr.charAt(zeros) == '0');
175+
parsedNumber.exponent = exponent - i - 1;
176+
parsedNumber.digits = [];
177+
// Convert string to array of digits without leading/trailing zeros.
178+
j = 0;
179+
while (i <= zeros) {
180+
parsedNumber.digits[j++] = +numStr.charAt(i++);
181+
}
182+
}
183+
return parsedNumber;
184+
}
185+
186+
function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
187+
var digit = 0;
188+
var digits = parsedNumber.digits;
189+
var exponent = parsedNumber.exponent;
190+
var fractionLen = digits.length - 1 - exponent;
191+
192+
// determine fractionSize if it is not specified
193+
if (isUndefined(fractionSize)) {
194+
fractionSize = Math.min(Math.max(minFrac, fractionLen), maxFrac);
195+
}
196+
197+
// Cut off unwanted digits with rounding
198+
while (fractionLen > fractionSize && digits.length) {
199+
digit = digits.pop();
200+
// Round up if necessary
201+
if (digit >= 5) digits[digits.length - 1]++;
202+
fractionLen--;
203+
}
204+
205+
if (digits.length === 0) {
206+
// We rounded to zero so reset the parsedNumber
207+
parsedNumber.exponent = 0;
208+
// If the last removed digit was >= 5 then we need to round up
209+
if (digit >= 5) digits.push(1);
210+
// Pad out with the necessary zeros
211+
while (digits.length <= fractionSize) digits.unshift(0);
212+
}
213+
214+
while (fractionLen < fractionSize) {
215+
digits.push(0);
216+
fractionLen++;
217+
}
218+
219+
// Do a final clear of any carrying, e.g. the last digit was rounded up to 10
220+
var carry = digits.reduceRight(function(carry, d, i, digits) {
221+
d = d + carry;
222+
digits[i] = d % 10;
223+
return Math.floor(d / 10);
224+
}, 0);
225+
if (carry) {
226+
digits.unshift(carry);
227+
parsedNumber.exponent++;
228+
}
229+
}
230+
142231
var DECIMAL_SEP = '.';
232+
var MAX_DIGITS = 21;
233+
234+
/**
235+
* Format a number into a string
236+
* @param {number} number The number to format
237+
* @param {{
238+
* minFrac, // the minimum number of digits required in the fraction part of the number
239+
* maxFrac, // the maximum number of digits required in the fraction part of the number
240+
* gSize, // number of digits in each group of separated digits
241+
* lgSize, // number of digits in the last group of digits before the decimal separator
242+
* negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
243+
* posPre, // the string to go in front of a positive number
244+
* negSuf, // the string to go after a negative number (e.g. `)`)
245+
* posSuf // the string to go after a positive number
246+
* }} pattern
247+
* @param {string} groupSep The string to separate groups of number (e.g. `,`)
248+
* @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
249+
* @param {[type]} fractionSize The size of the fractional part of the number
250+
* @return {string} The number formatted as a string
251+
*/
143252
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
144-
if (isObject(number)) return '';
145253

254+
if (isObject(number)) return '';
255+
var isInfinity = number === Infinity || number === -Infinity;
256+
if (!isInfinity && !isFinite(number)) return '';
146257
var isNegative = number < 0;
258+
var isZero = false;
147259
number = Math.abs(number);
148-
149-
var isInfinity = number === Infinity;
150-
if (!isInfinity && !isFinite(number)) return '';
151-
152260
var numStr = number + '',
153-
formatedText = '',
154-
hasExponent = false,
155-
parts = [];
261+
formattedText = '',
262+
realExponent = 0,
263+
parsedNumber;
156264

157-
if (isInfinity) formatedText = '\u221e';
265+
if (isInfinity) {
266+
formattedText = '\u221e';
267+
} else {
268+
parsedNumber = parse(numStr);
158269

159-
if (!isInfinity && numStr.indexOf('e') !== -1) {
160-
var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
161-
if (match && match[2] == '-' && match[3] > fractionSize + 1) {
162-
number = 0;
163-
} else {
164-
formatedText = numStr;
165-
hasExponent = true;
270+
if (parsedNumber.exponent > MAX_DIGITS) {
271+
parsedNumber.digits = parsedNumber.digits.splice(0, MAX_DIGITS - 1);
272+
realExponent = parsedNumber.exponent;
273+
parsedNumber.exponent = 0;
166274
}
167-
}
168275

169-
if (!isInfinity && !hasExponent) {
170-
var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
276+
roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
171277

172-
// determine fractionSize if it is not specified
173-
if (isUndefined(fractionSize)) {
174-
fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
278+
var digits = parsedNumber.digits;
279+
var exponent = parsedNumber.exponent;
280+
var decimals = [];
281+
isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
282+
283+
// pad zeros for small numbers
284+
while (exponent < -1) {
285+
digits.unshift(0);
286+
exponent++;
175287
}
176288

177-
// safely round numbers in JS without hitting imprecisions of floating-point arithmetics
178-
// inspired by:
179-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
180-
number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);
181-
182-
var fraction = ('' + number).split(DECIMAL_SEP);
183-
var whole = fraction[0];
184-
fraction = fraction[1] || '';
185-
186-
var i, pos = 0,
187-
lgroup = pattern.lgSize,
188-
group = pattern.gSize;
189-
190-
if (whole.length >= (lgroup + group)) {
191-
pos = whole.length - lgroup;
192-
for (i = 0; i < pos; i++) {
193-
if ((pos - i) % group === 0 && i !== 0) {
194-
formatedText += groupSep;
195-
}
196-
formatedText += whole.charAt(i);
197-
}
289+
// extract decimals digits
290+
if (exponent >= 0) {
291+
decimals = digits.splice(exponent + 1);
292+
} else {
293+
decimals = digits;
294+
digits = [0];
198295
}
199296

200-
for (i = pos; i < whole.length; i++) {
201-
if ((whole.length - i) % lgroup === 0 && i !== 0) {
202-
formatedText += groupSep;
203-
}
204-
formatedText += whole.charAt(i);
297+
// format the integer digits with grouping separators
298+
var groups = [];
299+
if (digits.length > pattern.lgSize) {
300+
groups.unshift(digits.splice(-pattern.lgSize).join(''));
301+
}
302+
while (digits.length > pattern.gSize) {
303+
groups.unshift(digits.splice(-pattern.gSize).join(''));
205304
}
305+
if (digits.length) {
306+
groups.unshift(digits.join(''));
307+
}
308+
formattedText = groups.join(groupSep);
206309

207-
// format fraction part.
208-
while (fraction.length < fractionSize) {
209-
fraction += '0';
310+
// append the decimal digits
311+
if (decimals.length) {
312+
formattedText += decimalSep + decimals.join('');
210313
}
211314

212-
if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
213-
} else {
214-
if (fractionSize > 0 && number < 1) {
215-
formatedText = number.toFixed(fractionSize);
216-
number = parseFloat(formatedText);
217-
formatedText = formatedText.replace(DECIMAL_SEP, decimalSep);
315+
if (realExponent) {
316+
formattedText += 'e+' + realExponent;
218317
}
219318
}
220-
221-
if (number === 0) {
222-
isNegative = false;
319+
if (isNegative && !isZero) {
320+
return pattern.negPre + formattedText + pattern.negSuf;
321+
} else {
322+
return pattern.posPre + formattedText + pattern.posSuf;
223323
}
224-
225-
parts.push(isNegative ? pattern.negPre : pattern.posPre,
226-
formatedText,
227-
isNegative ? pattern.negSuf : pattern.posSuf);
228-
return parts.join('');
229324
}
230325

231326
function padNumber(num, digits, trim) {

test/ng/filter/filtersSpec.js

+23-7
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ describe('filters', function() {
9292
expect(formatNumber(-0.0001, pattern, ',', '.', 3)).toBe('0.000');
9393
expect(formatNumber(-0.0000001, pattern, ',', '.', 6)).toBe('0.000000');
9494
});
95+
96+
it('should work with numbers that are close to the limit for exponent notation', function() {
97+
// previously, numbers that n * (10 ^ fractionSize) > localLimitMax
98+
// were ending up with a second exponent in them, then coercing to
99+
// NaN when formatNumber rounded them with the safe rounding
100+
// function.
101+
102+
var localLimitMax = 999999999999999900000,
103+
localLimitMin = 10000000000000000000,
104+
exampleNumber = 444444444400000000000;
105+
106+
expect(formatNumber(localLimitMax, pattern, ',', '.', 2))
107+
.toBe('999,999,999,999,999,900,000.00');
108+
expect(formatNumber(localLimitMin, pattern, ',', '.', 2))
109+
.toBe('10,000,000,000,000,000,000.00');
110+
expect(formatNumber(exampleNumber, pattern, ',', '.', 2))
111+
.toBe('444,444,444,400,000,000,000.00');
112+
113+
});
95114
});
96115

97116
describe('currency', function() {
@@ -186,13 +205,10 @@ describe('filters', function() {
186205
});
187206

188207
it('should filter exponentially large numbers', function() {
189-
expect(number(1e50)).toEqual('1e+50');
190-
expect(number(-2e100)).toEqual('-2e+100');
191-
});
192-
193-
it('should ignore fraction sizes for large numbers', function() {
194-
expect(number(1e50, 2)).toEqual('1e+50');
195-
expect(number(-2e100, 5)).toEqual('-2e+100');
208+
expect(number(1.23e50)).toEqual('1.23e+50');
209+
expect(number(-2.3456e100)).toEqual('-2.346e+100');
210+
expect(number(1e50, 2)).toEqual('1.00e+50');
211+
expect(number(-2e100, 5)).toEqual('-2.00000e+100');
196212
});
197213

198214
it('should filter exponentially small numbers', function() {

0 commit comments

Comments
 (0)