decimal_checked tutorial
This tutorial shows how to run an IEEE decimal calculation as one pipeline that
remembers every exceptional condition: you fix a DecimalContext, start a
DecimalChecked from a string or number, apply operations, and at the end read
the value together with the union of all flags raised on the way. The typical
use is an auditable calculation (money, measurements) where “was anything
rounded?” or “did anything overflow?” must be answered for the whole
computation, not for the last step. Arithmetic comes from
decimal; the design page gives
the algebra of flag accumulation; the API reference
lists every method.
Quick start
moon add Luna-Flow/floating@0.8.0
import {
"Luna-Flow/floating/decimal",
"Luna-Flow/floating/decimal_checked",
}
Divide 10 by 4 in decimal64 and check that the result is exact:
///|
test "quick start: an exact division" {
let ctx = @decimal.DecimalContext::decimal64()
let r = @decimal_checked.DecimalChecked::from_int(10, ctx).div(
@decimal.Decimal::from_int(4),
)
inspect(r.value().to_string(), content="2.5")
inspect(r.flags().inexact, content="false")
}
Everyday tasks
The examples list flags with this helper:
///|
fn flags(f : @decimal.DecimalFlags) -> String {
let named = [
("inexact", f.inexact),
("rounded", f.rounded),
("invalid_operation", f.invalid_operation),
("division_by_zero", f.division_by_zero),
("overflow", f.overflow),
("underflow", f.underflow),
("subnormal", f.subnormal),
("conversion_syntax", f.conversion_syntax),
("invalid_context", f.invalid_context),
]
[ for p in named if p.1 => p.0 ].join(",")
}
Audit a price calculation
Compute a price with tax, rounded to cents. raised() describes the last step
and flags() the whole pipeline:
///|
test "price with tax" {
let ctx = @decimal.DecimalContext::decimal64()
let gross = @decimal_checked.DecimalChecked::parse("19.99", ctx)
.mul(@decimal.Decimal::from_string("1.0825").unwrap())
.quantize(@decimal.Decimal::from_string("0.01").unwrap())
inspect(gross.value().to_string(), content="21.64")
inspect(flags(gross.raised()), content="inexact,rounded")
inspect(flags(gross.flags()), content="inexact,rounded")
}
The multiplication is exact (); only the quantization to cents rounds, and the pipeline records that.
Find out that an earlier step rounded
Flags of earlier steps stay in flags() even when later steps are exact:
///|
test "an early rounding is remembered" {
let ctx = @decimal.DecimalContext::new(precision=5, e_min=-99, e_max=99)
let r = @decimal_checked.DecimalChecked::parse("1.234567", ctx)
.add(@decimal.Decimal::from_int(1))
.mul(@decimal.Decimal::from_int(2))
inspect(r.value().to_string(), content="4.4692")
inspect(flags(r.raised()), content="")
inspect(flags(r.flags()), content="inexact,rounded")
}
Keep going after a division by zero
IEEE decimal arithmetic defines a result for every operation. Dividing by zero
gives an infinity and the division_by_zero flag; the pipeline continues and
the flag is kept:
///|
test "division by zero is a flagged value" {
let ctx = @decimal.DecimalContext::decimal64()
let r = @decimal_checked.DecimalChecked::from_int(1, ctx)
.div(@decimal.Decimal::zero())
.add(@decimal.Decimal::from_int(5))
inspect(r.value().to_string(), content="inf")
inspect(flags(r.flags()), content="division_by_zero")
inspect(r.is_ok(), content="true")
}
Decide at the end what the flags mean for your application, for example with
DecimalFlags::has_error, which is true when any of invalid_operation,
division_by_zero, division_impossible, division_undefined or
invalid_context is set.
Start a new audit period
clear_flags resets both the raised and the accumulated flags without
touching the value:
///|
test "clear flags between phases" {
let ctx = @decimal.DecimalContext::decimal64()
let phase1 = @decimal_checked.DecimalChecked::from_int(2, ctx).div(
@decimal.Decimal::from_int(3),
)
inspect(flags(phase1.flags()), content="inexact,rounded")
let phase2 = phase1.clear_flags().mul(@decimal.Decimal::from_int(10))
inspect(phase2.value().to_string(), content="6.666666666666667")
inspect(flags(phase2.flags()), content="")
}
Multiplying the 16-digit quotient by ten is exact, so the second phase reports no flags although the first one rounded.
Going further
Elementary functions need a bounded context
Mathematical functions (exp, ln, power, the trigonometric family, …)
follow the General Decimal Arithmetic restriction to precision and exponent
limits within . Use one of the interchange contexts or explicit
bounds; with the default unbounded range they return NaN with
invalid_context:
///|
test "a bounded context for ln" {
let good = @decimal_checked.DecimalChecked::from_int(
10,
@decimal.DecimalContext::decimal128(),
).ln()
inspect(good.value().to_string(), content="2.302585092994045684017991454684364")
let bad = @decimal_checked.DecimalChecked::from_int(
10,
@decimal.DecimalContext::new(precision=34),
).ln()
inspect(flags(bad.raised()), content="invalid_context")
}
Start from a Luna-Flow/arithmetic context
Code that is generic over Luna-Flow/arithmetic carries an
ArithmeticContext. DecimalContext::from_arithmetic_context maps its
precision, rounding direction, exponent bounds and clamp flag; the predefined
ArithmeticContext::decimal64() maps to the same context as
DecimalContext::decimal64():
///|
test "from an arithmetic context" {
let ctx = @decimal.DecimalContext::from_arithmetic_context(
@lf_arith.ArithmeticContext::decimal64(),
)
inspect(ctx == @decimal.DecimalContext::decimal64(), content="true")
let r = @decimal_checked.DecimalChecked::from_int(2, ctx).sqrt()
inspect(r.value().to_string(), content="1.414213562373095")
}
Hand the result to the contextual traits
result() returns Ok((value, flags)) or the recorded error. The
AddContextual, SqrtContextual, … implementations of Decimal in
Luna-Flow/arithmetic report diagnostics instead of flags; the six shared
conditions (inexact, rounded, overflow, underflow, subnormal,
clamped) can be carried over field by field:
///|
fn diagnostics(f : @decimal.DecimalFlags) -> @lf_arith.ArithmeticDiagnostics {
@lf_arith.ArithmeticDiagnostics::new(
inexact=f.inexact,
rounded=f.rounded,
overflow=f.overflow,
underflow=f.underflow,
subnormal=f.subnormal,
clamped=f.clamped,
)
}
///|
test "accumulated flags become arithmetic diagnostics" {
let ctx = @decimal.DecimalContext::decimal64()
match @decimal_checked.DecimalChecked::from_int(1, ctx).div(@decimal.Decimal::from_int(3)).result() {
Ok((value, f)) => {
let outcome = @lf_arith.ArithmeticOutcome::with_diagnostics(value, diagnostics(f))
inspect(outcome.diagnostics.inexact, content="true")
}
Err(e) => fail(e.message)
}
}
The design page shows why converting the accumulated flags once gives the same diagnostics as combining the per-step diagnostics.
Common pitfalls
raised()is not the whole story. It describes the latest step only; audit withflags().- Exceptional results are not errors. NaN, infinities and rounded values
are successes with flags.
is_err()is true only for certification failures of elementary functions. - Unbounded contexts disable the mathematical functions. See above; this
also applies to contexts built from an
ArithmeticContextwithoute_min/e_max. - Binary sources.
from_double(0.1, …)converts the binary value of0.1(via 17 digits), not one tenth; parse the string"0.1"instead. - Operands are plain
Decimalvalues. They are not rounded to the context before the operation; the operation rounds the result. clear_flagsalso works after an error, but the error stays.
Next steps
decimal_checkeddesign: the state model, the flag monoid and the short-circuit rule.decimal_checkedAPI: every method.decimaltutorial: contexts, rounding modes and interchange formats.decimal_gda_checkedtutorial: the same idea for General Decimal Arithmetic with sticky status and traps.