///|
/// Check the RFC 3339 timestamp form used by STAC Item datetime fields.
fn validate_timestamp(value : String) -> Unit raise StacError {
let chars = value.iter().to_array()
if chars.length() < 20 {
raise Invalid("invalid datetime length")
}
let year = timestamp_digits(chars, 0, 4)
let month = timestamp_digits(chars, 5, 2)
let day = timestamp_digits(chars, 8, 2)
let hour = timestamp_digits(chars, 11, 2)
let minute = timestamp_digits(chars, 14, 2)
let second = timestamp_digits(chars, 17, 2)
if chars[4] != '-' ||
chars[7] != '-' ||
(chars[10] != 'T' && chars[10] != 't') ||
chars[13] != ':' ||
chars[16] != ':' {
raise Invalid("invalid datetime separators")
}
if year < 1 ||
month < 1 ||
month > 12 ||
day < 1 ||
day > days_in_month(year, month) ||
hour > 23 ||
minute > 59 ||
second > 60 {
raise Invalid("datetime component out of range")
}
let mut zone_start = 19
if chars[zone_start] == '.' {
zone_start += 1
let fraction_start = zone_start
while zone_start < chars.length() && is_digit(chars[zone_start]) {
zone_start += 1
}
if zone_start == fraction_start {
raise Invalid("empty datetime fraction")
}
}
if zone_start >= chars.length() {
raise Invalid("datetime requires timezone")
}
if chars[zone_start] == 'Z' || chars[zone_start] == 'z' {
if zone_start + 1 != chars.length() {
raise Invalid("invalid datetime suffix")
}
} else if chars[zone_start] == '+' || chars[zone_start] == '-' {
if zone_start + 6 != chars.length() || chars[zone_start + 3] != ':' {
raise Invalid("invalid timezone offset")
}
let offset_hour = timestamp_digits(chars, zone_start + 1, 2)
let offset_minute = timestamp_digits(chars, zone_start + 4, 2)
if offset_hour > 23 || offset_minute > 59 {
raise Invalid("timezone offset out of range")
}
} else {
raise Invalid("invalid timezone marker")
}
}
///|
fn is_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn timestamp_digits(
chars : Array[Char],
start : Int,
count : Int,
) -> Int raise StacError {
if start + count > chars.length() {
raise Invalid("short datetime")
}
let mut value = 0
for i in start..<(start + count) {
if !is_digit(chars[i]) {
raise Invalid("invalid datetime digit")
}
value = value * 10 + chars[i].to_int() - '0'.to_int()
}
value
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
match month {
4 | 6 | 9 | 11 => 30
2 =>
if year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) {
29
} else {
28
}
_ => 31
}
}