import gleam/int import gleam/option.{type Option} import gleam/result import glrss_parser/util import party as p pub type RssDate { RssDate( day_of_week: Option(DayOfWeek), year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, offset_minutes: Int, ) } pub type DayOfWeek { Monday Tuesday Wednesday Thursday Friday Saturday Sunday } pub fn dow_from(str: String) -> Result(DayOfWeek, Nil) { case str { "Mon" | "Monday" -> Ok(Monday) "Tue" | "Tuesday" -> Ok(Tuesday) "Wed" | "Wednesday" -> Ok(Wednesday) "Thu" | "Thursday" -> Ok(Thursday) "Fri" | "Friday" -> Ok(Friday) "Sat" | "Saturday" -> Ok(Saturday) "Sun" | "Sunday" -> Ok(Sunday) _ -> Error(Nil) } } pub fn dow_to_short(from dow: DayOfWeek) -> String { case dow { Monday -> "Mon" Tuesday -> "Tue" Wednesday -> "Wed" Thursday -> "Thu" Friday -> "Fri" Saturday -> "Sat" Sunday -> "Sun" } } pub fn empty() -> RssDate { RssDate(option.None, 0, 0, 0, 0, 0, 0, 0) } /// Parses [RFC 822](https://www.rfc-editor.org/rfc/rfc822.html#section-5) dates. /// Currently it does not use the minute zimezone offset. pub fn parse(str: String) { p.go(date_time(), str) } // #### SCARY PARSING STUFF ##### // [ day "," ] date time fn date_time() { //opt(day() |> t(w1())) |> t(date()) |> w1t(time()) use dow <- p.do(p.perhaps(day())) use Date(day:, month:, year:) <- p.do(date()) use _ <- p.do(p.whitespace1()) use Time(hour: t_hour, zone: Zone(offset_minutes)) <- p.do(time()) let Hour(hour:, minute:, second:) = t_hour let day_of_week = dow |> option.from_result() |> option.flatten() p.return(RssDate( day_of_week:, year:, month:, day:, hour:, minute:, second:, offset_minutes:, )) } /// day "," /// day = "Mon" / "Tue" / "Wed" / "Thu" /// / "Fri" / "Sat" / "Sun" fn day() { use str <- p.do(p.many1_concat(p.letter())) use _ <- p.do(p.seq(p.char(","), p.whitespace1())) str |> dow_from() |> option.from_result() |> p.return() } // date = [d]d MMM [yy]yy fn date() { use day <- p.do(p_integer()) use _ <- p.do(p.whitespace1()) use month <- p.do(p_letters() |> p.map(month3_to_int)) use _ <- p.do(p.whitespace1()) use year <- p.do(p_integer()) case day, month, year { Ok(day), Ok(month), Ok(year) -> p.return(Date(day:, month:, year:)) _, _, _ -> p.fail() } } type Date { Date(day: Int, month: Int, year: Int) } fn month3_to_int(month: String) { case month { "Jan" -> Ok(1) "Feb" -> Ok(2) "Mar" -> Ok(3) "Apr" -> Ok(4) "May" -> Ok(5) "Jun" -> Ok(6) "Jul" -> Ok(7) "Aug" -> Ok(8) "Sep" -> Ok(9) "Oct" -> Ok(10) "Nov" -> Ok(11) "Dec" -> Ok(12) _ -> Error(Nil) } } fn time() { use hour <- p.do(hour()) use _ <- p.do(p.whitespace1()) use zone <- p.do(zone()) p.return(Time(hour:, zone:)) } type Time { Time(hour: Hour, zone: Zone) } fn hour() { let colon = p.char(":") use hour <- p.do(p_integer()) use _ <- p.do(colon) use minute <- p.do(p_integer()) use second <- p.do(p.perhaps(p.seq(colon, p_integer()))) let second = second |> result.flatten() |> result.unwrap(0) case hour, minute { Ok(hour), Ok(minute) -> p.return(Hour(hour:, minute:, second:)) _, _ -> p.fail() } } type Hour { Hour(hour: Int, minute: Int, second: Int) } fn zone() { p.either(zone_str(), zone_int()) } fn zone_str() { let str_or_empty = fn(res) { result.unwrap(res, "") } use c1 <- p.do(p.letter()) use c2 <- p.do(p.perhaps(p.letter()) |> p.map(str_or_empty)) use c3 <- p.do(p.perhaps(p.letter()) |> p.map(str_or_empty)) let zone = c1 <> c2 <> c3 case zone_str_to_hour(zone) { Ok(hour) -> p.return(Zone(minutes: hour * 60)) Error(_) -> p.fail() } } fn zone_int() { let digit = p.digit() use sign <- p.do(p.either(p.char("+"), p.char("-"))) use hour_tens <- p.do(digit) use hour_ones <- p.do(digit) use min_tens <- p.do(digit) use min_ones <- p.do(digit) use hour <- util.try_unwrap(int.parse(hour_tens <> hour_ones), p.fail()) use min <- result.try(int.parse(min_tens <> min_ones)) let sign = case sign { "+" -> 1 "-" -> -1 _ -> panic as "unreachable" } Ok(p.return(Zone(minutes: sign * { hour * 60 + min }))) } type Zone { Zone(minutes: Int) } fn zone_str_to_hour(zone: String) { case zone { "Z" | "UT" | "GMT" -> Ok(0) "A" -> Ok(-1) "B" -> Ok(-2) "C" -> Ok(-3) "D" | "EDT" -> Ok(-4) "E" | "EST" | "CDT" -> Ok(-5) "F" | "CST" | "MDT" -> Ok(-6) "G" | "MST" | "PDT" -> Ok(-7) "H" | "PST" -> Ok(-8) "I" -> Ok(-9) "K" -> Ok(-10) "L" -> Ok(-11) "M" -> Ok(-12) "N" -> Ok(1) "O" -> Ok(2) "P" -> Ok(3) "Q" -> Ok(4) "R" -> Ok(5) "S" -> Ok(6) "T" -> Ok(7) "U" -> Ok(8) "V" -> Ok(9) "W" -> Ok(10) "X" -> Ok(11) "Y" -> Ok(12) _ -> Error(Nil) } } fn p_integer() { p.digits() |> p.map(int.parse) } fn p_letters() { p.many1_concat(p.letter()) } // fn offset_to_utc(rss_date: RssDate, zone: Zone) -> RssDate { // let RssDate(day_of_week:, year:, month:, day:, hour:, minute:, second:) = // rss_date // // let #(#(year, month, day), #(hour, minute, second)) = // // do_offset_to_utc( // // #(#(year, month, day), #(hour, minute, second)), // // zone.minutes, // // ) // let dt // RssDate(day_of_week:, year:, month:, day:, hour:, minute:, second:) // } // @external(erlang, "glrss_parser_ffi", "offset_to_utc") // @external(javascript, "../../../glrss_parser_ffi.mjs", "offsetToUtc") // fn do_offset_to_utc( // date_time: #(#(Int, Int, Int), #(Int, Int, Int)), // offset_minutes: Int, // ) -> #(#(Int, Int, Int), #(Int, Int, Int))