Dates, times and time zones
The time module turns instants into calendar dates and times of day, and
back: dates and times with no zone, fixed offsets from UTC, time zones with
daylight saving time, and the text forms of RFC 3339, ISO 8601, HTTP and
C’s strftime. Bring it in with import time. The full API is in the
reference.
An instant itself is an os.SystemTime: nanoseconds since
the Unix epoch. time says what date and time that is somewhere, and
which instant a date and time names.
The calendar is the proleptic Gregorian calendar, which extends today’s rules to years before it was adopted, and leap seconds are ignored, as Unix time, Go and Java ignore them: every day has 86400 seconds.
Dates and times of day
Four value types carry no time zone. Each is copied,
compared with ==, < and the other comparisons, and never allocated.
Dateis a year, a month (1 to 12) and a day. It knows itsweekday(),day_of_year()and whether its yearis_leap_year(), and moves withadd_days,add_monthsandadd_years. A month or year later keeps the day when it can and otherwise takes the month’s last day, so a month after January 31 is February 28 or 29.Timeis an hour, minute, second and nanosecond within a day.DateTimeis aDateand aTimetogether: what TOML calls a local date-time. Adding or subtracting anos.Durationmoves it.Weekdayis an enum,MondaytoSunday, whose values are ISO 8601’s numbers, 1 to 7.
import os;
import time;
main(): int {
# A Date is a day on the calendar, with no time of day or zone.
var leap_day = time.Date(2024, 2, 29);
assert(leap_day.weekday() == time.Weekday.Thursday, "2024-02-29 was a Thursday");
assert(leap_day.day_of_year() == 60, "the 60th day of the year");
assert(leap_day.add_years(1) == time.Date(2025, 2, 28), "no February 29 in 2025");
assert(time.Date(2024, 1, 31).add_months(1) == leap_day, "the last day of February");
assert(time.Date(2024, 12, 25).days_since(leap_day) == 300, "days between dates");
# Parts from outside the program are checked, not trusted.
assert(time.Date.checked(2023, 2, 29).is_empty(), "2023 is not a leap year");
assert(time.Date.parse("2024-02-29").value() == leap_day, "ISO 8601 text");
# A DateTime adds a time of day; durations move it.
var start = time.DateTime(2024, 3, 1, 23, 30);
var end = start + os.Duration.from_seconds(90 * 60);
print(end); # 2024-03-02T01:00:00
assert(end.date == time.Date(2024, 3, 2) && end.time == time.Time(1, 0), "past midnight");
assert(start < end, "date times compare");
return 0;
}
The constructors (Date(2024, 2, 29)) panic on parts that name no date or
time, which suits values written in the program. For parts that come from
outside it, checked returns an optional instead, and parse a
Result. Years run from -999999 to 999999, and year 0 is
1 BC.
Offsets and timestamps
Offset is a fixed offset from UTC, east positive: Offset.from_hours(5, 30) is +05:30. OffsetDateTime is a DateTime at an Offset, which
names one instant: RFC 3339’s timestamp and TOML’s offset date-time.
import time;
main(): int {
# An OffsetDateTime is an RFC 3339 timestamp: a date and time at an
# offset from UTC, which together name one instant.
var meeting = time.OffsetDateTime.parse("2024-03-01T09:00:00+01:00").value();
print(meeting.to_utc()); # 2024-03-01T08:00:00Z
assert(meeting.offset == time.Offset.from_hours(1), "the offset it was written at");
# The same instant at two offsets is not ==, but compares as equal.
var utc = meeting.to_utc();
assert(utc != meeting && !(utc < meeting) && !(utc > meeting), "one instant");
assert(utc.to_system_time() == meeting.to_system_time(), "one SystemTime");
# strftime codes lay a date out any way.
assert(meeting.format("%A %e %B %Y, %H:%M %:z") == "Friday 1 March 2024, 09:00 +01:00", "strftime");
assert(meeting.format("%G-W%V-%u") == "2024-W09-5", "the ISO week date");
# HTTP's Date and Last-Modified headers, both ways.
var header = meeting.to_http_date();
assert(header == "Fri, 01 Mar 2024 08:00:00 GMT", "an IMF-fixdate, in GMT");
assert(time.OffsetDateTime.parse_http_date(header).value() == utc, "and back");
return 0;
}
OffsetDateTime.from_system_time(t, offset)is the instanttatoffset(UTC by default), andto_system_time()goes back.to_offsetandto_utcshow the same instant at another offset.==compares the date, time and offset, so the same instant written at two offsets is not==.<,<=,>and>=compare instants.DateTime.from_system_time(t)andto_system_time()read aDateTimeas UTC.
Text
Each type’s to_string() writes ISO 8601, and its static parse reads it:
| Type | Text |
|---|---|
Date | 2024-02-29; outside the years 0 to 9999, signed: -0001-12-31, +10000-01-01 |
Time | 08:30:00, with the fraction of a second when there is one: 08:30:00.25 |
DateTime | 2024-03-01T09:00:00 |
Offset | +01:00; parse also takes Z, +0100 and +01 |
OffsetDateTime | 2024-03-01T09:00:00+01:00, Z for UTC |
parse also reads what TOML allows: a space or t for the T, z for
the Z, and a time without its seconds (09:00). Digits of a fraction
past the nanosecond are dropped.
format(pattern) lays a value out with strftime’s % codes, such as
%Y-%m-%d, %A, %B and %H:%M; the module
reference lists them all. Names are English
whatever the locale. %z, %:z and %s need an offset, so only an
OffsetDateTime takes them; an unknown code panics.
HTTP dates (RFC 9110) go both ways: to_http_date() writes the
IMF-fixdate of Date and Last-Modified headers, Fri, 01 Mar 2024 08:00:00 GMT, and OffsetDateTime.parse_http_date reads it and the two
obsolete forms servers must still accept, RFC 850’s and C’s asctime.
Time zones
A TimeZone gives the offset from UTC at each instant in one place:
TimeZone.utc(), andTimeZone.fixed(offset)for an offset that never changes.TimeZone.local(), the system’s zone: theTZenvironment variable or/etc/localtimeon Linux and macOS, as the C library’slocaltimereads them, and the zone in Windows’ settings. WebAssembly has no local zone, so it is UTC there.TimeZone.named("Europe/Paris"), a zone of the IANA time zone database, with its daylight saving time and history, or an error when the system does not know the name.
zone.offset_at(t) is the offset at the instant t, and zone.at(t) its
date and time there. zone.at_local(date_time) goes the other way, from
a date and time on the zone’s clocks to the instant. Where the clocks go
back, a time happens twice, and at_local picks the earlier; where they go
forward, a time never happens, and it moves on by the length of the gap.
time.now() is the current date and time in the local zone, and
time.now_utc() in UTC.
import os;
import time;
main(): int {
# The local zone and UTC are always there.
var now = os.SystemTime.now();
print($"here: {time.TimeZone.local().at(now)}");
print($"UTC: {time.now_utc()}");
# Named zones come from the system's time zone database.
var found = time.TimeZone.named("Europe/Paris");
if (found is Ok(paris)) {
var bastille = time.OffsetDateTime.parse("2024-07-14T10:00:00Z").value();
assert(paris.at(bastille.to_system_time()).to_string() == "2024-07-14T12:00:00+02:00", "summer time");
# A local time in the zone gets the offset in effect then.
var christmas = paris.at_local(time.DateTime(2024, 12, 25, 18, 0));
assert(christmas.offset == time.Offset.from_hours(1), "winter time");
# 02:30 never happens on the night the clocks go forward: it moves
# on by the hour that was skipped.
var skipped = paris.at_local(time.DateTime(2024, 3, 31, 2, 30));
assert(skipped.to_string() == "2024-03-31T03:30:00+02:00", "the gap");
}
assert(time.TimeZone.named("Nowhere/Atlantis").is_err(), "an unknown zone");
return 0;
}
Nothing is bundled: named zones come from the system.
- On Linux and macOS, the TZif files under
/usr/share/zoneinfo, or the folder theTZDIRenvironment variable names. A minimal Linux container may have none until itstzdatapackage is installed. The rule at the end of each file covers instants after its last listed change. - On Windows, ICU (
icu.dll, part of Windows 10 version 1903 and later), which knows the same names, or the TZif files in the folderTZDIRnames when it is set. - On WebAssembly, TZif files under
/usr/share/zoneinfo(orTZDIR) when the host lends the program that folder.
Current limitations
formatwrites text but nothing reads astrftimepattern back; parse ISO 8601, RFC 3339 and HTTP dates with theparsefunctions.- Names in
formatare English only, and there are no locale formats (%c,%x,%X) or zone abbreviations (%Z). - On Android, named zones are not found: its database is one packed file, not a folder of TZif files.