Skip to main content

decimal_scaled/
consts.rs

1//! Mathematical constants and float-compatibility constants for [`I128`].
2//!
3//! # Constants provided
4//!
5//! The [`DecimalConsts`] trait exposes `pi`, `tau`, `half_pi`,
6//! `quarter_pi`, `golden`, and `e` as methods on `I128<SCALE>`.
7//!
8//! Two inherent associated constants, `EPSILON` and `MIN_POSITIVE`, are
9//! provided as analogues to `f64::EPSILON` and `f64::MIN_POSITIVE` so
10//! that generic code parameterised over numeric types continues to compile
11//! when `T = I128<SCALE>`.
12//!
13//! # Precision strategy
14//!
15//! All constant values are derived from a 35-digit reference stored as a
16//! raw `i128` at `SCALE_REF = 35`. They do not pass through `f64` at any
17//! point. The rescale from `SCALE_REF` to the caller's `SCALE` uses
18//! integer division with half-away-from-zero rounding.
19//!
20//! Going through `f64` would cap precision at roughly 15-17 decimal digits
21//! (f64 mantissa width). The raw-i128 path preserves up to 35 digits, which
22//! exceeds every practical scale value.
23//!
24//! At `SCALE > SCALE_REF` (i.e. `SCALE > 35`) the constant is multiplied
25//! up from the reference, so trailing digits are zero-extended and carry no
26//! additional precision. At `SCALE = 38` the multiplication may overflow
27//! `i128` for some constants; callers that need `SCALE > 35` should verify
28//! that the result is in range.
29//!
30//! # Reference scale
31//!
32//! `SCALE_REF = 35` was chosen so that each constant fits in `i128` at
33//! that scale (the largest value, `tau ~= 6.28e35`, is well under
34//! `i128::MAX ~= 1.7e38`) while still providing more digits than any
35//! practical caller `SCALE`. Each raw constant is the half-away-from-zero
36//! rounding of the canonical decimal expansion to 35 fractional digits.
37//! Sources: ISO 80000-2 (pi, tau, pi/2, pi/4), OEIS A001113 (e),
38//! OEIS A001622 (golden ratio).
39
40use crate::core_type::I128;
41
42/// Reference scale for the high-precision raw constants below.
43///
44/// Every constant fits in `i128` at this scale; the largest (tau ~= 6.28e35)
45/// is well under `i128::MAX ~= 1.7e38`. Caller scales above this value are
46/// handled by multiplying the reference by `10^(SCALE - SCALE_REF)`.
47///
48/// # Precision
49///
50/// N/A: constant value, no arithmetic performed.
51const SCALE_REF: u32 = 35;
52
53// Raw i128 constants at SCALE_REF = 35.
54//
55// Each value is the half-away-from-zero rounding of the canonical decimal
56// expansion to 35 fractional digits. Sources: ISO 80000-2 (pi, tau, pi/2,
57// pi/4), OEIS A001113 (e), OEIS A001622 (golden = (1 + sqrt(5)) / 2).
58
59/// pi at SCALE_REF = 35.
60/// Value: 3.14159265358979323846264338327950288
61/// (36th digit was 4; no round-up.)
62///
63/// # Precision
64///
65/// N/A: constant value, no arithmetic performed.
66const PI_RAW_S35: i128 = 314_159_265_358_979_323_846_264_338_327_950_288_i128;
67
68/// tau = 2 * pi at SCALE_REF = 35.
69/// Value: 6.28318530717958647692528676655900577
70/// (36th digit was 8; rounded up from ...576 to ...577.)
71///
72/// # Precision
73///
74/// N/A: constant value, no arithmetic performed.
75const TAU_RAW_S35: i128 = 628_318_530_717_958_647_692_528_676_655_900_577_i128;
76
77/// pi / 2 at SCALE_REF = 35.
78/// Value: 1.57079632679489661923132169163975144
79/// (36th digit was 2; no round-up.)
80///
81/// # Precision
82///
83/// N/A: constant value, no arithmetic performed.
84const HALF_PI_RAW_S35: i128 = 157_079_632_679_489_661_923_132_169_163_975_144_i128;
85
86/// pi / 4 at SCALE_REF = 35.
87/// Value: 0.78539816339744830961566084581987572
88/// (36th digit was 1; no round-up.)
89///
90/// # Precision
91///
92/// N/A: constant value, no arithmetic performed.
93const QUARTER_PI_RAW_S35: i128 = 78_539_816_339_744_830_961_566_084_581_987_572_i128;
94
95/// e at SCALE_REF = 35.
96/// Value: 2.71828182845904523536028747135266250
97/// (36th digit was 7; rounded up from ...249 to ...250.)
98///
99/// # Precision
100///
101/// N/A: constant value, no arithmetic performed.
102const E_RAW_S35: i128 = 271_828_182_845_904_523_536_028_747_135_266_250_i128;
103
104/// Golden ratio = (1 + sqrt(5)) / 2 at SCALE_REF = 35.
105/// Value: 1.61803398874989484820458683436563812
106/// (36th digit was 7; rounded up from ...811 to ...812.)
107///
108/// # Precision
109///
110/// N/A: constant value, no arithmetic performed.
111const GOLDEN_RAW_S35: i128 = 161_803_398_874_989_484_820_458_683_436_563_812_i128;
112
113// Rescale helper (half-away-from-zero).
114
115/// Rescales `raw` from `SCALE_REF` to `target_scale` using half-away-from-zero
116/// rounding.
117///
118/// - Equal scales: returns `raw` unchanged.
119/// - `target_scale < SCALE_REF`: integer division with half-away-from-zero rounding.
120/// - `target_scale > SCALE_REF`: integer multiplication; panics on overflow in
121///   debug builds (only reachable at `SCALE >= 38` for large constants).
122///
123/// # Precision
124///
125/// Strict: all arithmetic is integer-only; result is bit-exact.
126#[inline]
127fn rescale_from_ref(raw: i128, target_scale: u32) -> i128 {
128    if target_scale == SCALE_REF {
129        return raw;
130    }
131    if target_scale < SCALE_REF {
132        let shift = SCALE_REF - target_scale;
133        let divisor = 10i128.pow(shift);
134        let half = divisor / 2;
135        // Add half with the same sign as `raw` before truncating-toward-zero
136        // division to achieve half-away-from-zero semantics.
137        let rounded = if raw >= 0 { raw + half } else { raw - half };
138        rounded / divisor
139    } else {
140        // Multiply up; panics in debug on overflow.
141        let shift = target_scale - SCALE_REF;
142        raw * 10i128.pow(shift)
143    }
144}
145
146/// Well-known mathematical constants available on any [`I128<SCALE>`].
147///
148/// Import this trait to call `I128s12::pi()`, `I128s12::e()`, etc.
149///
150/// All returned values are computed from a 35-digit raw-`i128` reference
151/// without passing through `f64`. The result is bit-exact at the target
152/// `SCALE` for every supported scale up to `SCALE = 35`.
153pub trait DecimalConsts: Sized {
154    /// Pi (~3.14159265...). One half-turn in radians.
155    ///
156    /// Source: ISO 80000-2 / OEIS A000796. 35-digit reference rescaled to
157    /// `SCALE` with half-away-from-zero rounding.
158    ///
159    /// # Precision
160    ///
161    /// N/A: constant value, no arithmetic performed.
162    fn pi() -> Self;
163
164    /// Tau (~6.28318530...). One full turn in radians.
165    ///
166    /// Defined as `2 * pi`. 35-digit reference rescaled to `SCALE` with
167    /// half-away-from-zero rounding.
168    ///
169    /// # Precision
170    ///
171    /// N/A: constant value, no arithmetic performed.
172    fn tau() -> Self;
173
174    /// Half-pi (~1.57079632...). One quarter-turn in radians.
175    ///
176    /// Defined as `pi / 2`. 35-digit reference rescaled to `SCALE` with
177    /// half-away-from-zero rounding.
178    ///
179    /// # Precision
180    ///
181    /// N/A: constant value, no arithmetic performed.
182    fn half_pi() -> Self;
183
184    /// Quarter-pi (~0.78539816...). One eighth-turn in radians.
185    ///
186    /// Defined as `pi / 4`. 35-digit reference rescaled to `SCALE` with
187    /// half-away-from-zero rounding.
188    ///
189    /// # Precision
190    ///
191    /// N/A: constant value, no arithmetic performed.
192    fn quarter_pi() -> Self;
193
194    /// The golden ratio (~1.61803398...). Dimensionless.
195    ///
196    /// Defined as `(1 + sqrt(5)) / 2`. Source: OEIS A001622. 35-digit
197    /// reference rescaled to `SCALE` with half-away-from-zero rounding.
198    ///
199    /// # Precision
200    ///
201    /// N/A: constant value, no arithmetic performed.
202    fn golden() -> Self;
203
204    /// Euler's number (~2.71828182...). Dimensionless.
205    ///
206    /// Source: OEIS A001113. 35-digit reference rescaled to `SCALE` with
207    /// half-away-from-zero rounding.
208    ///
209    /// # Precision
210    ///
211    /// N/A: constant value, no arithmetic performed.
212    fn e() -> Self;
213}
214
215impl<const SCALE: u32> DecimalConsts for I128<SCALE> {
216    #[inline]
217    fn pi() -> Self {
218        Self(rescale_from_ref(PI_RAW_S35, SCALE))
219    }
220
221    #[inline]
222    fn tau() -> Self {
223        Self(rescale_from_ref(TAU_RAW_S35, SCALE))
224    }
225
226    #[inline]
227    fn half_pi() -> Self {
228        Self(rescale_from_ref(HALF_PI_RAW_S35, SCALE))
229    }
230
231    #[inline]
232    fn quarter_pi() -> Self {
233        Self(rescale_from_ref(QUARTER_PI_RAW_S35, SCALE))
234    }
235
236    #[inline]
237    fn golden() -> Self {
238        Self(rescale_from_ref(GOLDEN_RAW_S35, SCALE))
239    }
240
241    #[inline]
242    fn e() -> Self {
243        Self(rescale_from_ref(E_RAW_S35, SCALE))
244    }
245}
246
247// Inherent associated constants: EPSILON / MIN_POSITIVE.
248//
249// These mirror `f64::EPSILON` and `f64::MIN_POSITIVE` so that generic
250// numeric code that calls `T::EPSILON` or `T::MIN_POSITIVE` compiles
251// when `T = I128<SCALE>`. For I128 both equal `I128(1)` -- the smallest
252// representable positive value (1 LSB = 10^-SCALE). There are no subnormals.
253
254impl<const SCALE: u32> I128<SCALE> {
255    /// Smallest representable positive value: 1 LSB = `10^-SCALE`.
256    ///
257    /// Provided as an analogue to `f64::EPSILON` for generic numeric code.
258    /// Note that this differs from the f64 definition ("difference between
259    /// 1.0 and the next-larger f64"): for `I128` the LSB is uniform across
260    /// the entire representable range.
261    ///
262    /// # Precision
263    ///
264    /// N/A: constant value, no arithmetic performed.
265    pub const EPSILON: Self = Self(1);
266
267    /// Smallest positive value (equal to [`Self::EPSILON`]).
268    ///
269    /// Provided as an analogue to `f64::MIN_POSITIVE` for generic numeric
270    /// code. Unlike `f64`, `I128` has no subnormals, so `MIN_POSITIVE`
271    /// and `EPSILON` are the same value.
272    ///
273    /// # Precision
274    ///
275    /// N/A: constant value, no arithmetic performed.
276    pub const MIN_POSITIVE: Self = Self(1);
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::core_type::I128s12;
283
284    // Bit-exact assertions at SCALE = 12.
285    //
286    // At SCALE = 12 each constant is the 35-digit raw integer divided by
287    // 10^23, rounded half-away-from-zero.
288
289    /// pi at SCALE=12: raw / 10^23.
290    /// Truncated 13 digits: 3_141_592_653_589.
291    /// 14th digit is 7 (from position 14 of the raw) -> round up.
292    /// Expected: 3_141_592_653_590.
293    #[test]
294    fn pi_is_bit_exact_at_scale_12() {
295        assert_eq!(I128s12::pi().to_bits(), 3_141_592_653_590_i128);
296    }
297
298    /// tau at SCALE=12: raw / 10^23.
299    /// Truncated 13 digits: 6_283_185_307_179.
300    /// 14th digit is 5 -> round up. Expected: 6_283_185_307_180.
301    #[test]
302    fn tau_is_bit_exact_at_scale_12() {
303        assert_eq!(I128s12::tau().to_bits(), 6_283_185_307_180_i128);
304    }
305
306    /// half_pi at SCALE=12: raw / 10^23.
307    /// Truncated 13 digits: 1_570_796_326_794.
308    /// 14th digit is 8 -> round up. Expected: 1_570_796_326_795.
309    #[test]
310    fn half_pi_is_bit_exact_at_scale_12() {
311        assert_eq!(I128s12::half_pi().to_bits(), 1_570_796_326_795_i128);
312    }
313
314    /// quarter_pi at SCALE=12: raw / 10^23.
315    /// Truncated 12 digits: 785_398_163_397.
316    /// 13th digit is 4 -> no round-up. Expected: 785_398_163_397.
317    #[test]
318    fn quarter_pi_is_bit_exact_at_scale_12() {
319        assert_eq!(I128s12::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        assert_eq!(I128s12::e().to_bits(), 2_718_281_828_459_i128);
328    }
329
330    /// golden at SCALE=12: raw / 10^23.
331    /// Truncated 13 digits: 1_618_033_988_749.
332    /// 14th digit is 8 -> round up. Expected: 1_618_033_988_750.
333    #[test]
334    fn golden_is_bit_exact_at_scale_12() {
335        assert_eq!(I128s12::golden().to_bits(), 1_618_033_988_750_i128);
336    }
337
338    // Closeness checks against core::f64::consts.
339    // These verify that the correct reference digits were selected; the
340    // bit-exact tests above are the primary acceptance criteria.
341
342    /// pi() converted to f64 is within 1e-11 of `core::f64::consts::PI`.
343    /// At SCALE=12, 1 LSB = 1e-12, so 1e-11 covers rescale rounding plus
344    /// the f64 conversion step.
345    #[test]
346    fn pi_close_to_f64_pi() {
347        let diff = (I128s12::pi().to_f64_lossy() - core::f64::consts::PI).abs();
348        assert!(diff < 1e-11, "pi diverges from f64 PI by {diff}");
349    }
350
351    #[test]
352    fn tau_close_to_f64_tau() {
353        let diff = (I128s12::tau().to_f64_lossy() - core::f64::consts::TAU).abs();
354        assert!(diff < 1e-11, "tau diverges from f64 TAU by {diff}");
355    }
356
357    #[test]
358    fn half_pi_close_to_f64_frac_pi_2() {
359        let diff =
360            (I128s12::half_pi().to_f64_lossy() - core::f64::consts::FRAC_PI_2).abs();
361        assert!(diff < 1e-11, "half_pi diverges from f64 FRAC_PI_2 by {diff}");
362    }
363
364    #[test]
365    fn quarter_pi_close_to_f64_frac_pi_4() {
366        let diff =
367            (I128s12::quarter_pi().to_f64_lossy() - core::f64::consts::FRAC_PI_4).abs();
368        assert!(
369            diff < 1e-11,
370            "quarter_pi diverges from f64 FRAC_PI_4 by {diff}"
371        );
372    }
373
374    #[test]
375    fn e_close_to_f64_e() {
376        let diff = (I128s12::e().to_f64_lossy() - core::f64::consts::E).abs();
377        assert!(diff < 1e-11, "e diverges from f64 E by {diff}");
378    }
379
380    /// golden() converted to f64 is within 1e-11 of the closed form
381    /// `(1 + sqrt(5)) / 2`. Requires std for `f64::sqrt`.
382    #[cfg(feature = "std")]
383    #[test]
384    fn golden_close_to_closed_form() {
385        let expected = (1.0_f64 + 5.0_f64.sqrt()) / 2.0;
386        let diff = (I128s12::golden().to_f64_lossy() - expected).abs();
387        assert!(diff < 1e-11, "golden diverges from closed-form by {diff}");
388    }
389
390    // EPSILON / MIN_POSITIVE
391
392    #[test]
393    fn epsilon_is_one_ulp() {
394        assert_eq!(I128s12::EPSILON.to_bits(), 1_i128);
395        assert!(I128s12::EPSILON > I128s12::ZERO);
396    }
397
398    #[test]
399    fn min_positive_is_one_ulp() {
400        assert_eq!(I128s12::MIN_POSITIVE.to_bits(), 1_i128);
401        assert_eq!(I128s12::MIN_POSITIVE, I128s12::EPSILON);
402    }
403
404    /// At SCALE = 6 the LSB is 10^-6; EPSILON is still raw 1.
405    #[test]
406    fn epsilon_at_scale_6_is_one_ulp() {
407        type D6 = I128<6>;
408        assert_eq!(D6::EPSILON.to_bits(), 1_i128);
409        assert_eq!(D6::MIN_POSITIVE.to_bits(), 1_i128);
410    }
411
412    // Cross-scale exercises
413
414    /// At SCALE = 6, pi() should equal 3.141593 (rounded half-up from
415    /// 3.1415926535...). Expected raw bits: 3_141_593.
416    #[test]
417    fn pi_at_scale_6_is_bit_exact() {
418        type D6 = I128<6>;
419        assert_eq!(D6::pi().to_bits(), 3_141_593_i128);
420    }
421
422    /// At SCALE = 0, pi() rounds to 3 (first fractional digit is 1, no
423    /// round-up).
424    #[test]
425    fn pi_at_scale_0_is_three() {
426        type D0 = I128<0>;
427        assert_eq!(D0::pi().to_bits(), 3_i128);
428    }
429
430    /// At SCALE = SCALE_REF (35), pi() returns exactly the raw constant.
431    #[test]
432    fn pi_at_scale_ref_is_raw_constant() {
433        type D35 = I128<35>;
434        assert_eq!(D35::pi().to_bits(), PI_RAW_S35);
435    }
436
437    /// At SCALE = SCALE_REF + 1 (36), pi() multiplies by 10, appending
438    /// one trailing zero digit.
439    #[test]
440    fn pi_at_scale_36_multiplies_raw_by_ten() {
441        type D36 = I128<36>;
442        assert_eq!(D36::pi().to_bits(), PI_RAW_S35 * 10);
443    }
444
445    /// Negative-side rounding: negating pi gives the expected raw bits.
446    #[test]
447    fn neg_pi_round_trip() {
448        let pi = I128s12::pi();
449        let neg_pi = -pi;
450        assert_eq!(neg_pi.to_bits(), -3_141_592_653_590_i128);
451    }
452}