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}