oxinum_float/precision.rs
1//! Precision-control helpers for the `DBig` wrapper.
2//!
3//! These free functions wrap `dashu-float` precision primitives in an
4//! ergonomic, allocation-friendly API:
5//!
6//! * [`with_precision`] — rebind a value to a different working precision.
7//! * [`epsilon`] — the smallest positive *unit* representable at a given
8//! decimal precision, i.e. `10^(1 - precision)` at the requested
9//! precision.
10//! * [`ulp`] — the unit in the last place of a specific value at the
11//! precision currently carried by its context.
12//!
13//! All three operate on the decimal big-float (`DBig = FBig<HalfAway, 10>`)
14//! so callers do not have to spell out the underlying generic parameters.
15//!
16//! # `precision == 0` (unlimited)
17//!
18//! `dashu-float` reserves a `precision` of `0` to mean *unlimited*.
19//! `ulp` is not meaningful in that regime and the underlying
20//! `dashu_float::FBig::ulp` panics in that case. We surface that as an
21//! [`OxiNumError::Precision`] instead — see [`ulp`] for details.
22
23use std::str::FromStr;
24
25use crate::{DBig, OxiNumError, OxiNumResult};
26
27/// Return `x` rebound to the requested *decimal* precision.
28///
29/// This delegates to [`dashu_float::FBig::with_precision`], which returns
30/// an [`dashu_base::Approximation`] indicating whether the rebind was
31/// exact (the new precision was at least the old digit count) or
32/// inexact (digits were rounded off). This helper discards that
33/// distinction and returns the rounded value directly. Use the raw
34/// `dashu-float` API if you need the inexact-flag.
35///
36/// `precision == 0` is *unlimited* precision in `dashu-float`'s
37/// vocabulary, which is also passed through unchanged.
38///
39/// # Examples
40///
41/// ```
42/// use std::str::FromStr;
43/// use oxinum_float::{precision::with_precision, DBig};
44///
45/// let a = DBig::from_str("1.234").expect("parse 1.234");
46/// let a100 = with_precision(&a, 100);
47/// assert_eq!(a100.precision(), 100);
48/// ```
49#[must_use]
50pub fn with_precision(x: &DBig, precision: usize) -> DBig {
51 // `with_precision` consumes `self`, so we clone the borrowed input.
52 // `.value()` discards the inexact-flag (see module docs).
53 x.clone().with_precision(precision).value()
54}
55
56/// Return the smallest positive "unit" representable at the requested
57/// decimal precision, i.e. `10^(1 - precision)` rounded to `precision`
58/// significant digits.
59///
60/// In decimal base, a value with precision `p` has its least significant
61/// digit at position `10^(1 - p)` when the leading digit is `1` —
62/// this is the "epsilon" in the floating-point sense.
63///
64/// `precision == 0` is *unlimited* precision in `dashu-float`'s
65/// vocabulary; in that case there is no meaningful machine epsilon, so
66/// this helper returns an error.
67///
68/// # Errors
69///
70/// Returns [`OxiNumError::Precision`] if `precision == 0`.
71///
72/// # Examples
73///
74/// ```
75/// use std::str::FromStr;
76/// use oxinum_float::{precision::epsilon, DBig};
77///
78/// let eps10 = epsilon(10).expect("precision 10");
79/// // 10^(1 - 10) = 10^-9, at precision 10.
80/// assert_eq!(eps10.precision(), 10);
81///
82/// let expected = DBig::from_str("1e-9")
83/// .expect("parse 1e-9")
84/// .with_precision(10)
85/// .value();
86/// assert_eq!(eps10, expected);
87/// ```
88pub fn epsilon(precision: usize) -> OxiNumResult<DBig> {
89 if precision == 0 {
90 return Err(OxiNumError::Precision(
91 "epsilon is undefined for unlimited precision (precision = 0)".into(),
92 ));
93 }
94 // Build "1e-(precision-1)" as the canonical decimal epsilon, then
95 // rebind to the requested precision so callers can rely on
96 // `epsilon(p).precision() == p`.
97 let raw = format!("1e-{}", precision - 1);
98 let parsed = DBig::from_str(&raw)
99 .map_err(|e| OxiNumError::Parse(format!("epsilon('{raw}'): {e}").into()))?;
100 Ok(parsed.with_precision(precision).value())
101}
102
103/// Return the unit in the last place of `x` at its current precision.
104///
105/// This is `dashu_float::FBig::ulp` re-exported as a free function for
106/// API symmetry with [`epsilon`] and [`with_precision`]. The result is
107/// a positive value whose least significant decimal position matches
108/// `x` (i.e. `ulp(x)` is the smallest positive `d` such that `x + d`
109/// is the next representable neighbour of `x`).
110///
111/// # Errors
112///
113/// Returns [`OxiNumError::Precision`] if `x` carries unlimited
114/// precision (`x.precision() == 0`). In that case `ulp` is
115/// undefined — `dashu_float::FBig::ulp` would itself panic, which we
116/// surface as a recoverable error here.
117///
118/// # Examples
119///
120/// ```
121/// use std::str::FromStr;
122/// use oxinum_float::{precision::ulp, DBig};
123///
124/// let x = DBig::from_str("1.23").expect("parse 1.23");
125/// let u = ulp(&x).expect("finite precision");
126/// assert_eq!(u, DBig::from_str("0.01").expect("parse 0.01"));
127/// // ulp does not change the carried precision.
128/// assert_eq!(u.precision(), x.precision());
129/// ```
130pub fn ulp(x: &DBig) -> OxiNumResult<DBig> {
131 if x.precision() == 0 {
132 return Err(OxiNumError::Precision(
133 "ulp is undefined for values with unlimited precision (precision = 0)".into(),
134 ));
135 }
136 Ok(x.ulp())
137}