--- title: "Contexts and Signals" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Contexts and Signals} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") # Non-trapped signals warn by default (see "Reporting signals" below). Silence # them here so the flag examples stay focused; the warning is shown on its own. options(decimal.report_flags = FALSE) ``` When you add two decimals, how many digits should the result keep? What happens when it doesn't fit — round it, or error? And which way should it round? In decimal arithmetic, these are not hard-coded answers: they come from the **context**, an explicit, session-wide setting that governs every operation. This vignette shows you how to inspect the context, change it (safely!), and work with the *signals* that operations raise along the way — conditions like `inexact`, `division_by_zero`, and `overflow`. If you haven't yet, start with `vignette("decimal-values")`: values themselves are immutable. Parsing and promotion to a finer shared scale are exact; an explicit coarser scale is a quantization request. The context shapes arithmetic and quantization, so changing it never changes values already created. ```{r setup} library(decimal) ``` ## The default context You can look at the active context at any time: ```{r} get_decimal_context() ``` The defaults are sensible for most work: * **precision 28** — results keep up to 28 significant digits; * **round half even** — banker's rounding, the same default as Python; * **`Emin = -999999` and `Emax = 999999`** — generous exponent limits; * **traps on `invalid_operation`, `division_by_zero`, and `overflow`** — the conditions most likely to mean a genuine bug are errors, while routine rounding is not. ## Changing the context, temporarily You'll rarely want to change the context for a whole session. Instead, scope the change, in the same spirit as `withr::with_*()` and `withr::local_*()`. `with_decimal_context()` applies a context to a single expression, then restores the previous one: ```{r} ctx <- decimal_context(precision = 2L, traps = character()) with_decimal_context(ctx, decimal("1.25") + decimal("0")) get_decimal_context() ``` `local_decimal_context()` applies a context until the calling function returns, which is handy when several statements need it: ```{r} f <- function() { local_decimal_context(decimal_context(precision = 2L, traps = character())) decimal("1.234") + decimal("0") } f() get_decimal_context() ``` Either way, the previous context comes back automatically when the scope ends — even if an error interrupts it. ## Signals and sticky flags Decimal operations *signal* noteworthy conditions: a result was rounded, a division hit zero, a value overflowed. What happens next depends on the context, and each signal gets one of three dispositions: * **Trapped** — the operation raises a classed R error. * **Reported** — the signal is recorded as a flag *and* surfaced as a warning. This is the default for any signal you haven't trapped. * **Silent** — the signal is recorded as a flag only. The flags are *sticky*: once raised, they stay raised until you clear them, so you can run a whole computation and check afterwards what happened along the way: ```{r} ctx <- decimal_context(precision = 2L, traps = character()) with_decimal_context(ctx, { clear_decimal_flags() decimal("1.25") + decimal("0") decimal("1") / decimal("8") decimal_flags() }) ``` Both operations had to round to fit two significant digits, raising `inexact` and `rounded`; the flags simply stay raised across the whole block. Use `clear_decimal_flags()` to start a fresh slate before a computation you want to audit. ## Reporting signals By default, any signal that isn't trapped is also *reported* — surfaced as a warning as it occurs, so precision loss never passes silently: ```{r} withr::with_options( list(decimal.report_flags = TRUE), with_decimal_context( decimal_context(precision = 2L, traps = character()), decimal("1.25") + decimal("0") ) ) ``` The warning is purely informational; the sticky flags accumulate either way. If you'd rather check flags on your own schedule, set `options(decimal.report_flags = FALSE)` and inspect `decimal_flags()` directly — that's what this vignette does behind the scenes to keep the output focused. A few operations are exempt from the warning, because for them `inexact`/`rounded` is the *requested* outcome, not a surprise: `quantize()` (and `round()` and `signif()`, which build on it) exist precisely to drop digits you named, and `sqrt()`, `exp()`, `log()`, and `log10()` are irrational for nearly every input. Division is *not* exempt — it's only sometimes inexact, so the warning there is genuinely informative: ```{r} withr::with_options( list(decimal.report_flags = TRUE), with_decimal_context(decimal_context(precision = 10L, traps = character()), { sqrt(decimal("2")) # inexact, but silent -- expected of sqrt() decimal("1") / decimal("3") # inexact, and warns -- not every division is }) ) ``` Exempted operations still update the sticky flags as usual; only the warning is suppressed. ## Traps: turning signals into errors A trapped signal stops the computation with a classed R error you can handle with `tryCatch()`: ```{r, error = TRUE} with_decimal_context(decimal_context(traps = "division_by_zero"), { decimal("1") / decimal("0") }) ``` The default traps (`division_by_zero`, `invalid_operation`, `overflow`) catch the conditions that usually indicate a bug. `invalid_operation` groups the standard invalid subconditions, including undefined division such as `0 / 0`. You can go stricter: for example, in a context where *any* loss of precision should be an error — reconciling ledgers, say — trap `inexact` too: ```{r, error = TRUE} strict <- decimal_context( traps = c("division_by_zero", "invalid_operation", "overflow", "inexact") ) with_decimal_context(strict, decimal("1") / decimal("3")) ``` ## Classifying values The context even shapes how values are *classified*. A finite, nonzero value is **subnormal** when its exponent falls below the context's `emin`, meaning fewer significant digits are available to it than `precision` allows; otherwise it's **normal**. So classification depends on the active context, not on the value alone — below, `1E-3` is subnormal only because `emin = -2L` puts it out of the normal range: ```{r} ctx <- decimal_context(precision = 3L, emin = -2L) with_decimal_context(ctx, { x <- decimal(c("1E-2", "1E-3", "-0", "NaN", "sNaN")) number_class(x) }) ``` For quick checks there's a family of predicates: ```{r} x <- decimal(c("NaN", "sNaN", "Infinity", "-0", "1E-3")) is_qnan(x) is_snan(x) is.infinite(x) is_zero(x) is_signed(x) ``` `is_signed()` reports the sign bit directly, including on zero — it's how you tell `-0` apart from `0`, since `sign()` treats both as `0`.