diff --git a/HISTORY.md b/HISTORY.md index c2bfa04e6..49ee008e9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,7 +3,7 @@ ## not-yet-released, version 1.5.1 - Fixed #316: a bug in rounding values when formatting. -- Fixed #317: a bug in formatting negative values. +- Fixed #317, #319: a bug in formatting negative values. ## 2015-03-28, version 1.5.0 diff --git a/lib/util/NumberFormatter.js b/lib/util/NumberFormatter.js index fd367aaa4..f2b29b7f4 100644 --- a/lib/util/NumberFormatter.js +++ b/lib/util/NumberFormatter.js @@ -47,18 +47,28 @@ function NumberFormatter (value) { * decimal point. Zero by default. */ NumberFormatter.prototype.toFixed = function (precision) { - if (precision) { - var dot = this.exponent > 0 ? this.exponent : 0; - var all = init(-this.exponent, 0).concat(this.coefficients); - var coefficients = round(all, dot + precision + 1); - coefficients.splice(dot + 1, 0, '.'); + var rounded = this.roundDigits(this.exponent + 1 + (precision || 0)); + var c = rounded.coefficients; + var p = rounded.exponent + 1; // exponent may have changed - return this.sign + coefficients.join(''); + // append zeros if needed + var pp = p + (precision || 0); + if (c.length < pp) { + c = c.concat(zeros(pp - c.length)); } - else { - // TODO: make the || '0' redundant - return this.sign + round(this.coefficients, this.exponent + 1).join('') || '0'; + + // prepend zeros if needed + if (p < 0) { + c = zeros(-p + 1).concat(c); + p = 1; } + + // insert a dot if needed + if (precision) { + c.splice(p, 0, (p === 0) ? '0.' : '.'); + } + + return this.sign + c.join(''); }; /** @@ -68,15 +78,20 @@ NumberFormatter.prototype.toFixed = function (precision) { * is used. */ NumberFormatter.prototype.toExponential = function (precision) { - var coefficients = precision - ? round(this.coefficients, precision) - : this.coefficients.slice(0); + // round if needed, else create a clone + var rounded = precision ? this.roundDigits(precision) : this.clone(); + var c = rounded.coefficients; + var e = rounded.exponent; - var first = coefficients.shift(); - return this.sign + - first + - (coefficients.length > 0 ? ('.' + coefficients.join('')) : '') + - 'e' + (this.exponent >= 0 ? '+' : '') + this.exponent; + // append zeros if needed + if (c.length < precision) { + c = c.concat(zeros(precision - c.length)); + } + + // format as `C.CCCe+EEE` or `C.CCCe-EEE` + var first = c.shift(); + return this.sign + first + (c.length > 0 ? ('.' + c.join('')) : '') + + 'e' + (e >= 0 ? '+' : '') + e; }; /** @@ -99,74 +114,92 @@ NumberFormatter.prototype.toPrecision = function(precision, options) { return this.toExponential(precision); } else { - var all = init(-this.exponent, 0).concat(this.coefficients); - - var coefficients = precision - ? round(all, precision + (this.exponent < 0 ? -this.exponent : 0)) - : all; + var rounded = precision ? this.roundDigits(precision) : this.clone(); + var c = rounded.coefficients; + var e = rounded.exponent; // append trailing zeros - var trailing = init(this.exponent - coefficients.length + 1, 0); - coefficients = coefficients.concat(trailing); - - var dot = this.exponent > 0 ? this.exponent : 0; - if (dot < coefficients.length - 1) { - coefficients.splice(dot + 1, 0, '.'); + if (c.length < precision) { + c = c.concat(zeros(precision - c.length)); } - return this.sign + coefficients.join(''); + // append trailing zeros + // TODO: simplify the next statement + c = c.concat(zeros(e - c.length + 1 + + (c.length < precision ? precision - c.length : 0))); + + // prepend zeros + c = zeros(-e).concat(c); + + var dot = e > 0 ? e : 0; + if (dot < c.length - 1) { + c.splice(dot + 1, 0, '.'); + } + + return this.sign + c.join(''); } }; /** - * Round an array with coefficients. For example: - * - * round([2,3,4,5,6], 2) returns '23' - * round([2,3,4,5,6], 4) returns '2346' - * round([2,3], 4) returns '2300' - * - * @param {number[]} coefficients - * @param {number} count - * @return {number[]} Returns array with rounded coefficients + * Crete a clone of the NumberFormatter + * @return {NumberFormatter} Returns a clone of the NumberFormatter */ -function round(coefficients, count) { - // TODO: simplify this method, write in a more compact way - var rounded = coefficients.slice(0, count); - if (coefficients[count] >= 5) { - if (count === 0) { - rounded.unshift(0); - count++; - } - rounded[count - 1]++; +NumberFormatter.prototype.clone = function () { + var clone = new NumberFormatter('0'); + clone.sign = this.sign; + clone.coefficients = this.coefficients.slice(0); + clone.exponent = this.exponent; + return clone; +}; - var i = count - 1; - while (i > 0) { - if (rounded[i] === 10) { - rounded[i] = 0; +/** + * Round the number of digits of a number * + * @param {number} precision A positive integer + * @return {NumberFormatter} Returns a new NumberFormatter with the rounded + * digits + */ +NumberFormatter.prototype.roundDigits = function (precision) { + var rounded = this.clone(); + var c = rounded.coefficients; + // prepend zeros if needed + while (precision <= 0) { + c.unshift(0); + rounded.exponent++; + precision++; + } + + if (c.length > precision) { + var removed = c.splice(precision); + + if (removed[0] >= 5) { + var i = precision - 1; + c[i]++; + while (c[i] === 10) { + c.pop(); if (i === 0) { - rounded.unshift(0); + c.unshift(0); + rounded.exponent++; i++; } - rounded[i - 1]++; + i--; + c[i]++; } - i--; } } - return rounded.concat(init(count - rounded.length, 0)); -} + return rounded; +}; /** - * Initialize an array with a certain length and initialize with values. + * Create an array filled with zeros. * @param {number} length - * @param {number} value * @return {Array} */ -function init(length, value) { +function zeros(length) { var arr = []; for (var i = 0; i < length; i++) { - arr.push(value); + arr.push(0); } return arr; } diff --git a/test/util/NumberFormatter.test.js b/test/util/NumberFormatter.test.js index e7b3aaece..d8c941f32 100644 --- a/test/util/NumberFormatter.test.js +++ b/test/util/NumberFormatter.test.js @@ -28,6 +28,21 @@ describe('NumberFormatter', function() { assert.deepEqual(new NumberFormatter('2.3e-3'), {sign: '', coefficients: [2, 3], exponent: -3}); assert.deepEqual(new NumberFormatter('23e-3'), {sign: '', coefficients: [2, 3], exponent: -2}); assert.deepEqual(new NumberFormatter('-23e-3'), {sign: '-', coefficients: [2, 3], exponent: -2}); + assert.deepEqual(new NumberFormatter('99.99'), {sign: '', coefficients: [9,9,9,9], exponent: 1}); + }); + + it('should clone a NumberFormatter', function () { + var a = new NumberFormatter(2.3); + var clone = a.clone(); + assert.deepEqual(clone, a); + assert.notStrictEqual(clone, a); + }); + + it('should round a NumberFormatter', function () { + assert.deepEqual(new NumberFormatter(123456).roundDigits(3), new NumberFormatter(123000)); + assert.deepEqual(new NumberFormatter(123456).roundDigits(4), new NumberFormatter(123500)); + assert.deepEqual(new NumberFormatter(0.00555).roundDigits(2), new NumberFormatter(0.0056)); + assert.deepEqual(new NumberFormatter(99.99).roundDigits(2), new NumberFormatter(100)); }); it('should format a number with toFixed', function () { @@ -58,6 +73,8 @@ describe('NumberFormatter', function() { it('should format a number with toExponential', function () { assert.strictEqual(new NumberFormatter(0).toExponential(), '0e+0'); + assert.strictEqual(new NumberFormatter(1).toExponential(), '1e+0'); + assert.strictEqual(new NumberFormatter(1000).toExponential(), '1e+3'); assert.strictEqual(new NumberFormatter(2300).toExponential(), '2.3e+3'); assert.strictEqual(new NumberFormatter(3.568).toExponential(), '3.568e+0'); assert.strictEqual(new NumberFormatter(0.00123).toExponential(), '1.23e-3'); @@ -67,6 +84,7 @@ describe('NumberFormatter', function() { assert.strictEqual(new NumberFormatter(0).toExponential(2), '0.0e+0'); assert.strictEqual(new NumberFormatter(1234).toExponential(2), '1.2e+3'); assert.strictEqual(new NumberFormatter(1234).toExponential(6), '1.23400e+3'); + assert.strictEqual(new NumberFormatter(9999).toExponential(2), '1.0e+4'); }); it('should format a number with toPrecision', function () { @@ -79,9 +97,14 @@ describe('NumberFormatter', function() { assert.strictEqual(new NumberFormatter(2300).toPrecision(6), '2300.00'); assert.strictEqual(new NumberFormatter(1234.5678).toPrecision(6), '1234.57'); + assert.strictEqual(new NumberFormatter(1234.5678).toPrecision(2), '1200'); assert.strictEqual(new NumberFormatter(1234).toPrecision(2), '1200'); assert.strictEqual(new NumberFormatter(0.004).toPrecision(3), '0.00400'); assert.strictEqual(new NumberFormatter(0.00123456).toPrecision(5), '0.0012346'); + assert.strictEqual(new NumberFormatter(999).toPrecision(2), '1000'); + assert.strictEqual(new NumberFormatter(9990).toPrecision(2), '10000'); + assert.strictEqual(new NumberFormatter(99999).toPrecision(2), '100000'); + assert.strictEqual(new NumberFormatter(999e7).toPrecision(2), '1.0e+10'); }); it('should should throw an error on invalid input', function () { diff --git a/test/util/number.test.js b/test/util/number.test.js index 8b9897335..95ef35e7e 100644 --- a/test/util/number.test.js +++ b/test/util/number.test.js @@ -128,6 +128,7 @@ describe('number', function() { assert.equal(number.format(123.456, options), '123'); assert.equal(number.format(123.7, options), '124'); assert.equal(number.format(-123.7, options), '-124'); + assert.equal(number.format(-66, options), '-66'); assert.equal(number.format(0.123456, options), '0'); assert.equal(number.format(123456789, options), '123456789');