From f3c6d5d91e23eea1a71ca19bff3ffcb4129d98bd Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 13 Jan 2026 19:54:39 -0800 Subject: [PATCH] fix zero precision --- src/formatDecimal.js | 4 ++-- src/formatPrefixAuto.js | 2 +- src/locale.js | 2 +- test/format-type-r-test.js | 7 ++++++- test/format-type-s-test.js | 7 +++++++ test/precisionFixed-test.js | 13 +++++++++++++ test/precisionPrefix-test.js | 13 +++++++++++++ test/precisionRound-test.js | 13 +++++++++++++ 8 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/formatDecimal.js b/src/formatDecimal.js index 4b34192..3b9263b 100644 --- a/src/formatDecimal.js +++ b/src/formatDecimal.js @@ -8,8 +8,8 @@ export default function(x) { // significant digits p, where x is positive and p is in [1, 21] or undefined. // For example, formatDecimalParts(1.23) returns ["123", 0]. export function formatDecimalParts(x, p) { - if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity - var i, coefficient = x.slice(0, i); + if (!isFinite(x) || x === 0) return null; // NaN, ±Infinity, ±0 + var i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e"), coefficient = x.slice(0, i); // The string returned by toExponential either has the form \d\.\d+e[-+]\d+ // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3). diff --git a/src/formatPrefixAuto.js b/src/formatPrefixAuto.js index 907742a..4392459 100644 --- a/src/formatPrefixAuto.js +++ b/src/formatPrefixAuto.js @@ -4,7 +4,7 @@ export var prefixExponent; export default function(x, p) { var d = formatDecimalParts(x, p); - if (!d) return x + ""; + if (!d) return prefixExponent = undefined, x.toPrecision(p); var coefficient = d[0], exponent = d[1], i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1, diff --git a/src/locale.js b/src/locale.js index 404f941..e6a2cdd 100644 --- a/src/locale.js +++ b/src/locale.js @@ -87,7 +87,7 @@ export default function(locale) { // Compute the prefix and suffix. valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix; - valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : ""); + valueSuffix = (type === "s" && !isNaN(value) && prefixExponent !== undefined ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : ""); // Break the formatted value into the integer “value” part that can be // grouped, and fractional or exponential “suffix” part that is not. diff --git a/test/format-type-r-test.js b/test/format-type-r-test.js index 25e661f..d3297af 100644 --- a/test/format-type-r-test.js +++ b/test/format-type-r-test.js @@ -2,7 +2,6 @@ import {assert, test} from "vitest"; import {format} from "../src/index.js"; test("format(\"r\") can round to significant digits", () => { - assert.strictEqual(format(".2r")(0), "0.0"); assert.strictEqual(format(".1r")(0.049), "0.05"); assert.strictEqual(format(".1r")(-0.049), "−0.05"); assert.strictEqual(format(".1r")(0.49), "0.5"); @@ -32,6 +31,12 @@ test("format(\"r\") can round to significant digits", () => { assert.strictEqual(format(".15r")(.999999999999999), "0.999999999999999"); }); +test("format(\"r\") can round zero", () => { + assert.strictEqual(format(".2r")(0), "0"); + assert.strictEqual(format(".1r")(0), "0"); + assert.strictEqual(format("r")(0), "0"); +}); + test("format(\"r\") can round very small numbers", () => { const f = format(".2r"); assert.strictEqual(f(1e-22), "0.00000000000000000000010"); diff --git a/test/format-type-s-test.js b/test/format-type-s-test.js index b507463..96f78e6 100644 --- a/test/format-type-s-test.js +++ b/test/format-type-s-test.js @@ -17,6 +17,13 @@ test("format(\"s\") outputs SI-prefix notation with default precision 6", () => assert.strictEqual(f(.000001), "1.00000µ"); }); +test("format(\"s\") does not get confused by NaN, Infinity, etc.", () => { + const f = format("s"); + assert.strictEqual(f(999500), "999.500k"); + assert.strictEqual(f(Infinity), "Infinity"); + assert.strictEqual(f(NaN), "NaN"); +}); + test("format(\"[.precision]s\") outputs SI-prefix notation with precision significant digits", () => { const f1 = format(".3s"); assert.strictEqual(f1(0), "0.00"); diff --git a/test/precisionFixed-test.js b/test/precisionFixed-test.js index 2e3796b..96121d0 100644 --- a/test/precisionFixed-test.js +++ b/test/precisionFixed-test.js @@ -9,3 +9,16 @@ test("precisionFixed(number) returns the expected value", () => { assert.strictEqual(precisionFixed(0.089), 2); assert.strictEqual(precisionFixed(0.011), 2); }); + +test("precisionFixed(0) returns NaN", () => { + assert.isNaN(precisionFixed(0)); +}); + +test("precisionFixed(NaN) returns NaN", () => { + assert.isNaN(precisionFixed(NaN)); +}); + +test("precisionFixed(Infinity) returns NaN", () => { + assert.isNaN(precisionFixed(Infinity)); + assert.isNaN(precisionFixed(-Infinity)); +}); diff --git a/test/precisionPrefix-test.js b/test/precisionPrefix-test.js index 2910f04..dad0be0 100644 --- a/test/precisionPrefix-test.js +++ b/test/precisionPrefix-test.js @@ -40,3 +40,16 @@ test("precisionPrefix(step, value) returns the expected precision when value is assert.strictEqual(precisionPrefix(1e24, 1e27), 0); // 1000Y assert.strictEqual(precisionPrefix(1e23, 1e27), 1); // 1000.0Y }); + +test("precisionPrefix(0, value) returns NaN", () => { + assert.isNaN(precisionPrefix(0, 1)); +}); + +test("precisionPrefix(NaN, value) returns NaN", () => { + assert.isNaN(precisionPrefix(NaN, 1)); +}); + +test("precisionPrefix(Infinity, value) returns NaN", () => { + assert.isNaN(precisionPrefix(Infinity, 1)); + assert.isNaN(precisionPrefix(-Infinity, 1)); +}); diff --git a/test/precisionRound-test.js b/test/precisionRound-test.js index 7a48613..fba5fb6 100644 --- a/test/precisionRound-test.js +++ b/test/precisionRound-test.js @@ -7,3 +7,16 @@ test("precisionRound(step, max) returns the expected value", () => { assert.strictEqual(precisionRound(0.01, 1.00), 2); // "0.99", "1.0" assert.strictEqual(precisionRound(0.01, 1.01), 3); // "1.00", "1.01" }); + +test("precisionRound(0, max) returns NaN", () => { + assert.isNaN(precisionRound(0, 1)); +}); + +test("precisionRound(NaN, max) returns NaN", () => { + assert.isNaN(precisionRound(NaN, 1)); +}); + +test("precisionRound(Infinity, max) returns NaN", () => { + assert.isNaN(precisionRound(Infinity, 1)); + assert.isNaN(precisionRound(-Infinity, 1)); +});