decimal_checked 教程

本教程展示如何把一次 IEEE 十进制计算作为一条记住每个异常情形的流水线来运行:你确定一个 DecimalContext,从字符串或数开始一个 DecimalChecked,依次应用运算,最后读取值以及途中引发的所有标志的并集。典型用途是可审计的计算(金额、测量值),其中“是否有任何舍入?”或“是否有任何上溢?”必须针对整个计算而不是最后一步来回答。算术来自 decimal;设计页面给出了标志累积的代数;API 参考列出了所有方法。

快速入门

moon add Luna-Flow/floating@0.8.0
import {
  "Luna-Flow/floating/decimal",
  "Luna-Flow/floating/decimal_checked",
}

在 decimal64 下计算 10 除以 4,并检查结果是否精确:

///|
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")
}

日常任务

示例使用如下辅助函数列出标志:

///|
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(",")
}

审计价格计算

计算含税价格并舍入到分。raised() 描述最后一步,flags() 描述整条流水线:

///|
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")
}

乘法是精确的(19.99×1.0825=21.63917519.99 \times 1.0825 = 21.639175);只有量化到分时发生了舍入,流水线记录了这一点。

发现先前某一步发生了舍入

即使后续步骤是精确的,先前步骤的标志也会保留在 flags() 中:

///|
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")
}

除以零之后继续执行

IEEE 十进制算术为每个运算都定义了结果。除以零得到无穷和 division_by_zero 标志;流水线继续执行,标志被保留:

///|
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")
}

最后再决定这些标志对你的应用意味着什么,例如使用 DecimalFlags::has_error,只要 invalid_operation、division_by_zero、division_impossible、division_undefined 或 invalid_context 中任一被设置,它就为真。

开始新的审计周期

clear_flags 同时重置最近引发的标志和累积的标志,而不改动值:

///|
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="")
}

把 16 位的商乘以十是精确的,因此尽管第一阶段发生了舍入,第二阶段也不报告任何标志。

深入了解

初等函数需要有界上下文

数学函数(exp、ln、power、三角函数族……)遵循通用十进制算术的限制,精度和指数界须在 ±999 999\pm 999\,999 以内。请使用某个交换格式上下文或显式的界;在默认的无界范围下,它们会返回带 invalid_context 的 NaN:

///|
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")
}

从 Luna-Flow/arithmetic 上下文开始

基于 Luna-Flow/arithmetic 的泛型代码携带一个 ArithmeticContext。DecimalContext::from_arithmetic_context 映射其精度、舍入方向、指数界和 clamp 标志;预定义的 ArithmeticContext::decimal64() 映射到与 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")
}

把结果交给 contextual trait

result() 返回 Ok((value, flags)) 或记录下的错误。Decimal 在 Luna-Flow/arithmetic 中的 AddContextual、SqrtContextual……实现报告的是诊断信息而不是标志;六种共享的情形(inexact、rounded、overflow、underflow、subnormal、clamped)可以逐字段转移过去:

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

设计页面说明了为什么一次性转换累积的标志与合并逐步的诊断信息结果相同。

常见陷阱

  • raised() 并不是全部。 它只描述最近一步;审计请用 flags()。
  • 异常结果不是错误。 NaN、无穷和舍入后的值都是带标志的成功结果。is_err() 只在初等函数认证失败时为真。
  • 无界上下文会禁用数学函数。 见上文;这同样适用于由不带 e_min / e_max 的 ArithmeticContext 构造的上下文。
  • 二进制来源。 from_double(0.1, …) 转换的是 0.1 的二进制值(经由 17 位数字),而不是十分之一;请改为解析字符串 "0.1"。
  • 操作数是普通的 Decimal 值。 它们在运算之前不会按上下文舍入;运算会对结果舍入。
  • clear_flags 在出错之后也能使用, 但错误仍然保留。

后续步骤