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