@taskade/temporal-parser
Advanced tools
| import type { TimeAst } from './parser-types.js'; | ||
| /** | ||
| * Parse a time-of-day string from multiple common formats. | ||
| * Supports: locale time ("9:07 AM"), 24h ("09:00", "14:30"), short 24h ("9:00"), | ||
| * and 12h with lowercase period ("9:07 am"). | ||
| * | ||
| * TAA (LLM) often generates time in 24h format ("09:00") instead of the locale | ||
| * format ("9:00 AM") that Luxon's 't' token expects. | ||
| * | ||
| * Supported formats: | ||
| * - Locale time (12-hour with AM/PM): "9:07 AM", "2:30 PM", "02:30PM", "2:30 p.m." | ||
| * - 24-hour format: "09:00", "14:30", "23:59" | ||
| * - Short 24-hour (single digit hour): "9:00", "9:30" | ||
| * - Lowercase am/pm: "9:07 am", "2:30 pm" | ||
| * - With optional seconds: "2:30:45 PM", "14:30:45" | ||
| * - With optional fractional seconds: "2:30:45.123 PM", "14:30:45.123" | ||
| * | ||
| * Returns a TimeAst object compatible with Temporal.PlainTime.from() | ||
| * | ||
| * @param input - The time string to parse | ||
| * @returns TimeAst object with hour, minute, and optional second/fraction | ||
| * @throws ParseError if the input is invalid | ||
| */ | ||
| export declare function parseTimeString(input: string): TimeAst; | ||
| //# sourceMappingURL=parseTimeString.d.ts.map |
| {"version":3,"file":"parseTimeString.d.ts","sourceRoot":"","sources":["../../src/parseTimeString.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA2JtD"} |
+6
-0
| # @taskade/temporal-parser | ||
| ## 1.2.0 | ||
| ### Minor Changes | ||
| - e2b6e24: Add `parseTimeString` function for parsing standalone time strings in multiple formats (12h/24h, AM/PM, locale variations). Returns `TimeAst` compatible with Temporal.PlainTime. Handles common LLM-generated time formats like "09:00" alongside locale formats like "9:07 AM". Features LLM-friendly error messages with examples and suggestions. | ||
| ## 1.1.0 | ||
@@ -4,0 +10,0 @@ |
+115
-0
@@ -31,2 +31,3 @@ "use strict"; | ||
| parseTemporal: () => parseTemporal, | ||
| parseTimeString: () => parseTimeString, | ||
| stringifyAnnotation: () => stringifyAnnotation, | ||
@@ -675,2 +676,115 @@ stringifyDate: () => stringifyDate, | ||
| // src/parseTimeString.ts | ||
| function parseTimeString(input) { | ||
| const trimmed = input.trim(); | ||
| if (!trimmed) { | ||
| throw new ParseError('Empty time string. Expected format: "9:07 AM", "09:00", or "14:30"', 0); | ||
| } | ||
| const tokens = lexTemporal(trimmed); | ||
| let i = 0; | ||
| const peek = (k = 0) => tokens[Math.min(i + k, tokens.length - 1)]; | ||
| const at = (type) => peek().type === type; | ||
| const eat = (type) => { | ||
| const t = peek(); | ||
| if (t.type !== type) { | ||
| throw new ParseError( | ||
| `Invalid time format: "${trimmed}". Expected format: "HH:MM" (e.g., "09:00", "14:30") or "H:MM AM/PM" (e.g., "9:07 AM")`, | ||
| i | ||
| ); | ||
| } | ||
| i++; | ||
| return t; | ||
| }; | ||
| const tryEat = (type) => { | ||
| if (at(type)) { | ||
| return eat(type); | ||
| } | ||
| return null; | ||
| }; | ||
| const hourTok = eat("Number" /* Number */); | ||
| let hour = toInt(hourTok.value, "hour", i); | ||
| eat("Colon" /* Colon */); | ||
| const minuteTok = eat("Number" /* Number */); | ||
| const minute = toInt(minuteTok.value, "minute", i); | ||
| if (minuteTok.value.length !== 2) { | ||
| throw new ParseError( | ||
| `Invalid time format: "${trimmed}". Minutes must be 2 digits (e.g., "9:07" not "9:7")`, | ||
| i | ||
| ); | ||
| } | ||
| if (minute < 0 || minute > 59) { | ||
| throw new ParseError(`Invalid minute: ${minute}. Minutes must be between 00-59`, i); | ||
| } | ||
| let second; | ||
| let fraction; | ||
| if (tryEat("Colon" /* Colon */)) { | ||
| const secondTok = eat("Number" /* Number */); | ||
| second = toInt(secondTok.value, "second", i); | ||
| if (second < 0 || second > 59) { | ||
| throw new ParseError(`Invalid second: ${second}. Seconds must be between 00-59`, i); | ||
| } | ||
| if (tryEat("Dot" /* Dot */) || tryEat("Comma" /* Comma */)) { | ||
| const fracTok = eat("Number" /* Number */); | ||
| fraction = fracTok.value; | ||
| } | ||
| } | ||
| let ampmStr; | ||
| if (at("Ident" /* Ident */)) { | ||
| const identTok = eat("Ident" /* Ident */); | ||
| const ident = String(identTok.value).toUpperCase(); | ||
| if (ident === "A" || ident === "P" || ident === "AM" || ident === "PM") { | ||
| ampmStr = ident[0]; | ||
| } else { | ||
| throw new ParseError( | ||
| `Invalid time format: unexpected "${identTok.value}". Use AM/PM for 12-hour format or omit for 24-hour format`, | ||
| i | ||
| ); | ||
| } | ||
| tryEat("Dot" /* Dot */); | ||
| if (at("Ident" /* Ident */)) { | ||
| const mTok = tryEat("Ident" /* Ident */); | ||
| if (mTok && String(mTok.value).toUpperCase() !== "M") { | ||
| throw new ParseError( | ||
| `Invalid time format: unexpected "${mTok.value}". Expected "a.m." or "p.m." format`, | ||
| i | ||
| ); | ||
| } | ||
| tryEat("Dot" /* Dot */); | ||
| } | ||
| } | ||
| eat("EOF" /* EOF */); | ||
| if (ampmStr !== void 0) { | ||
| const isPM = ampmStr === "P"; | ||
| if (hour < 1 || hour > 12) { | ||
| throw new ParseError( | ||
| `Invalid hour for 12-hour format: ${hour}. Hours with AM/PM must be between 1-12 (e.g., "9:07 AM", not "0:07 AM" or "13:07 AM")`, | ||
| i | ||
| ); | ||
| } | ||
| if (isPM) { | ||
| if (hour !== 12) { | ||
| hour += 12; | ||
| } | ||
| } else { | ||
| if (hour === 12) { | ||
| hour = 0; | ||
| } | ||
| } | ||
| } else { | ||
| if (hour < 0 || hour > 23) { | ||
| throw new ParseError( | ||
| `Invalid hour for 24-hour format: ${hour}. Hours must be between 0-23 (e.g., "09:00", "14:30"). For hours > 12, use 24-hour format or add AM/PM`, | ||
| i | ||
| ); | ||
| } | ||
| } | ||
| return { | ||
| kind: "Time", | ||
| hour, | ||
| minute, | ||
| second, | ||
| fraction | ||
| }; | ||
| } | ||
| // src/stringify.ts | ||
@@ -798,2 +912,3 @@ function stringifyTemporal(ast) { | ||
| parseTemporal, | ||
| parseTimeString, | ||
| stringifyAnnotation, | ||
@@ -800,0 +915,0 @@ stringifyDate, |
+114
-0
@@ -632,2 +632,115 @@ // src/lexer-types.ts | ||
| // src/parseTimeString.ts | ||
| function parseTimeString(input) { | ||
| const trimmed = input.trim(); | ||
| if (!trimmed) { | ||
| throw new ParseError('Empty time string. Expected format: "9:07 AM", "09:00", or "14:30"', 0); | ||
| } | ||
| const tokens = lexTemporal(trimmed); | ||
| let i = 0; | ||
| const peek = (k = 0) => tokens[Math.min(i + k, tokens.length - 1)]; | ||
| const at = (type) => peek().type === type; | ||
| const eat = (type) => { | ||
| const t = peek(); | ||
| if (t.type !== type) { | ||
| throw new ParseError( | ||
| `Invalid time format: "${trimmed}". Expected format: "HH:MM" (e.g., "09:00", "14:30") or "H:MM AM/PM" (e.g., "9:07 AM")`, | ||
| i | ||
| ); | ||
| } | ||
| i++; | ||
| return t; | ||
| }; | ||
| const tryEat = (type) => { | ||
| if (at(type)) { | ||
| return eat(type); | ||
| } | ||
| return null; | ||
| }; | ||
| const hourTok = eat("Number" /* Number */); | ||
| let hour = toInt(hourTok.value, "hour", i); | ||
| eat("Colon" /* Colon */); | ||
| const minuteTok = eat("Number" /* Number */); | ||
| const minute = toInt(minuteTok.value, "minute", i); | ||
| if (minuteTok.value.length !== 2) { | ||
| throw new ParseError( | ||
| `Invalid time format: "${trimmed}". Minutes must be 2 digits (e.g., "9:07" not "9:7")`, | ||
| i | ||
| ); | ||
| } | ||
| if (minute < 0 || minute > 59) { | ||
| throw new ParseError(`Invalid minute: ${minute}. Minutes must be between 00-59`, i); | ||
| } | ||
| let second; | ||
| let fraction; | ||
| if (tryEat("Colon" /* Colon */)) { | ||
| const secondTok = eat("Number" /* Number */); | ||
| second = toInt(secondTok.value, "second", i); | ||
| if (second < 0 || second > 59) { | ||
| throw new ParseError(`Invalid second: ${second}. Seconds must be between 00-59`, i); | ||
| } | ||
| if (tryEat("Dot" /* Dot */) || tryEat("Comma" /* Comma */)) { | ||
| const fracTok = eat("Number" /* Number */); | ||
| fraction = fracTok.value; | ||
| } | ||
| } | ||
| let ampmStr; | ||
| if (at("Ident" /* Ident */)) { | ||
| const identTok = eat("Ident" /* Ident */); | ||
| const ident = String(identTok.value).toUpperCase(); | ||
| if (ident === "A" || ident === "P" || ident === "AM" || ident === "PM") { | ||
| ampmStr = ident[0]; | ||
| } else { | ||
| throw new ParseError( | ||
| `Invalid time format: unexpected "${identTok.value}". Use AM/PM for 12-hour format or omit for 24-hour format`, | ||
| i | ||
| ); | ||
| } | ||
| tryEat("Dot" /* Dot */); | ||
| if (at("Ident" /* Ident */)) { | ||
| const mTok = tryEat("Ident" /* Ident */); | ||
| if (mTok && String(mTok.value).toUpperCase() !== "M") { | ||
| throw new ParseError( | ||
| `Invalid time format: unexpected "${mTok.value}". Expected "a.m." or "p.m." format`, | ||
| i | ||
| ); | ||
| } | ||
| tryEat("Dot" /* Dot */); | ||
| } | ||
| } | ||
| eat("EOF" /* EOF */); | ||
| if (ampmStr !== void 0) { | ||
| const isPM = ampmStr === "P"; | ||
| if (hour < 1 || hour > 12) { | ||
| throw new ParseError( | ||
| `Invalid hour for 12-hour format: ${hour}. Hours with AM/PM must be between 1-12 (e.g., "9:07 AM", not "0:07 AM" or "13:07 AM")`, | ||
| i | ||
| ); | ||
| } | ||
| if (isPM) { | ||
| if (hour !== 12) { | ||
| hour += 12; | ||
| } | ||
| } else { | ||
| if (hour === 12) { | ||
| hour = 0; | ||
| } | ||
| } | ||
| } else { | ||
| if (hour < 0 || hour > 23) { | ||
| throw new ParseError( | ||
| `Invalid hour for 24-hour format: ${hour}. Hours must be between 0-23 (e.g., "09:00", "14:30"). For hours > 12, use 24-hour format or add AM/PM`, | ||
| i | ||
| ); | ||
| } | ||
| } | ||
| return { | ||
| kind: "Time", | ||
| hour, | ||
| minute, | ||
| second, | ||
| fraction | ||
| }; | ||
| } | ||
| // src/stringify.ts | ||
@@ -754,2 +867,3 @@ function stringifyTemporal(ast) { | ||
| parseTemporal, | ||
| parseTimeString, | ||
| stringifyAnnotation, | ||
@@ -756,0 +870,0 @@ stringifyDate, |
| { | ||
| "version": 3, | ||
| "sources": ["../../src/lexer-types.ts", "../../src/combineTimezoneOffsets.ts", "../../src/errors.ts", "../../src/helpers/isAlpha.ts", "../../src/helpers/isDigit.ts", "../../src/helpers/isBracketWordChar.ts", "../../src/lexer.ts", "../../src/helpers/toInt.ts", "../../src/parseOffset.ts", "../../src/parser.ts", "../../src/stringify.ts"], | ||
| "sourcesContent": ["// lexer-types.ts\n// Token types and interfaces for the lexer\n\nexport enum TokType {\n // Numbers: sequences of digits (0-9)\n // Examples: \"2025\", \"01\", \"12\", \"59\"\n Number = 'Number',\n\n // Ident (Identifier): sequences of alphabetic characters (A-Z, a-z)\n // This is the generic \"text\" token for letter-based designators.\n // Examples: \"P\" (duration), \"Y\" (year), \"M\" (month), \"gregory\" (calendar)\n // The lexer keeps it simple; the parser interprets the meaning.\n Ident = 'Ident',\n\n // Single-character punctuation tokens\n Dash = 'Dash', // -\n Colon = 'Colon', // :\n Dot = 'Dot', // .\n Plus = 'Plus', // +\n Slash = 'Slash', // /\n Comma = 'Comma', // ,\n Equals = 'Equals', // =\n Underscore = 'Underscore', // _\n Exclamation = 'Exclamation', // ! (critical flag in IXDTF)\n\n // Special letter tokens with dedicated meaning in ISO 8601\n T = 'T', // Date/time separator: 2025-01-01T10:00:00\n Z = 'Z', // UTC timezone marker: 2025-01-01T10:00:00Z\n\n // Bracket tokens for IXDTF annotations/extensions\n LBracket = 'LBracket', // [\n RBracket = 'RBracket', // ]\n\n // BracketText: complex text inside brackets that contains non-letter chars\n // Example: in [u-ca=gregory], \"u-ca\" is BracketText (has dash), \"gregory\" is Ident (letters only)\n // Example: in [Asia/Singapore], \"Asia/Singapore\" is BracketText (has slash)\n BracketText = 'BracketText',\n\n EOF = 'EOF',\n}\n\nexport type Token = {\n type: TokType;\n value: string;\n start: number; // inclusive\n end: number; // exclusive\n};\n\n// Combined token types for timezone offsets\nexport enum CombinedTokType {\n TZOffset = 'TZOffset', // +08:00, -0530, +09\n}\n\nexport type CombinedToken = {\n type: CombinedTokType;\n value: string;\n tokens: Token[];\n start: number;\n end: number;\n};\n\nexport type AnyToken = Token | CombinedToken;\n", "// combineTimezoneOffsets.ts\n// Combines numeric timezone offset tokens into single tokens for easier parsing\n\nimport { type AnyToken, CombinedTokType, type Token, TokType } from './lexer-types.js';\n\n/**\n * Combine numeric timezone offsets into single tokens for easier parsing.\n * This is an optional post-processing step for the lexer output.\n *\n * Combines patterns like:\n * - (+|-) Number Colon Number => +08:00, -05:30\n * - (+|-) Number (2 or 4 digits) => +09, -0530\n *\n * Only combines when context suggests a timezone offset (after time components),\n * NOT after:\n * - Z (mutually exclusive per ISO 8601)\n * - brackets (offset comes BEFORE brackets per RFC 9557/IXDTF)\n * - standalone dates (e.g., 2025-01 where - is a date separator)\n *\n * @param tokens - The raw token array from lexTemporal\n * @returns Array of tokens (either original Token or combined CombinedToken with TZOffset type where applicable)\n *\n * @example\n * ```typescript\n * const raw = lexTemporal('10:00:00+08:00');\n * const combined = combineTimezoneOffsets(raw);\n * // Result includes: { type: 'TZOffset', value: '+08:00' }\n * ```\n */\nexport function combineTimezoneOffsets(tokens: Token[]): AnyToken[] {\n const out: AnyToken[] = [];\n let i = 0;\n\n const peek = (k: number) => tokens[i + k];\n\n // Helper to check if previous tokens suggest this could be a timezone offset\n // Timezone offsets come after time components (HH:MM:SS)\n // NOT after just a date like 2025-01\n // NOT after Z (Z and numeric offsets are mutually exclusive per ISO 8601)\n // NOT after brackets (per RFC 9557/IXDTF: offset comes BEFORE brackets)\n const couldBeTzOffset = () => {\n if (out.length < 1) {\n return false;\n }\n const prev: AnyToken | undefined = out[out.length - 1];\n if (prev == null) {\n return false;\n }\n\n // After Z - NOT valid (Z and numeric offsets are mutually exclusive)\n // Per ISO 8601: use either Z OR +HH:MM, never both\n if (prev.type === TokType.Z) {\n return false;\n }\n\n // After bracket - NOT valid (offset must come BEFORE brackets per RFC 9557)\n // Valid: Time+08:00[Asia/Tokyo]\n // Invalid: [Asia/Tokyo]+08:00\n if (prev.type === TokType.RBracket) {\n return false;\n }\n\n // After a number, check context\n if (prev.type === TokType.Number) {\n if (out.length < 2) {\n return false;\n }\n const prevPrev = out[out.length - 2];\n\n // After a number with colon before it (time component)\n // e.g., ...10:00:00+08:00 - the number before + has a colon before it\n if (prevPrev?.type === TokType.Colon) {\n return true;\n }\n\n // After a number with dot before it (milliseconds)\n // e.g., ...10:00:00.999+08:00 - the number before + has a dot before it\n if (prevPrev?.type === TokType.Dot) {\n return true;\n }\n }\n\n return false;\n };\n\n while (i < tokens.length) {\n const t = tokens[i]!;\n const isSign = t.type === TokType.Plus || t.type === TokType.Dash;\n\n // Only try to combine if context suggests timezone offset\n if (isSign && couldBeTzOffset()) {\n // (+|-) Number Colon Number => +08:00\n if (\n peek(1)?.type === TokType.Number &&\n peek(2)?.type === TokType.Colon &&\n peek(3)?.type === TokType.Number\n ) {\n const originalTokens = [t, peek(1)!, peek(2)!, peek(3)!];\n const start = t.start;\n const end = peek(3)!.end;\n const value = `${t.value}${peek(1)!.value}:${peek(3)!.value}`;\n out.push({ type: CombinedTokType.TZOffset, value, tokens: originalTokens, start, end });\n i += 4;\n continue;\n }\n\n // (+|-) Number where Number is 2 or 4 digits => +09 or -0530\n if (peek(1)?.type === TokType.Number) {\n const num = peek(1)!.value;\n if (num.length === 2 || num.length === 4) {\n const originalTokens = [t, peek(1)!];\n const start = t.start;\n const end = peek(1)!.end;\n const value = `${t.value}${num}`;\n out.push({ type: CombinedTokType.TZOffset, value, tokens: originalTokens, start, end });\n i += 2;\n continue;\n }\n }\n }\n\n out.push(t);\n i++;\n }\n\n return out;\n}\n", "// errors.ts\n// Error classes for lexer and parser\n\nexport class LexError extends Error {\n public readonly position: number;\n constructor(message: string, position: number) {\n super(`${message} at position ${position}`);\n this.name = 'LexError';\n this.position = position;\n }\n}\n\nexport class ParseError extends Error {\n public readonly position: number;\n constructor(message: string, position: number) {\n super(`${message} at token index ${position}`);\n this.name = 'ParseError';\n this.position = position;\n }\n}\n", "/**\n * Check if character is alphabetic (A-Z or a-z)\n * This defines what can be part of an IDENT token in default mode\n * @param c - the character to check\n * @returns true if character is alphabetic, false otherwise\n */\nexport function isAlpha(c: string): boolean {\n return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');\n}\n", "/**\n * Check if character is a digit (0-9)\n * @param c - the character to check\n * @returns true if character is a digit, false otherwise\n */\nexport function isDigit(c: string) {\n return c >= '0' && c <= '9';\n}\n", "import { isAlpha } from './isAlpha.js';\nimport { isDigit } from './isDigit.js';\n\n/**\n * Check if character is valid inside bracket annotations\n * In default mode, IDENT is letters only (keeps date/time parsing clean).\n * In bracket mode, we allow broader \"words\" like:\n * - u-ca (has dash)\n * - Asia/Singapore (has slash)\n * - Etc/GMT+8 (has plus)\n * - iso8601 (has digits)\n * @param c - the character to check\n * @returns true if character is valid in bracket context, false otherwise\n */\nexport function isBracketWordChar(c: string): boolean {\n return (\n isAlpha(c) || isDigit(c) || c === '-' || c === '/' || c === '+' || c === '.' // allow dots in some TZ-ish values\n );\n}\n", "// lexer.ts\n// Temporal (IXDTF / ISO-8601-ish) lexer with support for *extensions/annotations* like calendar.\n//\n// Supports:\n// - Basic date/time tokens: numbers, identifiers, T, Z, punctuation\n// - Ranges: '/'\n// - Durations: 'P' (as IDENT; parser enforces rules)\n// - Time zone forms:\n// - 'Z'\n// - numeric offsets: +08:00 / -0530 / +09 (optionally combined via helper)\n// - bracketed IANA TZ: [Asia/Singapore]\n// - IXDTF-style annotations/extensions in brackets, e.g.:\n// - [u-ca=gregory] (calendar)\n// - [u-tz=Asia/Singapore] (timeZone)\n// - multiple bracket groups: 2025-01-01T00:00Z[u-ca=iso8601][u-tz=UTC]\n//\n// The lexer is permissive; your parser should validate semantics.\n\nimport { LexError } from './errors.js';\nimport { isAlpha } from './helpers/isAlpha.js';\nimport { isBracketWordChar } from './helpers/isBracketWordChar.js';\nimport { isDigit } from './helpers/isDigit.js';\nimport type { Token } from './lexer-types.js';\nimport { TokType } from './lexer-types.js';\n\nexport function lexTemporal(src: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n const emit = (type: TokType, start: number, end: number, value?: string) => {\n tokens.push({ type, value: value ?? src.slice(start, end), start, end });\n };\n\n // Lex a number: consume consecutive digits\n // Examples: \"2025\", \"01\", \"59\"\n const lexNumber = () => {\n const start = i;\n i++;\n while (i < src.length && isDigit(src[i]!)) {\n i++;\n }\n emit(TokType.Number, start, i);\n };\n\n // Lex an identifier: consume consecutive alphabetic characters\n // This is where IDENT tokens are created!\n //\n // Examples of what becomes IDENT:\n // - Duration designators: P, Y, M, W, D, H, S\n // - Calendar names: gregory, hebrew, iso8601\n // - Any letter sequence: P, YMD, UTC, America\n //\n // Special cases:\n // - \"T\" gets its own token type (datetime separator)\n // - \"Z\" gets its own token type (UTC marker)\n // - Everything else becomes Ident\n //\n // IMPORTANT: We only consume a single letter at a time to allow T and Z\n // to be recognized individually (e.g., \"DT\" should be \"D\" then \"T\", not \"DT\")\n const lexIdent = () => {\n const start = i;\n const ch = src[i]!;\n i++;\n\n // Special handling for T and Z - emit them as special tokens\n if (ch === 'T') {\n emit(TokType.T, start, i, 'T');\n return;\n }\n if (ch === 'Z') {\n emit(TokType.Z, start, i, 'Z');\n return;\n }\n\n // For other letters, consume consecutive letters that are NOT T or Z\n // This allows multi-letter identifiers like \"gregory\" but keeps T and Z separate\n while (i < src.length && isAlpha(src[i]!) && src[i] !== 'T' && src[i] !== 'Z') {\n i++;\n }\n\n emit(TokType.Ident, start, i); // Generic identifier\n };\n\n const lexBracketContent = () => {\n // We are positioned after '['. Emit content tokens until ']'.\n while (i < src.length) {\n const ch = src[i]!;\n\n if (ch === ']') {\n return;\n }\n\n // Skip whitespace inside brackets (rare but harmless)\n if (/\\s/.test(ch)) {\n i++;\n continue;\n }\n\n // Single-char tokens inside brackets\n if (ch === '=') {\n emit(TokType.Equals, i, i + 1);\n i++;\n continue;\n }\n if (ch === '_') {\n emit(TokType.Underscore, i, i + 1);\n i++;\n continue;\n }\n if (ch === ':') {\n emit(TokType.Colon, i, i + 1);\n i++;\n continue;\n }\n if (ch === '-') {\n emit(TokType.Dash, i, i + 1);\n i++;\n continue;\n }\n if (ch === '+') {\n emit(TokType.Plus, i, i + 1);\n i++;\n continue;\n }\n if (ch === '/') {\n emit(TokType.Slash, i, i + 1);\n i++;\n continue;\n }\n if (ch === '.') {\n emit(TokType.Dot, i, i + 1);\n i++;\n continue;\n }\n if (ch === ',') {\n emit(TokType.Comma, i, i + 1);\n i++;\n continue;\n }\n if (ch === '!') {\n emit(TokType.Exclamation, i, i + 1);\n i++;\n continue;\n }\n\n // Words inside brackets (u-ca, gregory, Asia/Singapore, iso8601, Etc/GMT+8, etc.)\n if (isBracketWordChar(ch)) {\n const start = i;\n i++;\n while (i < src.length && isBracketWordChar(src[i]!)) {\n i++;\n }\n const text = src.slice(start, i);\n\n // Decision: Ident vs BracketText\n // - If purely letters (A-Z, a-z): emit as Ident\n // Examples: \"gregory\", \"hebrew\", \"u\", \"ca\"\n // - If contains other chars (digits, -, /, +, .): emit as BracketText\n // Examples: \"u-ca\", \"Asia/Singapore\", \"iso8601\", \"Etc/GMT+8\"\n if (/^[A-Za-z]+$/.test(text)) {\n emit(TokType.Ident, start, i, text);\n } else {\n emit(TokType.BracketText, start, i, text);\n }\n\n continue;\n }\n\n throw new LexError(`Unexpected character ${JSON.stringify(ch)} inside brackets`, i);\n }\n\n throw new LexError(\"Unterminated bracket annotation, expected ']'\", src.length);\n };\n\n while (i < src.length) {\n const ch = src[i]!;\n\n // Skip whitespace\n if (/\\s/.test(ch)) {\n i++;\n continue;\n }\n\n // Bracketed extensions/annotations/timezone IDs\n if (ch === '[') {\n emit(TokType.LBracket, i, i + 1);\n i++;\n\n lexBracketContent();\n\n if (i >= src.length || src[i] !== ']') {\n throw new LexError(\"Unterminated bracket annotation, expected ']'\", i);\n }\n emit(TokType.RBracket, i, i + 1);\n i++;\n continue;\n }\n\n // Numbers: sequences of digits\n if (isDigit(ch)) {\n lexNumber();\n continue;\n }\n\n // Letters: this is where IDENT tokens are created!\n // Any alphabetic character triggers identifier lexing.\n // Examples: P (duration), Y (year), M (month), gregory, UTC\n if (isAlpha(ch)) {\n lexIdent();\n continue;\n }\n\n // Punctuation (default mode)\n switch (ch) {\n case '-':\n emit(TokType.Dash, i, i + 1);\n i++;\n continue;\n case ':':\n emit(TokType.Colon, i, i + 1);\n i++;\n continue;\n case '.':\n emit(TokType.Dot, i, i + 1);\n i++;\n continue;\n case '+':\n emit(TokType.Plus, i, i + 1);\n i++;\n continue;\n case '/':\n emit(TokType.Slash, i, i + 1);\n i++;\n continue;\n case ',':\n emit(TokType.Comma, i, i + 1);\n i++;\n continue;\n case '=':\n emit(TokType.Equals, i, i + 1);\n i++;\n continue;\n case '_':\n emit(TokType.Underscore, i, i + 1);\n i++;\n continue;\n default:\n throw new LexError(`Unexpected character ${JSON.stringify(ch)}`, i);\n }\n }\n\n emit(TokType.EOF, src.length, src.length, '');\n return tokens;\n}\n", "import { ParseError } from '../errors.js';\n\n/**\n * Convert a string to an integer, throwing a ParseError if invalid\n * @param s - the string to convert\n * @param label - a descriptive label for error messages (e.g., \"year\", \"month\")\n * @param position - the position in the token stream for error reporting\n * @returns the parsed integer value\n * @throws {ParseError} if the string is not a valid integer\n */\nexport function toInt(s: string, label: string, position: number): number {\n const n = Number(s);\n if (!Number.isFinite(n) || !Number.isInteger(n)) {\n throw new ParseError(`Invalid integer for ${label}: ${JSON.stringify(s)}`, position);\n }\n return n;\n}\n", "// parseOffset.ts\n// Parse timezone offset strings into structured components\n\nimport { ParseError } from './errors.js';\nimport { toInt } from './helpers/toInt.js';\nimport type { OffsetAst } from './parser-types.js';\n\n// Timezone offset validation constants\n// Per ISO 8601 and real-world usage:\n// - Westernmost: UTC-12:00 (Baker Island, Howland Island)\n// - Easternmost: UTC+14:00 (Line Islands, Kiritimati)\nconst MAX_OFFSET_HOURS = 14;\nconst MAX_MINUTES = 59;\n\n/**\n * Parse a timezone offset string into structured components.\n *\n * Supports three formats:\n * - Standard: +08:00, -05:30\n * - Compact: +0530, -0800\n * - Short: +09, -05\n *\n * Valid timezone offset ranges (per ISO 8601):\n * - Hours: 0-14 (UTC-12:00 to UTC+14:00)\n * - Minutes: 0-59\n *\n * Note: This function only parses numeric offsets (e.g., +08:00).\n * For UTC, use 'Z' (UtcOffset kind) which is semantically different from +00:00.\n * Per RFC 3339 section 4.3, -00:00 indicates \"local time with unknown offset\".\n *\n * @param offsetString - The offset string to parse (e.g., '+08:00', '-0530', '+09')\n * @param position - Position in token stream for error reporting (optional)\n * @returns Parsed offset with sign, hours, and minutes\n * @throws {ParseError} If the offset format is invalid or out of range\n *\n * @example\n * ```typescript\n * parseOffset('+08:00') // { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' }\n * parseOffset('-05:30') // { kind: 'NumericOffset', sign: '-', hours: 5, minutes: 30, raw: '-05:30' }\n * parseOffset('+0530') // { kind: 'NumericOffset', sign: '+', hours: 5, minutes: 30, raw: '+0530' }\n * parseOffset('+09') // { kind: 'NumericOffset', sign: '+', hours: 9, minutes: 0, raw: '+09' }\n * parseOffset('+14:00') // { kind: 'NumericOffset', sign: '+', hours: 14, minutes: 0, raw: '+14:00' }\n * ```\n */\nexport function parseOffset(offsetString: string, position = 0): OffsetAst {\n // Validate input is not empty\n if (offsetString.length === 0) {\n throw new ParseError('Offset string cannot be empty', position);\n }\n\n const raw = offsetString;\n\n // Validate first character is sign and assert type\n const sign = raw[0];\n if (sign !== '+' && sign !== '-') {\n throw new ParseError(`Offset must start with + or -, got ${JSON.stringify(sign)}`, position);\n }\n // Type assertion: TypeScript now knows sign is '+' | '-'\n const validSign: '+' | '-' = sign;\n\n const rest = raw.slice(1);\n\n // Validate rest is not empty\n if (rest.length === 0) {\n throw new ParseError(`Offset sign must be followed by digits`, position);\n }\n\n let hours: number;\n let minutes: number;\n\n if (rest.includes(':')) {\n // Format: +08:00 or -05:30\n const parts = rest.split(':');\n if (parts.length !== 2) {\n throw new ParseError(`Invalid offset format: ${raw} (expected HH:MM)`, position);\n }\n // After length check, we know parts[0] and parts[1] exist\n const [h, m] = parts;\n hours = toInt(h!, 'offset hours', position);\n minutes = toInt(m!, 'offset minutes', position);\n } else if (rest.length === 4) {\n // Format: +0530 or -0800\n hours = toInt(rest.slice(0, 2), 'offset hours', position);\n minutes = toInt(rest.slice(2, 4), 'offset minutes', position);\n } else if (rest.length === 2) {\n // Format: +08 or -05\n hours = toInt(rest, 'offset hours', position);\n minutes = 0;\n } else {\n throw new ParseError(\n `Invalid offset format: ${raw} (expected +HH:MM, +HHMM, or +HH)`,\n position,\n );\n }\n\n // Validate ranges\n if (hours < 0 || hours > MAX_OFFSET_HOURS) {\n throw new ParseError(`Offset hours must be 0-${MAX_OFFSET_HOURS}, got ${hours}`, position);\n }\n if (minutes < 0 || minutes > MAX_MINUTES) {\n throw new ParseError(`Offset minutes must be 0-${MAX_MINUTES}, got ${minutes}`, position);\n }\n\n return { kind: 'NumericOffset', sign: validSign, hours, minutes, raw };\n}\n", "// parser.ts\n// A practical recursive-descent parser that works with the lexer we wrote.\n// It builds an AST for a useful Temporal subset:\n// - date/time (with optional fractional seconds)\n// - timezone: Z | numeric offset (+08:00, -0530, +09) | bracket TZ id [Asia/Singapore]\n// - duration: P... with optional T part\n// - range: <expr> \"/\" <expr> where each side can be dateTime/date/duration, and either side may be empty (open range)\n// - bracket extensions like [u-ca=gregory], [u-tz=Asia/Singapore], etc. (stored as key/value pairs)\n\nimport { combineTimezoneOffsets } from './combineTimezoneOffsets.js';\nimport { ParseError } from './errors.js';\nimport { toInt } from './helpers/toInt.js';\nimport { lexTemporal } from './lexer.js';\nimport { type AnyToken, CombinedTokType, TokType } from './lexer-types.js';\nimport { parseOffset } from './parseOffset.js';\nimport type {\n AnnotationAst,\n DateAst,\n DateTimeAst,\n DurationAst,\n OffsetAst,\n TemporalAst,\n TimeAst,\n TimeZoneAst,\n ValueAst,\n} from './parser-types.js';\n\n// ---------- Parser ----------\n\nclass Parser {\n private i = 0;\n constructor(private toks: AnyToken[]) {}\n\n private peek(k = 0): AnyToken {\n return this.toks[Math.min(this.i + k, this.toks.length - 1)]!;\n }\n private at(type: TokType | CombinedTokType): boolean {\n return this.peek().type === type;\n }\n private eat(type: TokType | CombinedTokType): AnyToken {\n const t = this.peek();\n if (t.type !== type) {\n throw new ParseError(`Expected ${type} but got ${t.type}`, this.i);\n }\n this.i++;\n return t;\n }\n private tryEat(type: TokType | CombinedTokType): AnyToken | null {\n if (this.at(type)) {\n return this.eat(type);\n }\n return null;\n }\n private eof(): boolean {\n return this.at(TokType.EOF);\n }\n\n // Top-level: parse a range if we see '/', otherwise a single value.\n parseTemporal(): TemporalAst {\n // Open-start range: \"/...\"\n if (this.tryEat(TokType.Slash)) {\n const end = this.parseValueOrNull(); // allow empty end too\n this.eat(TokType.EOF);\n return { kind: 'Range', start: null, end };\n }\n\n const first = this.parseValueOrNull();\n // If first is null, only valid if it's \"/\" (handled above). So error:\n if (!first) {\n throw new ParseError('Expected a value', this.i);\n }\n\n if (this.tryEat(TokType.Slash)) {\n const second = this.parseValueOrNull(); // open-ended allowed\n this.eat(TokType.EOF);\n return { kind: 'Range', start: first, end: second };\n }\n\n this.eat(TokType.EOF);\n return first;\n }\n\n private parseValueOrNull(): ValueAst | null {\n // Empty side for open range:\n if (this.at(TokType.EOF) || this.at(TokType.Slash) || this.at(TokType.RBracket)) {\n return null;\n }\n\n // Duration starts with IDENT \"P\" (as produced by lexer)\n if (this.at(TokType.Ident) && String(this.peek().value) === 'P') {\n return this.parseDuration();\n }\n\n // Otherwise parse a date/datetime (common case)\n return this.parseDateTime();\n }\n\n // -------- DateTime parsing --------\n // Supports:\n // - YYYY\n // - YYYY-MM\n // - YYYY-MM-DD\n // - + time part with T: YYYY-MM-DDTHH:MM[:SS[.fff]]\n // - offset: Z | +08:00 | -0530\n // - timezone: [Asia/Singapore] (first bracketed IANA timezone)\n // - annotations: [u-ca=...][u-tz=...] (other bracketed content)\n private parseDateTime(): DateTimeAst {\n const date = this.parseDate();\n\n let time: DateTimeAst['time'] | undefined;\n\n // time separator is TokType.T (from lexer special-casing \"T\")\n if (this.tryEat(TokType.T)) {\n time = this.parseTime();\n }\n\n // Parse numeric offset (Z or +HH:MM)\n const offset = this.parseOffsetIfPresent();\n\n // Parse bracket groups: first IANA timezone becomes timeZone, rest are annotations\n const annotations: AnnotationAst[] = [];\n let timeZone: TimeZoneAst | undefined;\n\n while (this.at(TokType.LBracket)) {\n const { raw, critical, isLikelyTzId, pairs } = this.parseBracketGroup();\n\n // First timezone-like bracket becomes the timeZone field\n // Note: Even though IXDTF doesn't typically use critical flags for timezones,\n // we preserve it in case the input has it\n if (timeZone === undefined && isLikelyTzId) {\n // Strip leading ! from id (it's stored in the critical flag)\n const id = critical ? raw.slice(1) : raw;\n timeZone = { kind: 'IanaTimeZone', id, critical };\n } else {\n annotations.push({ kind: 'Annotation', raw, critical, pairs });\n }\n }\n\n return { kind: 'DateTime', date, time, offset, timeZone, annotations };\n }\n\n private parseDate(): DateAst {\n // Check for optional leading dash (negative year / BC date)\n // ISO 8601: Year 0 = 1 BC, Year -1 = 2 BC, etc.\n const isNegative = this.tryEat(TokType.Dash);\n \n const yTok = this.eat(TokType.Number);\n let year = toInt(yTok.value, 'year', this.i);\n if (isNegative) {\n year = -year;\n }\n\n // Optional -MM\n if (!this.tryEat(TokType.Dash)) {\n return { kind: 'Date', year };\n }\n const mTok = this.eat(TokType.Number);\n const month = toInt(mTok.value, 'month', this.i);\n\n // Optional -DD\n if (!this.tryEat(TokType.Dash)) {\n return { kind: 'Date', year, month };\n }\n const dTok = this.eat(TokType.Number);\n const day = toInt(dTok.value, 'day', this.i);\n\n return { kind: 'Date', year, month, day };\n }\n\n private parseTime(): TimeAst {\n const hTok = this.eat(TokType.Number);\n const hour = toInt(hTok.value, 'hour', this.i);\n\n this.eat(TokType.Colon);\n const minTok = this.eat(TokType.Number);\n const minute = toInt(minTok.value, 'minute', this.i);\n\n let second: number | undefined;\n let fraction: string | undefined;\n\n if (this.tryEat(TokType.Colon)) {\n const sTok = this.eat(TokType.Number);\n second = toInt(sTok.value, 'second', this.i);\n\n // ISO 8601 allows both . and , as decimal separators for fractional seconds\n if (this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma)) {\n const fracTok = this.eat(TokType.Number);\n fraction = fracTok.value; // keep raw digits\n }\n }\n\n return { kind: 'Time', hour, minute, second, fraction };\n }\n\n /**\n * Parse timezone offset: Z | +HH:MM | -HH:MM\n *\n * Important semantic distinction per RFC 3339 and ISO 8601:\n *\n * - Z (Zulu time):\n * Explicitly indicates UTC. Used when the time is known to be in UTC.\n * Has no sign ambiguity - it's always UTC, never local time.\n *\n * - +00:00 (zero positive offset):\n * Indicates local time at the prime meridian (UTC+0 timezone).\n * Semantically different from Z - this is a local time that happens to match UTC.\n *\n * - -00:00 (zero negative offset):\n * Per RFC 3339 section 4.3: indicates local time with unknown UTC offset.\n * Used when the time is known to be local but the offset is not known.\n * This is semantically very different from both Z and +00:00.\n * https://www.rfc-editor.org/rfc/rfc3339.html#section-4.3\n *\n * We preserve this distinction by treating Z as UtcOffset (no sign, no components)\n * while +00:00 and -00:00 are NumericOffset (with sign and parsed components).\n */\n private parseOffsetIfPresent(): OffsetAst | undefined {\n if (this.tryEat(TokType.Z)) {\n return { kind: 'UtcOffset' };\n }\n\n // combined numeric offset token if you used combineTimezoneOffsets()\n if (this.at(CombinedTokType.TZOffset)) {\n const t = this.eat(CombinedTokType.TZOffset);\n return parseOffset(t.value, this.i);\n }\n\n return undefined;\n }\n\n // -------- Bracket group parsing --------\n // Consumes: [ ... ]\n // Returns:\n // - raw inner text (reconstructed)\n // - critical flag (true if starts with !)\n // - pairs if it looks like key=value (or flags)\n // - isLikelyTzId for things like [Asia/Singapore]\n private parseBracketGroup(): {\n raw: string;\n critical: boolean;\n pairs: Record<string, string | true>;\n isLikelyTzId: boolean;\n } {\n this.eat(TokType.LBracket);\n\n const parts: string[] = [];\n const pairs: Record<string, string | true> = {};\n\n // Check for critical flag (IXDTF: [!u-ca=gregory])\n const critical = this.tryEat(TokType.Exclamation) != null;\n if (critical) {\n parts.push('!');\n }\n\n // Gather tokens until RBracket\n while (!this.at(TokType.RBracket)) {\n if (this.eof()) {\n throw new ParseError('Unterminated bracket group', this.i);\n }\n\n const t = this.peek();\n this.i++;\n\n parts.push(String(t.value));\n }\n\n this.eat(TokType.RBracket);\n\n const raw = parts.join('');\n\n // Best-effort parse into key/value pairs:\n // Support \"key=value\" and \"flag\" and \"k=v,k2=v2\"\n // Strip leading ! if present for parsing\n const contentToParse = critical ? raw.slice(1) : raw;\n const segments = contentToParse\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n for (const seg of segments) {\n const eq = seg.indexOf('=');\n if (eq === -1) {\n pairs[seg] = true;\n } else {\n const k = seg.slice(0, eq).trim();\n const v = seg.slice(eq + 1).trim();\n if (k) {\n pairs[k] = v || true;\n }\n }\n }\n\n // Heuristic: likely TZ ID if no '=' present and contains '/' (e.g. Asia/Singapore) or starts with Etc/\n // We now preserve critical flag even for timezones, so don't exclude them\n const isLikelyTzId =\n contentToParse.indexOf('=') === -1 &&\n (contentToParse.includes('/') || contentToParse.startsWith('Etc/'));\n\n // Note: If you want STRICTLY separate tz-id brackets vs extension brackets, do it here.\n\n return { raw, critical, pairs, isLikelyTzId };\n }\n\n // -------- Duration parsing --------\n // Grammar (subset):\n // P (Y M W D)* (T (H M S)*)?\n // Examples:\n // P1Y2M\n // P3W\n // P10DT2H30M\n // PT1.5S\n private parseDuration(): DurationAst {\n // Collect raw string as we parse\n const rawParts: string[] = [];\n const annotations: AnnotationAst[] = [];\n\n const pTok = this.eat(TokType.Ident);\n if (String(pTok.value) !== 'P') {\n throw new ParseError(\"Duration must start with 'P'\", this.i);\n }\n rawParts.push('P');\n\n let inTime = false;\n\n const dur: Omit<DurationAst, 'kind' | 'raw' | 'annotations'> = {};\n\n // Parse designators until we hit bracket annotations, slash, or EOF\n while (!this.eof() && !this.at(TokType.Slash) && !this.at(TokType.LBracket)) {\n // Time marker T\n if (this.tryEat(TokType.T)) {\n rawParts.push('T');\n inTime = true;\n continue;\n }\n\n // Expect a number (possibly with fraction for seconds)\n const numTok = this.tryEat(TokType.Number);\n if (!numTok) {\n break;\n } // be permissive; let parser user decide if this is error-worthy\n rawParts.push(numTok.value);\n\n let fraction: string | undefined;\n // ISO 8601 allows both . and , as decimal separators\n const dotOrComma = this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma);\n if (dotOrComma) {\n rawParts.push(dotOrComma.value); // Preserve the actual separator (. or ,)\n const fracTok = this.eat(TokType.Number);\n rawParts.push(fracTok.value);\n fraction = fracTok.value;\n }\n\n // Unit is IDENT (Y,M,W,D,H,M,S). Note M is months if !inTime else minutes.\n const unitTok = this.eat(TokType.Ident);\n const unit = String(unitTok.value);\n rawParts.push(unit);\n\n const n = toInt(numTok.value, 'duration number', this.i);\n\n if (!inTime) {\n if (unit === 'Y') {\n dur.years = (dur.years ?? 0) + n;\n } else if (unit === 'M') {\n dur.months = (dur.months ?? 0) + n;\n } else if (unit === 'W') {\n dur.weeks = (dur.weeks ?? 0) + n;\n } else if (unit === 'D') {\n dur.days = (dur.days ?? 0) + n;\n } else {\n throw new ParseError(`Invalid duration unit '${unit}' in date part`, this.i);\n }\n } else {\n if (unit === 'H') {\n dur.hours = (dur.hours ?? 0) + n;\n } else if (unit === 'M') {\n dur.minutes = (dur.minutes ?? 0) + n;\n } else if (unit === 'S') {\n dur.seconds = (dur.seconds ?? 0) + n;\n if (fraction !== undefined && fraction.length > 0) {\n dur.secondsFraction = fraction;\n }\n } else {\n throw new ParseError(`Invalid duration unit '${unit}' in time part`, this.i);\n }\n }\n }\n\n // bracket annotations after duration\n while (this.at(TokType.LBracket)) {\n const { raw, critical, pairs } = this.parseBracketGroup();\n annotations.push({ kind: 'Annotation', raw, critical, pairs });\n }\n\n return { kind: 'Duration', ...dur, raw: rawParts.join(''), annotations };\n }\n}\n\n// ---------- Public API ----------\nexport function parseTemporal(src: string): TemporalAst {\n const raw = lexTemporal(src);\n const toks = combineTimezoneOffsets(raw);\n const p = new Parser(toks);\n return p.parseTemporal();\n}\n", "// stringify.ts\n// Convert AST back to temporal string representation\n\nimport type {\n AnnotationAst,\n DateAst,\n DateTimeAst,\n DurationAst,\n OffsetAst,\n RangeAst,\n TemporalAst,\n TimeAst,\n TimeZoneAst,\n} from './parser-types.js';\n\n/**\n * Convert a temporal AST back to its string representation.\n *\n * @param ast - The AST to stringify\n * @returns ISO 8601 / IXDTF formatted string\n *\n * @example\n * ```typescript\n * const ast = parseTemporal('2025-01-12T10:00:00+08:00');\n * const str = stringifyTemporal(ast);\n * // '2025-01-12T10:00:00+08:00'\n * ```\n */\nexport function stringifyTemporal(ast: TemporalAst): string {\n if (ast.kind === 'Range') {\n return stringifyRange(ast);\n }\n if (ast.kind === 'Duration') {\n return stringifyDuration(ast);\n }\n return stringifyDateTime(ast);\n}\n\n/**\n * Stringify a date AST to ISO 8601 format.\n *\n * @param date - The date AST\n * @returns Date string (YYYY, YYYY-MM, or YYYY-MM-DD)\n */\nexport function stringifyDate(date: DateAst): string {\n // Handle negative years (BC dates) - ISO 8601 uses negative years\n // Year 0 = 1 BC, Year -1 = 2 BC, etc.\n let yearStr: string;\n if (date.year < 0) {\n // For negative years, pad the absolute value and prepend the minus sign\n yearStr = '-' + Math.abs(date.year).toString().padStart(4, '0');\n } else {\n yearStr = date.year.toString().padStart(4, '0');\n }\n\n const parts: string[] = [yearStr];\n\n if (date.month != null) {\n parts.push(date.month.toString().padStart(2, '0'));\n\n if (date.day != null) {\n parts.push(date.day.toString().padStart(2, '0'));\n }\n }\n\n return parts.join('-');\n}\n\n/**\n * Stringify a time AST to ISO 8601 format.\n *\n * @param time - The time AST\n * @returns Time string (HH:MM, HH:MM:SS, or HH:MM:SS.fff)\n */\nexport function stringifyTime(time: TimeAst): string {\n const parts: string[] = [\n time.hour.toString().padStart(2, '0'),\n time.minute.toString().padStart(2, '0'),\n ];\n\n if (time.second != null) {\n let secondStr = time.second.toString().padStart(2, '0');\n if (time.fraction != null) {\n secondStr += `.${time.fraction}`;\n }\n parts.push(secondStr);\n }\n\n return parts.join(':');\n}\n\n/**\n * Stringify a timezone offset AST to canonical format.\n *\n * @param offset - The offset AST\n * @returns Offset string in canonical format (Z or \u00B1HH:MM)\n */\nexport function stringifyOffset(offset: OffsetAst): string {\n if (offset.kind === 'UtcOffset') {\n return 'Z';\n }\n // Return canonical format: +HH:MM or -HH:MM\n const hours = offset.hours.toString().padStart(2, '0');\n const minutes = offset.minutes.toString().padStart(2, '0');\n return `${offset.sign}${hours}:${minutes}`;\n}\n\n/**\n * Stringify a timezone AST.\n *\n * @param timeZone - The timezone AST\n * @returns Timezone string [Asia/Singapore] or [!Asia/Singapore]\n */\nexport function stringifyTimeZone(timeZone: TimeZoneAst): string {\n const id = timeZone.critical ? `!${timeZone.id}` : timeZone.id;\n return `[${id}]`;\n}\n\n/**\n * Stringify an annotation AST.\n *\n * @param annotation - The annotation AST\n * @returns Annotation string [u-ca=gregory] or [!u-ca=gregory]\n */\nexport function stringifyAnnotation(annotation: AnnotationAst): string {\n return `[${annotation.raw}]`;\n}\n\n/**\n * Stringify a datetime AST to ISO 8601 / IXDTF format.\n *\n * @param dateTime - The datetime AST\n * @returns DateTime string with optional time, offset, timezone, and annotations\n */\nexport function stringifyDateTime(dateTime: DateTimeAst): string {\n let result = stringifyDate(dateTime.date);\n\n if (dateTime.time) {\n result += `T${stringifyTime(dateTime.time)}`;\n }\n\n if (dateTime.offset) {\n result += stringifyOffset(dateTime.offset);\n }\n\n if (dateTime.timeZone) {\n result += stringifyTimeZone(dateTime.timeZone);\n }\n\n for (const annotation of dateTime.annotations) {\n result += stringifyAnnotation(annotation);\n }\n\n return result;\n}\n\n/**\n * Stringify a duration AST to ISO 8601 format.\n *\n * @param duration - The duration AST\n * @returns Duration string (P1Y2M3DT4H5M6S)\n */\nexport function stringifyDuration(duration: DurationAst): string {\n // Always reconstruct from components to ensure normalization\n let result = 'P';\n\n // Date part - include component if defined (even if zero)\n if (duration.years != null) {\n result += `${duration.years}Y`;\n }\n if (duration.months != null) {\n result += `${duration.months}M`;\n }\n if (duration.weeks != null) {\n result += `${duration.weeks}W`;\n }\n if (duration.days != null) {\n result += `${duration.days}D`;\n }\n\n // Time part - add T separator if any time component is defined\n const hasTimePart =\n duration.hours != null || duration.minutes != null || duration.seconds != null;\n if (hasTimePart) {\n result += 'T';\n if (duration.hours != null) {\n result += `${duration.hours}H`;\n }\n if (duration.minutes != null) {\n result += `${duration.minutes}M`;\n }\n if (duration.seconds != null) {\n result += `${duration.seconds}`;\n if (duration.secondsFraction != null && duration.secondsFraction.length > 0) {\n result += `.${duration.secondsFraction}`;\n }\n result += 'S';\n }\n }\n\n // Annotations\n for (const annotation of duration.annotations) {\n result += stringifyAnnotation(annotation);\n }\n\n return result;\n}\n\n/**\n * Stringify a range AST to ISO 8601 format.\n *\n * @param range - The range AST\n * @returns Range string (start/end, /end, or start/)\n */\nexport function stringifyRange(range: RangeAst): string {\n const start = range.start\n ? range.start.kind === 'Duration'\n ? stringifyDuration(range.start)\n : stringifyDateTime(range.start)\n : '';\n\n const end = range.end\n ? range.end.kind === 'Duration'\n ? stringifyDuration(range.end)\n : stringifyDateTime(range.end)\n : '';\n\n return `${start}/${end}`;\n}\n"], | ||
| "mappings": "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", | ||
| "sources": ["../../src/lexer-types.ts", "../../src/combineTimezoneOffsets.ts", "../../src/errors.ts", "../../src/helpers/isAlpha.ts", "../../src/helpers/isDigit.ts", "../../src/helpers/isBracketWordChar.ts", "../../src/lexer.ts", "../../src/helpers/toInt.ts", "../../src/parseOffset.ts", "../../src/parser.ts", "../../src/parseTimeString.ts", "../../src/stringify.ts"], | ||
| "sourcesContent": ["// lexer-types.ts\n// Token types and interfaces for the lexer\n\nexport enum TokType {\n // Numbers: sequences of digits (0-9)\n // Examples: \"2025\", \"01\", \"12\", \"59\"\n Number = 'Number',\n\n // Ident (Identifier): sequences of alphabetic characters (A-Z, a-z)\n // This is the generic \"text\" token for letter-based designators.\n // Examples: \"P\" (duration), \"Y\" (year), \"M\" (month), \"gregory\" (calendar)\n // The lexer keeps it simple; the parser interprets the meaning.\n Ident = 'Ident',\n\n // Single-character punctuation tokens\n Dash = 'Dash', // -\n Colon = 'Colon', // :\n Dot = 'Dot', // .\n Plus = 'Plus', // +\n Slash = 'Slash', // /\n Comma = 'Comma', // ,\n Equals = 'Equals', // =\n Underscore = 'Underscore', // _\n Exclamation = 'Exclamation', // ! (critical flag in IXDTF)\n\n // Special letter tokens with dedicated meaning in ISO 8601\n T = 'T', // Date/time separator: 2025-01-01T10:00:00\n Z = 'Z', // UTC timezone marker: 2025-01-01T10:00:00Z\n\n // Bracket tokens for IXDTF annotations/extensions\n LBracket = 'LBracket', // [\n RBracket = 'RBracket', // ]\n\n // BracketText: complex text inside brackets that contains non-letter chars\n // Example: in [u-ca=gregory], \"u-ca\" is BracketText (has dash), \"gregory\" is Ident (letters only)\n // Example: in [Asia/Singapore], \"Asia/Singapore\" is BracketText (has slash)\n BracketText = 'BracketText',\n\n EOF = 'EOF',\n}\n\nexport type Token = {\n type: TokType;\n value: string;\n start: number; // inclusive\n end: number; // exclusive\n};\n\n// Combined token types for timezone offsets\nexport enum CombinedTokType {\n TZOffset = 'TZOffset', // +08:00, -0530, +09\n}\n\nexport type CombinedToken = {\n type: CombinedTokType;\n value: string;\n tokens: Token[];\n start: number;\n end: number;\n};\n\nexport type AnyToken = Token | CombinedToken;\n", "// combineTimezoneOffsets.ts\n// Combines numeric timezone offset tokens into single tokens for easier parsing\n\nimport { type AnyToken, CombinedTokType, type Token, TokType } from './lexer-types.js';\n\n/**\n * Combine numeric timezone offsets into single tokens for easier parsing.\n * This is an optional post-processing step for the lexer output.\n *\n * Combines patterns like:\n * - (+|-) Number Colon Number => +08:00, -05:30\n * - (+|-) Number (2 or 4 digits) => +09, -0530\n *\n * Only combines when context suggests a timezone offset (after time components),\n * NOT after:\n * - Z (mutually exclusive per ISO 8601)\n * - brackets (offset comes BEFORE brackets per RFC 9557/IXDTF)\n * - standalone dates (e.g., 2025-01 where - is a date separator)\n *\n * @param tokens - The raw token array from lexTemporal\n * @returns Array of tokens (either original Token or combined CombinedToken with TZOffset type where applicable)\n *\n * @example\n * ```typescript\n * const raw = lexTemporal('10:00:00+08:00');\n * const combined = combineTimezoneOffsets(raw);\n * // Result includes: { type: 'TZOffset', value: '+08:00' }\n * ```\n */\nexport function combineTimezoneOffsets(tokens: Token[]): AnyToken[] {\n const out: AnyToken[] = [];\n let i = 0;\n\n const peek = (k: number) => tokens[i + k];\n\n // Helper to check if previous tokens suggest this could be a timezone offset\n // Timezone offsets come after time components (HH:MM:SS)\n // NOT after just a date like 2025-01\n // NOT after Z (Z and numeric offsets are mutually exclusive per ISO 8601)\n // NOT after brackets (per RFC 9557/IXDTF: offset comes BEFORE brackets)\n const couldBeTzOffset = () => {\n if (out.length < 1) {\n return false;\n }\n const prev: AnyToken | undefined = out[out.length - 1];\n if (prev == null) {\n return false;\n }\n\n // After Z - NOT valid (Z and numeric offsets are mutually exclusive)\n // Per ISO 8601: use either Z OR +HH:MM, never both\n if (prev.type === TokType.Z) {\n return false;\n }\n\n // After bracket - NOT valid (offset must come BEFORE brackets per RFC 9557)\n // Valid: Time+08:00[Asia/Tokyo]\n // Invalid: [Asia/Tokyo]+08:00\n if (prev.type === TokType.RBracket) {\n return false;\n }\n\n // After a number, check context\n if (prev.type === TokType.Number) {\n if (out.length < 2) {\n return false;\n }\n const prevPrev = out[out.length - 2];\n\n // After a number with colon before it (time component)\n // e.g., ...10:00:00+08:00 - the number before + has a colon before it\n if (prevPrev?.type === TokType.Colon) {\n return true;\n }\n\n // After a number with dot before it (milliseconds)\n // e.g., ...10:00:00.999+08:00 - the number before + has a dot before it\n if (prevPrev?.type === TokType.Dot) {\n return true;\n }\n }\n\n return false;\n };\n\n while (i < tokens.length) {\n const t = tokens[i]!;\n const isSign = t.type === TokType.Plus || t.type === TokType.Dash;\n\n // Only try to combine if context suggests timezone offset\n if (isSign && couldBeTzOffset()) {\n // (+|-) Number Colon Number => +08:00\n if (\n peek(1)?.type === TokType.Number &&\n peek(2)?.type === TokType.Colon &&\n peek(3)?.type === TokType.Number\n ) {\n const originalTokens = [t, peek(1)!, peek(2)!, peek(3)!];\n const start = t.start;\n const end = peek(3)!.end;\n const value = `${t.value}${peek(1)!.value}:${peek(3)!.value}`;\n out.push({ type: CombinedTokType.TZOffset, value, tokens: originalTokens, start, end });\n i += 4;\n continue;\n }\n\n // (+|-) Number where Number is 2 or 4 digits => +09 or -0530\n if (peek(1)?.type === TokType.Number) {\n const num = peek(1)!.value;\n if (num.length === 2 || num.length === 4) {\n const originalTokens = [t, peek(1)!];\n const start = t.start;\n const end = peek(1)!.end;\n const value = `${t.value}${num}`;\n out.push({ type: CombinedTokType.TZOffset, value, tokens: originalTokens, start, end });\n i += 2;\n continue;\n }\n }\n }\n\n out.push(t);\n i++;\n }\n\n return out;\n}\n", "// errors.ts\n// Error classes for lexer and parser\n\nexport class LexError extends Error {\n public readonly position: number;\n constructor(message: string, position: number) {\n super(`${message} at position ${position}`);\n this.name = 'LexError';\n this.position = position;\n }\n}\n\nexport class ParseError extends Error {\n public readonly position: number;\n constructor(message: string, position: number) {\n super(`${message} at token index ${position}`);\n this.name = 'ParseError';\n this.position = position;\n }\n}\n", "/**\n * Check if character is alphabetic (A-Z or a-z)\n * This defines what can be part of an IDENT token in default mode\n * @param c - the character to check\n * @returns true if character is alphabetic, false otherwise\n */\nexport function isAlpha(c: string): boolean {\n return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');\n}\n", "/**\n * Check if character is a digit (0-9)\n * @param c - the character to check\n * @returns true if character is a digit, false otherwise\n */\nexport function isDigit(c: string) {\n return c >= '0' && c <= '9';\n}\n", "import { isAlpha } from './isAlpha.js';\nimport { isDigit } from './isDigit.js';\n\n/**\n * Check if character is valid inside bracket annotations\n * In default mode, IDENT is letters only (keeps date/time parsing clean).\n * In bracket mode, we allow broader \"words\" like:\n * - u-ca (has dash)\n * - Asia/Singapore (has slash)\n * - Etc/GMT+8 (has plus)\n * - iso8601 (has digits)\n * @param c - the character to check\n * @returns true if character is valid in bracket context, false otherwise\n */\nexport function isBracketWordChar(c: string): boolean {\n return (\n isAlpha(c) || isDigit(c) || c === '-' || c === '/' || c === '+' || c === '.' // allow dots in some TZ-ish values\n );\n}\n", "// lexer.ts\n// Temporal (IXDTF / ISO-8601-ish) lexer with support for *extensions/annotations* like calendar.\n//\n// Supports:\n// - Basic date/time tokens: numbers, identifiers, T, Z, punctuation\n// - Ranges: '/'\n// - Durations: 'P' (as IDENT; parser enforces rules)\n// - Time zone forms:\n// - 'Z'\n// - numeric offsets: +08:00 / -0530 / +09 (optionally combined via helper)\n// - bracketed IANA TZ: [Asia/Singapore]\n// - IXDTF-style annotations/extensions in brackets, e.g.:\n// - [u-ca=gregory] (calendar)\n// - [u-tz=Asia/Singapore] (timeZone)\n// - multiple bracket groups: 2025-01-01T00:00Z[u-ca=iso8601][u-tz=UTC]\n//\n// The lexer is permissive; your parser should validate semantics.\n\nimport { LexError } from './errors.js';\nimport { isAlpha } from './helpers/isAlpha.js';\nimport { isBracketWordChar } from './helpers/isBracketWordChar.js';\nimport { isDigit } from './helpers/isDigit.js';\nimport type { Token } from './lexer-types.js';\nimport { TokType } from './lexer-types.js';\n\nexport function lexTemporal(src: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n const emit = (type: TokType, start: number, end: number, value?: string) => {\n tokens.push({ type, value: value ?? src.slice(start, end), start, end });\n };\n\n // Lex a number: consume consecutive digits\n // Examples: \"2025\", \"01\", \"59\"\n const lexNumber = () => {\n const start = i;\n i++;\n while (i < src.length && isDigit(src[i]!)) {\n i++;\n }\n emit(TokType.Number, start, i);\n };\n\n // Lex an identifier: consume consecutive alphabetic characters\n // This is where IDENT tokens are created!\n //\n // Examples of what becomes IDENT:\n // - Duration designators: P, Y, M, W, D, H, S\n // - Calendar names: gregory, hebrew, iso8601\n // - Any letter sequence: P, YMD, UTC, America\n //\n // Special cases:\n // - \"T\" gets its own token type (datetime separator)\n // - \"Z\" gets its own token type (UTC marker)\n // - Everything else becomes Ident\n //\n // IMPORTANT: We only consume a single letter at a time to allow T and Z\n // to be recognized individually (e.g., \"DT\" should be \"D\" then \"T\", not \"DT\")\n const lexIdent = () => {\n const start = i;\n const ch = src[i]!;\n i++;\n\n // Special handling for T and Z - emit them as special tokens\n if (ch === 'T') {\n emit(TokType.T, start, i, 'T');\n return;\n }\n if (ch === 'Z') {\n emit(TokType.Z, start, i, 'Z');\n return;\n }\n\n // For other letters, consume consecutive letters that are NOT T or Z\n // This allows multi-letter identifiers like \"gregory\" but keeps T and Z separate\n while (i < src.length && isAlpha(src[i]!) && src[i] !== 'T' && src[i] !== 'Z') {\n i++;\n }\n\n emit(TokType.Ident, start, i); // Generic identifier\n };\n\n const lexBracketContent = () => {\n // We are positioned after '['. Emit content tokens until ']'.\n while (i < src.length) {\n const ch = src[i]!;\n\n if (ch === ']') {\n return;\n }\n\n // Skip whitespace inside brackets (rare but harmless)\n if (/\\s/.test(ch)) {\n i++;\n continue;\n }\n\n // Single-char tokens inside brackets\n if (ch === '=') {\n emit(TokType.Equals, i, i + 1);\n i++;\n continue;\n }\n if (ch === '_') {\n emit(TokType.Underscore, i, i + 1);\n i++;\n continue;\n }\n if (ch === ':') {\n emit(TokType.Colon, i, i + 1);\n i++;\n continue;\n }\n if (ch === '-') {\n emit(TokType.Dash, i, i + 1);\n i++;\n continue;\n }\n if (ch === '+') {\n emit(TokType.Plus, i, i + 1);\n i++;\n continue;\n }\n if (ch === '/') {\n emit(TokType.Slash, i, i + 1);\n i++;\n continue;\n }\n if (ch === '.') {\n emit(TokType.Dot, i, i + 1);\n i++;\n continue;\n }\n if (ch === ',') {\n emit(TokType.Comma, i, i + 1);\n i++;\n continue;\n }\n if (ch === '!') {\n emit(TokType.Exclamation, i, i + 1);\n i++;\n continue;\n }\n\n // Words inside brackets (u-ca, gregory, Asia/Singapore, iso8601, Etc/GMT+8, etc.)\n if (isBracketWordChar(ch)) {\n const start = i;\n i++;\n while (i < src.length && isBracketWordChar(src[i]!)) {\n i++;\n }\n const text = src.slice(start, i);\n\n // Decision: Ident vs BracketText\n // - If purely letters (A-Z, a-z): emit as Ident\n // Examples: \"gregory\", \"hebrew\", \"u\", \"ca\"\n // - If contains other chars (digits, -, /, +, .): emit as BracketText\n // Examples: \"u-ca\", \"Asia/Singapore\", \"iso8601\", \"Etc/GMT+8\"\n if (/^[A-Za-z]+$/.test(text)) {\n emit(TokType.Ident, start, i, text);\n } else {\n emit(TokType.BracketText, start, i, text);\n }\n\n continue;\n }\n\n throw new LexError(`Unexpected character ${JSON.stringify(ch)} inside brackets`, i);\n }\n\n throw new LexError(\"Unterminated bracket annotation, expected ']'\", src.length);\n };\n\n while (i < src.length) {\n const ch = src[i]!;\n\n // Skip whitespace\n if (/\\s/.test(ch)) {\n i++;\n continue;\n }\n\n // Bracketed extensions/annotations/timezone IDs\n if (ch === '[') {\n emit(TokType.LBracket, i, i + 1);\n i++;\n\n lexBracketContent();\n\n if (i >= src.length || src[i] !== ']') {\n throw new LexError(\"Unterminated bracket annotation, expected ']'\", i);\n }\n emit(TokType.RBracket, i, i + 1);\n i++;\n continue;\n }\n\n // Numbers: sequences of digits\n if (isDigit(ch)) {\n lexNumber();\n continue;\n }\n\n // Letters: this is where IDENT tokens are created!\n // Any alphabetic character triggers identifier lexing.\n // Examples: P (duration), Y (year), M (month), gregory, UTC\n if (isAlpha(ch)) {\n lexIdent();\n continue;\n }\n\n // Punctuation (default mode)\n switch (ch) {\n case '-':\n emit(TokType.Dash, i, i + 1);\n i++;\n continue;\n case ':':\n emit(TokType.Colon, i, i + 1);\n i++;\n continue;\n case '.':\n emit(TokType.Dot, i, i + 1);\n i++;\n continue;\n case '+':\n emit(TokType.Plus, i, i + 1);\n i++;\n continue;\n case '/':\n emit(TokType.Slash, i, i + 1);\n i++;\n continue;\n case ',':\n emit(TokType.Comma, i, i + 1);\n i++;\n continue;\n case '=':\n emit(TokType.Equals, i, i + 1);\n i++;\n continue;\n case '_':\n emit(TokType.Underscore, i, i + 1);\n i++;\n continue;\n default:\n throw new LexError(`Unexpected character ${JSON.stringify(ch)}`, i);\n }\n }\n\n emit(TokType.EOF, src.length, src.length, '');\n return tokens;\n}\n", "import { ParseError } from '../errors.js';\n\n/**\n * Convert a string to an integer, throwing a ParseError if invalid\n * @param s - the string to convert\n * @param label - a descriptive label for error messages (e.g., \"year\", \"month\")\n * @param position - the position in the token stream for error reporting\n * @returns the parsed integer value\n * @throws {ParseError} if the string is not a valid integer\n */\nexport function toInt(s: string, label: string, position: number): number {\n const n = Number(s);\n if (!Number.isFinite(n) || !Number.isInteger(n)) {\n throw new ParseError(`Invalid integer for ${label}: ${JSON.stringify(s)}`, position);\n }\n return n;\n}\n", "// parseOffset.ts\n// Parse timezone offset strings into structured components\n\nimport { ParseError } from './errors.js';\nimport { toInt } from './helpers/toInt.js';\nimport type { OffsetAst } from './parser-types.js';\n\n// Timezone offset validation constants\n// Per ISO 8601 and real-world usage:\n// - Westernmost: UTC-12:00 (Baker Island, Howland Island)\n// - Easternmost: UTC+14:00 (Line Islands, Kiritimati)\nconst MAX_OFFSET_HOURS = 14;\nconst MAX_MINUTES = 59;\n\n/**\n * Parse a timezone offset string into structured components.\n *\n * Supports three formats:\n * - Standard: +08:00, -05:30\n * - Compact: +0530, -0800\n * - Short: +09, -05\n *\n * Valid timezone offset ranges (per ISO 8601):\n * - Hours: 0-14 (UTC-12:00 to UTC+14:00)\n * - Minutes: 0-59\n *\n * Note: This function only parses numeric offsets (e.g., +08:00).\n * For UTC, use 'Z' (UtcOffset kind) which is semantically different from +00:00.\n * Per RFC 3339 section 4.3, -00:00 indicates \"local time with unknown offset\".\n *\n * @param offsetString - The offset string to parse (e.g., '+08:00', '-0530', '+09')\n * @param position - Position in token stream for error reporting (optional)\n * @returns Parsed offset with sign, hours, and minutes\n * @throws {ParseError} If the offset format is invalid or out of range\n *\n * @example\n * ```typescript\n * parseOffset('+08:00') // { kind: 'NumericOffset', sign: '+', hours: 8, minutes: 0, raw: '+08:00' }\n * parseOffset('-05:30') // { kind: 'NumericOffset', sign: '-', hours: 5, minutes: 30, raw: '-05:30' }\n * parseOffset('+0530') // { kind: 'NumericOffset', sign: '+', hours: 5, minutes: 30, raw: '+0530' }\n * parseOffset('+09') // { kind: 'NumericOffset', sign: '+', hours: 9, minutes: 0, raw: '+09' }\n * parseOffset('+14:00') // { kind: 'NumericOffset', sign: '+', hours: 14, minutes: 0, raw: '+14:00' }\n * ```\n */\nexport function parseOffset(offsetString: string, position = 0): OffsetAst {\n // Validate input is not empty\n if (offsetString.length === 0) {\n throw new ParseError('Offset string cannot be empty', position);\n }\n\n const raw = offsetString;\n\n // Validate first character is sign and assert type\n const sign = raw[0];\n if (sign !== '+' && sign !== '-') {\n throw new ParseError(`Offset must start with + or -, got ${JSON.stringify(sign)}`, position);\n }\n // Type assertion: TypeScript now knows sign is '+' | '-'\n const validSign: '+' | '-' = sign;\n\n const rest = raw.slice(1);\n\n // Validate rest is not empty\n if (rest.length === 0) {\n throw new ParseError(`Offset sign must be followed by digits`, position);\n }\n\n let hours: number;\n let minutes: number;\n\n if (rest.includes(':')) {\n // Format: +08:00 or -05:30\n const parts = rest.split(':');\n if (parts.length !== 2) {\n throw new ParseError(`Invalid offset format: ${raw} (expected HH:MM)`, position);\n }\n // After length check, we know parts[0] and parts[1] exist\n const [h, m] = parts;\n hours = toInt(h!, 'offset hours', position);\n minutes = toInt(m!, 'offset minutes', position);\n } else if (rest.length === 4) {\n // Format: +0530 or -0800\n hours = toInt(rest.slice(0, 2), 'offset hours', position);\n minutes = toInt(rest.slice(2, 4), 'offset minutes', position);\n } else if (rest.length === 2) {\n // Format: +08 or -05\n hours = toInt(rest, 'offset hours', position);\n minutes = 0;\n } else {\n throw new ParseError(\n `Invalid offset format: ${raw} (expected +HH:MM, +HHMM, or +HH)`,\n position,\n );\n }\n\n // Validate ranges\n if (hours < 0 || hours > MAX_OFFSET_HOURS) {\n throw new ParseError(`Offset hours must be 0-${MAX_OFFSET_HOURS}, got ${hours}`, position);\n }\n if (minutes < 0 || minutes > MAX_MINUTES) {\n throw new ParseError(`Offset minutes must be 0-${MAX_MINUTES}, got ${minutes}`, position);\n }\n\n return { kind: 'NumericOffset', sign: validSign, hours, minutes, raw };\n}\n", "// parser.ts\n// A practical recursive-descent parser that works with the lexer we wrote.\n// It builds an AST for a useful Temporal subset:\n// - date/time (with optional fractional seconds)\n// - timezone: Z | numeric offset (+08:00, -0530, +09) | bracket TZ id [Asia/Singapore]\n// - duration: P... with optional T part\n// - range: <expr> \"/\" <expr> where each side can be dateTime/date/duration, and either side may be empty (open range)\n// - bracket extensions like [u-ca=gregory], [u-tz=Asia/Singapore], etc. (stored as key/value pairs)\n\nimport { combineTimezoneOffsets } from './combineTimezoneOffsets.js';\nimport { ParseError } from './errors.js';\nimport { toInt } from './helpers/toInt.js';\nimport { lexTemporal } from './lexer.js';\nimport { type AnyToken, CombinedTokType, TokType } from './lexer-types.js';\nimport { parseOffset } from './parseOffset.js';\nimport type {\n AnnotationAst,\n DateAst,\n DateTimeAst,\n DurationAst,\n OffsetAst,\n TemporalAst,\n TimeAst,\n TimeZoneAst,\n ValueAst,\n} from './parser-types.js';\n\n// ---------- Parser ----------\n\nclass Parser {\n private i = 0;\n constructor(private toks: AnyToken[]) {}\n\n private peek(k = 0): AnyToken {\n return this.toks[Math.min(this.i + k, this.toks.length - 1)]!;\n }\n private at(type: TokType | CombinedTokType): boolean {\n return this.peek().type === type;\n }\n private eat(type: TokType | CombinedTokType): AnyToken {\n const t = this.peek();\n if (t.type !== type) {\n throw new ParseError(`Expected ${type} but got ${t.type}`, this.i);\n }\n this.i++;\n return t;\n }\n private tryEat(type: TokType | CombinedTokType): AnyToken | null {\n if (this.at(type)) {\n return this.eat(type);\n }\n return null;\n }\n private eof(): boolean {\n return this.at(TokType.EOF);\n }\n\n // Top-level: parse a range if we see '/', otherwise a single value.\n parseTemporal(): TemporalAst {\n // Open-start range: \"/...\"\n if (this.tryEat(TokType.Slash)) {\n const end = this.parseValueOrNull(); // allow empty end too\n this.eat(TokType.EOF);\n return { kind: 'Range', start: null, end };\n }\n\n const first = this.parseValueOrNull();\n // If first is null, only valid if it's \"/\" (handled above). So error:\n if (!first) {\n throw new ParseError('Expected a value', this.i);\n }\n\n if (this.tryEat(TokType.Slash)) {\n const second = this.parseValueOrNull(); // open-ended allowed\n this.eat(TokType.EOF);\n return { kind: 'Range', start: first, end: second };\n }\n\n this.eat(TokType.EOF);\n return first;\n }\n\n private parseValueOrNull(): ValueAst | null {\n // Empty side for open range:\n if (this.at(TokType.EOF) || this.at(TokType.Slash) || this.at(TokType.RBracket)) {\n return null;\n }\n\n // Duration starts with IDENT \"P\" (as produced by lexer)\n if (this.at(TokType.Ident) && String(this.peek().value) === 'P') {\n return this.parseDuration();\n }\n\n // Otherwise parse a date/datetime (common case)\n return this.parseDateTime();\n }\n\n // -------- DateTime parsing --------\n // Supports:\n // - YYYY\n // - YYYY-MM\n // - YYYY-MM-DD\n // - + time part with T: YYYY-MM-DDTHH:MM[:SS[.fff]]\n // - offset: Z | +08:00 | -0530\n // - timezone: [Asia/Singapore] (first bracketed IANA timezone)\n // - annotations: [u-ca=...][u-tz=...] (other bracketed content)\n private parseDateTime(): DateTimeAst {\n const date = this.parseDate();\n\n let time: DateTimeAst['time'] | undefined;\n\n // time separator is TokType.T (from lexer special-casing \"T\")\n if (this.tryEat(TokType.T)) {\n time = this.parseTime();\n }\n\n // Parse numeric offset (Z or +HH:MM)\n const offset = this.parseOffsetIfPresent();\n\n // Parse bracket groups: first IANA timezone becomes timeZone, rest are annotations\n const annotations: AnnotationAst[] = [];\n let timeZone: TimeZoneAst | undefined;\n\n while (this.at(TokType.LBracket)) {\n const { raw, critical, isLikelyTzId, pairs } = this.parseBracketGroup();\n\n // First timezone-like bracket becomes the timeZone field\n // Note: Even though IXDTF doesn't typically use critical flags for timezones,\n // we preserve it in case the input has it\n if (timeZone === undefined && isLikelyTzId) {\n // Strip leading ! from id (it's stored in the critical flag)\n const id = critical ? raw.slice(1) : raw;\n timeZone = { kind: 'IanaTimeZone', id, critical };\n } else {\n annotations.push({ kind: 'Annotation', raw, critical, pairs });\n }\n }\n\n return { kind: 'DateTime', date, time, offset, timeZone, annotations };\n }\n\n private parseDate(): DateAst {\n // Check for optional leading dash (negative year / BC date)\n // ISO 8601: Year 0 = 1 BC, Year -1 = 2 BC, etc.\n const isNegative = this.tryEat(TokType.Dash);\n\n const yTok = this.eat(TokType.Number);\n let year = toInt(yTok.value, 'year', this.i);\n if (isNegative) {\n year = -year;\n }\n\n // Optional -MM\n if (!this.tryEat(TokType.Dash)) {\n return { kind: 'Date', year };\n }\n const mTok = this.eat(TokType.Number);\n const month = toInt(mTok.value, 'month', this.i);\n\n // Optional -DD\n if (!this.tryEat(TokType.Dash)) {\n return { kind: 'Date', year, month };\n }\n const dTok = this.eat(TokType.Number);\n const day = toInt(dTok.value, 'day', this.i);\n\n return { kind: 'Date', year, month, day };\n }\n\n private parseTime(): TimeAst {\n const hTok = this.eat(TokType.Number);\n const hour = toInt(hTok.value, 'hour', this.i);\n\n this.eat(TokType.Colon);\n const minTok = this.eat(TokType.Number);\n const minute = toInt(minTok.value, 'minute', this.i);\n\n let second: number | undefined;\n let fraction: string | undefined;\n\n if (this.tryEat(TokType.Colon)) {\n const sTok = this.eat(TokType.Number);\n second = toInt(sTok.value, 'second', this.i);\n\n // ISO 8601 allows both . and , as decimal separators for fractional seconds\n if (this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma)) {\n const fracTok = this.eat(TokType.Number);\n fraction = fracTok.value; // keep raw digits\n }\n }\n\n return { kind: 'Time', hour, minute, second, fraction };\n }\n\n /**\n * Parse timezone offset: Z | +HH:MM | -HH:MM\n *\n * Important semantic distinction per RFC 3339 and ISO 8601:\n *\n * - Z (Zulu time):\n * Explicitly indicates UTC. Used when the time is known to be in UTC.\n * Has no sign ambiguity - it's always UTC, never local time.\n *\n * - +00:00 (zero positive offset):\n * Indicates local time at the prime meridian (UTC+0 timezone).\n * Semantically different from Z - this is a local time that happens to match UTC.\n *\n * - -00:00 (zero negative offset):\n * Per RFC 3339 section 4.3: indicates local time with unknown UTC offset.\n * Used when the time is known to be local but the offset is not known.\n * This is semantically very different from both Z and +00:00.\n * https://www.rfc-editor.org/rfc/rfc3339.html#section-4.3\n *\n * We preserve this distinction by treating Z as UtcOffset (no sign, no components)\n * while +00:00 and -00:00 are NumericOffset (with sign and parsed components).\n */\n private parseOffsetIfPresent(): OffsetAst | undefined {\n if (this.tryEat(TokType.Z)) {\n return { kind: 'UtcOffset' };\n }\n\n // combined numeric offset token if you used combineTimezoneOffsets()\n if (this.at(CombinedTokType.TZOffset)) {\n const t = this.eat(CombinedTokType.TZOffset);\n return parseOffset(t.value, this.i);\n }\n\n return undefined;\n }\n\n // -------- Bracket group parsing --------\n // Consumes: [ ... ]\n // Returns:\n // - raw inner text (reconstructed)\n // - critical flag (true if starts with !)\n // - pairs if it looks like key=value (or flags)\n // - isLikelyTzId for things like [Asia/Singapore]\n private parseBracketGroup(): {\n raw: string;\n critical: boolean;\n pairs: Record<string, string | true>;\n isLikelyTzId: boolean;\n } {\n this.eat(TokType.LBracket);\n\n const parts: string[] = [];\n const pairs: Record<string, string | true> = {};\n\n // Check for critical flag (IXDTF: [!u-ca=gregory])\n const critical = this.tryEat(TokType.Exclamation) != null;\n if (critical) {\n parts.push('!');\n }\n\n // Gather tokens until RBracket\n while (!this.at(TokType.RBracket)) {\n if (this.eof()) {\n throw new ParseError('Unterminated bracket group', this.i);\n }\n\n const t = this.peek();\n this.i++;\n\n parts.push(String(t.value));\n }\n\n this.eat(TokType.RBracket);\n\n const raw = parts.join('');\n\n // Best-effort parse into key/value pairs:\n // Support \"key=value\" and \"flag\" and \"k=v,k2=v2\"\n // Strip leading ! if present for parsing\n const contentToParse = critical ? raw.slice(1) : raw;\n const segments = contentToParse\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n for (const seg of segments) {\n const eq = seg.indexOf('=');\n if (eq === -1) {\n pairs[seg] = true;\n } else {\n const k = seg.slice(0, eq).trim();\n const v = seg.slice(eq + 1).trim();\n if (k) {\n pairs[k] = v || true;\n }\n }\n }\n\n // Heuristic: likely TZ ID if no '=' present and contains '/' (e.g. Asia/Singapore) or starts with Etc/\n // We now preserve critical flag even for timezones, so don't exclude them\n const isLikelyTzId =\n contentToParse.indexOf('=') === -1 &&\n (contentToParse.includes('/') || contentToParse.startsWith('Etc/'));\n\n // Note: If you want STRICTLY separate tz-id brackets vs extension brackets, do it here.\n\n return { raw, critical, pairs, isLikelyTzId };\n }\n\n // -------- Duration parsing --------\n // Grammar (subset):\n // P (Y M W D)* (T (H M S)*)?\n // Examples:\n // P1Y2M\n // P3W\n // P10DT2H30M\n // PT1.5S\n private parseDuration(): DurationAst {\n // Collect raw string as we parse\n const rawParts: string[] = [];\n const annotations: AnnotationAst[] = [];\n\n const pTok = this.eat(TokType.Ident);\n if (String(pTok.value) !== 'P') {\n throw new ParseError(\"Duration must start with 'P'\", this.i);\n }\n rawParts.push('P');\n\n let inTime = false;\n\n const dur: Omit<DurationAst, 'kind' | 'raw' | 'annotations'> = {};\n\n // Parse designators until we hit bracket annotations, slash, or EOF\n while (!this.eof() && !this.at(TokType.Slash) && !this.at(TokType.LBracket)) {\n // Time marker T\n if (this.tryEat(TokType.T)) {\n rawParts.push('T');\n inTime = true;\n continue;\n }\n\n // Expect a number (possibly with fraction for seconds)\n const numTok = this.tryEat(TokType.Number);\n if (!numTok) {\n break;\n } // be permissive; let parser user decide if this is error-worthy\n rawParts.push(numTok.value);\n\n let fraction: string | undefined;\n // ISO 8601 allows both . and , as decimal separators\n const dotOrComma = this.tryEat(TokType.Dot) || this.tryEat(TokType.Comma);\n if (dotOrComma) {\n rawParts.push(dotOrComma.value); // Preserve the actual separator (. or ,)\n const fracTok = this.eat(TokType.Number);\n rawParts.push(fracTok.value);\n fraction = fracTok.value;\n }\n\n // Unit is IDENT (Y,M,W,D,H,M,S). Note M is months if !inTime else minutes.\n const unitTok = this.eat(TokType.Ident);\n const unit = String(unitTok.value);\n rawParts.push(unit);\n\n const n = toInt(numTok.value, 'duration number', this.i);\n\n if (!inTime) {\n if (unit === 'Y') {\n dur.years = (dur.years ?? 0) + n;\n } else if (unit === 'M') {\n dur.months = (dur.months ?? 0) + n;\n } else if (unit === 'W') {\n dur.weeks = (dur.weeks ?? 0) + n;\n } else if (unit === 'D') {\n dur.days = (dur.days ?? 0) + n;\n } else {\n throw new ParseError(`Invalid duration unit '${unit}' in date part`, this.i);\n }\n } else {\n if (unit === 'H') {\n dur.hours = (dur.hours ?? 0) + n;\n } else if (unit === 'M') {\n dur.minutes = (dur.minutes ?? 0) + n;\n } else if (unit === 'S') {\n dur.seconds = (dur.seconds ?? 0) + n;\n if (fraction !== undefined && fraction.length > 0) {\n dur.secondsFraction = fraction;\n }\n } else {\n throw new ParseError(`Invalid duration unit '${unit}' in time part`, this.i);\n }\n }\n }\n\n // bracket annotations after duration\n while (this.at(TokType.LBracket)) {\n const { raw, critical, pairs } = this.parseBracketGroup();\n annotations.push({ kind: 'Annotation', raw, critical, pairs });\n }\n\n return { kind: 'Duration', ...dur, raw: rawParts.join(''), annotations };\n }\n}\n\n// ---------- Public API ----------\nexport function parseTemporal(src: string): TemporalAst {\n const raw = lexTemporal(src);\n const toks = combineTimezoneOffsets(raw);\n const p = new Parser(toks);\n return p.parseTemporal();\n}\n", "// parseTimeString.ts\n// Standalone time parser for hh:mm format with AM/PM support\n// Supports both 12-hour (with AM/PM) and 24-hour international formats\n\nimport { ParseError } from './errors.js';\nimport { toInt } from './helpers/toInt.js';\nimport { lexTemporal } from './lexer.js';\nimport type { AnyToken } from './lexer-types.js';\nimport { TokType } from './lexer-types.js';\nimport type { TimeAst } from './parser-types.js';\n\n/**\n * Parse a time-of-day string from multiple common formats.\n * Supports: locale time (\"9:07 AM\"), 24h (\"09:00\", \"14:30\"), short 24h (\"9:00\"),\n * and 12h with lowercase period (\"9:07 am\").\n *\n * TAA (LLM) often generates time in 24h format (\"09:00\") instead of the locale\n * format (\"9:00 AM\") that Luxon's 't' token expects.\n *\n * Supported formats:\n * - Locale time (12-hour with AM/PM): \"9:07 AM\", \"2:30 PM\", \"02:30PM\", \"2:30 p.m.\"\n * - 24-hour format: \"09:00\", \"14:30\", \"23:59\"\n * - Short 24-hour (single digit hour): \"9:00\", \"9:30\"\n * - Lowercase am/pm: \"9:07 am\", \"2:30 pm\"\n * - With optional seconds: \"2:30:45 PM\", \"14:30:45\"\n * - With optional fractional seconds: \"2:30:45.123 PM\", \"14:30:45.123\"\n *\n * Returns a TimeAst object compatible with Temporal.PlainTime.from()\n *\n * @param input - The time string to parse\n * @returns TimeAst object with hour, minute, and optional second/fraction\n * @throws ParseError if the input is invalid\n */\nexport function parseTimeString(input: string): TimeAst {\n const trimmed = input.trim();\n if (!trimmed) {\n throw new ParseError('Empty time string. Expected format: \"9:07 AM\", \"09:00\", or \"14:30\"', 0);\n }\n\n // Lex the input\n const tokens = lexTemporal(trimmed);\n\n // Create a simple token parser\n let i = 0;\n const peek = (k = 0): AnyToken => tokens[Math.min(i + k, tokens.length - 1)]!;\n const at = (type: TokType): boolean => peek().type === type;\n const eat = (type: TokType): AnyToken => {\n const t = peek();\n if (t.type !== type) {\n throw new ParseError(\n `Invalid time format: \"${trimmed}\". Expected format: \"HH:MM\" (e.g., \"09:00\", \"14:30\") or \"H:MM AM/PM\" (e.g., \"9:07 AM\")`,\n i,\n );\n }\n i++;\n return t;\n };\n const tryEat = (type: TokType): AnyToken | null => {\n if (at(type)) {\n return eat(type);\n }\n return null;\n };\n\n // Parse: Number : Number [: Number [. Number]] [Ident]\n // Example: 2:30 PM, 14:30, 2:30:45.123 PM\n\n // Parse hour\n const hourTok = eat(TokType.Number);\n let hour = toInt(hourTok.value, 'hour', i);\n\n // Parse colon\n eat(TokType.Colon);\n\n // Parse minute\n const minuteTok = eat(TokType.Number);\n const minute = toInt(minuteTok.value, 'minute', i);\n\n // Validate minute format (must be 2 digits)\n if (minuteTok.value.length !== 2) {\n throw new ParseError(\n `Invalid time format: \"${trimmed}\". Minutes must be 2 digits (e.g., \"9:07\" not \"9:7\")`,\n i,\n );\n }\n\n // Validate minute\n if (minute < 0 || minute > 59) {\n throw new ParseError(`Invalid minute: ${minute}. Minutes must be between 00-59`, i);\n }\n\n // Optional seconds\n let second: number | undefined;\n let fraction: string | undefined;\n\n if (tryEat(TokType.Colon)) {\n const secondTok = eat(TokType.Number);\n second = toInt(secondTok.value, 'second', i);\n\n // Validate second\n if (second < 0 || second > 59) {\n throw new ParseError(`Invalid second: ${second}. Seconds must be between 00-59`, i);\n }\n\n // Optional fractional seconds (. or ,)\n if (tryEat(TokType.Dot) || tryEat(TokType.Comma)) {\n const fracTok = eat(TokType.Number);\n fraction = fracTok.value;\n }\n }\n\n // Optional AM/PM\n let ampmStr: string | undefined;\n if (at(TokType.Ident)) {\n const identTok = eat(TokType.Ident);\n const ident = String(identTok.value).toUpperCase();\n\n // Check for AM/PM patterns: A, P, AM, PM\n if (ident === 'A' || ident === 'P' || ident === 'AM' || ident === 'PM') {\n ampmStr = ident[0]; // Extract 'A' or 'P'\n } else {\n throw new ParseError(\n `Invalid time format: unexpected \"${identTok.value}\". Use AM/PM for 12-hour format or omit for 24-hour format`,\n i,\n );\n }\n\n // Optional dot after A/P (for a.m./p.m. format)\n tryEat(TokType.Dot);\n\n // Optional M after dot (for a.m./p.m. format)\n if (at(TokType.Ident)) {\n const mTok = tryEat(TokType.Ident);\n if (mTok && String(mTok.value).toUpperCase() !== 'M') {\n throw new ParseError(\n `Invalid time format: unexpected \"${mTok.value}\". Expected \"a.m.\" or \"p.m.\" format`,\n i,\n );\n }\n // Optional dot after M\n tryEat(TokType.Dot);\n }\n }\n\n // Expect EOF\n eat(TokType.EOF);\n\n // Handle AM/PM conversion\n if (ampmStr !== undefined) {\n const isPM = ampmStr === 'P';\n\n // Validate 12-hour format hour range\n if (hour < 1 || hour > 12) {\n throw new ParseError(\n `Invalid hour for 12-hour format: ${hour}. Hours with AM/PM must be between 1-12 (e.g., \"9:07 AM\", not \"0:07 AM\" or \"13:07 AM\")`,\n i,\n );\n }\n\n // Convert to 24-hour format\n if (isPM) {\n // PM: 12 PM = 12, 1 PM = 13, ..., 11 PM = 23\n if (hour !== 12) {\n hour += 12;\n }\n } else {\n // AM: 12 AM = 0, 1 AM = 1, ..., 11 AM = 11\n if (hour === 12) {\n hour = 0;\n }\n }\n } else {\n // 24-hour format validation\n if (hour < 0 || hour > 23) {\n throw new ParseError(\n `Invalid hour for 24-hour format: ${hour}. Hours must be between 0-23 (e.g., \"09:00\", \"14:30\"). For hours > 12, use 24-hour format or add AM/PM`,\n i,\n );\n }\n }\n\n return {\n kind: 'Time',\n hour,\n minute,\n second,\n fraction,\n };\n}\n", "// stringify.ts\n// Convert AST back to temporal string representation\n\nimport type {\n AnnotationAst,\n DateAst,\n DateTimeAst,\n DurationAst,\n OffsetAst,\n RangeAst,\n TemporalAst,\n TimeAst,\n TimeZoneAst,\n} from './parser-types.js';\n\n/**\n * Convert a temporal AST back to its string representation.\n *\n * @param ast - The AST to stringify\n * @returns ISO 8601 / IXDTF formatted string\n *\n * @example\n * ```typescript\n * const ast = parseTemporal('2025-01-12T10:00:00+08:00');\n * const str = stringifyTemporal(ast);\n * // '2025-01-12T10:00:00+08:00'\n * ```\n */\nexport function stringifyTemporal(ast: TemporalAst): string {\n if (ast.kind === 'Range') {\n return stringifyRange(ast);\n }\n if (ast.kind === 'Duration') {\n return stringifyDuration(ast);\n }\n return stringifyDateTime(ast);\n}\n\n/**\n * Stringify a date AST to ISO 8601 format.\n *\n * @param date - The date AST\n * @returns Date string (YYYY, YYYY-MM, or YYYY-MM-DD)\n */\nexport function stringifyDate(date: DateAst): string {\n // Handle negative years (BC dates) - ISO 8601 uses negative years\n // Year 0 = 1 BC, Year -1 = 2 BC, etc.\n let yearStr: string;\n if (date.year < 0) {\n // For negative years, pad the absolute value and prepend the minus sign\n yearStr = '-' + Math.abs(date.year).toString().padStart(4, '0');\n } else {\n yearStr = date.year.toString().padStart(4, '0');\n }\n\n const parts: string[] = [yearStr];\n\n if (date.month != null) {\n parts.push(date.month.toString().padStart(2, '0'));\n\n if (date.day != null) {\n parts.push(date.day.toString().padStart(2, '0'));\n }\n }\n\n return parts.join('-');\n}\n\n/**\n * Stringify a time AST to ISO 8601 format.\n *\n * @param time - The time AST\n * @returns Time string (HH:MM, HH:MM:SS, or HH:MM:SS.fff)\n */\nexport function stringifyTime(time: TimeAst): string {\n const parts: string[] = [\n time.hour.toString().padStart(2, '0'),\n time.minute.toString().padStart(2, '0'),\n ];\n\n if (time.second != null) {\n let secondStr = time.second.toString().padStart(2, '0');\n if (time.fraction != null) {\n secondStr += `.${time.fraction}`;\n }\n parts.push(secondStr);\n }\n\n return parts.join(':');\n}\n\n/**\n * Stringify a timezone offset AST to canonical format.\n *\n * @param offset - The offset AST\n * @returns Offset string in canonical format (Z or \u00B1HH:MM)\n */\nexport function stringifyOffset(offset: OffsetAst): string {\n if (offset.kind === 'UtcOffset') {\n return 'Z';\n }\n // Return canonical format: +HH:MM or -HH:MM\n const hours = offset.hours.toString().padStart(2, '0');\n const minutes = offset.minutes.toString().padStart(2, '0');\n return `${offset.sign}${hours}:${minutes}`;\n}\n\n/**\n * Stringify a timezone AST.\n *\n * @param timeZone - The timezone AST\n * @returns Timezone string [Asia/Singapore] or [!Asia/Singapore]\n */\nexport function stringifyTimeZone(timeZone: TimeZoneAst): string {\n const id = timeZone.critical ? `!${timeZone.id}` : timeZone.id;\n return `[${id}]`;\n}\n\n/**\n * Stringify an annotation AST.\n *\n * @param annotation - The annotation AST\n * @returns Annotation string [u-ca=gregory] or [!u-ca=gregory]\n */\nexport function stringifyAnnotation(annotation: AnnotationAst): string {\n return `[${annotation.raw}]`;\n}\n\n/**\n * Stringify a datetime AST to ISO 8601 / IXDTF format.\n *\n * @param dateTime - The datetime AST\n * @returns DateTime string with optional time, offset, timezone, and annotations\n */\nexport function stringifyDateTime(dateTime: DateTimeAst): string {\n let result = stringifyDate(dateTime.date);\n\n if (dateTime.time) {\n result += `T${stringifyTime(dateTime.time)}`;\n }\n\n if (dateTime.offset) {\n result += stringifyOffset(dateTime.offset);\n }\n\n if (dateTime.timeZone) {\n result += stringifyTimeZone(dateTime.timeZone);\n }\n\n for (const annotation of dateTime.annotations) {\n result += stringifyAnnotation(annotation);\n }\n\n return result;\n}\n\n/**\n * Stringify a duration AST to ISO 8601 format.\n *\n * @param duration - The duration AST\n * @returns Duration string (P1Y2M3DT4H5M6S)\n */\nexport function stringifyDuration(duration: DurationAst): string {\n // Always reconstruct from components to ensure normalization\n let result = 'P';\n\n // Date part - include component if defined (even if zero)\n if (duration.years != null) {\n result += `${duration.years}Y`;\n }\n if (duration.months != null) {\n result += `${duration.months}M`;\n }\n if (duration.weeks != null) {\n result += `${duration.weeks}W`;\n }\n if (duration.days != null) {\n result += `${duration.days}D`;\n }\n\n // Time part - add T separator if any time component is defined\n const hasTimePart =\n duration.hours != null || duration.minutes != null || duration.seconds != null;\n if (hasTimePart) {\n result += 'T';\n if (duration.hours != null) {\n result += `${duration.hours}H`;\n }\n if (duration.minutes != null) {\n result += `${duration.minutes}M`;\n }\n if (duration.seconds != null) {\n result += `${duration.seconds}`;\n if (duration.secondsFraction != null && duration.secondsFraction.length > 0) {\n result += `.${duration.secondsFraction}`;\n }\n result += 'S';\n }\n }\n\n // Annotations\n for (const annotation of duration.annotations) {\n result += stringifyAnnotation(annotation);\n }\n\n return result;\n}\n\n/**\n * Stringify a range AST to ISO 8601 format.\n *\n * @param range - The range AST\n * @returns Range string (start/end, /end, or start/)\n */\nexport function stringifyRange(range: RangeAst): string {\n const start = range.start\n ? range.start.kind === 'Duration'\n ? stringifyDuration(range.start)\n : stringifyDateTime(range.start)\n : '';\n\n const end = range.end\n ? range.end.kind === 'Duration'\n ? stringifyDuration(range.end)\n : stringifyDateTime(range.end)\n : '';\n\n return `${start}/${end}`;\n}\n"], | ||
| "mappings": "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", | ||
| "names": ["TokType", "CombinedTokType"] | ||
| } |
@@ -7,2 +7,3 @@ export { combineTimezoneOffsets } from './combineTimezoneOffsets.js'; | ||
| export { parseOffset } from './parseOffset.js'; | ||
| export { parseTimeString } from './parseTimeString.js'; | ||
| export { stringifyAnnotation, stringifyDate, stringifyDateTime, stringifyDuration, stringifyOffset, stringifyRange, stringifyTemporal, stringifyTime, stringifyTimeZone, } from './stringify.js'; | ||
@@ -9,0 +10,0 @@ export type { AnnotationAst, DateAst, DateTimeAst, DurationAst, OffsetAst, RangeAst, TemporalAst, TimeAst, TimeZoneAst, ValueAst, } from './parser-types.js'; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzC,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,aAAa,EACb,OAAO,EACP,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzC,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,aAAa,EACb,OAAO,EACP,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"} |
+1
-1
| { | ||
| "name": "@taskade/temporal-parser", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "A lexer/parser based temporal expression parser for ISO 8601 and IXDTF formats", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+53
-0
@@ -110,2 +110,35 @@ # Temporal Parser | ||
| ### Standalone Time Parser | ||
| ```typescript | ||
| import { parseTimeString } from '@taskade/temporal-parser'; | ||
| // Parse 12-hour format with AM/PM | ||
| const time1 = parseTimeString('2:30 PM'); | ||
| // { kind: 'Time', hour: 14, minute: 30 } | ||
| // Parse 24-hour format (international) | ||
| const time2 = parseTimeString('14:30'); | ||
| // { kind: 'Time', hour: 14, minute: 30 } | ||
| // Parse with seconds | ||
| const time3 = parseTimeString('2:30:45 PM'); | ||
| // { kind: 'Time', hour: 14, minute: 30, second: 45 } | ||
| // Parse with fractional seconds | ||
| const time4 = parseTimeString('14:30:45.123'); | ||
| // { kind: 'Time', hour: 14, minute: 30, second: 45, fraction: '123' } | ||
| // Flexible AM/PM formats | ||
| parseTimeString('2:30 PM'); // Standard | ||
| parseTimeString('2:30PM'); // No space | ||
| parseTimeString('2:30 pm'); // Lowercase | ||
| parseTimeString('2:30 p.m.'); // With periods | ||
| // Special times | ||
| parseTimeString('12:00 AM'); // Midnight (hour: 0) | ||
| parseTimeString('12:00 PM'); // Noon (hour: 12) | ||
| parseTimeString('11:59 PM'); // End of day (hour: 23) | ||
| ``` | ||
| ### Stringify AST Back to String | ||
@@ -208,2 +241,22 @@ | ||
| ### `parseTimeString(input: string): TimeAst` | ||
| Parses a standalone time string in various formats. | ||
| **Supported formats:** | ||
| - 12-hour with AM/PM: `2:30 PM`, `02:30PM`, `2:30 p.m.` | ||
| - 24-hour (international): `14:30`, `02:30`, `23:59` | ||
| - With seconds: `2:30:45 PM`, `14:30:45` | ||
| - With fractional seconds: `2:30:45.123 PM`, `14:30:45,123` (comma or dot) | ||
| **Special cases:** | ||
| - `12:00 AM` → midnight (hour: 0) | ||
| - `12:00 PM` → noon (hour: 12) | ||
| - `12:30 AM` → 00:30 (after midnight) | ||
| - `12:30 PM` → 12:30 (after noon) | ||
| **Returns:** `TimeAst` object compatible with `Temporal.PlainTime.from()` | ||
| **Throws:** `ParseError` if the input is invalid | ||
| ### `stringifyTemporal(ast: TemporalAst): string` | ||
@@ -210,0 +263,0 @@ |
Sorry, the diff of this file is too big to display
218877
13.79%36
5.88%2088
13.73%305
21.03%