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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "3.9.1",
"version": "3.9.3",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "3.9.1",
"version": "3.9.3",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
12 changes: 7 additions & 5 deletions packages/plugins/ticker/doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Tempo.init({
plugins: [TickerPlugin]
});

// You can now access Ticker-based execution loops through the Tempo API:
// You can access Ticker-based execution loops through the Tempo API:
const ticker = Tempo.ticker({ seconds: 1 });
```

Expand Down Expand Up @@ -66,12 +66,14 @@ await using quarterly = Tempo.ticker({ '#quarter': 1 });
```

### 2. Term-Based Intervals
Ticker intervals can now be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts.
Ticker intervals can be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts.

> **Snapping vs Shifting:** Use directional shorthands (like `>`) to snap pulses exactly to the **boundaries** of the term (e.g., the very start of the morning). Using numeric values (like `1`) performs a relative shift, which preserves your current time-offset into the next period (e.g. two hours into a time-period will always be two hours into the next time-period).

```typescript
// Pulse at the start of every 'morning', 'afternoon', etc.
using shiftTicker = Tempo.ticker({ '#period': 1 }, (t) => {
console.log(`New period started: ${t.term.per}`);
// Snap and pulse exactly at the start of every 'morning', 'afternoon', etc.
using shiftTicker = Tempo.ticker({ '#timeOfDay': '>' }, (t) => {
console.log(`New period started: ${t.term.tod}`);
});
```

Expand Down
5 changes: 3 additions & 2 deletions packages/tempo/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,21 @@ export default defineConfig({
text: 'Core Concepts',
items: [
{ text: 'Configuration', link: '/doc/2-core-concepts/tempo.config' },
{ text: 'Registries', link: '/doc/2-core-concepts/tempo.registry' },
{ text: 'Core Getters', link: '/doc/2-core-concepts/tempo.getters' },
{ text: 'Smart Parsing', link: '/doc/2-core-concepts/tempo.parse' },
{ text: 'Smart Formatting', link: '/doc/2-core-concepts/tempo.format' },
{ text: 'Layout Patterns', link: '/doc/2-core-concepts/tempo.layout' },
{ text: 'Duration Logic', link: '/doc/2-core-concepts/tempo.duration' },
{ text: 'Mutation & Math', link: '/doc/2-core-concepts/tempo.mutate' },
{ text: 'Intervals', link: '/doc/2-core-concepts/tempo.interval' }
]
},
{
text: 'Extending Tempo',
items: [
{ text: 'Modules', link: '/doc/3-extending-tempo/tempo.modularity' },
{ text: 'Registries', link: '/doc/3-extending-tempo/tempo.registry' },
{ text: 'Plugins', link: '/doc/3-extending-tempo/tempo.plugin' },
{ text: 'Layout Patterns', link: '/doc/3-extending-tempo/tempo.layout' },
{ text: 'Terms', link: '/doc/3-extending-tempo/tempo.term' },
{ text: 'Namespaces', link: '/doc/3-extending-tempo/tempo.namespace' },
{ text: 'Creating Custom Plugins', link: '/doc/3-extending-tempo/tempo.extension' },
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/.vitepress/theme/components/CatalogList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const copyInstall = (pkgName: string) => {
position: absolute;
top: -10px;
right: 15px;
background: var(--vp-c-brand);
background: var(--vp-c-danger-1, #ef4444);
color: white;
padding: 2px 8px;
border-radius: 12px;
Expand Down
13 changes: 13 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ 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.9.3] - 2026-07-18

### Added
- **Core Concepts Documentation**: Added a new `Mutation & Math` (`tempo.mutate.md`) guide to the Core Concepts section to explicitly document `.add()` and `.set()` immutability, chainability, and the design decision to omit a dedicated `.subtract()` method.

### Changed
- **Documentation Architecture**: Relocated `tempo.registry.md` and `tempo.layout.md` from Core Concepts to Extending Tempo to better reflect their advanced, extensibility-focused usage patterns.
- **README Updates**: Added `AstroPlugin` initialization examples, an "Ultra Lightweight" architecture bullet, and restructured the ecosystem markdown table for cleaner VitePress rendering.

### Fixed
- **Format Leading Zeros**: Fixed a data-corruption bug in `Tempo.format()` where numeric-looking tokens (e.g., `{dd}`, `{mm}`) were implicitly cast to numbers, stripping their leading zeros. The `.format()` method now strictly adheres to a string-only return contract, ensuring zero-padded tokens retain their exact formatting. Removed `NumericPattern` and `BigIntPattern` complexity and simplified internal casting logic.
- **VitePress UI**: Fixed a CSS variable reference in the `CatalogList.vue` component, mapping the install badge background to `--vp-c-danger-1` instead of `--vp-c-brand`.

## [3.9.1] - 2026-07-17

### Added
Expand Down
21 changes: 16 additions & 5 deletions packages/tempo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
## ⚡ Quick Start
```javascript
import { Tempo } from '@magmacomputing/tempo';
import { AstroPlugin } from '@magmacomputing/tempo-plugin-astro';

// 🔌 Instantly Extensible (with deterministic defaults)
Tempo.init({
plugins: [AstroPlugin],
timeZone: 'America/New_York'
});

// 🎯 Natural Language Parsing (Deterministic anchor)
const event = new Tempo('next Friday 3pm', { anchor: '2026-10-15' });
Expand All @@ -39,6 +46,9 @@ console.log(diff.iso); // P2M2D

// 📝 Beautiful Formatting
console.log(event.format('{mon} {dd:ord}, {yyyy}')); // October 23rd, 2026

// 🌌 Domain Logic (via Plugin)
console.log(event.term.astronomy.season); // 'Autumn'
```

---
Expand Down Expand Up @@ -105,6 +115,7 @@ While the native Temporal API gives you perfect primitives (`ZonedDateTime`, `Pl
* **⚡ Zero-Cost Parsing**: Lazy evaluation and smart matching ensure instantiation overhead is near-zero.
* **🛡️ Monorepo Resilient**: Built for stability in complex environments with proxy-protected registries.
* **📦 Tree-Shakable**: Keep your bundle light. Only load what you need—from Fiscal calendars to high-performance Tickers.
* **🪶 Ultra Lightweight**: Tempo itself is incredibly lean. While the required `Temporal` polyfill adds weight today, it can be dropped entirely the moment JavaScript environments natively adopt the Stage 4 standard.

---

Expand All @@ -121,11 +132,11 @@ For a deeper dive into the API, architecture, and advanced features:

Tempo is the core library, but the ecosystem extends further:

| Package | Description |
| :--- | :--- |
| **[`@magmacomputing/tempo`](https://www.npmjs.com/package/@magmacomputing/tempo)** | Core library — parsing, formatting, natural-language engine |
| **[`@magmacomputing/tempo-fns`](https://www.npmjs.com/package/@magmacomputing/tempo-fns)** | Pure functional utilities built on native Temporal & Tempo — tree-shakeable helpers for calendars, business logic, and scheduling   [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) |
| **[`@magmacomputing/tempo-plugin-*`](https://www.npmjs.com/search?q=%40magmacomputing%2Ftempo-plugin)** | Premium & community plugins — Ticker, Astro, Finance, Sync, Snap and more   [![Ecosystem](https://img.shields.io/badge/Browse-Plugin%20Ecosystem-blueviolet?logo=npm&style=flat-square)](https://magmacomputing.github.io/magma/doc/3-extending-tempo/ecosystem) |
| Package | Description | Resources |
| :--- | :--- | :--- |
| **[`@magmacomputing/tempo`](https://www.npmjs.com/package/@magmacomputing/tempo)** | Core library — parsing, formatting, natural-language engine | [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/) |
| **[`@magmacomputing/tempo-fns`](https://www.npmjs.com/package/@magmacomputing/tempo-fns)** | Pure functional utilities built on native Temporal & Tempo — tree-shakeable helpers | [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) |
| **[`@magmacomputing/tempo-plugin-*`](https://www.npmjs.com/search?q=%40magmacomputing%2Ftempo-plugin)** | Premium & community plugins — Ticker, Astro, Finance, Sync, Snap and more | [![Ecosystem](https://img.shields.io/badge/Browse-Plugin%20Ecosystem-blueviolet?logo=npm&style=flat-square)](https://magmacomputing.github.io/magma/doc/3-extending-tempo/ecosystem) |


---
Expand Down
84 changes: 84 additions & 0 deletions packages/tempo/doc/2-core-concepts/tempo.mutate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Mutation & Math

Tempo's API for modifying instances is intentionally microscopic. Rather than bloating the prototype with dozens of distinct methods (`.add()`, `.subtract()`, `.startOf()`, etc.), Tempo provides unified utilities that natively support intuitive shorthand strings.

> [!IMPORTANT] Immutability & Chainability
> All mutation methods in Tempo (`.add()`, `.set()`) are strictly **immutable**. They never modify the original instance. Instead, they evaluate the change and return a **new `Tempo` instance**, ensuring absolute safety and allowing for predictable method chaining.

## The `.add()` Method

The `.add()` method returns a new `Tempo` instance shifted by a specific amount.

```typescript
const t = tempo();
t.add({ days: 5 }); // Adds 5 days
```

### Where is `.subtract()`?

> [!NOTE] Design Choice
> **Where is `.subtract()`?**
> Tempo keeps its core API intentionally microscopic. Because `.add()` natively supports negative durations and Tempo's Slick math provides directional operators (e.g., `t.add('<5d')` or `t.add({ days: -5 })`), a separate `.subtract()` method is mathematically redundant. We chose a smaller bundle size over duplicate methods.

You can subtract time simply by using negative values:

```typescript
t.add({ days: -5 }); // Subtracts 5 days
```

Or using **[Slick Math](../4-advanced-reference/tempo.shorthand.md)**:

```typescript
t.add('>5d'); // Adds 5 days
t.add('<5d'); // Subtracts 5 days
```

## The `.set()` Method

While `.add()` *shifts* a date, the `.set()` method *replaces* components.

```typescript
t.set({ year: 2026, month: 1 }); // Sets to January 2026
```

### Navigating to Boundaries

[Slick Math](../4-advanced-reference/tempo.shorthand.md) also works inside `.set()` for boundary navigation:

```typescript
t.set('start.month'); // Start of the current month
t.set('end.year'); // End of the current year
```

## Chainability

Because all mutations return a new instance, you can safely chain `.add()` and `.set()` methods together to perform complex temporal logic in a single, readable line.

```typescript
const endOfQ1 = t
.set('start.year') // Snap to January 1st
.add('>3mm') // Shift forward 3 months (to April 1st)
.set('end.month'); // Snap to April 30th at 23:59:59.999
```

## Relational vs. Navigation Shifting

When using custom terminology plugins (like Fiscal Quarters or Seasons), Tempo provides two distinct shorthand styles for mutation:

### 1. Navigation Mode (String)
Use a string to **jump** to a specific boundary. This relies on chronological momentum.

```typescript
t.set('#qtr.>q1'); // Snaps to the start of the next Q1
t.add('#timeOfDay.>afternoon'); // Jumps to the start of the next Afternoon
```

### 2. Relational Mode (Object)
Use an object to **shift** by a specific semantic step while preserving your relative position in the cycle.

```typescript
t.add({ '#qtr': 1 }); // Shift forward 1 quarter, preserving progress
```
If you are 20 days into Q1, relational shifting will put you exactly 20 days into Q2.

👉 **Learn More:** For deeper details on cycle preservation and directional operators, see the [Shorthand Engine Reference](../4-advanced-reference/tempo.shorthand.md).
2 changes: 1 addition & 1 deletion packages/tempo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
"version": "3.9.1",
"version": "3.9.3",
"engines": {
"node": ">=20.0.0"
},
Expand Down
32 changes: 14 additions & 18 deletions packages/tempo/src/module/module.format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,35 @@ import '#library/temporal.polyfill.js';
import { pad, toTitleCase } from '#library/string.library.js';
import { deepMerge } from '#library/object.library.js';
import { suffix } from '#library/number.library.js';
import { ifNumeric } from '#library/coercion.library.js';
import { isString, isObject, isZonedDateTime, isInstant, isPlainDate, isPlainDateTime, isUndefined, isDefined, isFunction } from '#library/assertion.library.js';
import { formatDayPeriod, getDTF, getPR, getISOWeekOfYear } from '#library/international.library.js';
import { delegator } from '#library/proxy.library.js';

import { isTempo, enums, Match, getRuntime, NumericPattern, BigIntPattern, hasOwn, $Internal } from '#tempo/support';
import { isTempo, enums, Match, getRuntime, hasOwn, $Internal } from '#tempo/support';
import { defineInterpreterModule } from '../plugin/plugin.util.js';
import { findTermPlugin } from '../plugin/term/term.util.js';
import type { FormatOptions, ValidateFormat, TempoFormatTokens } from '../tempo.type.js';
import type { Tempo } from '../tempo.class.js';

declare module '../tempo.class.js' {
interface Tempo {
/** applies a format to the instance. */ format(options: import('../tempo.type.js').FormatOptions): string;
/** applies a format to the instance. */ format(fmt: BigIntPattern, options?: any): bigint;
/** applies a format to the instance. */ format(fmt: NumericPattern, options?: any): number;
/** applies a format to the instance. */ format(options: FormatOptions): string;

/**
* Applies a format to the instance.
* Format strings are validated at compile time — any unrecognised `{token}`
* will produce an IDE error showing the bad token name.
* @see {@link import('../tempo.type.js').TempoFormatTokens} to extend the token set.
* @see {@link TempoFormatTokens} to extend the token set.
*/
format<S extends string>(
fmt: string extends S
? S // variable string — no validation, accept as-is
: string extends import('../tempo.type.js').ValidateFormat<S>
? S // ValidateFormat<S> is `string` → all tokens valid, accept
: import('../tempo.type.js').ValidateFormat<S>, // ValidateFormat<S> is an error literal → mismatch forces IDE error
? S // variable string — no validation, accept as-is
: string extends ValidateFormat<S>
? S // ValidateFormat<S> is `string` → all tokens valid, accept
: ValidateFormat<S>, // ValidateFormat<S> is an error literal → mismatch forces IDE error
options?: any
): string | number | bigint;
/** applies a format to the instance (zero-argument — returns a pre-built format proxy). */ format(): string | number | bigint;
): string;
/** applies a format to the instance (zero-argument — returns a pre-built format proxy). */ format(): string;
}
}

Expand All @@ -48,9 +47,8 @@ declare module '../tempo.class.js' {
* const stamp = format().logStamp; // defaults to 'Now'
*/
export function format(obj?: any): any;
export function format(obj: any, options: import('../tempo.type.js').FormatOptions): string;
export function format(obj: any, fmt: BigIntPattern, options?: any): bigint;
export function format(obj: any, fmt: NumericPattern, options?: any): number;
export function format(obj: any, options: FormatOptions): string;

export function format(obj: any, fmt: string | symbol, options?: any): string;
export function format(obj?: any, fmt?: any, options?: any): any {
const state = getRuntime().state;
Expand Down Expand Up @@ -390,9 +388,7 @@ export function format(obj?: any, fmt?: any, options?: any): any {
return res;
});

const tokens = template.match(new RegExp(Match.formatBraces, 'g'));
const isNumericOutput = BigIntPattern.includes(template as any) || NumericPattern.includes(template as any) || (tokens && tokens.length > 1 && /^[0-9]+$/.test(result));
return (isNumericOutput ? ifNumeric(result, true) : result) as any;
return result as any;
}

/**
Expand Down
26 changes: 5 additions & 21 deletions packages/tempo/src/support/support.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,33 +177,17 @@ export const DURATIONS = enumify(STATE.DURATIONS, false);
export type DURATIONS = KeyOf<typeof DURATIONS>

/** common format aliases */
export const FORMAT = looseIndex<string, string | number>()(enumify(STATE.FORMAT, false));
export const FORMAT = looseIndex<string, string>()(enumify(STATE.FORMAT, false));
export type FORMAT = typeof FORMAT;
export type Format = LooseUnion<KeyOf<typeof FORMAT> & string>

/** patterns that return a number */
export const NumericPattern = ['{yyyy}{wy}', '{yyyy}{mm}', '{yyyy}{mm}{dd}', '{yywy}', '{yw}{wy}', '{yw}', '{ymd}', '{ymd6}', '{hms}', '{ff}', '{dmy}', '{dmy6}', '{mdy}', '{mdy6}'] as const;
export type NumericPattern = typeof NumericPattern[number]

/** patterns that return a bigint */
export const BigIntPattern = ['{nano}'] as const;
export type BigIntPattern = typeof BigIntPattern[number]

/** pre-configured format strings */
export type OwnFormat = Mutable<OwnOf<typeof FORMAT>>

/** mapping of format names to instance-resolutions (string | number) */
export type FormatType<K extends PropertyKey> = K extends BigIntPattern ? bigint : K extends keyof OwnFormat
? (OwnFormat[K] extends NumericPattern ? number : string)
: K extends NumericPattern ? number : string | number | bigint;

/** mapping of format names to instance-resolutions (string | number) */
/** mapping of format names to instance-resolutions */
export type Formats = {
[K in keyof OwnFormat]: FormatType<K>;
} & Record<string, string | number>;
[K in keyof OwnOf<typeof FORMAT>]: string;
} & Record<string, string>;

/** Enum registry of format strings */
export type FormatEnum = Enum.wrap<OwnFormat & Record<string, string | number>>;
export type FormatEnum = Enum.wrap<Formats>;

export const LIMIT = proxify(STATE.LIMIT, true, false);

Expand Down
2 changes: 0 additions & 2 deletions packages/tempo/src/support/support.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ export {
PARSE,
MONTH_DAY,
LICENSE,
NumericPattern,
BigIntPattern,
} from './support.enum.js';

export { markConfig } from '#library/symbol.library.js';
Expand Down
Loading
Loading