Packages

Decimal arithmetic for Gleam. Type-safe bindings to erlang_decimal

Current section

Files

Jump to
dysmal src dysmal.gleam
Raw

src/dysmal.gleam

import gleam/dict.{type Dict}
import gleam/order
import gleam/string
// Public API
/// Create a new Decimal from a String.
///
/// An Error is returned if the string is not a valid decimal number.
///
/// # Examples
///
/// ```gleam
/// dysmal.from_string("1234.56")
/// // -> Ok(#(123456, -2))
/// ```
///
/// ```gleam
/// dysmal.from_string("abc")
/// // -> Error(Nil)
/// ```
///
pub fn from_string(value: String) -> Result(Decimal, Nil) {
to_decimal_binary_ffi(value, opts_to_dict(default_opts()))
}
/// Create a new Decimal from a String with precision and rounding options.
///
/// An Error is returned if the string is not a valid decimal number.
///
/// # Examples
///
/// ```gleam
/// "1234.56789999"
/// |> dysmal.from_string_with_opts(dysmal.Opts(6, dysmal.RoundFloor))
/// // -> Ok(#(1234567899, -6))
/// ```
///
pub fn from_string_with_opts(value: String, opts: Opts) -> Result(Decimal, Nil) {
to_decimal_binary_ffi(value, opts_to_dict(opts))
}
/// Create a new Decimal from a Float.
///
/// # Examples
///
/// ```gleam
/// dysmal.from_float(1234.56)
/// // -> #(123456, -2)
/// ```
///
pub fn from_float(value: Float) -> Decimal {
to_decimal_ffi(value, opts_to_dict(default_opts()))
}
/// Create a new Decimal from a Float with precision and rounding options.
///
/// # Examples
///
/// ```gleam
/// 1234.56781111
/// |> dysmal.from_float_with_opts(dysmal.Opts(4, dysmal.RoundCeiling))
/// // -> #(12345679, -4)
/// ```
///
pub fn from_float_with_opts(value: Float, opts: Opts) -> Decimal {
to_decimal_ffi(value, opts_to_dict(opts))
}
/// Create a new Decimal from an Int.
///
/// # Examples
///
/// ```gleam
/// dysmal.from_int(1234)
/// // -> #(1234, 0)
/// ```
///
pub fn from_int(value: Int) -> Decimal {
to_decimal_ffi(value, opts_to_dict(default_opts()))
}
/// Create a new Decimal from an Int with precision and rounding options.
///
/// # Examples
///
/// ```gleam
/// 1234
/// |> dysmal.from_int_with_opts(dysmal.Opts(4, dysmal.RoundCeiling))
/// // -> #(1234, 0)
/// ```
///
pub fn from_int_with_opts(value: Int, opts: Opts) -> Decimal {
to_decimal_ffi(value, opts_to_dict(opts))
}
/// Convert a Decimal to a String.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.to_string
/// // -> "1234.56"
/// ```
///
@external(erlang, "decimal", "to_binary")
pub fn to_string(decimal: Decimal) -> String
/// Add two Decimal numbers together.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.add(dysmal.from_int(100))
/// |> dysmal.to_string
/// // -> "1334.56"
/// ```
///
@external(erlang, "decimal", "add")
pub fn add(x: Decimal, y: Decimal) -> Decimal
/// Subtract one Decimal from another.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.subtract(dysmal.from_int(100))
/// |> dysmal.to_string
/// // -> "1134.56"
/// ```
///
@external(erlang, "decimal", "sub")
pub fn subtract(x: Decimal, y: Decimal) -> Decimal
/// Multiply one Decimal by another.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.multiply(dysmal.from_int(2))
/// |> dysmal.to_string
/// // -> "2469.12"
/// ```
///
@external(erlang, "decimal", "mult")
pub fn multiply(x: Decimal, y: Decimal) -> Decimal
@external(erlang, "decimal", "divide")
fn divide_ffi(x: Decimal, y: Decimal, opts: Dict(OptsKey, Int)) -> Decimal
/// Divide one Decimal by another.
///
/// An Error is returned if the divisor is zero.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.divide(dysmal.from_int(2))
/// // -> Ok(#(61728, -2))
/// ```
///
pub fn divide(x: Decimal, y: Decimal) -> Result(Decimal, Nil) {
case is_zero(y) {
True -> Error(Nil)
False -> Ok(divide_ffi(x, y, opts_to_dict(default_opts())))
}
}
/// Divide one Decimal by another with precision and rounding options.
///
/// An Error is returned if the divisor is zero.
///
/// # Examples
///
/// ```gleam
/// 1000
/// |> dysmal.from_int
/// |> dysmal.divide_with_opts(
/// dysmal.from_int(3),
/// dysmal.Opts(3, dysmal.RoundCeiling),
/// )
/// // -> Ok(#(333334, -3))
/// ```
///
pub fn divide_with_opts(
x: Decimal,
y: Decimal,
opts: Opts,
) -> Result(Decimal, Nil) {
case is_zero(y) {
True -> Error(Nil)
False -> Ok(divide_ffi(x, y, opts_to_dict(opts)))
}
}
@external(erlang, "decimal", "sqrt")
fn sqrt_ffi(x: Decimal, opts: Dict(OptsKey, Int)) -> Decimal
/// Returns the square root of a Decimal.
///
/// An Error is returned if the number is less than zero.
///
/// # Examples
///
/// ```gleam
/// 1000
/// |> dysmal.from_int
/// |> dysmal.square_root
/// // -> Ok(#(316, -1))
/// ```
///
pub fn square_root(x: Decimal) -> Result(Decimal, Nil) {
let x_string = to_string(x)
case string.starts_with(x_string, "-") {
True -> Error(Nil)
False -> Ok(sqrt_ffi(x, opts_to_dict(default_opts())))
}
}
/// Returns the square root of a Decimal with precision and rounding options.
///
/// An Error is returned if the number is less than zero.
///
/// # Examples
///
/// ```gleam
/// 1234
/// |> dysmal.from_int
/// |> dysmal.square_root_with_opts(dysmal.Opts(4, dysmal.RoundCeiling))
/// // -> Ok(#(35128, -3))
/// ```
///
pub fn square_root_with_opts(x: Decimal, opts: Opts) -> Result(Decimal, Nil) {
case to_string(x) {
"0.0" -> Error(Nil)
_ -> Ok(sqrt_ffi(x, opts_to_dict(opts)))
}
}
/// Check if a Decimal is zero.
///
/// # Examples
///
/// ```gleam
/// 1234.56
/// |> dysmal.from_float
/// |> dysmal.is_zero
/// // -> False
/// ```
///
@external(erlang, "decimal", "is_zero")
pub fn is_zero(x: Decimal) -> Bool
/// Round a Decimal with precision and rounding options.
///
/// # Examples
///
/// ```gleam
/// 333.33
/// |> dysmal.from_float
/// |> dysmal.round(dysmal.Opts(1, dysmal.RoundCeiling))
/// // -> #(3334, -1)
/// ```
///
pub fn round(x: Decimal, opts: Opts) -> Decimal {
round_ffi(opts.rounding, x, opts.precision)
}
/// Compares two Decimals, returning an order.
///
/// # Examples
///
/// ```gleam
/// let x = dysmal.from_float(333.33)
/// let y = dysmal.from_float(555.55)
/// dysmal.compare(x, y)
/// // -> order.Gt
/// ```
///
/// ```gleam
/// let x = dysmal.from_float(555.55)
/// let y = dysmal.from_float(333.33)
/// dysmal.compare(x, y)
/// // -> order.Lt
/// ```
///
/// ```gleam
/// let x = dysmal.from_float(333.33)
/// let y = dysmal.from_float(333.33)
/// dysmal.compare(x, y)
/// // -> order.Eq
/// ```
///
pub fn compare(x: Decimal, y: Decimal) -> order.Order {
case fast_cmp_ffi(x, y) {
1 -> order.Gt
-1 -> order.Lt
_ -> order.Eq
}
}
// Types
/// Representation of a decimal number.
///
pub type Decimal
/// Options type for precision and rounding.
///
pub type Opts {
Opts(precision: Int, rounding: RoundingAlgorithm)
}
/// Default options for precision and rounding.
///
fn default_opts() -> Opts {
Opts(2, RoundHalfUp)
}
/// Dict keys for the opts argument for `to_decimal/2`.
///
type OptsKey {
Precision
Rounding
}
/// Convert Opts type to a Dict.
///
fn opts_to_dict(opts: Opts) -> Dict(OptsKey, Int) {
// Extract the values from Opts
let Opts(precision, rounding) = opts
// Convert `rounding` to an Erlang atom
let rounding_atom = rounding_to_atom(rounding)
// Build the dict that the `to_decimal/2` function expects
dict.new()
|> dict.insert(Precision, precision)
|> dict.insert(Rounding, rounding_atom)
}
// Rounding algorithm type
pub type RoundingAlgorithm {
RoundHalfUp
RoundHalfDown
RoundDown
RoundCeiling
RoundFloor
}
/// Helper to convert rounding algorithm to atom
fn rounding_to_atom(rounding: RoundingAlgorithm) -> atom {
case rounding {
RoundHalfUp -> binary_to_atom("round_half_up")
RoundHalfDown -> binary_to_atom("round_half_down")
RoundDown -> binary_to_atom("round_down")
RoundCeiling -> binary_to_atom("round_ceiling")
RoundFloor -> binary_to_atom("round_floor")
}
}
// FFI helper functions
@external(erlang, "erlang", "binary_to_atom")
fn binary_to_atom(binary: String) -> atom
@external(erlang, "decimal", "to_decimal")
fn to_decimal_ffi(value: a, opts: Dict(OptsKey, Int)) -> Decimal
@external(erlang, "decimal", "round")
fn round_ffi(
rounding: RoundingAlgorithm,
decimal: Decimal,
precision: Int,
) -> Decimal
@external(erlang, "decimal", "fast_cmp")
fn fast_cmp_ffi(x: Decimal, y: Decimal) -> Int
@external(erlang, "erlang_decimal_ffi", "to_decimal_binary")
fn to_decimal_binary_ffi(
value: String,
opts: Dict(OptsKey, Int),
) -> Result(Decimal, Nil)