README
dates
Calendar helpers for SSPUR, built on the builtin Time, Duration and Zone: start and end of a day, week, month, quarter or year (in UTC or in a time zone, with DST handled), business days with holidays, the nth weekday of a month, Easter, ISO 8601 week and ordinal dates, date ranges, intervals, calendar differences and ages, ISO 8601 durations, strptime-style parsing, RFC 3339, RFC 2822 and HTTP dates, and relative descriptions such as "yesterday" or "last Tuesday".
Every function is pure. Functions that work in a zone take the Zone as an argument (from the builtin time_zone(name), fixed_zone(minutes) or local_zone()), so nothing reads the clock; pass now explicitly where a function needs it. Weekdays are numbered as Time.weekday numbers them: 1 is Monday, 7 is Sunday.
Example
use dates.{add_business_days, standard_calendar, nth_weekday, iso_week, format_iso_week, end_of_month_in, parse_http_date, parse_iso_duration, add_period, relative_date, date_range, Months}
fn main() -> Unit ! log, time
= do
ny = time_zone("America/New_York").or(fixed_zone(-300))
t = parse_time("2026-10-16T16:30:00Z").or(time_ms(0))
cal = standard_calendar([date(2026, 10, 19).or(t)])
log("due: {add_business_days(t, 3, cal)}")
log("thanksgiving: {nth_weekday(2026, 11, 4, 4)}")
log("week: {format_iso_week(t)} ({iso_week(t).week})")
log("month ends in New York at {end_of_month_in(t, ny)}")
log("http: {parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT")}")
log("plus P1M2D: {parse_iso_duration("P1M2D").map(p => add_period(t, p))}")
log("{relative_date(parse_time("2026-10-13T09:00:00Z").or(t), t, ny)}")
log("{date_range(date(2026, 1, 31).or(t), date(2026, 5, 1).or(t), 1, Months).map(_.day)}")
due: 2026-10-22T16:30:00Z
thanksgiving: some(2026-11-26T00:00:00Z)
week: 2026-W42-5 (42)
month ends in New York at 2026-11-01T03:59:59.999Z
http: some(1994-11-06T08:49:37Z)
plus P1M2D: some(2026-11-18T16:30:00Z)
last Tuesday
[31, 28, 31, 30]
(main performs time only because time_zone reads the system's zone database.)
API
Types
Period: years, months, weeks, days, hours, minutes, seconds and millis, the parts of an ISO 8601 duration; parts may be negative.Interval: a half-open span {start, end}, start included, end excluded.IsoWeek: an ISO week date {year, week, weekday}.CalUnit: Days | Weeks | Months | Quarters | Years, the unit of add_units and ranges.BusinessCalendar: {weekend, holidays}, the weekend weekdays and the holiday dates.
Calendar facts
is_leap_year(y), days_in_month(y, m), days_in_year(y): proleptic Gregorian.quarter(t): 1 to 4.weekday_name(d), weekday_abbr(d), month_name(m), month_abbr(m): English names.epoch_day(t), from_epoch_day(n): days since 1970-01-01, rounded down.same_day(a, b), same_day_in(a, b, z): same UTC or local date.is_weekend(t): Saturday or Sunday.
Boundaries
- UTC:
start_of_day, end_of_day, start_of_week(t, first), end_of_week(t, first), start_of_month, end_of_month, start_of_quarter, end_of_quarter, start_of_year, end_of_year. Ends are the last millisecond (23:59:59.999). first is the weekday weeks start on (1 for ISO, 7 for the US). - In a zone: the same names with
_in and a Zone (start_of_day_in(t, z), end_of_week_in(t, z, first), ...). A local day can be 23 or 25 hours long; where local midnight falls in a DST gap the day starts at the first instant after the gap. resolve_local(z, wall): the instant a wall-clock time shows in zone z: the earlier one in an overlap, the moment the clocks jump in a gap.local_date(t, z): the local date of t, as midnight UTC.
Business days
standard_calendar(holidays): Saturday and Sunday off, plus the holidays.is_business_day(t, cal).add_business_days(t, n, cal): forward or backward, keeping the time of day; n == 0 returns t.business_days_between(a, b, cal): from a's date (included) to b's (excluded), negative when reversed; O(holidays), not O(days).next_business_day, previous_business_day, roll_forward, roll_backward.
Weekdays and holidays
next_weekday(t, d), previous_weekday(t, d): strictly after or before, 1 to 7 days away.next_or_same_weekday(t, d), previous_or_same_weekday(t, d).nth_weekday(y, m, d, n): the 3rd Monday (n = 3) or the last Friday (n = -1); none for a missing fifth one.last_weekday(y, m, d).easter(y): Western Easter Sunday (anonymous Gregorian algorithm).
ISO week and ordinal dates
iso_week(t), iso_weeks_in_year(y) (52 or 53), from_iso_week(y, w, d).format_iso_week(t) ("2026-W42-5"), parse_iso_week(s) (extended or basic, weekday optional).format_ordinal_date(t) ("2026-289"), from_ordinal(y, n), parse_ordinal_date(s).
Ranges and differences
add_units(t, n, unit): months, quarters and years clamp the day to the month's end.date_range(start, end, every, unit): end excluded; date_range_inclusive includes it. Each step is computed from start, so monthly ranges from the 31st don't drift.days_between(a, b), months_between(a, b), years_between(a, b): whole calendar units.age(birth, on): whole years; a February 29 birthday counts on February 28 in common years.diff(a, b): the calendar difference as a Period, so add_period(a, diff(a, b)) == b.
Intervals
interval(a, b): none when b < a.interval_length, interval_is_empty, covers(iv, t), encloses(outer, inner), overlaps, abuts, intersection, gap, hull, merge_intervals(xs).
Periods and ISO 8601 durations
period_zero(), negate_period(p), period_is_zero(p).add_period(t, p): years and months first (clamping), then the exact parts.period_duration(p): none when it has years or months; duration_period(d).format_iso_duration(p) ("P1Y2M3DT4H5M6.5S", "-P1D", "PT0S"), parse_iso_duration(s).
Parsing and formatting
unix_seconds(t), from_unix_seconds(s).format_rfc3339(t, z), parse_rfc3339(s): strict, requires seconds and an offset.format_rfc2822(t, z), parse_rfc2822(s): e-mail dates, including the obsolete zone names and two-digit years of RFC 5322.format_http_date(t), parse_http_date(s): IMF-fixdate, and the RFC 850 and asctime forms recipients must accept (RFC 9110).parse_format(s, pat): strptime with the directives Time.format writes (%Y %m %d %H %M %S %L %j %u %a %A %b %B %F %T %%) plus %z. Fixed widths, so it reads back what Time.format writes.
Relative descriptions
relative_date(t, now, z): "today", "yesterday", "tomorrow", "last Tuesday", "next Friday", "last week", "in 2 weeks", "last month", "in 3 years".relative_days(t, now, z): the same with numbers: "3 days ago", "in 5 days", "1 week ago".
Out of scope
- Reading the clock: pass
now() yourself, so the functions stay pure and testable. - Locale-aware names and formats: the builtin
Locale (format_date, format_date_long) covers six locales; names here are English. - Non-Gregorian calendars (Julian, Hebrew, Islamic, Chinese), Orthodox Easter, and holiday tables for particular countries (pass the holidays in a
BusinessCalendar). - Leap seconds, which
Time can't represent (parse_rfc3339 rejects ":60"). - Duration-based phrases such as "3 minutes ago" or "about 2 hours": see the
humanize package. This package describes calendar days. - Cron expressions and recurrence rules (RFC 5545 RRULE), as in
python-dateutil.rrule; date_range covers fixed steps. - Time zone abbreviations in parsing (
%Z): they are ambiguous (CST is three different zones).
Compared with Python's datetime and dateutil, Java's java.time and npm's date-fns, the boundaries, business days, ISO weeks, ranges, intervals, periods and RFC formats are covered; formatting itself is the builtin Time.format and Time.format_in.
Install
sspur add dates
MIT licensed.
API
pub type Period = {years: Int, months: Int, weeks: Int, days: Int, hours: Int, minutes: Int, seconds: Int, millis: Int} derive Eq, Show
A calendar period: the parts of an ISO 8601 duration. Years and months have no fixed length, so they are applied with Time.add_months (clamping the day to the month's end); weeks and days are 7 and 1 days of 24 hours; the rest are exact. Parts may be negative.
pub type Interval = {start: Time, end: Time} derive Eq, Show
A half-open span of time, from start (included) to end (excluded), with start <= end.
pub type IsoWeek = {year: Int, week: Int, weekday: Int} derive Eq, Show
An ISO 8601 week date: the ISO week-numbering year, the week (1 to 53) and the weekday (1 is Monday).
pub type CalUnit = Days | Weeks | Months | Quarters | Years derive Eq, Show
The unit of a step in a date range, or of add_units.
pub type BusinessCalendar = {weekend: List[Int], holidays: List[Time]} derive Eq, Show
Which days are business days: the weekdays of the weekend (1 is Monday, 7 Sunday) and a list of holidays (only their UTC dates count).
fn is_leap_year(y: Int) -> Bool
Whether y is a leap year in the proleptic Gregorian calendar (year 0 is a leap year).
fn days_in_month(y: Int, m: Int where _ >= 1 and _ <= 12) -> Int
The number of days in month m (1 to 12) of year y.
fn days_in_year(y: Int) -> Int
The number of days in year y: 365 or 366.
fn quarter(t: Time) -> Int
The quarter of the year t is in (UTC): 1 for January to March, up to 4.
fn weekday_name(d: Int where _ >= 1 and _ <= 7) -> Str
The English name of weekday d (1 is Monday): "Monday" to "Sunday".
fn weekday_abbr(d: Int where _ >= 1 and _ <= 7) -> Str
The three-letter English abbreviation of weekday d: "Mon" to "Sun".
fn month_name(m: Int where _ >= 1 and _ <= 12) -> Str
The English name of month m: "January" to "December".
fn month_abbr(m: Int where _ >= 1 and _ <= 12) -> Str
The three-letter English abbreviation of month m: "Jan" to "Dec".
fn epoch_day(t: Time) -> Int
The number of days from 1970-01-01 to the UTC date of t, rounded down (negative before 1970).
fn from_epoch_day(n: Int) -> Time
Midnight UTC of the date n days after 1970-01-01.
fn same_day(a: Time, b: Time) -> Bool
Whether a and b fall on the same UTC date.
fn same_day_in(a: Time, b: Time, z: Zone) -> Bool
Whether a and b fall on the same local date in zone z.
fn is_weekend(t: Time) -> Bool
Whether t is on a Saturday or a Sunday (UTC).
fn start_of_day(t: Time) -> Time
Midnight UTC at the start of t's date.
fn end_of_day(t: Time) -> Time
The last millisecond of t's UTC date (23:59:59.999).
fn start_of_week(t: Time, first: Int where _ >= 1 and _ <= 7) -> Time
Midnight at the start of the week containing t, where weeks start on weekday first (1 is Monday, as in ISO 8601; 7 is Sunday, as in the US).
fn end_of_week(t: Time, first: Int where _ >= 1 and _ <= 7) -> Time
The last millisecond of the week containing t, where weeks start on weekday first.
fn start_of_month(t: Time) -> Time
Midnight UTC on the first day of t's month.
fn end_of_month(t: Time) -> Time
The last millisecond of t's month (UTC).
fn start_of_quarter(t: Time) -> Time
Midnight UTC on the first day of t's quarter (January, April, July or October 1).
fn end_of_quarter(t: Time) -> Time
The last millisecond of t's quarter (UTC).
fn start_of_year(t: Time) -> Time
Midnight UTC on January 1 of t's year.
fn end_of_year(t: Time) -> Time
The last millisecond of t's year (UTC): December 31, 23:59:59.999.
fn resolve_local(z: Zone, wall: Time) -> Time
The instant at which the wall clock of zone z shows wall (a Time whose UTC fields are read as local fields, as Zone.local returns). In an overlap (clocks turned back) it is the earlier instant; in a gap (clocks turned forward, so wall never shows) it is the first instant after the gap, the moment the clocks jump.
fn start_of_day_in(t: Time, z: Zone) -> Time
The first instant of t's local date in zone z (usually local midnight; where midnight falls in a DST gap, the first instant after the gap).
fn end_of_day_in(t: Time, z: Zone) -> Time
The last millisecond of t's local date in zone z: one millisecond before the start of the next local date, so a day can be 23 or 25 hours long.
fn start_of_week_in(t: Time, z: Zone, first: Int where _ >= 1 and _ <= 7) -> Time
The first instant of the local week containing t in zone z, weeks starting on first.
fn end_of_week_in(t: Time, z: Zone, first: Int where _ >= 1 and _ <= 7) -> Time
The last millisecond of the local week containing t in zone z, weeks starting on first.
fn start_of_month_in(t: Time, z: Zone) -> Time
The first instant of t's local month in zone z.
fn end_of_month_in(t: Time, z: Zone) -> Time
The last millisecond of t's local month in zone z.
fn start_of_quarter_in(t: Time, z: Zone) -> Time
The first instant of t's local quarter in zone z.
fn end_of_quarter_in(t: Time, z: Zone) -> Time
The last millisecond of t's local quarter in zone z.
fn start_of_year_in(t: Time, z: Zone) -> Time
The first instant of t's local year in zone z.
fn end_of_year_in(t: Time, z: Zone) -> Time
The last millisecond of t's local year in zone z.
fn local_date(t: Time, z: Zone) -> Time
The local date of t in zone z, as midnight UTC of that date (for date arithmetic and comparisons that should ignore the zone afterwards).
fn standard_calendar(holidays: List[Time]) -> BusinessCalendar
A calendar with Saturday and Sunday as the weekend and the given holidays.
fn is_business_day(t: Time, cal: BusinessCalendar) -> Bool
Whether the UTC date of t is a business day of cal: not a weekend day and not a holiday.
fn add_business_days(t: Time, n: Int, cal: BusinessCalendar) -> Time
t moved by n business days of cal (backwards when n is negative), keeping the time of day. Counting starts from the day after (or before) t, so t itself need not be a business day; n == 0 returns t unchanged. The weekend must leave at least one business day a week.
fn business_days_between(a: Time, b: Time, cal: BusinessCalendar) -> Int
The number of business days of cal from the date of a (included) to the date of b (excluded); negative when b is before a. Times of day are ignored.
fn next_business_day(t: Time, cal: BusinessCalendar) -> Time
The first business day of cal strictly after t's date, keeping the time of day.
fn previous_business_day(t: Time, cal: BusinessCalendar) -> Time
The last business day of cal strictly before t's date, keeping the time of day.
fn roll_forward(t: Time, cal: BusinessCalendar) -> Time
t if it is a business day of cal, else the next business day (the "following" convention).
fn roll_backward(t: Time, cal: BusinessCalendar) -> Time
t if it is a business day of cal, else the previous business day ("preceding").
fn next_weekday(t: Time, d: Int where _ >= 1 and _ <= 7) -> Time
The first instant after t that is on weekday d, at the same time of day: 1 to 7 days later.
fn previous_weekday(t: Time, d: Int where _ >= 1 and _ <= 7) -> Time
The last instant before t that is on weekday d, at the same time of day: 1 to 7 days earlier.
fn next_or_same_weekday(t: Time, d: Int where _ >= 1 and _ <= 7) -> Time
t if it is on weekday d, else next_weekday(t, d).
fn previous_or_same_weekday(t: Time, d: Int where _ >= 1 and _ <= 7) -> Time
t if it is on weekday d, else previous_weekday(t, d).
fn nth_weekday(y: Int, m: Int where _ >= 1 and _ <= 12, d: Int where _ >= 1 and _ <= 7, n: Int where _ != 0 and _ >= -5 and _ <= 5) -> Opt[Time]
The nth weekday d of month m of year y, at midnight UTC: n is 1 to 5 counting from the start (3 and Monday is the third Monday), or -1 to -5 counting from the end (-1 and Friday is the last Friday). none when the month has no such day (a fifth Monday).
fn last_weekday(y: Int, m: Int where _ >= 1 and _ <= 12, d: Int where _ >= 1 and _ <= 7) -> Time
The last weekday d of month m of year y, at midnight UTC (the last Friday).
fn easter(y: Int where _ >= 1) -> Time
Western (Gregorian) Easter Sunday of year y, at midnight UTC, by the anonymous Gregorian algorithm (Meeus, Jones, Butcher). It is computed for any year from 1, proleptically before 1583.
fn iso_week(t: Time) -> IsoWeek
The ISO 8601 week date of t (UTC): weeks start on Monday and week 1 is the week with the year's first Thursday, so early January can be in the previous year's week 52 or 53 and late December in the next year's week 1.
fn iso_weeks_in_year(y: Int) -> Int
The number of ISO weeks in ISO year y: 53 when January 1 is a Thursday, or a Wednesday in a leap year, else 52.
fn from_iso_week(y: Int, w: Int, d: Int) -> Opt[Time]
Midnight UTC of ISO week date (y, w, d); none when the week or weekday is out of range.
fn format_iso_week(t: Time) -> Str
The ISO week date of t as text: "2026-W42-5".
fn parse_iso_week(s: Str) -> Opt[Time]
Parses an ISO week date, extended ("2026-W42-5", "2026-W42") or basic ("2026W425", "2026W42"); a missing weekday is Monday. none for anything else or a week the year doesn't have.
fn format_ordinal_date(t: Time) -> Str
The ordinal date of t (UTC) as text: year and day of the year, "2026-289".
fn from_ordinal(y: Int, n: Int) -> Opt[Time]
Midnight UTC of day n (1 to 365 or 366) of year y; none when n is out of range.
fn parse_ordinal_date(s: Str) -> Opt[Time]
Parses an ordinal date, "2026-289" or "2026289".
fn add_units(t: Time, n: Int, unit: CalUnit) -> Time
t moved by n units (backwards when negative). Months, quarters and years use Time.add_months, which clamps the day to the end of the month (January 31 plus one month is February 28 or 29); days and weeks are exact multiples of 24 hours.
fn date_range(start: Time, end: Time, every: Int where _ > 0, unit: CalUnit) -> List[Time]
The times start, start + every units, start + 2 * every units, ... that are before end (end excluded); empty when end <= start. Each element is computed from start (add_units(start, k * every, unit)), so a monthly range from January 31 gives February 28, March 31, April 30 rather than drifting to the 28th.
fn date_range_inclusive(start: Time, end: Time, every: Int where _ > 0, unit: CalUnit) -> List[Time]
Like date_range but end is included when the steps land on it exactly.
fn days_between(a: Time, b: Time) -> Int
The number of calendar days from the UTC date of a to the UTC date of b (negative when b is earlier); times of day are ignored.
fn months_between(a: Time, b: Time) -> Int
The number of whole months from a to b: the largest m with a.add_months(m) <= b, or the negation of months_between(b, a) when b is before a.
fn years_between(a: Time, b: Time) -> Int
The number of whole years from a to b, by the rule of months_between.
fn age(birth: Time, on: Time) -> Int
The age in whole years on date on of someone born on birth, from their dates (UTC; times of day are ignored). A February 29 birthday is reached on February 28 in common years. Negative when on is before birth.
fn diff(a: Time, b: Time) -> Period
The calendar difference from a to b as years, months, days, hours, minutes, seconds and milliseconds (weeks 0), such that add_period(a, diff(a, b)) == b when a <= b (except when a month end clamps). When b is before a every part is negated from diff(b, a).
fn interval(a: Time, b: Time) -> Opt[Interval]
The interval from a to b; none when b is before a.
fn interval_length(iv: Interval) -> Duration
The length of interval iv.
fn interval_is_empty(iv: Interval) -> Bool
Whether iv is empty (its start equals its end).
fn covers(iv: Interval, t: Time) -> Bool
Whether instant t is in iv: start <= t < end.
fn encloses(outer: Interval, inner: Interval) -> Bool
Whether outer contains all of inner.
fn overlaps(a: Interval, b: Interval) -> Bool
Whether a and b share some instant (empty intervals overlap nothing).
fn abuts(a: Interval, b: Interval) -> Bool
Whether one interval ends exactly where the other starts.
fn intersection(a: Interval, b: Interval) -> Opt[Interval]
The part a and b share; none when they don't overlap.
fn gap(a: Interval, b: Interval) -> Opt[Interval]
The interval between a and b when they are apart; none when they overlap or abut.
fn hull(a: Interval, b: Interval) -> Interval
The smallest interval that contains both a and b.
fn merge_intervals(xs: List[Interval]) -> List[Interval]
The intervals sorted by start, with overlapping and abutting ones merged; empty ones dropped.
fn period_zero() -> Period
The period with every part 0.
fn negate_period(p: Period) -> Period
The period with every part of p negated.
fn period_is_zero(p: Period) -> Bool
Whether every part of p is 0.
fn add_period(t: Time, p: Period) -> Time
t moved by period p: first the years and months together with Time.add_months (clamping the day to the month's end), then the weeks, days, hours, minutes, seconds and milliseconds as exact durations.
fn period_duration(p: Period) -> Opt[Duration]
The exact length of p when it has no years or months (days count 24 hours); none otherwise.
fn duration_period(d: Duration) -> Period
Duration d split into days, hours, minutes, seconds and milliseconds (no weeks, months or years); every part has the sign of d.
fn format_iso_duration(p: Period) -> Str
p as an ISO 8601 duration: "P1Y2M3DT4H5M6.5S", "P2W", "PT0S" for zero. When every non-zero part is negative the text starts with "-" ("-P1D"); mixed signs are written per part ("P1M-2D"), as java.time does. Milliseconds are written as a fraction of the seconds.
fn parse_iso_duration(s: Str) -> Opt[Period]
Parses an ISO 8601 duration: "P1Y2M10DT2H30M", "PT1.5S", "P3W", "-P1D", "P1M-2D". Units must come in the order Y M W D, then "T" and H M S; at least one part is required, and "T" must be followed by one. Only seconds may have a fraction ("." or ","), kept to milliseconds and truncated beyond. A leading "-" negates every part. none for anything else, or for a value that does not fit an Int.
fn unix_seconds(t: Time) -> Int
The Unix time of t in whole seconds, rounded down.
fn from_unix_seconds(s: Int) -> Opt[Time]
The time s seconds after 1970-01-01T00:00:00Z; none when s * 1000 overflows an Int.
fn format_rfc3339(t: Time, z: Zone) -> Str
t as RFC 3339 text in zone z, with milliseconds only when they are not 0: "2026-10-16T21:43:27+05:30", "2026-10-16T16:13:27.123Z" in UTC.
fn parse_rfc3339(s: Str) -> Opt[Time]
Parses an RFC 3339 timestamp: date, "T" (or "t" or a space), hours, minutes and seconds, an optional fraction (kept to milliseconds, truncated beyond), and "Z" or an offset "+05:30". A leap second (":60") is rejected, as Time can't hold one. Stricter than the builtin parse_time, which also takes dates alone and offsets without minutes.
fn format_rfc2822(t: Time, z: Zone) -> Str
t as an RFC 5322 (RFC 2822) date in zone z: "Fri, 16 Oct 2026 21:43:27 +0530".
fn parse_rfc2822(s: Str) -> Opt[Time]
Parses an RFC 5322 (RFC 2822) date: an optional weekday and comma, a day of one or two digits, an English month abbreviation, a year (four digits; the obsolete two-digit years are 2000 to 2049 for 00 to 49 and 1950 to 1999 for 50 to 99, three-digit years add 1900), hours and minutes with optional seconds, and a zone: "+0530", "-0000", or the obsolete "UT", "GMT", "Z", "EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT" (military letters count as +0000, as RFC 5322 says). Names are case-insensitive, comments in parentheses and extra whitespace are ignored. A weekday that doesn't match the date gives none.
fn format_http_date(t: Time) -> Str
t as an HTTP date (IMF-fixdate, RFC 9110 section 5.6.7), always in GMT: "Sun, 06 Nov 1994 08:49:37 GMT".
fn parse_http_date(s: Str) -> Opt[Time]
Parses an HTTP date in the three forms RFC 9110 requires recipients to accept: IMF-fixdate ("Sun, 06 Nov 1994 08:49:37 GMT"), the obsolete RFC 850 form ("Sunday, 06-Nov-94 08:49:37 GMT", two-digit years 00 to 69 are 2000 to 2069 and 70 to 99 are 1970 to 1999) and asctime ("Sun Nov 6 08:49:37 1994"). Names are case-sensitive, as the RFC specifies. A weekday that doesn't match the date gives none.
fn parse_format(s: Str, pat: Str) -> Opt[Time]
Parses s by strftime pattern pat, the directives Time.format writes plus %z: %Y (four digits), %m %d %H %M %S (two digits), %L (three-digit milliseconds), %j (three-digit day of the year), %u (weekday digit, 1 is Monday), %a %A %b %B (English names, case-insensitive), %F (%Y-%m-%d), %T (%H:%M:%S), %z ("Z", "+0530" or "+05:30") and %%; every other character must match itself. Missing fields default to 1970-01-01 00:00:00.000 UTC; %j sets the month and day unless %m or %d is given, and then they must agree. A weekday that disagrees with the date, an unknown directive, an invalid date or leftover text gives none. Zone names (%Z) can't be parsed, since abbreviations are ambiguous.
fn relative_date(t: Time, now: Time, z: Zone) -> Str
How the local date of t relates to the local date of now in zone z, in words: "today", "yesterday", "tomorrow", a weekday within six days ("last Tuesday", "next Friday"), "last week" or "next week" (7 to 13 days), "3 weeks ago" or "in 2 weeks" (under a month), "last month", "in 5 months", "last year", "in 3 years". Times of day are ignored; whole months and years are counted by months_between of the local dates.
fn relative_days(t: Time, now: Time, z: Zone) -> Str
Like relative_date but with numbers instead of weekday names and "last"/"next": "today", "yesterday", "tomorrow", "3 days ago", "in 5 days", "1 week ago", "in 2 months", "4 years ago".