npm.io
1.11.7 • Published 4 months ago

iso-time

Licence
MIT
Version
1.11.7
Deps
6
Size
110 kB
Vulns
0
Weekly
0

iso-time

test publish

A glossary of intuitive, universally unambiguous time, date, and duration domain literals.

purpose

declare a universally unambiguous serialization format for dates, times, and datetimes via strict ISO 8601 types with runtime validation.

the core insight: time types are either stamps or floats

  • stamp = absolute instant in history (anchored to a specific point in time)
  • float = detached time pattern (unanchored, could refer to many instants)

etymology:

  • Stamp — matches PostgreSQL timestamp semantics; a time "stamped" into history
  • Float — a time that floats, not anchored; could land on many different instants

usecases:

  • clarify the format a date string should be in (const since: IsoDateStamp = '2024-12-15')
  • cast inputs to strict iso format (const birthday: IsoDateStamp = asIsoDateStamp(new Date()))
  • narrow type of strings with runtime validation (if (!isIsoDateStamp(since)) throw new Error('wrong format'))

types

stamps (absolute instants)

  • IsoTimeStamp = yyyy-MM-ddTHH:mm:ssZ (e.g., 2024-06-15T14:30:00Z)
  • IsoDateStamp = yyyy-MM-dd (e.g., 2024-06-15)
  • IsoMonthStamp = yyyy-MM (e.g., 2024-06)
  • IsoYearStamp = yyyy (e.g., 2024)

floats (detached patterns)

  • IsoTimeFloat = HH:mm:ss (e.g., 14:30:00)
  • IsoHourFloat = HH (e.g., 14)
  • IsoMinuteFloat = mm (e.g., 30)
  • IsoMonthFloat = MM (e.g., 06)
  • IsoDayFloat = dd (e.g., 15)
  • IsoWeekdayFloat = i (e.g., 1 for Monday)

ranges (time spans)

  • IsoTimeStampRange = { since: IsoTimeStamp; until: IsoTimeStamp }
  • IsoDateStampRange = { since: IsoDateStamp; until: IsoDateStamp }

durations (time quantities)

  • IsoDurationWords = iso 8601 string format with compile-time validation (e.g., 'P1Y', 'PT30S', 'P1DT2H')
  • IsoDurationShape = structured object format (e.g., { years: 1 }, { seconds: 30 }, { days: 1, hours: 2 })
  • IsoDuration = union of both formats — use whichever is more convenient

install

npm install iso-time

use

stamps

cast to strict iso format

import { asIsoTimeStamp, asIsoDateStamp } from 'iso-time';

const timestamp = asIsoTimeStamp(new Date()); // '2024-06-15T14:30:00Z'
const datestamp = asIsoDateStamp(new Date()); // '2024-06-15'
validate format
import { isIsoTimeStamp, isIsoDateStamp } from 'iso-time';

isIsoTimeStamp('2024-06-15T14:30:00Z'); // true
isIsoTimeStamp('2024-06-15'); // false

isIsoDateStamp('2024-06-15'); // true
isIsoDateStamp('06/15/2024'); // false
type declarations
import type { IsoTimeStamp, IsoDateStamp } from 'iso-time';

const occurredAt: IsoTimeStamp = asIsoTimeStamp(new Date());
const birthday: IsoDateStamp = asIsoDateStamp('1990-01-15');

floats

extract time components
import { asIsoHourFloat, asIsoMinuteFloat, asIsoWeekdayFloat } from 'iso-time';

const hour = asIsoHourFloat(new Date()); // '14'
const minute = asIsoMinuteFloat(new Date()); // '30'
const weekday = asIsoWeekdayFloat(new Date()); // '1' (Monday)

ranges

create time ranges
import { asIsoTimeStampRange, asIsoDateStampRange } from 'iso-time';

const standup = asIsoTimeStampRange({
  since: new Date('2024-06-15T10:00:00Z'),
  until: new Date('2024-06-15T11:00:00Z'),
});

const vacation = asIsoDateStampRange({
  since: new Date('2024-07-01'),
  until: new Date('2024-07-14'),
});

durations

durations can be expressed in two formats:

  • words = iso 8601 string format ('P1Y', 'PT30S', 'P1DT2H30M')
  • shape = structured object format ({ years: 1 }, { seconds: 30 })

note

  • IsoDurationShape is easier to work with programmatically, so all operations return it by default.
  • IsoDurationWords can be convenient for inputs but is rarely convenient for manipulation.
declare durations
import type { IsoDuration, IsoDurationWords, IsoDurationShape } from 'iso-time';

// string format — concise, readable, compile-time validated
const cacheTtl: IsoDurationWords = 'PT5M';           // 5 minutes
const invoicePeriod: IsoDurationWords = 'P1M';       // 1 month
const trialPeriod: IsoDurationWords = 'P14D';        // 14 days
const sessionTimeout: IsoDurationWords = 'PT30M';   // 30 minutes

// object format — easy to manipulate programmatically
const timeout: IsoDurationShape = { seconds: 30 };
const workday: IsoDurationShape = { hours: 8 };
const sprint: IsoDurationShape = { weeks: 2 };

// union type — accepts either format
const flexible: IsoDuration = 'P1D';                 // string ✓
const alsoFlexible: IsoDuration = { days: 1 };       // object ✓
compile-time validation

IsoDurationWords catches invalid iso 8601 strings at compile time:

import type { IsoDurationWords } from 'iso-time';

// ✅ valid — compiles
const good1: IsoDurationWords = 'P1Y';
const good2: IsoDurationWords = 'PT30S';
const good3: IsoDurationWords = 'P1DT2H30M';

// ❌ invalid — compile error
const bad1: IsoDurationWords = '1 day';              // not iso format
const bad2: IsoDurationWords = 'P';                  // lacks components
const bad3: IsoDurationWords = 'T30S';               // lacks P prefix
const bad4: IsoDurationWords = { days: 1 };          // wrong type (object)
convert to milliseconds
import { toMilliseconds } from 'iso-time';

// both formats work
toMilliseconds({ hours: 1 });       // 3600000
toMilliseconds('PT1H');             // 3600000
toMilliseconds({ days: 1 });        // 86400000
toMilliseconds('P1D');              // 86400000
date arithmetic
import { addDuration, subDuration, getDuration } from 'iso-time';

const later = addDuration(asIsoTimeStamp(new Date()), { hours: 2 });
const earlier = subDuration(asIsoTimeStamp(new Date()), { days: 1 });
const earlierStill = subDuration(asIsoTimeStamp(new Date()), 'P3D');

const elapsed = getDuration({
  of: { range: { since: '2024-06-15T10:00:00Z', until: '2024-06-15T12:30:00Z' } },
}); // { hours: 2, minutes: 30 }
sum durations
import { sumDurations } from 'iso-time';

// spread syntax
const total = sumDurations({ hours: 1 }, { minutes: 30 }, { seconds: 45 });
// { hours: 1, minutes: 30, seconds: 45 }

// array syntax
const durations = [{ hours: 2 }, 'PT30M', { minutes: 15 }];
const combined = sumDurations(durations);
// { hours: 2, minutes: 45 }

// get sum in a specific unit
const totalMinutes = sumDurations(durations, { as: 'minutes' });
// { minutes: 165 }
sleep
import { sleep } from 'iso-time';

await sleep('PT5S');
await sleep({ seconds: 5 });
await sleep({ milliseconds: 100 });

observe

current
import { now, today } from 'iso-time';

const timestamp = now(); // '2024-06-15T14:30:00Z'
const datestamp = today(); // '2024-06-15'
stopwatch
import { startDurationStopwatch } from 'iso-time';

const stopwatch = startDurationStopwatch({ for: 'api call' }, { log: console });
await fetchData();
const { duration } = stopwatch.stop(); // logs elapsed time

examples

event schedule system

import type {
  IsoTimeStamp, IsoDateStamp, IsoTimeFloat,
  IsoWeekdayFloat, IsoDuration
} from 'iso-time';

// when exactly the event occurs (absolute)
interface ScheduledEvent {
  createdAt: IsoTimeStamp;     // '2024-01-15T14:30:00Z'
  occursOn: IsoDateStamp;      // '2024-03-20'
}

// detached schedule pattern (float)
interface DailySchedule {
  since: IsoTimeFloat;         // '09:00:00' - every day at 9am
  until: IsoTimeFloat;         // '10:00:00'
}

// scheduled standup with date range and weekday pattern
interface ScheduledStandup {
  repeats: {
    since: IsoDateStamp;       // '2024-01-15' - start date
    until: IsoDateStamp;       // '2024-12-31' - end date
  };
  occurs: {
    days: IsoWeekdayFloat[];   // ['1', '3', '5'] - mon, wed, fri
    at: IsoTimeFloat;          // '09:00:00'
    for: IsoDuration;          // { minutes: 15 } or 'PT15M'
  };
}

invoice system

import type { IsoMonthStamp, IsoDayFloat, IsoMonthFloat } from 'iso-time';

// bill period (absolute)
interface Invoice {
  period: IsoMonthStamp;  // '2024-01' - January 2024 invoice
}

// bill schedule (float)
interface BillSchedule {
  dueOn: IsoDayFloat;             // '15' - due on 15th of each month
  renewsOn: IsoMonthFloat;        // '01' - annual renewal in January
}

equipment rental

type-safe rental periods with compile-time validated durations:

import type { IsoDurationWords, IsoDuration, IsoDurationShape, IsoTimeStamp } from 'iso-time';
import { addDuration, getDuration, now } from 'iso-time';

// rental options — iso strings validated at compile time
interface RentalOption {
  label: string;
  duration: IsoDurationWords;
  pricePerDay: number;
}

const excavatorRentals: RentalOption[] = [
  { label: 'half day', duration: 'PT4H', pricePerDay: 150 },
  { label: 'full day', duration: 'P1D', pricePerDay: 250 },
  { label: 'weekend', duration: 'P2D', pricePerDay: 200 },
  { label: 'week', duration: 'P7D', pricePerDay: 175 },

  // ❌ compile error — '3 days' is not valid iso 8601
  // { label: 'bad', duration: '3 days', pricePerDay: 180 },
];

// rental agreement with typed durations
interface RentalAgreement {
  equipment: string;
  pickedUpAt: IsoTimeStamp;
  duration: IsoDuration;           // accepts both formats
  gracePeriod: IsoDuration;        // 'PT1H' — 1 hour grace
}

// compute when equipment is due back
const computeDueAt = (input: { agreement: RentalAgreement }): IsoTimeStamp => {
  const dueAt = addDuration(input.agreement.pickedUpAt, input.agreement.duration);
  return addDuration(dueAt, input.agreement.gracePeriod);
};

// check if rental is overdue
const computeOverdueHours = (input: {
  agreement: RentalAgreement;
  returnedAt: IsoTimeStamp;
}): number => {
  const dueAt = computeDueAt({ agreement: input.agreement });
  const overdue = getDuration({
    of: { range: { since: dueAt, until: input.returnedAt } },
    as: 'hours',
  });
  return overdue.hours;
};

// check how much to refund, minus early return fee
const computeRefundDuration = (input: {
  agreement: RentalAgreement;
  returnedAt: IsoTimeStamp;
}): IsoDurationShape => {
  const dueAt = computeDueAt({ agreement: input.agreement });
  const hoursUnused = getDuration({
    of: { range: { since: input.returnedAt, until: dueAt } },
    as: 'hours',
  }).hours;
  const hoursRefundable = hoursUnused * 0.8; // 20% early return fee
  return { hours: hoursRefundable };         // object format — value is computed
};

Keywords