Skip to main content

decimal_scaled/
consts.rs

1//! Mathematical constants and float-compatibility constants for every
2//! decimal width.
3//!
4//! # Constants provided
5//!
6//! The [`DecimalConsts`] trait exposes `pi`, `tau`, `half_pi`,
7//! `quarter_pi`, `golden`, and `e` as methods on every width. The
8//! native-tier (`D38` and narrower) impls live here; the wide tier
9//! (`D76` / `D153` / `D307`) impls live in `consts_wide.rs`.
10//!
11//! Two inherent associated constants, `EPSILON` and `MIN_POSITIVE`, are
12//! provided as analogues to `f64::EPSILON` and `f64::MIN_POSITIVE` so
13//! that generic code parameterised over numeric types continues to
14//! compile when `T` is any of the decimal widths.
15//!
16//! # Precision strategy
17//!
18//! Constants are derived from raw integer references — no `f64`
19//! anywhere. Each tier has its own reference at the tier's maximum
20//! storage precision:
21//!
22//! | Tier           | Storage of reference | `SCALE_REF` (= reference digits) | Source file       |
23//! |----------------|---------------------|----------------------------------|-------------------|
24//! | D9 / D18 / D38 | `i128`              | 37                               | this file         |
25//! | D76            | `Int256`            | 75                               | `consts_wide.rs`  |
26//! | D153           | `Int512`            | 153                              | `consts_wide.rs`  |
27//! | D307           | `Int1024`           | 307                              | `consts_wide.rs`  |
28//!
29//! The rescale from `SCALE_REF` to the caller's `SCALE` uses integer
30//! division with the crate-default [`RoundingMode`] (half-to-even by
31//! default; overridable via the `rounding-*` Cargo features). Going
32//! through `f64` would cap precision at ~15–17 decimal digits; the
33//! raw-integer path preserves the full per-tier reference width.
34//!
35//! At `SCALE ≤ SCALE_REF` (every supported scale on D9 / D18 / D76 /
36//! D153 / D307, and every D38 scale up to 37) the result is within
37//! **0.5 ULP** of the canonical decimal expansion. The single
38//! exception is `D38<38>`: the largest D38 reference fits 37 fractional
39//! digits in `i128` (`tau ≈ 6.28×10³⁷` is below `i128::MAX ≈
40//! 1.7×10³⁸`), so `SCALE = 38` (the D38 maximum) is rescaled
41//! upward — multiplying the 37-digit reference by 10 — which appends
42//! a placeholder zero rather than adding precision. The error there
43//! is bounded at ≈ 5 ULP for `pi` / `tau` / `e` / `golden`; `half_pi`
44//! and `quarter_pi` (smaller in magnitude) remain inside 0.5 ULP.
45//!
46//! [`RoundingMode`]: crate::rounding::RoundingMode
47//!
48//! # Sources
49//!
50//! Each raw constant is the half-to-even rounding of the canonical
51//! decimal expansion to the tier's `SCALE_REF` fractional digits. ISO
52//! 80000-2 (pi, tau, pi/2, pi/4), OEIS A001113 (e), OEIS A001622
53//! (golden ratio).
54
55use crate::core_type::D38;
56
57/// Reference scale for the high-precision raw constants below.
58///
59/// Every constant fits in `i128` at this scale; the largest
60/// (tau ≈ 6.28×10³⁷) is below `i128::MAX ≈ 1.7×10³⁸`. Caller scales
61/// above this value rescale up by `10^(SCALE - SCALE_REF)`, which
62/// appends placeholder zeros without adding precision — `SCALE = 38`
63/// loses up to ≈ 5 ULP this way. Caller scales at or below
64/// `SCALE_REF` rescale down via the crate-default [`RoundingMode`],
65/// preserving the 0.5 ULP contract.
66///
67/// # Precision
68///
69/// N/A: constant value, no arithmetic performed.
70///
71/// [`RoundingMode`]: crate::rounding::RoundingMode
72const SCALE_REF: u32 = 37;
73
74// Raw i128 constants at SCALE_REF = 37, materialised at build time
75// by `build.rs` (the same hand-rolled multi-precision generator that
76// emits the wide-tier constants). Sources: ISO 80000-2 (pi, tau,
77// pi/2, pi/4), OEIS A001113 (e), OEIS A001622 (golden ratio).
78//
79// The build-time string -> i128 parse is `const fn` (Rust 1.83+).
80
81include!(concat!(env!("OUT_DIR"), "/wide_consts.rs"));
82
83const PI_RAW_S37: i128 = match i128::from_str_radix(PI_D38_S37, 10) {
84    Ok(v) => v,
85    Err(_) => panic!("consts: PI_D38_S37 not parseable"),
86};
87const TAU_RAW_S37: i128 = match i128::from_str_radix(TAU_D38_S37, 10) {
88    Ok(v) => v,
89    Err(_) => panic!("consts: TAU_D38_S37 not parseable"),
90};
91const HALF_PI_RAW_S37: i128 = match i128::from_str_radix(HALF_PI_D38_S37, 10) {
92    Ok(v) => v,
93    Err(_) => panic!("consts: HALF_PI_D38_S37 not parseable"),
94};
95const QUARTER_PI_RAW_S37: i128 = match i128::from_str_radix(QUARTER_PI_D38_S37, 10) {
96    Ok(v) => v,
97    Err(_) => panic!("consts: QUARTER_PI_D38_S37 not parseable"),
98};
99const E_RAW_S37: i128 = match i128::from_str_radix(E_D38_S37, 10) {
100    Ok(v) => v,
101    Err(_) => panic!("consts: E_D38_S37 not parseable"),
102};
103const GOLDEN_RAW_S37: i128 = match i128::from_str_radix(GOLDEN_D38_S37, 10) {
104    Ok(v) => v,
105    Err(_) => panic!("consts: GOLDEN_D38_S37 not parseable"),
106};
107
108// Rescaling from SCALE_REF to the caller's SCALE is delegated to
109// `D38::rescale` (which uses round-half-to-even by default; see
110// `src/rescale.rs`). The constants below construct a `D38<SCALE_REF>`
111// from the raw integer literal and then rescale to the caller's
112// `D38<SCALE>`.
113
114/// Well-known mathematical constants available on every decimal width
115/// (`D9` / `D18` / `D38` / `D76` / `D153` / `D307`).
116///
117/// Import this trait to call `D38s12::pi()`, `D76::<35>::e()`, etc.
118///
119/// All returned values are computed from a raw integer reference at
120/// each tier's maximum storage precision (37 digits for D9/D18/D38, 75
121/// for D76, 153 for D153, 307 for D307) without passing through `f64`.
122/// The result is within 0.5 ULP of the canonical decimal expansion at
123/// the target `SCALE` for every supported scale, with one exception:
124/// `D38<38>` (the D38 maximum) rescales the 37-digit reference upward
125/// by 10, appending a placeholder zero rather than adding precision;
126/// the error there is bounded at ≈ 5 ULP for the larger-magnitude
127/// constants. See the module-level docs for the per-tier table.
128pub trait DecimalConsts: Sized {
129    /// Pi (~3.14159265...). One half-turn in radians.
130    ///
131    /// Source: ISO 80000-2 / OEIS A000796. Rescaled per-tier (see the
132    /// module-level table) to the caller's `SCALE` via the crate-default
133    /// rounding mode.
134    ///
135    /// # Precision
136    ///
137    /// N/A: constant value, no arithmetic performed.
138    fn pi() -> Self;
139
140    /// Tau (~6.28318530...). One full turn in radians.
141    ///
142    /// Defined as `2 * pi`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
143    ///
144    /// # Precision
145    ///
146    /// N/A: constant value, no arithmetic performed.
147    fn tau() -> Self;
148
149    /// Half-pi (~1.57079632...). One quarter-turn in radians.
150    ///
151    /// Defined as `pi / 2`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
152    ///
153    /// # Precision
154    ///
155    /// N/A: constant value, no arithmetic performed.
156    fn half_pi() -> Self;
157
158    /// Quarter-pi (~0.78539816...). One eighth-turn in radians.
159    ///
160    /// Defined as `pi / 4`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
161    ///
162    /// # Precision
163    ///
164    /// N/A: constant value, no arithmetic performed.
165    fn quarter_pi() -> Self;
166
167    /// The golden ratio (~1.61803398...). Dimensionless.
168    ///
169    /// Defined as `(1 + sqrt(5)) / 2`. Source: OEIS A001622. Rescaled
170    /// per-tier (see the module-level table) to the caller's `SCALE`
171    /// via the crate-default rounding mode.
172    ///
173    /// # Precision
174    ///
175    /// N/A: constant value, no arithmetic performed.
176    fn golden() -> Self;
177
178    /// Euler's number (~2.71828182...). Dimensionless.
179    ///
180    /// Source: OEIS A001113. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
181    ///
182    /// # Precision
183    ///
184    /// N/A: constant value, no arithmetic performed.
185    fn e() -> Self;
186}
187
188// Public-to-crate helpers that return each constant's rescaled bits at
189// the caller's target SCALE. Used by the `decl_decimal_consts!` macro
190// to provide DecimalConsts for narrower widths (D9, D18) without
191// duplicating the rescale logic.
192
193pub(crate) fn pi_at_target<const TARGET: u32>() -> i128 {
194    D38::<SCALE_REF>::from_bits(PI_RAW_S37).rescale::<TARGET>().to_bits()
195}
196pub(crate) fn tau_at_target<const TARGET: u32>() -> i128 {
197    D38::<SCALE_REF>::from_bits(TAU_RAW_S37).rescale::<TARGET>().to_bits()
198}
199pub(crate) fn half_pi_at_target<const TARGET: u32>() -> i128 {
200    D38::<SCALE_REF>::from_bits(HALF_PI_RAW_S37).rescale::<TARGET>().to_bits()
201}
202pub(crate) fn quarter_pi_at_target<const TARGET: u32>() -> i128 {
203    D38::<SCALE_REF>::from_bits(QUARTER_PI_RAW_S37).rescale::<TARGET>().to_bits()
204}
205pub(crate) fn golden_at_target<const TARGET: u32>() -> i128 {
206    D38::<SCALE_REF>::from_bits(GOLDEN_RAW_S37).rescale::<TARGET>().to_bits()
207}
208pub(crate) fn e_at_target<const TARGET: u32>() -> i128 {
209    D38::<SCALE_REF>::from_bits(E_RAW_S37).rescale::<TARGET>().to_bits()
210}
211
212// The `DecimalConsts` impl for `D38<SCALE>` is emitted by the
213// `decl_decimal_consts!` macro — the same macro D9 / D18 / D76+ use.
214// It expands to `Self(pi_at_target::<SCALE>())` etc., which is
215// identical to the previous hand-coded
216// `D38::<SCALE_REF>::from_bits(PI_RAW_S37).rescale::<SCALE>()` because
217// `pi_at_target` is defined as exactly that, then `.to_bits()`.
218crate::macros::consts::decl_decimal_consts!(D38, i128);
219
220// Inherent associated constants: EPSILON / MIN_POSITIVE.
221//
222// These mirror `f64::EPSILON` and `f64::MIN_POSITIVE` so that generic
223// numeric code that calls `T::EPSILON` or `T::MIN_POSITIVE` compiles
224// when `T = D38<SCALE>`. For D38 both equal `D38(1)` -- the smallest
225// representable positive value (1 LSB = 10^-SCALE). There are no subnormals.
226
227impl<const SCALE: u32> D38<SCALE> {
228    /// Smallest representable positive value: 1 LSB = `10^-SCALE`.
229    ///
230    /// Provided as an analogue to `f64::EPSILON` for generic numeric code.
231    /// Note that this differs from the f64 definition ("difference between
232    /// 1.0 and the next-larger f64"): for `D38` the LSB is uniform across
233    /// the entire representable range.
234    ///
235    /// # Precision
236    ///
237    /// N/A: constant value, no arithmetic performed.
238    pub const EPSILON: Self = Self(1);
239
240    /// Smallest positive value (equal to [`Self::EPSILON`]).
241    ///
242    /// Provided as an analogue to `f64::MIN_POSITIVE` for generic numeric
243    /// code. Unlike `f64`, `D38` has no subnormals, so `MIN_POSITIVE`
244    /// and `EPSILON` are the same value.
245    ///
246    /// # Precision
247    ///
248    /// N/A: constant value, no arithmetic performed.
249    pub const MIN_POSITIVE: Self = Self(1);
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::core_type::D38s12;
256
257    // Bit-exact assertions at SCALE = 12.
258    //
259    // At SCALE = 12 each constant is the 37-digit raw integer divided by
260    // 10^23, rounded half-to-even.
261
262    /// pi at SCALE=12: raw / 10^23.
263    /// Truncated 13 digits: 3_141_592_653_589.
264    /// 14th digit is 7 (from position 14 of the raw) -> round up.
265    /// Expected: 3_141_592_653_590.
266    #[test]
267    fn pi_is_bit_exact_at_scale_12() {
268        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
269        assert_eq!(D38s12::pi().to_bits(), 3_141_592_653_590_i128);
270    }
271
272    /// tau at SCALE=12: raw / 10^23.
273    /// Truncated 13 digits: 6_283_185_307_179.
274    /// 14th digit is 5 -> round up. Expected: 6_283_185_307_180.
275    #[test]
276    fn tau_is_bit_exact_at_scale_12() {
277        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
278        assert_eq!(D38s12::tau().to_bits(), 6_283_185_307_180_i128);
279    }
280
281    /// half_pi at SCALE=12: raw / 10^23.
282    /// Truncated 13 digits: 1_570_796_326_794.
283    /// 14th digit is 8 -> round up. Expected: 1_570_796_326_795.
284    #[test]
285    fn half_pi_is_bit_exact_at_scale_12() {
286        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
287        assert_eq!(D38s12::half_pi().to_bits(), 1_570_796_326_795_i128);
288    }
289
290    /// quarter_pi at SCALE=12: raw / 10^23.
291    /// Truncated 12 digits: 785_398_163_397.
292    /// 13th digit is 4 -> no round-up. Expected: 785_398_163_397.
293    #[test]
294    fn quarter_pi_is_bit_exact_at_scale_12() {
295        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
296        assert_eq!(D38s12::quarter_pi().to_bits(), 785_398_163_397_i128);
297    }
298
299    /// e at SCALE=12: raw / 10^23.
300    /// Truncated 13 digits: 2_718_281_828_459.
301    /// 14th digit is 0 -> no round-up. Expected: 2_718_281_828_459.
302    #[test]
303    fn e_is_bit_exact_at_scale_12() {
304        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
305        assert_eq!(D38s12::e().to_bits(), 2_718_281_828_459_i128);
306    }
307
308    /// golden at SCALE=12: raw / 10^23.
309    /// Truncated 13 digits: 1_618_033_988_749.
310    /// 14th digit is 8 -> round up. Expected: 1_618_033_988_750.
311    #[test]
312    fn golden_is_bit_exact_at_scale_12() {
313        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
314        assert_eq!(D38s12::golden().to_bits(), 1_618_033_988_750_i128);
315    }
316
317    // Closeness checks against core::f64::consts.
318    // These verify that the correct reference digits were selected; the
319    // bit-exact tests above are the primary acceptance criteria.
320
321    /// pi() converted to f64 is within 1e-11 of `core::f64::consts::PI`.
322    /// At SCALE=12, 1 LSB = 1e-12, so 1e-11 covers rescale rounding plus
323    /// the f64 conversion step.
324    #[test]
325    fn pi_close_to_f64_pi() {
326        let diff = (D38s12::pi().to_f64() - core::f64::consts::PI).abs();
327        assert!(diff < 1e-11, "pi diverges from f64 PI by {diff}");
328    }
329
330    #[test]
331    fn tau_close_to_f64_tau() {
332        let diff = (D38s12::tau().to_f64() - core::f64::consts::TAU).abs();
333        assert!(diff < 1e-11, "tau diverges from f64 TAU by {diff}");
334    }
335
336    #[test]
337    fn half_pi_close_to_f64_frac_pi_2() {
338        let diff =
339            (D38s12::half_pi().to_f64() - core::f64::consts::FRAC_PI_2).abs();
340        assert!(diff < 1e-11, "half_pi diverges from f64 FRAC_PI_2 by {diff}");
341    }
342
343    #[test]
344    fn quarter_pi_close_to_f64_frac_pi_4() {
345        let diff =
346            (D38s12::quarter_pi().to_f64() - core::f64::consts::FRAC_PI_4).abs();
347        assert!(
348            diff < 1e-11,
349            "quarter_pi diverges from f64 FRAC_PI_4 by {diff}"
350        );
351    }
352
353    #[test]
354    fn e_close_to_f64_e() {
355        let diff = (D38s12::e().to_f64() - core::f64::consts::E).abs();
356        assert!(diff < 1e-11, "e diverges from f64 E by {diff}");
357    }
358
359    /// golden() converted to f64 is within 1e-11 of the closed form
360    /// `(1 + sqrt(5)) / 2`. Requires std for `f64::sqrt`.
361    #[cfg(feature = "std")]
362    #[test]
363    fn golden_close_to_closed_form() {
364        let expected = (1.0_f64 + 5.0_f64.sqrt()) / 2.0;
365        let diff = (D38s12::golden().to_f64() - expected).abs();
366        assert!(diff < 1e-11, "golden diverges from closed-form by {diff}");
367    }
368
369    // EPSILON / MIN_POSITIVE
370
371    #[test]
372    fn epsilon_is_one_ulp() {
373        assert_eq!(D38s12::EPSILON.to_bits(), 1_i128);
374        assert!(D38s12::EPSILON > D38s12::ZERO);
375    }
376
377    #[test]
378    fn min_positive_is_one_ulp() {
379        assert_eq!(D38s12::MIN_POSITIVE.to_bits(), 1_i128);
380        assert_eq!(D38s12::MIN_POSITIVE, D38s12::EPSILON);
381    }
382
383    /// At SCALE = 6 the LSB is 10^-6; EPSILON is still raw 1.
384    #[test]
385    fn epsilon_at_scale_6_is_one_ulp() {
386        type D6 = D38<6>;
387        assert_eq!(D6::EPSILON.to_bits(), 1_i128);
388        assert_eq!(D6::MIN_POSITIVE.to_bits(), 1_i128);
389    }
390
391    // Cross-scale exercises
392
393    /// At SCALE = 6, pi() should equal 3.141593 (rounded half-to-even from
394    /// 3.1415926535...). Expected raw bits: 3_141_593.
395    #[test]
396    fn pi_at_scale_6_is_bit_exact() {
397        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
398        type D6 = D38<6>;
399        assert_eq!(D6::pi().to_bits(), 3_141_593_i128);
400    }
401
402    /// At SCALE = 0, pi() rounds to 3 (first fractional digit is 1, no
403    /// round-up).
404    #[test]
405    fn pi_at_scale_0_is_three() {
406        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
407        type D0 = D38<0>;
408        assert_eq!(D0::pi().to_bits(), 3_i128);
409    }
410
411    /// At SCALE = SCALE_REF (37), pi() returns exactly the raw constant.
412    #[test]
413    fn pi_at_scale_ref_is_raw_constant() {
414        type D37 = D38<37>;
415        assert_eq!(D37::pi().to_bits(), PI_RAW_S37);
416    }
417
418    /// At SCALE = SCALE_REF + 1 (38), pi() multiplies by 10, appending
419    /// one trailing zero digit. PI_RAW_S37 * 10 ≈ 3.14×10³⁸ which is
420    /// larger than i128::MAX ≈ 1.7×10³⁸, so this case overflows
421    /// `D38<38>` storage at compile time — exercising the upper end
422    /// of the rescale-up path is left to the SCALE = 37 case above.
423    #[test]
424    fn pi_at_scale_37_is_raw_constant() {
425        type D37 = D38<37>;
426        assert_eq!(D37::pi().to_bits(), PI_RAW_S37);
427    }
428
429    /// Negative-side rounding: negating pi gives the expected raw bits.
430    #[test]
431    fn neg_pi_round_trip() {
432        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
433        let pi = D38s12::pi();
434        let neg_pi = -pi;
435        assert_eq!(neg_pi.to_bits(), -3_141_592_653_590_i128);
436    }
437
438    // (`rescale_from_ref` boundary tests removed: the rounding logic now
439    // lives in `D38::rescale` / `src/rounding.rs::apply_rounding` and is
440    // covered by the tests in those modules.)
441}