///|
/// Checks if a year is a leap year according to Gregorian calendar rules.
fn is_leap_year(year : Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
/// Returns the number of days in the given month of the specified year.
fn days_in_month(year : Int, month : 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
_ => 0
}
}
///|
/// Maps a 3-character day name to weekday index (0 = Monday .. 6 = Sunday).
fn parse_weekday(c0 : Char, c1 : Char, c2 : Char) -> Int64? {
match (c0, c1, c2) {
('M', 'o', 'n') => Some(0L)
('T', 'u', 'e') => Some(1L)
('W', 'e', 'd') => Some(2L)
('T', 'h', 'u') => Some(3L)
('F', 'r', 'i') => Some(4L)
('S', 'a', 't') => Some(5L)
('S', 'u', 'n') => Some(6L)
_ => None
}
}
///|
/// Maps a 3-character month name to 1-based month index (1..12).
fn parse_month(c0 : Char, c1 : Char, c2 : Char) -> Int? {
match (c0, c1, c2) {
('J', 'a', 'n') => Some(1)
('F', 'e', 'b') => Some(2)
('M', 'a', 'r') => Some(3)
('A', 'p', 'r') => Some(4)
('M', 'a', 'y') => Some(5)
('J', 'u', 'n') => Some(6)
('J', 'u', 'l') => Some(7)
('A', 'u', 'g') => Some(8)
('S', 'e', 'p') => Some(9)
('O', 'c', 't') => Some(10)
('N', 'o', 'v') => Some(11)
('D', 'e', 'c') => Some(12)
_ => None
}
}
///|
/// Parses a single ASCII decimal digit.
fn parse_digit(c : Char) -> Int? {
match c {
'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
}
}
///|
/// Parses two ASCII decimal digits into an Int (0..99).
fn parse_2digit(c0 : Char, c1 : Char) -> Int? {
match (parse_digit(c0), parse_digit(c1)) {
(Some(d0), Some(d1)) => Some(d0 * 10 + d1)
_ => None
}
}
///|
/// Parses four ASCII decimal digits into an Int (0..9999).
fn parse_4digit(c0 : Char, c1 : Char, c2 : Char, c3 : Char) -> Int? {
match (parse_digit(c0), parse_digit(c1), parse_digit(c2), parse_digit(c3)) {
(Some(d0), Some(d1), Some(d2), Some(d3)) =>
Some(d0 * 1000 + d1 * 100 + d2 * 10 + d3)
_ => None
}
}
///|
/// Computes days relative to 1970 using complete Gregorian years and months.
fn days_from_civil(year : Int, month : Int, day : Int) -> Int64 {
let previous = (year - 1).to_int64()
let mut days = previous * 365L +
previous / 4L -
previous / 100L +
previous / 400L
for m = 1; m < month; m = m + 1 {
days = days + days_in_month(year, m).to_int64()
}
days + day.to_int64() - 1L - 719162L
}
///|
/// Parses a strict IMF-fixdate HTTP-date string (RFC 9110 Section 5.6.7 / RFC 7231 Section 7.1.1.1)
/// into a Unix timestamp (seconds since 1970-01-01T00:00:00Z).
///
/// Expected exact format: `Sun, 06 Nov 1994 08:49:37 GMT`
/// - Length must be exactly 29 characters.
/// - ASCII case sensitive.
/// - Exact punctuation (comma, spaces, colons, "GMT").
///
/// Validation rules:
/// - Gregorian calendar years in range 1601..9999.
/// - Month and day valid according to Gregorian calendar and leap years.
/// - Time fields: hour 00..23, minute 00..59, second 00..59.
/// - Weekday must match the calculated Gregorian day of week.
///
/// Limitations:
/// - Pure computation with zero IO, system clock access, or external dependencies.
/// - Obsolete formats (RFC 850 / RFC 1036 and ANSI C asctime) are not supported.
/// - Leap seconds (second 60) are deliberately rejected by this strict parser.
/// - Years before 1601 or after 9999 are rejected.
pub fn parse_http_date(value : String) -> Int64? {
if value.length() != 29 {
return None
}
let chars = value.to_array()
if chars.length() != 29 {
return None
}
if chars[3] != ',' ||
chars[4] != ' ' ||
chars[7] != ' ' ||
chars[11] != ' ' ||
chars[16] != ' ' ||
chars[19] != ':' ||
chars[22] != ':' ||
chars[25] != ' ' ||
chars[26] != 'G' ||
chars[27] != 'M' ||
chars[28] != 'T' {
return None
}
let w_opt = parse_weekday(chars[0], chars[1], chars[2])
let d_opt = parse_2digit(chars[5], chars[6])
let m_opt = parse_month(chars[8], chars[9], chars[10])
let y_opt = parse_4digit(chars[12], chars[13], chars[14], chars[15])
let h_opt = parse_2digit(chars[17], chars[18])
let min_opt = parse_2digit(chars[20], chars[21])
let s_opt = parse_2digit(chars[23], chars[24])
match (w_opt, d_opt, m_opt, y_opt, h_opt, min_opt, s_opt) {
(
Some(expected_weekday),
Some(day),
Some(month),
Some(year),
Some(hour),
Some(minute),
Some(second),
) =>
if year < 1601 || year > 9999 {
None
} else if day < 1 || day > days_in_month(year, month) {
None
} else if hour > 23 || minute > 59 || second > 59 {
None
} else {
let days = days_from_civil(year, month, day)
let w = (days + 3L) % 7L
let actual_weekday = if w < 0L { w + 7L } else { w }
if actual_weekday != expected_weekday {
None
} else {
let total_seconds = days * 86400L +
hour.to_int64() * 3600L +
minute.to_int64() * 60L +
second.to_int64()
Some(total_seconds)
}
}
_ => None
}
}