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 桁の商に 10 を掛けるのは厳密なので、第 1 段階では丸めが起きていても、第 2 段階はフラグを報告しません。

さらに進んで

初等関数には有界なコンテキストが必要

数学関数(exp、ln、power、三角関数系、…)は、精度と指数の限界を ±999 999\pm 999\,999 以内とする General Decimal Arithmetic の制約に従います。交換形式のコンテキストのいずれかか、明示的な境界を使ってください。デフォルトの非有界な範囲では、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 トレイトに渡す

result() は Ok((value, flags)) か、記録されたエラーを返します。Luna-Flow/arithmetic における Decimal の 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 はエラーの後でも動作するが、エラーは残ります。

次のステップ