///|
/// Gregorian calendar arithmetic.
///
/// The conversions between a year/month/day triple and a day count relative to
/// the Unix epoch use Neri and Schneider's "Euclidean affine functions and
/// their application to calendar algorithms", the same branch-free formulation
/// `temporal_rs` uses. Temporal's supported year range (`-271821 ..= 275760`)
/// is far wider than the shift window given in the paper, so the epoch is
/// re-centred with an extended shift constant.
///|
/// The computational Rata Die of the Unix epoch.
const EPOCH_COMPUTATIONAL_RATA_DIE : Int64 = 719_468L
///|
/// Days in a full 400-year Gregorian cycle.
const DAYS_IN_A_400Y_CYCLE : Int64 = 146_097L
///|
/// Cycle shift used when converting epoch days back to a date. Chosen so the
/// epoch sits in the middle of the shifted window.
const SHIFT_CONSTANT : Int64 = 3670L
///|
/// Cycle shift used when converting a date to epoch days. Larger than
/// [`SHIFT_CONSTANT`] because the intermediate year may be arbitrarily far
/// outside the supported range before balancing.
const SHIFT_CONSTANT_EXTENDED : Int64 = 5_368_710L
///|
/// Floor division for `Int64`, rounding toward negative infinity.
fn div_euclid(dividend : Int64, divisor : Int64) -> Int64 {
let q = dividend / divisor
if dividend % divisor != 0L && (dividend < 0L) != (divisor < 0L) {
q - 1L
} else {
q
}
}
///|
/// Non-negative remainder for `Int64`.
fn rem_euclid(dividend : Int64, divisor : Int64) -> Int64 {
let r = dividend % divisor
if r < 0L {
r + divisor.abs()
} else {
r
}
}
///|
/// Floor division and non-negative remainder in one step.
fn div_mod(dividend : Int64, divisor : Int64) -> (Int64, Int64) {
(div_euclid(dividend, divisor), rem_euclid(dividend, divisor))
}
///|
/// Floor division for `Int`, rounding toward negative infinity.
fn div_euclid_i(dividend : Int, divisor : Int) -> Int {
let q = dividend / divisor
if dividend % divisor != 0 && (dividend < 0) != (divisor < 0) {
q - 1
} else {
q
}
}
///|
/// Non-negative remainder for `Int`.
fn rem_euclid_i(dividend : Int, divisor : Int) -> Int {
let r = dividend % divisor
if r < 0 {
r + divisor.abs()
} else {
r
}
}
///|
/// Returns whether `year` is a leap year in the proleptic Gregorian calendar.
///
/// ```mbt check
/// test {
/// inspect(@temporal.is_leap_year(2024), content="true")
/// inspect(@temporal.is_leap_year(1900), content="false")
/// inspect(@temporal.is_leap_year(2000), content="true")
/// }
/// ```
pub fn is_leap_year(year : Int) -> Bool {
if year % 4 != 0 {
false
} else if year % 100 != 0 {
true
} else {
year % 400 == 0
}
}
///|
/// Returns the number of days in the given ISO month.
///
/// `month` is 1-based.
///
/// ```mbt check
/// test {
/// inspect(@temporal.iso_days_in_month(2024, 2), content="29")
/// inspect(@temporal.iso_days_in_month(2023, 2), content="28")
/// inspect(@temporal.iso_days_in_month(2023, 12), content="31")
/// }
/// ```
pub fn iso_days_in_month(year : Int, month : Int) -> Int {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
/// Returns the number of days in the given ISO year.
pub fn iso_days_in_year(year : Int) -> Int {
if is_leap_year(year) {
366
} else {
365
}
}
///|
/// Converts a Gregorian year/month/day to days since 1970-01-01.
///
/// `month` is 1-based. The date need not be valid: an out-of-range day is
/// simply carried, which is what the balancing operations rely on.
///
/// ```mbt check
/// test {
/// inspect(@temporal.epoch_days_from_gregorian_date(1970, 1, 1), content="0")
/// inspect(@temporal.epoch_days_from_gregorian_date(1969, 12, 31), content="-1")
/// inspect(
/// @temporal.epoch_days_from_gregorian_date(275760, 9, 14),
/// content="100000001",
/// )
/// }
/// ```
pub fn epoch_days_from_gregorian_date(
year : Int,
month : Int,
day : Int,
) -> Int64 {
let shift = SHIFT_CONSTANT_EXTENDED * DAYS_IN_A_400Y_CYCLE +
EPOCH_COMPUTATIONAL_RATA_DIE
// Shift January and February into the previous computational year so that
// the leap day lands at the end of the cycle.
let j = if month <= 2 { 1L } else { 0L }
let computational_year = year.to_int64() + 400L * SHIFT_CONSTANT_EXTENDED - j
let computational_month = month.to_int64() + 12L * j
let computational_day = day.to_int64() - 1L
let century = computational_year / 100L
let y_star = 1461L * computational_year / 4L - century + century / 4L
let m_star = (979L * computational_month - 2919L) / 32L
y_star + m_star + computational_day - shift
}
///|
/// Converts days since 1970-01-01 to a Gregorian year/month/day triple.
///
/// ```mbt check
/// test {
/// debug_inspect(@temporal.ymd_from_epoch_days(0), content="(1970, 1, 1)")
/// debug_inspect(
/// @temporal.ymd_from_epoch_days(-100000001),
/// content="(-271821, 4, 19)",
/// )
/// }
/// ```
pub fn ymd_from_epoch_days(epoch_days : Int64) -> (Int, Int, Int) {
let rata_die = epoch_days +
EPOCH_COMPUTATIONAL_RATA_DIE +
DAYS_IN_A_400Y_CYCLE * SHIFT_CONSTANT
let year_shift = 400L * SHIFT_CONSTANT
let (year, month, day) = gregorian_ymd(rata_die)
((year - year_shift).to_int(), month.to_int(), day.to_int())
}
///|
/// Neri-Schneider's third set of equations: computational Rata Die to a
/// year/month/day triple in the shifted computational calendar.
fn gregorian_ymd(rata_die : Int64) -> (Int64, Int64, Int64) {
let n1 = 4L * rata_die + 3L
let century = div_euclid(n1, DAYS_IN_A_400Y_CYCLE)
let century_rem = rem_euclid(n1, DAYS_IN_A_400Y_CYCLE)
let n2 = century_rem | 3L
let p2 = 2_939_745L * n2
let year_of_century = p2 / 4_294_967_296L
let day_of_year = p2 % 4_294_967_296L / 2_939_745L / 4L
let year = 100L * century + year_of_century
let n3 = 2141L * day_of_year + 197_913L
let month = n3 / 65_536L
let day = n3 % 65_536L / 2141L
// Undo the January/February shift applied on the way in.
let j = if day_of_year >= 306L { 1L } else { 0L }
(year + j, month - 12L * j, day + 1L)
}
///|
/// Converts epoch milliseconds to a Gregorian year/month/day triple.
pub fn ymd_from_epoch_milliseconds(epoch_ms : Int64) -> (Int, Int, Int) {
ymd_from_epoch_days(div_euclid(epoch_ms, MS_PER_DAY))
}
///|
/// `EpochDaysToEpochMS`: combines a day count and a within-day millisecond
/// offset into epoch milliseconds.
fn epoch_days_to_epoch_ms(day : Int64, time : Int64) -> Int64 {
day * MS_PER_DAY + time
}
///|
/// Returns the ISO day of the week, Monday = 1 through Sunday = 7.
///
/// ```mbt check
/// test {
/// // 1970-01-01 was a Thursday.
/// inspect(@temporal.iso_day_of_week(1970, 1, 1), content="4")
/// }
/// ```
pub fn iso_day_of_week(year : Int, month : Int, day : Int) -> Int {
let epoch_days = epoch_days_from_gregorian_date(year, month, day)
// 1970-01-01 was a Thursday, ISO weekday 4.
(rem_euclid(epoch_days + 3L, 7L) + 1L).to_int()
}
///|
/// Returns the 1-based day of the year.
///
/// ```mbt check
/// test {
/// inspect(@temporal.iso_day_of_year(2024, 3, 1), content="61")
/// }
/// ```
pub fn iso_day_of_year(year : Int, month : Int, day : Int) -> Int {
let start = epoch_days_from_gregorian_date(year, 1, 1)
let current = epoch_days_from_gregorian_date(year, month, day)
(current - start + 1L).to_int()
}
///|
/// Returns the ISO 8601 week-of-year number and the year that week belongs to.
///
/// ISO weeks start on Monday, and week 1 is the week containing the first
/// Thursday of the year, so days at either end of a year can belong to a week
/// of the adjacent year.
///
/// ```mbt check
/// test {
/// // 2021-01-01 was a Friday, falling in week 53 of 2020.
/// debug_inspect(@temporal.iso_week_of_year(2021, 1, 1), content="(53, 2020)")
/// debug_inspect(@temporal.iso_week_of_year(2024, 1, 1), content="(1, 2024)")
/// }
/// ```
pub fn iso_week_of_year(year : Int, month : Int, day : Int) -> (Int, Int) {
let day_of_week = iso_day_of_week(year, month, day)
let day_of_year = iso_day_of_year(year, month, day)
// The Thursday of this week determines which year the week belongs to.
let week = (day_of_year - day_of_week + 10) / 7
if week < 1 {
// Belongs to the last week of the previous year.
let prev = year - 1
let jan1_dow = iso_day_of_week(prev, 1, 1)
let weeks = if jan1_dow == 4 || (is_leap_year(prev) && jan1_dow == 3) {
53
} else {
52
}
(weeks, prev)
} else if week > 52 {
// Only years starting on Thursday, or leap years starting on Wednesday,
// have a 53rd week.
let jan1_dow = iso_day_of_week(year, 1, 1)
if jan1_dow == 4 || (is_leap_year(year) && jan1_dow == 3) {
(53, year)
} else {
(1, year + 1)
}
} else {
(week, year)
}
}