Skip to content

Commit 692b0f7

Browse files
authored
fix: support comma as decimal separator (ISO 8601 European format) (#11)
Add support for comma (,) as a decimal separator in fractional seconds for both time and duration components, as specified in ISO 8601. Changes: - Parser now accepts both . and , as decimal separators - Stringify normalizes all output to canonical . notation - Added comprehensive test coverage for both formats - Updated documentation with European format examples This improves ISO 8601 compliance and enables parsing of temporal strings from European locales where comma is the standard decimal separator (e.g., "T10:30:45,123" and "PT1,5S").
1 parent 7223124 commit 692b0f7

File tree

5 files changed

+66
-3
lines changed

5 files changed

+66
-3
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@taskade/temporal-parser': patch
3+
---
4+
5+
fix: Support comma as decimal separator in fractional seconds (European format)
6+
7+
Add support for comma (`,`) as a decimal separator in fractional seconds for both time and duration components, as specified in ISO 8601. This enables parsing of European-formatted temporal strings while maintaining canonical dot (`.`) notation in serialized output.
8+
9+
**Supported formats:**
10+
- Time with fractional seconds: `T10:30:45,123``T10:30:45.123`
11+
- Duration with fractional seconds: `PT1,5S``PT1.5S`
12+
13+
**Behavior:**
14+
- Parser accepts both `.` and `,` as decimal separators
15+
- Stringify normalizes all output to use `.` for consistency
16+
- Full round-trip compatibility maintained
17+
18+
This change improves ISO 8601 compliance and enables parsing of temporal strings from European locales where comma is the standard decimal separator.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const range = parseTemporal('2025-01-01/2025-12-31');
5555
- Hour-Minute: `T10:30`
5656
- With seconds: `T10:30:45`
5757
- With fractional seconds: `T10:30:45.123456789`
58+
- European format (comma): `T10:30:45,123` (normalized to dot in output)
5859

5960
### Timezones
6061
- UTC: `Z`
@@ -65,6 +66,7 @@ const range = parseTemporal('2025-01-01/2025-12-31');
6566
- Date parts: `P1Y2M3D` (1 year, 2 months, 3 days)
6667
- Time parts: `PT4H5M6S` (4 hours, 5 minutes, 6 seconds)
6768
- Combined: `P1Y2M3DT4H5M6S`
69+
- Fractional seconds: `PT1.5S` or `PT1,5S` (comma normalized to dot)
6870

6971
### IXDTF Annotations
7072
- Calendar: `[u-ca=gregory]`

src/parser.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ describe('parseTemporal', () => {
7272
time: { fraction: '123456789' },
7373
});
7474
});
75+
76+
it('should parse fractional seconds with comma (European format)', () => {
77+
const ast = parseTemporal('2025-01-07T10:30:45,123');
78+
expect(ast).toMatchObject({
79+
kind: 'DateTime',
80+
date: { year: 2025, month: 1, day: 7 },
81+
time: { hour: 10, minute: 30, second: 45, fraction: '123' },
82+
annotations: [],
83+
});
84+
});
85+
86+
it('should parse fractional seconds with comma and high precision', () => {
87+
const ast = parseTemporal('2025-01-07T10:30:45,123456789');
88+
expect(ast).toMatchObject({
89+
kind: 'DateTime',
90+
time: { fraction: '123456789' },
91+
});
92+
});
7593
});
7694

7795
describe('timezone parsing', () => {
@@ -369,6 +387,16 @@ describe('parseTemporal', () => {
369387
});
370388
});
371389

390+
it('should parse fractional seconds with comma (European format)', () => {
391+
const ast = parseTemporal('PT1,5S');
392+
expect(ast).toMatchObject({
393+
kind: 'Duration',
394+
seconds: 1,
395+
secondsFraction: '5',
396+
raw: 'PT1,5S',
397+
});
398+
});
399+
372400
it('should parse combined time parts', () => {
373401
const ast = parseTemporal('PT2H30M45S');
374402
expect(ast).toMatchObject({

src/parser.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ class Parser {
175175
const sTok = this.eat(TokType.Number);
176176
second = toInt(sTok.value, 'second', this.i);
177177

178-
if (this.tryEat(TokType.Dot)) {
178+
// ISO 8601 allows both . and , as decimal separators for fractional seconds
179+
if (this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma)) {
179180
const fracTok = this.eat(TokType.Number);
180181
fraction = fracTok.value; // keep raw digits
181182
}
@@ -332,8 +333,10 @@ class Parser {
332333
rawParts.push(numTok.value);
333334

334335
let fraction: string | undefined;
335-
if (this.tryEat(TokType.Dot)) {
336-
rawParts.push('.');
336+
// ISO 8601 allows both . and , as decimal separators
337+
const dotOrComma = this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma);
338+
if (dotOrComma) {
339+
rawParts.push(dotOrComma.value); // Preserve the actual separator (. or ,)
337340
const fracTok = this.eat(TokType.Number);
338341
rawParts.push(fracTok.value);
339342
fraction = fracTok.value;

src/stringify.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,18 @@ describe('format normalization', () => {
526526
const result = stringifyTemporal(ast);
527527
expect(result).toBe('2025-01-12T10:00:00-08:00');
528528
});
529+
530+
it('should normalize comma decimal separator to dot', () => {
531+
const ast = parseTemporal('2025-01-12T10:30:45,123');
532+
const result = stringifyTemporal(ast);
533+
expect(result).toBe('2025-01-12T10:30:45.123');
534+
});
535+
536+
it('should normalize comma in duration to dot', () => {
537+
const ast = parseTemporal('PT1,5S');
538+
const result = stringifyTemporal(ast);
539+
expect(result).toBe('PT1.5S');
540+
});
529541
});
530542

531543
describe('edge cases', () => {

0 commit comments

Comments
 (0)