Skip to main content

src/fixdate.erl

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