src/finanza@decimal.erl

-module(finanza@decimal).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/finanza/decimal.gleam").
-export([zero/0, one/0, from_int/1, new/2, coefficient/1, exponent/1, to_string/1, format/3, is_zero/1, is_positive/1, is_negative/1, negate/1, absolute/1, equal/2, round/3, truncate/2, multiply/2, divide/4, rescale/3, add/2, subtract/2, compare/2, from_string/1]).
-export_type([decimal/0, parse_error/0, arithmetic_error/0, parse_state/0, char_kind/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(
    " Fixed-point decimal arithmetic with explicit rounding.\n"
    "\n"
    " A [`Decimal`](#Decimal) is represented internally as a signed\n"
    " integer `coefficient` and an `exponent`:\n"
    "\n"
    " ```text\n"
    " value = coefficient × 10^exponent\n"
    " ```\n"
    "\n"
    " `Decimal` is `pub opaque`; construct values through [`from_int`](#from_int),\n"
    " [`from_string`](#from_string), or [`new`](#new), and inspect them through\n"
    " [`coefficient`](#coefficient) and [`exponent`](#exponent).\n"
    "\n"
    " ## Precision boundary\n"
    "\n"
    " On the JavaScript target, Gleam's `Int` is a 64-bit IEEE 754 number,\n"
    " so coefficients are limited to ±(2^53 − 1) = 9_007_199_254_740_991.\n"
    " Operations that would produce a larger coefficient return\n"
    " [`PrecisionExceeded`](#ArithmeticError). On the Erlang target,\n"
    " integers are arbitrary precision; the same bound is enforced anyway\n"
    " so behaviour is consistent across targets.\n"
).

-opaque decimal() :: {decimal, integer(), integer()}.

-type parse_error() :: empty_input |
    {invalid_character, binary(), integer()} |
    multiple_decimal_points |
    multiple_signs |
    no_digits |
    parsed_value_too_large.

-type arithmetic_error() :: division_by_zero | precision_exceeded.

-type parse_state() :: {parse_state,
        list(binary()),
        integer(),
        integer(),
        integer(),
        integer(),
        boolean(),
        boolean(),
        boolean()}.

-type char_kind() :: {sign_char, integer()} |
    dot_char |
    {digit_char, integer()} |
    other_char.

-file("src/finanza/decimal.gleam", 78).
?DOC(" The decimal value 0.\n").
-spec zero() -> decimal().
zero() ->
    {decimal, 0, 0}.

-file("src/finanza/decimal.gleam", 83).
?DOC(" The decimal value 1.\n").
-spec one() -> decimal().
one() ->
    {decimal, 1, 0}.

-file("src/finanza/decimal.gleam", 88).
?DOC(" Build a `Decimal` from an integer.\n").
-spec from_int(integer()) -> decimal().
from_int(N) ->
    {decimal, N, 0}.

-file("src/finanza/decimal.gleam", 95).
?DOC(
    " Build a `Decimal` directly from a coefficient and exponent.\n"
    "\n"
    " `new(coefficient: 1234, exponent: -2)` represents `12.34`.\n"
).
-spec new(integer(), integer()) -> decimal().
new(Coefficient, Exponent) ->
    {decimal, Coefficient, Exponent}.

-file("src/finanza/decimal.gleam", 173).
-spec classify(binary()) -> char_kind().
classify(Char) ->
    case Char of
        <<"+"/utf8>> ->
            {sign_char, 1};

        <<"-"/utf8>> ->
            {sign_char, -1};

        <<"."/utf8>> ->
            dot_char;

        <<"0"/utf8>> ->
            {digit_char, 0};

        <<"1"/utf8>> ->
            {digit_char, 1};

        <<"2"/utf8>> ->
            {digit_char, 2};

        <<"3"/utf8>> ->
            {digit_char, 3};

        <<"4"/utf8>> ->
            {digit_char, 4};

        <<"5"/utf8>> ->
            {digit_char, 5};

        <<"6"/utf8>> ->
            {digit_char, 6};

        <<"7"/utf8>> ->
            {digit_char, 7};

        <<"8"/utf8>> ->
            {digit_char, 8};

        <<"9"/utf8>> ->
            {digit_char, 9};

        _ ->
            other_char
    end.

-file("src/finanza/decimal.gleam", 251).
?DOC(" The signed coefficient component.\n").
-spec coefficient(decimal()) -> integer().
coefficient(D) ->
    erlang:element(2, D).

-file("src/finanza/decimal.gleam", 256).
?DOC(" The exponent component (base 10).\n").
-spec exponent(decimal()) -> integer().
exponent(D) ->
    erlang:element(3, D).

-file("src/finanza/decimal.gleam", 328).
-spec group_right(list(binary()), integer(), list(list(binary()))) -> list(list(binary())).
group_right(Chars, Length, Acc) ->
    gleam@bool:guard(
        Length =< 3,
        [Chars | Acc],
        fun() ->
            Head_size = Length - 3,
            Head = gleam@list:take(Chars, Head_size),
            Tail = gleam@list:drop(Chars, Head_size),
            group_right(Head, Head_size, [Tail | Acc])
        end
    ).

-file("src/finanza/decimal.gleam", 318).
-spec group_integer(binary(), binary()) -> binary().
group_integer(Digits, Separator) ->
    gleam@bool:guard(
        Separator =:= <<""/utf8>>,
        Digits,
        fun() ->
            Chars = gleam@string:to_graphemes(Digits),
            Length = erlang:length(Chars),
            Groups = group_right(Chars, Length, []),
            _pipe = Groups,
            _pipe@1 = gleam@list:map(_pipe, fun erlang:list_to_binary/1),
            gleam@string:join(_pipe@1, Separator)
        end
    ).

-file("src/finanza/decimal.gleam", 303).
-spec inject_thousands(binary(), binary(), binary()) -> binary().
inject_thousands(Unsigned, Thousands, Decimal_separator) ->
    case gleam@string:split(Unsigned, <<"."/utf8>>) of
        [Integer_part] ->
            group_integer(Integer_part, Thousands);

        [Integer_part@1, Fraction_part] ->
            <<<<(group_integer(Integer_part@1, Thousands))/binary,
                    Decimal_separator/binary>>/binary,
                Fraction_part/binary>>;

        _ ->
            Unsigned
    end.

-file("src/finanza/decimal.gleam", 340).
-spec render_zero(integer()) -> binary().
render_zero(Exp) ->
    gleam@bool:guard(
        Exp >= 0,
        <<"0"/utf8>>,
        fun() ->
            <<"0."/utf8, (gleam@string:repeat(<<"0"/utf8>>, - Exp))/binary>>
        end
    ).

-file("src/finanza/decimal.gleam", 362).
-spec render_with_fraction(binary(), binary(), integer()) -> binary().
render_with_fraction(Sign_prefix, Digits, Fraction_size) ->
    Digit_length = string:length(Digits),
    gleam@bool:guard(
        Digit_length =< Fraction_size,
        <<<<<<Sign_prefix/binary, "0."/utf8>>/binary,
                (gleam@string:repeat(<<"0"/utf8>>, Fraction_size - Digit_length))/binary>>/binary,
            Digits/binary>>,
        fun() ->
            Integer_part = gleam@string:slice(
                Digits,
                0,
                Digit_length - Fraction_size
            ),
            Fraction_part = gleam@string:slice(
                Digits,
                Digit_length - Fraction_size,
                Fraction_size
            ),
            <<<<<<Sign_prefix/binary, Integer_part/binary>>/binary, "."/utf8>>/binary,
                Fraction_part/binary>>
        end
    ).

-file("src/finanza/decimal.gleam", 345).
-spec render_nonzero(integer(), integer()) -> binary().
render_nonzero(Coefficient, Exp) ->
    Sign_prefix = case Coefficient < 0 of
        true ->
            <<"-"/utf8>>;

        false ->
            <<""/utf8>>
    end,
    Digits = erlang:integer_to_binary(gleam@int:absolute_value(Coefficient)),
    gleam@bool:guard(
        Exp >= 0,
        <<<<Sign_prefix/binary, Digits/binary>>/binary,
            (gleam@string:repeat(<<"0"/utf8>>, Exp))/binary>>,
        fun() -> render_with_fraction(Sign_prefix, Digits, - Exp) end
    ).

-file("src/finanza/decimal.gleam", 263).
?DOC(
    " Render a `Decimal` as a plain string. Preserves the encoded\n"
    " exponent (`new(coefficient: 100, exponent: -2)` renders as `\"1.00\"`,\n"
    " not `\"1\"`).\n"
).
-spec to_string(decimal()) -> binary().
to_string(D) ->
    case erlang:element(2, D) of
        0 ->
            render_zero(erlang:element(3, D));

        _ ->
            render_nonzero(erlang:element(2, D), erlang:element(3, D))
    end.

-file("src/finanza/decimal.gleam", 280).
?DOC(
    " Render a `Decimal` with custom thousands and decimal separators.\n"
    "\n"
    " ```gleam\n"
    " format(d, thousands: \",\", decimal_separator: \".\")  // \"1,234.56\"\n"
    " format(d, thousands: \".\", decimal_separator: \",\")  // \"1.234,56\" (German)\n"
    " format(d, thousands: \"\",  decimal_separator: \".\")  // \"1234.56\"\n"
    " ```\n"
    "\n"
    " Equivalent to [`to_string`](#to_string) when `thousands` is empty\n"
    " and `decimal_separator` is `\".\"`.\n"
).
-spec format(decimal(), binary(), binary()) -> binary().
format(D, Thousands, Decimal_separator) ->
    Raw = to_string(D),
    Sign_prefix = case erlang:element(2, D) < 0 of
        true ->
            <<"-"/utf8>>;

        false ->
            <<""/utf8>>
    end,
    Unsigned = case Sign_prefix of
        <<""/utf8>> ->
            Raw;

        _ ->
            gleam@string:drop_start(Raw, 1)
    end,
    Body = inject_thousands(Unsigned, Thousands, Decimal_separator),
    <<Sign_prefix/binary, Body/binary>>.

-file("src/finanza/decimal.gleam", 384).
?DOC(" Test for the zero decimal.\n").
-spec is_zero(decimal()) -> boolean().
is_zero(D) ->
    erlang:element(2, D) =:= 0.

-file("src/finanza/decimal.gleam", 389).
?DOC(" Test for a strictly positive decimal.\n").
-spec is_positive(decimal()) -> boolean().
is_positive(D) ->
    erlang:element(2, D) > 0.

-file("src/finanza/decimal.gleam", 394).
?DOC(" Test for a strictly negative decimal.\n").
-spec is_negative(decimal()) -> boolean().
is_negative(D) ->
    erlang:element(2, D) < 0.

-file("src/finanza/decimal.gleam", 402).
?DOC(
    " Negate the value. Always safe (the coefficient sign flips but\n"
    " magnitude does not change).\n"
).
-spec negate(decimal()) -> decimal().
negate(D) ->
    {decimal, - erlang:element(2, D), erlang:element(3, D)}.

-file("src/finanza/decimal.gleam", 407).
?DOC(" Absolute value. Always safe.\n").
-spec absolute(decimal()) -> decimal().
absolute(D) ->
    {decimal,
        gleam@int:absolute_value(erlang:element(2, D)),
        erlang:element(3, D)}.

-file("src/finanza/decimal.gleam", 533).
-spec half_even_bump(integer(), integer(), integer()) -> boolean().
half_even_bump(R, Denominator, Q) ->
    case gleam@int:compare(2 * R, Denominator) of
        gt ->
            true;

        lt ->
            false;

        eq ->
            (Q rem 2) =:= 1
    end.

-file("src/finanza/decimal.gleam", 515).
-spec should_bump_up(
    integer(),
    integer(),
    integer(),
    integer(),
    finanza@decimal@rounding:mode()
) -> boolean().
should_bump_up(R, Denominator, Q, Result_sign, Mode) ->
    case Mode of
        half_even ->
            half_even_bump(R, Denominator, Q);

        half_up ->
            (2 * R) >= Denominator;

        half_down ->
            (2 * R) > Denominator;

        up ->
            true;

        down ->
            false;

        ceiling ->
            Result_sign > 0;

        floor ->
            Result_sign < 0
    end.

-file("src/finanza/decimal.gleam", 493).
-spec apply_rounding(
    integer(),
    integer(),
    integer(),
    integer(),
    finanza@decimal@rounding:mode()
) -> integer().
apply_rounding(Q, R, Denominator, Result_sign, Mode) ->
    gleam@bool:guard(
        R =:= 0,
        Q,
        fun() -> case should_bump_up(R, Denominator, Q, Result_sign, Mode) of
                true ->
                    Q + 1;

                false ->
                    Q
            end end
    ).

-file("src/finanza/decimal.gleam", 636).
-spec strip_trailing_zeros(decimal()) -> decimal().
strip_trailing_zeros(D) ->
    case erlang:element(2, D) rem 10 of
        0 ->
            strip_trailing_zeros(
                {decimal, erlang:element(2, D) div 10, erlang:element(3, D) + 1}
            );

        _ ->
            D
    end.

-file("src/finanza/decimal.gleam", 629).
-spec normalize(decimal()) -> decimal().
normalize(D) ->
    case erlang:element(2, D) of
        0 ->
            {decimal, 0, 0};

        _ ->
            strip_trailing_zeros(D)
    end.

-file("src/finanza/decimal.gleam", 623).
?DOC(
    " Equality test by numeric value, not by representation.\n"
    " `equal(new(coefficient: 100, exponent: -2), one())` is `True`.\n"
).
-spec equal(decimal(), decimal()) -> boolean().
equal(A, B) ->
    Na = normalize(A),
    Nb = normalize(B),
    (erlang:element(2, Na) =:= erlang:element(2, Nb)) andalso (erlang:element(
        3,
        Na
    )
    =:= erlang:element(3, Nb)).

-file("src/finanza/decimal.gleam", 660).
-spec reverse_order(gleam@order:order()) -> gleam@order:order().
reverse_order(O) ->
    case O of
        lt ->
            gt;

        gt ->
            lt;

        eq ->
            eq
    end.

-file("src/finanza/decimal.gleam", 668).
-spec digit_count(integer()) -> integer().
digit_count(N) ->
    gleam@bool:guard(N < 10, 1, fun() -> 1 + digit_count(N div 10) end).

-file("src/finanza/decimal.gleam", 650).
-spec compare_same_sign(decimal(), decimal()) -> gleam@order:order().
compare_same_sign(A, B) ->
    Na = normalize(A),
    Nb = normalize(B),
    Mag_a = digit_count(gleam@int:absolute_value(erlang:element(2, Na))) + erlang:element(
        3,
        Na
    ),
    Mag_b = digit_count(gleam@int:absolute_value(erlang:element(2, Nb))) + erlang:element(
        3,
        Nb
    ),
    Raw = gleam@int:compare(Mag_a, Mag_b),
    gleam@bool:guard(
        erlang:element(2, Na) < 0,
        reverse_order(Raw),
        fun() -> Raw end
    ).

-file("src/finanza/decimal.gleam", 675).
-spec sign_of(integer()) -> integer().
sign_of(N) ->
    case gleam@int:compare(N, 0) of
        lt ->
            -1;

        gt ->
            1;

        eq ->
            1
    end.

-file("src/finanza/decimal.gleam", 643).
-spec compare_by_magnitude(decimal(), decimal()) -> gleam@order:order().
compare_by_magnitude(A, B) ->
    case gleam@int:compare(
        sign_of(erlang:element(2, A)),
        sign_of(erlang:element(2, B))
    ) of
        eq ->
            compare_same_sign(A, B);

        Other ->
            Other
    end.

-file("src/finanza/decimal.gleam", 709).
-spec pow_10_loop(integer(), integer()) -> integer().
pow_10_loop(N, Acc) ->
    gleam@bool:guard(N =< 0, Acc, fun() -> pow_10_loop(N - 1, Acc * 10) end).

-file("src/finanza/decimal.gleam", 705).
-spec pow_10(integer()) -> integer().
pow_10(N) ->
    pow_10_loop(N, 1).

-file("src/finanza/decimal.gleam", 587).
-spec drop_digits(decimal(), integer(), finanza@decimal@rounding:mode()) -> decimal().
drop_digits(D, Target_exponent, Mode) ->
    Diff = Target_exponent - erlang:element(3, D),
    Divisor = pow_10(Diff),
    Abs_c = gleam@int:absolute_value(erlang:element(2, D)),
    Q = case Divisor of
        0 -> 0;
        Gleam@denominator -> Abs_c div Gleam@denominator
    end,
    R = Abs_c - (Q * Divisor),
    Sign = sign_of(erlang:element(2, D)),
    Bumped = apply_rounding(Q, R, Divisor, Sign, Mode),
    {decimal, Sign * Bumped, Target_exponent}.

-file("src/finanza/decimal.gleam", 547).
?DOC(
    " Round to `digits` decimal places. When the input is already at\n"
    " equal or coarser precision, the original `Decimal` is returned\n"
    " unchanged (no zero-padding); call [`rescale`](#rescale) to force a\n"
    " target exponent.\n"
).
-spec round(decimal(), integer(), finanza@decimal@rounding:mode()) -> decimal().
round(D, Digits, Mode) ->
    Target_exponent = - Digits,
    gleam@bool:guard(
        Target_exponent =< erlang:element(3, D),
        D,
        fun() -> drop_digits(D, Target_exponent, Mode) end
    ).

-file("src/finanza/decimal.gleam", 560).
?DOC(
    " Truncate to `digits` decimal places (rounding toward zero). When\n"
    " the input is already at equal or coarser precision, the original\n"
    " `Decimal` is returned unchanged.\n"
).
-spec truncate(decimal(), integer()) -> decimal().
truncate(D, Digits) ->
    round(D, Digits, down).

-file("src/finanza/decimal.gleam", 144).
-spec finalize_parse(parse_state()) -> {ok, decimal()} | {error, parse_error()}.
finalize_parse(State) ->
    gleam@bool:guard(
        not erlang:element(9, State),
        {error, no_digits},
        fun() ->
            gleam@bool:guard(
                gleam@int:absolute_value(erlang:element(5, State)) > 9007199254740991,
                {error, parsed_value_too_large},
                fun() ->
                    {ok,
                        {decimal,
                            erlang:element(4, State) * erlang:element(5, State),
                            - erlang:element(6, State)}}
                end
            )
        end
    ).

-file("src/finanza/decimal.gleam", 683).
-spec check_precision(integer()) -> {ok, integer()} |
    {error, arithmetic_error()}.
check_precision(N) ->
    gleam@bool:guard(
        gleam@int:absolute_value(N) > 9007199254740991,
        {error, precision_exceeded},
        fun() -> {ok, N} end
    ).

-file("src/finanza/decimal.gleam", 426).
?DOC(" Multiply two decimals.\n").
-spec multiply(decimal(), decimal()) -> {ok, decimal()} |
    {error, arithmetic_error()}.
multiply(A, B) ->
    gleam@result:map(
        check_precision(erlang:element(2, A) * erlang:element(2, B)),
        fun(Product) ->
            {decimal, Product, erlang:element(3, A) + erlang:element(3, B)}
        end
    ).

-file("src/finanza/decimal.gleam", 698).
-spec scale_up(integer(), integer()) -> {ok, integer()} |
    {error, arithmetic_error()}.
scale_up(C, By) ->
    case By of
        0 ->
            {ok, C};

        _ ->
            check_precision(C * pow_10(By))
    end.

-file("src/finanza/decimal.gleam", 476).
-spec prepare_division(integer(), integer(), integer()) -> {ok,
        {integer(), integer()}} |
    {error, arithmetic_error()}.
prepare_division(Abs_a, Abs_b, Shift) ->
    case Shift >= 0 of
        true ->
            gleam@result:map(
                scale_up(Abs_a, Shift),
                fun(Scaled_a) -> {Scaled_a, Abs_b} end
            );

        false ->
            gleam@result:map(
                scale_up(Abs_b, - Shift),
                fun(Scaled_b) -> {Abs_a, Scaled_b} end
            )
    end.

-file("src/finanza/decimal.gleam", 447).
-spec divide_nonzero(
    decimal(),
    decimal(),
    integer(),
    finanza@decimal@rounding:mode()
) -> {ok, decimal()} | {error, arithmetic_error()}.
divide_nonzero(A, B, Digits, Mode) ->
    Result_sign = sign_of(erlang:element(2, A)) * sign_of(erlang:element(2, B)),
    Abs_a = gleam@int:absolute_value(erlang:element(2, A)),
    Abs_b = gleam@int:absolute_value(erlang:element(2, B)),
    Shift = (erlang:element(3, A) - erlang:element(3, B)) + Digits,
    gleam@result:'try'(
        prepare_division(Abs_a, Abs_b, Shift),
        fun(_use0) ->
            {Numerator, Denominator} = _use0,
            Q = case Denominator of
                0 -> 0;
                Gleam@denominator -> Numerator div Gleam@denominator
            end,
            R = Numerator - (Q * Denominator),
            Bumped = apply_rounding(Q, R, Denominator, Result_sign, Mode),
            gleam@result:map(
                check_precision(Bumped),
                fun(Safe_q) -> {decimal, Result_sign * Safe_q, - Digits} end
            )
        end
    ).

-file("src/finanza/decimal.gleam", 437).
?DOC(
    " Divide `a` by `b`, returning a result rounded to `digits` decimal\n"
    " places using `mode`.\n"
    "\n"
    " Returns [`DivisionByZero`](#ArithmeticError) when `b` is zero, or\n"
    " [`PrecisionExceeded`](#ArithmeticError) when the intermediate\n"
    " representation would exceed `±9_007_199_254_740_991`.\n"
).
-spec divide(decimal(), decimal(), integer(), finanza@decimal@rounding:mode()) -> {ok,
        decimal()} |
    {error, arithmetic_error()}.
divide(A, B, Digits, Mode) ->
    gleam@bool:guard(
        erlang:element(2, B) =:= 0,
        {error, division_by_zero},
        fun() -> divide_nonzero(A, B, Digits, Mode) end
    ).

-file("src/finanza/decimal.gleam", 568).
?DOC(
    " Force the decimal to a specific exponent. When the new exponent is\n"
    " finer (smaller), the coefficient grows by zero-padding (may overflow).\n"
    " When the new exponent is coarser (larger), digits are dropped using\n"
    " `mode`.\n"
).
-spec rescale(decimal(), integer(), finanza@decimal@rounding:mode()) -> {ok,
        decimal()} |
    {error, arithmetic_error()}.
rescale(D, Target_exponent, Mode) ->
    case gleam@int:compare(Target_exponent, erlang:element(3, D)) of
        eq ->
            {ok, D};

        lt ->
            gleam@result:map(
                scale_up(
                    erlang:element(2, D),
                    erlang:element(3, D) - Target_exponent
                ),
                fun(Scaled) -> {decimal, Scaled, Target_exponent} end
            );

        gt ->
            {ok, drop_digits(D, Target_exponent, Mode)}
    end.

-file("src/finanza/decimal.gleam", 691).
-spec align(decimal(), decimal()) -> {ok, {integer(), integer(), integer()}} |
    {error, arithmetic_error()}.
align(A, B) ->
    Target = gleam@int:min(erlang:element(3, A), erlang:element(3, B)),
    gleam@result:'try'(
        scale_up(erlang:element(2, A), erlang:element(3, A) - Target),
        fun(Scaled_a) ->
            gleam@result:map(
                scale_up(erlang:element(2, B), erlang:element(3, B) - Target),
                fun(Scaled_b) -> {Scaled_a, Scaled_b, Target} end
            )
        end
    ).

-file("src/finanza/decimal.gleam", 414).
?DOC(" Add two decimals.\n").
-spec add(decimal(), decimal()) -> {ok, decimal()} | {error, arithmetic_error()}.
add(A, B) ->
    gleam@result:'try'(
        align(A, B),
        fun(_use0) ->
            {Ac, Bc, Target} = _use0,
            gleam@result:map(
                check_precision(Ac + Bc),
                fun(Sum) -> {decimal, Sum, Target} end
            )
        end
    ).

-file("src/finanza/decimal.gleam", 421).
?DOC(" Subtract `b` from `a`.\n").
-spec subtract(decimal(), decimal()) -> {ok, decimal()} |
    {error, arithmetic_error()}.
subtract(A, B) ->
    add(A, negate(B)).

-file("src/finanza/decimal.gleam", 613).
?DOC(
    " Total ordering. Two values with the same numeric value compare as\n"
    " equal even when their exponents differ.\n"
).
-spec compare(decimal(), decimal()) -> gleam@order:order().
compare(A, B) ->
    case align(A, B) of
        {ok, {Ac, Bc, _}} ->
            gleam@int:compare(Ac, Bc);

        {error, precision_exceeded} ->
            compare_by_magnitude(A, B);

        {error, division_by_zero} ->
            compare_by_magnitude(A, B)
    end.

-file("src/finanza/decimal.gleam", 192).
-spec handle_sign(parse_state(), integer(), list(binary())) -> {ok, decimal()} |
    {error, parse_error()}.
handle_sign(State, Value, Rest) ->
    gleam@bool:guard(
        (erlang:element(3, State) /= 0) orelse erlang:element(8, State),
        {error, multiple_signs},
        fun() ->
            parse_loop(
                {parse_state,
                    Rest,
                    erlang:element(3, State) + 1,
                    Value,
                    erlang:element(5, State),
                    erlang:element(6, State),
                    erlang:element(7, State),
                    true,
                    erlang:element(9, State)}
            )
        end
    ).

-file("src/finanza/decimal.gleam", 137).
-spec parse_loop(parse_state()) -> {ok, decimal()} | {error, parse_error()}.
parse_loop(State) ->
    case erlang:element(2, State) of
        [] ->
            finalize_parse(State);

        [Head | Tail] ->
            parse_step(State, Head, Tail)
    end.

-file("src/finanza/decimal.gleam", 153).
-spec parse_step(parse_state(), binary(), list(binary())) -> {ok, decimal()} |
    {error, parse_error()}.
parse_step(State, Char, Rest) ->
    case classify(Char) of
        {sign_char, Value} ->
            handle_sign(State, Value, Rest);

        dot_char ->
            handle_dot(State, Rest);

        {digit_char, Value@1} ->
            handle_digit(State, Value@1, Rest);

        other_char ->
            {error, {invalid_character, Char, erlang:element(3, State)}}
    end.

-file("src/finanza/decimal.gleam", 109).
?DOC(
    " Parse a decimal from a string. Accepts an optional leading `+` or\n"
    " `-`, decimal digits, and at most one `.` separator. Scientific\n"
    " notation is not supported.\n"
    "\n"
    " ```gleam\n"
    " from_string(\"3.14\")    // Ok(Decimal with coefficient=314, exponent=-2)\n"
    " from_string(\"-0.5\")    // Ok(Decimal with coefficient=-5, exponent=-1)\n"
    " from_string(\"\")        // Error(EmptyInput)\n"
    " from_string(\"1.2.3\")   // Error(MultipleDecimalPoints)\n"
    " ```\n"
).
-spec from_string(binary()) -> {ok, decimal()} | {error, parse_error()}.
from_string(Input) ->
    Trimmed = gleam@string:trim(Input),
    gleam@bool:guard(
        Trimmed =:= <<""/utf8>>,
        {error, empty_input},
        fun() ->
            parse_loop(
                {parse_state,
                    gleam@string:to_graphemes(Trimmed),
                    0,
                    1,
                    0,
                    0,
                    false,
                    false,
                    false}
            )
        end
    ).

-file("src/finanza/decimal.gleam", 212).
-spec handle_dot(parse_state(), list(binary())) -> {ok, decimal()} |
    {error, parse_error()}.
handle_dot(State, Rest) ->
    gleam@bool:guard(
        erlang:element(7, State),
        {error, multiple_decimal_points},
        fun() ->
            parse_loop(
                {parse_state,
                    Rest,
                    erlang:element(3, State) + 1,
                    erlang:element(4, State),
                    erlang:element(5, State),
                    erlang:element(6, State),
                    true,
                    erlang:element(8, State),
                    erlang:element(9, State)}
            )
        end
    ).

-file("src/finanza/decimal.gleam", 227).
-spec handle_digit(parse_state(), integer(), list(binary())) -> {ok, decimal()} |
    {error, parse_error()}.
handle_digit(State, Value, Rest) ->
    New_fraction = case erlang:element(7, State) of
        true ->
            erlang:element(6, State) + 1;

        false ->
            erlang:element(6, State)
    end,
    parse_loop(
        {parse_state,
            Rest,
            erlang:element(3, State) + 1,
            erlang:element(4, State),
            (erlang:element(5, State) * 10) + Value,
            New_fraction,
            erlang:element(7, State),
            erlang:element(8, State),
            true}
    ).