Skip to content

Commit

Permalink
WIP: basic working code - needs optmizing
Browse files Browse the repository at this point in the history
  • Loading branch information
petebacondarwin committed Nov 27, 2015
1 parent 0b5ecc6 commit c512845
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 72 deletions.
225 changes: 160 additions & 65 deletions src/ng/filter/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,93 +139,188 @@ function numberFilter($locale) {
};
}

var isValid = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
function parse(numStr) {
var parsedNumber = {};
var exponent, i, j, zeros;

// Decimal point?
if ((exponent = numStr.indexOf('.')) > -1) {
numStr = numStr.replace('.', '');
}
// Exponential form?
if ((i = numStr.search(/e/i)) > 0) {
// Determine exponent.
if (exponent < 0) {
exponent = i;
}
exponent += +numStr.slice(i + 1);
numStr = numStr.substring(0, i);
} else if (exponent < 0) {
// Integer.
exponent = numStr.length;
}
// Determine leading zeros.
i = 0;
while (numStr.charAt(i) == '0') i++;

if (i == (zeros = numStr.length)) {
// Zero.
parsedNumber.digits = [parsedNumber.exponent = 0];
} else {
// Determine trailing zeros.
do {
zeros--;
} while (numStr.charAt(zeros) == '0');
parsedNumber.exponent = exponent - i - 1;
parsedNumber.digits = [];
// Convert string to array of digits without leading/trailing zeros.
j = 0;
while (i <= zeros) {
parsedNumber.digits[j++] = +numStr.charAt(i++);
}
}
return parsedNumber;
}

function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
var digit = 0;
var digits = parsedNumber.digits;
var exponent = parsedNumber.exponent;
var fractionLen = digits.length - 1 - exponent;

// determine fractionSize if it is not specified
if (isUndefined(fractionSize)) {
fractionSize = Math.min(Math.max(minFrac, fractionLen), maxFrac);
}

// Cut off unwanted digits with rounding
while (fractionLen > fractionSize && digits.length) {
digit = digits.pop();
// Round up if necessary
if (digit >= 5) digits[digits.length - 1]++;
fractionLen--;
}

if (digits.length === 0) {
// We rounded to zero so reset the parsedNumber
parsedNumber.exponent = 0;
// If the last removed digit was >= 5 then we need to round up
if (digit >= 5) digits.push(1);
// Pad out with the necessary zeros
while (digits.length <= fractionSize) digits.unshift(0);
}

while (fractionLen < fractionSize) {
digits.push(0);
fractionLen++;
}

// Do a final clear of any carrying, e.g. the last digit was rounded up to 10
var carry = digits.reduceRight(function(carry, d, i, digits) {
d = d + carry;
digits[i] = d % 10;
return Math.floor(d / 10);
}, 0);
if (carry) {
digits.unshift(carry);
parsedNumber.exponent++;
}
}

var DECIMAL_SEP = '.';
var MAX_DIGITS = 21;

/**
* Format a number into a string
* @param {number} number The number to format
* @param {{
* minFrac, // the minimum number of digits required in the fraction part of the number
* maxFrac, // the maximum number of digits required in the fraction part of the number
* gSize, // number of digits in each group of separated digits
* lgSize, // number of digits in the last group of digits before the decimal separator
* negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
* posPre, // the string to go in front of a positive number
* negSuf, // the string to go after a negative number (e.g. `)`)
* posSuf // the string to go after a positive number
* }} pattern
* @param {string} groupSep The string to separate groups of number (e.g. `,`)
* @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
* @param {[type]} fractionSize The size of the fractional part of the number
* @return {string} The number formatted as a string
*/
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
if (isObject(number)) return '';

if (isObject(number)) return '';
var isInfinity = number === Infinity || number === -Infinity;
if (!isInfinity && !isFinite(number)) return '';
var isNegative = number < 0;
var isZero = false;
number = Math.abs(number);

var isInfinity = number === Infinity;
if (!isInfinity && !isFinite(number)) return '';

var numStr = number + '',
formatedText = '',
hasExponent = false,
parts = [];
formattedText = '',
realExponent = 0,
parsedNumber;

if (isInfinity) formatedText = '\u221e';
if (isInfinity) {
formattedText = '\u221e';
} else {
parsedNumber = parse(numStr);

if (!isInfinity && numStr.indexOf('e') !== -1) {
var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
if (match && match[2] == '-' && match[3] > fractionSize + 1) {
number = 0;
} else {
formatedText = numStr;
hasExponent = true;
if (parsedNumber.exponent > MAX_DIGITS) {
parsedNumber.digits = parsedNumber.digits.splice(0, MAX_DIGITS - 1);
realExponent = parsedNumber.exponent;
parsedNumber.exponent = 0;
}
}

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

// determine fractionSize if it is not specified
if (isUndefined(fractionSize)) {
fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
var digits = parsedNumber.digits;
var exponent = parsedNumber.exponent;
var decimals = [];
isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);

// pad zeros for small numbers
while (exponent < -1) {
digits.unshift(0);
exponent++;
}

// safely round numbers in JS without hitting imprecisions of floating-point arithmetics
// inspired by:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);

var fraction = ('' + number).split(DECIMAL_SEP);
var whole = fraction[0];
fraction = fraction[1] || '';

var i, pos = 0,
lgroup = pattern.lgSize,
group = pattern.gSize;

if (whole.length >= (lgroup + group)) {
pos = whole.length - lgroup;
for (i = 0; i < pos; i++) {
if ((pos - i) % group === 0 && i !== 0) {
formatedText += groupSep;
}
formatedText += whole.charAt(i);
}
// extract decimals digits
if (exponent >= 0) {
decimals = digits.splice(exponent + 1);
} else {
decimals = digits;
digits = [0];
}

for (i = pos; i < whole.length; i++) {
if ((whole.length - i) % lgroup === 0 && i !== 0) {
formatedText += groupSep;
}
formatedText += whole.charAt(i);
// format the integer digits with grouping separators
var groups = [];
if (digits.length > pattern.lgSize) {
groups.unshift(digits.splice(-pattern.lgSize).join(''));
}
while (digits.length > pattern.gSize) {
groups.unshift(digits.splice(-pattern.gSize).join(''));
}
if (digits.length) {
groups.unshift(digits.join(''));
}
formattedText = groups.join(groupSep);

// format fraction part.
while (fraction.length < fractionSize) {
fraction += '0';
// append the decimal digits
if (decimals.length) {
formattedText += decimalSep + decimals.join('');
}

if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
} else {
if (fractionSize > 0 && number < 1) {
formatedText = number.toFixed(fractionSize);
number = parseFloat(formatedText);
formatedText = formatedText.replace(DECIMAL_SEP, decimalSep);
if (realExponent) {
formattedText += 'e+' + realExponent;
}
}

if (number === 0) {
isNegative = false;
if (isNegative && !isZero) {
return pattern.negPre + formattedText + pattern.negSuf;
} else {
return pattern.posPre + formattedText + pattern.posSuf;
}

parts.push(isNegative ? pattern.negPre : pattern.posPre,
formatedText,
isNegative ? pattern.negSuf : pattern.posSuf);
return parts.join('');
}

function padNumber(num, digits, trim) {
Expand Down
30 changes: 23 additions & 7 deletions test/ng/filter/filtersSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ describe('filters', function() {
expect(formatNumber(-0.0001, pattern, ',', '.', 3)).toBe('0.000');
expect(formatNumber(-0.0000001, pattern, ',', '.', 6)).toBe('0.000000');
});

it('should work with numbers that are close to the limit for exponent notation', function() {
// previously, numbers that n * (10 ^ fractionSize) > localLimitMax
// were ending up with a second exponent in them, then coercing to
// NaN when formatNumber rounded them with the safe rounding
// function.

var localLimitMax = 999999999999999900000,
localLimitMin = 10000000000000000000,
exampleNumber = 444444444400000000000;

expect(formatNumber(localLimitMax, pattern, ',', '.', 2))
.toBe('999,999,999,999,999,900,000.00');
expect(formatNumber(localLimitMin, pattern, ',', '.', 2))
.toBe('10,000,000,000,000,000,000.00');
expect(formatNumber(exampleNumber, pattern, ',', '.', 2))
.toBe('444,444,444,400,000,000,000.00');

});
});

describe('currency', function() {
Expand Down Expand Up @@ -186,13 +205,10 @@ describe('filters', function() {
});

it('should filter exponentially large numbers', function() {
expect(number(1e50)).toEqual('1e+50');
expect(number(-2e100)).toEqual('-2e+100');
});

it('should ignore fraction sizes for large numbers', function() {
expect(number(1e50, 2)).toEqual('1e+50');
expect(number(-2e100, 5)).toEqual('-2e+100');
expect(number(1.23e50)).toEqual('1.23e+50');
expect(number(-2.3456e100)).toEqual('-2.346e+100');
expect(number(1e50, 2)).toEqual('1.00e+50');
expect(number(-2e100, 5)).toEqual('-2.00000e+100');
});

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

0 comments on commit c512845

Please sign in to comment.