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; the rescale to the caller's `SCALE` is always
21//! **downward**, never upward, so half-to-even rounding always lands
22//! on the **correctly-rounded** value at the target scale:
23//!
24//! | Tier           | Reference storage | `SCALE_REF` (= reference digits) | Source file       |
25//! |----------------|-------------------|----------------------------------|-------------------|
26//! | D9 / D18 / D38 | `Int256`          | 75                               | this file         |
27//! | D76            | `Int256`          | 75                               | `consts_wide.rs`  |
28//! | D153           | `Int512`          | 153                              | `consts_wide.rs`  |
29//! | D307           | `Int1024`         | 307                              | `consts_wide.rs`  |
30//!
31//! The rescale from `SCALE_REF` to the caller's `SCALE` uses integer
32//! division with the crate-default [`RoundingMode`] (half-to-even by
33//! default; overridable via the `rounding-*` Cargo features). Going
34//! through `f64` would cap precision at ~15–17 decimal digits; the
35//! raw-integer path preserves the full per-tier reference width.
36//!
37//! **0.5 ULP at every supported scale**, on every width, with no
38//! exceptions in the precision contract. The only constraint is the
39//! width's *storage range*: a value that mathematically exceeds the
40//! type's `Storage::MAX / 10^SCALE` cannot be represented at all. At
41//! `D38<38>` the storage range is approximately ±1.7, so the four
42//! larger-magnitude constants — `pi ≈ 3.14`, `tau ≈ 6.28`, `e ≈ 2.72`,
43//! `golden ≈ 1.62` — overflow `i128` and the corresponding methods
44//! panic with a clear "constant out of storage range" message;
45//! `half_pi ≈ 1.57` and `quarter_pi ≈ 0.79` fit and remain
46//! correctly-rounded to 0.5 ULP.
47//!
48//! [`RoundingMode`]: crate::rounding::RoundingMode
49//!
50//! # Sources
51//!
52//! Each raw constant is the half-to-even rounding of the canonical
53//! decimal expansion to the tier's `SCALE_REF` fractional digits. ISO
54//! 80000-2 (pi, tau, pi/2, pi/4), OEIS A001113 (e), OEIS A001622
55//! (golden ratio).
56
57use crate::core_type::D38;
58use crate::d_w128_kernels::Fixed;
59use crate::wide_int::Int256;
60
61/// Reference scale for every constant in this file: the 75-digit
62/// representation that fits an `Int256` (`2 · 128` bits). Every D38
63/// scale (0..=38) is at most 38 digits, so we always rescale **down**
64/// from 75 → SCALE, never up. The half-to-even rescale-down step is
65/// performed by [`Fixed::round_to_i128`] (`Fixed` is the same 256-bit
66/// guard-digit type the strict transcendentals use), giving 0.5 ULP at
67/// the caller's `SCALE` for every value that fits `i128` at that
68/// scale.
69///
70/// # Precision
71///
72/// N/A: constant value, no arithmetic performed.
73const SCALE_REF: u32 = 75;
74
75// Raw decimal strings at 75 fractional digits, materialised at build
76// time by `build.rs` (the same hand-rolled multi-precision generator
77// that emits the wide-tier constants). Sources: ISO 80000-2 (pi, tau,
78// pi/2, pi/4), OEIS A001113 (e), OEIS A001622 (golden ratio).
79//
80// The build-time string -> Int256 parse is `const fn` (via
81// `Int256::from_str_radix`, base 10 only). The 75-digit reference is
82// the largest decimal expansion that always fits Int256 for the
83// biggest of these constants (tau ≈ 6.28×10⁷⁵ < Int256::MAX ≈
84// 5.78×10⁷⁶); a single shared SCALE_REF keeps the rescale helpers
85// uniform across all six methods on the trait.
86
87include!(concat!(env!("OUT_DIR"), "/wide_consts.rs"));
88
89const PI_RAW: Int256 = match Int256::from_str_radix(PI_D76_S75, 10) {
90    Ok(v) => v,
91    Err(_) => panic!("consts: PI_D76_S75 not parseable"),
92};
93const TAU_RAW: Int256 = match Int256::from_str_radix(TAU_D76_S75, 10) {
94    Ok(v) => v,
95    Err(_) => panic!("consts: TAU_D76_S75 not parseable"),
96};
97const HALF_PI_RAW: Int256 = match Int256::from_str_radix(HALF_PI_D76_S75, 10) {
98    Ok(v) => v,
99    Err(_) => panic!("consts: HALF_PI_D76_S75 not parseable"),
100};
101const QUARTER_PI_RAW: Int256 = match Int256::from_str_radix(QUARTER_PI_D76_S75, 10) {
102    Ok(v) => v,
103    Err(_) => panic!("consts: QUARTER_PI_D76_S75 not parseable"),
104};
105const E_RAW: Int256 = match Int256::from_str_radix(E_D76_S75, 10) {
106    Ok(v) => v,
107    Err(_) => panic!("consts: E_D76_S75 not parseable"),
108};
109const GOLDEN_RAW: Int256 = match Int256::from_str_radix(GOLDEN_D76_S75, 10) {
110    Ok(v) => v,
111    Err(_) => panic!("consts: GOLDEN_D76_S75 not parseable"),
112};
113
114/// Rescale a 75-digit `Int256` reference down to the caller's `TARGET`
115/// scale as an `i128`, half-to-even. Panics if the value at `TARGET`
116/// does not fit `i128` (the type's storage range at that scale just
117/// doesn't include this constant — e.g. `pi ≈ 3.14` at `D38<38>` would
118/// need `3.14 × 10^38 ≈ 3.14e38`, which exceeds `i128::MAX ≈ 1.7e38`).
119fn rescale_75_to_target<const TARGET: u32>(raw: Int256, name: &'static str) -> i128 {
120    let limbs = raw.0;  // [u128; 2], little-endian
121    let f = Fixed { negative: false, mag: limbs };
122    match f.round_to_i128(SCALE_REF, TARGET) {
123        Some(v) => v,
124        None => panic!(
125            "D38 constant out of storage range: {name} cannot fit i128 at SCALE = {TARGET} \
126             (storage range is ±i128::MAX / 10^SCALE)",
127            name = name,
128            TARGET = TARGET,
129        ),
130    }
131}
132
133/// Well-known mathematical constants available on every decimal width
134/// (`D9` / `D18` / `D38` / `D76` / `D153` / `D307`).
135///
136/// Import this trait to call `D38s12::pi()`, `D76::<35>::e()`, etc.
137///
138/// All returned values are computed from a raw integer reference at
139/// the tier's maximum storage precision (75 digits for D9/D18/D38 and
140/// D76; 153 for D153; 307 for D307) without passing through `f64`,
141/// then rescaled down to the caller's `SCALE` with half-to-even
142/// rounding. The result is **within 0.5 ULP** of the canonical
143/// decimal expansion at every supported scale on every width.
144///
145/// The one situation where a method does not return a value is when
146/// the constant's magnitude exceeds the type's storage range at the
147/// caller's `SCALE` — e.g. `D38<38>::pi()` would need `3.14 × 10³⁸`,
148/// which exceeds `i128::MAX ≈ 1.7×10³⁸`. The method panics with a
149/// clear "constant out of storage range" message in that case.
150pub trait DecimalConsts: Sized {
151    /// Pi (~3.14159265...). One half-turn in radians.
152    ///
153    /// Source: ISO 80000-2 / OEIS A000796. Rescaled per-tier (see the
154    /// module-level table) to the caller's `SCALE` via the crate-default
155    /// rounding mode.
156    ///
157    /// # Precision
158    ///
159    /// N/A: constant value, no arithmetic performed.
160    fn pi() -> Self;
161
162    /// Tau (~6.28318530...). One full turn in radians.
163    ///
164    /// Defined as `2 * pi`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
165    ///
166    /// # Precision
167    ///
168    /// N/A: constant value, no arithmetic performed.
169    fn tau() -> Self;
170
171    /// Half-pi (~1.57079632...). One quarter-turn in radians.
172    ///
173    /// Defined as `pi / 2`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
174    ///
175    /// # Precision
176    ///
177    /// N/A: constant value, no arithmetic performed.
178    fn half_pi() -> Self;
179
180    /// Quarter-pi (~0.78539816...). One eighth-turn in radians.
181    ///
182    /// Defined as `pi / 4`. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
183    ///
184    /// # Precision
185    ///
186    /// N/A: constant value, no arithmetic performed.
187    fn quarter_pi() -> Self;
188
189    /// The golden ratio (~1.61803398...). Dimensionless.
190    ///
191    /// Defined as `(1 + sqrt(5)) / 2`. Source: OEIS A001622. Rescaled
192    /// per-tier (see the module-level table) to the caller's `SCALE`
193    /// via the crate-default rounding mode.
194    ///
195    /// # Precision
196    ///
197    /// N/A: constant value, no arithmetic performed.
198    fn golden() -> Self;
199
200    /// Euler's number (~2.71828182...). Dimensionless.
201    ///
202    /// Source: OEIS A001113. Rescaled per-tier (see the module-level table) to the caller's `SCALE` via the crate-default rounding mode.
203    ///
204    /// # Precision
205    ///
206    /// N/A: constant value, no arithmetic performed.
207    fn e() -> Self;
208}
209
210// Public-to-crate helpers that return each constant's rescaled bits at
211// the caller's target SCALE. Used by the `decl_decimal_consts!` macro
212// to provide DecimalConsts for narrower widths (D9, D18) without
213// duplicating the rescale logic.
214
215pub(crate) fn pi_at_target<const TARGET: u32>() -> i128 {
216    rescale_75_to_target::<TARGET>(PI_RAW, "pi")
217}
218pub(crate) fn tau_at_target<const TARGET: u32>() -> i128 {
219    rescale_75_to_target::<TARGET>(TAU_RAW, "tau")
220}
221pub(crate) fn half_pi_at_target<const TARGET: u32>() -> i128 {
222    rescale_75_to_target::<TARGET>(HALF_PI_RAW, "half_pi")
223}
224pub(crate) fn quarter_pi_at_target<const TARGET: u32>() -> i128 {
225    rescale_75_to_target::<TARGET>(QUARTER_PI_RAW, "quarter_pi")
226}
227pub(crate) fn golden_at_target<const TARGET: u32>() -> i128 {
228    rescale_75_to_target::<TARGET>(GOLDEN_RAW, "golden")
229}
230pub(crate) fn e_at_target<const TARGET: u32>() -> i128 {
231    rescale_75_to_target::<TARGET>(E_RAW, "e")
232}
233
234// The `DecimalConsts` impl for `D38<SCALE>` is emitted by the
235// `decl_decimal_consts!` macro — the same macro D9 / D18 / D76+ use.
236// It expands to `Self(pi_at_target::<SCALE>())` etc.; each
237// `*_at_target` helper above rescales the 75-digit Int256 reference
238// down to the caller's `SCALE` via half-to-even and narrows to i128
239// (or panics with a clear message if the constant's magnitude
240// exceeds the storage range at that scale).
241crate::macros::consts::decl_decimal_consts!(D38, i128);
242
243// Inherent associated constants: EPSILON / MIN_POSITIVE.
244//
245// These mirror `f64::EPSILON` and `f64::MIN_POSITIVE` so that generic
246// numeric code that calls `T::EPSILON` or `T::MIN_POSITIVE` compiles
247// when `T = D38<SCALE>`. For D38 both equal `D38(1)` -- the smallest
248// representable positive value (1 LSB = 10^-SCALE). There are no subnormals.
249
250impl<const SCALE: u32> D38<SCALE> {
251    /// Smallest representable positive value: 1 LSB = `10^-SCALE`.
252    ///
253    /// Provided as an analogue to `f64::EPSILON` for generic numeric code.
254    /// Note that this differs from the f64 definition ("difference between
255    /// 1.0 and the next-larger f64"): for `D38` the LSB is uniform across
256    /// the entire representable range.
257    ///
258    /// # Precision
259    ///
260    /// N/A: constant value, no arithmetic performed.
261    pub const EPSILON: Self = Self(1);
262
263    /// Smallest positive value (equal to [`Self::EPSILON`]).
264    ///
265    /// Provided as an analogue to `f64::MIN_POSITIVE` for generic numeric
266    /// code. Unlike `f64`, `D38` has no subnormals, so `MIN_POSITIVE`
267    /// and `EPSILON` are the same value.
268    ///
269    /// # Precision
270    ///
271    /// N/A: constant value, no arithmetic performed.
272    pub const MIN_POSITIVE: Self = Self(1);
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::core_type::D38s12;
279
280    // Bit-exact assertions at SCALE = 12.
281    //
282    // At SCALE = 12 each constant is the 37-digit raw integer divided by
283    // 10^23, rounded half-to-even.
284
285    /// pi at SCALE=12: raw / 10^23.
286    /// Truncated 13 digits: 3_141_592_653_589.
287    /// 14th digit is 7 (from position 14 of the raw) -> round up.
288    /// Expected: 3_141_592_653_590.
289    #[test]
290    fn pi_is_bit_exact_at_scale_12() {
291        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
292        assert_eq!(D38s12::pi().to_bits(), 3_141_592_653_590_i128);
293    }
294
295    /// tau at SCALE=12: raw / 10^23.
296    /// Truncated 13 digits: 6_283_185_307_179.
297    /// 14th digit is 5 -> round up. Expected: 6_283_185_307_180.
298    #[test]
299    fn tau_is_bit_exact_at_scale_12() {
300        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
301        assert_eq!(D38s12::tau().to_bits(), 6_283_185_307_180_i128);
302    }
303
304    /// half_pi at SCALE=12: raw / 10^23.
305    /// Truncated 13 digits: 1_570_796_326_794.
306    /// 14th digit is 8 -> round up. Expected: 1_570_796_326_795.
307    #[test]
308    fn half_pi_is_bit_exact_at_scale_12() {
309        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
310        assert_eq!(D38s12::half_pi().to_bits(), 1_570_796_326_795_i128);
311    }
312
313    /// quarter_pi at SCALE=12: raw / 10^23.
314    /// Truncated 12 digits: 785_398_163_397.
315    /// 13th digit is 4 -> no round-up. Expected: 785_398_163_397.
316    #[test]
317    fn quarter_pi_is_bit_exact_at_scale_12() {
318        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
319        assert_eq!(D38s12::quarter_pi().to_bits(), 785_398_163_397_i128);
320    }
321
322    /// e at SCALE=12: raw / 10^23.
323    /// Truncated 13 digits: 2_718_281_828_459.
324    /// 14th digit is 0 -> no round-up. Expected: 2_718_281_828_459.
325    #[test]
326    fn e_is_bit_exact_at_scale_12() {
327        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
328        assert_eq!(D38s12::e().to_bits(), 2_718_281_828_459_i128);
329    }
330
331    /// golden at SCALE=12: raw / 10^23.
332    /// Truncated 13 digits: 1_618_033_988_749.
333    /// 14th digit is 8 -> round up. Expected: 1_618_033_988_750.
334    #[test]
335    fn golden_is_bit_exact_at_scale_12() {
336        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
337        assert_eq!(D38s12::golden().to_bits(), 1_618_033_988_750_i128);
338    }
339
340    // Closeness checks against core::f64::consts.
341    // These verify that the correct reference digits were selected; the
342    // bit-exact tests above are the primary acceptance criteria.
343
344    /// pi() converted to f64 is within 1e-11 of `core::f64::consts::PI`.
345    /// At SCALE=12, 1 LSB = 1e-12, so 1e-11 covers rescale rounding plus
346    /// the f64 conversion step.
347    #[test]
348    fn pi_close_to_f64_pi() {
349        let diff = (D38s12::pi().to_f64() - core::f64::consts::PI).abs();
350        assert!(diff < 1e-11, "pi diverges from f64 PI by {diff}");
351    }
352
353    #[test]
354    fn tau_close_to_f64_tau() {
355        let diff = (D38s12::tau().to_f64() - core::f64::consts::TAU).abs();
356        assert!(diff < 1e-11, "tau diverges from f64 TAU by {diff}");
357    }
358
359    #[test]
360    fn half_pi_close_to_f64_frac_pi_2() {
361        let diff =
362            (D38s12::half_pi().to_f64() - core::f64::consts::FRAC_PI_2).abs();
363        assert!(diff < 1e-11, "half_pi diverges from f64 FRAC_PI_2 by {diff}");
364    }
365
366    #[test]
367    fn quarter_pi_close_to_f64_frac_pi_4() {
368        let diff =
369            (D38s12::quarter_pi().to_f64() - core::f64::consts::FRAC_PI_4).abs();
370        assert!(
371            diff < 1e-11,
372            "quarter_pi diverges from f64 FRAC_PI_4 by {diff}"
373        );
374    }
375
376    #[test]
377    fn e_close_to_f64_e() {
378        let diff = (D38s12::e().to_f64() - core::f64::consts::E).abs();
379        assert!(diff < 1e-11, "e diverges from f64 E by {diff}");
380    }
381
382    /// golden() converted to f64 is within 1e-11 of the closed form
383    /// `(1 + sqrt(5)) / 2`. Requires std for `f64::sqrt`.
384    #[cfg(feature = "std")]
385    #[test]
386    fn golden_close_to_closed_form() {
387        let expected = (1.0_f64 + 5.0_f64.sqrt()) / 2.0;
388        let diff = (D38s12::golden().to_f64() - expected).abs();
389        assert!(diff < 1e-11, "golden diverges from closed-form by {diff}");
390    }
391
392    // EPSILON / MIN_POSITIVE
393
394    #[test]
395    fn epsilon_is_one_ulp() {
396        assert_eq!(D38s12::EPSILON.to_bits(), 1_i128);
397        assert!(D38s12::EPSILON > D38s12::ZERO);
398    }
399
400    #[test]
401    fn min_positive_is_one_ulp() {
402        assert_eq!(D38s12::MIN_POSITIVE.to_bits(), 1_i128);
403        assert_eq!(D38s12::MIN_POSITIVE, D38s12::EPSILON);
404    }
405
406    /// At SCALE = 6 the LSB is 10^-6; EPSILON is still raw 1.
407    #[test]
408    fn epsilon_at_scale_6_is_one_ulp() {
409        type D6 = D38<6>;
410        assert_eq!(D6::EPSILON.to_bits(), 1_i128);
411        assert_eq!(D6::MIN_POSITIVE.to_bits(), 1_i128);
412    }
413
414    // Cross-scale exercises
415
416    /// At SCALE = 6, pi() should equal 3.141593 (rounded half-to-even from
417    /// 3.1415926535...). Expected raw bits: 3_141_593.
418    #[test]
419    fn pi_at_scale_6_is_bit_exact() {
420        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
421        type D6 = D38<6>;
422        assert_eq!(D6::pi().to_bits(), 3_141_593_i128);
423    }
424
425    /// At SCALE = 0, pi() rounds to 3 (first fractional digit is 1, no
426    /// round-up).
427    #[test]
428    fn pi_at_scale_0_is_three() {
429        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
430        type D0 = D38<0>;
431        assert_eq!(D0::pi().to_bits(), 3_i128);
432    }
433
434    /// `D38<37>::pi()` is the canonical pi rounded half-to-even to 37
435    /// fractional digits. The 75-digit Int256 reference is rescaled
436    /// down to 37 digits; the result is bit-identical to the
437    /// hand-tabulated constant.
438    #[test]
439    fn pi_at_scale_37_matches_canonical_37_digit_rounding() {
440        type D37 = D38<37>;
441        // pi to 38 digits: 3.14159265358979323846264338327950288420
442        //                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
443        //                   keep 37 frac digits; the 38th digit is 0
444        //                   so half-to-even rounds down — no bump.
445        let expected: i128 = 31_415_926_535_897_932_384_626_433_832_795_028_842;
446        assert_eq!(D37::pi().to_bits(), expected);
447    }
448
449    /// `D38<38>` cannot represent pi (storage range is ~±1.7 at
450    /// SCALE=38, pi is 3.14). The method panics with a clear
451    /// "constant out of storage range" message rather than silently
452    /// returning a fudged value.
453    #[test]
454    #[should_panic(expected = "out of storage range")]
455    fn pi_at_scale_38_panics_storage_range() {
456        let _ = D38::<38>::pi();
457    }
458
459    /// `D38<38>` storage range covers ±1.7, which DOES include
460    /// `half_pi ≈ 1.57` and `quarter_pi ≈ 0.79`. They must be
461    /// correctly rounded to 0.5 ULP (= 1 LSB).
462    #[test]
463    fn half_pi_and_quarter_pi_at_scale_38_are_correctly_rounded() {
464        // half_pi to 38 digits: 1.57079632679489661923132169163975144210
465        let expected_half_pi: i128 = 157_079_632_679_489_661_923_132_169_163_975_144_210;
466        let got = D38::<38>::half_pi().to_bits();
467        let diff = (got - expected_half_pi).abs();
468        assert!(diff <= 1, "half_pi: got {got}, expected {expected_half_pi}, diff {diff} > 1 LSB");
469
470        // quarter_pi to 38 digits: 0.78539816339744830961566084581987572105
471        let expected_quarter_pi: i128 = 78_539_816_339_744_830_961_566_084_581_987_572_105;
472        let got = D38::<38>::quarter_pi().to_bits();
473        let diff = (got - expected_quarter_pi).abs();
474        assert!(diff <= 1, "quarter_pi: got {got}, expected {expected_quarter_pi}, diff {diff} > 1 LSB");
475    }
476
477    /// Negative-side rounding: negating pi gives the expected raw bits.
478    #[test]
479    fn neg_pi_round_trip() {
480        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
481        let pi = D38s12::pi();
482        let neg_pi = -pi;
483        assert_eq!(neg_pi.to_bits(), -3_141_592_653_590_i128);
484    }
485
486    // (`rescale_from_ref` boundary tests removed: the rounding logic now
487    // lives in `D38::rescale` / `src/rounding.rs::apply_rounding` and is
488    // covered by the tests in those modules.)
489}