///|
fn leap(year : Int32) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn month_days(year : Int32, month : Int32) -> Int32 {
match month {
2 => if leap(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
fn days_before(year : Int32) -> Int32 {
let y = year - 1
365 * y + y / 4 - y / 100 + y / 400
}
///|
fn decimal_at(
text : String,
start : Int32,
size : Int32,
) -> Int32 raise PlistError {
if start < 0 || size > text.length() - start {
raise Invalid("date field truncated")
}
let mut n = 0
for i = start; i < start + size; i = i + 1 {
let c = text[i].to_int()
if c < 48 || c > 57 {
raise Invalid("date field is not decimal")
}
n = n * 10 + c - 48
}
n
}
///|
/// UTC whole-second XML date, returned as seconds from 2001-01-01.
pub fn parse_date(text : String) -> Double raise PlistError {
if text.length() != 20 ||
text[4] != 45 ||
text[7] != 45 ||
text[10] != 84 ||
text[13] != 58 ||
text[16] != 58 ||
text[19] != 90 {
raise Invalid("expected YYYY-MM-DDTHH:MM:SSZ")
}
let y = decimal_at(text, 0, 4)
let m = decimal_at(text, 5, 2)
let d = decimal_at(text, 8, 2)
let h = decimal_at(text, 11, 2)
let minute = decimal_at(text, 14, 2)
let second = decimal_at(text, 17, 2)
if y < 1 ||
m < 1 ||
m > 12 ||
d < 1 ||
d > month_days(y, m) ||
h > 23 ||
minute > 59 ||
second > 59 {
raise Invalid("date component out of range")
}
let mut days = days_before(y) - days_before(2001) + d - 1
for month = 1; month < m; month = month + 1 {
days += month_days(y, month)
}
days.to_double() * 86400.0 + (h * 3600 + minute * 60 + second).to_double()
}
///|
fn padded(n : Int32, width : Int32) -> String {
let s = n.to_string()
let out = StringBuilder()
for i = s.length(); i < width; i = i + 1 {
out.write_char('0')
}
out.write_string(s)
out.to_string()
}
///|
/// Rejects fractional seconds rather than silently losing binary date precision.
pub fn format_date(seconds : Double) -> String raise PlistError {
if seconds.is_nan() || seconds.is_inf() || seconds.floor() != seconds {
raise Unsupported("XML date requires finite whole seconds")
}
let absolute = seconds + days_before(2001).to_double() * 86400.0
if absolute < 0.0 || absolute >= days_before(10000).to_double() * 86400.0 {
raise Unsupported("XML date year must be 0001..9999")
}
let days = (absolute / 86400.0).floor().to_int()
let within = (absolute - days.to_double() * 86400.0).to_int()
let mut lo = 1
let mut hi = 10000
while lo + 1 < hi {
let mid = (lo + hi) / 2
if days_before(mid) <= days {
lo = mid
} else {
hi = mid
}
}
let mut remaining = days - days_before(lo)
let mut month = 1
while remaining >= month_days(lo, month) {
remaining -= month_days(lo, month)
month += 1
}
padded(lo, 4) +
"-" +
padded(month, 2) +
"-" +
padded(remaining + 1, 2) +
"T" +
padded(within / 3600, 2) +
":" +
padded(within / 60 % 60, 2) +
":" +
padded(within % 60, 2) +
"Z"
}