-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.