///|
/// Absolute time stored as nanoseconds since the Unix epoch (1970-01-01 UTC).
pub(all) struct Time {
ns : Int64
} derive(Eq, Debug)
///|
let ns_per_second : Int64 = 1_000_000_000L
///|
let seconds_per_day : Int64 = 86_400L
///|
/// Construct from miniSEED header parts (nanos, year, day-of-year, h:m:s).
/// `sec == 60` is a positive leap second, clamped to 59.
pub fn Time::from_header(
nanos : UInt,
year : Int,
doy : Int,
hour : Int,
min : Int,
sec : Int,
) -> Time? {
if nanos >= 1_000_000_000 ||
year < 1677 ||
year > 2262 ||
doy < 1 ||
doy > 366 ||
hour < 0 ||
hour > 23 ||
min < 0 ||
min > 59 {
return None
}
let s = if sec == 60 { 59 } else { sec }
if s < 0 || s > 59 {
return None
}
let md = match day_of_year_to_month_day(year, doy) {
Some(md) => md
None => return None
}
let (month, day) = md
let days = days_from_civil(year, month, day)
let seconds_of_day = (hour * 3600 + min * 60 + s).to_int64()
let total_sec = days.to_int64() * seconds_per_day + seconds_of_day
let ns = total_sec * ns_per_second + nanos.to_int64()
Some({ ns, })
}
///|
pub fn Time::from_epoch_ns(ns : Int64) -> Time {
{ ns, }
}
///|
pub fn Time::to_epoch_ns(self : Time) -> Int64 {
self.ns
}
///|
pub fn Time::add_ns(self : Time, delta : Int64) -> Time {
{ ns: self.ns + delta }
}
///|
pub fn Time::diff_secs(self : Time, other : Time) -> Double {
(self.ns - other.ns).to_double() / ns_per_second.to_double()
}
///|
/// ISO8601 with microsecond precision, UTC: `2026-08-17T08:30:15.123000Z`.
pub fn Time::to_iso8601(self : Time) -> String {
let (days, secs_of_day, micros) = self.to_days_secs()
let (year, month, day) = civil_from_days(days)
let hour = secs_of_day / 3600
let min = secs_of_day % 3600 / 60
let sec = secs_of_day % 60
let micro_str = if micros == 0 {
"000000"
} else {
pad_left(micros.to_string(), 6)
}
"\{pad_left(year.to_string(), 4)}-\{pad_left(month.to_string(), 2)}-\{pad_left(day.to_string(), 2)}T\{pad_left(hour.to_string(), 2)}:\{pad_left(min.to_string(), 2)}:\{pad_left(sec.to_string(), 2)}.\{micro_str}Z"
}
///|
fn Time::to_days_secs(self : Time) -> (Int, Int, Int) {
let mut whole_secs = self.ns / ns_per_second
let mut rem_ns = self.ns % ns_per_second
if rem_ns < 0L {
whole_secs = whole_secs - 1L
rem_ns = rem_ns + ns_per_second
}
let mut days = whole_secs / seconds_per_day
let mut secs_of_day = whole_secs % seconds_per_day
if secs_of_day < 0L {
days = days - 1L
secs_of_day = secs_of_day + seconds_per_day
}
let micros = (rem_ns / 1_000L).to_int()
(days.to_int(), secs_of_day.to_int(), micros)
}
///|
/// Howard Hinnant civil_from_days: days since 1970-01-01 -> (y, m, d).
fn civil_from_days(z : Int) -> (Int, Int, Int) {
let z = z + 719468
let era = (if z >= 0 { z } else { z - 146096 }) / 146097
let doe = z - era * 146097
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
let y = yoe + era * 400
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
let mp = (5 * doy + 2) / 153
let d = doy - (153 * mp + 2) / 5 + 1
let m = if mp < 10 { mp + 3 } else { mp - 9 }
(if m <= 2 { y + 1 } else { y }, m, d)
}
///|
/// Howard Hinnant days_from_civil: (y, m, d) -> days since 1970-01-01.
fn days_from_civil(y : Int, m : Int, d : Int) -> Int {
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 = (153 * mp + 2) / 5 + d - 1
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
era * 146097 + doe - 719468
}
///|
/// day-of-year (1-based) -> (month, day). None when out of range.
fn day_of_year_to_month_day(year : Int, doy : Int) -> (Int, Int)? {
if doy < 1 || doy > 366 {
return None
}
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
if doy == 366 && !leap {
return None
}
let cum : Array[Int] = if leap {
[0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
} else {
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
}
let mut month = 1
while month < 12 && doy > cum[month] {
month = month + 1
}
let day = doy - cum[month - 1]
Some((month, day))
}
///|
/// Left-pad a string to a target length with '0'.
fn pad_left(s : String, len : Int) -> String {
if s.length() >= len {
return s
}
let mut out = ""
let mut i = s.length()
while i < len {
out = out + "0"
i = i + 1
}
out + s
}