Skip to content

Commit 7223124

Browse files
authored
feat: add comprehensive stringify API for AST serialization (#9)
Implements full round-trip serialization support by converting parsed temporal ASTs back to ISO 8601/IXDTF formatted strings. Key features: - stringifyTemporal() for all temporal types (DateTime, Duration, Range) - Individual stringify functions for each AST component - Automatic normalization of offsets to canonical format (±HH:MM) - Component-based reconstruction for consistent output - Full round-trip compatibility with parser Includes 631 lines of tests covering: - Unit tests for each stringify function - Round-trip parsing validation - Format normalization (compact/short → canonical) - Edge cases (year 0001, midnight, empty duration, negative zero) - RFC 9557 compliance (calendar annotations, critical flags) This enables workflows like: - AST manipulation and re-serialization - Format normalization and validation - Temporal data transformation pipelines
1 parent 7d619f9 commit 7223124

File tree

6 files changed

+947
-2
lines changed

6 files changed

+947
-2
lines changed

.changeset/kind-donkeys-bake.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@taskade/temporal-parser': minor
3+
---
4+
5+
feat: add comprehensive stringify API for AST serialization
6+
7+
Add comprehensive stringify API for AST serialization. Implements full round-trip serialization support by converting parsed temporal ASTs back to ISO 8601/IXDTF formatted strings.
8+
9+
**New exports:**
10+
- `stringifyTemporal()` - Main function for all temporal types
11+
- `stringifyDate()`, `stringifyTime()`, `stringifyDateTime()` - DateTime components
12+
- `stringifyDuration()`, `stringifyRange()` - Duration and Range types
13+
- `stringifyOffset()`, `stringifyTimeZone()`, `stringifyAnnotation()` - Supporting components
14+
15+
**Features:**
16+
- Automatic normalization of offsets to canonical format (±HH:MM)
17+
- Component-based reconstruction for consistent output
18+
- Full round-trip compatibility with parser
19+
- Preserves all AST information including annotations and critical flags
20+
21+
**Example:**
22+
```typescript
23+
import { parseTemporal, stringifyTemporal } from '@taskade/temporal-parser';
24+
25+
const ast = parseTemporal('2025-01-12T10:00:00+0530'); // Compact format
26+
const normalized = stringifyTemporal(ast);
27+
// '2025-01-12T10:00:00+05:30' (canonical format)
28+
```

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ const offset = parseOffset('+08:00');
100100
// { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' }
101101
```
102102

103+
### Stringify AST Back to String
104+
105+
```typescript
106+
import { parseTemporal, stringifyTemporal } from '@taskade/temporal-parser';
107+
108+
// Parse and stringify
109+
const ast = parseTemporal('2025-01-12T10:00:00+08:00[Asia/Singapore]');
110+
const str = stringifyTemporal(ast);
111+
// '2025-01-12T10:00:00+08:00[Asia/Singapore]'
112+
113+
// Offsets are normalized to canonical format (±HH:MM)
114+
const ast2 = parseTemporal('2025-01-12T10:00:00+0530'); // Compact format
115+
const str2 = stringifyTemporal(ast2);
116+
// '2025-01-12T10:00:00+05:30' (normalized)
117+
118+
// Stringify individual components
119+
import { stringifyDate, stringifyTime, stringifyDuration } from '@taskade/temporal-parser';
120+
121+
stringifyDate({ kind: 'Date', year: 2025, month: 1, day: 12 });
122+
// '2025-01-12'
123+
124+
stringifyTime({ kind: 'Time', hour: 10, minute: 30, second: 45 });
125+
// '10:30:45'
126+
127+
stringifyDuration({ kind: 'Duration', years: 1, months: 2, raw: 'P1Y2M', annotations: [] });
128+
// 'P1Y2M'
129+
```
130+
103131
## Motivation
104132

105133
Time is one of the most complex human inventions.
@@ -169,6 +197,22 @@ Parses a numeric timezone offset string.
169197
- Hours: 0-14 (UTC-12:00 to UTC+14:00)
170198
- Minutes: 0-59
171199

200+
### `stringifyTemporal(ast: TemporalAst): string`
201+
202+
Converts a temporal AST back to its string representation.
203+
204+
**Returns:** ISO 8601 / IXDTF formatted string
205+
206+
**Also available:**
207+
- `stringifyDate(date: DateAst): string`
208+
- `stringifyTime(time: TimeAst): string`
209+
- `stringifyDateTime(dateTime: DateTimeAst): string`
210+
- `stringifyDuration(duration: DurationAst): string`
211+
- `stringifyRange(range: RangeAst): string`
212+
- `stringifyOffset(offset: OffsetAst): string`
213+
- `stringifyTimeZone(timeZone: TimeZoneAst): string`
214+
- `stringifyAnnotation(annotation: AnnotationAst): string`
215+
172216
## TypeScript Support
173217

174218
Full TypeScript definitions are included. All AST types are exported:

src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ export { parseTemporal } from './parser.js';
1515
// Export offset parser (useful standalone utility)
1616
export { parseOffset } from './parseOffset.js';
1717

18+
// Export stringify functionality
19+
export {
20+
stringifyAnnotation,
21+
stringifyDate,
22+
stringifyDateTime,
23+
stringifyDuration,
24+
stringifyOffset,
25+
stringifyRange,
26+
stringifyTemporal,
27+
stringifyTime,
28+
stringifyTimeZone,
29+
} from './stringify.js';
30+
1831
// Export parser types
1932
export type {
2033
AnnotationAst,

src/parser-types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,21 @@ export type DateTimeAst = {
2222

2323
export type DateAst = {
2424
kind: 'Date';
25+
/**
26+
* Components are receivable by Temporal.PlainDate.from().
27+
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate/from
28+
*/
2529
year: number;
2630
month?: number;
2731
day?: number;
2832
};
2933

3034
export type TimeAst = {
3135
kind: 'Time';
36+
/**
37+
* Components are receivable by Temporal.PlainTime.from().
38+
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime/from
39+
*/
3240
hour: number;
3341
minute: number;
3442
second?: number;
@@ -61,8 +69,10 @@ export type AnnotationAst = {
6169

6270
export type DurationAst = {
6371
kind: 'Duration';
64-
// Keep both parsed fields and the original string form.
65-
// Months vs minutes ambiguity is handled by position (date part vs time part).
72+
// ISO 8601 duration components (P1Y2M3DT4H5M6S)
73+
// Note: 'M' is disambiguated by position - months in date part, minutes in time part
74+
// Components are compatible with Temporal.Duration.from()
75+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/from
6676
years?: number;
6777
months?: number;
6878
weeks?: number;

0 commit comments

Comments
 (0)