From 61bd61012bc2725401060da0ec123521c1572180 Mon Sep 17 00:00:00 2001 From: MFA-G Date: Sun, 20 Sep 2026 20:18:15 +0700 Subject: [PATCH] fix: match the `format` argument like an input unit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parse('2MIN')` resolves fine because input units are lower-cased and retried without a trailing `s`, but the `format` argument was looked up verbatim on `parse.unit`. So `parse('2h', 'MIN')`, `parse('2h', 'mins')` and `parse('2h', 'hrs')` all silently fell back to ms and returned 7200000 instead of 120 / 120 / 2 — a 60000x error with no signal. `parse('2h', 'M')` was the worst case: `M` is a valid unit name in any casing on the input side, so the caller gets a plausible-looking number in the wrong unit. Extract the input-side normalization into `lookupUnit` and use it for both sides. An unrecognized format still falls back to ms. --- Readme.md | 1 + index.js | 17 ++++++++++++++--- test.js | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index e8dc204..a3fc042 100644 --- a/Readme.md +++ b/Readme.md @@ -43,6 +43,7 @@ parse('2e3s') // => 2000 * s // custom output format parse('1hr 20mins', 'm') // => 80 +parse('1hr 20mins', 'MINS') // => 80, same matching rules as input units // add units parse.unit['μs'] = parse.unit.microsecond diff --git a/index.js b/index.js index 90380a5..23270ca 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,18 @@ parse.unit = en // group/placeholder cleanup regex — rebuilt only when the locale strings change let groupRE, placeholder = null, group +/** + * look up a unit name, ignoring case and a trailing plural `s` + * + * @param {object} unit + * @param {string} name + * @return {number|undefined} + */ +const lookupUnit = (unit, name) => { + name = String(name).toLowerCase() + return unit[name] ?? (name.endsWith('s') ? unit[name.slice(0, -1)] : undefined) +} + /** * convert `str` to ms * @@ -35,12 +47,11 @@ export default function parse(str = '', format = 'ms') { } else units = format } - else units = units.toLowerCase() - prevUnits = units = unit[units] || (units.endsWith('s') ? unit[units.slice(0, -1)] : undefined) + prevUnits = units = lookupUnit(unit, units) if (typeof units == 'number') result = (result || 0) + m[1] * units } - return result && ((result / (unit[format] || 1)) * (str.trimStart()[0] === '-' ? -1 : 1)) + return result && ((result / (lookupUnit(unit, format) || 1)) * (str.trimStart()[0] === '-' ? -1 : 1)) } diff --git a/test.js b/test.js index f292ee3..5a7cfa7 100644 --- a/test.js +++ b/test.js @@ -158,6 +158,23 @@ t('format', t => { t.end() }) +t('format is matched like an input unit', t => { + // upper-case, exactly as accepted on the input side (`parse('2MIN')`) + t.equal(parse('2h', 'M'), 120) + t.equal(parse('2h', 'MIN'), 120) + t.equal(parse('2h', 'Hours'), 2) + + // plural, matching `unit.endsWith('s')` handling on the input side + t.equal(parse('2h', 'mins'), 120) + t.equal(parse('2h', 'hrs'), 2) + t.equal(parse('2h', 'seconds'), 7200) + + // an unknown format still falls back to ms + t.equal(parse('2h', 'bogus'), 7200000) + + t.end() +}) + t('unicode support', t => { parse.unit['сек'] = parse.unit['s'] // ru seconds t.equal(parse('5сек'), 5000)