///|
/// Current wall-clock time in epoch milliseconds.
pub fn now_ms() -> UInt64 {
@env.now()
}
///|
/// Current Unix time in whole seconds.
pub fn now_unix() -> UInt64 {
@env.now() / 1000UL
}
///|
/// Decompose epoch milliseconds into UTC components:
/// (year, month, day, hour, minute, second, millisecond).
fn civil_from_ms(
epoch_ms : UInt64,
) -> (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64) {
let ms = epoch_ms % 1000UL
let secs = epoch_ms / 1000UL
let days = secs / 86400UL
let rem = secs % 86400UL
// civil-from-days algorithm (Howard Hinnant, public domain)
let z = days + 719468UL
let era = z / 146097UL
let doe = z - era * 146097UL
let yoe = (doe - doe / 1460UL + doe / 36524UL - doe / 146096UL) / 365UL
let y0 = yoe + era * 400UL
let doy = doe - (365UL * yoe + yoe / 4UL - yoe / 100UL)
let mp = (5UL * doy + 2UL) / 153UL
let day = doy - (153UL * mp + 2UL) / 5UL + 1UL
let month = if mp < 10UL { mp + 3UL } else { mp - 9UL }
let year = if month <= 2UL { y0 + 1UL } else { y0 }
(year, month, day, rem / 3600UL, rem % 3600UL / 60UL, rem % 60UL, ms)
}
///|
/// UTC year of `epoch_ms`.
pub fn year(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).0.to_int()
}
///|
/// UTC month of `epoch_ms`, 1-12.
pub fn month(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).1.to_int()
}
///|
/// UTC day of month of `epoch_ms`, 1-31.
pub fn day(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).2.to_int()
}
///|
/// UTC hour of `epoch_ms`, 0-23.
pub fn hour(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).3.to_int()
}
///|
/// UTC minute of `epoch_ms`, 0-59.
pub fn minute(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).4.to_int()
}
///|
/// UTC second of `epoch_ms`, 0-59.
pub fn second(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).5.to_int()
}
///|
/// UTC millisecond of `epoch_ms`, 0-999.
pub fn millisecond(epoch_ms : UInt64) -> Int {
civil_from_ms(epoch_ms).6.to_int()
}
///|
/// Format epoch milliseconds as an ISO 8601 UTC timestamp:
/// `2026-10-03T08:15:30.123Z`.
pub fn iso8601(epoch_ms : UInt64) -> String {
let c = civil_from_ms(epoch_ms)
pad4(c.0) +
"-" +
pad2(c.1) +
"-" +
pad2(c.2) +
"T" +
pad2(c.3) +
":" +
pad2(c.4) +
":" +
pad2(c.5) +
"." +
pad3(c.6) +
"Z"
}
///|
fn pad2(n : UInt64) -> String {
if n < 10UL {
"0" + n.to_string()
} else {
n.to_string()
}
}
///|
fn pad3(n : UInt64) -> String {
if n < 10UL {
"00" + n.to_string()
} else if n < 100UL {
"0" + n.to_string()
} else {
n.to_string()
}
}
///|
fn pad4(n : UInt64) -> String {
if n < 1000UL {
"0" + pad3(n)
} else {
n.to_string()
}
}
///|
/// Two decimal digits at a known offset of an ASCII string. Only ever used
/// on the ISO timestamp produced by `iso8601`, so byte and char offsets
/// coincide.
fn two_digits(text : String, start : Int) -> Int {
let mut value = 0
let mut index = 0
for c in text {
if index >= start && index < start + 2 {
value = value * 10 + c.to_int() - 48
}
index += 1
}
value
}
///|
/// Format epoch milliseconds as an RFC 5322 date-time header:
/// `Sat, 03 Oct 2026 08:15:30 +0000`. Email needs the weekday and month
/// names spelled out, which the ISO timestamp does not carry.
pub fn rfc5322(epoch_ms : UInt64) -> String {
let weekdays = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]
let months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
"Dec",
]
let iso = iso8601(epoch_ms)
let days = epoch_ms / 1000UL / 86400UL
weekdays[(days % 7UL).to_int()] +
", " +
iso[8:10].to_owned() +
" " +
months[two_digits(iso, 5) - 1] +
" " +
iso[:4].to_owned() +
" " +
iso[11:19].to_owned() +
" +0000"
}
///|
/// days since 1970-01-01 for a proleptic Gregorian date
/// (days_from_civil algorithm, Howard Hinnant, public domain).
fn days_from_civil(y_ : Int64, m_ : Int64, d_ : Int64) -> Int64 {
let y = if m_ <= 2 { y_ - 1 } else { y_ }
let era = (if y >= 0 { y } else { y - 399 }) / 400
let yoe = y - era * 400
let mp = if m_ > 2 { m_ - 3 } else { m_ + 9 }
let doy = (153L * mp + 2L) / 5L + d_ - 1L
let doe = yoe * 365L + yoe / 4L - yoe / 100L + doy
era * 146097 + doe - 719468
}
///|
/// Construct epoch milliseconds from UTC components. Out-of-range
/// components roll over naturally (like `make_epoch(2026, 13, 1)` being
/// January of the next year).
pub fn make_epoch(
year : Int,
month : Int,
day : Int,
hour? : Int = 0,
minute? : Int = 0,
second? : Int = 0,
millisecond? : Int = 0,
) -> UInt64 {
let days = days_from_civil(year.to_int64(), month.to_int64(), day.to_int64())
let secs = days * 86400L +
hour.to_int64() * 3600L +
minute.to_int64() * 60L +
second.to_int64()
(secs * 1000L + millisecond.to_int64()).reinterpret_as_uint64()
}
///|
/// Gregorian leap-year rule.
pub fn is_leap_year(year : Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
/// Number of days in the given UTC month, 0 for an invalid month.
pub fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 => 31
2 => if is_leap_year(year) { 29 } else { 28 }
3 => 31
4 => 30
5 => 31
6 => 30
7 => 31
8 => 31
9 => 30
10 => 31
11 => 30
12 => 31
_ => 0
}
}
///|
/// Shift `epoch_ms` by a number of days (negative shifts backwards).
pub fn add_days(epoch_ms : UInt64, days : Int) -> UInt64 {
epoch_ms + (days.to_int64() * 86400000L).reinterpret_as_uint64()
}
///|
/// Shift `epoch_ms` by a number of hours (negative shifts backwards).
pub fn add_hours(epoch_ms : UInt64, hours : Int) -> UInt64 {
epoch_ms + (hours.to_int64() * 3600000L).reinterpret_as_uint64()
}
///|
/// Shift `epoch_ms` by a number of minutes (negative shifts backwards).
pub fn add_minutes(epoch_ms : UInt64, minutes : Int) -> UInt64 {
epoch_ms + (minutes.to_int64() * 60000L).reinterpret_as_uint64()
}
///|
/// Shift `epoch_ms` by a number of seconds (negative shifts backwards).
pub fn add_seconds(epoch_ms : UInt64, seconds : Int) -> UInt64 {
epoch_ms + (seconds.to_int64() * 1000L).reinterpret_as_uint64()
}
///|
/// Epoch milliseconds of UTC midnight for the day of `epoch_ms`.
pub fn start_of_day(epoch_ms : UInt64) -> UInt64 {
epoch_ms - epoch_ms % 86400000UL
}
///|
/// True when both instants fall on the same UTC day.
pub fn is_same_day(a : UInt64, b : UInt64) -> Bool {
a / 86400000UL == b / 86400000UL
}