///|
fn ascii_digit_at(text : String, index : Int) -> Int? {
if index < 0 || index >= text.length() {
return None
}
let code = text[index].to_int()
if code >= 48 && code <= 57 {
Some(code - 48)
} else {
None
}
}
///|
fn decimal_2(text : String, start : Int) -> Int? {
guard ascii_digit_at(text, start) is Some(a) else { return None }
guard ascii_digit_at(text, start + 1) is Some(b) else { return None }
Some(a * 10 + b)
}
///|
fn decimal_4(text : String, start : Int) -> Int? {
guard ascii_digit_at(text, start) is Some(a) else { return None }
guard ascii_digit_at(text, start + 1) is Some(b) else { return None }
guard ascii_digit_at(text, start + 2) is Some(c) else { return None }
guard ascii_digit_at(text, start + 3) is Some(d) else { return None }
Some(a * 1000 + b * 100 + c * 10 + d)
}
///|
fn is_leap_year(year : Int) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 0
}
}
///|
fn valid_date(year : Int, month : Int, day : Int) -> Bool {
year >= 0 &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= days_in_month(year, month)
}
///|
fn valid_clock(hour : Int, minute : Int, second : Int) -> Bool {
hour >= 0 &&
hour <= 23 &&
minute >= 0 &&
minute <= 59 &&
second >= 0 &&
second <= 60
}
///|
fn valid_zone_offset(hour : Int, minute : Int) -> Bool {
hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
}
///|
fn timestamp_separator(text : String, index : Int) -> Bool {
if index >= text.length() {
false
} else {
let value = text[index]
value == 'T' || value == 't'
}
}
///|
fn parse_fraction_end(text : String, start : Int) -> Int? {
if start >= text.length() || text[start] != '.' {
return Some(start)
}
let mut index = start + 1
let first = index
while index < text.length() {
match ascii_digit_at(text, index) {
Some(_) => index += 1
None => break
}
}
if index == first {
None
} else {
Some(index)
}
}
///|
fn valid_timezone(text : String, start : Int) -> Bool {
if start >= text.length() {
return false
}
let marker = text[start]
if marker == 'Z' || marker == 'z' {
return start + 1 == text.length()
}
if marker != '+' && marker != '-' {
return false
}
if start + 6 != text.length() || text[start + 3] != ':' {
return false
}
guard decimal_2(text, start + 1) is Some(hour) else { return false }
guard decimal_2(text, start + 4) is Some(minute) else { return false }
valid_zone_offset(hour, minute)
}
///|
/// Validate the RFC 3339 profile required by RFC 8927 timestamp values.
pub fn is_rfc3339_timestamp(text : String) -> Bool {
if text.length() < 20 {
return false
}
if text[4] != '-' ||
text[7] != '-' ||
!timestamp_separator(text, 10) ||
text[13] != ':' ||
text[16] != ':' {
return false
}
guard decimal_4(text, 0) is Some(year) else { return false }
guard decimal_2(text, 5) is Some(month) else { return false }
guard decimal_2(text, 8) is Some(day) else { return false }
guard decimal_2(text, 11) is Some(hour) else { return false }
guard decimal_2(text, 14) is Some(minute) else { return false }
guard decimal_2(text, 17) is Some(second) else { return false }
if !valid_date(year, month, day) || !valid_clock(hour, minute, second) {
return false
}
guard parse_fraction_end(text, 19) is Some(zone_start) else { return false }
valid_timezone(text, zone_start)
}
///|
pub fn rfc3339_year(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_4(text, 0)
} else {
None
}
}
///|
pub fn rfc3339_month(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_2(text, 5)
} else {
None
}
}
///|
pub fn rfc3339_day(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_2(text, 8)
} else {
None
}
}
///|
pub fn rfc3339_hour(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_2(text, 11)
} else {
None
}
}
///|
pub fn rfc3339_minute(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_2(text, 14)
} else {
None
}
}
///|
pub fn rfc3339_second(text : String) -> Int? {
if is_rfc3339_timestamp(text) {
decimal_2(text, 17)
} else {
None
}
}