///|
fn digit(ch : Char) -> Int? {
match ch {
'0' => Some(0)
'1' => Some(1)
'2' => Some(2)
'3' => Some(3)
'4' => Some(4)
'5' => Some(5)
'6' => Some(6)
'7' => Some(7)
'8' => Some(8)
'9' => Some(9)
_ => None
}
}
///|
fn number(chars : Array[Char], start : Int, end : Int) -> Int? {
if start >= end || start < 0 || end > chars.length() {
return None
}
let mut value = 0
for index = start; index < end; index = index + 1 {
match digit(chars[index]) {
Some(d) => value = value * 10 + d
None => return None
}
}
Some(value)
}
///|
fn parse_number(value : String) -> Int? {
let chars = value.to_array()
number(chars, 0, chars.length())
}
///|
fn leap_year(year : Int) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
fn month_length(year : Int, month : Int) -> Int {
match month {
2 => if leap_year(year) { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
fn validate_date(
year : Int,
month : Int,
day : Int,
) -> Unit raise DateTimeError {
if year < 1 || year > 9999 {
raise InvalidYear
}
if month < 1 || month > 12 {
raise InvalidMonth
}
if day < 1 || day > month_length(year, month) {
raise InvalidDay
}
}
///|
fn validate_clock(
hour : Int,
minute : Int,
second : Int,
) -> Unit raise DateTimeError {
if hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 59 {
raise InvalidTime
}
}
///|
/// Return Gregorian calendar days relative to 1970-01-01, then apply the numeric offset.
fn to_unix_seconds(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
second : Int,
offset_seconds : Int,
) -> Int64 {
let y = if month <= 2 { year - 1 } else { year }
let era = y / 400
let year_of_era = y - era * 400
let month_prime = if month > 2 { month - 3 } else { month + 9 }
let day_of_year = (153 * month_prime + 2) / 5 + day - 1
let day_of_era = year_of_era * 365 +
year_of_era / 4 -
year_of_era / 100 +
day_of_year
let days = era * 146097 + day_of_era - 719468
Int64::from_int(days) * 86400L +
Int64::from_int(hour * 3600 + minute * 60 + second - offset_seconds)
}
///|
fn signed_offset(
chars : Array[Char],
start : Int,
colonized : Bool,
) -> Int raise DateTimeError {
let expected = if colonized { 6 } else { 5 }
if start + expected != chars.length() ||
(chars[start] != '+' && chars[start] != '-') {
raise InvalidOffset
}
let hour = match number(chars, start + 1, start + 3) {
Some(value) => value
None => raise InvalidOffset
}
let minute_start = if colonized {
if chars[start + 3] != ':' {
raise InvalidOffset
}
start + 4
} else {
start + 3
}
let minute = match number(chars, minute_start, minute_start + 2) {
Some(value) => value
None => raise InvalidOffset
}
if hour > 23 || minute > 59 {
raise InvalidOffset
}
let magnitude = hour * 3600 + minute * 60
if chars[start] == '+' {
magnitude
} else {
-magnitude
}
}
///|
fn rfc3339_offset(chars : Array[Char], start : Int) -> Int raise DateTimeError {
if start >= chars.length() {
raise InvalidFormat
}
match chars[start] {
'Z' | 'z' =>
if start + 1 == chars.length() {
0
} else {
raise InvalidFormat
}
'+' | '-' => signed_offset(chars, start, true)
_ => raise InvalidFormat
}
}
///|
/// Parse strict RFC 3339 timestamps with optional fractional seconds and explicit UTC offset.
pub fn parse_rfc3339(input : String) -> DateTime raise DateTimeError {
let chars = input.to_array()
if chars.length() < 20 ||
chars[4] != '-' ||
chars[7] != '-' ||
(chars[10] != 'T' && chars[10] != 't') ||
chars[13] != ':' ||
chars[16] != ':' {
raise InvalidFormat
}
let year = match number(chars, 0, 4) {
Some(value) => value
None => raise InvalidYear
}
let month = match number(chars, 5, 7) {
Some(value) => value
None => raise InvalidMonth
}
let day = match number(chars, 8, 10) {
Some(value) => value
None => raise InvalidDay
}
validate_date(year, month, day)
let hour = match number(chars, 11, 13) {
Some(value) => value
None => raise InvalidTime
}
let minute = match number(chars, 14, 16) {
Some(value) => value
None => raise InvalidTime
}
let second = match number(chars, 17, 19) {
Some(value) => value
None => raise InvalidTime
}
validate_clock(hour, minute, second)
let mut zone_start = 19
if zone_start < chars.length() && chars[zone_start] == '.' {
zone_start += 1
let fraction_start = zone_start
while zone_start < chars.length() && digit(chars[zone_start]) is Some(_) {
zone_start += 1
}
if zone_start == fraction_start {
raise InvalidTime
}
}
let offset = rfc3339_offset(chars, zone_start)
{
raw: input,
unix_seconds: to_unix_seconds(
year, month, day, hour, minute, second, offset,
),
}
}
///|
fn rss_month(value : String) -> Int? {
match value {
"Jan" | "jan" => Some(1)
"Feb" | "feb" => Some(2)
"Mar" | "mar" => Some(3)
"Apr" | "apr" => Some(4)
"May" | "may" => Some(5)
"Jun" | "jun" => Some(6)
"Jul" | "jul" => Some(7)
"Aug" | "aug" => Some(8)
"Sep" | "sep" => Some(9)
"Oct" | "oct" => Some(10)
"Nov" | "nov" => Some(11)
"Dec" | "dec" => Some(12)
_ => None
}
}
///|
fn weekday_token(value : String) -> Bool {
match value {
"Mon," | "Tue," | "Wed," | "Thu," | "Fri," | "Sat," | "Sun," => true
_ => false
}
}
///|
fn whitespace(ch : Char) -> Bool {
ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
}
///|
fn tokens(input : String) -> Array[String] {
let result = []
let mut part = StringBuilder()
let mut active = false
for ch in input.to_array() {
if whitespace(ch) {
if active {
result.push(part.to_string())
part = StringBuilder()
active = false
}
} else {
part.write_char(ch)
active = true
}
}
if active {
result.push(part.to_string())
}
result
}
///|
fn rss_offset(value : String) -> Int raise DateTimeError {
match value {
"GMT" | "UTC" | "gmt" | "utc" => 0
_ => {
let chars = value.to_array()
if chars.is_empty() {
raise InvalidOffset
}
if chars[0] == '+' || chars[0] == '-' {
signed_offset(chars, 0, chars.length() == 6)
} else {
raise UnsupportedTimezone(value)
}
}
}
}
///|
/// Parse common RSS/RFC 822 and RFC 1123 dates. Unknown timezone abbreviations are rejected.
pub fn parse_rss_datetime(input : String) -> DateTime raise DateTimeError {
let parts = tokens(input.trim(chars=" \t\r\n").to_owned())
let start = if !parts.is_empty() && weekday_token(parts[0]) { 1 } else { 0 }
if parts.length() - start != 5 {
raise InvalidFormat
}
let day = match parse_number(parts[start]) {
Some(value) => value
None => raise InvalidDay
}
let month = match rss_month(parts[start + 1]) {
Some(value) => value
None => raise InvalidMonth
}
let year = match parse_number(parts[start + 2]) {
Some(value) => value
None => raise InvalidYear
}
validate_date(year, month, day)
let time = parts[start + 3].to_array()
let colons = []
for index, ch in time {
if ch == ':' {
colons.push(index)
}
}
if colons.length() != 1 && colons.length() != 2 {
raise InvalidTime
}
let hour = match number(time, 0, colons[0]) {
Some(value) => value
None => raise InvalidTime
}
let minute_end = if colons.length() == 2 { colons[1] } else { time.length() }
let minute = match number(time, colons[0] + 1, minute_end) {
Some(value) => value
None => raise InvalidTime
}
let second = if colons.length() == 2 {
match number(time, colons[1] + 1, time.length()) {
Some(value) => value
None => raise InvalidTime
}
} else {
0
}
validate_clock(hour, minute, second)
let offset = rss_offset(parts[start + 4])
{
raw: input,
unix_seconds: to_unix_seconds(
year, month, day, hour, minute, second, offset,
),
}
}
///|
/// Parse RFC 3339 first, then common RSS/RFC 822 date spellings.
pub fn parse_datetime(input : String) -> DateTime raise DateTimeError {
let chars = input.to_array()
if chars.length() > 10 &&
chars[4] == '-' &&
(chars[10] == 'T' || chars[10] == 't') {
parse_rfc3339(input)
} else {
parse_rss_datetime(input)
}
}
///|
/// Parse an optional source date without rejecting the containing feed when it is malformed.
pub fn parse_optional_datetime(input : String?) -> DateTime? {
match input {
Some(value) => Some(parse_datetime(value)) catch { _ => None }
None => None
}
}