Skip to content

strict ISO 8601 timestamps, date stamps, and durations with runtime validation

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
license.md
Notifications You must be signed in to change notification settings

ehmpathy/iso-time

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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
};

About

strict ISO 8601 timestamps, date stamps, and durations with runtime validation

Resources

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
license.md

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •