///|
/// Gregorian UTC components. Not a timezone or leap-second model.
pub(all) struct UtcDateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
millisecond : Int
} derive(Eq, Debug)
///|
fn leap(year : Int) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn month_days(year : Int, month : Int) -> Int {
match month {
2 => if leap(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
/// Construct a UTC instant. Rejects nonexistent dates, leap seconds, and rollover.
pub fn utc(
year : Int,
month : Int,
day : Int,
hour? : Int = 0,
minute? : Int = 0,
second? : Int = 0,
millisecond? : Int = 0,
) -> Result[Instant, InputError] {
if year < 1900 ||
year > 2150 ||
month < 1 ||
month > 12 ||
day < 1 ||
day > month_days(year, month) ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 59 ||
millisecond < 0 ||
millisecond > 999 {
return Err(InvalidDate)
}
let mut days = -25567
for y = 1900; y < year; y = y + 1 {
days += if leap(y) { 366 } else { 365 }
}
for m = 1; m < month; m = m + 1 {
days += month_days(year, m)
}
days += day - 1
instant(
days.to_double() * DayMs +
(hour * 3600000 + minute * 60000 + second * 1000 + millisecond).to_double(),
)
}
///|
/// UTC breakdown. Event solvers can return neighbouring days outside the input domain.
pub fn Instant::to_utc(self : Instant) -> UtcDateTime {
let midnight = @math.floor(self.ms / DayMs)
let mut days = midnight.to_int() + 25567
let mut year = 1900
if days < 0 {
year = 1899
days += 365
}
while days >= (if leap(year) { 366 } else { 365 }) {
days -= if leap(year) { 366 } else { 365 }
year += 1
}
let mut month = 1
while days >= month_days(year, month) {
days -= month_days(year, month)
month += 1
}
let ms = (self.ms - midnight * DayMs).to_int()
{
year,
month,
day: days + 1,
hour: ms / 3600000,
minute: ms / 60000 % 60,
second: ms / 1000 % 60,
millisecond: ms % 1000,
}
}
///|
fn pad(n : Int, width : Int) -> String {
let s = n.to_string()
"0".repeat((width - s.length()).max(0)) + s
}
///|
/// Always YYYY-MM-DDTHH:MM:SS.sssZ; no machine-local timezone conversion.
pub fn Instant::to_iso_utc(self : Instant) -> String {
let d = self.to_utc()
pad(d.year, 4) +
"-" +
pad(d.month, 2) +
"-" +
pad(d.day, 2) +
"T" +
pad(d.hour, 2) +
":" +
pad(d.minute, 2) +
":" +
pad(d.second, 2) +
"." +
pad(d.millisecond, 3) +
"Z"
}
///|
fn decimal_part(s : String, start : Int, len : Int) -> Int? {
let mut n = 0
for i = start; i < start + len; i = i + 1 {
let c = s.at(i).to_int()
if c < 48 || c > 57 {
return None
}
n = n * 10 + c - 48
}
Some(n)
}
///|
/// Strict UTC forms YYYY-MM-DDTHH:MM:SSZ and YYYY-MM-DDTHH:MM:SS.sssZ only.
pub fn parse_utc(s : String) -> Result[Instant, InputError] {
let n = s.length()
if n != 20 && n != 24 {
return Err(InvalidDate)
}
if s.at(4) != 45 ||
s.at(7) != 45 ||
s.at(10) != 84 ||
s.at(13) != 58 ||
s.at(16) != 58 ||
s.at(n - 1) != 90 ||
(n == 24 && s.at(19) != 46) {
return Err(InvalidDate)
}
let parts = [(0, 4), (5, 2), (8, 2), (11, 2), (14, 2), (17, 2)]
let fields = []
for part in parts {
match decimal_part(s, part.0, part.1) {
Some(v) => fields.push(v)
None => return Err(InvalidDate)
}
}
let ms = if n == 24 {
match decimal_part(s, 19 + 1, 3) {
Some(v) => v
None => return Err(InvalidDate)
}
} else {
0
}
utc(
fields[0],
fields[1],
fields[2],
hour=fields[3],
minute=fields[4],
second=fields[5],
millisecond=ms,
)
}