Skip to main content

src/svg_path@number_format.erl

-module(svg_path@number_format).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/number_format.gleam").
-export([raw_number/2, prepare/2, number/2, code_number/2]).
-export_type([left_padding_style/0, left_decimal_options/0, right_decimal_options/0, options/0, number_format/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(
    " Shared numeric formatting helpers.\n"
    "\n"
    " This module keeps decimal rounding, fixed decimal formatting, and\n"
    " left-padding behavior consistent across structural inspection and SVG path\n"
    " serialization. Higher-level modules still decide which numbers should be\n"
    " included in the lookahead list for automatic padding.\n"
).

-type left_padding_style() :: zero | space.

-type left_decimal_options() :: succinct |
    {auto_left_padding, left_padding_style()} |
    {left_padding, integer(), left_padding_style()}.

-type right_decimal_options() :: system |
    {at_most, integer()} |
    {fixed, integer()}.

-type options() :: {options, left_decimal_options(), right_decimal_options()}.

-opaque number_format() :: {number_format,
        options(),
        gleam@option:option({integer(), left_padding_style()})}.

-file("src/svg_path/number_format.gleam", 155).
-spec left_width(binary()) -> integer().
left_width(Number) ->
    case gleam@string:split_once(Number, <<"."/utf8>>) of
        {ok, {Whole, _}} ->
            string:length(Whole);

        {error, _} ->
            string:length(Number)
    end.

-file("src/svg_path/number_format.gleam", 210).
-spec strip_trailing_zeros(binary()) -> binary().
strip_trailing_zeros(String) ->
    case gleam_stdlib:string_ends_with(String, <<"0"/utf8>>) of
        true ->
            _pipe = String,
            _pipe@1 = gleam@string:drop_end(_pipe, 1),
            strip_trailing_zeros(_pipe@1);

        false ->
            String
    end.

-file("src/svg_path/number_format.gleam", 196).
-spec strip_trailing_decimal_zeros(binary()) -> binary().
strip_trailing_decimal_zeros(Number) ->
    case gleam@string:split_once(Number, <<"."/utf8>>) of
        {error, _} ->
            Number;

        {ok, {Whole, Fractional}} ->
            Fractional@1 = strip_trailing_zeros(Fractional),
            case Fractional@1 of
                <<""/utf8>> ->
                    Whole;

                _ ->
                    <<<<Whole/binary, "."/utf8>>/binary, Fractional@1/binary>>
            end
    end.

-file("src/svg_path/number_format.gleam", 225).
-spec power_of_ten_int(integer()) -> integer().
power_of_ten_int(Exponent) ->
    case Exponent =< 0 of
        true ->
            1;

        false ->
            10 * power_of_ten_int(Exponent - 1)
    end.

-file("src/svg_path/number_format.gleam", 221).
-spec power_of_ten(integer()) -> float().
power_of_ten(Exponent) ->
    erlang:float(power_of_ten_int(Exponent)).

-file("src/svg_path/number_format.gleam", 171).
-spec fixed_decimal(float(), integer()) -> binary().
fixed_decimal(Number, Decimal_places) ->
    Decimal_places@1 = gleam@int:max(Decimal_places, 0),
    Scale = power_of_ten(Decimal_places@1),
    Scaled = begin
        _pipe = Number * Scale,
        erlang:round(_pipe)
    end,
    Sign = case Scaled < 0 of
        true ->
            <<"-"/utf8>>;

        false ->
            <<""/utf8>>
    end,
    Absolute_scaled = gleam@int:absolute_value(Scaled),
    case Decimal_places@1 of
        0 ->
            <<Sign/binary, (erlang:integer_to_binary(Absolute_scaled))/binary>>;

        _ ->
            Whole = case power_of_ten_int(Decimal_places@1) of
                0 -> 0;
                Gleam@denominator -> Absolute_scaled div Gleam@denominator
            end,
            Fractional = case power_of_ten_int(Decimal_places@1) of
                0 -> 0;
                Gleam@denominator@1 -> Absolute_scaled rem Gleam@denominator@1
            end,
            Fractional@1 = begin
                _pipe@1 = Fractional,
                _pipe@2 = erlang:integer_to_binary(_pipe@1),
                gleam@string:pad_start(_pipe@2, Decimal_places@1, <<"0"/utf8>>)
            end,
            <<<<<<Sign/binary, (erlang:integer_to_binary(Whole))/binary>>/binary,
                    "."/utf8>>/binary,
                Fractional@1/binary>>
    end.

-file("src/svg_path/number_format.gleam", 162).
-spec decimal(float(), integer(), boolean()) -> binary().
decimal(Number, Decimal_places, Fixed_decimals) ->
    Fixed = fixed_decimal(Number, Decimal_places),
    case Fixed_decimals of
        true ->
            Fixed;

        false ->
            strip_trailing_decimal_zeros(Fixed)
    end.

-file("src/svg_path/number_format.gleam", 106).
?DOC(
    " Format a number without applying left-padding.\n"
    "\n"
    " This is useful when deciding which compact SVG command form a number would\n"
    " use before padding is added.\n"
).
-spec raw_number(float(), options()) -> binary().
raw_number(Number, Options) ->
    case erlang:element(3, Options) of
        system ->
            _pipe = Number,
            _pipe@1 = gleam_stdlib:float_to_string(_pipe),
            strip_trailing_decimal_zeros(_pipe@1);

        {at_most, Decimal_places} ->
            decimal(Number, Decimal_places, false);

        {fixed, Decimal_places@1} ->
            decimal(Number, Decimal_places@1, true)
    end.

-file("src/svg_path/number_format.gleam", 114).
-spec auto_left_padding_width(list(float()), options()) -> integer().
auto_left_padding_width(Numbers, Options) ->
    _pipe = Numbers,
    _pipe@3 = gleam@list:map(_pipe, fun(Number) -> _pipe@1 = Number,
            _pipe@2 = raw_number(_pipe@1, Options),
            left_width(_pipe@2) end),
    gleam@list:fold(_pipe@3, 0, fun gleam@int:max/2).

-file("src/svg_path/number_format.gleam", 66).
?DOC(" Prepare a formatter, using the supplied numbers to choose automatic padding.\n").
-spec prepare(options(), list(float())) -> number_format().
prepare(Options, Numbers) ->
    Left_padding = case erlang:element(2, Options) of
        succinct ->
            none;

        {left_padding, Width, Style} ->
            {some, {gleam@int:max(Width, 0), Style}};

        {auto_left_padding, Style@1} ->
            {some, {auto_left_padding_width(Numbers, Options), Style@1}}
    end,
    {number_format, Options, Left_padding}.

-file("src/svg_path/number_format.gleam", 145).
-spec zero_pad_whole(binary(), integer()) -> binary().
zero_pad_whole(Whole, Width) ->
    case gleam_stdlib:string_starts_with(Whole, <<"-"/utf8>>) of
        true ->
            Digits = gleam@string:drop_start(Whole, 1),
            <<"-"/utf8,
                (gleam@string:pad_start(
                    Digits,
                    gleam@int:max(Width - 1, 0),
                    <<"0"/utf8>>
                ))/binary>>;

        false ->
            gleam@string:pad_start(Whole, Width, <<"0"/utf8>>)
    end.

-file("src/svg_path/number_format.gleam", 127).
-spec pad_left_side(binary(), integer(), left_padding_style()) -> binary().
pad_left_side(Number, Width, Style) ->
    {Whole@1, Suffix} = case gleam@string:split_once(Number, <<"."/utf8>>) of
        {ok, {Whole, Fractional}} ->
            {Whole, <<"."/utf8, Fractional/binary>>};

        {error, _} ->
            {Number, <<""/utf8>>}
    end,
    Whole@2 = case Style of
        space ->
            gleam@string:pad_start(Whole@1, Width, <<" "/utf8>>);

        zero ->
            zero_pad_whole(Whole@1, Width)
    end,
    <<Whole@2/binary, Suffix/binary>>.

-file("src/svg_path/number_format.gleam", 120).
-spec left_pad(binary(), number_format()) -> binary().
left_pad(Number, Format) ->
    case erlang:element(3, Format) of
        none ->
            Number;

        {some, {Width, Style}} ->
            pad_left_side(Number, Width, Style)
    end.

-file("src/svg_path/number_format.gleam", 78).
?DOC(" Format a number.\n").
-spec number(float(), number_format()) -> binary().
number(Number, Format) ->
    _pipe = Number,
    _pipe@1 = raw_number(_pipe, erlang:element(2, Format)),
    left_pad(_pipe@1, Format).

-file("src/svg_path/number_format.gleam", 88).
?DOC(
    " Format a number for copy-pasteable Gleam code.\n"
    "\n"
    " Whole numbers are given an explicit `.0` suffix before left-padding is\n"
    " applied.\n"
).
-spec code_number(float(), number_format()) -> binary().
code_number(Value, Format) ->
    Number = raw_number(Value, erlang:element(2, Format)),
    Number@1 = case (gleam_stdlib:contains_string(Number, <<"."/utf8>>) orelse gleam_stdlib:contains_string(
        Number,
        <<"e"/utf8>>
    ))
    orelse gleam_stdlib:contains_string(Number, <<"E"/utf8>>) of
        true ->
            Number;

        false ->
            <<Number/binary, ".0"/utf8>>
    end,
    left_pad(Number@1, Format).