Date
import "ghost:date"
import { now, format } from "ghost:date"
The date module works with instants in time.
Two things shape the whole module:
Every date is immutable. Nothing here changes a date in place. Every function takes a date and returns a new one, which is why the operations are functions in this module rather than methods on the value.
A date is an instant plus a time zone. The instant is what comparisons look at, so it never depends on where the program runs. The zone only decides what reading a calendar position back out — the year, the hour, the weekday — answers. New dates are UTC unless you say otherwise. See Time zones.
import "ghost:date"
launch = date.of(2024, 1, 31)
console.log(date.format(launch, "EEEE, MMMM d, yyyy"))
// >> Wednesday, January 31, 2024
console.log(date.format(date.addMonths(launch, 1), "yyyy-MM-dd"))
// >> 2024-02-29
A date's own toString() gives ISO-8601, which is also what printing one shows:
console.log(date.of(2024, 1, 31))
// >> 2024-01-31T00:00:00Z
Comparing dates
Dates support ==, !=, <, <=, >, and >= directly, comparing instants. Which time zone each date is attached to makes no difference — two dates naming the same moment are equal even when they read it in different zones:
if (date.now() > deadline) {
console.log("late")
}
Arithmetic operators do not work on dates — a + b on two dates is a type error. Use addDays() and the rest below.
Building and converting
| Method | What it does |
|---|---|
now() | The current instant. |
today() | Midnight UTC today. |
of(year, month, day, [hour, minute, second]) | A date from its parts, read as UTC. The month is 1–12. |
ofInZone(year, month, day, [hour, minute, second], zone) | of() with a required trailing zone — the parts are read as local time in that zone. |
parseISO(text) | Parses RFC3339, or a bare YYYY-MM-DD. An explicit offset in the text is kept rather than normalized away. |
fromUnix(seconds) | A date from a Unix timestamp. |
toUnix(date) | Seconds since the Unix epoch. |
toUnixNano(date) | Nanoseconds since the Unix epoch. |
format(date, pattern) | The date as a string — see Formatting. |
An out-of-range day, hour, minute, or second given to of() is a value error rather than silently rolling into the next period.
console.log(date.toUnix(date.of(2024, 1, 31)))
// >> 1706659200
console.log(date.parseISO("2024-03-01"))
// >> 2024-03-01T00:00:00Z
Time zones
A date carries the zone it should be read in. Moving it to another one changes nothing about the instant — only what the calendar reads answer from then on.
| Method | What it does |
|---|---|
inTimeZone(date, zone) | The same instant, read in a different zone. |
timeZone(date) | The zone's IANA name, or "" for a date built from a bare numeric offset. |
zoneOffset(date) | The offset from UTC in seconds, east positive, at that exact instant — so it accounts for daylight saving. |
import "ghost:date"
meeting = date.of(2024, 7, 15, 9, 0, 0)
console.log(meeting) // >> 2024-07-15T09:00:00Z
console.log(date.hour(meeting)) // >> 9
local = date.inTimeZone(meeting, "America/New_York")
console.log(local) // >> 2024-07-15T05:00:00-04:00
console.log(date.hour(local)) // >> 5
console.log(date.timeZone(local)) // >> America/New_York
Zones are always named explicitly, using IANA identifiers like America/New_York. Ghost embeds the zone database in its own binary, so the same name resolves the same way on every machine — there is deliberately no date.local() reading the host's configured zone, which would make a script's output depend on where it ran. An unrecognized name is a value error.
ofInZone() and inTimeZone() are not the same operation. ofInZone(2024, 7, 15, 9, 0, 0, "America/New_York") builds the instant that is 9am in New York. inTimeZone(of(2024, 7, 15, 9, 0, 0), "America/New_York") takes 9am UTC and reads it in New York, giving 5am. Build with the first, re-read with the second.Arithmetic
Each takes a date and a count, and returns a new date.
| Method | |
|---|---|
addDays(date, n) / subDays(date, n) | |
addWeeks(date, n) / subWeeks(date, n) | |
addMonths(date, n) / subMonths(date, n) | |
addYears(date, n) / subYears(date, n) | |
addHours(date, n) / subHours(date, n) | |
addMinutes(date, n) / subMinutes(date, n) | |
addSeconds(date, n) / subSeconds(date, n) |
The calendar units — days, weeks, months, years — keep the wall-clock reading in the date's own zone across a daylight-saving change, the way a calendar app's "same time next month" does. The clock units — hours, minutes, seconds — shift by a fixed real duration regardless of zone, so "add 3 hours" always means three real hours.
Differences
Each takes two dates and returns a whole number, truncated toward zero — so differenceInDays(a, b) is always exactly -differenceInDays(b, a).
differenceInDays(a, b), differenceInHours(a, b), differenceInMinutes(a, b), differenceInSeconds(a, b).
console.log(date.differenceInDays(date.of(2024, 3, 1), date.of(2024, 1, 31)))
// >> 30
Durations
A duration is a calendar-and-clock span, kept as six separate components rather than one number of seconds — because "one month" has no fixed length in days.
| Method | What it does |
|---|---|
duration(years, months, days, [hours, minutes, seconds]) | Builds one directly. |
durationBetween(a, b) | The full calendar breakdown of a - b. |
addDuration(date, duration) | Applies one to a date. |
subDuration(date, duration) | Applies one backwards. |
Every component you give duration() has to point the same way — all positive or all negative, with zeroes not counting either way — or it is a value error.
A duration reads its own fields with methods, which is the one place this module departs from "everything is a free function": years(), months(), days(), hours(), minutes(), seconds(), and toString().
import "ghost:date"
span = date.durationBetween(date.of(2024, 3, 15), date.of(2023, 1, 10))
console.log(span) // >> P1Y2M5D
console.log(span.years()) // >> 1
console.log(span.months()) // >> 2
console.log(span.days()) // >> 5
toString() gives an ISO 8601 duration — PT0S for an empty one.
addDuration() and durationBetween() are exact inverses, so a span taken between two dates puts one back on the other:
start = date.of(2023, 1, 10)
end = date.of(2024, 3, 15)
console.log(date.addDuration(start, date.durationBetween(end, start)))
// >> 2024-03-15T00:00:00Z
This sits alongside the differenceInX functions rather than replacing them — "how many whole days apart" and "the full calendar breakdown" are different questions.
Two durations compare with == and !=, component by component. Ordering them with < or > is not supported: which of two spans is longer has no answer without a reference date.
Components
year(date), month(date), day(date), hour(date), minute(date), second(date), weekday(date).
weekday() counts from 0 for Sunday.
Each reads the date's own time zone, so the same instant can answer a different day, hour, or weekday depending on where it was last moved to.
Predicates
isSameDay(a, b), isWeekend(date), isLeapYear(date).
These read the date's own zone too — an instant that is Saturday in UTC but already Sunday in Tokyo answers for Tokyo once moved there.
Period boundaries
startOfDay(date), endOfDay(date), startOfWeek(date), endOfWeek(date), startOfMonth(date), endOfMonth(date), startOfYear(date), endOfYear(date).
console.log(date.endOfMonth(date.of(2024, 1, 31)))
// >> 2024-01-31T23:59:59Z
console.log(date.endOfYear(date.of(2024, 7, 15)))
// >> 2024-12-31T23:59:59Z
Each is computed in the date's own zone — midnight in Tokyo for a date moved there, not midnight UTC. startOfWeek() and endOfWeek() treat Sunday as the first day of the week, matching weekday()'s own numbering.
Formatting
format(date, pattern) builds a string from pattern letters. A run of the same letter is one token, and anything else in the pattern copies through literally.
| Letter | Means | Longer runs |
|---|---|---|
y | year | yyyy → 2024 |
M | month | M → 1, MM → 01, MMM → Jan, MMMM → January |
d | day | padded at two or more |
E | weekday | abbreviated below four letters, full name at four or more |
H | hour, 24-hour | |
h | hour, 12-hour | |
m | minute | |
s | second | |
a | AM/PM |
d = date.of(2024, 1, 31)
date.format(d, "yyyy-MM-dd") // '2024-01-31'
date.format(d, "EEEE, MMMM d, yyyy") // 'Wednesday, January 31, 2024'
date.format(d, "h:mm a") // '12:00 AM'
Pausing a program
Sleeping is not a date operation and does not live here — it belongs to the program itself. See os.sleep().