Skip to content

Normalize the decimal separator on every component, not just the first#68

Merged
dy merged 1 commit into
jkroso:masterfrom
spokodev:fix/multi-component-decimal-separator
Jun 24, 2026
Merged

Normalize the decimal separator on every component, not just the first#68
dy merged 1 commit into
jkroso:masterfrom
spokodev:fix/multi-component-decimal-separator

Conversation

@spokodev

Copy link
Copy Markdown
Contributor

Problem

parse() normalizes the decimal separator with String.prototype.replace(parse.unit.decimal, '.'). With a string first argument, replace only replaces the first occurrence. So in any locale whose decimal separator is , (de, fr, es, ru, pt, id, ...), every component after the first keeps its comma and is mis-tokenized:

import parse from 'parse-duration'
import de from 'parse-duration/locale/de.js'
parse.unit = de

parse('1,5 s 1,5 s')  // 6501       (should be 3000)
parse('2,5h 3,5h')    // 27180000   (should be 21600000)

Trace of '1,5 s 1,5 s' (de): '1,5 s 1,5 s' → replace first ,'1.5 s 1,5 s' → tokenized as 1.5 s (1500) + bare 1 (no unit → 1ms) + 5 s (5000) = 6501. Single-component inputs (the existing locale separators test) happen to work because there is only one separator.

Fix

Use replaceAll so every component's separator is normalized:

-    .replace(parse.unit.decimal, '.') // normalize decimal separator
+    .replaceAll(parse.unit.decimal, '.') // normalize decimal separator

replaceAll with a string does a literal global replacement, so the en . decimal stays safe (replaceAll('.', '.') is a no-op). Available on all CI-tested runtimes (Node 18/20/22).

Verification

  • Added two assertions to the existing locale separators test (1,5 s 1,5 s → 3000, 2,5h 3,5h → 21600000). They fail on master and pass with the fix.
  • Independent oracle (component sum from the unit constants), fuzzed across en/de/fr/es/ru, 40k each: 0 divergences after the fix; en output byte-identical (period decimal unaffected), comma-decimal locales corrected.
  • Full tape suite: 82/82 (80 existing + 2 new).

parse() normalized only the first decimal separator: String.prototype
.replace with a string argument replaces a single occurrence. In locales
whose decimal separator is "," (de, fr, es, ru, pt, ...), every component
after the first kept its comma and was mis-tokenized:

  parse.unit = de
  parse('1,5 s 1,5 s')  // 6501      (should be 3000)
  parse('2,5h 3,5h')    // 27180000  (should be 21600000)

Use replaceAll so each component's separator is normalized. The en "."
decimal is unaffected (replaceAll('.', '.') is a no-op).
@dy

dy commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Cases are a bit confusing to me - can you give examples of something more real, why would you expect values like 1,5s 1,5s to turn into 3000? That's a bit wild expectation, isn't it?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a locale parsing bug where only the first decimal separator was normalized, causing multi-component durations (e.g., 1,5 s 1,5 s) to be mis-tokenized in comma-decimal locales.

Changes:

  • Normalize decimal separators across the entire input string (not just the first occurrence).
  • Extend the existing locale separators test with multi-component comma-decimal cases to prevent regressions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
index.js Updates decimal normalization logic to apply to all components in the input.
test.js Adds assertions covering multi-component inputs in comma-decimal locales.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread index.js
Comment on lines +19 to 20
.replaceAll(parse.unit.decimal, '.') // normalize decimal separator
.replace(durationRE, (_, n, units) => {
@spokodev

Copy link
Copy Markdown
Contributor Author

Fair point, 1,5s 1,5s was a reduced repro and reads odd out of context. The real-world trigger is a localized multi-part duration. In a comma-decimal locale (de/fr/es/ru/pt/...) a user writes a duration like 1,5h 30,5min, meaning "1.5 hours and 30.5 minutes". Today only the first separator is normalized, so the second component is mis-parsed:

import parse from 'parse-duration'
import de from 'parse-duration/locale/de.js'
parse.unit = de

parse('1,5h 30,5min')  // 7500000  (wrong; the 30,5min keeps its comma)
// expected 7230000 = 1.5h + 30.5min

replaceAll normalizes every component, and the en . decimal is untouched (replaceAll('.', '.') is a no-op). I can swap the PR's 1,5s 1,5s example for this one if it reads clearer.

@dy
dy merged commit 6d3e21b into jkroso:master Jun 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants