A lexer and parser for ISO 8601, RFC 3339, and IXDTF temporal expressions, built with compiler design principles.
- Standards compliant: ISO 8601, RFC 3339, and IXDTF support
- Extensible architecture: Exposed lexer allows custom parser implementations
- Zero dependencies: No external runtime dependencies
- Type-safe: Written in TypeScript with full type definitions
- Small footprint: ~18KB minified
- Dual module support: ESM and CommonJS builds
- Well tested: 91% test coverage with 271 test cases
npm install @taskade/temporal-parserimport { parseTemporal } from '@taskade/temporal-parser';
// Parse a complete datetime with timezone
const result = parseTemporal('2025-01-12T10:00:00+08:00[Asia/Singapore]');
console.log(result);
// {
// kind: 'DateTime',
// date: { kind: 'Date', year: 2025, month: 1, day: 12 },
// time: { kind: 'Time', hour: 10, minute: 0, second: 0 },
// offset: { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' },
// timeZone: { kind: 'IanaTimeZone', id: 'Asia/Singapore', critical: false },
// annotations: []
// }
// Parse a duration
const duration = parseTemporal('P1Y2M3DT4H5M6S');
// { kind: 'Duration', years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, ... }
// Parse a date range
const range = parseTemporal('2025-01-01/2025-12-31');
// { kind: 'Range', start: {...}, end: {...} }
// Parse BC dates (negative years in ISO 8601)
const bcDate = parseTemporal('-0044-03-15');
// { kind: 'DateTime', date: { year: -44, month: 3, day: 15 }, ... }- Year:
2025 - Year-Month:
2025-01 - Full date:
2025-01-12 - BC dates (negative years):
-0044-03-15(44 BC),0000-01-01(1 BC)
- 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)
- UTC:
Z - Numeric offset:
+08:00,-05:30,+0530,+09 - IANA timezone:
[Asia/Singapore],[America/New_York]
- Date parts:
P1Y2M3D(1 year, 2 months, 3 days) - Time parts:
PT4H5M6S(4 hours, 5 minutes, 6 seconds) - Combined:
P1Y2M3DT4H5M6S - Fractional seconds:
PT1.5SorPT1,5S(comma normalized to dot)
- Calendar:
[u-ca=gregory] - Critical flags:
[!u-ca=iso8601] - Multiple annotations:
2025-01-12[u-ca=gregory][u-tz=UTC]
- Closed:
2025-01-01/2025-12-31 - Open start:
/2025-12-31 - Open end:
2025-01-01/ - Duration-based:
2025-01-01/P1Y
import { lexTemporal, combineTimezoneOffsets } from '@taskade/temporal-parser';
// Tokenize a temporal string
const tokens = lexTemporal('2025-01-12T10:00:00+08:00');
// Optionally combine timezone offset tokens
const combined = combineTimezoneOffsets(tokens);import { parseOffset } from '@taskade/temporal-parser';
const offset = parseOffset('+08:00');
// { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' }import { parseTemporal, stringifyTemporal } from '@taskade/temporal-parser';
// Parse and stringify
const ast = parseTemporal('2025-01-12T10:00:00+08:00[Asia/Singapore]');
const str = stringifyTemporal(ast);
// '2025-01-12T10:00:00+08:00[Asia/Singapore]'
// Offsets are normalized to canonical format (±HH:MM)
const ast2 = parseTemporal('2025-01-12T10:00:00+0530'); // Compact format
const str2 = stringifyTemporal(ast2);
// '2025-01-12T10:00:00+05:30' (normalized)
// Stringify individual components
import { stringifyDate, stringifyTime, stringifyDuration } from '@taskade/temporal-parser';
stringifyDate({ kind: 'Date', year: 2025, month: 1, day: 12 });
// '2025-01-12'
stringifyTime({ kind: 'Time', hour: 10, minute: 30, second: 45 });
// '10:30:45'
stringifyDuration({ kind: 'Duration', years: 1, months: 2, raw: 'P1Y2M', annotations: [] });
// 'P1Y2M'Time is one of the most complex human inventions. Leap years, calendars, time zones, daylight saving rules, cultural conventions—every attempt to model time exposes exceptions and edge cases. Even today, we still struggle to write correct and maintainable code for something as fundamental as dates and times.
Despite its wide adoption, ISO 8601 / RFC 3339 is incomplete. It lacks proper support for time zones beyond numeric offsets, forcing real-world systems to rely on extensions such as IXDTF (inspired by Java's ZonedDateTime). Unfortunately, only very recent tools—and the latest generation of LLMs—have begun to meaningfully understand these formats.
In JavaScript and TypeScript, temporal parsing remains especially difficult. No single data structure can fully represent time. Instead, we are left with a wide variety of string representations, each with different semantics and assumptions.
Even the TC39 community explicitly chose not to fully solve parsing when designing the Temporal API, acknowledging the scope and complexity of the problem. (See: https://tc39.es/proposal-temporal/docs/parse-draft.html)
And yet, time remains one of the most important concepts for human productivity and coordination.
This project tackles the problem head-on.
This repository treats temporal parsing as a compiler problem.
Instead of relying on fragile regexes or opinionated parsers, we apply classic compiler techniques—lexing and parsing—to temporal strings. Our goal is not to impose a single "correct" interpretation of time, but to make the structure of temporal expressions explicit and programmable.
What makes this project different is that we intentionally expose the lexer.
- The lexer is designed to be generic and logic-light
- It focuses on turning temporal strings into a meaningful token stream
- Parsers are built on top of this lexer to interpret tokens into higher-level structures
If the provided parser does not match your needs, you are free to:
- Write your own parser
- Extend or replace parts of the grammar
- Apply your own semantics to the same token stream
In other words, this project does not claim to "solve time." It gives you the tools to reason about it.
Main parser function that accepts an ISO 8601 / IXDTF string and returns an AST.
Returns: One of:
DateTimeAst- A datetime value with optional timezone and annotationsDurationAst- A duration value (P...)RangeAst- A range between two values
Throws: ParseError if the input is invalid.
Tokenizes the input string into a stream of tokens.
Post-processes tokens to combine timezone offset components into single tokens.
Parses a numeric timezone offset string.
Supported formats:
- Standard:
+08:00,-05:30 - Compact:
+0530,-0800 - Short:
+09,-05
Valid ranges:
- Hours: 0-14 (UTC-12:00 to UTC+14:00)
- Minutes: 0-59
Converts a temporal AST back to its string representation.
Returns: ISO 8601 / IXDTF formatted string
Also available:
stringifyDate(date: DateAst): stringstringifyTime(time: TimeAst): stringstringifyDateTime(dateTime: DateTimeAst): stringstringifyDuration(duration: DurationAst): stringstringifyRange(range: RangeAst): stringstringifyOffset(offset: OffsetAst): stringstringifyTimeZone(timeZone: TimeZoneAst): stringstringifyAnnotation(annotation: AnnotationAst): string
Full TypeScript definitions are included. All AST types are exported:
import type {
TemporalAst,
DateTimeAst,
DurationAst,
RangeAst,
DateAst,
TimeAst,
OffsetAst,
TimeZoneAst,
AnnotationAst,
} from '@taskade/temporal-parser';See CONTRIBUTING.md for development setup and guidelines.
This project was developed with LLM assistance (GPT 5.2/Claude Sonnet 4.5), under human direction for design decisions, architecture, and verification. All code is tested and reviewed on a best-effort basis.
MIT © Taskade