1use num_bigint::BigInt;
8use num_rational::BigRational;
9
10#[derive(Clone, Debug)]
16pub enum SemaNumber {
17 Integer(BigInt),
18 Rational(BigRational),
19 Real(f64),
20 Complex(Box<Complex>),
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct Complex {
28 pub re: SemaNumber,
29 pub im: SemaNumber,
30}
31
32impl SemaNumber {
33 pub fn is_exact(&self) -> bool {
35 match self {
36 SemaNumber::Integer(_) | SemaNumber::Rational(_) => true,
37 SemaNumber::Real(_) => false,
38 SemaNumber::Complex(c) => c.re.is_exact() && c.im.is_exact(),
39 }
40 }
41
42 pub fn is_integer(&self) -> bool {
45 match self {
46 SemaNumber::Integer(_) => true,
47 SemaNumber::Rational(_) => false,
48 SemaNumber::Real(f) => f.is_finite() && f.fract() == 0.0,
49 SemaNumber::Complex(_) => false,
50 }
51 }
52
53 pub fn is_real(&self) -> bool {
55 !matches!(self, SemaNumber::Complex(_))
56 }
57
58 pub fn normalize(self) -> SemaNumber {
62 use num_traits::{One, Zero};
63 match self {
64 SemaNumber::Rational(r) => {
65 if r.denom().is_one() {
66 SemaNumber::Integer(r.numer().clone())
67 } else {
68 SemaNumber::Rational(r)
69 }
70 }
71 SemaNumber::Complex(c) => {
72 let re = c.re.normalize();
73 let im = c.im.normalize();
74 let im_is_exact_zero = matches!(&im, SemaNumber::Integer(n) if n.is_zero());
77 if im_is_exact_zero {
78 re
79 } else {
80 SemaNumber::Complex(Box::new(Complex { re, im }))
81 }
82 }
83 other => other,
84 }
85 }
86
87 pub fn to_f64(&self) -> f64 {
91 use num_traits::ToPrimitive;
92 match self {
93 SemaNumber::Integer(n) => n.to_f64().unwrap_or(f64::INFINITY),
94 SemaNumber::Rational(r) => r.to_f64().unwrap_or(f64::INFINITY),
95 SemaNumber::Real(f) => *f,
96 SemaNumber::Complex(_) => f64::NAN,
97 }
98 }
99
100 fn level(&self) -> u8 {
102 match self {
103 SemaNumber::Integer(_) => 0,
104 SemaNumber::Rational(_) => 1,
105 SemaNumber::Real(_) => 2,
106 SemaNumber::Complex(_) => 3,
107 }
108 }
109
110 fn lift_to(self, level: u8) -> SemaNumber {
114 use num_traits::Zero;
115 match (self.level(), level) {
116 (a, b) if a >= b => self,
117 (0, 1) => match self {
118 SemaNumber::Integer(n) => SemaNumber::Rational(BigRational::from(n)),
119 _ => unreachable!(),
120 },
121 (_, 2) => SemaNumber::Real(self.to_f64()),
122 (_, 3) => SemaNumber::Complex(Box::new(Complex {
123 re: self,
124 im: SemaNumber::Integer(BigInt::zero()),
125 })),
126 _ => self,
128 }
129 }
130
131 pub fn promote(a: SemaNumber, b: SemaNumber) -> (SemaNumber, SemaNumber) {
134 let target = a.level().max(b.level());
135 (a.lift_to(target), b.lift_to(target))
136 }
137
138 #[allow(clippy::should_implement_trait)]
143 pub fn neg(self) -> SemaNumber {
144 match self {
145 SemaNumber::Integer(n) => SemaNumber::Integer(-n),
146 SemaNumber::Rational(r) => SemaNumber::Rational(-r),
147 SemaNumber::Real(f) => SemaNumber::Real(-f),
148 SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
149 re: c.re.neg(),
150 im: c.im.neg(),
151 })),
152 }
153 .normalize()
154 }
155
156 pub fn exact_sqrt(&self) -> Option<SemaNumber> {
161 use num_traits::Signed;
162 match self {
163 SemaNumber::Integer(n) if !n.is_negative() => {
164 let root = n.sqrt();
165 (&root * &root == *n).then_some(SemaNumber::Integer(root))
166 }
167 SemaNumber::Rational(r) if !r.is_negative() => {
168 let (rn, rd) = (r.numer().sqrt(), r.denom().sqrt());
171 (&rn * &rn == *r.numer() && &rd * &rd == *r.denom())
172 .then(|| SemaNumber::Rational(BigRational::new(rn, rd)).normalize())
173 }
174 _ => None,
175 }
176 }
177
178 #[allow(clippy::should_implement_trait)]
179 pub fn add(self, other: SemaNumber) -> SemaNumber {
180 let (a, b) = SemaNumber::promote(self, other);
181 match (a, b) {
182 (SemaNumber::Integer(x), SemaNumber::Integer(y)) => SemaNumber::Integer(x + y),
183 (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x + y),
184 (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x + y),
185 (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
186 SemaNumber::Complex(Box::new(Complex {
187 re: x.re.add(y.re),
188 im: x.im.add(y.im),
189 }))
190 }
191 _ => unreachable!("promote guarantees equal levels"),
192 }
193 .normalize()
194 }
195
196 #[allow(clippy::should_implement_trait)]
197 pub fn sub(self, other: SemaNumber) -> SemaNumber {
198 self.add(other.neg())
199 }
200
201 pub fn abs(self) -> SemaNumber {
206 use num_traits::Signed;
207 match self {
208 SemaNumber::Integer(n) => SemaNumber::Integer(n.abs()),
209 SemaNumber::Rational(r) => SemaNumber::Rational(r.abs()),
210 SemaNumber::Real(f) => SemaNumber::Real(f.abs()),
211 SemaNumber::Complex(c) => SemaNumber::Real(c.re.to_f64().hypot(c.im.to_f64())),
212 }
213 .normalize()
214 }
215
216 pub fn floor(self) -> SemaNumber {
221 match self {
222 SemaNumber::Integer(n) => SemaNumber::Integer(n),
223 SemaNumber::Rational(r) => SemaNumber::Integer(r.floor().to_integer()),
224 SemaNumber::Real(f) => SemaNumber::Real(f.floor()),
225 SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
226 }
227 }
228
229 pub fn ceil(self) -> SemaNumber {
231 match self {
232 SemaNumber::Integer(n) => SemaNumber::Integer(n),
233 SemaNumber::Rational(r) => SemaNumber::Integer(r.ceil().to_integer()),
234 SemaNumber::Real(f) => SemaNumber::Real(f.ceil()),
235 SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
236 }
237 }
238
239 pub fn truncate(self) -> SemaNumber {
242 match self {
243 SemaNumber::Integer(n) => SemaNumber::Integer(n),
244 SemaNumber::Rational(r) => SemaNumber::Integer(r.trunc().to_integer()),
245 SemaNumber::Real(f) => SemaNumber::Real(f.trunc()),
246 SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
247 }
248 }
249
250 pub fn round(self) -> SemaNumber {
253 use num_integer::Integer;
254 use std::cmp::Ordering;
255 match self {
256 SemaNumber::Integer(n) => SemaNumber::Integer(n),
257 SemaNumber::Rational(r) => {
258 let (numer, denom) = (r.numer().clone(), r.denom().clone());
259 let floor = numer.div_floor(&denom);
262 let rem = &numer - &floor * &denom; let twice_rem = &rem * BigInt::from(2);
264 let round_up = match twice_rem.cmp(&denom) {
265 Ordering::Less => false,
266 Ordering::Greater => true,
267 Ordering::Equal => floor.is_odd(),
269 };
270 let result = if round_up { floor + 1 } else { floor };
271 SemaNumber::Integer(result)
272 }
273 SemaNumber::Real(f) => SemaNumber::Real(f.round_ties_even()),
274 SemaNumber::Complex(_) => unreachable!("caller must guard complex via is_real()"),
275 }
276 }
277
278 #[allow(clippy::should_implement_trait)]
279 pub fn mul(self, other: SemaNumber) -> SemaNumber {
280 let (a, b) = SemaNumber::promote(self, other);
281 match (a, b) {
282 (SemaNumber::Integer(x), SemaNumber::Integer(y)) => SemaNumber::Integer(x * y),
283 (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x * y),
284 (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x * y),
285 (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
286 let ac = x.re.clone().mul(y.re.clone());
288 let bd = x.im.clone().mul(y.im.clone());
289 let ad = x.re.mul(y.im.clone());
290 let bc = x.im.mul(y.re);
291 SemaNumber::Complex(Box::new(Complex {
292 re: ac.sub(bd),
293 im: ad.add(bc),
294 }))
295 }
296 _ => unreachable!("promote guarantees equal levels"),
297 }
298 .normalize()
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub struct DivByZero;
306
307impl SemaNumber {
308 #[allow(clippy::should_implement_trait)]
309 pub fn div(self, other: SemaNumber) -> Result<SemaNumber, DivByZero> {
310 use num_traits::Zero;
311 if matches!(&other, SemaNumber::Integer(n) if n.is_zero())
314 || matches!(&other, SemaNumber::Rational(r) if r.numer().is_zero())
315 {
316 return Err(DivByZero);
317 }
318 let (a, b) = SemaNumber::promote(self, other);
319 let out = match (a, b) {
320 (SemaNumber::Integer(x), SemaNumber::Integer(y)) => {
322 SemaNumber::Rational(BigRational::new(x, y))
323 }
324 (SemaNumber::Rational(x), SemaNumber::Rational(y)) => SemaNumber::Rational(x / y),
325 (SemaNumber::Real(x), SemaNumber::Real(y)) => SemaNumber::Real(x / y),
326 (SemaNumber::Complex(x), SemaNumber::Complex(y)) => {
327 let denom =
329 y.re.clone()
330 .mul(y.re.clone())
331 .add(y.im.clone().mul(y.im.clone()));
332 let num = SemaNumber::Complex(x).mul(SemaNumber::Complex(Box::new(Complex {
333 re: y.re,
334 im: y.im.neg(),
335 })));
336 match num {
337 SemaNumber::Complex(nc) => SemaNumber::Complex(Box::new(Complex {
338 re: nc.re.div(denom.clone())?,
339 im: nc.im.div(denom)?,
340 })),
341 real => real.div(denom)?,
343 }
344 }
345 _ => unreachable!("promote guarantees equal levels"),
346 };
347 Ok(out.normalize())
348 }
349
350 pub fn powi(self, exp: &BigInt) -> Result<SemaNumber, DivByZero> {
358 use num_integer::Integer;
359 use num_traits::{Signed, Zero};
360 let negative = exp.is_negative();
361 let mut e = if negative { -exp } else { exp.clone() };
362 let mut result = SemaNumber::from_i64(1);
363 let mut base = self;
364 while !e.is_zero() {
365 if e.is_odd() {
366 result = result.mul(base.clone());
367 }
368 e = e.div_floor(&BigInt::from(2));
369 if !e.is_zero() {
370 base = base.clone().mul(base);
371 }
372 }
373 if negative {
374 SemaNumber::from_i64(1).div(result)
375 } else {
376 Ok(result)
377 }
378 }
379}
380
381impl SemaNumber {
382 fn real_to_exact(f: f64) -> Option<SemaNumber> {
385 if !f.is_finite() {
386 return None;
387 }
388 num_rational::BigRational::from_float(f).map(SemaNumber::Rational)
390 }
391
392 pub fn num_eq(&self, other: &SemaNumber) -> bool {
393 match (self, other) {
394 (SemaNumber::Complex(a), SemaNumber::Complex(b)) => {
395 a.re.num_eq(&b.re) && a.im.num_eq(&b.im)
396 }
397 (SemaNumber::Complex(_), _) | (_, SemaNumber::Complex(_)) => false,
398 _ => self.cmp_real(other) == Some(std::cmp::Ordering::Equal),
399 }
400 }
401
402 pub fn cmp_real(&self, other: &SemaNumber) -> Option<std::cmp::Ordering> {
406 use std::cmp::Ordering;
407 if matches!(self, SemaNumber::Complex(_)) || matches!(other, SemaNumber::Complex(_)) {
408 return None;
409 }
410 if let (SemaNumber::Real(x), SemaNumber::Real(y)) = (self, other) {
412 return x.partial_cmp(y);
413 }
414 match (self, other) {
417 (SemaNumber::Real(f), _) if !f.is_finite() => {
418 return if f.is_nan() {
419 None
420 } else if *f > 0.0 {
421 Some(Ordering::Greater)
422 } else {
423 Some(Ordering::Less)
424 };
425 }
426 (_, SemaNumber::Real(f)) if !f.is_finite() => {
427 return if f.is_nan() {
428 None
429 } else if *f > 0.0 {
430 Some(Ordering::Less)
431 } else {
432 Some(Ordering::Greater)
433 };
434 }
435 _ => {}
436 }
437 let to_exact = |v: &SemaNumber| -> SemaNumber {
439 match v {
440 SemaNumber::Real(f) => {
441 SemaNumber::real_to_exact(*f).expect("finite (checked above)")
442 }
443 other => other.clone(),
444 }
445 };
446 let a = to_exact(self);
447 let b = to_exact(other);
448 let (a, b) = SemaNumber::promote(a, b);
449 match (a, b) {
450 (SemaNumber::Integer(x), SemaNumber::Integer(y)) => Some(x.cmp(&y)),
451 (SemaNumber::Rational(x), SemaNumber::Rational(y)) => Some(x.cmp(&y)),
452 _ => unreachable!("both exact after real_to_exact + promote"),
453 }
454 }
455}
456
457fn fmt_real(n: &SemaNumber, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 match n {
461 SemaNumber::Integer(v) => write!(f, "{v}"),
462 SemaNumber::Rational(r) => write!(f, "{}/{}", r.numer(), r.denom()),
463 SemaNumber::Real(v) => {
464 if v.fract() == 0.0 && v.is_finite() {
465 write!(f, "{v:.1}")
466 } else {
467 write!(f, "{v}")
468 }
469 }
470 SemaNumber::Complex(_) => unreachable!("complex component is never complex"),
471 }
472}
473
474impl std::fmt::Display for SemaNumber {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 use num_traits::Zero;
477 match self {
478 SemaNumber::Complex(c) => {
479 fmt_real(&c.re, f)?;
480 let (sign, mag) = match &c.im {
482 SemaNumber::Integer(v) if *v < BigInt::zero() => {
483 ('-', SemaNumber::Integer(-v.clone()))
484 }
485 SemaNumber::Rational(r) if *r < BigRational::from(BigInt::zero()) => {
486 ('-', SemaNumber::Rational(-r.clone()))
487 }
488 SemaNumber::Real(v) if v.is_sign_negative() => ('-', SemaNumber::Real(-v)),
489 other => ('+', other.clone()),
490 };
491 write!(f, "{sign}")?;
492 fmt_real(&mag, f)?;
493 write!(f, "i")
494 }
495 real => fmt_real(real, f),
496 }
497 }
498}
499
500impl SemaNumber {
501 pub fn from_i64(v: i64) -> SemaNumber {
502 SemaNumber::Integer(BigInt::from(v))
503 }
504 pub fn from_f64(v: f64) -> SemaNumber {
505 SemaNumber::Real(v)
506 }
507
508 pub fn to_inexact(self) -> SemaNumber {
509 match self {
510 SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
511 re: c.re.to_inexact(),
512 im: c.im.to_inexact(),
513 })),
514 other => SemaNumber::Real(other.to_f64()),
515 }
516 }
517
518 pub fn to_exact(self) -> SemaNumber {
522 match self {
523 SemaNumber::Real(f) => SemaNumber::real_to_exact(f)
524 .map(|n| n.normalize())
525 .unwrap_or(SemaNumber::Real(f)),
526 SemaNumber::Complex(c) => SemaNumber::Complex(Box::new(Complex {
527 re: c.re.to_exact(),
528 im: c.im.to_exact(),
529 }))
530 .normalize(),
531 exact => exact,
532 }
533 }
534}
535
536fn simplest_rational_in(lo: BigRational, hi: BigRational) -> BigRational {
542 use num_traits::{Signed, Zero};
543 if lo.is_positive() {
544 simplest_positive(lo, hi)
545 } else if hi.is_negative() {
546 -simplest_positive(-hi, -lo)
548 } else {
549 BigRational::zero()
551 }
552}
553
554fn simplest_positive(lo: BigRational, hi: BigRational) -> BigRational {
556 use num_traits::One;
557 let fl = lo.floor(); if fl == lo {
559 fl
561 } else if fl < hi.floor() {
562 fl + BigRational::one()
564 } else {
565 let frac_lo = &lo - &fl;
569 let frac_hi = &hi - &fl;
570 fl + simplest_positive(frac_hi.recip(), frac_lo.recip()).recip()
571 }
572}
573
574impl SemaNumber {
575 fn to_exact_rational(&self) -> Option<BigRational> {
578 match self {
579 SemaNumber::Integer(i) => Some(BigRational::from(i.clone())),
580 SemaNumber::Rational(r) => Some(r.clone()),
581 SemaNumber::Real(f) => BigRational::from_float(*f),
582 SemaNumber::Complex(_) => None,
583 }
584 }
585
586 pub fn rationalize(&self, tol: &SemaNumber) -> SemaNumber {
592 use num_traits::{Signed, Zero};
593 let inexact = !self.is_exact() || !tol.is_exact();
594 let wrap = |r: BigRational| {
595 let n = SemaNumber::Rational(r).normalize();
596 if inexact {
597 n.to_inexact()
598 } else {
599 n
600 }
601 };
602 let xr = match self.to_exact_rational() {
604 Some(x) => x,
605 None => return self.clone(),
606 };
607 let er = match tol.to_exact_rational() {
608 Some(e) => e.abs(),
609 None => {
612 return if matches!(tol, SemaNumber::Real(f) if f.is_infinite()) {
613 wrap(BigRational::zero())
614 } else {
615 self.clone()
616 };
617 }
618 };
619 let lo = &xr - &er;
620 let hi = &xr + &er;
621 wrap(simplest_rational_in(lo, hi))
622 }
623}
624
625impl PartialEq for SemaNumber {
626 fn eq(&self, other: &Self) -> bool {
627 use SemaNumber::*;
628 match (self, other) {
629 (Integer(a), Integer(b)) => a == b,
630 (Rational(a), Rational(b)) => a == b,
631 (Real(a), Real(b)) => a.to_bits() == b.to_bits(),
632 (Complex(a), Complex(b)) => a.re == b.re && a.im == b.im,
633 _ => false,
634 }
635 }
636}
637impl Eq for SemaNumber {}
638impl std::hash::Hash for SemaNumber {
639 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
640 use SemaNumber::*;
641 match self {
642 Integer(n) => {
643 0u8.hash(state);
644 n.hash(state);
645 }
646 Rational(r) => {
647 1u8.hash(state);
648 r.hash(state);
649 }
650 Real(f) => {
651 2u8.hash(state);
652 f.to_bits().hash(state);
653 }
654 Complex(c) => {
655 3u8.hash(state);
656 c.re.hash(state);
657 c.im.hash(state);
658 }
659 }
660 }
661}
662impl Ord for SemaNumber {
663 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
664 use SemaNumber::*;
665 self.level()
666 .cmp(&other.level())
667 .then_with(|| match (self, other) {
668 (Integer(a), Integer(b)) => a.cmp(b),
669 (Rational(a), Rational(b)) => a.cmp(b),
670 (Real(a), Real(b)) => a.total_cmp(b),
671 (Complex(a), Complex(b)) => a.re.cmp(&b.re).then_with(|| a.im.cmp(&b.im)),
672 _ => std::cmp::Ordering::Equal, })
674 }
675}
676impl PartialOrd for SemaNumber {
677 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
678 Some(self.cmp(other))
679 }
680}
681
682impl SemaNumber {
683 pub fn parse_int_radix(digits: &str, radix: u32) -> Option<SemaNumber> {
686 let (sign, body) = match digits.strip_prefix('-') {
687 Some(rest) => (num_bigint::Sign::Minus, rest),
688 None => (
689 num_bigint::Sign::Plus,
690 digits.strip_prefix('+').unwrap_or(digits),
691 ),
692 };
693 if body.is_empty() {
694 return None;
695 }
696 let bytes = body.as_bytes();
697 let magnitude = num_bigint::BigUint::parse_bytes(bytes, radix)?;
698 Some(SemaNumber::Integer(BigInt::from_biguint(sign, magnitude)).normalize())
699 }
700
701 pub fn parse_rational(s: &str) -> Option<SemaNumber> {
704 use num_traits::Zero;
705 use std::str::FromStr;
706 let (n, d) = s.split_once('/')?;
707 let numer = BigInt::from_str(n).ok()?;
708 let denom = BigInt::from_str(d).ok()?;
709 if denom.is_zero() {
710 return None;
711 }
712 Some(SemaNumber::Rational(BigRational::new(numer, denom)).normalize())
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use num_traits::One;
720
721 #[test]
722 fn classification() {
723 assert!(SemaNumber::Integer(BigInt::from(5)).is_exact());
724 assert!(SemaNumber::Integer(BigInt::from(5)).is_integer());
725 assert!(!SemaNumber::Real(2.5).is_exact());
726 assert!(SemaNumber::Real(2.0).is_integer());
727 assert!(!SemaNumber::Real(2.5).is_integer());
728 let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
729 assert!(half.is_exact());
730 assert!(!half.is_integer());
731 assert!(half.is_real());
732 }
733
734 #[test]
735 fn normalize_collapses() {
736 use num_traits::Zero;
737 let r = SemaNumber::Rational(BigRational::new(BigInt::from(4), BigInt::from(2)));
739 assert!(matches!(r.normalize(), SemaNumber::Integer(n) if n == BigInt::from(2)));
740 let c = SemaNumber::Complex(Box::new(Complex {
742 re: SemaNumber::Integer(BigInt::from(3)),
743 im: SemaNumber::Integer(BigInt::zero()),
744 }));
745 assert!(matches!(c.normalize(), SemaNumber::Integer(n) if n == BigInt::from(3)));
746 let c2 = SemaNumber::Complex(Box::new(Complex {
748 re: SemaNumber::Integer(BigInt::from(3)),
749 im: SemaNumber::Real(0.0),
750 }));
751 assert!(matches!(c2.normalize(), SemaNumber::Complex(_)));
752 }
753
754 #[test]
755 fn to_f64_projection() {
756 assert_eq!(SemaNumber::Integer(BigInt::from(7)).to_f64(), 7.0);
757 assert_eq!(
758 SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(4))).to_f64(),
759 0.25
760 );
761 assert_eq!(SemaNumber::Real(1.5).to_f64(), 1.5);
762 }
763
764 #[test]
765 fn promote_to_common_level() {
766 let (a, b) = SemaNumber::promote(
768 SemaNumber::Integer(BigInt::from(2)),
769 SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2))),
770 );
771 assert!(matches!(a, SemaNumber::Rational(_)));
772 assert!(matches!(b, SemaNumber::Rational(_)));
773 let (a, b) =
775 SemaNumber::promote(SemaNumber::Integer(BigInt::from(2)), SemaNumber::Real(0.5));
776 assert!(matches!(a, SemaNumber::Real(_)));
777 assert!(matches!(b, SemaNumber::Real(_)));
778 }
779
780 #[test]
781 fn add_sub_mul_neg() {
782 use num_traits::Zero;
783 let two = || SemaNumber::Integer(BigInt::from(2));
784 let half = || SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
785 assert_eq!(two().add(half()).to_f64(), 2.5);
787 assert!(matches!(two().add(half()), SemaNumber::Rational(_)));
789 assert!(matches!(half().add(half()), SemaNumber::Integer(n) if n == BigInt::one()));
791 assert!(matches!(two().sub(two()), SemaNumber::Integer(n) if n == BigInt::zero()));
793 assert!(matches!(two().mul(half()), SemaNumber::Integer(n) if n == BigInt::one()));
795 assert_eq!(half().neg().to_f64(), -0.5);
797 assert!(matches!(
799 two().add(SemaNumber::Real(0.5)),
800 SemaNumber::Real(_)
801 ));
802 }
803
804 #[test]
805 fn abs_preserves_exactness_over_reals_inexact_over_complex() {
806 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
807 assert!(matches!(n(-5).abs(), SemaNumber::Integer(v) if v == BigInt::from(5)));
809 assert!(matches!(n(5).abs(), SemaNumber::Integer(v) if v == BigInt::from(5)));
810 let neg_half = SemaNumber::Rational(BigRational::new(BigInt::from(-1), BigInt::from(2)));
812 assert!(matches!(neg_half.abs(),
813 SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(2))));
814 assert!(matches!(SemaNumber::Real(-2.5).abs(), SemaNumber::Real(f) if f == 2.5));
816 let c = SemaNumber::Complex(Box::new(Complex { re: n(3), im: n(4) }));
818 assert!(matches!(c.abs(), SemaNumber::Real(f) if f == 5.0));
819 }
820
821 #[test]
822 fn division_is_exact_when_possible() {
823 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
824 let third = n(1).div(n(3)).unwrap();
826 assert!(matches!(&third, SemaNumber::Rational(r)
827 if *r == BigRational::new(BigInt::one(), BigInt::from(3))));
828 assert!(matches!(n(6).div(n(3)).unwrap(), SemaNumber::Integer(k) if k == BigInt::from(2)));
830 assert!(matches!(
832 n(1).div(SemaNumber::Real(2.0)).unwrap(),
833 SemaNumber::Real(_)
834 ));
835 assert!(n(1).div(n(0)).is_err());
837 assert!(
839 matches!(n(1).div(SemaNumber::Real(0.0)).unwrap(), SemaNumber::Real(f) if f.is_infinite())
840 );
841 }
842
843 #[test]
844 fn compare_and_equal() {
845 use std::cmp::Ordering;
846 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
847 let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
848 assert!(half.num_eq(&SemaNumber::Real(0.5)));
850 assert!(n(2).num_eq(&SemaNumber::Real(2.0)));
852 assert_eq!(half.cmp_real(&n(1)), Some(Ordering::Less));
854 assert_eq!(n(3).cmp_real(&n(2)), Some(Ordering::Greater));
855 let big = SemaNumber::Integer(BigInt::from(9_007_199_254_740_993_i64));
857 assert_eq!(
858 big.cmp_real(&SemaNumber::Real(9_007_199_254_740_992.0)),
859 Some(Ordering::Greater)
860 );
861 let i = SemaNumber::Complex(Box::new(Complex { re: n(0), im: n(1) }));
863 assert_eq!(i.cmp_real(&n(0)), None);
864 assert!(!i.num_eq(&n(0)));
865 }
866
867 #[test]
868 fn display_round_trippable() {
869 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
870 assert_eq!(n(42).to_string(), "42");
871 assert_eq!(
872 SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(3))).to_string(),
873 "1/3"
874 );
875 assert_eq!(SemaNumber::Real(2.0).to_string(), "2.0");
876 assert_eq!(SemaNumber::Real(2.5).to_string(), "2.5");
877 let c = SemaNumber::Complex(Box::new(Complex { re: n(3), im: n(4) }));
878 assert_eq!(c.to_string(), "3+4i");
879 let c2 = SemaNumber::Complex(Box::new(Complex {
880 re: n(0),
881 im: n(-1),
882 }));
883 assert_eq!(c2.to_string(), "0-1i");
884 }
885
886 #[test]
887 fn exactness_conversions() {
888 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
889 assert!(matches!(n(3).to_inexact(), SemaNumber::Real(f) if f == 3.0));
891 assert!(matches!(SemaNumber::Real(0.5).to_exact(),
893 SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(2))));
894 assert!(
896 matches!(SemaNumber::Real(2.0).to_exact(), SemaNumber::Integer(k) if k == BigInt::from(2))
897 );
898 assert!(matches!(SemaNumber::from_i64(5), SemaNumber::Integer(k) if k == BigInt::from(5)));
900 assert!(matches!(SemaNumber::from_f64(1.5), SemaNumber::Real(f) if f == 1.5));
901 }
902
903 #[test]
904 fn parse_literals() {
905 let big =
907 SemaNumber::parse_int_radix("170141183460469231731687303715884105728", 10).unwrap();
908 assert!(matches!(big, SemaNumber::Integer(_)));
909 assert!(matches!(SemaNumber::parse_int_radix("ff", 16).unwrap(),
911 SemaNumber::Integer(n) if n == BigInt::from(255)));
912 assert!(matches!(SemaNumber::parse_int_radix("-101", 2).unwrap(),
913 SemaNumber::Integer(n) if n == BigInt::from(-5)));
914 assert!(matches!(SemaNumber::parse_rational("22/7").unwrap(),
916 SemaNumber::Rational(r) if r == BigRational::new(BigInt::from(22), BigInt::from(7))));
917 assert!(
919 matches!(SemaNumber::parse_rational("6/3").unwrap(), SemaNumber::Integer(n) if n == BigInt::from(2))
920 );
921 assert!(SemaNumber::parse_rational("1/0").is_none()); assert!(SemaNumber::parse_int_radix("xyz", 16).is_none());
924 }
925
926 #[test]
927 fn rounding_preserves_exactness() {
928 let r = |n: i64, d: i64| {
929 SemaNumber::Rational(BigRational::new(BigInt::from(n), BigInt::from(d)))
930 };
931 assert!(matches!(r(7, 2).floor(), SemaNumber::Integer(n) if n == BigInt::from(3)));
933 assert!(matches!(r(7, 2).ceil(), SemaNumber::Integer(n) if n == BigInt::from(4)));
934 assert!(matches!(r(7, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(4))); assert!(matches!(r(5, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(2))); assert!(matches!(r(-5, 2).round(), SemaNumber::Integer(n) if n == BigInt::from(-2))); assert!(matches!(r(-7, 2).truncate(), SemaNumber::Integer(n) if n == BigInt::from(-3)));
939 assert!(
941 matches!(SemaNumber::Integer(BigInt::from(5)).floor(), SemaNumber::Integer(n) if n == BigInt::from(5))
942 );
943 assert!(matches!(SemaNumber::Real(2.5).floor(), SemaNumber::Real(f) if f == 2.0));
945 assert!(matches!(SemaNumber::Real(2.5).round(), SemaNumber::Real(f) if f == 2.0));
946 }
947
948 #[test]
949 fn structural_traits() {
950 use std::collections::HashSet;
951 let a = SemaNumber::Integer(BigInt::from(3));
952 let b = SemaNumber::Integer(BigInt::from(3));
953 assert_eq!(a, b);
954 let mut set = HashSet::new();
955 set.insert(SemaNumber::Real(1.5));
956 assert!(set.contains(&SemaNumber::Real(1.5)));
957 assert!(SemaNumber::Integer(BigInt::from(1)) < SemaNumber::Real(0.0)); }
960
961 #[test]
962 fn rationalize_simplest_in_interval() {
963 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
964 let r = |a: i64, b: i64| {
965 SemaNumber::Rational(BigRational::new(BigInt::from(a), BigInt::from(b)))
966 };
967 assert_eq!(r(1, 3).rationalize(&r(1, 100)), r(1, 3));
969 assert_eq!(r(3, 10).rationalize(&r(1, 10)), r(1, 3));
971 assert!(r(1, 3).rationalize(&r(1, 100)).is_exact());
973 assert_eq!(n(5).rationalize(&n(4)), n(1));
975 assert_eq!(n(3).rationalize(&n(5)), n(0));
977 assert!(!SemaNumber::Real(0.3).rationalize(&r(1, 10)).is_exact());
979 assert_eq!(r(-3, 10).rationalize(&r(1, 10)), r(-1, 3));
981 }
982
983 #[test]
984 fn powi_repeated_squaring() {
985 let n = |v: i64| SemaNumber::Integer(BigInt::from(v));
986 let expected: BigInt = "1267650600228229401496703205376".parse().unwrap();
988 assert!(
989 matches!(n(2).powi(&BigInt::from(100)).unwrap(), SemaNumber::Integer(v) if v == expected)
990 );
991 assert!(matches!(n(2).powi(&BigInt::from(-3)).unwrap(),
993 SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(8))));
994 let half = SemaNumber::Rational(BigRational::new(BigInt::one(), BigInt::from(2)));
996 assert!(matches!(half.powi(&BigInt::from(3)).unwrap(),
997 SemaNumber::Rational(r) if r == BigRational::new(BigInt::one(), BigInt::from(8))));
998 assert!(
1000 matches!(n(0).powi(&BigInt::from(0)).unwrap(), SemaNumber::Integer(v) if v == BigInt::one())
1001 );
1002 assert!(n(0).powi(&BigInt::from(-1)).is_err());
1004 }
1005}