-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" " `hms` 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 `hms` 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).