Convert any epoch timestamp to a human-readable date in TypeScript — auto-detects seconds, milliseconds, microseconds, and nanoseconds with zero configuration. Also converts date strings to epoch, computes calendar-accurate durations, and handles any IANA timezone.
v2.x is the current line. Engine swapped from moment.js to Luxon — same API, ~60% smaller bundle (~71 KB vs ~180 KB), no maintenance-mode dependency. Upgrading from v1.x is a one-command migration thanks to the companion codemod:
npx @master4n/temporal-transformer-codemod --update-deps ./The codemod rewrites every moment-style format string in your source (
YYYY-MM-DD→yyyy-MM-dd,dddd→cccc,[literal]→'literal', etc.), handles untranslatable tokens with stderr warnings, AND bumps@master4n/temporal-transformerin everypackage.jsonit finds to^2.0.2. See MIGRATION.md for the full upgrade story; codemod repo for the tool itself.
Working with epoch timestamps in TypeScript is surprisingly painful:
- You don't know if a value is in seconds, milliseconds, microseconds, or nanoseconds until it breaks in production.
new Date(epoch)silently produces wrong dates if the unit is wrong.- Parsing date strings without a format string is unreliable and locale-dependent.
- Getting the UTC offset for a given timezone requires knowing a timezone library's specific API.
temporal-transformer solves all of these in one small, fully-typed package:
// No idea what unit this is? No problem.
// (All four resolve to the same instant — 2021-06-01T11:43:20Z. Display below in IST for illustration.)
convertEpoch(1622547800).dateTime; // seconds → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000).dateTime; // millis → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000000).dateTime; // micros → "2021-06-01 17:13:20.000"
convertEpoch(1622547800000000000).dateTime; // nanos → "2021-06-01 17:13:20.000"- Auto-detects epoch unit — seconds, milliseconds, microseconds, nanoseconds
- Epoch → human-readable date in local timezone and GMT simultaneously
- Date string → epoch with explicit format and timezone (strict parsing, no surprises)
- Calendar-accurate duration between two timestamps (years, months, days, hours, minutes, seconds)
- Timezone conversion — format any epoch in any IANA timezone
- Timezone utilities — list all available IANA timezones, get UTC offset for any zone
- Result-style safe API — every function has a
safeXxxvariant returning{ ok, value | error }instead of throwing - Safe predicates —
isValidEpoch/isValidTimezonethat never throw - Frozen results — returned objects are
Object.freeze()'d to prevent downstream tampering - Token allowlist — format strings are validated against a Luxon-token allowlist; typos throw
FormatInvalidinstead of silently producing garbage - Relative time — "3 hours 15 minutes ago" / "2 days from now"
- Full TypeScript support — strict types, enums, and interfaces exported
- Dual ESM + CJS build — works in Next.js, Node.js, Vite, webpack
- Hardened input validation — DoS-capped string and format lengths, rejects HTML/XSS payloads,
Infinity,NaN, prototype-pollution attempts, and clock-corruption edge cases - Powered by Luxon — immutable, IANA-aware via the
IntlAPI, no moment.js, Node 18+
npm install @master4n/temporal-transformer
# or
yarn add @master4n/temporal-transformer
# or
pnpm add @master4n/temporal-transformerimport {
convertEpoch,
convertDateToEpoch,
convertEpochToTimezone,
parseToEpoch,
getDurationBetween,
isValidEpoch,
} from '@master4n/temporal-transformer';
// Convert any epoch to a readable date — unit is auto-detected
const result = convertEpoch(1622547800000);
console.log(result.dateTime); // e.g. "2021-06-01 17:13:20.000" (your local TZ — IST shown)
console.log(result.dateTimeInGMT); // "2021-06-01 11:43:20.000"
console.log(result.epochUnit); // "milliseconds"
console.log(result.relative); // e.g. "4 years 11 months ago"
// Convert a Date object to epoch values
const epoch = convertDateToEpoch(new Date(), 'America/New_York');
console.log(epoch.epochInSeconds); // e.g. 1748000000
console.log(epoch.epochInMilliseconds); // e.g. 1748000000000
// Parse a date string to epoch (strict, timezone-aware)
const parsed = parseToEpoch('2024-12-25T00:00:00', undefined, 'Asia/Kolkata');
console.log(parsed.epochInMilliseconds);
// Format an epoch in a specific timezone
const formatted = convertEpochToTimezone(1622547800000, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm');
console.log(formatted); // "2021-06-02 02:43"
// Calendar-accurate duration between two timestamps
const duration = getDurationBetween(1609459200000, 1622547800000);
console.log(duration.humanReadable); // "4 months, 15 days, 17 hours, 43 minutes, 20 seconds"
// Safe validation — never throws
console.log(isValidEpoch(1622547800000)); // true
console.log(isValidEpoch('bad value')); // false
console.log(isValidEpoch(Infinity)); // falseConverts any epoch value to a human-readable date. Automatically detects the epoch unit (seconds / milliseconds / microseconds / nanoseconds).
Pass a whole number. Auto-detection keys off the integer magnitude. Fractional epochs (e.g.
1622547800.5) are rejected withEpochError.NotAnIntegerrather than silently misclassified — pass whole seconds / ms / µs / ns. Detection is unambiguous for the ranges callers hit in practice (seconds up to ~year 2286; ms from ~Apr 1970 on); convert any value outside those bands to milliseconds yourself before calling.
function convertEpoch(epoch: number, format?: string): EpochToDate| Parameter | Type | Default | Description |
|---|---|---|---|
epoch |
number |
— | Integer epoch value in any unit |
format |
string |
'yyyy-MM-dd HH:mm:ss.SSS' |
Luxon format token (validated against SUPPORTED_FORMAT_TOKENS) |
Returns: EpochToDate
const result = convertEpoch(1622547800000, 'yyyy-MM-dd');
// { epoch: 1622547800000, epochUnit: 'milliseconds', timezone: 'Asia/Kolkata',
// dateTime: '2021-06-01', dateTimeInGMT: '2021-06-01', relative: '...' }Converts a Date object to epoch values (seconds and milliseconds) in the specified timezone.
function convertDateToEpoch(date: Date, timezone?: string): DateToEpoch| Parameter | Type | Default | Description |
|---|---|---|---|
date |
Date |
— | Any valid JavaScript Date |
timezone |
string |
System local timezone | IANA timezone name |
Returns: DateToEpoch
const result = convertDateToEpoch(new Date('2024-01-15'), 'America/New_York');
// { epochInSeconds: 1705276800, epochInMilliseconds: 1705276800000,
// dateTime: '2024-01-14 19:00:00', dateTimeInGMT: '2024-01-15 00:00:00',
// timezone: 'America/New_York' }Formats an epoch value as a date string in a specific IANA timezone.
function convertEpochToTimezone(epoch: number, timezone: string, format?: string): stringconvertEpochToTimezone(1622547800000, 'America/Los_Angeles', 'yyyy-MM-dd HH:mm:ss');
// "2021-06-01 04:43:20" (PDT, UTC−7 in June)
convertEpochToTimezone(1622547800000, 'Asia/Tokyo');
// "2021-06-01 20:43:20.000" (JST, UTC+9)Returns just the detected unit of an epoch value — useful for routing logic without doing the full conversion.
function getEpochUnit(epoch: number | string): EpochUnitgetEpochUnit(1622547800); // EpochUnit.SECONDS === 'seconds'
getEpochUnit(1622547800000); // EpochUnit.MILLI_SECONDS === 'milliseconds'
getEpochUnit(1622547800000000); // EpochUnit.MICRO_SECONDS === 'microseconds'
getEpochUnit(1622547800000000000); // EpochUnit.NANO_SECONDS === 'nanoseconds'Parses a date string into epoch values. Uses strict parsing when a format is provided — never silently produces a wrong date.
function parseToEpoch(input: string, format?: string, timezone?: string): DateToEpoch| Parameter | Type | Default | Description |
|---|---|---|---|
input |
string |
— | Date string to parse |
format |
string or undefined |
Auto (ISO 8601) | Luxon format token (validated against SUPPORTED_FORMAT_TOKENS) |
timezone |
string |
System local timezone | IANA timezone to interpret the date in |
Returns: DateToEpoch
Throws: EpochValidationError with EpochError.ParseError if the string cannot be parsed, or EpochError.TimezoneError for an unknown timezone.
// ISO 8601 auto-parsing
parseToEpoch('2024-12-25T00:00:00Z');
// Explicit format (strict mode — wrong format throws immediately)
parseToEpoch('25/12/2024', 'dd/MM/yyyy', 'Europe/London');
// With timezone context
parseToEpoch('2024-12-25 09:00:00', 'yyyy-MM-dd HH:mm:ss', 'Asia/Kolkata');Computes a calendar-accurate duration between two epoch timestamps.
function getDurationBetween(fromEpoch: number, toEpoch: number): ParsedDurationReturns: ParsedDuration
Throws: EpochValidationError with EpochError.RangeError if fromEpoch > toEpoch.
const d = getDurationBetween(1609459200000, 1748000000000);
console.log(d.years); // 4
console.log(d.months); // 5
console.log(d.days); // 18
console.log(d.humanReadable); // "4 years, 5 months, 18 days, ..."
console.log(d.totalMilliseconds); // exact diff in msCalendar-accurate: months and years account for actual calendar boundaries (Feb 28/29, 30/31-day months), not fixed 30.44-day approximations.
Returns the UTC offset for an IANA timezone, optionally at a specific point in time (useful for DST-aware offsets).
function getTimezoneOffset(timezone: string, epoch?: number): TimezoneOffsetgetTimezoneOffset('Asia/Kolkata');
// { offset: '+05:30', offsetMinutes: 330 }
getTimezoneOffset('America/New_York');
// { offset: '-05:00', offsetMinutes: -300 } (or -04:00 during DST)
// DST-aware: pass the epoch you care about
getTimezoneOffset('America/New_York', 1622547800000);
// { offset: '-04:00', offsetMinutes: -240 }Returns the full list of supported IANA timezone names (600+).
function getTimezoneList(): string[]const zones = getTimezoneList();
zones.includes('Asia/Kolkata'); // true
zones.includes('America/Chicago'); // trueReturns the current moment as epoch values (seconds, milliseconds, ISO string, and detected timezone).
function getEpochNow(): EpochNowconst now = getEpochNow();
// { seconds: 1748000000, milliseconds: 1748000000000,
// iso: '2025-05-23T10:13:20.000Z', timezone: 'Asia/Kolkata' }Formats a duration in milliseconds as a human-readable string.
function formatDuration(milliseconds: number): stringformatDuration(3661000); // "1 hour, 1 minute, 1 second"
formatDuration(86400000); // "1 day"
formatDuration(90061000); // "1 day, 1 hour, 1 minute, 1 second"
formatDuration(500); // "0 seconds"Safe predicates that return boolean without throwing. Useful for conditional logic and input validation at system boundaries.
function isValidEpoch(epoch: unknown): boolean
function isValidTimezone(timezone: string): booleanisValidEpoch(1622547800000); // true
isValidEpoch('1622547800000'); // true (numeric string)
isValidEpoch(null); // false
isValidEpoch(Infinity); // false
isValidEpoch(NaN); // false
isValidTimezone('UTC'); // true
isValidTimezone('Asia/Kolkata'); // true
isValidTimezone('Not/ATimezone'); // falseinterface EpochToDate {
epoch: number; // original input
epochUnit: string; // 'seconds' | 'milliseconds' | 'microseconds' | 'nanoseconds'
timezone: string; // IANA timezone used for dateTime
dateTime: string; // formatted date in local timezone
dateTimeInGMT: string; // formatted date in GMT
relative: string; // e.g. "3 hours 15 minutes ago"
}interface DateToEpoch {
epochInSeconds: number;
epochInMilliseconds: number;
timezone: string;
dateTime: string; // formatted in the given timezone
dateTimeInGMT: string;
}interface ParsedDuration {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
totalMilliseconds: number;
humanReadable: string; // e.g. "1 year, 2 months, 3 days"
}interface EpochNow {
seconds: number;
milliseconds: number;
iso: string; // ISO 8601 UTC string
timezone: string; // detected local timezone
}interface TimezoneOffset {
offset: string; // e.g. "+05:30"
offsetMinutes: number; // e.g. 330
}enum EpochUnit {
SECONDS = 'seconds',
MILLI_SECONDS = 'milliseconds',
MICRO_SECONDS = 'microseconds',
NANO_SECONDS = 'nanoseconds',
}type DurationUnit =
| 'years' | 'months' | 'weeks' | 'days'
| 'hours' | 'minutes' | 'seconds' | 'milliseconds';| Absolute value range | Detected unit | Example value |
|---|---|---|
< 10,000,000,000 |
seconds | 1622547800 |
< 10,000,000,000,000 |
milliseconds | 1622547800000 |
< 10,000,000,000,000,000 |
microseconds | 1622547800000000 |
< 1e19 |
nanoseconds | 1622547800000000000 |
Negative epochs (dates before Unix epoch, i.e. before 1970-01-01) are fully supported.
All functions throw EpochValidationError (extends Error) with a message from the EpochError enum.
import { EpochValidationError, EpochError } from '@master4n/temporal-transformer';
try {
convertEpoch(null as any);
} catch (err) {
if (err instanceof EpochValidationError) {
console.log(err.message); // 'Epoch Is Undefined Or Null'
console.log(err.name); // 'EpochValidationError'
}
}EpochError value |
Thrown when |
|---|---|
UndefinedOrNull |
Input is null or undefined |
NotANumber |
Input is not numeric, is Infinity, or is NaN |
Empty |
Input is an empty or whitespace-only string |
EpochUnit |
Epoch value is too large to classify into any unit |
DateError |
Date object passed to convertDateToEpoch is invalid |
TimezoneError |
Timezone string is not a recognized IANA timezone |
ParseError |
Date string cannot be parsed (in parseToEpoch) |
RangeError |
fromEpoch > toEpoch in getDurationBetween |
Every function has a safeXxx counterpart that returns a discriminated Result<T> object instead of throwing. This is ideal for code paths handling untrusted input — API endpoints, form validation, batch processors — where try/catch becomes noise.
import { safeConvertEpoch, safeParseToEpoch, safeGetEpochNow } from '@master4n/temporal-transformer';
const result = safeConvertEpoch(req.body.timestamp);
if (result.ok) {
console.log(result.value.dateTime); // typed as EpochToDate
} else {
console.log(result.error.message); // typed as EpochValidationError
}| Safe variant | Wraps | Returns |
|---|---|---|
safeConvertEpoch |
convertEpoch |
Result<EpochToDate, EpochValidationError> |
safeConvertDateToEpoch |
convertDateToEpoch |
Result<DateToEpoch, EpochValidationError> |
safeConvertEpochToTimezone |
convertEpochToTimezone |
Result<string, EpochValidationError> |
safeParseToEpoch |
parseToEpoch |
Result<DateToEpoch, EpochValidationError> |
safeGetDurationBetween |
getDurationBetween |
Result<ParsedDuration, EpochValidationError> |
safeGetTimezoneOffset |
getTimezoneOffset |
Result<TimezoneOffset, EpochValidationError> |
safeGetEpochNow |
getEpochNow |
Result<EpochNow, EpochValidationError> |
safeGetEpochUnit |
getEpochUnit |
Result<EpochUnit, EpochValidationError> |
type Result<T, E = Error> =
| { ok: true; value: T; error?: never }
| { ok: false; value?: never; error: E };The discriminant is ok. TypeScript narrows value and error automatically once you branch on it.
temporal-transformer treats every input as untrusted. The library has been audited against the OWASP-style threat model below; the test suite test/epoch/security.test.ts pins each defense.
| Threat | Defense |
|---|---|
Prototype pollution via __proto__ |
Object inputs are rejected with NotANumber before any property access |
Infinity, -Infinity, NaN |
Explicit isFinite guard, throws NotANumber |
| String DoS (e.g. 1M-char numeric string) | MAX_INPUT_STRING_LENGTH = 256 → throws InputTooLong |
| Format-string DoS (memory amplification) | MAX_FORMAT_STRING_LENGTH = 256 → throws FormatTooLong |
XSS / HTML in parseToEpoch |
Character allowlist regex rejects payloads before reaching moment |
| Stderr leakage of payloads | Strict ISO 8601 mode bypasses moment's js Date() fallback path |
| Timezone injection | validTimezone checks input against the IANA name list |
| Result object tampering | All return values are Object.freeze()'d |
| Internal array mutation | getTimezoneList returns a defensive copy |
System clock corruption (Date.now()) |
getEpochNow validates against ±MAX_EPOCH_MS, throws ClockOutOfRange |
| Reversed-range duration | getDurationBetween throws RangeError if from > to |
| ReDoS | All regexes are linear-time (no nested quantifiers, no backreferences) |
- Format string output is literal: moment's
[bracketed]literals in a format string are emitted verbatim. If you pass user-controlled format strings and output the result to HTML, you must HTML-escape on output. The library does not interpret format strings as user-supplied content. convertDateToEpoch(date)trustsdate.getTime(): aDatesubclass with an overriddenvalueOf()/getTime()returns whatever those methods return. Validateinstanceof Dateand untainted construction at your trust boundary if this matters.
import {
MAX_EPOCH_MS, // 8.64e15 — JS Date upper bound
MIN_EPOCH_MS, // -8.64e15 — JS Date lower bound
MAX_INPUT_STRING_LENGTH, // 256
MAX_FORMAT_STRING_LENGTH, // 256
} from '@master4n/temporal-transformer';If you discover a security issue, please open a private advisory on github.com/Master4Novice/temporal-transformer/security rather than a public issue. See SECURITY.md for the full policy.
Real numbers, run yourself with npm run bench. See bench/RESULTS.md for the full report. Headline on Node 24 / Apple Silicon:
| Operation | Native JS | Raw Luxon | This library | Library overhead |
|---|---|---|---|---|
getEpochUnit (auto-detect) |
— | — | 170M ops/s | the unique feature is essentially free |
isValidEpoch (safe predicate) |
227M ops/s | — | 167M ops/s | 1.4× — JIT-friendly |
convertEpochToTimezone |
— | 183K ops/s | 168K ops/s | 1.1× — thin wrapper |
getDurationBetween (calendar) |
— | 87K ops/s | 92K ops/s | actually faster than raw Luxon diff |
parseToEpoch (ISO + allowlist) |
7.76M ops/s | 603K ops/s | 219K ops/s | 35× over native (security cost) |
convertEpoch (dual-TZ + relative) |
2.08M ops/s | 527K ops/s | 25K ops/s | 82× — does dual-TZ format + relative time |
| Use case | Verdict |
|---|---|
| Backend ingest / API endpoints (10K req/s) | Imperceptible. Use freely. |
| Per-request formatting (handful of calls) | Network/DB latency dominates by 1000×. |
| Batch processing 100K+ rows/second | convertEpoch is hot — use getEpochUnit once to determine units, then loop with raw Date. |
| Frontend display | Imperceptible. |
Bottom line: This library is a validation + ergonomics layer. Its job is to be correct (auto-detect, security guards, frozen results, Result API) — not to be the fastest format-string emitter. The auto-detect itself is essentially free; the cost is the dual-output convenience and the safety checks.
| Feature | temporal-transformer |
moment.js |
dayjs |
date-fns |
luxon |
Native Date |
|---|---|---|---|---|---|---|
| Auto-detect epoch unit | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Result-style safe API ({ok, value|error}) |
✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Frozen results (immutable by construction) | ✅ | ❌ | ✅ | ✅ | ✅ | n/a |
| Input length DoS caps | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| Format-token allowlist (rejects typos) | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| HTML/XSS payload rejection in parse | ✅ | ❌ | ❌ | ❌ | ❌ | n/a |
| IANA timezone support | ✅ (Intl) | ✅ (bundled) | plugin | plugin | ✅ (Intl) | partial |
| TypeScript-first | ✅ | ❌ (@types/moment) |
✅ | ✅ | ✅ | ✅ |
| Tree-shakeable | partial | ❌ | ✅ | ✅ | partial | n/a |
| Min+gzip size | ~25 KB + Luxon | ~290 KB w/tz | ~7 KB | ~13 KB (modular) | ~71 KB | 0 |
| Maintenance status | active | maintenance-only | active | active | active | spec |
| Best for | Ingesting untrusted, mixed-unit epochs | Legacy codebases | Lightweight client bundles | Functional/modular use | Heavy timezone work | Trivial / known-format use |
- Pick
temporal-transformerif you process timestamps where the unit isn't known up front, or you handle untrusted input and want the safe Result API. - Pick
dayjsif bundle size is paramount and you don't need timezone-aware parsing. - Pick
date-fnsif you want a treeshakeable, functional API and you control the input. - Pick
luxondirectly if you already know your epoch unit and need raw speed for timezone formatting at scale. - Pick
Datedirectly if your input is always milliseconds, you don't care about timezones beyond UTC, and you live bytoISOString(). - Pick
momentonly if you're maintaining an existing codebase that depends on it.
// API returns epoch in unknown unit — temporal-transformer handles it
const ts = apiResponse.created_at; // could be seconds or ms
const { dateTime, epochUnit } = convertEpoch(ts);const userInput = req.body.timestamp;
if (!isValidEpoch(userInput)) {
return res.status(400).json({ error: 'Invalid timestamp' });
}
const { dateTime } = convertEpoch(Number(userInput));const { relative } = convertEpoch(event.createdAt);
console.log(`Event created ${relative}`); // "Event created 2 days 4 hours ago"// User enters "25/12/2024" in a UK-locale date picker
const { epochInMilliseconds } = parseToEpoch('25/12/2024', 'dd/MM/yyyy', 'Europe/London');const duration = getDurationBetween(job.startedAt, job.finishedAt);
console.log(`Job ran for ${duration.humanReadable}`);A small ecosystem of focused, agent-friendly packages:
@master4n/temporal-transformer-codemod— codemod to migrate temporal-transformer v1→v2@master4n/http-status— machine-readable HTTP status-code registry for apps & AI agents@master4n/decorators— zero-dependency TypeScript decorators (DI, validation, resilience, redaction)@master4n/master-cli— headless, JSON-first dev CLI (mfn) for humans and AI agents
- Packaging:
llms.txtis now included in the published tarball (it was built but not copied intodist, so it wasn't shipped). - Discoverability: broadened npm keywords (convert-unix-timestamp-to-date, timestamp-to-date, time-ago, humanize-duration, date-converter) and added a "Part of the @master4n toolkit" section.
- Fix (correctness): fractional epochs are now rejected with the new
EpochError.NotAnIntegerinstead of being silently misclassified. Previously the validator stripped the decimal point and re-ran unit auto-detection, soconvertEpoch(1622547800.5)returned a 1970 date (read as16225478005ms) instead of throwing. Integer epochs are unaffected. The Result-API variants (safeConvertEpoch, …) return{ ok: false, error }as usual. - Docs: documented the integer requirement and the auto-detection's
supported magnitude range on
convertEpoch.
- Docs: Top-of-README callout updated to highlight the companion codemod's new one-command migration (
npx @master4n/temporal-transformer-codemod --update-deps ./). The codemod (currently2.0.3) now also bumps@master4n/temporal-transformerto^2.0.2in everypackage.jsonit finds, in addition to rewriting format strings. - Docs: Replaced unreliable bundlephobia badge with a static gzipped-size badge (
7.5 KB) linking tobench/RESULTS.md. Added a "tests passing" badge. - No code change — runtime behavior identical to 2.0.2.
- CI fix: Build now succeeds on Node 18 and on all Windows runners. The
@rollup/plugin-terserdependency was transitively pullingserialize-javascriptwhich callscrypto.randomUUID()from globals at module load — this throwsReferenceError: crypto is not definedin Node 18 CJS contexts and on Windows runners. Dropped the terser plugin entirely; the bundle is still tiny (~7.5 KB gzipped). Downstream bundlers can re-minify if needed. - CI: Workflow now triggers on
master,main, andv1.x-maintenance(was onlymain). Addedworkflow_dispatchfor manual runs. - CI: Set
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=trueto silence the Node 20 deprecation warning ahead of the 2026-09-16 removal. - DX: Test scripts are now cross-platform. Added
cross-envforTZ=UTCand simplified jest invocations to rely onjest.config.mjsfor ignore patterns. - No runtime API changes — drop-in upgrade from 2.0.1.
- Docs: Repository, homepage, and bug-tracker URLs now point at the new public single-package repos at Master4Novice/temporal-transformer and Master4Novice/temporal-transformer-codemod (previously pointed at a private monorepo).
- Docs: Added benchmark suite (
npm run bench) with results published in bench/RESULTS.md. Headline:getEpochUnit~170M ops/s,getDurationBetweenslightly faster than raw Luxon diff,convertEpoch82× slower than rawDate.toISOString()(cost of dual-TZ + relative-time). - Docs: New comparison matrix vs moment / dayjs / date-fns / luxon / native
Datewith explicit "when to pick what" guidance. - Docs: Added
SECURITY.md,CONTRIBUTING.md, GitHub issue & PR templates. - CI: Workflow now runs on Ubuntu/macOS/Windows × Node 18/20/22/24.
- No code changes — behavior identical to v2.0.0.
- Engine swap: moment + moment-timezone → Luxon. ~60% smaller bundle (~71 KB vs ~180 KB), no maintenance-mode dependency.
- Breaking: Format-string grammar is now Luxon syntax. Use
yyyy-MM-ddinstead ofYYYY-MM-DD,ccccinstead ofdddd,'literal'instead of[literal], and so on. Run the migration codemod:npx @master4n/temporal-transformer-codemod ./src. - Breaking: Default format is now
'yyyy-MM-dd HH:mm:ss.SSS'(was'YYYY-MM-DD HH:mm:ss.SSSSSS'). JS Date has millisecond precision; the 6-digit fractional was always zero-padded fiction. - New error:
EpochError.FormatInvalid— thrown when a format string contains a token outside the allowlist. Typos like'YYYY'(still moment syntax) now surface immediately instead of producing garbage output. - New exports:
DEFAULT_FORMAT,SUPPORTED_FORMAT_TOKENS(the runtime-enforced allowlist). - New companion package:
@master4n/temporal-transformer-codemod— one-shot CLI that rewrites moment-style format strings in your codebase. Handles greedy token matching,[literal]→'literal'escape conversion (with proper''escaping for embedded quotes), and emits warnings for tokens with no Luxon equivalent (X,x,Q). - Requirement: Node 18+ (for
Intl.supportedValuesOf). - Maintenance: v1.x stays on npm under the
legacydist-tag; install withnpm i @master4n/temporal-transformer@legacy. Security backports continue onv1.x-maintenancefor 12 months.
See MIGRATION.md for the full upgrade guide.
- Test infrastructure: Added golden-test baseline (
test/golden/) pinning v1.x behavior for the v2.0 parity gate. No runtime behavior changes.
- New (unique): Result-style safe API — every function has a
safeXxxcounterpart returning{ ok, value | error }instead of throwing (8 functions, see Result-Style Safe API) - New:
getEpochUnit(epoch)— standalone helper that returns just the detectedEpochUnit - Security: Frozen result objects (
Object.freeze) prevent downstream tampering - Security:
MAX_INPUT_STRING_LENGTH(256) caps DoS via outsized numeric/date strings — throwsInputTooLong - Security:
MAX_FORMAT_STRING_LENGTH(256) caps memory-amplification via format strings — throwsFormatTooLong - Security:
getEpochNowvalidatesDate.now()againstMIN_EPOCH_MS/MAX_EPOCH_MSand throwsClockOutOfRangeinstead of a rawRangeErroron a corrupted system clock - Security:
getTimezoneListreturns a defensive copy, preventing mutation of the shared internal array - Security:
parseToEpochnow rejects non-string input early withParseError - Security: Fallback to
'UTC'whenmoment.tz.guess()returns an empty string - New exports:
Result<T, E>,MAX_EPOCH_MS,MIN_EPOCH_MS,MAX_INPUT_STRING_LENGTH,MAX_FORMAT_STRING_LENGTH,EpochThreshold - New errors:
EpochError.InputTooLong,EpochError.FormatTooLong,EpochError.ClockOutOfRange - Docs: New Security Model section enumerating the threat model and defenses
- Security fix:
parseToEpochnow validates input against an allowlist of date-safe characters before passing to moment, preventing HTML/XSS payloads from reaching moment's internal parser and appearing in stderr stack traces - Security fix:
parseToEpochwithout an explicit format now uses strict ISO 8601 parsing (moment.ISO_8601, true) instead of moment's lenient auto-detection, eliminating thejs Date()fallback that could behave inconsistently across environments
- New:
convertEpochToTimezone— format epoch in any IANA timezone - New:
parseToEpoch— parse date strings to epoch with strict, timezone-aware parsing - New:
getDurationBetween— calendar-accurate duration between two epochs - New:
getTimezoneOffset— get UTC offset (DST-aware) for any IANA timezone - New:
getTimezoneList— list all supported IANA timezone names - New:
getEpochNow— current time as seconds, milliseconds, ISO string, and timezone - New:
formatDuration— format a duration in ms as a human-readable string - New:
isValidEpoch/isValidTimezone— safe boolean predicates (never throw) - New types:
ParsedDuration,EpochNow,TimezoneOffset,DurationUnit,StartEndUnit - New exports:
EpochUnitenum,EpochErrorenum,EpochValidationErrorclass - Fix:
validateEpochno longer crashes onInfinity,-Infinity,NaN, or whitespace-only strings - Fix: Empty-string check now trims whitespace before checking length
- Deps: Removed redundant
@types/moment; movedtypescriptand@types/nodetodevDependencies; updatedmoment-timezoneto^0.5.46andtypescriptto^5.8.3
Initial release — convertEpoch and convertDateToEpoch.
Issues and PRs are welcome at github.com/Master4Novice/temporal-transformer. The companion codemod lives at github.com/Master4Novice/temporal-transformer-codemod.
Written by Master4Novice.
MIT © Master4Novice