From 93dd2b2f0612549d6f20fb6f311d9990d318d3a5 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Fri, 4 Sep 2026 09:04:26 +0200 Subject: [PATCH] fix: pluralize unit word using the maxDecimalPoints-truncated count When maxDecimalPoints truncates a unit count to exactly 1, the rendered number showed 1 while the word was pluralized from the raw count (for example humanizeDuration(1500, { maxDecimalPoints: 0 }) returned "1 seconds"). Pluralize from the normalized count that is actually displayed so the number and word agree. --- PR.md | 87 ++++++++++++++++++++++++++++++++++++++++++++ humanize-duration.js | 5 ++- test/humanizer.js | 14 +++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 0000000..2ccae10 --- /dev/null +++ b/PR.md @@ -0,0 +1,87 @@ +# Fix pluralization when `maxDecimalPoints` truncates the count to 1 + +## Problem + +When the `maxDecimalPoints` option truncates a unit count down to exactly `1`, +the rendered number and the unit word disagree. The number is shown as `1` but +the word is pluralized: + +```js +const humanizeDuration = require("humanize-duration"); + +humanizeDuration(1500, { maxDecimalPoints: 0 }); // "1 seconds" (expected "1 second") +humanizeDuration(1005, { maxDecimalPoints: 2 }); // "1 seconds" (expected "1 second") +``` + +The bug is language-independent; any language whose unit word is a function of +the count is affected (for example German returns `"1 Sekunden"` instead of +`"1 Sekunde"`). + +## Root cause + +`humanize-duration.js`, in `renderPiece`. + +The displayed count is `normalizedUnitCount`, computed from `maxDecimalPoints`: + +```js +var normalizedUnitCount = + maxDecimalPoints === void 0 + ? unitCount + : Math.floor(unitCount * Math.pow(10, maxDecimalPoints)) / + Math.pow(10, maxDecimalPoints); +var countStr = normalizedUnitCount.toString(); +``` + +but the unit word was pluralized from the raw, un-truncated `unitCount`: + +```js +if (typeof languageWord === "function") { + word = languageWord(unitCount); +} +``` + +So `1.5` seconds with `maxDecimalPoints: 0` renders the count as `"1"` (from +`normalizedUnitCount`) while `languageWord(1.5)` returns the plural form, giving +`"1 seconds"`. + +## Fix + +Pluralize based on the count that is actually rendered, `normalizedUnitCount`: + +```js +if (typeof languageWord === "function") { + word = languageWord(normalizedUnitCount); +} +``` + +When `maxDecimalPoints` is unset, `normalizedUnitCount === unitCount`, so +existing behavior is unchanged. The fix only affects cases where the option +truncates the count. + +## Test + +Added a test to `test/humanizer.js` covering the truncated-to-one case, a +truncated count that stays plural, and the same mechanism in German: + +```js +assert.strictEqual(humanizer({ maxDecimalPoints: 0 })(1500), "1 second"); +assert.strictEqual(humanizer({ maxDecimalPoints: 2 })(1005), "1 second"); +assert.strictEqual(humanizer({ maxDecimalPoints: 0 })(2500), "2 seconds"); +assert.strictEqual( + humanizer({ language: "de", maxDecimalPoints: 0 })(1500), + "1 Sekunde" +); +``` + +Counterfactual (revert the one-line fix in `renderPiece`): + +``` +Expected values to be strictly equal: +expected: '1 second' +actual: '1 seconds' +``` + +## Gate + +- `npm test` (runs `prettier --check` + `eslint` + `tsc --noEmit`, then + `node --test`): exit 0, all tests pass. diff --git a/humanize-duration.js b/humanize-duration.js index 0df4917..ed8dff1 100644 --- a/humanize-duration.js +++ b/humanize-duration.js @@ -1382,7 +1382,10 @@ var languageWord = language[unitName]; var word; if (typeof languageWord === "function") { - word = languageWord(unitCount); + // Pluralize based on the count that is actually rendered. When + // `maxDecimalPoints` truncates the count (for example 1.5 -> "1"), + // the word must agree with the displayed number, not the raw one. + word = languageWord(normalizedUnitCount); } else { word = languageWord; } diff --git a/test/humanizer.js b/test/humanizer.js index 2d966ed..fdaa101 100644 --- a/test/humanizer.js +++ b/test/humanizer.js @@ -162,6 +162,20 @@ describe("humanizer", function () { assert.strictEqual(h(7999), "7.999 seconds"); }); + it('pluralizes based on the count rendered by "maxDecimalPoints"', function () { + // When `maxDecimalPoints` truncates the count down to exactly 1, the unit + // word must agree with the displayed "1", i.e. be singular. + assert.strictEqual(humanizer({ maxDecimalPoints: 0 })(1500), "1 second"); + assert.strictEqual(humanizer({ maxDecimalPoints: 2 })(1005), "1 second"); + // A truncated count that is not 1 stays plural. + assert.strictEqual(humanizer({ maxDecimalPoints: 0 })(2500), "2 seconds"); + // German shares the same singular/plural mechanism. + assert.strictEqual( + humanizer({ language: "de", maxDecimalPoints: 0 })(1500), + "1 Sekunde" + ); + }); + it("can ask for the largest units", function () { const h = humanizer({ largest: 2 });