From 15b4189477258ce228cce73219a201dd049bcf43 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 6 Aug 2026 11:24:17 +0100 Subject: [PATCH] fix: parse full-length numbers instead of truncating digit runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The number regex capped each digit run at {1,16}, so a value with more than 16 digits in a run was split: the first 16 digits matched as one (unitless) number and the remainder started a fresh match. parse('0.30000000000000004s') returned 4000.3 instead of 300.00000000000006 — the 17th fraction digit '4' broke off and was reparsed as '4s'. Any decimal with a long fraction (e.g. String(0.1 + 0.2)) or a >16-digit integer was silently mis-parsed. The {1,16} bound was introduced to guard against ReDoS, but the catastrophic backtracking came from the overlapping alternation in the earlier pattern (\d+\.?\d*|\d*\.?\d+), not from the run length. The alternation here (\d+(?:\.\d+)?|\.\d+) has no overlapping quantifiers, so it stays linear without a length cap: a 200k-digit input parses in well under a millisecond. --- index.js | 2 +- test.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index cfc5fa2..90380a5 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ import en from './locale/en.js' -const durationRE = /((?:\d{1,16}(?:\.\d{1,16})?|\.\d{1,16})(?:[eE][-+]?\d{1,4})?)\s*([\p{L}]{0,14})/gu +const durationRE = /((?:\d+(?:\.\d+)?|\.\d+)(?:[eE][-+]?\d+)?)\s*([\p{L}]{0,14})/gu parse.unit = en diff --git a/test.js b/test.js index d806c9a..f292ee3 100644 --- a/test.js +++ b/test.js @@ -210,3 +210,10 @@ t('custom locale without group/placeholder', t => { parse.unit = en t.end() }) + +t('numbers with long digit runs are not split into separate matches', t => { + t.equal(parse('0.30000000000000004s'), 0.30000000000000004 * 1000) + t.equal(parse('0.12345678901234567s'), 0.12345678901234567 * 1000) + t.equal(parse('12345678901234567ms'), 12345678901234567) + t.end() +})