fix(common): handle JS floating point errors in percent pipe (#20329)

Fixes #20136
PR Close #20329
This commit is contained in:
Olivier Combe 2017-11-10 10:41:15 +01:00 committed by Igor Minar
parent 5a7bf36723
commit 07b81ae741
2 changed files with 61 additions and 11 deletions

View File

@ -46,11 +46,6 @@ export function formatNumber(
num = value;
}
if (style === NumberFormatStyle.Percent) {
num = num * 100;
}
const numStr = Math.abs(num) + '';
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
let formattedText = '';
let isZero = false;
@ -58,7 +53,11 @@ export function formatNumber(
if (!isFinite(num)) {
formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity);
} else {
const parsedNumber = parseNumber(numStr);
let parsedNumber = parseNumber(num);
if (style === NumberFormatStyle.Percent) {
parsedNumber = toPercent(parsedNumber);
}
let minInt = pattern.minInt;
let minFraction = pattern.minFrac;
@ -249,11 +248,35 @@ interface ParsedNumber {
integerLen: number;
}
// Transforms a parsed number into a percentage by multiplying it by 100
function toPercent(parsedNumber: ParsedNumber): ParsedNumber {
// if the number is 0, don't do anything
if (parsedNumber.digits[0] === 0) {
return parsedNumber;
}
// Getting the current number of decimals
const fractionLen = parsedNumber.digits.length - parsedNumber.integerLen;
if (parsedNumber.exponent) {
parsedNumber.exponent += 2;
} else {
if (fractionLen === 0) {
parsedNumber.digits.push(0, 0);
} else if (fractionLen === 1) {
parsedNumber.digits.push(0);
}
parsedNumber.integerLen += 2;
}
return parsedNumber;
}
/**
* Parse a number (as a string)
* Parses a number.
* Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/
*/
function parseNumber(numStr: string): ParsedNumber {
function parseNumber(num: number): ParsedNumber {
let numStr = Math.abs(num) + '';
let exponent = 0, digits, integerLen;
let i, j, zeros;
@ -356,12 +379,23 @@ function roundNumber(parsedNumber: ParsedNumber, minFrac: number, maxFrac: numbe
// Pad out with zeros to get the required fraction length
for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
let dropTrailingZeros = fractionSize !== 0;
// Minimal length = nb of decimals required + current nb of integers
// Any number besides that is optional and can be removed if it's a trailing 0
const minLen = minFrac + parsedNumber.integerLen;
// Do any carrying, e.g. a digit was rounded up to 10
const carry = digits.reduceRight(function(carry, d, i, digits) {
d = d + carry;
digits[i] = d % 10;
return Math.floor(d / 10);
digits[i] = d < 10 ? d : d - 10; // d % 10
if (dropTrailingZeros) {
// Do not keep meaningless fractional trailing zeros (e.g. 15.52000 --> 15.52)
if (digits[i] === 0 && i >= minLen) {
digits.pop();
} else {
dropTrailingZeros = false;
}
}
return d >= 10 ? 1 : 0; // Math.floor(d / 10);
}, 0);
if (carry) {
digits.unshift(carry);

View File

@ -79,6 +79,22 @@ export function main() {
expect(pipe.transform(1.2, '.2')).toEqual('120.00%');
expect(pipe.transform(1.2, '4.2')).toEqual('0,120.00%');
expect(pipe.transform(1.2, '4.2', 'fr')).toEqual('0 120,00 %');
// see issue #20136
expect(pipe.transform(0.12345674, '0.0-10')).toEqual('12.345674%');
expect(pipe.transform(0, '0.0-10')).toEqual('0%');
expect(pipe.transform(0.00, '0.0-10')).toEqual('0%');
expect(pipe.transform(1, '0.0-10')).toEqual('100%');
expect(pipe.transform(0.1, '0.0-10')).toEqual('10%');
expect(pipe.transform(0.12, '0.0-10')).toEqual('12%');
expect(pipe.transform(0.123, '0.0-10')).toEqual('12.3%');
expect(pipe.transform(12.3456, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345600, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345699999, '0.0-6')).toEqual('1,234.57%');
expect(pipe.transform(12.345699999, '0.4-6')).toEqual('1,234.5700%');
expect(pipe.transform(100, '0.4-6')).toEqual('10,000.0000%');
expect(pipe.transform(100, '0.0-10')).toEqual('10,000%');
expect(pipe.transform(1.5e2)).toEqual('15,000%');
expect(pipe.transform(1e100)).toEqual('1E+102%');
});
it('should not support other objects', () => {