Skip to content
heff

std.period

Periods: amounts of time a calendar counts — a year, a month, a week, a day, an hour — not fixed lengths: a month after January 31 is 29 days on in 2024 and 28 in 2023, so a period has no length until it is laid on a date, and is no std.duration. NodaTime’s Period is the model: a period holds each unit apart, as it was given, and 1 month is not 4 weeks nor 30 days, nor 60 minutes 1 hour, until it is laid on a date or a time (addToDate, addToTime, addToDateTime). Two dates are a period apart in years, months and days (between). A period is a record, its fields in plain sight; ISO 8601 writes one, P1Y2M3W4DT5H6M7.5S. Import it by a name, import std.period as Period, as its names are short.

import std.localdate as LocalDate
import std.period as Period
-- | the day a quarter of a year after the date
-- | >>> maybe "" (\d -> show (nextQuarter d)) (LocalDate.parse "2024-11-30")
-- | "2025-02-28"
nextQuarter : LocalDate.LocalDate -> LocalDate.LocalDate
nextQuarter d = Period.addToDate d (Period.months 3)

Period

type Period =
{ years : Int
, months : Int
, weeks : Int
, days : Int
, hours : Int
, minutes : Int
, seconds : Int
, nanoseconds : Int
}

A period: how many of each unit, each apart, any of them less than none for a period back in time.

zero

zero : Period

The period of nothing: every unit 0.

years

years : Int -> Period

n years.

>>> years 2
{ years = 2, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0, nanoseconds = 0 }

months

months : Int -> Period

n months.

weeks

weeks : Int -> Period

n weeks.

days

days : Int -> Period

n days.

hours

hours : Int -> Period

n hours.

minutes

minutes : Int -> Period

n minutes.

seconds

seconds : Int -> Period

n seconds.

nanoseconds

nanoseconds : Int -> Period

n nanoseconds.

instanceEqualityPeriod

instance instanceEqualityPeriod : Equality Period

Equal unit by unit: 1 hour is not 60 minutes, as NodaTime has it.

>>> hours 1 == hours 1 && hours 1 /= minutes 60
True

instanceAdditionPeriod

instance instanceAdditionPeriod : Addition Period

Added unit by unit, each kept apart.

>>> years 1 + months 14 + days 3
{ years = 1, months = 14, weeks = 0, days = 3, hours = 0, minutes = 0, seconds = 0, nanoseconds = 0 }

instanceNegationPeriod

instance instanceNegationPeriod : Negation Period

Every unit negated.

>>> -(months 1 + days (-2)) == months (-1) + days 2
True

instanceDebugPeriod

instance instanceDebugPeriod : Debug Period

As a record literal writes it, every field.

>>> debug (months (-1))
"{ years = 0, months = -1, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0, nanoseconds = 0 }"

instanceShowPeriod

instance instanceShowPeriod : Show Period

As ISO 8601 writes it, as NodaTime does: P, each date unit that is not 0 (1Y2M3W4D), then, if a time unit is not 0, T and each time unit that is not 0 (5H6M7.5S) — the seconds and the nanoseconds together as seconds and their fraction, without the 0s it ends with. A unit less than none is written with its own - (P-1M-1D), and the period of nothing P0D.

>>> show (years 1 + months 2 + weeks 3 + days 4 + hours 5 + minutes 6 + seconds 7 + nanoseconds 500000000)
"P1Y2M3W4DT5H6M7.5S"
>>> List.map show [zero, days 1, hours 36, months (-1) + days (-1), seconds 1 + nanoseconds (-1500000000), nanoseconds 1, years 1 + minutes (-1)]
["P0D", "P1D", "PT36H", "P-1M-1D", "PT-0.5S", "PT0.000000001S", "P1YT-1M"]

addToDate

addToDate : LocalDate.LocalDate -> Period -> LocalDate.LocalDate

The date the period after it: its years first, then its months, each as std.localdate’s plusYears and plusMonths lay them, the day clamped to the month’s last (2024-01-31 and a month is 2024-02-29), then its weeks and days. A period with any time unit not 0 is abandoned, as NodaTime throws: a date has no time of day to move.

>>> List.map (\p -> maybe "" (\d -> show (addToDate d p)) (LocalDate.parse "2024-01-31")) [months 1, years 1 + months 1, weeks 1 + days (-1), months (-11) + days 1, years (-2025)]
["2024-02-29", "2025-02-28", "2024-02-06", "2023-03-01", "-0001-01-31"]
>>> maybe "" (\d -> show (addToDate d (years 1 + months 1))) (LocalDate.parse "2024-02-29")
"2025-03-28"

addToTime

addToTime : LocalTime.LocalTime -> Period -> LocalTime.LocalTime

The time the period’s time units after it, round the clock: past midnight on into the next day, and before it back into the day before. A period with any date unit not 0 is abandoned, as NodaTime throws: a time of day has no date to move.

>>> List.map (\p -> maybe "" (\t -> show (addToTime t p)) (LocalTime.make 22 30 0)) [hours 1, hours 2, minutes (-1350), hours 24, seconds 1 + nanoseconds (-1), hours 48 + minutes 1]
["23:30:00", "00:30:00", "00:00:00", "22:30:00", "22:30:00.999999999", "22:31:00"]

addToDateTime

addToDateTime : LocalDateTime.LocalDateTime
-> Period
-> LocalDateTime.LocalDateTime

The date-time the period after it: its date units laid on the date as addToDate lays them, then its time units, as a length of time, on the time, the days they go past carried into the date.

>>> List.map (\p -> maybe "" (\t -> show (addToDateTime t p)) (LocalDateTime.parse "2024-01-31T22:30:00")) [months 1 + hours 2, hours (-23), days 1 + minutes 90, hours 8784, nanoseconds (-1)]
["2024-03-01T00:30:00", "2024-01-30T23:30:00", "2024-02-02T00:00:00", "2025-01-31T22:30:00", "2024-01-31T22:29:59.999999999"]

between

between : LocalDate.LocalDate -> LocalDate.LocalDate -> Period

The period from the first date to the second, in years, months and days (no weeks), as NodaTime’s Period.Between with its years, months and days units: as many whole years as do not go past the second, then as many whole months, each as plusYears and plusMonths lay them, then the days left; all less than none if the second comes first. addToDate a (between a b) is b.

>>> List.map (\p -> maybe "" show (maybe Nothing (\a -> maybe Nothing (\b -> Just (between a b)) (LocalDate.parse (snd p))) (LocalDate.parse (fst p)))) [("2024-01-31", "2024-03-01"), ("2024-03-01", "2024-01-31"), ("2020-02-29", "2024-02-28"), ("2024-02-29", "2025-02-28"), ("1969-12-31", "2024-02-29"), ("2024-05-05", "2024-05-05"), ("0000-03-01", "0002-02-28")]
["P1M1D", "P-1M-1D", "P3Y12M", "P1Y", "P54Y2M", "P0D", "P1Y11M27D"]
>>> maybe "" (\a -> show (between a (LocalDate.fromEpochDays 0)) ^ " " ^ show (between (LocalDate.fromEpochDays 0) a)) (LocalDate.make (-1) 3 1)
"P1970Y10M P-1970Y-10M"
>>> List.all (\p -> maybe False (\a -> maybe False (\b -> addToDate a (between a b) == b) (LocalDate.parse (snd p))) (LocalDate.parse (fst p))) [("2024-01-31", "2024-03-01"), ("2024-03-01", "2024-01-31"), ("2024-03-31", "2023-02-28"), ("2023-02-28", "2024-03-31"), ("2000-02-29", "1999-02-28"), ("0000-01-01", "9999-12-31")]
True