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
10 changes: 5 additions & 5 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": "3.1.0",
"version": "3.1.1",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand Down Expand Up @@ -59,4 +59,4 @@
"edgedriver@6.3.0": true,
"geckodriver@6.1.0": true
}
}
}
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.1.0",
"version": "3.1.1",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
19 changes: 14 additions & 5 deletions packages/library/src/common/coercion.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,23 @@ export function asInteger<T extends string | number | bigint>(str?: T) {
/** return as Number if possible, else original String */
export const ifNumeric = (str: string | number | bigint, stripZero = false) => {
switch (true) {
case isInteger(str): // BigInt → Number
return Number(str);
case isInteger(str): {
const big = str as bigint;
if (big > BigInt(Number.MAX_SAFE_INTEGER) || big < BigInt(Number.MIN_SAFE_INTEGER)) return big;
return Number(big);
}

case isNumber(str): // Number → as-is
case isNumber(str):
return str;

case isNumeric(str) && (!str?.toString().startsWith('0') || stripZero):
return asNumber(str); // numeric String → Number
case isNumeric(str) && (!str?.toString().startsWith('0') || stripZero): {
const numStr = String(str);
if (/^-?[0-9]+$/.test(numStr)) {
const big = BigInt(numStr);
if (big > BigInt(Number.MAX_SAFE_INTEGER) || big < BigInt(Number.MIN_SAFE_INTEGER)) return big;
}
return asNumber(str);
}

default:
return str as string; // non-numeric String → as-is
Expand Down
2 changes: 1 addition & 1 deletion packages/library/src/server/file.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class File {
return targetPath;
}

static read = (file: string): Promise<string | number> => new Promise<string | number>((resolve, reject) => {
static read = (file: string): Promise<string | number | bigint> => new Promise<string | number | bigint>((resolve, reject) => {
try {
const target = File._resolvePath(file);
fs.readFile(target, File.encoding, (err, data) => {
Expand Down
11 changes: 11 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ 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.1.1] - 2026-06-14

### Added
- **Tokens & Modifiers**: Added `{h12}` for 12-hour clock output with automatic meridiem injection, `{cal}` for calendar system resolution, and the `:raw` modifier for stripping leading zeros and suppressing auto-meridiem.
- **Dynamic Format Options**: The `.format()` method on `Tempo` instances now natively accepts an optional configuration object (e.g., `{ locale: 'fr-FR' }`) as its second argument, aligning its signature with native `Intl` overrides.

### Changed
- **Automatic Meridiem Injection**: Rewritten to derive modifiers directly from the matched `{h12}` or `{HH}` token, properly applying casing and placement.
- **Zero-Padded Documentation**: Refined format token documentation to explicitly refer to fixed-length representations as "Zero-padded" (e.g., "Zero-padded Month"), clarifying the exact behavior of the `:raw` modifier.
- **Strict ISO Vocabulary**: Updated documentation for `{dow}`, `{ww}`, and `{yw}` to explicitly designate them as ISO standards, removing ambiguity with native JavaScript `Date` offsets.

Comment thread
magmacomputing marked this conversation as resolved.
## [3.1.0] - 2026-06-13

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const diff = event.until('2026-12-25');
console.log(diff.iso); // P2M2D

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

---
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/doc/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The easiest way to use Tempo natively in the browser is via the pre-optimized ES
import { Tempo } from '@magmacomputing/tempo';

const t = new Tempo('tomorrow');
console.log(t.format('{mon} {day}'));
console.log(t.format('{mon} {dd:ord}'));
</script>
```

Expand Down
7 changes: 4 additions & 3 deletions packages/tempo/doc/tempo.cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,16 +398,17 @@ You can extend the built-in registries (e.g. `formats`, `locales`) and toggle fo

```typescript
Tempo.init({
locale: 'fr-FR',
registry: {
formats: {
'customDate': '{yyyy}-{mm}-{dd} {HH}:{mi}'
'customDate': '{dd} {mon:upper} {yyyy} - {h12}:{mi}'
}
},
format: {
localize: true // Enable automatic localized number formatting
localize: true // Automatically applies the :locale modifier to {mon}
}
});

const t = new Tempo('2026-06-03 14:30');
console.log(t.format('customDate')); // "2026-06-03 14:30"
console.log(t.format('customDate')); // "03 JUIN 2026 - 02:30pm"
```
94 changes: 70 additions & 24 deletions packages/tempo/doc/tempo.format.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ If you have a native `Temporal.ZonedDateTime` and want to format it using Tempo'
import { format } from '@magmacomputing/tempo/format';

const zdt = Temporal.Now.zonedDateTimeISO();
const str = format(zdt, '{mon} {day}, {yyyy}');
const str = format(zdt, '{mon} {dd:ord}, {yyyy}');

console.log(str); // e.g., "October 24, 2026"
console.log(str); // e.g., "October 24th, 2026"
```

::: warning
Expand Down Expand Up @@ -45,7 +45,7 @@ Tempo comes with several pre-configured format aliases. You can also define your
```typescript
Tempo.init({
formats: {
'fancy': '{mon} the {dd}th day of {yyyy}'
'fancy': '{mon} the {dd:ord} day of {yyyy}'
}
});

Expand Down Expand Up @@ -78,54 +78,100 @@ Tempo.extend(FormatModule);

## 🔠 Supported Tokens

> [!NOTE]
> **Tempo is heavily opinionated.** To provide maximum predictability and eliminate common timezone or regional bugs, Tempo strictly defaults to **ISO-8601 standards**. This means weeks always start on Monday (`1`), and mathematical bounds (like week-of-year and year-of-week calculations) adhere to the rigorous ISO specification.

| Token | Description | Example |
| :--- | :--- | :--- |
| `{yyyy}` | 4-digit Year | `2026` |
| `{yy}` | 2-digit Year | `26` |
| `{yw}` | Year of Week (ISO) | `2026` |
| `{yyww}` | Year & Week (ISO) | `202617` |
| `{yw}` | ISO Year of Week | `2026` |
| `{yyww}` | ISO Year & Week | `202617` |
| `{mon}` | Full Month Name | `October` |
| `{mmm}` | Short Month Name | `Oct` |
| `{mm}` | 2-digit Month | `10` |
| `{MM}` | Ordinal Month | `10th` |
| `{dd}` | 2-digit Day | `24` |
| `{day}` | Unpadded Day | `24` (or `9`) |
| `{DAY}` | Ordinal Day | `24th` (or `9th`) |
| `{mm}` | Zero-padded Month | `10` |
| `{dd}` | Zero-padded Day | `24` |
| `{wkd}` | Full Weekday Name | `Saturday` |
| `{www}` | Short Weekday Name | `Sat` |
| `{dow}` | Day of Week (1-7) | `6` |
| `{ww}` | Week of Year | `43` |
| `{WW}` | Ordinal Week of Year | `43rd` |
| `{hh}` | 2-digit Hour (24h) | `15` |
| `{HH}` | 2-digit Hour (12h) | `03` |
| `{dow}` | ISO Day of Week (1=Mon, 7=Sun) | `6` |
| `{ww}` | Zero-padded ISO Week of Year | `43` |
| `{hh}` | Zero-padded Hour (24h) | `15` |
| `{h12}` | Zero-padded Hour (12h) plus meridiem | `03pm` |
| `{mer}` | am/pm marker | `pm` |
| `{MER}` | AM/PM marker | `PM` |
| `{mi}` | Minutes | `30` |
| `{ss}` | Seconds | `45` |
| `{mi}` | Zero-padded Minutes | `30` |
| `{ss}` | Zero-padded Seconds | `45` |
| `{dmy}` | Compact Date (ddmmyyyy) | `24102026` |
| `{mdy}` | Compact Date (mmddyyyy) | `10242026` |
| `{ymd}` | Compact Date (yyyymmdd) | `20261024` |
| `{hms}` | Compact Time (24h) | `153045` |
| `{ms}` | 3-digit Milliseconds | `123` |
| `{us}` | 3-digit Microseconds | `456` |
| `{ns}` | 3-digit Nanoseconds | `789` |
| `{ms}` | Zero-padded Milliseconds (3-digit) | `123` |
| `{us}` | Zero-padded Microseconds (3-digit) | `456` |
| `{ns}` | Zero-padded Nanoseconds (3-digit) | `789` |
| `{ff}` | Fractional Seconds | `123456789` |
| `{ts}` | Unix Timestamp | `1792843200000` |
| `{nano}` | Nanosecond Timestamp | `1792843200000000000` |
| `{tz}` | Time Zone ID | `Australia/Sydney` |
| `{cal}` | Calendar System | `iso8601` |

### 🎛️ Token Modifiers
You can append modifiers to any token using a colon (`:`) to transform its output. Multiple modifiers can be chained together (e.g., `{mon:locale:title}`).

| Modifier | Target | Description | Example |
| :--- | :--- | :--- | :--- |
| `:raw` | Number | Unpadded number, no meridiem | `{hh:raw}` → `3` |
| `:ord` | Number | Appends an ordinal suffix (unpadded) | `{dd:ord}` → `24th` |
| `:upper` | String | Converts to uppercase | `{mer:upper}` → `PM` |
| `:lower` | String | Converts to lowercase | `{mon:lower}` → `october` |
| `:title` | String | Converts to titlecase | `{mon:locale:title}` → `Octobre` |
| `:locale` | String | Resolves term via localization dictionary | `{mon:locale}` → `octobre` |

### 🔄 Automatic Meridiem
If your format string contains `{HH}` (12-hour clock) but lacks a `{mer}` or `{MER}` token, Tempo will automatically append `{mer}` to the end of the last time component to ensure the time remains unambiguous.
If your format string contains `{h12}` (12-hour clock) but lacks a `{mer}` token, Tempo will automatically append a `{mer}` token with the same modifiers as the `{h12}` token after the last time component to ensure the time remains unambiguous.

*(If you explicitly want a 12-hour digit without an auto-appended meridiem, use the `:raw` modifier: `{h12:raw}`)*

> [!NOTE]
> **Why `{h12}`?** In most date libraries, `{hh}` means 12-hour and `{HH}` means 24-hour time. However, Tempo standardizes `{hh}` on the default 24-hour expectation (with `{h12}` serving as the specific 12-hour override). This keeps all token definitions, and their corresponding time getters, fully lowercase and semantic.

```typescript
t.format('{HH}:{mi}'); // "03:30pm" (auto-appended pm)
t.format('{h12}:{mi}'); // "03:30pm" (auto-append meridiem)
t.format('{h12:upper}:{mi}'); // "03:30PM" (auto-append meridiem)
t.format('{h12:raw}:{mi}'); // "3:30" (no meridiem added)
t.format('{h12:raw}:{mi} {mer}'); // "3:30 am" (blank + meridiem added manually)
```

### 🔢 Numeric Resolution
If your format string consists *only* of numeric tokens (e.g., `{yyyy}{mm}{dd}`), the `format()` function will return a **Number** instead of a string. This is useful for generating sortable keys or IDs.
If your format string consists *only* of numeric tokens (e.g., `{yyyy}{mm}{dd}`), the `format()` function will automatically coerce the output to a numeric primitive instead of a string. This is useful for generating sortable keys or IDs.

For standard lengths, it returns a **Number**. However, if the resulting value exceeds JavaScript's `Number.MAX_SAFE_INTEGER` (such as the `{nano}` token), Tempo safely upgrades the return type to a **BigInt** to prevent precision loss.

```typescript
// Standard Numeric Format -> Number
const key = t.format('{yyyy}{mm}{dd}');
console.log(typeof key); // "number"
console.log(key); // 20261024

// Large Numeric Format -> BigInt
const epoch = t.format('{nano}');
console.log(typeof epoch); // "bigint"
console.log(epoch); // 1792843200000000000n
```

### 📝 Common Formatting Examples
Here are a few real-world examples demonstrating how tokens and modifiers can be composed together to build readable sentences and structured strings.

```typescript
const t = new Tempo('2026-10-05T15:30:00');

// Injecting tokens directly into sentences
t.format('Today is the {dd:ord} of {mon:title}');
// "Today is the 5th of October"

// Building standard UI formats
t.format('{wkd}, {mmm} {dd:raw}, {yyyy} @ {h12}:{mi}');
// "Monday, Oct 5, 2026 @ 03:30pm"

// Forcing fully lowercase strings
t.format('{wkd:lower} afternoon');
// "monday afternoon"
```
2 changes: 1 addition & 1 deletion packages/tempo/doc/tempo.registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ In v3.1.0+, you can access active registries using the static `Tempo.registry` g
import { Tempo } from '@magmacomputing/tempo';

console.log(Tempo.registry.formats);
// { '{iso}': '{yyyy}-{mm}-{dd}T{HH}:{mi}:{ss}', ... }
// { '{iso}': '{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}', ... }
```

::: warning
Expand Down
4 changes: 2 additions & 2 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
"version": "3.1.0",
"version": "3.1.1",
"engines": {
"node": ">=20.0.0"
},
Expand Down Expand Up @@ -211,7 +211,7 @@
},
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"@magmacomputing/library": "3.1.0",
"@magmacomputing/library": "*",
"@rollup/plugin-alias": "^6.0.0",
"javascript-obfuscator": "^5.4.3",
"magic-string": "^0.30.21",
Expand Down
8 changes: 4 additions & 4 deletions packages/tempo/public/bundle.index.html
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ <h1>Tempo</h1>
<code><span class="keyword">import</span> { <span class="type">Tempo</span> } <span class="keyword">from</span> <span class="string">'@magmacomputing/tempo'</span>;

<span class="keyword">const</span> t = <span class="keyword">new</span> <span class="type">Tempo</span>(<span class="string">'next friday'</span>);
t.<span class="method">format</span>(<span class="string">'{mon} {day}'</span>);</code>
t.<span class="method">format</span>(<span class="string">'{mon} {dd:raw}'</span>);</code>
</div>

<div class="output-panel">
Expand All @@ -226,7 +226,7 @@ <h1>Tempo</h1>
{
"imports": {
"jsbi": "https://cdn.jsdelivr.net/npm/jsbi@4.3.0/dist/jsbi.mjs",
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.esm.js",
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3.1.1/dist/tempo.bundle.esm.js",
"@js-temporal/polyfill": "https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.esm.js"
}
}
Expand All @@ -237,10 +237,10 @@ <h1>Tempo</h1>

try {
const t = new Tempo('next friday');
console.log('Tempo execution successful:', t.format('{mon} {day}'));
console.log('Tempo execution successful:', t.format('{mon} {dd:raw}'));

const resEl = document.getElementById('result');
resEl.textContent = t.format('{mon} {day}');
resEl.textContent = t.format('{mon} {dd:raw}');
Comment thread
magmacomputing marked this conversation as resolved.
resEl.classList.remove('pulse-loading');
} catch (e) {
console.error(e);
Expand Down
Loading
Loading