New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/time

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/time - npm Package Compare versions

Comparing version
5.0.1
to
6.0.0
+11
-0
CLAUDE.md

@@ -35,2 +35,13 @@ # @ultimat3/time — agent notes

possible version of the rule above. Never reintroduce either half.
- **`Intl` answers "can I format this", never "is this an IANA zone", and the two stopped
agreeing.** ICU 78 (Bun 1.4) RESOLVES `CET`, `EST`, `EST5EDT`, `GMT`, `MST` and their families
where ICU 75 threw, so a runtime upgrade alone reopened the golden rule above — silently, and in
the direction that fails dangerous: an abbreviation names no DST rule. So the judgement is never
delegated to `Intl`. `canonicalTimeZone` asserts the structural property itself: a zone is
`Area/Location`, and `UTC` is the one legal exception. Never a denylist of the names ICU newly
accepts — that list grows with every tzdata and ICU release, and no rule in it keeps `CET` out
while letting `Japan` in, both being one label. The single-label `backward` links go with them
(`Japan` → `Asia/Tokyo`, `GB` → `Europe/London`) and that is the point: the slashed spelling is
the one that survives being a formatter-cache key. **Breaking at 6.0.0.** `zones.test.ts` pins
one named case per refused name, so an ICU bump that reopens one names it.
- **Never cache an `Intl` formatter on a raw caller string.** A zone and a locale both arrive from

@@ -37,0 +48,0 @@ a request header, so the key must be canonical (`canonicalTimeZone` for a zone, `canonicalLocale`

+2
-2
{
"name": "@ultimat3/time",
"version": "5.0.1",
"version": "6.0.0",
"description": "UTC instants, DST-correct zone math, cron, durations and Intl formatting with an explicit timezone",

@@ -34,4 +34,4 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "5.0.1"
"@ultimat3/core": "6.0.0"
}
}

@@ -24,2 +24,9 @@ # 🕒 @ultimat3/time

**A zone is `Area/Location`, or `UTC`.** Nothing else. `CET`, `EST5EDT` and `+02:00` name no
jurisdiction and carry no DST rule; the single-label `backward` links (`Japan`, `GB`, `Eire`) are
refused with them, because no rule keeps the first group out and lets the second in. Write the
slashed spelling — `Europe/Paris`, `Asia/Tokyo`, `Europe/London`. `Intl` is not the judge: ICU 78
resolves what ICU 75 threw on, so the check is structural and does not move with the runtime.
**Breaking at 6.0.0**, `Japan` → `Asia/Tokyo`.
One **locale** is one key for the same reason — `Accept-Language` spells one locale `EN-us`,

@@ -145,3 +152,3 @@ `en-US` and `en-latn-us`, and `formatDateTime` and `describeCron` collapse the three before they

|---|---|
| `X_TIMEZONE_INVALID` | not an IANA name (abbreviations and numeric offsets are rejected) |
| `X_TIMEZONE_INVALID` | not `Area/Location` or `UTC`: an abbreviation (`CET`), a numeric offset (`+02:00`), or a single-label legacy name (`Japan`) |
| `X_CRON_INVALID` | unparseable expression, or one that can never match |

@@ -148,0 +155,0 @@ | `X_DURATION_INVALID` | `'3'` with no unit, trailing junk, unknown unit |

@@ -66,7 +66,12 @@ /**

/**
* Two refused classes, and they need different instructions — one `fix:` that only described the
* abbreviations left an operator holding `"Japan"` reading advice about `CET`. A legacy single-label
* link has a mechanical replacement; an abbreviation has none, and saying so IS the instruction.
*/
export function timezoneInvalid(zone: string): TimeError {
return new TimeError({
code: 'X_TIMEZONE_INVALID',
cause: `"${zone}" is not an IANA timezone name`,
fix: 'use an IANA identifier such as Europe/Berlin, America/New_York or UTC — never an abbreviation like CET or a numeric offset',
cause: `"${zone}" is not an IANA Area/Location zone name`,
fix: "use Area/Location, or UTC. A single-label legacy name swaps mechanically — Japan → Asia/Tokyo, GB → Europe/London, Universal → UTC. An abbreviation or a numeric offset does not: CET and EST5EDT name no jurisdiction and carry no DST rule, so name the city whose clock you mean (Europe/Paris, America/New_York). Every accepted name: Intl.supportedValuesOf('timeZone')",
});

@@ -73,0 +78,0 @@ }

@@ -28,5 +28,6 @@ /**

/**
* Deprecated aliases (`US/Eastern`, `Asia/Calcutta`) and the runtime's extras (`EST`, `GMT`) are
* not in the listed set, so they take the `Intl` probe once — bounded for the same reason every
* other cache here is.
* Deprecated aliases (`US/Eastern`, `Asia/Calcutta`) are not in the listed set — `supportedValuesOf`
* holds canonical zones only, and ICU does not fold a `backward` link into its target — so they
* take the `resolve` probe once, as do the runtime's extras (`EST`, `GMT`), the aliases to be
* accepted and the extras refused. Both cached: either can arrive from a header on every request.
*/

@@ -49,5 +50,25 @@ const probed = new Map<string, string | ''>();

/**
* `Intl` answers "can I format this", never "is this an IANA zone", and the two stopped agreeing:
* ICU 78 (Bun 1.4) resolves `CET`, `EST`, `EST5EDT`, `GMT` and `MST` where ICU 75 threw, so a
* runtime upgrade alone reopened the guard — silently, and in the direction that fails dangerous,
* because an abbreviation names no DST rule. The IANA-ness judgement is therefore never delegated
* to `Intl`: an identifier is `Area/Location`, and `UTC` is the one legal exception.
*
* That refuses the single-label `backward` links (`Japan`, `GB`, `Eire`) along with the
* abbreviations, and it is meant to. No structural rule keeps `CET` out and lets `Japan` in — both
* are one label — and the alternative is a denylist that grows with every tzdata and ICU release.
* `Asia/Tokyo` is the spelling that survives being a formatter-cache key, which is what this file
* is for. `Etc/GMT+2` passes: the `+` is inside a real zone name, and only a LEADING sign is a
* bare offset.
*
* `UTC` is compared on the RESOLVED name rather than assumed unreachable. It is unreachable today
* — `UTC` is in `supportedValuesOf` and never gets this far — but a runtime that folds an alias
* into its target would resolve `Etc/UTC` to `UTC`, and refusing `Etc/UTC` would be the bug.
*/
function resolve(zone: string): string | '' {
try {
return new Intl.DateTimeFormat('en-US', { timeZone: zone }).resolvedOptions().timeZone;
const resolved = new Intl.DateTimeFormat('en-US', { timeZone: zone }).resolvedOptions()
.timeZone;
return resolved === 'UTC' || resolved.includes('/') ? resolved : '';
} catch {

@@ -54,0 +75,0 @@ return '';