///| ISO 8601 timestamp parser and formatter
///|
/// Check if a year is a leap year
fn is_leap_year(year : Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
/// Days in each month for a given year
fn days_in_month(month : Int, year : 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
_ => 30
}
}
///|
/// Days in a year
fn days_in_year(year : Int) -> Int {
if is_leap_year(year) {
366
} else {
365
}
}
///|
/// Parse a substring as an integer
fn parse_int_sub(s : String, start : Int, end : Int) -> Int {
let sub = String::unsafe_substring(s, start~, end~)
parse_int(sub)
}
///|
/// Parse ISO 8601 timestamp string to Unix seconds.
/// Supports "YYYY-MM-DDThh:mm:ssZ" and "YYYY-MM-DDThh:mm:ss+00:00" formats.
/// Returns 0 for empty or unparseable strings.
pub fn parse_iso8601_to_unix(iso_str : String) -> Int64 {
if iso_str.length() < 19 {
return 0L
}
let year = parse_int_sub(iso_str, 0, 4)
let month = parse_int_sub(iso_str, 5, 7)
let day = parse_int_sub(iso_str, 8, 10)
let hour = parse_int_sub(iso_str, 11, 13)
let minute = parse_int_sub(iso_str, 14, 16)
let second = parse_int_sub(iso_str, 17, 19)
// Count days from 1970-01-01 to the given date
let mut days = 0L
// Add days for complete years
for y = 1970; y < year; y = y + 1 {
days = days + days_in_year(y).to_int64()
}
// Add days for complete months in the target year
for m = 1; m < month; m = m + 1 {
days = days + days_in_month(m, year).to_int64()
}
// Add remaining days (day is 1-based)
days = days + (day - 1).to_int64()
days * 86400L +
hour.to_int64() * 3600L +
minute.to_int64() * 60L +
second.to_int64()
}
///|
/// Convert Unix seconds to ISO 8601 string in UTC ("YYYY-MM-DDThh:mm:ssZ").
pub fn unix_to_iso8601(unix : Int64) -> String {
let mut remaining = unix
let second = (remaining % 60L).to_int()
remaining = remaining / 60L
let minute = (remaining % 60L).to_int()
remaining = remaining / 60L
let hour = (remaining % 24L).to_int()
let mut days = (remaining / 24L).to_int()
// Compute year
let mut year = 1970
while true {
let diy = days_in_year(year)
if days < diy {
break
}
days = days - diy
year = year + 1
}
// Compute month
let mut month = 1
while month <= 12 {
let dim = days_in_month(month, year)
if days < dim {
break
}
days = days - dim
month = month + 1
}
let day = days + 1
// Format with zero-padding
let buf = StringBuilder::new()
buf.write_string(pad4(year))
buf.write_string("-")
buf.write_string(pad2(month))
buf.write_string("-")
buf.write_string(pad2(day))
buf.write_string("T")
buf.write_string(pad2(hour))
buf.write_string(":")
buf.write_string(pad2(minute))
buf.write_string(":")
buf.write_string(pad2(second))
buf.write_string("Z")
buf.to_string()
}
///|
/// Zero-pad an integer to 2 digits
fn pad2(n : Int) -> String {
if n < 10 {
"0" + n.to_string()
} else {
n.to_string()
}
}
///|
/// Zero-pad an integer to 4 digits
fn pad4(n : Int) -> String {
if n < 10 {
"000" + n.to_string()
} else if n < 100 {
"00" + n.to_string()
} else if n < 1000 {
"0" + n.to_string()
} else {
n.to_string()
}
}