Skip to main content

src/duration_format@go.erl

-module(duration_format@go).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/duration_format/go.gleam").
-export([parse/1, to_string/1, to_string_trimmed/1]).
-export_type([error/0]).

-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(
    " Parse and format durations using Go's `time.ParseDuration` grammar.\n"
    "\n"
    " The same format is also used by Prometheus, Kubernetes, HashiCorp tools\n"
    " (Terraform, Consul, Nomad), and InfluxDB — there is no formal\n"
    " specification beyond [Go's docs](https://pkg.go.dev/time#ParseDuration).\n"
    "\n"
    " ## Grammar\n"
    "\n"
    " ```text\n"
    " duration  = [ sign ] component { component } | [ sign ] \"0\"\n"
    " sign      = \"+\" | \"-\"\n"
    " component = number unit\n"
    " number    = digits [ \".\" digits ] | \".\" digits\n"
    " unit      = \"ns\" | \"us\" | \"µs\" | \"μs\" | \"ms\" | \"s\" | \"m\" | \"h\"\n"
    " ```\n"
    "\n"
    " `µs` is U+00B5 (micro sign — what Go emits); `μs` is U+03BC (Greek\n"
    " small mu) and is accepted on input but never produced.\n"
    "\n"
    " ## Examples\n"
    "\n"
    " ```gleam\n"
    " go.parse(\"1h30m\")\n"
    " // -> Ok(duration.nanoseconds(5_400_000_000_000))\n"
    "\n"
    " go.parse(\"-2m3.4s\")\n"
    " // -> Ok(duration.nanoseconds(-123_400_000_000))\n"
    "\n"
    " go.parse(\"1d\")\n"
    " // -> Error(UnknownUnit(\"d\"))\n"
    "\n"
    " go.to_string(duration.nanoseconds(5_400_000_000_000))\n"
    " // -> \"1h30m0s\"\n"
    " ```\n"
).

-type error() :: invalid_duration |
    missing_unit |
    {unknown_unit, binary()} |
    overflow.

-file("src/duration_format/go.gleam", 75).
-spec unit_to_nanos(binary()) -> {ok, integer()} | {error, error()}.
unit_to_nanos(U) ->
    case U of
        <<"ns"/utf8>> ->
            {ok, 1};

        <<"us"/utf8>> ->
            {ok, 1000};

        <<"µs"/utf8>> ->
            {ok, 1000};

        <<"μs"/utf8>> ->
            {ok, 1000};

        <<"ms"/utf8>> ->
            {ok, 1000000};

        <<"s"/utf8>> ->
            {ok, 1000000000};

        <<"m"/utf8>> ->
            {ok, 60 * 1000000000};

        <<"h"/utf8>> ->
            {ok, 3600 * 1000000000};

        Other ->
            {error, {unknown_unit, Other}}
    end.

-file("src/duration_format/go.gleam", 212).
-spec checked_mul(integer(), integer()) -> {ok, integer()} | {error, error()}.
checked_mul(A, B) ->
    case (B =:= 0) orelse (A =< (case B of
        0 -> 0;
        Gleam@denominator -> 9223372036854775808 div Gleam@denominator
    end)) of
        true ->
            {ok, A * B};

        false ->
            {error, overflow}
    end.

-file("src/duration_format/go.gleam", 204).
-spec take_unit_suffix(list(binary())) -> {binary(), list(binary())}.
take_unit_suffix(G) ->
    {Unit, Rest} = gleam@list:split_while(
        G,
        fun(C) ->
            (C /= <<"."/utf8>>) andalso gleam@result:is_error(
                gleam_stdlib:parse_int(C)
            )
        end
    ),
    {erlang:list_to_binary(Unit), Rest}.

-file("src/duration_format/go.gleam", 179).
-spec leading_fraction(list(binary()), integer(), float(), boolean()) -> {integer(),
    float(),
    list(binary())}.
leading_fraction(G, Acc, Scale, Overflowed) ->
    case G of
        [C | Rest] ->
            case gleam_stdlib:parse_int(C) of
                {error, _} ->
                    {Acc, Scale, G};

                {ok, D} ->
                    Y = (Acc * 10) + D,
                    case Overflowed orelse (Y > 9223372036854775808) of
                        true ->
                            leading_fraction(Rest, Acc, Scale, true);

                        false ->
                            leading_fraction(Rest, Y, Scale * 10.0, false)
                    end
            end;

        [] ->
            {Acc, Scale, G}
    end.

-file("src/duration_format/go.gleam", 161).
-spec leading_int(list(binary()), integer()) -> {ok,
        {integer(), list(binary())}} |
    {error, error()}.
leading_int(G, Acc) ->
    case G of
        [C | Rest] ->
            case gleam_stdlib:parse_int(C) of
                {error, _} ->
                    {ok, {Acc, G}};

                {ok, D} ->
                    case ((Acc * 10) + D) > 9223372036854775808 of
                        true ->
                            {error, overflow};

                        false ->
                            leading_int(Rest, (Acc * 10) + D)
                    end
            end;

        [] ->
            {ok, {Acc, G}}
    end.

-file("src/duration_format/go.gleam", 128).
-spec parse_component(list(binary())) -> {ok, {integer(), list(binary())}} |
    {error, error()}.
parse_component(G) ->
    gleam@result:'try'(
        leading_int(G, 0),
        fun(_use0) ->
            {Int_part, After_int} = _use0,
            Had_int = After_int /= G,
            {Frac, Scale, After_frac, Had_frac} = case After_int of
                [<<"."/utf8>> | Rest] ->
                    {F, Sc, R} = leading_fraction(Rest, 0, 1.0, false),
                    {F, Sc, R, R /= Rest};

                _ ->
                    {0, 1.0, After_int, false}
            end,
            gleam@bool:guard(
                not Had_int andalso not Had_frac,
                {error, invalid_duration},
                fun() ->
                    {Unit_str, After_unit} = take_unit_suffix(After_frac),
                    gleam@result:'try'(case Unit_str of
                            <<""/utf8>> ->
                                {error, missing_unit};

                            U ->
                                unit_to_nanos(U)
                        end, fun(Per_unit) ->
                            gleam@result:'try'(
                                checked_mul(Int_part, Per_unit),
                                fun(V_int) ->
                                    V_frac = case Frac of
                                        0 ->
                                            0;

                                        _ ->
                                            erlang:trunc(
                                                erlang:float(Frac) * (case Scale of
                                                    +0.0 -> +0.0;
                                                    -0.0 -> -0.0;
                                                    Gleam@denominator -> erlang:float(
                                                        Per_unit
                                                    )
                                                    / Gleam@denominator
                                                end)
                                            )
                                    end,
                                    V = V_int + V_frac,
                                    gleam@bool:guard(
                                        V > 9223372036854775808,
                                        {error, overflow},
                                        fun() -> {ok, {V, After_unit}} end
                                    )
                                end
                            )
                        end)
                end
            )
        end
    ).

-file("src/duration_format/go.gleam", 116).
-spec parse_components(list(binary()), integer()) -> {ok, integer()} |
    {error, error()}.
parse_components(G, Acc) ->
    case G of
        [] ->
            {ok, Acc};

        _ ->
            gleam@result:'try'(
                parse_component(G),
                fun(_use0) ->
                    {Nanos, Rest} = _use0,
                    Sum = Acc + Nanos,
                    gleam@bool:guard(
                        Sum > 9223372036854775808,
                        {error, overflow},
                        fun() -> parse_components(Rest, Sum) end
                    )
                end
            )
    end.

-file("src/duration_format/go.gleam", 108).
-spec strip_sign(list(binary())) -> {boolean(), list(binary())}.
strip_sign(G) ->
    case G of
        [<<"-"/utf8>> | R] ->
            {true, R};

        [<<"+"/utf8>> | R@1] ->
            {false, R@1};

        _ ->
            {false, G}
    end.

-file("src/duration_format/go.gleam", 90).
?DOC(
    " Parse a duration string in Go's `time.ParseDuration` format.\n"
    "\n"
    " See the module documentation for the accepted grammar.\n"
).
-spec parse(binary()) -> {ok, gleam@time@duration:duration()} | {error, error()}.
parse(Input) ->
    {Neg, Rest} = strip_sign(gleam@string:to_graphemes(Input)),
    case Rest of
        [<<"0"/utf8>>] ->
            {ok, gleam@time@duration:nanoseconds(0)};

        [] ->
            {error, invalid_duration};

        _ ->
            gleam@result:'try'(
                parse_components(Rest, 0),
                fun(Total) -> case {Neg, Total > 9223372036854775807} of
                        {true, _} ->
                            {ok, gleam@time@duration:nanoseconds(- Total)};

                        {false, true} ->
                            {error, overflow};

                        {false, false} ->
                            {ok, gleam@time@duration:nanoseconds(Total)}
                    end end
            )
    end.

-file("src/duration_format/go.gleam", 320).
-spec trim_trailing_zeros(binary()) -> binary().
trim_trailing_zeros(S) ->
    _pipe = S,
    _pipe@1 = gleam@string:to_graphemes(_pipe),
    _pipe@2 = lists:reverse(_pipe@1),
    _pipe@3 = gleam@list:drop_while(_pipe@2, fun(C) -> C =:= <<"0"/utf8>> end),
    _pipe@4 = lists:reverse(_pipe@3),
    gleam@string:join(_pipe@4, <<""/utf8>>).

-file("src/duration_format/go.gleam", 308).
-spec format_fraction(integer(), integer()) -> binary().
format_fraction(Frac, Digits) ->
    case Frac of
        0 ->
            <<""/utf8>>;

        _ ->
            Raw = erlang:integer_to_binary(Frac),
            Padding = Digits - string:length(Raw),
            Padded = <<(gleam@string:repeat(<<"0"/utf8>>, Padding))/binary,
                Raw/binary>>,
            <<"."/utf8, (trim_trailing_zeros(Padded))/binary>>
    end.

-file("src/duration_format/go.gleam", 266).
-spec format_supersecond(integer(), boolean()) -> binary().
format_supersecond(U, Trim) ->
    Total_seconds = case 1000000000 of
        0 -> 0;
        Gleam@denominator -> U div Gleam@denominator
    end,
    Frac_nanos = case 1000000000 of
        0 -> 0;
        Gleam@denominator@1 -> U rem Gleam@denominator@1
    end,
    Secs = case 60 of
        0 -> 0;
        Gleam@denominator@2 -> Total_seconds rem Gleam@denominator@2
    end,
    Total_minutes = case 60 of
        0 -> 0;
        Gleam@denominator@3 -> Total_seconds div Gleam@denominator@3
    end,
    Mins = case 60 of
        0 -> 0;
        Gleam@denominator@4 -> Total_minutes rem Gleam@denominator@4
    end,
    Hours = case 60 of
        0 -> 0;
        Gleam@denominator@5 -> Total_minutes div Gleam@denominator@5
    end,
    Show_hours = Hours > 0,
    Show_secs = case Trim of
        true ->
            (Secs > 0) orelse (Frac_nanos > 0);

        false ->
            true
    end,
    Show_mins = case Trim of
        true ->
            (Mins > 0) orelse (Show_hours andalso Show_secs);

        false ->
            (Hours > 0) orelse (Mins > 0)
    end,
    Seconds_str = case Show_secs of
        true ->
            <<<<(erlang:integer_to_binary(Secs))/binary,
                    (format_fraction(Frac_nanos, 9))/binary>>/binary,
                "s"/utf8>>;

        false ->
            <<""/utf8>>
    end,
    With_mins = case Show_mins of
        true ->
            <<<<(erlang:integer_to_binary(Mins))/binary, "m"/utf8>>/binary,
                Seconds_str/binary>>;

        false ->
            Seconds_str
    end,
    case Show_hours of
        true ->
            <<<<(erlang:integer_to_binary(Hours))/binary, "h"/utf8>>/binary,
                With_mins/binary>>;

        false ->
            With_mins
    end.

-file("src/duration_format/go.gleam", 302).
-spec format_with_fraction(integer(), integer(), integer()) -> binary().
format_with_fraction(U, Divisor, Digits) ->
    Whole = case Divisor of
        0 -> 0;
        Gleam@denominator -> U div Gleam@denominator
    end,
    Frac = case Divisor of
        0 -> 0;
        Gleam@denominator@1 -> U rem Gleam@denominator@1
    end,
    <<(erlang:integer_to_binary(Whole))/binary,
        (format_fraction(Frac, Digits))/binary>>.

-file("src/duration_format/go.gleam", 258).
-spec format_subsecond(integer()) -> binary().
format_subsecond(U) ->
    case U of
        _ when U < 1000 ->
            <<(erlang:integer_to_binary(U))/binary, "ns"/utf8>>;

        _ when U < 1000000 ->
            <<(format_with_fraction(U, 1000, 3))/binary, "µs"/utf8>>;

        _ ->
            <<(format_with_fraction(U, 1000000, 6))/binary, "ms"/utf8>>
    end.

-file("src/duration_format/go.gleam", 251).
-spec format_magnitude(integer(), boolean()) -> binary().
format_magnitude(U, Trim) ->
    case U < 1000000000 of
        true ->
            format_subsecond(U);

        false ->
            format_supersecond(U, Trim)
    end.

-file("src/duration_format/go.gleam", 242).
-spec format_duration(gleam@time@duration:duration(), boolean()) -> binary().
format_duration(D, Trim) ->
    {S, Ns} = gleam@time@duration:to_seconds_and_nanoseconds(D),
    case (S * 1000000000) + Ns of
        0 ->
            <<"0s"/utf8>>;

        N when N < 0 ->
            <<"-"/utf8, (format_magnitude(- N, Trim))/binary>>;

        N@1 ->
            format_magnitude(N@1, Trim)
    end.

-file("src/duration_format/go.gleam", 225).
?DOC(
    " Format a duration using Go's `Duration.String()` rules.\n"
    "\n"
    " Zero formats as `\"0s\"`. Sub-second magnitudes use the largest unit that\n"
    " keeps a non-zero leading digit (`ns`, `µs`, `ms`). One-second-and-up emits\n"
    " `<h>h<m>m<s>s` with trailing zero units omitted and a fractional seconds\n"
    " component when needed.\n"
).
-spec to_string(gleam@time@duration:duration()) -> binary().
to_string(D) ->
    format_duration(D, false).

-file("src/duration_format/go.gleam", 238).
?DOC(
    " Like `to_string`, but with trailing zero components dropped.\n"
    "\n"
    " `to_string` always emits a full `<h>h<m>m<s>s` tail for one-second-and-up\n"
    " durations, so an exact hour formats as `\"1h0m0s\"`. This variant drops\n"
    " trailing zero units, yielding `\"1h\"` or `\"1h30m\"` instead. Intermediate\n"
    " zeros are preserved — `\"1h0m30s\"` keeps its `0m` — and a zero seconds\n"
    " component is kept when it carries a fraction (e.g. `\"8m0.000000001s\"`).\n"
    " Zero still formats as `\"0s\"`, and sub-second magnitudes are unchanged. The\n"
    " result always parses back to the same duration.\n"
).
-spec to_string_trimmed(gleam@time@duration:duration()) -> binary().
to_string_trimmed(D) ->
    format_duration(D, true).