diff --git a/.changeset/european-decimal-separator.md b/.changeset/european-decimal-separator.md new file mode 100644 index 0000000..43073f1 --- /dev/null +++ b/.changeset/european-decimal-separator.md @@ -0,0 +1,18 @@ +--- +'@taskade/temporal-parser': patch +--- + +fix: Support comma as decimal separator in fractional seconds (European format) + +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. + +**Supported formats:** +- Time with fractional seconds: `T10:30:45,123` → `T10:30:45.123` +- Duration with fractional seconds: `PT1,5S` → `PT1.5S` + +**Behavior:** +- Parser accepts both `.` and `,` as decimal separators +- Stringify normalizes all output to use `.` for consistency +- Full round-trip compatibility maintained + +This change improves ISO 8601 compliance and enables parsing of temporal strings from European locales where comma is the standard decimal separator. diff --git a/README.md b/README.md index e055b00..52b4628 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ const range = parseTemporal('2025-01-01/2025-12-31'); - Hour-Minute: `T10:30` - With seconds: `T10:30:45` - With fractional seconds: `T10:30:45.123456789` +- European format (comma): `T10:30:45,123` (normalized to dot in output) ### Timezones - UTC: `Z` @@ -65,6 +66,7 @@ const range = parseTemporal('2025-01-01/2025-12-31'); - Date parts: `P1Y2M3D` (1 year, 2 months, 3 days) - Time parts: `PT4H5M6S` (4 hours, 5 minutes, 6 seconds) - Combined: `P1Y2M3DT4H5M6S` +- Fractional seconds: `PT1.5S` or `PT1,5S` (comma normalized to dot) ### IXDTF Annotations - Calendar: `[u-ca=gregory]` diff --git a/src/parser.test.ts b/src/parser.test.ts index 4aca723..a81a038 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -72,6 +72,24 @@ describe('parseTemporal', () => { time: { fraction: '123456789' }, }); }); + + it('should parse fractional seconds with comma (European format)', () => { + const ast = parseTemporal('2025-01-07T10:30:45,123'); + expect(ast).toMatchObject({ + kind: 'DateTime', + date: { year: 2025, month: 1, day: 7 }, + time: { hour: 10, minute: 30, second: 45, fraction: '123' }, + annotations: [], + }); + }); + + it('should parse fractional seconds with comma and high precision', () => { + const ast = parseTemporal('2025-01-07T10:30:45,123456789'); + expect(ast).toMatchObject({ + kind: 'DateTime', + time: { fraction: '123456789' }, + }); + }); }); describe('timezone parsing', () => { @@ -369,6 +387,16 @@ describe('parseTemporal', () => { }); }); + it('should parse fractional seconds with comma (European format)', () => { + const ast = parseTemporal('PT1,5S'); + expect(ast).toMatchObject({ + kind: 'Duration', + seconds: 1, + secondsFraction: '5', + raw: 'PT1,5S', + }); + }); + it('should parse combined time parts', () => { const ast = parseTemporal('PT2H30M45S'); expect(ast).toMatchObject({ diff --git a/src/parser.ts b/src/parser.ts index f561b0c..0d33d80 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -175,7 +175,8 @@ class Parser { const sTok = this.eat(TokType.Number); second = toInt(sTok.value, 'second', this.i); - if (this.tryEat(TokType.Dot)) { + // ISO 8601 allows both . and , as decimal separators for fractional seconds + if (this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma)) { const fracTok = this.eat(TokType.Number); fraction = fracTok.value; // keep raw digits } @@ -332,8 +333,10 @@ class Parser { rawParts.push(numTok.value); let fraction: string | undefined; - if (this.tryEat(TokType.Dot)) { - rawParts.push('.'); + // ISO 8601 allows both . and , as decimal separators + const dotOrComma = this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma); + if (dotOrComma) { + rawParts.push(dotOrComma.value); // Preserve the actual separator (. or ,) const fracTok = this.eat(TokType.Number); rawParts.push(fracTok.value); fraction = fracTok.value; diff --git a/src/stringify.test.ts b/src/stringify.test.ts index b92fd90..9312fbe 100644 --- a/src/stringify.test.ts +++ b/src/stringify.test.ts @@ -526,6 +526,18 @@ describe('format normalization', () => { const result = stringifyTemporal(ast); expect(result).toBe('2025-01-12T10:00:00-08:00'); }); + + it('should normalize comma decimal separator to dot', () => { + const ast = parseTemporal('2025-01-12T10:30:45,123'); + const result = stringifyTemporal(ast); + expect(result).toBe('2025-01-12T10:30:45.123'); + }); + + it('should normalize comma in duration to dot', () => { + const ast = parseTemporal('PT1,5S'); + const result = stringifyTemporal(ast); + expect(result).toBe('PT1.5S'); + }); }); describe('edge cases', () => {