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