-module(fixdate). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/fixdate.gleam"). -export([to_string/1, parse/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Format and parse HTTP-date timestamps (RFC 9110, section 5.6.7), the\n" " format used by HTTP headers such as `Date`, `Last-Modified`, and\n" " `Expires`.\n" ). -file("src/fixdate.gleam", 39). -spec pad2(integer()) -> binary(). pad2(N) -> gleam@string:pad_start(erlang:integer_to_binary(N), 2, <<"0"/utf8>>). -file("src/fixdate.gleam", 43). -spec pad4(integer()) -> binary(). pad4(N) -> gleam@string:pad_start(erlang:integer_to_binary(N), 4, <<"0"/utf8>>). -file("src/fixdate.gleam", 47). -spec month_name(gleam@time@calendar:month()) -> binary(). month_name(Month) -> case Month of january -> <<"Jan"/utf8>>; february -> <<"Feb"/utf8>>; march -> <<"Mar"/utf8>>; april -> <<"Apr"/utf8>>; may -> <<"May"/utf8>>; june -> <<"Jun"/utf8>>; july -> <<"Jul"/utf8>>; august -> <<"Aug"/utf8>>; september -> <<"Sep"/utf8>>; october -> <<"Oct"/utf8>>; november -> <<"Nov"/utf8>>; december -> <<"Dec"/utf8>> end. -file("src/fixdate.gleam", 77). -spec day_of_week(integer(), gleam@time@calendar:month(), integer()) -> integer(). day_of_week(Year, Month, Day) -> Y = case Month of january -> Year - 1; february -> Year - 1; _ -> Year end, T = case Month of january -> 0; february -> 3; march -> 2; april -> 5; may -> 0; june -> 3; july -> 5; august -> 1; september -> 4; october -> 6; november -> 2; december -> 4 end, (((((Y + (Y div 4)) - (Y div 100)) + (Y div 400)) + T) + Day) rem 7. -file("src/fixdate.gleam", 64). -spec weekday_name(integer()) -> binary(). weekday_name(Dow) -> case Dow of 0 -> <<"Sun"/utf8>>; 1 -> <<"Mon"/utf8>>; 2 -> <<"Tue"/utf8>>; 3 -> <<"Wed"/utf8>>; 4 -> <<"Thu"/utf8>>; 5 -> <<"Fri"/utf8>>; _ -> <<"Sat"/utf8>> end. -file("src/fixdate.gleam", 18). ?DOC( " Render a `Timestamp` as an RFC 9110 IMF-fixdate string.\n" "\n" " ```gleam\n" " to_string(timestamp.from_unix_seconds(1_782_697_748))\n" " // -> \"Mon, 29 Jun 2026 01:49:08 GMT\"\n" " ```\n" ). -spec to_string(gleam@time@timestamp:timestamp()) -> binary(). to_string(Timestamp) -> {Date, Time} = gleam@time@timestamp:to_calendar(Timestamp, {duration, 0, 0}), {date, Year, Month, Day} = Date, {time_of_day, Hours, Minutes, Seconds, _} = Time, <<<<<<<<<<<<<<<<<<<<<<<<<<(weekday_name(day_of_week(Year, Month, Day)))/binary, ", "/utf8>>/binary, (pad2(Day))/binary>>/binary, " "/utf8>>/binary, (month_name(Month))/binary>>/binary, " "/utf8>>/binary, (pad4(Year))/binary>>/binary, " "/utf8>>/binary, (pad2(Hours))/binary>>/binary, ":"/utf8>>/binary, (pad2(Minutes))/binary>>/binary, ":"/utf8>>/binary, (pad2(Seconds))/binary>>/binary, " GMT"/utf8>>. -file("src/fixdate.gleam", 205). -spec valid_time(integer(), integer(), integer()) -> boolean(). valid_time(Hours, Minutes, Seconds) -> (((((Hours >= 0) andalso (Hours =< 23)) andalso (Minutes >= 0)) andalso (Minutes =< 59)) andalso (Seconds >= 0)) andalso (Seconds =< 60). -file("src/fixdate.gleam", 192). -spec parse_time(binary()) -> {ok, {integer(), integer(), integer()}} | {error, nil}. parse_time(Time) -> case gleam@string:split(Time, <<":"/utf8>>) of [Hours, Minutes, Seconds] -> gleam@result:'try'( gleam_stdlib:parse_int(Hours), fun(Hours@1) -> gleam@result:'try'( gleam_stdlib:parse_int(Minutes), fun(Minutes@1) -> gleam@result:'try'( gleam_stdlib:parse_int(Seconds), fun(Seconds@1) -> {ok, {Hours@1, Minutes@1, Seconds@1}} end ) end ) end ); _ -> {error, nil} end. -file("src/fixdate.gleam", 174). -spec month_from_name(binary()) -> {ok, gleam@time@calendar:month()} | {error, nil}. month_from_name(Name) -> case Name of <<"Jan"/utf8>> -> {ok, january}; <<"Feb"/utf8>> -> {ok, february}; <<"Mar"/utf8>> -> {ok, march}; <<"Apr"/utf8>> -> {ok, april}; <<"May"/utf8>> -> {ok, may}; <<"Jun"/utf8>> -> {ok, june}; <<"Jul"/utf8>> -> {ok, july}; <<"Aug"/utf8>> -> {ok, august}; <<"Sep"/utf8>> -> {ok, september}; <<"Oct"/utf8>> -> {ok, october}; <<"Nov"/utf8>> -> {ok, november}; <<"Dec"/utf8>> -> {ok, december}; _ -> {error, nil} end. -file("src/fixdate.gleam", 135). -spec build_timestamp(binary(), binary(), integer(), binary()) -> {ok, gleam@time@timestamp:timestamp()} | {error, nil}. build_timestamp(Day, Month, Year, Time) -> gleam@result:'try'( gleam_stdlib:parse_int(Day), fun(Day@1) -> gleam@result:'try'( month_from_name(Month), fun(Month@1) -> gleam@result:'try'( parse_time(Time), fun(_use0) -> {Hours, Minutes, Seconds} = _use0, Date = {date, Year, Month@1, Day@1}, case gleam@time@calendar:is_valid_date(Date) andalso valid_time( Hours, Minutes, Seconds ) of true -> Time_of_day = {time_of_day, Hours, Minutes, Seconds, 0}, {ok, gleam@time@timestamp:from_calendar( Date, Time_of_day, {duration, 0, 0} )}; false -> {error, nil} end end ) end ) end ). -file("src/fixdate.gleam", 125). -spec parse_fields(binary(), binary(), binary(), binary()) -> {ok, gleam@time@timestamp:timestamp()} | {error, nil}. parse_fields(Day, Month, Year, Time) -> gleam@result:'try'( gleam_stdlib:parse_int(Year), fun(Year@1) -> build_timestamp(Day, Month, Year@1, Time) end ). -file("src/fixdate.gleam", 166). -spec expand_two_digit_year(binary()) -> {ok, integer()} | {error, nil}. expand_two_digit_year(Two_digit_year) -> case gleam_stdlib:parse_int(Two_digit_year) of {ok, Year} when (Year >= 0) andalso (Year =< 69) -> {ok, 2000 + Year}; {ok, Year@1} when (Year@1 >= 70) andalso (Year@1 =< 99) -> {ok, 1900 + Year@1}; _ -> {error, nil} end. -file("src/fixdate.gleam", 156). -spec parse_rfc850(binary(), binary()) -> {ok, gleam@time@timestamp:timestamp()} | {error, nil}. parse_rfc850(Date, Time) -> case gleam@string:split(Date, <<"-"/utf8>>) of [Day, Month, Two_digit_year] -> gleam@result:'try'( expand_two_digit_year(Two_digit_year), fun(Year) -> build_timestamp(Day, Month, Year, Time) end ); _ -> {error, nil} end. -file("src/fixdate.gleam", 108). ?DOC( " Parse an HTTP-date into a `Timestamp`. Accepts all three formats in\n" " RFC 9110: IMF-fixdate, the obsolete RFC 850 (2-digit year), and\n" " asctime. Returns `Error(Nil)` on malformed or out-of-range input.\n" "\n" " ```gleam\n" " parse(\"Mon, 29 Jun 2026 01:49:08 GMT\")\n" " // -> Ok(timestamp.from_unix_seconds(1_782_697_748))\n" " ```\n" ). -spec parse(binary()) -> {ok, gleam@time@timestamp:timestamp()} | {error, nil}. parse(Input) -> Tokens = begin _pipe = Input, _pipe@1 = gleam@string:split(_pipe, <<" "/utf8>>), gleam@list:filter(_pipe@1, fun(Token) -> Token /= <<""/utf8>> end) end, case Tokens of [_, Day, Month, Year, Time, <<"GMT"/utf8>>] -> parse_fields(Day, Month, Year, Time); [_, Date, Time@1, <<"GMT"/utf8>>] -> parse_rfc850(Date, Time@1); [_, Month@1, Day@1, Time@2, Year@1] -> parse_fields(Day@1, Month@1, Year@1, Time@2); _ -> {error, nil} end.