slang

decimal

Package decimal.

Markdown version · llms.txt · api.json

Exact decimal arithmetic for money: up to 38 significant digits (SQL's DECIMAL(38)), an explicit scale, seven rounding modes, and overflow as none or an error, never a wrap.

import "decimal";

let r = decimal.parse("19.99");                // result[Decimal, str]
guard let price = r else let e = err_of(r) { panic(e); }
let total = decimal.mul(price, decimal.from_int(3)) ?? decimal.zero();   // 59.97
let rate = decimal.parse("0.075") ?? decimal.zero();
let vat = decimal.mul_round(total, rate, 2, decimal.Rounding.HalfEven)
          ?? decimal.zero();                    // 4.50
decimal.format(vat);                            // "4.50"
decimal.div(total, decimal.from_int(7), 2, decimal.Rounding.HalfEven);  // ok(8.57)
decimal.split(total, 2);                        // [29.99, 29.98]: no cent lost
decimal.allocate(total, [1, 2]);                // ok([19.99, 39.98])
decimal.to_units(total, 2, decimal.Rounding.HalfEven) ?? 0;   // 5997
decimal.eq(decimal.parse("1.5") ?? total, decimal.parse("1.50") ?? total);  // true

A Decimal is a coefficient and a scale (digits after the point, 0 to 38), kept as Postgres's numeric keeps it: "1.50" stays 1.50, add keeps the larger scale, mul adds the two. It is a value (five base-10^9 limbs), so arithmetic allocates nothing; only format, split and allocate build something on the heap.

  • Exact unless you ask. add, sub and mul are exact and return none when the result needs more than 38 digits (or mul, a scale past 38: use mul_round). mul_round, div and round round to the scale you give, by the Rounding you give: HalfEven (banker's, the usual choice for money), HalfUp, HalfDown, Down and Up (toward and away from zero), Floor, Ceiling. div is an error for a zero divisor.
  • Parsing is strict, like to_int: an optional sign, digits, an optional fraction and exponent ("1.5e3"). No spaces, separators, leading or trailing point, NaN or infinity. parse_bytes(b, lo, hi) reads a number inside a buffer without making a str. from_float goes through the float's shortest text, so 0.1 is 0.1, not 0.1000000000000000055511151231257827.
  • Comparing: cmp, eq, lt, le, gt, ge, min, max compare values, so 1.5 equals 1.50. Structs have no ==. Use normalize (strip trailing zeros) before a Decimal becomes a map key.
  • Splitting money: split(d, n) and allocate(d, weights) divide an amount at its own scale so the parts add up to it exactly. The leftover smallest units go to the parts with the largest remainders, earlier ones first.
  • JSON and Postgres. A decimal.Decimal field is written by json.encode as a string holding its exact text ("19.90"), which is safe for JavaScript clients, whose numbers lose digits past 2^53. json.decode reads one from a string or a number, exactly, without a float in between. pg.arg_decimal(d) binds one for a numeric parameter, and pg.get_decimal(rows, r, c) reads a numeric or integer column at the scale Postgres reports. NaN and infinity are errors.

API#

let MAX_DIGITS = 38; stdlib/decimal/decimal.sl:40#

The most significant digits a Decimal holds, and the largest scale.

let MAX_SCALE = 38; stdlib/decimal/decimal.sl:41#
struct Decimal stdlib/decimal/decimal.sl:45#

A decimal number. Its fields are the representation, not an API: make one with parse, from_int, from_units or from_float.

fn zero() -> Decimal stdlib/decimal/decimal.sl:72#

0, at scale 0.

fn from_int(n: int) -> Decimal stdlib/decimal/decimal.sl:77#

n, at scale 0. Every int fits.

fn from_units(units: int, scale: int) -> opt[Decimal] stdlib/decimal/decimal.sl:83#

units * 10^-scale: from_units(1999, 2) is 19.99. none if scale is outside 0 to 38.

fn parse(s: str) -> result[Decimal, str] stdlib/decimal/decimal.sl:108#

The decimal written in s: an optional sign, digits, an optional fraction after a point, and an optional exponent ("1.5e3", "2E-4"). Strict, like to_int: no spaces, no thousands separators, no leading or trailing point, no NaN or infinity. The scale is the number of fraction digits less the exponent ("1.50" has scale 2, "1.5e3" scale 0); trailing zeros past scale 38 are dropped when they are exact.

fn parse_bytes(b: bytes, lo: int, hi: int) -> result[Decimal, str] stdlib/decimal/decimal.sl:114#

parse over b[lo..hi], with no str made: for a number inside a request body or a wire buffer.

fn from_float(f: float, scale: int, mode: Rounding) -> opt[Decimal] stdlib/decimal/decimal.sl:214#

The decimal nearest to f, rounded to scale digits by mode; none for NaN, an infinity, a scale outside 0 to 38, or a value past 38 digits. f is read through its shortest round-trip text, so 0.1 gives exactly 0.1, not the binary fraction nearest it.

fn scale_of(d: Decimal) -> int stdlib/decimal/decimal.sl:228#

The digits after the point.

fn precision(d: Decimal) -> int stdlib/decimal/decimal.sl:233#

How many significant digits the coefficient has (0 has 1).

fn is_zero(d: Decimal) -> bool stdlib/decimal/decimal.sl:237#
fn is_neg(d: Decimal) -> bool stdlib/decimal/decimal.sl:241#
fn sign(d: Decimal) -> int stdlib/decimal/decimal.sl:246#

-1, 0 or 1.

fn format(d: Decimal) -> str stdlib/decimal/decimal.sl:257#

Plain notation, every digit of the scale kept: "-19.90", "0.005", "42".

fn to_units(d: Decimal, scale: int, mode: Rounding) -> opt[int] stdlib/decimal/decimal.sl:302#

d in units of 10^-scale, rounded by mode: to_units(19.999, 2, HalfUp) is 2000. none if scale is outside 0 to 38 or the units do not fit an int.

fn int_of(d: Decimal, mode: Rounding) -> opt[int] stdlib/decimal/decimal.sl:323#

d rounded to a whole number by mode, as an int; none past an int.

fn float_of(d: Decimal) -> float stdlib/decimal/decimal.sl:329#

The float nearest to d. Floats cannot hold most decimals exactly: use this for display or statistics, never to compute money.

fn cmp(a: Decimal, b: Decimal) -> int stdlib/decimal/decimal.sl:337#

-1, 0 or 1 as a is less than, equal to or greater than b, by value: cmp(1.5, 1.50) is 0.

fn eq(a: Decimal, b: Decimal) -> bool stdlib/decimal/decimal.sl:357#

a and b are the same number, whatever their scales.

fn lt(a: Decimal, b: Decimal) -> bool stdlib/decimal/decimal.sl:361#
fn le(a: Decimal, b: Decimal) -> bool stdlib/decimal/decimal.sl:365#
fn gt(a: Decimal, b: Decimal) -> bool stdlib/decimal/decimal.sl:369#
fn ge(a: Decimal, b: Decimal) -> bool stdlib/decimal/decimal.sl:373#
fn min(a: Decimal, b: Decimal) -> Decimal stdlib/decimal/decimal.sl:377#
fn max(a: Decimal, b: Decimal) -> Decimal stdlib/decimal/decimal.sl:384#
fn neg(d: Decimal) -> Decimal stdlib/decimal/decimal.sl:393#
fn abs(d: Decimal) -> Decimal stdlib/decimal/decimal.sl:399#
fn add(a: Decimal, b: Decimal) -> opt[Decimal] stdlib/decimal/decimal.sl:406#

a + b, exactly, at the larger of the two scales; none past 38 digits.

fn sub(a: Decimal, b: Decimal) -> opt[Decimal] stdlib/decimal/decimal.sl:422#

a - b, exactly, at the larger of the two scales; none past 38 digits.

fn mul(a: Decimal, b: Decimal) -> opt[Decimal] stdlib/decimal/decimal.sl:438#

a * b, exactly, at the sum of the scales; none if the product needs more than 38 digits or a scale past 38 (use mul_round for those).

fn mul_round(a: Decimal, b: Decimal, scale: int, mode: Rounding) -> opt[Decimal] stdlib/decimal/decimal.sl:452#

a * b rounded to scale digits by mode; none if scale is outside 0 to 38 or the rounded product needs more than 38 digits.

fn div(a: Decimal, b: Decimal, scale: int, mode: Rounding) -> result[Decimal, str] stdlib/decimal/decimal.sl:474#

a / b rounded to scale digits by mode. An error for division by zero, a scale outside 0 to 38, or a quotient past 38 digits.

fn round(d: Decimal, scale: int, mode: Rounding) -> opt[Decimal] stdlib/decimal/decimal.sl:516#

d with exactly scale digits after the point: rounded by mode when that is fewer than it has, padded with zeros when more. none if scale is outside 0 to 38 or the result needs more than 38 digits.

fn normalize(d: Decimal) -> Decimal stdlib/decimal/decimal.sl:537#

d with trailing fraction zeros removed: 1.500 becomes 1.5, 2.00 becomes 2. Use it before a Decimal becomes a map key.

fn split(d: Decimal, n: int) -> [Decimal] stdlib/decimal/decimal.sl:555#

d in n parts at d's scale that add up to d exactly, as equal as they can be: the first ones take the leftover smallest units. split(100.00, 3) is [33.34, 33.33, 33.33]. Empty when n is not positive.

fn allocate(d: Decimal, weights: [int]) -> result[[Decimal], str] stdlib/decimal/decimal.sl:568#

d divided in proportion to weights, at d's scale, adding up to d exactly: each part is rounded down and the leftover smallest units go to the parts with the largest remainders (earlier ones first on a tie). allocate(10.00, [1, 2]) is [3.33, 6.67]. An error for an empty list, a negative weight or weights summing to zero.