{"id":37230400,"url":"https://github.com/taskade/temporal-parser","last_synced_at":"2026-01-15T03:38:15.731Z","repository":{"id":332065415,"uuid":"1131222739","full_name":"taskade/temporal-parser","owner":"taskade","description":"A lexer and parser for ISO 8601, RFC 3339, and IXDTF temporal expressions, built with compiler design principles.","archived":false,"fork":false,"pushed_at":"2026-01-12T13:12:23.000Z","size":81,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-12T17:59:48.508Z","etag":null,"topics":["compiler","date","datetime","duration","iso8601","ixdtf","lexer","parser","rfc3339","temporal","timezone"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@taskade/temporal-parser","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/taskade.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-09T16:55:40.000Z","updated_at":"2026-01-12T13:11:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/taskade/temporal-parser","commit_stats":null,"previous_names":["taskade/temporal-parser"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/taskade/temporal-parser","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taskade%2Ftemporal-parser","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taskade%2Ftemporal-parser/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taskade%2Ftemporal-parser/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taskade%2Ftemporal-parser/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/taskade","download_url":"https://codeload.github.com/taskade/temporal-parser/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taskade%2Ftemporal-parser/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28442303,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-15T00:55:22.719Z","status":"online","status_checked_at":"2026-01-15T02:00:08.019Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compiler","date","datetime","duration","iso8601","ixdtf","lexer","parser","rfc3339","temporal","timezone"],"created_at":"2026-01-15T03:38:15.090Z","updated_at":"2026-01-15T03:38:15.725Z","avatar_url":"https://github.com/taskade.png","language":"TypeScript","funding_links":[],"categories":["🧬 Taskade Ecosystem Snapshot (Verified 2026-06-01)"],"sub_categories":[],"readme":"# Temporal Parser\n\nA lexer and parser for ISO 8601, RFC 3339, and IXDTF temporal expressions, built with compiler design principles.\n\n## Features\n\n- **Standards compliant**: ISO 8601, RFC 3339, and IXDTF support\n- **Extensible architecture**: Exposed lexer allows custom parser implementations\n- **Zero dependencies**: No external runtime dependencies\n- **Type-safe**: Written in TypeScript with full type definitions\n- **Small footprint**: ~18KB minified\n- **Dual module support**: ESM and CommonJS builds\n- **Well tested**: 91% test coverage with 271 test cases\n\n## Installation\n\n```bash\nnpm install @taskade/temporal-parser\n```\n\n## Quick Start\n\n```typescript\nimport { parseTemporal } from '@taskade/temporal-parser';\n\n// Parse a complete datetime with timezone\nconst result = parseTemporal('2025-01-12T10:00:00+08:00[Asia/Singapore]');\nconsole.log(result);\n// {\n//   kind: 'DateTime',\n//   date: { kind: 'Date', year: 2025, month: 1, day: 12 },\n//   time: { kind: 'Time', hour: 10, minute: 0, second: 0 },\n//   offset: { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' },\n//   timeZone: { kind: 'IanaTimeZone', id: 'Asia/Singapore', critical: false },\n//   annotations: []\n// }\n\n// Parse a duration\nconst duration = parseTemporal('P1Y2M3DT4H5M6S');\n// { kind: 'Duration', years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, ... }\n\n// Parse a date range\nconst range = parseTemporal('2025-01-01/2025-12-31');\n// { kind: 'Range', start: {...}, end: {...} }\n\n// Parse BC dates (negative years in ISO 8601)\nconst bcDate = parseTemporal('-0044-03-15');\n// { kind: 'DateTime', date: { year: -44, month: 3, day: 15 }, ... }\n```\n\n## Supported Formats\n\n### Dates\n- Year: `2025`\n- Year-Month: `2025-01`\n- Full date: `2025-01-12`\n- BC dates (negative years): `-0044-03-15` (44 BC), `0000-01-01` (1 BC)\n\n### Times\n- Hour-Minute: `T10:30`\n- With seconds: `T10:30:45`\n- With fractional seconds: `T10:30:45.123456789`\n- European format (comma): `T10:30:45,123` (normalized to dot in output)\n\n### Timezones\n- UTC: `Z`\n- Numeric offset: `+08:00`, `-05:30`, `+0530`, `+09`\n- IANA timezone: `[Asia/Singapore]`, `[America/New_York]`\n\n### Durations\n- Date parts: `P1Y2M3D` (1 year, 2 months, 3 days)\n- Time parts: `PT4H5M6S` (4 hours, 5 minutes, 6 seconds)\n- Combined: `P1Y2M3DT4H5M6S`\n- Fractional seconds: `PT1.5S` or `PT1,5S` (comma normalized to dot)\n\n### IXDTF Annotations\n- Calendar: `[u-ca=gregory]`\n- Critical flags: `[!u-ca=iso8601]`\n- Multiple annotations: `2025-01-12[u-ca=gregory][u-tz=UTC]`\n\n### Ranges\n- Closed: `2025-01-01/2025-12-31`\n- Open start: `/2025-12-31`\n- Open end: `2025-01-01/`\n- Duration-based: `2025-01-01/P1Y`\n\n## Advanced Usage\n\n### Using the Lexer\n\n```typescript\nimport { lexTemporal, combineTimezoneOffsets } from '@taskade/temporal-parser';\n\n// Tokenize a temporal string\nconst tokens = lexTemporal('2025-01-12T10:00:00+08:00');\n\n// Optionally combine timezone offset tokens\nconst combined = combineTimezoneOffsets(tokens);\n```\n\n### Standalone Offset Parser\n\n```typescript\nimport { parseOffset } from '@taskade/temporal-parser';\n\nconst offset = parseOffset('+08:00');\n// { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' }\n```\n\n### Stringify AST Back to String\n\n```typescript\nimport { parseTemporal, stringifyTemporal } from '@taskade/temporal-parser';\n\n// Parse and stringify\nconst ast = parseTemporal('2025-01-12T10:00:00+08:00[Asia/Singapore]');\nconst str = stringifyTemporal(ast);\n// '2025-01-12T10:00:00+08:00[Asia/Singapore]'\n\n// Offsets are normalized to canonical format (±HH:MM)\nconst ast2 = parseTemporal('2025-01-12T10:00:00+0530'); // Compact format\nconst str2 = stringifyTemporal(ast2);\n// '2025-01-12T10:00:00+05:30' (normalized)\n\n// Stringify individual components\nimport { stringifyDate, stringifyTime, stringifyDuration } from '@taskade/temporal-parser';\n\nstringifyDate({ kind: 'Date', year: 2025, month: 1, day: 12 });\n// '2025-01-12'\n\nstringifyTime({ kind: 'Time', hour: 10, minute: 30, second: 45 });\n// '10:30:45'\n\nstringifyDuration({ kind: 'Duration', years: 1, months: 2, raw: 'P1Y2M', annotations: [] });\n// 'P1Y2M'\n```\n\n## Motivation\n\nTime is one of the most complex human inventions.\nLeap 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.\n\nDespite 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.\n\nIn 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.\n\nEven the TC39 community explicitly chose not to fully solve parsing when designing the Temporal API, acknowledging the scope and complexity of the problem.\n(See: https://tc39.es/proposal-temporal/docs/parse-draft.html)\n\nAnd yet, time remains one of the most important concepts for human productivity and coordination.\n\nThis project tackles the problem head-on.\n\n## Approach\n\nThis repository treats temporal parsing as a compiler problem.\n\nInstead 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.\n\nWhat makes this project different is that we intentionally expose the lexer.\n- The lexer is designed to be generic and logic-light\n- It focuses on turning temporal strings into a meaningful token stream\n- Parsers are built on top of this lexer to interpret tokens into higher-level structures\n\nIf the provided parser does not match your needs, you are free to:\n- Write your own parser\n- Extend or replace parts of the grammar\n- Apply your own semantics to the same token stream\n\nIn other words, this project does not claim to \"solve time.\"\nIt gives you the tools to reason about it.\n\n## API Reference\n\n### `parseTemporal(input: string): TemporalAst`\n\nMain parser function that accepts an ISO 8601 / IXDTF string and returns an AST.\n\n**Returns:** One of:\n- `DateTimeAst` - A datetime value with optional timezone and annotations\n- `DurationAst` - A duration value (P...)\n- `RangeAst` - A range between two values\n\n**Throws:** `ParseError` if the input is invalid.\n\n### `lexTemporal(input: string): Token[]`\n\nTokenizes the input string into a stream of tokens.\n\n### `combineTimezoneOffsets(tokens: Token[]): AnyToken[]`\n\nPost-processes tokens to combine timezone offset components into single tokens.\n\n### `parseOffset(offsetString: string, position?: number): OffsetAst`\n\nParses a numeric timezone offset string.\n\n**Supported formats:**\n- Standard: `+08:00`, `-05:30`\n- Compact: `+0530`, `-0800`\n- Short: `+09`, `-05`\n\n**Valid ranges:**\n- Hours: 0-14 (UTC-12:00 to UTC+14:00)\n- Minutes: 0-59\n\n### `stringifyTemporal(ast: TemporalAst): string`\n\nConverts a temporal AST back to its string representation.\n\n**Returns:** ISO 8601 / IXDTF formatted string\n\n**Also available:**\n- `stringifyDate(date: DateAst): string`\n- `stringifyTime(time: TimeAst): string`\n- `stringifyDateTime(dateTime: DateTimeAst): string`\n- `stringifyDuration(duration: DurationAst): string`\n- `stringifyRange(range: RangeAst): string`\n- `stringifyOffset(offset: OffsetAst): string`\n- `stringifyTimeZone(timeZone: TimeZoneAst): string`\n- `stringifyAnnotation(annotation: AnnotationAst): string`\n\n## TypeScript Support\n\nFull TypeScript definitions are included. All AST types are exported:\n\n```typescript\nimport type {\n  TemporalAst,\n  DateTimeAst,\n  DurationAst,\n  RangeAst,\n  DateAst,\n  TimeAst,\n  OffsetAst,\n  TimeZoneAst,\n  AnnotationAst,\n} from '@taskade/temporal-parser';\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.\n\n## Development\n\nThis 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.\n\n## License\n\nMIT © [Taskade](https://taskade.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaskade%2Ftemporal-parser","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftaskade%2Ftemporal-parser","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaskade%2Ftemporal-parser/lists"}