Packages

date

Build, parse, format and compare dates and times, do arithmetic on them, and show them in any time zone.

import "std:date"

std:date is for anything with a calendar or a clock: parsing a timestamp, adding 30 days to a deadline, working out how long ago something happened, or showing the same moment in London and Tokyo. Three types do the work:

  • date.DateTime is one instant in time, held in UTC. Every date you parse or build is one.
  • date.Duration is a length of time, such as the gap between two instants.
  • date.ZonedDateTime is a view of an instant in a time zone, for showing local times.

A DateTime never changes. Every method that "changes" it returns a new one.

Parsing a date#

date.parse(text) reads ISO 8601 dates and times and returns a result, because the text may not be a date:

main.osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:00Z").unwrap()
log d.iso()
log d.unix()
Click the code to edit it

iso() writes the instant back as text in UTC. unix() is whole seconds since 1 January 1970 (the Unix epoch), the form most APIs and files use.

Without a format, parse accepts these shapes:

main.osl
import "std:date"

string[] inputs = [
  "2024-03-01",
  "2024-03-01T12:30",
  "2024-03-01 12:30:05",
  "2024-03-01T12:30:05.250Z",
  "2024-03-01T12:30:05+02:00"
]
for text in inputs (
  log date.parse(text).unwrap().iso()
)
Click the code to edit it

Missing parts default to the start of the period (a date with no time is midnight). An offset such as +02:00 is applied, so the last line is two hours earlier in UTC.

When parsing fails#

Dates from users and files are often wrong. A bad one fails with a ParseError, and the calendar is checked, so 30 February is rejected:

main.osl
import "std:date"

string[] inputs = ["2024-03-15", "yesterday", "2024-02-30"]
for text in inputs (
  date.DateTime d = date.parse(text) catch (e) (
    log text ++ " -> " ++ e.kind
    continue
  )
  log text ++ " -> ok " ++ d.format("MMM D")
)
Click the code to edit it

Parsing other formats#

Pass format with the same tokens that format() uses (listed below) to read a layout that isn't ISO:

main.osl
import "std:date"

auto uk = date.parse("15/03/2024", {format: "DD/MM/YYYY"}).unwrap()
auto us = date.parse("March 5, 2024", {format: "MMMM D, YYYY"}).unwrap()
log uk.iso()
log us.iso()
Click the code to edit it

Text with no offset is read as UTC. If it was written in another zone, say which with the timezone option and it is converted:

main.osl
import "std:date"

auto tokyo = date.parse("2024-06-01 09:00", {timezone: "Asia/Tokyo"}).unwrap()
log tokyo.iso()
Click the code to edit it

Nine in the morning in Tokyo (UTC+9) is midnight UTC, nine hours earlier on the same clock. Keep this habit: parse at the edge of your program into an instant, work with instants, and convert to a zone only to display.

Formatting#

format(layout) writes the date using tokens:

main.osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:05Z").unwrap()
log d.format("YYYY-MM-DD")
log d.format("D MMM YYYY")
log d.format("dddd, MMMM D, YYYY")
log d.format("HH:mm:ss")
log d.format("h:mm A")
log d.format("[Week] ddd")
log d.format("X")
Click the code to edit it
TokenMeaning
YYYY YYyear
MMMM MMM MM Mmonth: name, short name, two digits, one or two digits
DD Dday of the month
dddd dddweekday name, short weekday name
HH Hhour, 24-hour clock
hh h and A ahour, 12-hour clock, and AM/PM
mm m, ss sminute, second
SSS SSSSSSfraction of a second
Z ZZ zoffset (+02:00, +0200) and zone name
Xunix seconds

Wrap literal text in square brackets, as [Week] is above, so its letters aren't read as tokens.

Building dates from parts#

There is no constructor taking year, month and day. Parse a string, or start from a unix time and move from there:

main.osl
import "std:date"

log date.fromUnix(0).iso()
log date.fromUnixMillis(1700000000000).iso()

auto year = 2025
auto month = 9
auto d = date.parse(year ++ "-0" ++ month ++ "-01").unwrap()
log d.format("dddd")
Click the code to edit it

date.daysInMonth(year, month) and date.isLeap(year) answer calendar questions without a date at all:

main.osl
import "std:date"

log date.daysInMonth(2024, 2) date.daysInMonth(2025, 2)
log date.isLeap(2024) date.isLeap(1900) date.isLeap(2000)
Click the code to edit it

Arithmetic#

add(unit, amount) and subtract move an instant. Units are "y", "mo", "d", "h", "m", "s", "ms" and "us", or the long names such as "days":

main.osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:00Z").unwrap()
log d.add("days", 20).iso()
log d.add("hours", 36).iso()
log d.subtract("m", 45).iso()
log d.add("y", 1).iso()
log d.iso()
Click the code to edit it

The last line shows d itself is untouched.

Months and years follow the calendar, so they can land past the end of a short month. January 31 plus a month is "February 31", which rolls over to March:

main.osl
import "std:date"

auto d = date.parse("2024-01-31T10:00:00Z").unwrap()
log d.add("mo", 1).iso()
Click the code to edit it

If you need "the last day of next month", use date.daysInMonth and with. Days are always 24 hours, so adding a day never lands on a different time of day.

Changing one field#

with(field, value) replaces one field. truncate(unit) zeroes everything smaller than a unit:

main.osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:45Z").unwrap()
log d.with("year", 2000).iso()
log d.with("day", 1).iso()
log d.truncate("day").iso()
log d.truncate("hour").iso()
Click the code to edit it

truncate("day") is how you get the start of a day, to group events by date or compare dates ignoring the time.

Comparing#

Compare with before, after, equals or compare, which is -1, 0 or 1 and suits sorting:

main.osl
import "std:date"

auto a = date.parse("2024-01-01").unwrap()
auto b = date.parse("2024-06-01").unwrap()
log a.before(b) a.after(b) a.equals(a)
log a.compare(b)

date.DateTime[] dates = [b, a, date.parse("2023-01-01").unwrap()]
log dates.toSortedBy(d -> d.unix()).map(d -> d.format("YYYY-MM-DD"))
Click the code to edit it

Durations#

until(other) and since(other) return a date.Duration, which you read in whatever unit you want. until is negative when the other date is earlier, and since is the opposite:

main.osl
import "std:date"

auto start = date.parse("2024-07-01T12:00:00Z").unwrap()
auto end = date.parse("2024-12-25").unwrap()
auto gap = start.until(end)

log floor(gap.days())
log gap.hours()
log end.until(start).days() < 0
Click the code to edit it

A duration offers days(), hours(), minutes(), seconds() (numbers, with fractions) and millis() and micros() (whole numbers). To get whole days, use floor as above.

date.millis(ms) makes a duration to add, and the constants date.second, date.minute, date.hour, date.day and date.week are those lengths in milliseconds:

main.osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:00Z").unwrap()
log d.addDuration(date.millis(date.hour * 2)).iso()
log date.week / date.day
Click the code to edit it

Time zones#

inTimezone(name) returns a ZonedDateTime for an IANA name such as Europe/London. It is the same instant, shown on that zone's clock, daylight saving included:

main.osl
import "std:date"

auto launch = date.parse("2024-07-01T12:00:00Z").unwrap()
for zone in ["Europe/London", "America/Los_Angeles", "Asia/Tokyo"] (
  auto z = launch.inTimezone(zone).unwrap()
  log zone z.format("HH:mm z")
)
log launch.inTimezone("America/Los_Angeles").unwrap().iso()
Click the code to edit it

A zoned value can format and write iso() with its offset, tell you its timezone(), and give back the plain instant with toDateTime(). To do arithmetic, go back to a DateTime, do it, and convert again. An unknown zone name returns an error result, so a user-supplied zone should be checked:

main.osl
import "std:date"

auto d = date.parse("2024-07-01T12:00:00Z").unwrap()
log d.inTimezone("Nowhere/Land").isErr()
Click the code to edit it

The current time#

date.now() is the current instant, and now.unix() is a timestamp in seconds. The value differs every run, so print facts about it, not the value:

main.osl
import "std:date"

auto now = date.now()
auto earlier = now.subtract("d", 1)
log now.after(earlier)
log floor(earlier.until(now).hours())
log now.unix() > 1700000000
Click the code to edit it

To time how long something takes, subtract two instants: date.now().since(start).millis().

Quick check

What does this print?

osl
auto d = date.parse("2024-01-31T10:00:00Z").unwrap()
log d.add("mo", 1).iso()
Your turn

Days until the deadline

A project is due on 2025-03-31. Given today's date, print how many whole days are left and the weekday the deadline falls on.

main.osl
Ctrl Enter runs
Expected output
45
Monday

API reference#

47 members, generated from the std:date source of OSL 9.3.0.

Functions

#date.daysInMonth(int year, int month) int

daysInMonth returns how many days month (1 to 12) of year has. Throws ValueError for any other month.

#date.duration(int64 value) date.Duration

Deprecated. Deprecated: use date.millis(ms) instead; it takes milliseconds.

duration returns a duration of value microseconds.

#date.fromUnix(int64 seconds) date.DateTime

fromUnix returns the instant seconds after the Unix epoch (1970-01-01 UTC); negative values are before it.

#date.fromUnixMicros(int64 us) date.DateTime

fromUnixMicros returns the instant us microseconds after the Unix epoch.

#date.fromUnixMillis(int64 ms) date.DateTime

fromUnixMillis returns the instant ms milliseconds after the Unix epoch.

#date.fromUnixMs(int64 ms) date.DateTime

Deprecated. Deprecated: use date.fromUnixMillis(ms) instead.

fromUnixMs returns the instant ms milliseconds after the Unix epoch.

#date.fromUnixUs(int64 us) date.DateTime

Deprecated. Deprecated: use date.fromUnixMicros(us) instead.

fromUnixUs returns the instant us microseconds after the Unix epoch.

#date.isLeap(int year) boolean

isLeap reports whether year has a 29 February.

#date.millis(number ms) date.Duration

millis returns a duration of ms milliseconds; fractions keep microsecond precision. Throws ValueError for NaN or an infinite or out-of-range ms.

#date.now() date.DateTime

now returns the current instant.

#date.nowUs() int64

Deprecated. Deprecated: use date.now().unixMicros() instead.

nowUs returns the current time in microseconds since the Unix epoch.

#date.parse(string text, object? options) result<date.DateTime, error>

parse reads a date or date-time from text. Without a format it accepts ISO 8601 / RFC 3339 forms: "2024-03-01", "2024-03-01T12:30", "2024-03-01 12:30:05", "2024-03-01T12:30:05.250Z" and "2024-03-01T12:30:05+02:00". Options: format (a layout using the same tokens as dt.format, such as "DD/MM/YYYY HH:mm"; [text] and \x are literal) and timezone (the IANA zone for text without an offset, default "UTC"). Missing fields default to the start of the period. Fails with ParseError when text doesn't match; throws ValueError for an unknown time zone or a layout token that can't be parsed (z).

Constants

int date.day

Day is one day in milliseconds, 86400000. A calendar day across a daylight saving change can be an hour longer or shorter.

int date.hour

Hour is one hour in milliseconds, 3600000.

int date.minute

Minute is one minute in milliseconds, 60000.

int date.second

Second is one second in milliseconds, 1000.

int date.week

Week is seven days in milliseconds, 604800000.

*date.DateTime

#dateTime.add(string unit, number value) date.DateTime

add returns the instant moved forward by value units, leaving dt alone. unit is "us", "ms", "s", "m", "h", "d", "mo" or "y", or the long name ("minutes", "day", ...). Months and years move by calendar (31 January plus a month is 2 or 3 March) and need whole numbers; days are 24 hours in UTC. Throws ValueError for an unknown unit, a fractional month or year, or a result out of range.

#dateTime.addDuration(date.Duration duration) date.DateTime

addDuration returns the instant moved by duration. Throws ValueError when the result is out of range.

#dateTime.after(date.DateTime other) boolean

after reports whether dt is later than other.

#dateTime.before(date.DateTime other) boolean

before reports whether dt is earlier than other.

#dateTime.compare(date.DateTime other) int

compare returns -1, 0 or 1 as dt is before, equal to or after other.

#dateTime.equals(date.DateTime other) boolean

equals reports whether dt and other are the same instant.

#dateTime.format(string layout) string

format writes the instant in UTC using layout tokens: YYYY YY (year), MMMM MMM MM M (month), DD D (day), dddd ddd (weekday), HH H (24-hour), hh h (12-hour), A a (AM/PM), mm m (minute), ss s (second), SSS SSSSSS (fraction), Z ZZ (offset), z (zone name), X (unix seconds) and x (unix microseconds). [text] and \x are written literally.

#dateTime.inTimezone(string tz) result<date.ZonedDateTime, error>

inTimezone returns a view of the instant in the IANA time zone tz, such as "Europe/London", for formatting local times. Fails with ValueError for an unknown zone.

#dateTime.iso() string

iso writes the instant as RFC 3339 text in UTC, such as "2024-03-01T12:30:00Z".

#dateTime.round(string unit) date.DateTime

Deprecated. Deprecated: use dt.truncate(unit) instead.

round returns the start of the UTC period that dt falls in; despite the name it never rounds up.

#dateTime.since(date.DateTime other) date.Duration

since returns the time from other to dt, negative when other is later.

#dateTime.subtract(string unit, number value) date.DateTime

subtract returns the instant moved back by value units; see dt.add.

#dateTime.truncate(string unit) date.DateTime

truncate returns the start of the UTC day, hour, minute, second or millisecond ("d", "h", "m", "s", "ms" or their long names) that dt falls in, leaving dt alone. Throws ValueError for any other unit.

#dateTime.unix() int64

unix returns the instant in whole seconds since the Unix epoch, rounded down.

#dateTime.unixMicros() int64

unixMicros returns the instant in microseconds since the Unix epoch.

#dateTime.unixMillis() int64

unixMillis returns the instant in whole milliseconds since the Unix epoch, rounded down.

#dateTime.unixMs() int64

Deprecated. Deprecated: use dt.unixMillis() instead.

unixMs returns the instant in milliseconds since the Unix epoch.

#dateTime.unixUs() int64

Deprecated. Deprecated: use dt.unixMicros() instead.

unixUs returns the instant in microseconds since the Unix epoch.

#dateTime.until(date.DateTime other) date.Duration

until returns the time from dt to other, negative when other is earlier.

#dateTime.with(string field, int value) date.DateTime

with returns the instant with one UTC calendar field replaced, leaving dt alone. field is "year", "month" (1 to 12), "day", "hour", "minute" or "second", or the short unit name ("y", "mo", "d", "h", "m", "s"). Out-of-range values roll over, as 31 February becomes 2 or 3 March. Throws ValueError for an unknown field or a result out of range.

*date.Duration

#duration.days() number

days returns the duration in 24-hour days.

#duration.hours() number

hours returns the duration in hours.

#duration.micros() int64

micros returns the duration in whole microseconds.

#duration.millis() int64

millis returns the duration in whole milliseconds, rounded toward zero.

#duration.minutes() number

minutes returns the duration in minutes.

#duration.seconds() number

seconds returns the duration in seconds.

*date.ZonedDateTime

#zonedDateTime.format(string layout) string

format writes the instant in the zone using the layout tokens of dt.format.

#zonedDateTime.iso() string

iso writes the instant as RFC 3339 text in the zone, with its offset, such as "2024-03-01T07:30:00-05:00".

#zonedDateTime.timezone() string

timezone returns the zone's IANA name, such as "Europe/London".

#zonedDateTime.toDateTime() date.DateTime

toDateTime returns the instant without its zone.