decimal_gda 教程
本教程教你使用 decimal_gda 进行计算。该包实现了 Cowlishaw 的通用十进制算术(GDA):保留尾随零的十进制数,确定精度、舍入和指数界限的上下文,记录所发生情况的粘滞状态,以及在终止计算的同时仍把确定结果交给你的陷阱。学完之后,你将能够在计算中传递上下文、对金额舍入、处理陷阱、上溢和下溢、调用初等函数,并选择合适的比较方式。每条规则背后的数学见设计页面;所有公开名称都在 API 页面上。
快速入门
添加模块并导入该包:
moon add Luna-Flow/floating
import {
"Luna-Flow/floating/decimal_gda",
}
每个 GDA 运算都接收操作数和一个 GdaContext,并返回一个 GdaOutcome:结果、下一步要使用的上下文,以及该运算引发的情形。
///|
test "quick start: one third in nine digits" {
let ctx = @decimal_gda.context(precision=9)
let one = @decimal_gda.parse("1", ctx)
let three = @decimal_gda.Decimal::from_string("3").unwrap()
let third = @decimal_gda.divide(one.value(), three, one.next_context())
inspect(third.value(), content="0.333333333")
inspect(third.raised().contains(Inexact), content="true")
inspect(third.next_context().status().contains(Rounded), content="true")
}
raised() 描述的是这一个运算。next_context().status() 是粘滞的:它收集自上下文创建或清除以来的所有情形。
日常任务
在计算中传递上下文
只有把每个结果的 next_context() 传给下一个运算,状态才会累积。你最初使用的上下文永远不会改变,因为上下文是一个值,而不是可变对象。
///|
test "sticky status follows the threaded context" {
let start = @decimal_gda.context(precision=5)
let two = @decimal_gda.Decimal::from_string("2").unwrap()
let three = @decimal_gda.Decimal::from_string("3").unwrap()
let q = @decimal_gda.divide(two, three, start) // inexact
let tiny = @decimal_gda.Decimal::from_string("0.00001").unwrap()
let s = @decimal_gda.add(q.value(), tiny, q.next_context()) // exact
inspect(s.value(), content="0.66668")
inspect(s.raised().contains(Inexact), content="false")
inspect(s.next_context().status().contains(Inexact), content="true")
inspect(start.status().contains(Inexact), content="false")
// Start a new observation window and keep the trap settings.
let fresh = s.next_context().clear_status()
inspect(fresh.status() == @decimal_gda.GdaFlags::none(), content="true")
}
保留并设定量子
GDA 数由系数和指数组成,因此 2.50 和 2.5 是相等的数,但指数不同(量子不同)。算术保留精确结果所要求的指数,而 quantize 显式地设定指数。这就是把金额舍入到分的方法:
///|
test "round to cents with quantize" {
let even = @decimal_gda.GdaContext::decimal64() // HalfEven
let half_up = @decimal_gda.context(precision=16, rounding=HalfUp)
let price = @decimal_gda.Decimal::from_string("2.50").unwrap()
let qty = @decimal_gda.Decimal::from_string("3").unwrap()
inspect(@decimal_gda.multiply(price, qty, even).value(), content="7.50")
let cents = @decimal_gda.Decimal::from_string("0.01").unwrap()
let x = @decimal_gda.Decimal::from_string("2.345").unwrap()
let banker = @decimal_gda.quantize(x, cents, even)
let school = @decimal_gda.quantize(x, cents, half_up)
inspect(banker.value(), content="2.34")
inspect(school.value(), content="2.35")
inspect(banker.raised().contains(Inexact), content="true")
// A quantum that needs more digits than the precision is invalid.
let tiny = @decimal_gda.Decimal::from_string("1E-20").unwrap()
let bad = @decimal_gda.quantize(x, tiny, even)
inspect(bad.value(), content="nan")
inspect(bad.raised().contains(InvalidOperation), content="true")
}
reduce 的作用正相反:它去除尾随零,因此 reduce(7.50) 是 7.5。只有在确实需要同值类中最短的成员时才使用它。
处理陷阱而不丢失结果
用 trap 派生出一个新上下文即可启用陷阱。被陷阱捕获的运算返回 Trapped(signal, value, next_context, raised):GDA 定义的结果仍然在其中,粘滞状态也照样更新。
///|
test "a trapped division keeps its defined result" {
let ctx = @decimal_gda.GdaContext::decimal64().trap(DivisionByZero)
let one = @decimal_gda.Decimal::one()
let zero = @decimal_gda.Decimal::zero()
match @decimal_gda.divide(one, zero, ctx) {
@decimal_gda.GdaOutcome::Trapped(signal, value, next, raised) => {
inspect(signal == DivisionByZero, content="true")
inspect(value, content="inf")
inspect(raised.contains(DivisionByZero), content="true")
inspect(next.status().contains(DivisionByZero), content="true")
}
@decimal_gda.GdaOutcome::Completed(_, _, _) => fail("expected a trap")
}
}
InvalidOperation 陷阱也会捕获四种细分的无效情形(ConversionSyntax、DivisionImpossible、DivisionUndefined、InvalidContext)。basic 上下文启用了它,因此格式错误的字面量会触发陷阱:
///|
test "the InvalidOperation trap covers conversion syntax" {
let basic = @decimal_gda.GdaContext::basic()
let out = @decimal_gda.parse("1.2.3", basic)
match out {
@decimal_gda.GdaOutcome::Trapped(signal, value, _, raised) => {
inspect(signal == InvalidOperation, content="true")
inspect(value, content="nan")
inspect(raised.conversion_syntax, content="true")
}
@decimal_gda.GdaOutcome::Completed(_, _, _) => fail("expected a trap")
}
}
当一个运算引发多个被陷阱捕获的情形时,本包按固定的优先级(列于 API 页面)报告其中之一,因此你无需自行规定顺序。
遵守指数范围
上下文把调整后的指数限制在 [e_min, e_max] 之内。超出 e_max 时结果上溢;上溢成什么取决于舍入模式。低于 e_min 时结果成为次正规数并丢失数字:
///|
test "overflow and underflow in decimal32" {
let d32 = @decimal_gda.GdaContext::decimal32() // p=7, e_max=96, HalfEven
let down = @decimal_gda.context(
precision=7,
rounding=Down,
e_min=-95,
e_max=96,
clamp=true,
)
let big = @decimal_gda.Decimal::from_string("9E+96").unwrap()
let ten = @decimal_gda.Decimal::from_string("10").unwrap()
inspect(@decimal_gda.multiply(big, ten, d32).value(), content="inf")
inspect(@decimal_gda.multiply(big, ten, down).value(), content="9.999999E+96")
let small = @decimal_gda.Decimal::from_string("1.234567E-95").unwrap()
let thousand = @decimal_gda.Decimal::from_string("1000").unwrap()
let sub = @decimal_gda.divide(small, thousand, d32)
inspect(sub.value(), content="1.235E-98")
inspect(sub.raised().contains(Subnormal), content="true")
inspect(sub.raised().contains(Underflow), content="true")
}
调用初等函数
sqrt、exp、ln 和 log10 是正确舍入的,并且无论上下文的舍入模式如何,总是采用半偶舍入。非整数指数的 power 同样是正确舍入的,但使用上下文自身的舍入模式。按照 GDA 规范的要求,exp、ln、log10 和非整数 power 只接受精度、e_max 和 -e_min 都不超过 999,999 的上下文;decimal32/64/128 预设满足这一条件,而 GdaContext::new 和 context 的默认值(指数范围 ±999,999,999)不满足:
///|
test "elementary functions" {
let ctx = @decimal_gda.GdaContext::decimal64()
let one = @decimal_gda.Decimal::one()
let two = @decimal_gda.Decimal::from_string("2").unwrap()
let ten = @decimal_gda.Decimal::from_string("10").unwrap()
inspect(@decimal_gda.exp(one, ctx).value(), content="2.718281828459045")
inspect(@decimal_gda.ln(ten, ctx).value(), content="2.302585092994046")
inspect(@decimal_gda.log10(two, ctx).value(), content="0.3010299956639812")
inspect(@decimal_gda.sqrt(two, ctx).value(), content="1.414213562373095")
// exp, ln, log10 and non-integer power need |e_min|, e_max <= 999999.
let floor3 = @decimal_gda.context(
precision=3,
rounding=Floor,
e_min=-999_999,
e_max=999_999,
)
let three_halves = @decimal_gda.Decimal::from_string("1.5").unwrap()
inspect(@decimal_gda.exp(one, floor3).value(), content="2.72") // still half-even
inspect(@decimal_gda.power(two, three_halves, floor3).value(), content="2.82") // floor
}
比较与打印
compare 按数值比较并返回一个十进制数(-1、0、1,或在某个操作数为 NaN 时返回 NaN)。compare_total 对表示进行排序,因此能区分 2.50 和 2.5。打印一个值会得到其科学记数法字符串;class_name 给出其类别名称:
///|
test "comparisons and text" {
let ctx = @decimal_gda.GdaContext::decimal64()
let a = @decimal_gda.Decimal::from_string("2.50").unwrap()
let b = @decimal_gda.Decimal::from_string("2.5").unwrap()
inspect(@decimal_gda.compare(a, b, ctx).value(), content="0")
inspect(@decimal_gda.compare_total(a, b, ctx).value(), content="-1")
let nan = @decimal_gda.Decimal::nan()
inspect(@decimal_gda.compare(a, nan, ctx).value(), content="nan")
inspect(
@decimal_gda.compare_signal(a, nan, ctx).raised().contains(InvalidOperation),
content="true",
)
let big = @decimal_gda.Decimal::from_string("123E+5").unwrap()
inspect(big, content="1.23E+7")
inspect(@decimal_gda.class_name(big, ctx).value(), content="+Normal")
let (eng, _) = @decimal_gda.Decimal::to_eng_string(
"123E+5",
@decimal_gda.DecimalContext::new(),
)
inspect(eng, content="12.3E+6")
}
深入了解
用 decimal_gda_checked 构建长链
手动传递上下文在边界处最为清晰。对于较长的线性流水线,decimal_gda_checked 只保留一个结果,替你传递粘滞上下文,并在第一个陷阱之后停止:
///|
test "a checked GDA pipeline" {
let ctx = @decimal_gda.GdaContext::decimal64()
let checked = @decimal_gda_checked.GdaDecimalChecked::parse("2", ctx)
.sqrt()
.multiply(@decimal_gda.Decimal::from_string("10").unwrap())
inspect(checked.value(), content="14.14213562373095")
inspect(checked.is_trapped(), content="false")
inspect(checked.status().contains(Inexact), content="true")
}
通过 Luna-Flow/arithmetic 编写泛型代码
Decimal 实现了 Luna-Flow/arithmetic 的 contextual trait,因此针对 AddContextual、DivContextual、SqrtContextual 等编写的代码可以在其上运行。这些适配器使用 ArithmeticContext 的五种 IEEE 风格舍入模式,以 Err 报告错误,并且对陷阱一无所知:
///|
fn[T : @lf_arith.AddContextual] sum3(
a : T,
b : T,
c : T,
ctx : @lf_arith.ArithmeticContext,
) -> Result[T, @lf_arith.ArithmeticError] {
match @lf_arith.AddContextual::add_contextual(a, b, ctx) {
Ok(ab) =>
@lf_arith.AddContextual::add_contextual(ab.value, c, ctx).map(o => o.value)
Err(error) => Err(error)
}
}
///|
test "generic contextual addition" {
let ctx = @lf_arith.ArithmeticContext::new(3)
let x = @decimal_gda.Decimal::from_string("1.25").unwrap()
inspect(sum3(x, x, x, ctx).unwrap(), content="3.75")
}
子集算术与丢失的数字
GdaContext::basic() 是 GDA 的基本默认上下文:精度 9、HalfUp、extended=false,并启用 DivisionByZero、InvalidOperation、Overflow、Underflow 和 Clamped 陷阱。非扩展上下文会在使用之前对长于精度的操作数进行舍入,并在因此丢失信息时报告 LostDigits:
///|
test "subset arithmetic rounds long operands" {
let basic = @decimal_gda.GdaContext::basic()
let long = @decimal_gda.Decimal::from_string("1234567891").unwrap()
let zero = @decimal_gda.Decimal::zero()
let out = @decimal_gda.add(long, zero, basic)
inspect(out.value(), content="1.23456789E+9")
inspect(out.raised().contains(LostDigits), content="true")
}
交换编码
GdaInterchange 以密集压缩十进制(DPD)编码保存 decimal32、decimal64 或 decimal128 位模式,写作 # 加十六进制数字:
///|
test "decimal64 interchange round trip" {
let x = @decimal_gda.Decimal::from_string("-7.50").unwrap()
let (bits, _) = @decimal_gda.GdaInterchange::from_decimal(x, Decimal64)
inspect(bits.to_hex(), content="#A2300000000003D0")
inspect(bits.to_decimal(), content="-7.50")
}
常见陷阱
- 重复使用原始上下文。 只有把
next_context()传递下去,状态才会累积。你在某个上下文上设置的陷阱集合或状态,也会随从它派生的每个上下文一起传递。 - 把
Trapped当作“没有值”。 陷阱改变的是变体,而不是结果;在决定停止之前,请用value()读取值。 - 指望构造器保留零。
Decimal::from_int(100)和Decimal::make会去除尾随零,因此from_int(100)打印为1E+2。当量子重要时,请使用Decimal::from_string("100")或parse。 - 用运算符做 GDA 计算。
+、-、*和/不接收上下文:它们以半偶舍入舍入到较大的操作数精度(*是精确的),从不发出信号,并且+和/返回同值类中最短的成员。只要 GDA 的结果、标志或陷阱有意义,就请使用包中的函数。 - 相等性与 NaN。
==和compare把每个 NaN 视为与其他所有 NaN 相等,并大于每个数(这是一个全预序,因此排序从不中止)。当 NaN 必须保持无序时,请使用compare(包中的函数)、compare_signal或Decimal::compare_checked。 - 混用两个十进制包。
@decimal_gda.Decimal和@decimal.Decimal是具有不同契约的不同类型;在它们之间转换请通过字符串或交换位模式。 - 指望
exp、ln、log10或sqrt遵循上下文的舍入。 它们总是采用半偶舍入;只有power遵循上下文。 - 用默认上下文调用
exp、ln、log10或非整数power。GdaContext::new()和context()允许指数达到 ±999,999,999,超出了这些函数有定义的范围,因此它们返回 NaN 并引发InvalidContext(一种InvalidOperation)。请使用预设,或传入e_min=-999_999, e_max=999_999。
后续步骤
- 设计:算术模型、理想指数、舍入函数、信号与陷阱状态机,以及初等函数如何认证。
- API 参考:每个类型、函数和方法。
- 符合性和性能:固定版本测试套件的结果以及如何测量速度。
decimal_gda_checked教程:流水线中的陷阱短路与恢复。decimal教程:带逐运算标志的 IEEE 754 十进制模型。