Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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).

## [2.11.0] - 2026-05-25

### Fixed
- **Open Core Build Fallback**: Updated the `build:resolve` script to gracefully fallback to Open Core licensing types when the proprietary `@core` module is unavailable, resolving CI build failures in public GitHub Actions environments.

## [2.10.0] - 2026-05-11

### Added
Expand Down
41 changes: 5 additions & 36 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "2.10.1",
"version": "2.11.0",
"private": true,
"description": "Magma Computing Monorepo",
"repository": {
Expand Down Expand Up @@ -43,4 +43,4 @@
"overrides": {
"esbuild": "^0.25.0"
}
}
}
5 changes: 5 additions & 0 deletions packages/library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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).

## [2.11.0] - 2026-05-25

### Added
- **Intl Utilities**: Added `getNF` (`Intl.NumberFormat`) memoization, along with `formatNumber` and `formatUnit` helper methods to `#library/international.library.js` to natively support plural-aware duration string generation.

## [2.8.0] - 2026-04-30

### Changed
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": "2.10.1",
"version": "2.11.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
23 changes: 23 additions & 0 deletions packages/library/src/common/international.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const getDTF = memoizeFunction((locale?: string) => {
return new Intl.DateTimeFormat(locale);
});

/** memoized helper for Intl.NumberFormat instances */
const getNF = memoizeFunction((locale?: string, options?: Intl.NumberFormatOptions) => {
return new Intl.NumberFormat(locale, options);
});

/**
* International Cookbook
* (using 'Intl' namespace objects)
Expand Down Expand Up @@ -53,6 +58,24 @@ export function formatList(list: string[], locale?: string, type: Intl.ListForma
}
}

/** return a localized number string */
export function formatNumber(value: number, locale?: string, options?: Intl.NumberFormatOptions) {
try {
return getNF(locale, options).format(value);
} catch (e) {
return value.toString();
}
}

/** return a localized unit string (e.g., '2 days') */
export function formatUnit(value: number, unit: string, locale?: string, unitDisplay: Intl.NumberFormatOptions['unitDisplay'] = 'long') {
try {
return getNF(locale, { style: 'unit', unit, unitDisplay }).format(value);
} catch (e) {
return `${value} ${unit}`;
}
}

/** try to infer hemisphere using the timezone's daylight-savings setting */
export function getHemisphere(timeZone: string = getDateTimeFormat().timeZone) {
try {
Expand Down
3 changes: 2 additions & 1 deletion packages/tempo/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export default defineConfig({
{ text: 'Parse Planner', link: '/doc/tempo.planner' },
{ text: 'Regional Parsing (MDY)', link: '/doc/tempo.month-day' },
{ text: 'Smart Formatting', link: '/doc/tempo.format' },
{ text: 'Layout Patterns', link: '/doc/tempo.layout' }
{ text: 'Layout Patterns', link: '/doc/tempo.layout' },
{ text: 'Duration Logic', link: '/doc/tempo.duration' }
]
},
{
Expand Down
10 changes: 10 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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).

## [2.11.0] - 2026-05-25

### Added
- **Duration Mathematics**: Introduced the `.balance()` method to `Tempo.Duration` objects to allow intelligent mathematical roll-up of duration units (e.g., converting 365 days into 1 year), with support for both strict calendar math and nominal overrides (`{ nominal: true }`).
- **Duration Formatting**: Introduced the `.format()` method to `Tempo.Duration` objects. This uses a shared, memoized `#library` implementation of `Intl.NumberFormat` to generate highly localized, plural-aware duration strings with excellent performance and robust cross-environment execution (no `navigator` dependencies).
- **Cascading Configuration**: Added `numberFormat` to the `IntlOptions` interface, allowing developers to set global formatting defaults (like `unitDisplay: 'short'`) via `Tempo.init()` that seamlessly cascade down to all `.format()` calls.

### Fixed
- **Parsing**: Resolved an engine edge-case where combining relative weekday modifiers with string-based period aliases (e.g., `<3 Wed afternoon`) would cause the parser to prematurely abort the relative offset, instead applying the period to the current system date.

## [2.10.1] - 2026-05-22

Expand Down
12 changes: 11 additions & 1 deletion packages/tempo/doc/releases/v2.x.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# 📜 Version 2.x History

## [v2.10.11] - 2026-05-23
## [v2.11.0] - 2026-05-25
### New Features

- **Duration Mathematics**: Introduced the `.balance()` method to `Tempo.Duration` objects to allow intelligent mathematical roll-up of duration units (e.g., converting 365 days into 1 year), with support for both strict calendar math and nominal overrides (`{ nominal: true }`).
- **Duration Formatting**: Introduced the `.format()` method to `Tempo.Duration` objects. This uses a shared, memoized `#library` implementation of `Intl.NumberFormat` to generate highly localized, plural-aware duration strings with excellent performance and robust cross-environment execution.
- **Cascading Configuration**: Added `numberFormat` to the `IntlOptions` interface, allowing developers to set global formatting defaults (like `unitDisplay: 'short'`) via `Tempo.init()` that seamlessly cascade down to all `.format()` calls.

### Fixed
- **Open Core Build Fallback**: Updated the `build:resolve` script to gracefully fallback to Open Core licensing types when the proprietary `@core` module is unavailable, resolving CI build failures in public GitHub Actions environments.

## [v2.10.1] - 2026-05-23
### New Features

- Added support for license key discovery via global browser context
Expand Down
69 changes: 19 additions & 50 deletions packages/tempo/doc/tempo-vs-temporal.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ t.format('{dd} {mmm} {yyyy}'); // Output: "24 Jan 2026"
t.fmt.date; // Output: "2026-01-24"
```

Tempo also provides a built-in **log-stamp** format for dropping a compact, sortable timestamp into a log entry:

```javascript
const ts = new Tempo('2026-05-20T13:55:19.623319620');
ts.fmt.logStamp; // → "20260520T135519.623319620"
// ^^^^^^^^ ^^^^^^ ^^^^^^^^^
// date time sub-seconds (nanosecond precision)
```

This format (`Tempo.FORMAT.logStamp`) is globally configurable via `Tempo.init`:
```javascript
Tempo.init({ formats: { logStamp: '{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}' } });
new Tempo('2026-05-20T13:55:19.623319620').fmt.logStamp; // → "2026-05-20 13:55:19"
```

### 3. Business Logic & Complex Terms

Native Temporal deals strictly with standard calendar units (days, months, years). If you need to map a date to domain-specific business logic (like a fiscal quarter or a meteorological season), you have to write and maintain your own math utilities.
Expand Down Expand Up @@ -80,20 +95,7 @@ For more information on adding your own business logic, see the [Terms Guide](te

Calculating the difference between two dates in native Temporal is mathematically sound, but it strictly returns a `Temporal.Duration` object. Tempo gives you the flexibility to return a `Duration` object, a precise floating-point number, or a human-readable string.

Tempo also provides a built-in **log-stamp** format for dropping a compact, sortable timestamp into a log entry:

```javascript
const t = new Tempo('2026-05-20T13:55:19.623319620');
t.fmt.logStamp; // → "20260520T135519.623319620"
// ^^^^^^^^ ^^^^^^ ^^^^^^^^^
// date time sub-seconds (nanosecond precision)
```

This format (`Tempo.FORMAT.logStamp`) is configurable via `Tempo.init`:
```javascript
Tempo.init({ formats: { logStamp: '{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}' } });
new Tempo('2026-05-20T13:55:19.623319620').fmt.logStamp; // → "2026-05-20 13:55:19"
```

**Native Temporal 🐢**
```javascript
Expand All @@ -103,42 +105,9 @@ const duration = now.until(target); // Returns a complex Duration object
```

**Tempo 🚀**
Tempo understands natural language targets and can format the resulting difference flexibly. `t.until()` and `t.since()` have distinct return types:

```javascript
const t = new Tempo('2026-05-20T11:35:33');
const t2 = new Tempo('2026-05-22T11:35:33');

// t.until(target, unit) → number
t.until('afternoon', 'minutes'); // → 84.45 (fractional: 'afternoon' has a fixed time, e.g. 13:00)
t.until('xmas', 'days'); // → 219 (whole number — see note below)
t.until('xmas', 'weeks'); // → 31.28 (fractional — weeks don't divide evenly into days)
t.until(t2, 'hours'); // → 48 (targets can also be other Tempo instances)
t.until(Temporal.Now.plainDateISO(), 'days'); // → 1 (same day = 1 day)
Tempo understands natural language targets and provides multiple ways to measure and format the resulting elapsed time.

// t.until(target) → Tempo.Duration object (with .iso, .years, .days, … fields)
t.until('xmas'); // → { iso: "P219DT0H0M0S", years: 0, months: 7, days: 4, ... }

// t.since(target, unit) → human-readable string via Intl.RelativeTimeFormat
t.since('yesterday', 'days'); // → "1d ago"

// t.since(target) → ISO 8601 Duration string
t.since('yesterday afternoon'); // → "-P1DT9H32M19.402S"
```
- `t.until()` returns a highly functional Extended Data Object (EDO) or a precise decimal number depending on your arguments.
- `t.since()` leverages `Intl.RelativeTimeFormat` to instantly return human-readable relative strings (like "3 days ago").

> **💡 Date-only targets inherit the current time.**
> When a target resolves to a **date without a time component** (e.g. `'xmas'`, `'tomorrow'`, `'next friday'`),
> Tempo copies the current time-of-day from the anchor into the target. This means:
>
> - `t.until('xmas', 'days')` → a **whole number** — the time components cancel out exactly.
> - `t.until('xmas', 'hours')` → a **whole number** — same reason.
> - `t.until('xmas', 'weeks')` → **fractional** — 219 days does not divide evenly into weeks.
>
> This matches natural-language intuition: *"How many days until Christmas?"* expects `219`, not `219.43`.
> If you need the precise elapsed duration including sub-day components, omit the unit:
> ```javascript
> t.until('xmas'); // → { days: 219, hours: 0, minutes: 0, seconds: 0, ... }
> ```
>
> Targets with an **explicit time** (e.g. `'afternoon'`, `'9am'`, `'2026-12-25T08:00'`) always produce
> fractional values because the target time differs from the anchor's current time-of-day.
For comprehensive examples of duration mathematics, intelligent balancing, and localization formatting, read the dedicated **[Duration Logic Guide](tempo.duration.md)**.
1 change: 1 addition & 0 deletions packages/tempo/doc/tempo.config.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Tempo.init({
| `timeStamp`| `'ss' \| 'ms' \| 'us' \| 'ns'` | `'ms'` | Precision for numeric inputs and the `.ts` property. |
| `sphere` | `'north' \| 'south'`| Auto-inferred | Hemisphere for seasonal plugins. |
| `relativeTime` | `RelativeTime` | `undefined` | Relative time formatting configuration (grouped). |
| `intl` | `IntlOptions` | `undefined` | Internationalization configuration grouping both `relativeTime` and `numberFormat`. |
| `event` | `Record<string, string \| Function>` | Built-in aliases | Custom date aliases merged into the event registry. |
| `period` | `Record<string, string \| Function>` | Built-in aliases | Custom time aliases merged into the period registry. |
| `snippet` | `Record<string, string \| RegExp>` | Built-in snippets | Custom snippet patterns used to compose parse layouts. |
Expand Down
Loading