Skip to content

@studnicky/time-conversion

Allocation-free epoch/ISO date conversion using integer civil-calendar arithmetic.

Converts between UTC epoch milliseconds and common textual date formats — ISO-8601 instants and dates, SQL datetimes, and US slash dates — using integer civil-calendar arithmetic (Howard Hinnant's algorithm). It never constructs a JS Date object, avoiding the allocation and locale/timezone ambiguity that come with it.

Install

bash
pnpm add @studnicky/time-conversion

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Parsing mixed formats

epochMs auto-detects the format — ISO instant, ISO date, SQL datetime, or US slash-date. Try it live — edit the code and press Execute:

Auto-detecting mixed date formats
/** parseMixedFormats — demonstrates epochMs parsing ISO, SQL, and slash-date formats. Run: npx tsx examples/parseMixedFormats.ts */

import assert from 'node:assert/strict';

// #region usage
import { CanonicalDate } from '../src/index.js';

function demonstrateParseMixedFormats(): { 'invalid': number | null; 'isoDate': number | null; 'isoInstant': number | null; 'sqlDateTime': number | null; 'usSlashDate': number | null } {
  // epochMs auto-detects the format: ISO instant, ISO date, SQL datetime, or US slash-date
  const isoInstant = CanonicalDate.epochMs('2024-02-29T12:34:56.789Z');
  console.log('ISO instant ->', isoInstant);

  const isoDate = CanonicalDate.epochMs('2024-01-01');
  console.log('ISO date ->', isoDate);

  const sqlDateTime = CanonicalDate.epochMs('2023-03-15 09:08:07');
  console.log('SQL datetime ->', sqlDateTime);

  const usSlashDate = CanonicalDate.epochMs('3/15/2023');
  console.log('US slash date ->', usSlashDate);

  // unparseable or empty input returns null instead of throwing
  const invalid = CanonicalDate.epochMs('not a date');
  console.log('invalid input ->', invalid);

  return { 'invalid': invalid, 'isoDate': isoDate, 'isoInstant': isoInstant, 'sqlDateTime': sqlDateTime, 'usSlashDate': usSlashDate };
}
// #endregion usage

const results = demonstrateParseMixedFormats();

assert.equal(results.isoInstant, 1_709_210_096_789);
assert.equal(results.isoDate, 1_704_067_200_000);
assert.equal(results.sqlDateTime, 1_678_871_287_000);
assert.equal(results.usSlashDate, 1_678_838_400_000);
assert.equal(results.invalid, null);

console.log('parseMixedFormats: all assertions passed');
Output
Press Execute to run this example against the real library.

Round-tripping through ISO

Formatting epoch milliseconds as an ISO-8601 instant string, including pre-1970 (negative) epoch values:

ts
import { CanonicalDate } from '../src/index.js';

function demonstrateRoundTripIso(): { 'formatted': string | null; 'notFinite': string | null; 'original': number; 'preEpoch': string | null; 'roundTripped': number | null } {
  // format epoch milliseconds as an ISO-8601 instant string
  const formatted = CanonicalDate.isoFromEpochMs(0);
  console.log('epoch 0 ->', formatted);

  // negative epoch values (pre-1970) format correctly too
  const preEpoch = CanonicalDate.isoFromEpochMs(-2_208_988_800_000);
  console.log('pre-1970 epoch ->', preEpoch);

  // parsing the formatted string returns the original epoch milliseconds
  const original = 1_709_210_096_789;
  const roundTripped = CanonicalDate.isoInstantEpochMs(CanonicalDate.isoFromEpochMs(original)!);
  console.log('round-tripped epoch ->', roundTripped);

  // non-finite input formats to null instead of throwing
  const notFinite = CanonicalDate.isoFromEpochMs(Number.NaN);
  console.log('NaN input ->', notFinite);

  return { 'formatted': formatted, 'notFinite': notFinite, 'original': original, 'preEpoch': preEpoch, 'roundTripped': roundTripped };
}

Source on GitHub