import bigdecimal/rounding.{ type RoundingMode, Ceiling, Floor, HalfDown, HalfEven, HalfUp, Up, } import bigi.{type BigInt} import gleam/float import gleam/int import gleam/list import gleam/order.{Eq, Gt, Lt} import gleam/result import gleam/string import gleam/string_tree pub opaque type BigDecimal { BigDecimal(unscaled_value: BigInt, scale: Int) } pub fn unscaled_value(of value: BigDecimal) -> BigInt { let BigDecimal(unscaled_value:, ..) = value unscaled_value } pub fn scale(of value: BigDecimal) -> Int { let BigDecimal(scale:, ..) = value scale } pub fn zero() -> BigDecimal { BigDecimal(bigi.from_int(0), 0) } pub fn one() -> BigDecimal { BigDecimal(bigi.from_int(1), 0) } /// The number of digits in the unscaled value. /// pub fn precision(of value: BigDecimal) -> Int { // todo: seems kinda inefficient let string_length = unscaled_value(value) |> bigi.to_string |> string.byte_size case signum(value) { -1 -> string_length - 1 _ -> string_length } } pub fn absolute_value(of value: BigDecimal) -> BigDecimal { BigDecimal(bigi.absolute(unscaled_value(value)), scale(value)) } /// Sign function. Returns +1 if the value is positive, /// -1 if the value is negative, 0 if the value is zero. /// pub fn signum(of value: BigDecimal) -> Int { compare_to_zero(value) |> order.to_int } fn compare_to_zero(value: BigDecimal) -> order.Order { unscaled_value(value) |> bigi.compare(with: bigi.zero()) } /// Returns the unit of least precision of this `BigDecimal`. /// pub fn ulp(of value: BigDecimal) -> BigDecimal { BigDecimal(bigi.from_int(1), scale(value)) } pub fn negate(value: BigDecimal) -> BigDecimal { BigDecimal(bigi.negate(unscaled_value(value)), scale(value)) } /// Trim trailing zeros from after the decimal point. /// pub fn trim_zeros(value: BigDecimal) -> BigDecimal { trim_zeros_to_match_scale(value, 0) } /// Truncate the `BigDecimal` to a `BigInt`. /// pub fn truncate_to_bigint(value: BigDecimal) -> BigInt { unscaled_value(value) |> bigi.divide(multiply_power_of_ten(bigi.from_int(1), scale(value))) } /// N.B. if the input has equal value to either of the extremes /// but different precision, the larger precision will be returned /// pub fn clamp( value: BigDecimal, min min: BigDecimal, max max: BigDecimal, ) -> BigDecimal { case compare(value, min) { Eq -> case int.compare(scale(value), scale(min)) { Eq | Gt -> value Lt -> min } Lt -> min Gt -> case compare(value, max) { Eq -> case int.compare(scale(value), scale(max)) { Eq | Lt -> value Gt -> max } Gt -> max Lt -> value } } } pub fn rescale( value: BigDecimal, scale new_scale: Int, rounding rounding: RoundingMode, ) -> BigDecimal { let scale_diff = scale(value) - new_scale case int.compare(scale_diff, 0) { Eq -> value Lt -> add_zeros(value, new_scale, -scale_diff) Gt -> rescale_with_rounding(value, new_scale, scale_diff, rounding) } } fn add_zeros(value: BigDecimal, new_scale: Int, scale_diff: Int) { unscaled_value(value) |> multiply_power_of_ten(scale_diff) |> BigDecimal(new_scale) } fn rescale_with_rounding( value: BigDecimal, new_scale: Int, scale_diff: Int, rounding: RoundingMode, ) { let tens = multiply_power_of_ten(bigi.from_int(1), scale_diff) let unscaled = unscaled_value(value) let quotient = bigi.divide(unscaled, tens) adjust_unscaled_value_for_rounding( unscaled, signum(value), quotient, tens, rounding, ) |> BigDecimal(new_scale) } fn adjust_unscaled_value_for_rounding( unscaled_value: BigInt, quotient_sign: Int, quotient: BigInt, divisor: BigInt, rounding: RoundingMode, ) { let adjustment = case rounding, quotient_sign { Floor, s if s < 0 -> bigi.from_int(-1) Ceiling, s if s > 0 -> bigi.from_int(1) Up, s -> bigi.from_int(s) HalfUp, s | HalfDown, s | HalfEven, s if s != 0 -> calculate_adjustment_for_half_round( unscaled_value, s, quotient, divisor, rounding, ) _, _ -> bigi.zero() } quotient |> bigi.add(adjustment) } fn calculate_adjustment_for_half_round( unscaled_value: BigInt, sign: Int, quotient: BigInt, power_of_ten: BigInt, rounding: RoundingMode, ) { let remainder = bigi.multiply(quotient, power_of_ten) |> bigi.subtract(unscaled_value) |> bigi.absolute let to_compare = get_magnitude(remainder, 0) |> multiply_power_of_ten(bigi.from_int(5), _) case bigi.compare(remainder, to_compare), rounding { Eq, HalfEven -> case is_even(quotient) { True -> bigi.zero() False -> bigi.from_int(sign) } Eq, HalfUp -> bigi.from_int(sign) Gt, _ -> bigi.from_int(sign) _, _ -> bigi.zero() } } fn is_even(value: BigInt) { case bigi.modulo(value, bigi.from_int(2)) |> bigi.to_int { Ok(0) -> True _ -> False } } fn get_magnitude(value: BigInt, magnitude: Int) { let quotient = bigi.divide(value, bigi.from_int(10)) case bigi.compare(quotient, bigi.zero()) { Lt | Gt -> get_magnitude(quotient, magnitude + 1) Eq -> magnitude } } pub fn add(augend: BigDecimal, addend: BigDecimal) -> BigDecimal { case int.subtract(scale(augend), scale(addend)) { scale_difference if scale_difference < 0 -> scale_adjusted_add(augend, addend, -scale_difference) scale_difference if scale_difference > 0 -> scale_adjusted_add(addend, augend, scale_difference) _same_scale -> BigDecimal( bigi.add(unscaled_value(augend), unscaled_value(addend)), scale(augend), ) } } pub fn sum(values: List(BigDecimal)) -> BigDecimal { // this may be more efficient? idk need to benchmark // list.reduce(over: values, with: add) // |> result.lazy_unwrap(zero) list.fold(over: values, from: zero(), with: add) } pub fn subtract(minuend: BigDecimal, subtrahend: BigDecimal) -> BigDecimal { case int.subtract(scale(minuend), scale(subtrahend)) { scale_difference if scale_difference < 0 -> scale_adjusted_add(minuend, negate(subtrahend), -scale_difference) scale_difference if scale_difference > 0 -> scale_adjusted_add(negate(subtrahend), minuend, scale_difference) _same_scale -> BigDecimal( bigi.subtract(unscaled_value(minuend), unscaled_value(subtrahend)), scale(minuend), ) } } fn scale_adjusted_add( to_scale: BigDecimal, to_add: BigDecimal, scale_difference: Int, ) -> BigDecimal { unscaled_value(to_scale) |> multiply_power_of_ten(scale_difference) |> bigi.add(unscaled_value(to_add)) |> BigDecimal(scale(to_add)) } fn multiply_power_of_ten(value: BigInt, n: Int) { let assert True = n >= 0 bigi.from_int(10) |> bigi.power(bigi.from_int(n)) // unreachable unwrap |> result.lazy_unwrap(fn() { bigi.from_int(0) }) |> bigi.multiply(value) } /// N.B. If scale is different, trailing zeros are ignored. /// pub fn compare(this: BigDecimal, with that: BigDecimal) -> order.Order { case int.subtract(scale(this), scale(that)) { scale_difference if scale_difference < 0 -> scale_adjusted_compare(this, that, scale_difference) scale_difference if scale_difference > 0 -> scale_adjusted_compare(that, this, scale_difference) |> order.negate _same_scale -> bigi.compare(unscaled_value(this), unscaled_value(that)) } } fn scale_adjusted_compare( to_scale: BigDecimal, to_compare: BigDecimal, scale_difference: Int, ) -> order.Order { // TODO: wonder if this could be sped up somehow let assert Ok(compare_order) = int.absolute_value(scale_difference) |> bigi.from_int |> bigi.power(bigi.from_int(10), _) |> result.map(bigi.multiply(_, unscaled_value(to_scale))) |> result.map(bigi.compare(_, unscaled_value(to_compare))) compare_order } pub fn multiply( multiplicand: BigDecimal, with multiplier: BigDecimal, ) -> BigDecimal { BigDecimal( bigi.multiply(unscaled_value(multiplicand), unscaled_value(multiplier)), int.add(scale(multiplicand), scale(multiplier)), ) } /// For an empty list, this will return `one()`. /// pub fn product(values: List(BigDecimal)) -> BigDecimal { // this may be more efficient? idk need to benchmark // list.reduce(over: values, with: multiply) // |> result.lazy_unwrap(one) list.fold(over: values, from: one(), with: multiply) } /// Divide the dividend by the divisor. /// The result has a preferred scale of `scale(dividend) - scale(divisor)`, /// but might have scale up to `precision(dividend) + ceil(10 * precision(divisor) / 3)`. /// pub fn divide( dividend: BigDecimal, by divisor: BigDecimal, rounding rounding: RoundingMode, ) -> BigDecimal { let new_scale = scale(dividend) - scale(divisor) case signum(dividend), signum(divisor) { _, 0 -> zero() 0, _ -> BigDecimal(bigi.from_int(0), new_scale) _, _ -> divide_internal(dividend, divisor, new_scale, rounding) } } fn divide_internal( dividend: BigDecimal, divisor: BigDecimal, preferred_scale: Int, rounding: RoundingMode, ) { let new_scale = { 10 * precision(divisor) } |> int.to_float |> float.divide(3.0) // unreachable unwrap |> result.lazy_unwrap(fn() { 0.0 }) |> float.ceiling |> float.round |> int.add(precision(dividend)) case scale(divisor) - scale(dividend) + new_scale { x if x > 0 -> unscaled_value(dividend) |> multiply_power_of_ten(x) |> divide_and_round(unscaled_value(divisor), new_scale, rounding) x -> unscaled_value(divisor) |> multiply_power_of_ten(-x) |> divide_and_round(unscaled_value(dividend), _, new_scale, rounding) } |> trim_zeros_to_match_scale(preferred_scale) } fn divide_and_round( dividend: BigInt, divisor: BigInt, scale: Int, rounding: RoundingMode, ) { let quotient = bigi.divide(dividend, divisor) let quotient_sign = bigi.compare(quotient, bigi.zero()) |> order.to_int let remainder = bigi.remainder(dividend, divisor) case bigi.compare(remainder, bigi.zero()) { Eq -> BigDecimal(quotient, scale) Lt | Gt -> adjust_unscaled_value_for_rounding( dividend, quotient_sign, quotient, divisor, rounding, ) |> BigDecimal(scale) } } /// trims trailing zeros until the requested scale /// is reached or no more zeros can be trimmed /// fn trim_zeros_to_match_scale(value: BigDecimal, preferred_scale: Int) { case scale(value) > preferred_scale { False -> value True -> { let non_zero_remainder = unscaled_value(value) |> bigi.remainder(bigi.from_int(10)) |> bigi.compare(bigi.zero()) != Eq case non_zero_remainder { True -> value False -> trim_zeros_to_match_scale( unscaled_value(value) |> bigi.divide(bigi.from_int(10)) |> BigDecimal(scale(value) - 1), preferred_scale, ) } } } } /// Computes the **truncating remainder** of two `BigDecimal` values. /// /// The sign of the result always matches the sign of `value` /// (the dividend), and the remainder may be negative. /// /// Follows the standard Gleam divide-by-zero rule of 0 when the divisor is 0. /// /// Note: This is **not** a modulo operation. /// pub fn remainder(value: BigDecimal, divisor: BigDecimal) -> BigDecimal { let common_scale = int.max(scale(value), scale(divisor)) bigi.remainder( rescale(value, common_scale, Floor) |> unscaled_value, rescale(divisor, common_scale, Floor) |> unscaled_value, ) |> BigDecimal(common_scale) } /// Returns an error if the exponent is negative. /// (Inherited behaviour from `bigi`) /// pub fn power(value: BigDecimal, exponent: Int) { unscaled_value(value) |> bigi.power(bigi.from_int(exponent)) |> result.map(BigDecimal(_, int.multiply(scale(value), exponent))) } pub fn from_float(value: Float) -> BigDecimal { // TODO: this works fine but idk if it could be better/quicker let assert Ok(bigd) = float.to_string(value) |> from_string bigd } pub fn from_string(value: String) -> Result(BigDecimal, Nil) { parse_exponential(value) } fn parse_exponential(value: String) -> Result(BigDecimal, Nil) { let value = value |> string.trim |> string.lowercase case value |> string.split_once("e") { Ok(#(number, exponent)) -> int.parse(exponent) |> result.map(int.negate) |> result.try(parse_decimal(number, _)) Error(_) -> parse_decimal(value, 0) } } fn parse_decimal(value: String, initial_scale: Int) -> Result(BigDecimal, Nil) { case value |> string.split_once(".") { Ok(#(before, after)) -> parse_unscaled(before <> after, string.byte_size(after) + initial_scale) Error(_) -> parse_unscaled(value, initial_scale) } } fn parse_unscaled(value: String, scale: Int) -> Result(BigDecimal, Nil) { bigi.from_string(value) |> result.map(BigDecimal(_, scale)) } /// Converts a BigDecimal into a plain (non-scientific) decimal string. /// /// This function returns a **lossless** string representation of the number: /// /// - The number’s **scale is preserved** /// - Trailing zeros are **not trimmed** /// - The result can be parsed back to the same exact BigDecimal /// pub fn to_plain_string(value: BigDecimal) -> String { let scale = scale(value) let unscaled_abs = unscaled_value(value) |> bigi.absolute |> bigi.to_string let string_builder = case compare_to_zero(value) { Lt -> string_tree.from_string("-") _ -> string_tree.new() } let integer_part_end_zeros = string.repeat("0", times: -scale) let integer_part_value = string.drop_end(unscaled_abs, scale) let string_builder = case integer_part_value { "" | "0" -> string_tree.append(string_builder, "0") _ -> string_tree.append(string_builder, integer_part_value) |> string_tree.append(integer_part_end_zeros) } let digits = string.byte_size(unscaled_abs) let fractional_part_start_zeros = string.repeat("0", times: scale - digits) let fractional_part_digits = int.min(digits, int.max(0, scale)) let fractional_part_value = string.slice( unscaled_abs, at_index: -fractional_part_digits, length: fractional_part_digits, ) let string_builder = case fractional_part_value { "" -> string_builder _ -> string_tree.append(string_builder, ".") |> string_tree.append(fractional_part_start_zeros) |> string_tree.append(fractional_part_value) } string_tree.to_string(string_builder) }