isn't it weird how we can do math in our head, but not date-math?
and worse - there's no real date calculator?
spacetime is a date-calculator
import spacetime from 'spacetime'
let s = spacetime.now()
// Days to the end of the year:
s.diff(s.endOf('year'), 'days')
// 292
// Time it was, 11 hours ago:
s.subtract(11, 'hours').time()
// 6:50am
// Is it lunchtime in France?
s = s.goto('Europe/Paris')
s.isBetween(s.time('11:00am'), s.time('2:00pm'))
// true 🥐These results depend on when you run the example.
- calculate time in remote timezones
- support daylight savings, leap years, and hemispheres
- Moment-like API (mostly immutable)
- Orient time by quarter, season, month, week..
- Zero Dependencies - timezone math uses bundled data;
Intlis used only to detect your local timezone - see the size badge above for the current minified bundle size
- has a cool plugin thing.
- frequent updates for approaching DST changes
- Temporal interoperability and an optional Temporal-backed entry
npm install spacetime
npx skills add spencermountain/spacetime
client-side:
<script src="https://unpkg.com/spacetime"></script>
<script>
var d = spacetime('March 1 2012', 'America/New_York')
//set the time
d = d.time('4:20pm')
d = d.goto('America/Los_Angeles')
d.time()
//'1:20pm'
</script>typescript:
import spacetime, { type Spacetime } from 'spacetime'
const meeting: Spacetime = spacetime('2026-01-15T09:30:00', 'America/New_York')
const london = meeting.goto('Europe/London')
const time: string = london.time() // '2:30pm'old node:
const spacetime = require('spacetime')
let d = spacetime('2026-04-01T16:32:00', 'Europe/Paris')
d.dayName()
//'wednesday'
d.format('nice')
//'Apr 1st, 4:32pm'Spacetime can exchange native Temporal objects when globalThis.Temporal is
available. No polyfill is bundled.
const s = spacetime('june 8th 2021', 'America/Los_Angeles').time('16:22')
const native = s.toTemporal() // Temporal.ZonedDateTime
const roundTrip = spacetime(native) // same instant and timezonePass native objects directly to preserve the selected instant during repeated DST hours. Regular Spacetime stores millisecond precision.
The experimental spacetime/temporal entry uses Temporal for calendar arithmetic
and timezone calculations, without bundling Spacetime's timezone database:
import spacetime from 'spacetime/temporal'
const nextWeek = spacetime('2027-10-09', 'America/New_York').add(1, 'week')It requires Temporal and supports a subset of the regular API. See the Temporal guide for supported methods and conversion limits.
we can parse all the normal stuff, and some fancy stuff:
// epoch in milliseconds
s = spacetime(1489520157124)
//array [yyyy, m, d] (zero-based months, 1-based days)
s = spacetime([2017, 5, 2])
// Arrays can also include hour, minute, second, millisecond
s = spacetime([2017, 5, 2, 14, 30, 0, 0])
// Objects also use zero-based months
s = spacetime({ year: 2017, month: 5, date: 2, hour: 14, minute: 30, second: 0, millisecond: 0 })
// Native Date or an existing spacetime instance
s = spacetime(new Date('2017-06-02T14:30:00Z'))
const copy = spacetime(s)
// Unix timestamps in seconds have a separate helper
s = spacetime.fromUnixSeconds(1489520157)
// ISO date-time
s = spacetime('2017-07-02T05:01:00')
// All inputs accept a timezone, as 2nd param:
s = spacetime(1489520157124, 'Canada/Pacific')
s = spacetime('2019/05/15', 'Canada/Pacific')
// or set the offset right in the date-string (ISO-8601)
s = spacetime('2017-04-03T08:00:00-0700')
// s.timezone().name === 'Etc/GMT+7' (UTC-7; IANA reverses the sign)
// Some helpers
s = spacetime.now()
s = spacetime.today() // This morning
s = spacetime.tomorrow() // Tomorrow morning
s = spacetime.min('UTC') // -271821-04-20T00:00:00.000Z
s = spacetime.max('UTC') // maximum native Date instant: September 13, year 275760
// To get the native Date object back
// Same instant; native Date does not retain the spacetime timezone.
const jsDate = s.toNativeDate()for fancier natural-language inputs, use compromise-dates.
you can whip things around, but stay intuitive. Most setters return a new instance, so capture the result:
const original = spacetime('2017-04-14T05:01:00', 'UTC')
original.date() // 14
let s = original.hour(9).minute(0)
s = s.add(1, 'week')
s.time() // '9:00am'
s.startOf('day').time() // '12:00am'
original.time() // '5:01am' (unchanged)Calendar days and weeks preserve the local clock where it exists, even across DST. See getters and setters for fields, rounding, boundaries, and choosing a forward or backward occurrence.
let s = spacetime([2017, 5, 2], 'UTC')
let d = s.add(5, 'days')
let start = s.subtract(1, 'milliseconds')
let end = s.add(1, 'milliseconds')
// gt/lt/equals
s.isAfter(d) // false
s.isEqual(d) // false
s.isBefore(d) // true
s.isBetween(start, end) // true; pass true as the third argument to include endpoints
// Comparison by unit
s.isSame(d, 'year') // true
s.isSame(d, 'date') // false
s.diff(d, 'day') // 5
s.diff(d, 'month') // 0
//make a human-readable diff
let before = spacetime([2017, 3, 28], 'UTC')
let now = spacetime([2018, 3, 28], 'UTC') // one year later
now.since(before).rounded
// '1 year ago'all comparisons are done with sensitivity of timezone - 8am EST is < 8am PST.
the best way to describe a timezone is an IANA code:
// Roll into a new timezone, at the same moment
s = s.goto('Australia/Brisbane')if you want to support relaxed timezone names like 'EST', Eastern time, use timezone-soft
import tzSoft from 'timezone-soft'
const matches = tzSoft('milwaukee')
if (matches.length > 0) {
s = s.goto(matches[0].iana) // 'America/Chicago'
}
// Informal names can be ambiguous; inspect the candidates when it matters.play-around with timezones, and their DST-changes:
//list timezones by their current time
spacetime.whereIts('8:30pm', '9:30pm') // ['America/Winnipeg', 'America/Yellowknife'... ]
spacetime.whereIts('9am') //(within this hour)
// Timezone metadata
s = spacetime('2024-07-01', 'America/New_York')
s.timezone().name // 'America/New_York'
s.hemisphere() // 'North'
s.timezone().current.offset // -4 (in hours)
s.hasDST() // true
s.isDST() // true
//list all timezones
Object.keys(spacetime.timezones())you can flip-around the world pretty quick.
spacetime uses Intl.DateTimeFormat to detect your local timezone by default. If that information is unavailable, it falls back to UTC. Pass an explicit timezone when you need the same result on every machine.
.goto(null) returns to that default timezone.
spacetime().time('4:30pm').goto('Europe/Paris').goto(null).time()
// 4:30pmIf, for some reason, you want to change the timezone without changing the date, you can do this via the .timezone(str) setter:
let s = spacetime('2023-01-01T5:30[America/Denver]')
s = s.timezone('Europe/Zagreb') // hot-swap
console.log(s.isoFull())
// '2023-01-01T05:30:00.000+01:00[Europe/Zagreb]' (same time, new tz)Under the hood, each instance stores an epoch in milliseconds (s.epoch) and a timezone identifier (s.tz). The main entry uses bundled timezone rules to find the offset, then reads a shifted native Date's UTC fields for the local clock. .goto() changes the timezone while keeping the epoch. Use .toNativeDate() to get a native Date representing the actual instant.
it's a pretty-sensible process to create nice-looking dates:
const s = spacetime('2017-04-14T05:01:00', 'Europe/Paris')
// Date + time formatting
s.format('time') // '5:01am'
s.format('numeric-uk') // '14/04/2017'
s.format('month') // 'April'
s.format('month-short') // 'Apr'
s.format('month-pad') // '03' (zero-based)
s.format('iso-month') // '04'
//if you want more complex formats, use {}'s
s.format('{year}-{iso-month}-{date-pad}') // '2017-04-14'
s.format("{hour} o'clock") // '5 o'clock'
s.format('{time} sharp') // '5:01am sharp'
//if you prefer, you can also use unix-formatting
s.unixFmt('yyyy.MM.dd h:mm a') // '2017.04.14 5:01 AM'
// support for the new Temporal ISO format
s.format('iso-full') // '2017-04-14T05:01:00.000+02:00[Europe/Paris]'
// support for the SQL ISO 9075 format
s.format('sql') // '2017-04-14 05:01:00'DST changes move around all the time, and timezones pop-in and out of existence. We store and use only the latest DST information, updated frequently, and apply it (wrongly) to historical and future dates.
.goto() preserves the instant. Crossing the international date line can change the calendar date: converting from Fiji to Midway moves the local clock backward by nearly a full day.
Most setters preserve smaller units: changing the month keeps the time of day. If the date does not exist in the new month or year, it is clamped to the last valid day. Some setters, such as .quarter(), reset smaller units to the start of the period. Capture the returned instance:
let s = spacetime('2016-03-30T14:30:00', 'UTC')
s.month(1).iso() // '2016-02-29T14:30:00.000Z'
s.month(1).year(2017).iso() // '2017-02-28T14:30:00.000Z'
s.quarter(2).iso() // '2016-04-01T00:00:00.000Z'
s = s.month(1) // save the changeSpacetime uses the Gregorian calendar and Western digits, with English names by default. .i18n() can customize names and labels (see below), but it does not provide other calendar systems or full locale-aware parsing and formatting.
for better or worse we copy the JavaScript spec for 0-based months, and 1-based dates.
ISO-formatting is different, so keep on your toes.
see more considerations and gotchas
We've written in detail about how spacetime handles Daylight-savings changes here
Fall DST changes can repeat a local hour; spring changes can skip one. The displayed clock can move backward even as time advances; .diff(..., 'minutes') measures elapsed time. Use an epoch or a native Temporal object when the exact occurrence of an ambiguous local time matters.
Pass constructor options as the third argument:
spacetime('12/01/2018', 'UTC', { dmy: true }).format('iso-short') // '2018-01-12'| Option | Purpose | Default |
|---|---|---|
silent |
Set false to enable warnings |
true |
dmy |
Interpret ambiguous numeric dates as day/month/year | false |
today |
Supply reference date fields for partial inputs | Current date |
weekStart |
Set week boundaries (0 = Sunday, 1 = Monday) | 1 |
See constructor options for examples. For country-specific week starts, use spacetime-week-start.
Use spacetime.extend() to add methods and s.i18n() to customize language
labels. Both affect shared state across instances; see
extensions and language settings.
Questions, bug reports, and focused pull requests are welcome. Please discuss larger additions first so we can keep the library small. Include tests for behavior changes; maintainers handle release version bumps and generated builds.
This is a pnpm workspace. Plugins use the root package as a workspace peer dependency.
- luxon - a small library from the clever moment people
- date-fns - a battle-hardened client-side Date utility
- sugarjs/dates - well-made date fns + timezone math
- Intl.DateTimeFormat - locale-aware date and time formatting
- BurntSushi/Jiff - Rust/wasm date library
(also, thank you to the amazing timeanddate.com)
Apache 2.0 - PRs respected





