Skip to content
spencermountainPublic

About

A lightweight javascript timezone library

Topics

Resources

Contributing

Stars

4.1k stars

Watchers

26 watching

Forks

Repository files navigation

isn't it weird how we can do math in our head, but not date-math?

- how many days until the end of the year?
-what time was it, 11 hours ago?
-is it lunchtime in france?

and worse - there's no real date calculator?

people end up asking google, and going to weird websites.
that's bad.

spacetime is a date-calculator

It's very small, and very handy.
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; Intl is 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
(for computers)

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'

Docs     •     Website     •     Demos

Temporal API adoption

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 timezone

Pass 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.

Plugins:

Date Inputs:

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.

Get & Set 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.

Comparisons:

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.

Timezones:

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:30pm

If, 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.

Date Formatting:

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'

Limitations & caveats

◆ Historical timezone info

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.

◆ International date line

.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.

◆ Destructive changes

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 change

◆ Calendars and locales

Spacetime 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.

◆ 0-based vs 1-based ...

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

Daylight-savings 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.

Config:

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.

Contributing

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.

See also:

(also, thank you to the amazing timeanddate.com)

Apache 2.0 - PRs respected

About

A lightweight javascript timezone library

Topics

Resources

Contributing

Stars

4.1k stars

Watchers

26 watching

Forks

Releases

Used by

Contributors

Languages