# date

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

This is the Markdown version of https://osl-lang.org/packages/date. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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](/packages/result.md), because the text may not be a date:

```osl
import "std:date"

auto d = date.parse("2024-03-15T14:30:00Z").unwrap()
log d.iso()
log d.unix()
```

Output:

```text
2024-03-15T14:30:00Z
1710513000
```

`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:

```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()
)
```

Output:

```text
2024-03-01T00:00:00Z
2024-03-01T12:30:00Z
2024-03-01T12:30:05Z
2024-03-01T12:30:05Z
2024-03-01T10:30:05Z
```

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:

```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")
)
```

Output:

```text
2024-03-15 -> ok Mar 15
yesterday -> ParseError
2024-02-30 -> ParseError
```

## Parsing other formats

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

```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()
```

Output:

```text
2024-03-15T00:00:00Z
2024-03-05T00:00:00Z
```

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:

```osl
import "std:date"

auto tokyo = date.parse("2024-06-01 09:00", {timezone: "Asia/Tokyo"}).unwrap()
log tokyo.iso()
```

Output:

```text
2024-06-01T00:00:00Z
```

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:

```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")
```

Output:

```text
2024-03-15
15 Mar 2024
Friday, March 15, 2024
14:30:05
2:30 PM
Week Fri
1710513005
```

| Token | Meaning |
| --- | --- |
| `YYYY` `YY` | year |
| `MMMM` `MMM` `MM` `M` | month: name, short name, two digits, one or two digits |
| `DD` `D` | day of the month |
| `dddd` `ddd` | weekday name, short weekday name |
| `HH` `H` | hour, 24-hour clock |
| `hh` `h` and `A` `a` | hour, 12-hour clock, and AM/PM |
| `mm` `m`, `ss` `s` | minute, second |
| `SSS` `SSSSSS` | fraction of a second |
| `Z` `ZZ` `z` | offset (`+02:00`, `+0200`) and zone name |
| `X` | unix 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:

```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")
```

Output:

```text
1970-01-01T00:00:00Z
2023-11-14T22:13:20Z
Monday
```

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

```osl
import "std:date"

log date.daysInMonth(2024, 2) date.daysInMonth(2025, 2)
log date.isLeap(2024) date.isLeap(1900) date.isLeap(2000)
```

Output:

```text
29 28
true false true
```

## 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"`:

```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()
```

Output:

```text
2024-04-04T14:30:00Z
2024-03-17T02:30:00Z
2024-03-15T13:45:00Z
2025-03-15T14:30:00Z
2024-03-15T14:30:00Z
```

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:

```osl
import "std:date"

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

Output:

```text
2024-03-02T10:00:00Z
```

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:

```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()
```

Output:

```text
2000-03-15T14:30:45Z
2024-03-01T14:30:45Z
2024-03-15T00:00:00Z
2024-03-15T14:00:00Z
```

`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:

```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"))
```

Output:

```text
true false true
-1
["2023-01-01","2024-01-01","2024-06-01"]
```

## 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:

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

Output:

```text
176
4236
true
```

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:

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

Output:

```text
2024-03-15T16:30:00Z
7
```

## 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:

```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()
```

Output:

```text
Europe/London 13:00 BST
America/Los_Angeles 05:00 PDT
Asia/Tokyo 21:00 JST
2024-07-01T05:00:00-07:00
```

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:

```osl
import "std:date"

auto d = date.parse("2024-07-01T12:00:00Z").unwrap()
log d.inTimezone("Nowhere/Land").isErr()
```

Output:

```text
true
```

## 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:

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

Output:

```text
true
24
true
```

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

**Quiz.** What does this print?

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

- [ ] `2024-02-29T10:00:00Z`, the last day of February.
- [x] `2024-03-02T10:00:00Z`
- [ ] It throws, because February has no 31st.

Answer: Months follow the calendar and don't clamp, so "February 31" rolls over into March (2024 is a leap year, so two days past the 29th).

### Exercise: 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.

Starting code:

```osl
import "std:date"

auto today = date.parse("2025-02-14").unwrap()
auto due = date.parse("2025-03-31").unwrap()

log today.iso() due.iso()
```

It should print:

```text
45
Monday
```

Hint 1: `today.until(due).days()` gives a number with a fraction. `floor` removes it.

Hint 2: `due.format("dddd")` is the weekday name.

A solution:

```osl
import "std:date"

auto today = date.parse("2025-02-14").unwrap()
auto due = date.parse("2025-03-31").unwrap()

log floor(today.until(due).days())
log due.format("dddd")
```

## API reference

## API reference

Every member of `std:date`, generated from the source of OSL.

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