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.
This commit is contained in:
1 parent
194562d9da
commit
93dd2b2f06
3 files changed
+105
-1
No files matched your search
@@ -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.
|
||||||
@@ -1382,7 +1382,10 @@
|
|||||||
var languageWord = language[unitName];
|
var languageWord = language[unitName];
|
||||||
var word;
|
var word;
|
||||||
if (typeof languageWord === "function") {
|
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 {
|
} else {
|
||||||
word = languageWord;
|
word = languageWord;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,20 @@ describe("humanizer", function () {
|
|||||||
assert.strictEqual(h(7999), "7.999 seconds");
|
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 () {
|
it("can ask for the largest units", function () {
|
||||||
const h = humanizer({ largest: 2 });
|
const h = humanizer({ largest: 2 });
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user