Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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. Localized modifiers: registry.modifiers + locale:'fr-FR' active
* C. English relative strings through the new modifier path
Comment thread
magmacomputing marked this conversation as resolved.
Outdated
*/
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
}
}
38 changes: 38 additions & 0 deletions packages/tempo/bench/modifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import '@js-temporal/polyfill';
import { Tempo } from '../src/tempo.class.js';

const ITERATIONS = 100_000;

function runBenchmark(name: string, expression: string) {
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.

70 changes: 70 additions & 0 deletions packages/tempo/bin/tempo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// CLI Arguments
const args = process.argv.slice(2);
const command = args[0];

// The location of the templates relative to this bin script (bin/tempo.js -> template/)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templateDir = path.join(__dirname, '..', 'template');
const cwd = process.cwd();

// Helper to copy a file safely
function safelyCopy(sourceFile, targetFile, successMessage) {
const sourcePath = path.join(templateDir, sourceFile);
const targetPath = path.join(cwd, targetFile);

if (fs.existsSync(targetPath)) {
console.error(`\x1b[31m[Tempo Scaffold] Aborted.\x1b[0m File already exists at: ${targetPath}`);
console.error(`We refused to overwrite your existing file. If you want to scaffold a new one, please delete the existing file first.`);
process.exit(1);
}

if (!fs.existsSync(sourcePath)) {
console.error(`\x1b[31m[Tempo Scaffold] Error.\x1b[0m Could not find template file at: ${sourcePath}`);
process.exit(1);
}

try {
fs.copyFileSync(sourcePath, targetPath);
console.log(`\x1b[32m[Tempo Scaffold] Success!\x1b[0m ${successMessage}`);
} catch (err) {
console.error(`\x1b[31m[Tempo Scaffold] Failed to copy file:\x1b[0m ${err.message}`);
process.exit(1);
}
}

// Route commands
if (command === 'scaffold:config') {
safelyCopy('tempo.config.sample.ts', 'tempo.config.ts', 'Created tempo.config.ts');
} else if (command === 'scaffold:html') {
safelyCopy('index.sample.html', 'index.html', 'Created index.html boilerplate');
} else if (command === 'scaffold:all') {
const configExists = fs.existsSync(path.join(cwd, 'tempo.config.ts'));
const htmlExists = fs.existsSync(path.join(cwd, 'index.html'));
if (configExists || htmlExists) {
const existingFiles = [configExists ? 'tempo.config.ts' : null, htmlExists ? 'index.html' : null].filter(Boolean).join(' and ');
console.error(`\x1b[31m[Tempo Scaffold] Aborted.\x1b[0m File(s) already exist: ${existingFiles}`);
console.error(`We refused to overwrite your existing file(s). If you want to scaffold new ones, please delete the existing file(s) first.`);
process.exit(1);
}
safelyCopy('tempo.config.sample.ts', 'tempo.config.ts', 'Created tempo.config.ts');
safelyCopy('index.sample.html', 'index.html', 'Created index.html boilerplate');
Comment thread
magmacomputing marked this conversation as resolved.
} else {
console.log(`
\x1b[36m@magmacomputing/tempo\x1b[0m CLI

Available Commands:
scaffold:config Copies a sample tempo.config.ts into your current directory.
scaffold:html Copies a working HTML sandbox (index.html) into your current directory.
scaffold:all Copies both files.

Usage:
npx @magmacomputing/tempo <command>
`);
}
Loading
Loading