1#![forbid(unsafe_code)]
2pub use dashu_base::Sign;
11
12pub use dashu_base::{
14 Abs, AbsOrd, BitTest, CubicRoot, DivEuclid, DivRem, DivRemAssign, DivRemEuclid, EstimatedLog2,
15 ExtendedGcd, Gcd, Inverse, PowerOfTwo, RemEuclid, Signed, SquareRoot, UnsignedAbs,
16};
17
18pub type OxiNumResult<T> = Result<T, OxiNumError>;
24
25#[non_exhaustive]
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum OxiNumError {
30 Parse(std::borrow::Cow<'static, str>),
32 Precision(std::borrow::Cow<'static, str>),
34 DivByZero,
36 Overflow(std::borrow::Cow<'static, str>),
38 InvalidRadix(u32),
40 Domain(std::borrow::Cow<'static, str>),
42}
43
44impl OxiNumError {
45 #[must_use]
68 pub fn context(self, ctx: impl AsRef<str>) -> Self {
69 let ctx = ctx.as_ref();
70 match self {
71 Self::Parse(s) => Self::Parse(format!("{ctx}: {s}").into()),
72 Self::Precision(s) => Self::Precision(format!("{ctx}: {s}").into()),
73 Self::Overflow(s) => Self::Overflow(format!("{ctx}: {s}").into()),
74 Self::Domain(s) => Self::Domain(format!("{ctx}: {s}").into()),
75 other => other,
76 }
77 }
78}
79
80impl std::fmt::Display for OxiNumError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::Parse(s) => write!(f, "parse error: {s}"),
84 Self::Precision(s) => write!(f, "precision error: {s}"),
85 Self::DivByZero => write!(f, "division by zero"),
86 Self::Overflow(s) => write!(f, "overflow: {s}"),
87 Self::InvalidRadix(r) => write!(f, "invalid radix: {r} (must be 2..=36)"),
88 Self::Domain(s) => write!(f, "domain error: {s}"),
89 }
90 }
91}
92
93impl std::error::Error for OxiNumError {}
94
95impl From<OxiNumError> for std::io::Error {
96 fn from(e: OxiNumError) -> Self {
97 std::io::Error::other(e.to_string())
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131pub struct ParseNumberError {
132 pub message: String,
134 pub line: usize,
136 pub column: usize,
138}
139
140impl ParseNumberError {
141 pub fn new(message: impl Into<String>, line: usize, column: usize) -> Self {
144 Self {
145 message: message.into(),
146 line,
147 column,
148 }
149 }
150}
151
152impl std::fmt::Display for ParseNumberError {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "parse error at line {line}, column {column}: {message}",
157 line = self.line,
158 column = self.column,
159 message = self.message,
160 )
161 }
162}
163
164impl std::error::Error for ParseNumberError {}
165
166impl From<ParseNumberError> for OxiNumError {
167 fn from(e: ParseNumberError) -> Self {
168 let ParseNumberError {
169 message,
170 line,
171 column,
172 } = e;
173 OxiNumError::Parse(format!("{message} (line {line}, col {column})").into())
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187pub enum RoundingMode {
188 Up,
190 Down,
192 Ceiling,
194 Floor,
196 HalfUp,
198 HalfDown,
200 HalfEven,
202 Unnecessary,
204}
205
206impl std::fmt::Display for RoundingMode {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 let name = match self {
209 Self::Up => "Up",
210 Self::Down => "Down",
211 Self::Ceiling => "Ceiling",
212 Self::Floor => "Floor",
213 Self::HalfUp => "HalfUp",
214 Self::HalfDown => "HalfDown",
215 Self::HalfEven => "HalfEven",
216 Self::Unnecessary => "Unnecessary",
217 };
218 f.write_str(name)
219 }
220}
221
222pub trait OxiNum: std::fmt::Display + std::fmt::Debug + Clone + PartialEq {
230 fn is_zero(&self) -> bool;
232
233 fn is_one(&self) -> bool;
235}
236
237pub trait OxiSigned: OxiNum {
239 fn signum(&self) -> Sign;
241
242 fn abs(&self) -> Self;
244
245 fn is_negative(&self) -> bool {
247 self.signum() == Sign::Negative
248 }
249
250 fn is_positive(&self) -> bool {
252 !self.is_zero() && self.signum() == Sign::Positive
253 }
254}
255
256pub trait OxiUnsigned: OxiNum {}
258
259pub trait FromRadix: Sized {
265 fn from_radix(src: &str, radix: u32) -> OxiNumResult<Self>;
272}
273
274pub trait ToRadix {
276 fn to_radix(&self, radix: u32) -> OxiNumResult<String>;
282}
283
284pub trait Pow<Exp> {
290 type Output;
292
293 fn pow(&self, exp: Exp) -> Self::Output;
295}
296
297pub trait Roots {
299 fn sqrt(&self) -> Self;
301
302 fn cbrt(&self) -> Self;
304
305 fn nth_root(&self, n: u32) -> Self;
307}
308
309pub trait ModularArithmetic {
315 fn mod_add(&self, rhs: &Self, modulus: &Self) -> Self;
317
318 fn mod_sub(&self, rhs: &Self, modulus: &Self) -> Self;
320
321 fn mod_mul(&self, rhs: &Self, modulus: &Self) -> Self;
323
324 fn mod_pow(&self, exp: &Self, modulus: &Self) -> Self;
326}
327
328pub trait Primality {
334 fn is_probably_prime(&self, witnesses: u32) -> bool;
340
341 fn next_prime(&self) -> Self;
343}
344
345#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn error_display_parse() {
355 let e = OxiNumError::Parse("bad input".into());
356 assert!(e.to_string().contains("bad input"));
357 }
358
359 #[test]
360 fn error_display_precision() {
361 let e = OxiNumError::Precision("too low".into());
362 assert!(e.to_string().contains("too low"));
363 }
364
365 #[test]
366 fn error_display_div_by_zero() {
367 let e = OxiNumError::DivByZero;
368 assert_eq!(e.to_string(), "division by zero");
369 }
370
371 #[test]
372 fn error_display_overflow() {
373 let e = OxiNumError::Overflow("u64 max exceeded".into());
374 assert!(e.to_string().contains("u64 max exceeded"));
375 }
376
377 #[test]
378 fn error_display_invalid_radix() {
379 let e = OxiNumError::InvalidRadix(42);
380 assert!(e.to_string().contains("42"));
381 assert!(e.to_string().contains("must be 2..=36"));
382 }
383
384 #[test]
385 fn error_display_domain() {
386 let e = OxiNumError::Domain("sqrt of negative is undefined for real BigFloat".into());
387 assert_eq!(
388 e.to_string(),
389 "domain error: sqrt of negative is undefined for real BigFloat"
390 );
391 }
392
393 #[test]
394 fn context_prefixes_domain_message() {
395 let e = OxiNumError::Domain("sqrt of negative".into()).context("BigFloat::sqrt");
396 match &e {
397 OxiNumError::Domain(s) => assert_eq!(s, "BigFloat::sqrt: sqrt of negative"),
398 other => panic!("expected Domain, got {other:?}"),
399 }
400 assert!(e.to_string().contains("domain error:"));
401 assert!(e.to_string().contains("BigFloat::sqrt:"));
402 }
403
404 #[test]
405 fn error_into_io_error() {
406 let e = OxiNumError::DivByZero;
407 let io_err: std::io::Error = e.into();
408 assert_eq!(io_err.kind(), std::io::ErrorKind::Other);
409 assert!(io_err.to_string().contains("division by zero"));
410 }
411
412 #[test]
413 fn sign_positive() {
414 let s = Sign::Positive;
415 assert_eq!(s, Sign::Positive);
416 }
417
418 #[test]
419 fn rounding_mode_display() {
420 assert_eq!(RoundingMode::HalfEven.to_string(), "HalfEven");
421 assert_eq!(RoundingMode::Up.to_string(), "Up");
422 assert_eq!(RoundingMode::Unnecessary.to_string(), "Unnecessary");
423 }
424
425 #[test]
426 fn rounding_mode_equality() {
427 assert_eq!(RoundingMode::Floor, RoundingMode::Floor);
428 assert_ne!(RoundingMode::Up, RoundingMode::Down);
429 }
430
431 #[test]
432 fn error_is_send_sync() {
433 fn assert_send_sync<T: Send + Sync>() {}
434 assert_send_sync::<OxiNumError>();
435 }
436
437 #[test]
438 fn error_size_is_small() {
439 let size = std::mem::size_of::<OxiNumError>();
441 assert!(size <= 32, "OxiNumError is {size} bytes, expected <= 32");
442 }
443
444 #[test]
445 fn oxinumerror_implements_std_error() {
446 fn assert_error<E: std::error::Error>(_: &E) {}
447 let e = OxiNumError::DivByZero;
448 assert_error(&e);
449 let _boxed: Box<dyn std::error::Error> = Box::new(OxiNumError::DivByZero);
451 }
452
453 #[test]
454 fn error_is_std_error() {
455 let e: Box<dyn std::error::Error> = Box::new(OxiNumError::Parse("test".into()));
456 assert!(e.to_string().contains("test"));
457 }
458
459 #[test]
460 fn oxi_num_result_alias() {
461 let ok: OxiNumResult<u32> = Ok(42);
462 assert_eq!(ok, Ok(42));
463 let err: OxiNumResult<u32> = Err(OxiNumError::DivByZero);
464 assert!(err.is_err());
465 }
466
467 #[test]
472 fn parse_number_error_constructs_and_displays() {
473 let pe = ParseNumberError::new("bad digit", 3, 7);
474 assert_eq!(pe.message, "bad digit");
475 assert_eq!(pe.line, 3);
476 assert_eq!(pe.column, 7);
477
478 let pe_disp = pe.to_string();
480 assert!(pe_disp.contains("line 3"), "got {pe_disp}");
481 assert!(pe_disp.contains("column 7"), "got {pe_disp}");
482 assert!(pe_disp.contains("bad digit"), "got {pe_disp}");
483
484 let oe: OxiNumError = pe.into();
486 match &oe {
487 OxiNumError::Parse(_) => {}
488 other => panic!("expected Parse, got {other:?}"),
489 }
490 let oe_disp = oe.to_string();
491 assert!(oe_disp.contains("bad digit"), "got {oe_disp}");
492 assert!(oe_disp.contains("line 3"), "got {oe_disp}");
494 assert!(oe_disp.contains("col 7"), "got {oe_disp}");
495 }
496
497 #[test]
498 fn parse_number_error_is_std_error() {
499 let boxed: Box<dyn std::error::Error> = Box::new(ParseNumberError::new("oops", 1, 1));
500 assert!(boxed.to_string().contains("oops"));
501 }
502
503 #[test]
504 fn context_prefixes_message_variants() {
505 let parse = OxiNumError::Parse("x".into()).context("at A");
506 match parse {
507 OxiNumError::Parse(ref s) => assert_eq!(s, "at A: x"),
508 other => panic!("expected Parse, got {other:?}"),
509 }
510 assert!(parse.to_string().contains("at A:"));
511
512 let precision = OxiNumError::Precision("y".into()).context("at B");
513 match precision {
514 OxiNumError::Precision(ref s) => assert_eq!(s, "at B: y"),
515 other => panic!("expected Precision, got {other:?}"),
516 }
517
518 let overflow = OxiNumError::Overflow("z".into()).context("at C");
519 match overflow {
520 OxiNumError::Overflow(ref s) => assert_eq!(s, "at C: z"),
521 other => panic!("expected Overflow, got {other:?}"),
522 }
523 }
524
525 #[test]
526 fn context_leaves_kindful_variants_unchanged() {
527 assert_eq!(
528 OxiNumError::DivByZero.context("ignored"),
529 OxiNumError::DivByZero,
530 );
531 assert_eq!(
532 OxiNumError::InvalidRadix(37).context("ignored"),
533 OxiNumError::InvalidRadix(37),
534 );
535 }
536
537 #[test]
538 fn existing_size_of_oxinumerror_unchanged() {
539 let size = std::mem::size_of::<OxiNumError>();
542 assert!(size <= 32, "OxiNumError is {size} bytes, expected <= 32");
543 }
544}
545
546#[cfg(test)]
547mod proptests {
548 use super::*;
549 use proptest::prelude::*;
550
551 proptest! {
552 #[test]
553 fn parse_display_roundtrip(s in any::<String>()) {
554 let e = OxiNumError::Parse(s.clone().into());
555 prop_assert!(e.to_string().contains(&s));
556 }
557
558 #[test]
559 fn precision_display_roundtrip(s in any::<String>()) {
560 let e = OxiNumError::Precision(s.clone().into());
561 prop_assert!(e.to_string().contains(&s));
562 }
563
564 #[test]
565 fn overflow_display_roundtrip(s in any::<String>()) {
566 let e = OxiNumError::Overflow(s.clone().into());
567 prop_assert!(e.to_string().contains(&s));
568 }
569
570 #[test]
571 fn domain_display_roundtrip(s in any::<String>()) {
572 let e = OxiNumError::Domain(s.clone().into());
573 prop_assert!(e.to_string().contains(&s));
574 }
575
576 #[test]
577 fn parse_number_error_display_roundtrip(
578 msg in any::<String>(),
579 line in 1usize..=10_000,
580 column in 1usize..=10_000,
581 ) {
582 let pe = ParseNumberError::new(msg.clone(), line, column);
583 let disp = pe.to_string();
584 prop_assert!(disp.contains(&msg));
585 prop_assert!(disp.contains(&line.to_string()));
586 prop_assert!(disp.contains(&column.to_string()));
587 }
588 }
589}
590
591#[cfg(all(test, feature = "serde"))]
592mod serde_tests {
593 use super::*;
594
595 fn roundtrip_oxi(original: OxiNumError) {
596 let json = serde_json::to_string(&original).expect("serialize OxiNumError");
597 let back: OxiNumError = serde_json::from_str(&json).expect("deserialize OxiNumError");
598 assert_eq!(back, original, "round-trip mismatch for {original:?}");
599 }
600
601 fn roundtrip_rounding(original: RoundingMode) {
602 let json = serde_json::to_string(&original).expect("serialize RoundingMode");
603 let back: RoundingMode = serde_json::from_str(&json).expect("deserialize RoundingMode");
604 assert_eq!(back, original, "round-trip mismatch for {original:?}");
605 }
606
607 #[test]
608 fn oxinum_error_json_roundtrip_all_variants() {
609 roundtrip_oxi(OxiNumError::Parse("e".into()));
610 roundtrip_oxi(OxiNumError::Precision("p".into()));
611 roundtrip_oxi(OxiNumError::DivByZero);
612 roundtrip_oxi(OxiNumError::Overflow("o".into()));
613 roundtrip_oxi(OxiNumError::InvalidRadix(3));
614 roundtrip_oxi(OxiNumError::Domain("sqrt of negative".into()));
615 }
616
617 #[test]
618 fn rounding_mode_json_roundtrip_all_variants() {
619 roundtrip_rounding(RoundingMode::Up);
620 roundtrip_rounding(RoundingMode::Down);
621 roundtrip_rounding(RoundingMode::Ceiling);
622 roundtrip_rounding(RoundingMode::Floor);
623 roundtrip_rounding(RoundingMode::HalfUp);
624 roundtrip_rounding(RoundingMode::HalfDown);
625 roundtrip_rounding(RoundingMode::HalfEven);
626 roundtrip_rounding(RoundingMode::Unnecessary);
627 }
628
629 #[test]
630 fn parse_number_error_json_roundtrip() {
631 let pe = ParseNumberError::new("bad", 4, 9);
632 let json = serde_json::to_string(&pe).expect("serialize ParseNumberError");
633 let back: ParseNumberError =
634 serde_json::from_str(&json).expect("deserialize ParseNumberError");
635 assert_eq!(back, pe);
636 }
637}