diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b1020a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ + +name: Tempo CI +env: + TZ: America/New_York + LANG: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + +on: + push: + branches: + - main + - release-c-layout-order-planner + pull_request: + branches: + - main + - release-c-layout-order-planner + +jobs: + test: + name: Standard Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('packages/tempo/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + - name: Install monorepo dependencies + run: npm ci + working-directory: ${{ github.workspace }} + - name: Run standard tests + run: npm test + working-directory: packages/tempo + + test-parse-prefilter: + name: Test with parsePrefilter enabled + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.ref == 'refs/heads/release-c-layout-order-planner' || github.event.pull_request.base.ref == 'main' + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('packages/tempo/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + - name: Install monorepo dependencies + run: npm ci + working-directory: ${{ github.workspace }} + - name: Write parsePrefilter setup file + run: | + echo "import { Tempo } from '../src/tempo.index.ts';\nTempo.init({ parsePrefilter: true });" > packages/tempo/test/ci.prefilter.setup.js + - name: Run all tests with parsePrefilter + run: npm test + working-directory: packages/tempo + env: + TEMPO_PREFILTER_CI: 'true' + - name: Run end-to-end benchmark + run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json + working-directory: packages/tempo + - name: Upload benchmark output + uses: actions/upload-artifact@v4 + with: + name: bench-parse-prefilter-e2e + path: packages/tempo/bench-output.json + - name: Validate benchmark output + run: | + node -e "const r=require('./packages/tempo/bench-output.json');if(!r.success){console.error('Benchmark failed:',r.errors);process.exit(1)}else{console.log('Benchmark passed.')}" + working-directory: ${{ github.workspace }} diff --git a/packages/tempo/bench/bench.parse.prefilter.e2e.ts b/packages/tempo/bench/bench.parse.prefilter.e2e.ts new file mode 100644 index 0000000..943fb92 --- /dev/null +++ b/packages/tempo/bench/bench.parse.prefilter.e2e.ts @@ -0,0 +1,102 @@ +import '../bin/temporal-polyfill.ts'; +import { Tempo } from '../src/tempo.index.ts'; +import { performance } from 'node:perf_hooks'; + +import fs from 'fs'; + +let corpus: string[] = []; +const layoutKeys = new Set([ + 'hourMinuteSecond', 'dayMonthYearShort', 'monthDayYearShort', 'yearMonthDayShort', + 'weekDay', 'date', 'time', 'dateTime', 'timeDate', 'dayMonthYear', 'monthDayYear', + 'yearMonthDay', 'offset', 'relativeOffset' +]); +try { + corpus = fs.readFileSync(new URL('./bench.parse.prefilter.ts', import.meta.url), 'utf-8') + .split(/\n/) + .filter(line => line.trim().startsWith("'") && line.includes(',')) + .map(line => line.replace(/['",]/g, '').trim()) + .filter(Boolean) + .filter(line => !layoutKeys.has(line)); +} catch { + corpus = [ + '04012026', + '310559', + '590531', + '09:30', + 'monday', + '2 days ago', + '+6', + '1234567890123', + '2026-04-25', + '2026/04/25 10:30', + '11:45pm', + 'tomorrow', + ]; +} + +function runE2E(enablePrefilter: boolean, iterations: number) { + Tempo.init({ + parsePrefilter: enablePrefilter, + debug: false, + catch: true, + timeZone: 'UTC', + }); + + let checksum = 0; + const start = performance.now(); + + for (let i = 0; i < iterations; i++) { + for (const input of corpus) { + const t = new Tempo(input, { timeZone: 'UTC' }); + checksum += t.toDateTime().epochMilliseconds; + } + } + + const elapsedMs = performance.now() - start; + const operations = iterations * corpus.length; + + return { + iterations, + operations, + elapsedMs: Number(elapsedMs.toFixed(3)), + msPerIteration: Number((elapsedMs / iterations).toFixed(6)), + msPerInput: Number((elapsedMs / operations).toFixed(6)), + checksum, + }; +} + +const warmupIterations = Number(process.env.PREFILTER_E2E_WARMUP ?? 200); +const benchIterations = Number(process.env.PREFILTER_E2E_ITERATIONS ?? 1000); + +// Warmup both paths to reduce one-time JIT effects. +runE2E(false, warmupIterations); +runE2E(true, warmupIterations); + +const base = runE2E(false, benchIterations); +const pre = runE2E(true, benchIterations); + +const timingDeltaPct = Number((((pre.elapsedMs - base.elapsedMs) / base.elapsedMs) * 100).toFixed(2)); + +const result = { + base, + pre, + timingDeltaPct, + thresholds: { + maxTimingDeltaPct: 10, // fail if prefilter is >10% slower than base + minChecksum: 1 // dummy threshold, adjust as needed + }, + success: true, + errors: [] +}; + +if (timingDeltaPct > result.thresholds.maxTimingDeltaPct) { + result.success = false; + result.errors.push(`Prefilter is too slow: ${timingDeltaPct}% > ${result.thresholds.maxTimingDeltaPct}%`); +} +if (base.checksum < result.thresholds.minChecksum || pre.checksum < result.thresholds.minChecksum) { + result.success = false; + result.errors.push('Checksum below minimum threshold'); +} + +console.log(JSON.stringify(result)); +if (!result.success) process.exit(1); \ No newline at end of file diff --git a/packages/tempo/bench/bench.parse.prefilter.ts b/packages/tempo/bench/bench.parse.prefilter.ts new file mode 100644 index 0000000..2bcc7ee --- /dev/null +++ b/packages/tempo/bench/bench.parse.prefilter.ts @@ -0,0 +1,256 @@ +import { selectLayoutPatterns } from '../src/engine/engine.planner.ts'; +import { performance } from 'node:perf_hooks'; + +const layoutNames = [ + 'hourMinuteSecond', + 'dayMonthYearShort', + 'monthDayYearShort', + 'yearMonthDayShort', + 'weekDay', + 'date', + 'time', + 'dateTime', + 'timeDate', + 'dayMonthYear', + 'monthDayYear', + 'yearMonthDay', + 'offset', + 'relativeOffset', +]; + +function makeState(names: string[]) { + const symbols = names.map(name => Symbol(name)); + const layout = Object.fromEntries(symbols.map(sym => [sym, `{${sym.description}}`])) as Record; + const pattern = new Map(symbols.map(sym => [sym, new RegExp(`^${sym.description}$`, 'i')])); + + return { + parse: { + layout, + pattern, + token: {}, + } + } as any; +} + +const corpus = [ + '04012026', + '310559', + '590531', + '09:30', + 'monday', + '2 days ago', + '+6', + '1234567890123', + '2026-04-25', + '2026/04/25 10:30', + '11:45pm', + 'tomorrow', + // Expanded real-world and event/timezone cases + '2026-04-25T10:30:00Z', + '2026-04-25T10:30:00+05:30', + '2026-04-25T10:30:00-07:00', + '2026-04-25T10:30:00[America/New_York]', + '2026-04-25T10:30:00[Europe/London]', + 'next Friday at 5pm', + 'last Monday', + 'in 3 weeks', + 'yesterday', + 'noon', + 'midnight', + '2026-12-31T23:59:59.999Z', + '2026-12-31T23:59:59.999+09:00', + '2026-12-31T23:59:59.999[Asia/Tokyo]', + '2026-12-31', + '2026-12-31 23:59', + '2026-12-31 11:59pm', + '2026-12-31T23:59:59', + '2026-12-31T23:59', + '2026-12-31T11:59pm', + '2026-12-31T23:59:59.999', + '2026-12-31T23:59:59.999999999', + '2026-12-31T23:59:59.999999999Z', + '2026-12-31T23:59:59.999999999+00:00', + '2026-12-31T23:59:59.999999999-08:00', + '2026-12-31T23:59:59.999999999[America/Los_Angeles]', + '2026-12-31T23:59:59.999999999[Australia/Sydney]', + '2026-12-31T23:59:59.999999999[UTC]', + '2026-12-31T23:59:59.999999999[Etc/GMT+2]', + '2026-12-31T23:59:59.999999999[Europe/Berlin]', + '2026-12-31T23:59:59.999999999[America/Sao_Paulo]', + '2026-12-31T23:59:59.999999999[Asia/Kolkata]', + '2026-12-31T23:59:59.999999999[Pacific/Auckland]', + '2026-12-31T23:59:59.999999999[America/Chicago]', + '2026-12-31T23:59:59.999999999[Europe/Moscow]', + '2026-12-31T23:59:59.999999999[Asia/Shanghai]', + '2026-12-31T23:59:59.999999999[America/Vancouver]', + '2026-12-31T23:59:59.999999999[Europe/Paris]', + '2026-12-31T23:59:59.999999999[America/Denver]', + '2026-12-31T23:59:59.999999999[Europe/Rome]', + '2026-12-31T23:59:59.999999999[Asia/Singapore]', + '2026-12-31T23:59:59.999999999[America/Toronto]', + '2026-12-31T23:59:59.999999999[Europe/Madrid]', + '2026-12-31T23:59:59.999999999[America/Mexico_City]', + '2026-12-31T23:59:59.999999999[Asia/Hong_Kong]', + '2026-12-31T23:59:59.999999999[Europe/Istanbul]', + '2026-12-31T23:59:59.999999999[America/Anchorage]', + '2026-12-31T23:59:59.999999999[Pacific/Honolulu]', + '2026-12-31T23:59:59.999999999[Europe/Zurich]', + '2026-12-31T23:59:59.999999999[America/Argentina/Buenos_Aires]', + '2026-12-31T23:59:59.999999999[Asia/Dubai]', + '2026-12-31T23:59:59.999999999[Europe/Stockholm]', + '2026-12-31T23:59:59.999999999[America/Phoenix]', + '2026-12-31T23:59:59.999999999[Asia/Seoul]', + '2026-12-31T23:59:59.999999999[Europe/Brussels]', + '2026-12-31T23:59:59.999999999[America/Edmonton]', + '2026-12-31T23:59:59.999999999[Asia/Bangkok]', + '2026-12-31T23:59:59.999999999[Europe/Amsterdam]', + '2026-12-31T23:59:59.999999999[America/Caracas]', + '2026-12-31T23:59:59.999999999[Asia/Jakarta]', + '2026-12-31T23:59:59.999999999[Europe/Prague]', + '2026-12-31T23:59:59.999999999[America/Guatemala]', + '2026-12-31T23:59:59.999999999[Asia/Manila]', + '2026-12-31T23:59:59.999999999[Europe/Bucharest]', + '2026-12-31T23:59:59.999999999[America/Santiago]', + '2026-12-31T23:59:59.999999999[Asia/Kathmandu]', + '2026-12-31T23:59:59.999999999[Europe/Copenhagen]', + '2026-12-31T23:59:59.999999999[America/Montevideo]', + '2026-12-31T23:59:59.999999999[Asia/Taipei]', + '2026-12-31T23:59:59.999999999[Europe/Helsinki]', + '2026-12-31T23:59:59.999999999[America/La_Paz]', + '2026-12-31T23:59:59.999999999[Asia/Karachi]', + '2026-12-31T23:59:59.999999999[Europe/Lisbon]', + '2026-12-31T23:59:59.999999999[America/Bogota]', + '2026-12-31T23:59:59.999999999[Asia/Tehran]', + '2026-12-31T23:59:59.999999999[Europe/Oslo]', + '2026-12-31T23:59:59.999999999[America/Lima]', + '2026-12-31T23:59:59.999999999[Asia/Kuala_Lumpur]', + '2026-12-31T23:59:59.999999999[Europe/Warsaw]', + '2026-12-31T23:59:59.999999999[America/Havana]', + '2026-12-31T23:59:59.999999999[Asia/Saigon]', + '2026-12-31T23:59:59.999999999[Europe/Athens]', + '2026-12-31T23:59:59.999999999[America/Detroit]', + '2026-12-31T23:59:59.999999999[Asia/Yangon]', + '2026-12-31T23:59:59.999999999[Europe/Dublin]', + '2026-12-31T23:59:59.999999999[America/Port-au-Prince]', + '2026-12-31T23:59:59.999999999[Asia/Tashkent]', + '2026-12-31T23:59:59.999999999[Europe/Vienna]', + '2026-12-31T23:59:59.999999999[America/Asuncion]', + '2026-12-31T23:59:59.999999999[Asia/Baghdad]', + '2026-12-31T23:59:59.999999999[Europe/Belgrade]', + '2026-12-31T23:59:59.999999999[America/Managua]', + '2026-12-31T23:59:59.999999999[Asia/Almaty]', + '2026-12-31T23:59:59.999999999[Europe/Sofia]', + '2026-12-31T23:59:59.999999999[America/Recife]', + '2026-12-31T23:59:59.999999999[Asia/Novosibirsk]', + '2026-12-31T23:59:59.999999999[Europe/Berlin]', +]; + +function run(enablePrefilter: boolean) { + const state = makeState(layoutNames); + let totalCandidates = 0; + let selectedCandidates = 0; + const ruleHits = new Map(); + + for (const input of corpus) { + selectLayoutPatterns(state, input, { + enablePrefilter, + onPlan: summary => { + totalCandidates += summary.totalCandidates; + selectedCandidates += summary.selectedCandidates; + summary.rulesApplied.forEach(rule => { + ruleHits.set(rule, (ruleHits.get(rule) ?? 0) + 1); + }); + } + }); + } + + return { + enablePrefilter, + inputs: corpus.length, + totalCandidates, + selectedCandidates, + reductionPct: totalCandidates === 0 + ? 0 + : Number((((totalCandidates - selectedCandidates) / totalCandidates) * 100).toFixed(2)), + ruleHits: Object.fromEntries([...ruleHits.entries()].sort()), + }; +} + +function timeRun(enablePrefilter: boolean, iterations: number) { + const state = makeState(layoutNames); + const start = performance.now(); + + for (let i = 0; i < iterations; i++) { + for (const input of corpus) { + selectLayoutPatterns(state, input, { enablePrefilter }); + } + } + + const elapsedMs = performance.now() - start; + const operations = iterations * corpus.length; + + return { + iterations, + operations, + elapsedMs: Number(elapsedMs.toFixed(3)), + msPerIteration: Number((elapsedMs / iterations).toFixed(6)), + msPerInput: Number((elapsedMs / operations).toFixed(6)), + }; +} + +const warmupIterations = Number(process.env.PREFILTER_BENCH_WARMUP ?? 200); +const benchIterations = Number(process.env.PREFILTER_BENCH_ITERATIONS ?? 5000); + +// Warmup both paths to reduce one-time JIT effects. +timeRun(false, warmupIterations); +timeRun(true, warmupIterations); + +const base = run(false); +const pre = run(true); +const baseTiming = timeRun(false, benchIterations); +const preTiming = timeRun(true, benchIterations); + +console.log('\nParse Planner Candidate Benchmark'); +console.table([ + { + mode: 'prefilter:off', + inputs: base.inputs, + totalCandidates: base.totalCandidates, + selectedCandidates: base.selectedCandidates, + reductionPct: base.reductionPct, + }, + { + mode: 'prefilter:on', + inputs: pre.inputs, + totalCandidates: pre.totalCandidates, + selectedCandidates: pre.selectedCandidates, + reductionPct: pre.reductionPct, + }, +]); + +console.log('Rule hits (prefilter:on):'); +console.table(pre.ruleHits); + +const timingDeltaPct = Number((((preTiming.elapsedMs - baseTiming.elapsedMs) / baseTiming.elapsedMs) * 100).toFixed(2)); + +console.log('\nParse Planner Timing Benchmark (selection phase only)'); +console.table([ + { + mode: 'prefilter:off', + iterations: baseTiming.iterations, + operations: baseTiming.operations, + elapsedMs: baseTiming.elapsedMs, + msPerIteration: baseTiming.msPerIteration, + msPerInput: baseTiming.msPerInput, + }, + { + mode: 'prefilter:on', + iterations: preTiming.iterations, + operations: preTiming.operations, + elapsedMs: preTiming.elapsedMs, + msPerIteration: preTiming.msPerIteration, + msPerInput: preTiming.msPerInput, + }, +]); + +console.log(`Timing delta (prefilter:on vs off): ${timingDeltaPct}%`); diff --git a/packages/tempo/doc/tempo.config.md b/packages/tempo/doc/tempo.config.md index 2b8b8a9..8fbdfcb 100644 --- a/packages/tempo/doc/tempo.config.md +++ b/packages/tempo/doc/tempo.config.md @@ -133,7 +133,7 @@ Tempo.init({ | `locale` | `string` | System Locale | Default BCP 47 language tag. used in .since() method | | `calendar` | `string` | `'iso8601'` | Default calendar system. | | `pivot` | `number` | `75` | Cutoff for parsing two-digit years. | -| `mdyLocales` | `string \| string[]` | `['en-US','en-AS']` | Locale list that prefers month-day-year parse ordering. | +| `mdyLocales` | `string \| string[]` | `['en-US','en-AS']` | Locale list that prefers month-day-year parse ordering. Tempo uses `Intl.Locale.getTimeZones()` to detect if your `timeZone` belongs to these locales, with an automated fallback for CI environments. | | `mdyLayouts` | `[string, string][]` | Built-in pairs | Layout swap pairs used when month-day ordering applies. | | `timeStamp`| `'ms' \| 'ns'` | `'ms'` | Precision for timestamps. | | `sphere` | `'north' \| 'south'`| Auto-inferred | Hemisphere for seasonal plugins. | diff --git a/packages/tempo/doc/tempo.parse.md b/packages/tempo/doc/tempo.parse.md index 2006509..f6a814e 100644 --- a/packages/tempo/doc/tempo.parse.md +++ b/packages/tempo/doc/tempo.parse.md @@ -90,6 +90,8 @@ Tempo uses your configuration to resolve ambiguous dates. ### US-Style Dates (`MM/DD/YYYY`) If you parse a numeric string like `04012026`, Tempo uses your `timeZone` to decide if it means **April 1st** (US) or **4th of January** (UK/AU). +Tempo achieves this by dynamically checking if your current `timeZone` is associated with a locale that prefers Month-Day ordering (like `en-US`). It uses the `Intl.Locale.prototype.getTimeZones()` API where available, and maintains a robust **hardcoded fallback list** for environments (like Node.js CI or non-ICU environments) where the full Intl API is not present. + ```typescript const us = new Tempo('04012026', { timeZone: 'America/New_York' }); // Apr 1 const au = new Tempo('04012026', { timeZone: 'Australia/Sydney' }); // Jan 4 diff --git a/packages/tempo/src/support/tempo.default.ts b/packages/tempo/src/support/tempo.default.ts index 3fbd3a2..02560e8 100644 --- a/packages/tempo/src/support/tempo.default.ts +++ b/packages/tempo/src/support/tempo.default.ts @@ -77,6 +77,12 @@ export type Snippet = typeof Snippet * a {layout} is a Record of snippet-combinations describing an input DateTime argument * the Layout's keys are in the order that they will be checked against an input value */ +/** @internal Layout components for date resolution */ +export const datePattern = { + dmy: '({dd}{sep}?{mm}({sep}?{yy})?|{mod}?({evt})|(?{slk})|{wkd})', + mdy: '({mm}{sep}?{dd}({sep}?{yy})?|{mod}?({evt})|(?{slk})|{wkd})' +} + /** @internal Tempo Layout registry */ export const Layout = looseIndex()({ [Token.hms]: '(?(?:[01][0-9]|2[0-4]))(?[0-5][0-9])(?[0-5][0-9])', // compact clock (hhmiss) @@ -84,7 +90,7 @@ export const Layout = looseIndex()({ [Token.mdy6]: '(?0[1-9]|1[0-2])(?
0[1-9]|[12][0-9]|3[01])(?[0-9]{2})',// compact date (mmddyy) [Token.ymd6]: '(?[0-9]{2})(?0[1-9]|1[0-2])(?
0[1-9]|[12][0-9]|3[01])',// compact date (yymmdd) [Token.wkd]: '{mod}?{wkd}{afx}?{sfx}?', // weekday-only layout; MUST precede {dt} (which also matches bare weekday names via its {wkd} alternative) - [Token.dt]: '({dd}{sep}?{mm}({sep}?{yy})?|{mod}?({evt})|(?{slk})|{wkd})',// calendar, event, slick or weekday + [Token.dt]: datePattern.dmy, // calendar, event, slick or weekday [Token.tm]: '({hh}{mi}?{ss}?{ff}?{mer}?|{per})', // clock or period [Token.dtm]: '({dt})(?:(?:{sep}+|T)({tm}))?{tzd}?{brk}?', // calendar/event and clock/period [Token.tmd]: '({tm})(?:(?:{sep}+|T)({dt}))?{tzd}?{brk}?', // clock/period and calendar/event @@ -192,3 +198,53 @@ export const Default = secure({ /** preferred parse-order of layouts */ layoutOrder: [], /** hemisphere for term.qtr or term.szn */ sphere: undefined, } as Options) + +/** @internal + * Fallback for environments which do not robustly support Intl.Locale.getTimeZones() + * Keep an eye on this list ! It may become necessary in a future release to allow Users to update this list. + */ +export const mdyFallback = { + 'en-US': [ + "America/Adak", + "America/Anchorage", + "America/Boise", + "America/Chicago", + "America/Denver", + "America/Detroit", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Los_Angeles", + "America/Louisville", + "America/Menominee", + "America/Metlakatla", + "America/New_York", + "America/Nome", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Phoenix", + "America/Sitka", + "America/Yakutat", + "Pacific/Honolulu", + "US/Aleutian", + "US/Alaska", + "US/Arizona", + "US/Central", + "US/Eastern", + "US/Mountain", + "US/Pacific" + ], + 'en-AS': [ + "Pacific/Pago_Pago" + ] +} as Record diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index dbe2289..259d017 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -21,6 +21,7 @@ import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engi import type { TermPlugin, Plugin } from './plugin/plugin.type.js'; import { setProperty, proto, hasOwn, create, compileRegExp, setPatterns, normalizeLayoutOrder } from './support/tempo.util.js'; +import { mdyFallback, datePattern } from './support/tempo.default.js'; import { sym, markConfig, TermError, getRuntime, init, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $buildGuard, $IsBase, type TempoBrand, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support'; import * as t from './tempo.type.js'; // namespaced types (Tempo.*) import { instant, normalizeUtcOffset } from '#library/temporal.library.js'; @@ -64,20 +65,20 @@ namespace Internal { export class Tempo { /** Weekday names (short-form) */ static get WEEKDAY() { return enums.WEEKDAY } /** Weekday names (long-form) */ static get WEEKDAYS() { return enums.WEEKDAYS } - /** Month names (short-form) */ static get MONTH() { return enums.MONTH } - /** Month names (long-form) */ static get MONTHS() { return enums.MONTHS } + /** Month names (short-form) */ static get MONTH() { return enums.MONTH } + /** Month names (long-form) */ static get MONTHS() { return enums.MONTHS } /** Time durations as seconds (singular) */ static get DURATION() { return enums.DURATION } - /** Time durations as milliseconds (plural) */ static get DURATIONS() { return enums.DURATIONS } + /** Time durations as milliseconds (plural) */ static get DURATIONS() { return enums.DURATIONS } /** Quarterly Seasons */ static get SEASON() { return enums.SEASON } /** Compass cardinal points */ static get COMPASS() { return enums.COMPASS } - /** Tempo to Temporal DateTime Units map */ static get ELEMENT() { return enums.ELEMENT } + /** Tempo to Temporal DateTime Units map */ static get ELEMENT() { return enums.ELEMENT } /** Pre-configured format {name -> string} pairs */ static get FORMAT() { return enums.FORMAT } /** Number names (0-10) */ static get NUMBER() { return enums.NUMBER } /** TimeZone aliases */ static get TIMEZONE() { return enums.TIMEZONE } /** initialization strategies */ static get MODE() { return enums.MODE } - /** some useful Dates */ static get LIMIT() { return enums.LIMIT } + /** some useful Dates */ static get LIMIT() { return enums.LIMIT } /** @internal check if Tempo is currently initializing */ static get isInitializing() { return !Tempo.#lifecycle.ready } /** @internal check if Tempo is currently extending */ static get isExtending() { return Tempo.#lifecycle.extendDepth > 0 } @@ -152,15 +153,15 @@ export class Tempo { } } - if (shape.parse.isMonthDay) { - const protoDt = proto(shape.parse.layout)[Token.dt] as string; - const localDt = '{mm}{sep}?{dd}({sep}?{yy})?|{mod}?({evt})'; - if (!isLocal(shape) || localDt !== protoDt) { - if (isLocal(shape) && !hasOwn(shape.parse, 'layout')) - shape.parse.layout = create(shape.parse, 'layout'); + const isMonthDay = Boolean(shape.parse.isMonthDay); + const protoDt = proto(shape.parse.layout)[Token.dt] as string; + const targetDt = isMonthDay ? datePattern.mdy : datePattern.dmy; - setProperty(shape.parse.layout, Token.dt, localDt); - } + if (!isLocal(shape) || targetDt !== protoDt) { + if (isLocal(shape) && !hasOwn(shape.parse, 'layout')) + shape.parse.layout = create(shape.parse, 'layout'); + + setProperty(shape.parse.layout, Token.dt, targetDt); } } @@ -209,13 +210,12 @@ export class Tempo { return isDefined(shape.config?.sphere) ? shape.config.sphere : undefined; } - /** determine if we have a {timeZone} which prefers {mdy} date-order */ static #isMonthDay(shape: Internal.State) { const monthDay = [...asArray((this as any)[$Internal]().parse.mdyLocales)]; if (isLocal(shape) && hasOwn(shape.parse, 'mdyLocales')) - monthDay.push(...shape.parse.mdyLocales); // append local mdyLocales (not overwrite global) + monthDay.push(...shape.parse.mdyLocales); // append local mdyLocales (not overwrite global) return monthDay.some(mdy => { const m = mdy as { locale: string, timeZones: string[] }; @@ -236,7 +236,7 @@ export class Tempo { const layout = resolveLayoutOrder({ layout: shape.parse.layout, mdyLayouts: shape.parse.mdyLayouts, - isMonthDay: !!shape.parse.isMonthDay, + isMonthDay: Boolean(shape.parse.isMonthDay), ...(layoutController !== undefined && { layoutController }), }); @@ -457,7 +457,10 @@ export class Tempo { static #mdyLocales(value: t.Options["mdyLocales"]) { return asArray(value) .map(mdy => new Intl.Locale(mdy)) - .map(mdy => ({ locale: mdy.baseName, timeZones: (mdy as Record).getTimeZones?.() ?? [] })) + .map(intl => ({ locale: intl.baseName, timeZones: intl.getTimeZones?.() ?? [] })) + // .map(intl => { console.log('pre: ', intl); return intl }) + .map(intl => (intl.timeZones.length > 0 ? intl : { ...intl, timeZones: mdyFallback[intl.locale] ?? [] })) + // .map(intl => { console.log('post: ', intl); return intl }) } /** support "Global Discovery" of user-options */ @@ -1007,12 +1010,12 @@ export class Tempo { /** allow instanceof to work across module boundaries via the local brand symbol */ static [$Identity] = true; static [Symbol.hasInstance](instance: any) { - return !!(instance?.[$Identity]) + return Boolean(instance?.[$Identity]) } /** check if a supplied variable is a valid Tempo instance */ static isTempo(instance?: any): instance is Tempo { - return !!(instance?.[$Identity]) + return instance instanceof Tempo;//Boolean(instance?.[$Identity]) } static { // Static initialization block to sequence the bootstrap phase