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}