From 07b81ae741e2420487d125df785643494117d85c Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Fri, 10 Nov 2017 10:41:15 +0100 Subject: [PATCH] fix(common): handle JS floating point errors in percent pipe (#20329) Fixes #20136 PR Close #20329 --- packages/common/src/i18n/format_number.ts | 56 +++++++++++++++---- .../common/test/pipes/number_pipe_spec.ts | 16 ++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/packages/common/src/i18n/format_number.ts b/packages/common/src/i18n/format_number.ts index e001ffb432..6ea59aac7b 100644 --- a/packages/common/src/i18n/format_number.ts +++ b/packages/common/src/i18n/format_number.ts @@ -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); diff --git a/packages/common/test/pipes/number_pipe_spec.ts b/packages/common/test/pipes/number_pipe_spec.ts index 92c61b91ca..5a34182868 100644 --- a/packages/common/test/pipes/number_pipe_spec.ts +++ b/packages/common/test/pipes/number_pipe_spec.ts @@ -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', () => {