Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.3.0] - 2026-06-21

### Added
- **Localization Modifier Registry**: Modifiers (e.g., "next", "last", "this") can now be localized via the `registry.modifiers` configuration. This allows defining custom words for temporal shifts (e.g., `'>': ['prochain']`) which seamlessly integrate with standard parsing and `#` shorthand syntaxes.

### Changed
- **Unified Registry Architecture**: Moved the top-level `modifiers` configuration key under the `registry` namespace (`registry: { modifiers }`) to maintain architectural consistency with formats and locales. Deep-merging ensures English default keywords remain active alongside custom localized additions.
- **Lexer Token Cleanups**: Stripped hardcoded English keywords out of all internal Match regular expressions (`Match.modifier`, `Match.shorthand`, `Match.slick`), standardizing the engine on the symbolic modifiers (`>`, `<`, `=`, `+`, `-`).

### Fixed
- **Pre-filter Guard Numeric Safety**: Refined the guard-bypass logic to trigger *only* when the input string actually contains a registered modifier keyword, restoring proper strictness and fixing numeric-safety validation gaps for nanosecond epoch inputs.
- **Lazy Evaluation Delegation**: Fixed a regression in format lazy-loading where built-in formats (like `{date}` or `{time}`) were temporarily eclipsed by empty registry instances.
- **Trailing Affix Collisions**: Added explicit safety checks and warnings when a date unit is passed both a leading modifier and a trailing affix (e.g., "next May ago").

## [3.2.3] - 2026-06-20
- **Centralized Configuration**: Introduced `tempo.config.ts` and `tempo.config.js` discovery.
- **Async Bootstrap**: Added `Tempo.bootstrap()` for safe, asynchronous ES Module configuration resolution without breaking the synchronous `Tempo.init()` API.
- **CLI Scaffolding**: Added an `npx @magmacomputing/tempo scaffold:all` CLI tool to easily bootstrap `tempo.config.ts` and HTML sandboxes into projects.

### Changed
- **Zero-Dependency Resolution**: Implemented custom filesystem traversal to ensure robust config discovery without adding external dependencies (e.g., `cosmiconfig`).

## [3.2.2] - 2026-06-18

### Added
Expand Down
164 changes: 164 additions & 0 deletions packages/tempo/bench/bench.v3.3.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* Post-refactor (v3.3.0) representative benchmark
*
* Compares three configurations:
* A. Baseline: stock Tempo, same corpus as pre-refactor
* B. Module-based comparison using BenchmarkModule across auto/defer/strict modes
* C. Localized fr-FR modifiers through the new registry.modifiers code path
*/
import '../bin/temporal-polyfill.js';
import { Tempo } from '../src/tempo.index.js';
import { BenchmarkModule } from '../src/module/module.benchmark.js';
import { performance } from 'node:perf_hooks';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// ── Corpus ──────────────────────────────────────────────────────────────────

/** Same 20-entry corpus used for the pre-refactor benchmark */
const baseCorpus: string[] = [
'04012026',
'310559',
'590531',
'09:30',
'monday',
'2 days ago',
'+6',
'1234567890123',
'2026-04-25',
'2026/04/25 10:30',
'11:45pm',
'tomorrow',
'2026-04-25T10:30:00Z',
'2026-04-25T10:30:00+05:30',
'next Friday at 5pm',
'last Monday',
'in 3 weeks',
'yesterday',
'noon',
'midnight',
];

/** New v3.3.0 corpus: French localized modifiers through registry.modifiers */
const frCorpus: string[] = [
'vendredi prochain', // localized 'next Friday'
'lundi dernier', // localized 'last Monday'
'mercredi prochain', // localized 'next Wednesday'
'3 jours', // plain French numeric
'vendredi', // plain French weekday
'jeudi dernier', // localized 'last Thursday'
'mardi suivant', // alias: suivant => >
'dimanche prochain', // localized 'next Sunday'
];

// ── Benchmark helpers ───────────────────────────────────────────────────────

function timedRun(label: string, data: string[], iterations: number, tempoOptions: any) {
// warm-up
for (const d of data) new Tempo(d, { catch: true, ...tempoOptions });

let success = 0, failure = 0;
const startHeap = process.memoryUsage().heapUsed;
const start = performance.now();

for (let i = 0; i < iterations; i++) {
for (const d of data) {
const t = new Tempo(d, { catch: true, ...tempoOptions });
if (t.isValid) success++; else failure++;
}
}

const elapsed = performance.now() - start;
const endHeap = process.memoryUsage().heapUsed;
const ops = iterations * data.length;

return {
label,
totalTimeMs: Number(elapsed.toFixed(2)),
opsPerSec: Math.round(ops / (elapsed / 1000)),
microSecPerOp: Number(((elapsed * 1000) / ops).toFixed(2)),
successCount: success,
failureCount: failure,
successRate: ((success / ops) * 100).toFixed(1) + '%',
heapDeltaMb: Number(((endHeap - startHeap) / 1024 / 1024).toFixed(2)),
};
}

// ── Run ──────────────────────────────────────────────────────────────────────

const ITERATIONS = 500;

console.log(`\n⏱ Tempo v3.3.0 Post-Refactor Benchmark (${ITERATIONS} iterations × corpus size)\n`);

// A. Baseline — stock config, base corpus (mirrors pre-refactor run)
Tempo.init({ debug: 0, catch: true, timeZone: 'UTC' });
const baseline = timedRun('A. Baseline (stock, 20-entry corpus)', baseCorpus, ITERATIONS, { timeZone: 'UTC' });

// B. Baseline with all three Tempo modes using BenchmarkModule
Tempo.init({ debug: 0, catch: true, timeZone: 'UTC' });
const moduleResults = BenchmarkModule.run(Tempo, {
data: baseCorpus,
iterations: ITERATIONS,
modes: ['auto', 'defer', 'strict'],
baseline: true,
});

// C. Localized modifier corpus — exercises the new registry.modifiers code path
const localizedConfig = {
locale: 'fr-FR',
debug: 0,
catch: true,
timeZone: 'UTC',
registry: {
modifiers: {
'>': ['prochain', 'suivant'],
'<': ['dernier', 'passé'],
'=': ['ce', 'cette'],
}
}
};
Tempo.init(localizedConfig as any);
const localized = timedRun('C. Localized fr-FR modifiers (8-entry corpus)', frCorpus, ITERATIONS, {});

// ── Output ───────────────────────────────────────────────────────────────────

const output = {
runAt: new Date().toISOString(),
version: '3.3.0',
iterations: ITERATIONS,
baselineRaw: baseline,
moduleResults,
localizedModifiers: localized,
};

// Pretty console table
console.log('── Module-based comparison (matches pre-refactor format) ──');
BenchmarkModule.printTable(moduleResults);

console.log('\n── Baseline (manual, matches pre-refactor corpus exactly) ──');
console.table([{
'Engine': baseline.label,
'Total Time (ms)': baseline.totalTimeMs,
'µs / Op': baseline.microSecPerOp,
'ops/sec': baseline.opsPerSec,
'Success Rate': baseline.successRate,
'Heap Delta (MB)': baseline.heapDeltaMb,
}]);

console.log('\n── New: Localized modifier throughput ──');
console.table([{
'Engine': localized.label,
'Total Time (ms)': localized.totalTimeMs,
'µs / Op': localized.microSecPerOp,
'ops/sec': localized.opsPerSec,
'Success Rate': localized.successRate,
'Heap Delta (MB)': localized.heapDeltaMb,
}]);

// Save to file
const outPath = path.join(__dirname, 'benchmark-results-v3.3.0.json');
fs.writeFileSync(outPath, JSON.stringify(output, null, 2));
console.log(`\n✅ Results saved to ${outPath}\n`);
63 changes: 63 additions & 0 deletions packages/tempo/bench/benchmark-results-v3.3.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"runAt": "2026-06-21T03:09:07.682Z",
"version": "3.3.0",
"iterations": 500,
"baselineRaw": {
"label": "A. Baseline (stock, 20-entry corpus)",
"totalTimeMs": 15517.8,
"opsPerSec": 644,
"microSecPerOp": 1551.78,
"successCount": 10000,
"failureCount": 0,
"successRate": "100.0%",
"heapDeltaMb": 113.02
},
"moduleResults": [
{
"name": "Native Date",
"totalTimeMs": 2.92,
"microSecPerOp": 0.29,
"successCount": 2500,
"failureCount": 7500,
"successRate": "25.0%",
"heapUsedDeltaMb": "1.08"
},
{
"name": "Tempo (mode: auto)",
"totalTimeMs": 15524.01,
"microSecPerOp": 1552.4,
"successCount": 10000,
"failureCount": 0,
"successRate": "100.0%",
"heapUsedDeltaMb": "10.34"
},
{
"name": "Tempo (mode: defer)",
"totalTimeMs": 15089.25,
"microSecPerOp": 1508.93,
"successCount": 10000,
"failureCount": 0,
"successRate": "100.0%",
"heapUsedDeltaMb": "-22.66"
},
{
"name": "Tempo (mode: strict)",
"totalTimeMs": 15784.87,
"microSecPerOp": 1578.49,
"successCount": 10000,
"failureCount": 0,
"successRate": "100.0%",
"heapUsedDeltaMb": "56.53"
}
],
"localizedModifiers": {
"label": "C. Localized fr-FR modifiers (8-entry corpus)",
"totalTimeMs": 11330.81,
"opsPerSec": 353,
"microSecPerOp": 2832.7,
"successCount": 4000,
"failureCount": 0,
"successRate": "100.0%",
"heapDeltaMb": -9.07
}
}
45 changes: 45 additions & 0 deletions packages/tempo/bench/modifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import '@js-temporal/polyfill';
import { Tempo } from '../src/tempo.class.js';

const ITERATIONS = 100_000;

function runBenchmark(name: string, expression: string) {
Tempo.init({ debug: 0, catch: true, timeZone: 'UTC' });

// Warm-up pass for JIT stabilization
const warmupIterations = Math.min(ITERATIONS, 1000);
for (let i = 0; i < warmupIterations; i++)
new Tempo(expression, { catch: true });

const start = performance.now();
for (let i = 0; i < ITERATIONS; i++)
new Tempo(expression);

const end = performance.now();
const duration = end - start;
const opsPerSec = Math.round((ITERATIONS / duration) * 1000);
Comment thread
magmacomputing marked this conversation as resolved.

console.log(`[${name}]`);
console.log(` Expression : "${expression}"`);
console.log(` Duration : ${duration.toFixed(2)} ms`);
console.log(` Speed : ${opsPerSec.toLocaleString()} ops/sec`);
console.log('');
}

console.log('=============================================');
console.log(`Tempo Modifier Benchmark (${ITERATIONS.toLocaleString()} iterations)`);
console.log('=============================================\n');

// 1. Standard Prefix Modifier (Weekday)
runBenchmark('Prefix Weekday', 'next Friday');

// 2. Relative Offset (Ago/Hence)
runBenchmark('Relative Offset', '3 days ago');

// 3. Complex Expression
runBenchmark('Complex Expression', 'next Friday at 5:00pm');

// 4. Baseline (No modifier)
runBenchmark('Baseline Date', '2025-01-01');

console.log('Benchmark complete.\n');
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Criteria: Pre Tempo Modifiers refactor
=============================================
Tempo Modifier Benchmark (100,000 iterations)
=============================================

[Prefix Weekday]
Expression : "next Friday"
Duration : 1300.42 ms
Speed : 76,898 ops/sec

[Relative Offset]
Expression : "3 days ago"
Duration : 1265.60 ms
Speed : 79,014 ops/sec

[Complex Expression]
Expression : "next Friday at 5:00pm"
Duration : 1580.71 ms
Speed : 63,263 ops/sec

[Baseline Date]
Expression : "2025-01-01"
Duration : 1657.52 ms
Speed : 60,331 ops/sec

Benchmark complete.

Loading
Loading