1use num_integer::Roots;
4use num_rational::Ratio;
5use rust_decimal::Decimal;
6use rust_decimal::MathematicalOps;
7use std::fmt;
8
9pub type RationalInteger = Ratio<i128>;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum NumericFailure {
13 DivisionByZero,
14 Overflow,
15 Irrational,
16 CommitFailed,
17}
18
19impl fmt::Display for NumericFailure {
20 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 NumericFailure::DivisionByZero => formatter.write_str("division by zero"),
23 NumericFailure::Overflow => formatter.write_str("numeric overflow"),
24 NumericFailure::Irrational => formatter.write_str("irrational numeric result"),
25 NumericFailure::CommitFailed => {
26 formatter.write_str("failed to commit rational to decimal")
27 }
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum NumericOperation {
34 Add,
35 Subtract,
36 Multiply,
37 Divide,
38 Modulo,
39 Power,
40}
41
42pub fn rational_one() -> RationalInteger {
43 RationalInteger::new(1, 1)
44}
45
46pub fn rational_zero() -> RationalInteger {
47 RationalInteger::new(0, 1)
48}
49
50pub fn rational_is_zero(rational: &RationalInteger) -> bool {
51 *rational.numer() == 0
52}
53
54pub fn rational_abs(rational: &RationalInteger) -> RationalInteger {
56 if *rational.numer() >= 0 {
57 *rational
58 } else {
59 RationalInteger::new(-rational.numer(), *rational.denom()).reduced()
60 }
61}
62
63pub fn rational_trunc(rational: &RationalInteger) -> RationalInteger {
65 RationalInteger::new(rational.numer() / rational.denom(), 1)
66}
67
68pub fn decimal_to_rational(decimal: Decimal) -> Result<RationalInteger, NumericFailure> {
69 let mantissa = decimal.mantissa();
70 if mantissa == 0 {
71 return Ok(RationalInteger::new(0, 1));
72 }
73 let scale = decimal.scale();
74 let mut denominator = 1i128;
75 for _ in 0..scale {
76 denominator = denominator
77 .checked_mul(10)
78 .ok_or(NumericFailure::Overflow)?;
79 }
80 Ok(RationalInteger::new(mantissa, denominator).reduced())
81}
82
83pub fn commit_rational_to_decimal(rational: &RationalInteger) -> Result<Decimal, NumericFailure> {
85 let reduced = rational.reduced();
86 let numerator = *reduced.numer();
87 let denominator = *reduced.denom();
88 if denominator == 0 {
89 return Err(NumericFailure::CommitFailed);
90 }
91 if numerator == 0 {
92 return Ok(Decimal::ZERO);
93 }
94 let numerator_decimal = decimal_from_i128(numerator)?;
95 let denominator_decimal = decimal_from_i128(denominator)?;
96 numerator_decimal
97 .checked_div(denominator_decimal)
98 .ok_or(NumericFailure::CommitFailed)
99}
100
101pub fn rational_to_display_str(rational: &RationalInteger) -> String {
103 match commit_rational_to_decimal(rational) {
104 Ok(decimal) => decimal_to_display_str(&decimal),
105 Err(_) => rational_fraction_str(rational),
106 }
107}
108
109pub fn rational_to_wire_str(rational: &RationalInteger) -> Result<String, NumericFailure> {
111 commit_rational_to_decimal(rational).map(|decimal| decimal_to_display_str(&decimal))
112}
113
114fn decimal_to_display_str(decimal: &Decimal) -> String {
115 let normalized = decimal.normalize();
116 if normalized.fract().is_zero() {
117 normalized.trunc().to_string()
118 } else {
119 normalized.to_string()
120 }
121}
122
123fn rational_fraction_str(rational: &RationalInteger) -> String {
124 let reduced = rational.reduced();
125 let numer = *reduced.numer();
126 let denom = *reduced.denom();
127 if denom == 1 {
128 numer.to_string()
129 } else {
130 format!("{numer}/{denom}")
131 }
132}
133
134fn decimal_from_i128(value: i128) -> Result<Decimal, NumericFailure> {
135 let max_mantissa = Decimal::MAX.mantissa();
136 let min_mantissa = Decimal::MIN.mantissa();
137 if value > max_mantissa || value < min_mantissa {
138 return Err(NumericFailure::CommitFailed);
139 }
140 Ok(Decimal::from(value))
141}
142
143pub fn rational_operation(
144 left: &RationalInteger,
145 operation: NumericOperation,
146 right: &RationalInteger,
147) -> Result<RationalInteger, NumericFailure> {
148 match operation {
149 NumericOperation::Add => checked_add(left, right),
150 NumericOperation::Subtract => checked_sub(left, right),
151 NumericOperation::Multiply => checked_mul(left, right),
152 NumericOperation::Divide => {
153 if rational_is_zero(right) {
154 return Err(NumericFailure::DivisionByZero);
155 }
156 checked_div(left, right)
157 }
158 NumericOperation::Modulo => {
159 if rational_is_zero(right) {
160 return Err(NumericFailure::DivisionByZero);
161 }
162 let quotient = checked_div(left, right)?;
163 let truncated = rational_trunc("ient);
164 let product = checked_mul(&truncated, right)?;
165 checked_sub(left, &product)
166 }
167 NumericOperation::Power => checked_rational_power(left, right),
168 }
169}
170
171pub fn rational_operation_with_fallback(
174 left: &RationalInteger,
175 operation: NumericOperation,
176 right: &RationalInteger,
177) -> Result<RationalInteger, NumericFailure> {
178 match rational_operation(left, operation, right) {
179 Ok(result) => Ok(result),
180 Err(NumericFailure::DivisionByZero) => Err(NumericFailure::DivisionByZero),
181 Err(_) => approximate_rational_operation(left, operation, right),
182 }
183}
184
185fn approximate_rational_operation(
186 left: &RationalInteger,
187 operation: NumericOperation,
188 right: &RationalInteger,
189) -> Result<RationalInteger, NumericFailure> {
190 let left_decimal = commit_rational_to_decimal(left)?;
191 let right_decimal = commit_rational_to_decimal(right)?;
192 let result_decimal = decimal_arithmetic(left_decimal, operation, right_decimal)?;
193 decimal_to_rational(result_decimal)
194}
195
196fn decimal_arithmetic(
197 left: Decimal,
198 operation: NumericOperation,
199 right: Decimal,
200) -> Result<Decimal, NumericFailure> {
201 match operation {
202 NumericOperation::Add => left.checked_add(right).ok_or(NumericFailure::CommitFailed),
203 NumericOperation::Subtract => left.checked_sub(right).ok_or(NumericFailure::CommitFailed),
204 NumericOperation::Multiply => left.checked_mul(right).ok_or(NumericFailure::CommitFailed),
205 NumericOperation::Divide => {
206 if right.is_zero() {
207 return Err(NumericFailure::DivisionByZero);
208 }
209 left.checked_div(right).ok_or(NumericFailure::CommitFailed)
210 }
211 NumericOperation::Modulo => {
212 if right.is_zero() {
213 return Err(NumericFailure::DivisionByZero);
214 }
215 let quotient = left
216 .checked_div(right)
217 .ok_or(NumericFailure::CommitFailed)?;
218 let truncated = quotient.trunc();
219 let product = truncated
220 .checked_mul(right)
221 .ok_or(NumericFailure::CommitFailed)?;
222 left.checked_sub(product)
223 .ok_or(NumericFailure::CommitFailed)
224 }
225 NumericOperation::Power => decimal_power(left, right),
226 }
227}
228
229fn decimal_is_half(exponent: Decimal) -> bool {
230 exponent
231 .checked_mul(Decimal::TWO)
232 .is_some_and(|doubled| doubled == Decimal::ONE)
233}
234
235fn decimal_power(base: Decimal, exponent: Decimal) -> Result<Decimal, NumericFailure> {
236 if exponent.fract().is_zero() {
237 let exponent_i64 =
238 i64::try_from(exponent.trunc().mantissa()).map_err(|_| NumericFailure::Overflow)?;
239 return base
240 .checked_powi(exponent_i64)
241 .ok_or(NumericFailure::CommitFailed);
242 }
243 if decimal_is_half(exponent) {
244 return base.sqrt().ok_or(NumericFailure::Irrational);
245 }
246 Err(NumericFailure::Irrational)
247}
248
249pub fn checked_add(
250 left: &RationalInteger,
251 right: &RationalInteger,
252) -> Result<RationalInteger, NumericFailure> {
253 left.numer()
254 .checked_mul(*right.denom())
255 .and_then(|left_cross| {
256 right
257 .numer()
258 .checked_mul(*left.denom())
259 .map(|right_cross| (left_cross, right_cross))
260 })
261 .and_then(|(left_cross, right_cross)| {
262 left_cross
263 .checked_add(right_cross)
264 .map(|numerator| (numerator, left.denom().checked_mul(*right.denom())))
265 })
266 .and_then(|(numerator, denominator)| {
267 if denominator == Some(0) {
268 None
269 } else {
270 denominator
271 .map(|denominator| RationalInteger::new(numerator, denominator).reduced())
272 }
273 })
274 .ok_or(NumericFailure::Overflow)
275}
276
277pub fn checked_sub(
278 left: &RationalInteger,
279 right: &RationalInteger,
280) -> Result<RationalInteger, NumericFailure> {
281 left.numer()
282 .checked_mul(*right.denom())
283 .and_then(|left_cross| {
284 right
285 .numer()
286 .checked_mul(*left.denom())
287 .map(|right_cross| (left_cross, right_cross))
288 })
289 .and_then(|(left_cross, right_cross)| {
290 left_cross
291 .checked_sub(right_cross)
292 .map(|numerator| (numerator, left.denom().checked_mul(*right.denom())))
293 })
294 .and_then(|(numerator, denominator)| {
295 if denominator == Some(0) {
296 None
297 } else {
298 denominator
299 .map(|denominator| RationalInteger::new(numerator, denominator).reduced())
300 }
301 })
302 .ok_or(NumericFailure::Overflow)
303}
304
305pub fn checked_mul(
306 left: &RationalInteger,
307 right: &RationalInteger,
308) -> Result<RationalInteger, NumericFailure> {
309 left.numer()
310 .checked_mul(*right.numer())
311 .and_then(|numerator| {
312 left.denom()
313 .checked_mul(*right.denom())
314 .map(|denominator| (numerator, denominator))
315 })
316 .map(|(numerator, denominator)| RationalInteger::new(numerator, denominator).reduced())
317 .ok_or(NumericFailure::Overflow)
318}
319
320pub fn checked_div(
321 left: &RationalInteger,
322 right: &RationalInteger,
323) -> Result<RationalInteger, NumericFailure> {
324 if *right.numer() == 0 {
325 return Err(NumericFailure::DivisionByZero);
326 }
327 left.numer()
328 .checked_mul(*right.denom())
329 .and_then(|numerator| {
330 left.denom()
331 .checked_mul(*right.numer())
332 .map(|denominator| (numerator, denominator))
333 })
334 .map(|(numerator, denominator)| RationalInteger::new(numerator, denominator).reduced())
335 .ok_or(NumericFailure::Overflow)
336}
337
338pub fn checked_pow_i32(
339 base: &RationalInteger,
340 exponent: i32,
341) -> Result<RationalInteger, NumericFailure> {
342 if exponent == 0 {
343 return Ok(rational_one());
344 }
345 if exponent < 0 {
346 if *base.numer() == 0 {
347 return Err(NumericFailure::DivisionByZero);
348 }
349 let positive_base = RationalInteger::new(*base.denom(), *base.numer()).reduced();
350 return checked_pow_i32(&positive_base, -exponent);
351 }
352 let mut result = rational_one();
353 let mut factor = base.reduced();
354 let mut remaining = exponent as u32;
355 while remaining > 0 {
356 if remaining % 2 == 1 {
357 result = checked_mul(&result, &factor)?;
358 }
359 remaining /= 2;
360 if remaining > 0 {
361 factor = checked_mul(&factor, &factor)?;
362 }
363 }
364 Ok(result)
365}
366
367pub fn checked_rational_power(
368 base: &RationalInteger,
369 exponent: &RationalInteger,
370) -> Result<RationalInteger, NumericFailure> {
371 let exp_numer = *exponent.numer();
372 let exp_denom = *exponent.denom();
373
374 assert!(
375 exp_denom > 0,
376 "BUG: rational exponent must have positive denominator (canonical Ratio invariant)"
377 );
378
379 if exp_denom == 1 {
380 let exponent = i32::try_from(exp_numer).map_err(|_| NumericFailure::Overflow)?;
381 return checked_pow_i32(base, exponent);
382 }
383
384 if *base.numer() == 0 {
385 if exp_numer <= 0 {
386 return Err(NumericFailure::DivisionByZero);
387 }
388 return Ok(RationalInteger::new(0, 1));
389 }
390
391 let abs_exp_numer = exp_numer.unsigned_abs();
392 let abs_exp_i32 = i32::try_from(abs_exp_numer).map_err(|_| NumericFailure::Overflow)?;
393 let raised = checked_pow_i32(base, abs_exp_i32)?;
394
395 let root_degree = u32::try_from(exp_denom).map_err(|_| NumericFailure::Overflow)?;
396
397 let raised_numer = *raised.numer();
398 let raised_denom = *raised.denom();
399
400 let (numer_root, numer_negative) = if raised_numer < 0 {
401 if root_degree % 2 == 0 {
402 return Err(NumericFailure::Irrational);
403 }
404 (raised_numer.unsigned_abs().nth_root(root_degree), true)
405 } else {
406 ((raised_numer as u128).nth_root(root_degree), false)
407 };
408
409 let denom_root = (raised_denom as u128).nth_root(root_degree);
410
411 let numer_root_i128 = i128::try_from(numer_root).map_err(|_| NumericFailure::Overflow)?;
412 let denom_root_i128 = i128::try_from(denom_root).map_err(|_| NumericFailure::Overflow)?;
413
414 let numer_reconstructed = numer_root_i128
415 .checked_pow(root_degree)
416 .ok_or(NumericFailure::Overflow)?;
417 let denom_reconstructed = denom_root_i128
418 .checked_pow(root_degree)
419 .ok_or(NumericFailure::Overflow)?;
420
421 if numer_reconstructed != raised_numer.unsigned_abs() as i128 {
422 return Err(NumericFailure::Irrational);
423 }
424 if denom_reconstructed != raised_denom {
425 return Err(NumericFailure::Irrational);
426 }
427
428 let signed_numer = if numer_negative {
429 -numer_root_i128
430 } else {
431 numer_root_i128
432 };
433
434 let result = RationalInteger::new(signed_numer, denom_root_i128).reduced();
435
436 if exp_numer < 0 {
437 if *result.numer() == 0 {
438 return Err(NumericFailure::DivisionByZero);
439 }
440 Ok(RationalInteger::new(*result.denom(), *result.numer()).reduced())
441 } else {
442 Ok(result)
443 }
444}
445
446pub fn convert_quantity_magnitude_rational(
448 magnitude: RationalInteger,
449 from_factor: &RationalInteger,
450 to_factor: &RationalInteger,
451) -> Result<RationalInteger, NumericFailure> {
452 let ratio = checked_div(from_factor, to_factor)?;
453 checked_mul(&magnitude, &ratio)
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use rust_decimal::Decimal;
460 use std::str::FromStr;
461
462 #[test]
463 fn rational_zero_is_zero() {
464 assert!(rational_is_zero(&rational_zero()));
465 }
466
467 #[test]
468 fn decimal_one_half_lifts_to_rational() {
469 let decimal = Decimal::from_str("0.5").unwrap();
470 let rational = decimal_to_rational(decimal).unwrap();
471 assert_eq!(rational, RationalInteger::new(1, 2));
472 }
473
474 #[test]
475 fn commit_one_third_to_decimal() {
476 let rational = RationalInteger::new(1, 3);
477 let decimal = commit_rational_to_decimal(&rational).unwrap();
478 let expected = Decimal::from_str("0.3333333333333333333333333333").unwrap();
479 assert_eq!(decimal, expected);
480 }
481
482 #[test]
483 fn checked_mul_integer() {
484 let left = RationalInteger::new(50, 1);
485 let right = RationalInteger::new(86400, 1);
486 let product = checked_mul(&left, &right).unwrap();
487 assert_eq!(product, RationalInteger::new(4_320_000, 1));
488 }
489
490 #[test]
491 fn checked_pow_negative_exponent_inverts_base() {
492 let hour_factor = RationalInteger::new(3600, 1);
493 let inverse = checked_pow_i32(&hour_factor, -1).unwrap();
494 assert_eq!(inverse, RationalInteger::new(1, 3600));
495 }
496
497 #[test]
498 fn rational_operation_divide_by_zero() {
499 let left = RationalInteger::new(1, 1);
500 let right = RationalInteger::new(0, 1);
501 let failure = rational_operation(&left, NumericOperation::Divide, &right).unwrap_err();
502 assert_eq!(failure, NumericFailure::DivisionByZero);
503 }
504
505 #[test]
506 fn rational_operation_power_irrational() {
507 let base = RationalInteger::new(2, 1);
508 let exponent = RationalInteger::new(1, 2);
509 let failure = rational_operation(&base, NumericOperation::Power, &exponent).unwrap_err();
510 assert_eq!(failure, NumericFailure::Irrational);
511 }
512
513 #[test]
514 fn rational_operation_power_exact() {
515 let base = RationalInteger::new(4, 1);
516 let exponent = RationalInteger::new(1, 2);
517 let result = rational_operation(&base, NumericOperation::Power, &exponent).unwrap();
518 assert_eq!(result, RationalInteger::new(2, 1));
519 }
520
521 #[test]
522 fn rational_operation_add() {
523 let left = RationalInteger::new(1, 3);
524 let right = RationalInteger::new(1, 6);
525 let sum = rational_operation(&left, NumericOperation::Add, &right).unwrap();
526 assert_eq!(sum, RationalInteger::new(1, 2));
527 }
528
529 #[test]
530 fn rational_operation_with_fallback_add_after_overflow_path() {
531 let left = RationalInteger::new(1, 3);
532 let right = RationalInteger::new(1, 6);
533 let sum = rational_operation_with_fallback(&left, NumericOperation::Add, &right).unwrap();
534 assert_eq!(sum, RationalInteger::new(1, 2));
535 }
536
537 #[test]
538 fn rational_operation_with_fallback_power_sqrt_via_decimal() {
539 let result = rational_operation_with_fallback(
540 &RationalInteger::new(2, 1),
541 NumericOperation::Power,
542 &RationalInteger::new(1, 2),
543 )
544 .unwrap();
545 assert_eq!(
546 commit_rational_to_decimal(&result).unwrap(),
547 commit_rational_to_decimal(&RationalInteger::new(2, 1))
548 .unwrap()
549 .sqrt()
550 .unwrap(),
551 );
552 }
553
554 #[test]
555 fn rational_abs_negates_negative_numerator() {
556 let negative = RationalInteger::new(-172_800, 1);
557 assert_eq!(rational_abs(&negative), RationalInteger::new(172_800, 1));
558 }
559
560 #[test]
561 fn rational_to_wire_str_rejects_uncommittable() {
562 let too_large = RationalInteger::new(i128::MAX, 1);
563 assert!(commit_rational_to_decimal(&too_large).is_err());
564 assert!(rational_to_wire_str(&too_large).is_err());
565 }
566
567 #[test]
568 fn rational_to_wire_str_matches_commit_for_committable() {
569 let rational = RationalInteger::new(37, 1);
570 assert_eq!(rational_to_wire_str(&rational).unwrap(), "37");
571 }
572
573 #[test]
574 fn rational_to_display_str_falls_back_to_fraction_when_commit_fails() {
575 let rational = RationalInteger::new(355, 113);
578 let display = rational_to_display_str(&rational);
579 assert!(
580 display.contains('/') || commit_rational_to_decimal(&rational).is_ok(),
581 "display must be either committable decimal or fraction, got {display}"
582 );
583 }
584}