#![forbid(unsafe_code)]
use std::str::FromStr;
use oxinum_float::{compute_e, compute_pi, ln, pow, sqrt, DBig, OxiNumError};
use dashu_float::FBig;
use proptest::prelude::*;
#[test]
fn midpoint_two_point_five_halfeven_to_two() {
let v = DBig::from_str("2.5").expect("parse 2.5");
assert_eq!(v.precision(), 2);
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "2", "HalfEven(2.5) should be 2");
}
#[test]
fn midpoint_two_point_five_halfaway_to_three() {
let v = DBig::from_str("2.5").expect("parse 2.5");
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "3", "HalfAway(2.5) should be 3");
}
#[test]
fn midpoint_four_point_five_halfeven_to_four() {
let v = DBig::from_str("4.5").expect("parse 4.5");
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "4", "HalfEven(4.5) should be 4");
}
#[test]
fn midpoint_four_point_five_halfaway_to_five() {
let v = DBig::from_str("4.5").expect("parse 4.5");
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "5", "HalfAway(4.5) should be 5");
}
#[test]
fn midpoint_six_point_five_halfeven_to_six() {
let v = DBig::from_str("6.5").expect("parse 6.5");
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "6", "HalfEven(6.5) should be 6");
}
#[test]
fn midpoint_six_point_five_halfaway_to_seven() {
let v = DBig::from_str("6.5").expect("parse 6.5");
let rounded = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(rounded.to_string(), "7", "HalfAway(6.5) should be 7");
}
#[test]
fn midpoint_zero_point_two_five_diverges() {
let v = DBig::from_str("0.25").expect("parse 0.25");
let he = v
.clone()
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
let ha = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(he.to_string(), "0.2", "HalfEven(0.25) -> 0.2");
assert_eq!(ha.to_string(), "0.3", "HalfAway(0.25) -> 0.3");
}
#[test]
fn midpoint_zero_point_four_five_diverges() {
let v = DBig::from_str("0.45").expect("parse 0.45");
let he = v
.clone()
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
let ha = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(he.to_string(), "0.4", "HalfEven(0.45) -> 0.4");
assert_eq!(ha.to_string(), "0.5", "HalfAway(0.45) -> 0.5");
}
#[test]
fn midpoint_negative_two_point_five_diverges() {
let v = DBig::from_str("-2.5").expect("parse -2.5");
let he = v
.clone()
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
let ha = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(he.to_string(), "-2", "HalfEven(-2.5) -> -2");
assert_eq!(ha.to_string(), "-3", "HalfAway(-2.5) -> -3");
}
#[test]
fn midpoint_one_point_five_both_modes_agree() {
let v = DBig::from_str("1.5").expect("parse 1.5");
let he = v
.clone()
.with_rounding::<dashu_float::round::mode::HalfEven>()
.with_precision(1)
.value();
let ha = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
assert_eq!(he.to_string(), "2");
assert_eq!(ha.to_string(), "2");
}
#[test]
fn midpoint_binary_fbig_halfeven_chooses_even() {
let v =
FBig::<dashu_float::round::mode::HalfEven, 2>::from_str("0.1B0").expect("parse binary 0.5");
let he = v.clone().with_precision(1).value();
let ha = v
.with_rounding::<dashu_float::round::mode::HalfAway>()
.with_precision(1)
.value();
let _ = (he.to_string(), ha.to_string());
}
fn round_trip_seeds() -> &'static [&'static str] {
&[
"0.0",
"1",
"-1",
"3.14159",
"-2.71828",
"1e-50",
"1e50",
"12345.6789",
"0.000001",
"-987654.321",
]
}
#[test]
fn round_trip_seeds_at_precisions() {
let precisions: [usize; 4] = [5, 10, 20, 50];
for seed in round_trip_seeds() {
let parsed =
DBig::from_str(seed).unwrap_or_else(|e| panic!("seed {seed:?} should parse: {e}"));
for &p in &precisions {
let canonical = parsed.clone().with_precision(p).value();
let displayed = canonical.to_string();
let reparsed =
DBig::from_str(&displayed).unwrap_or_else(|e| panic!("reparse {displayed:?}: {e}"));
let reparsed_canonical = reparsed.with_precision(p).value();
assert_eq!(
canonical.to_string(),
reparsed_canonical.to_string(),
"round-trip mismatch for seed {seed:?} @ precision {p}: \
display={displayed:?}"
);
}
}
}
#[test]
fn round_trip_high_precision_constants() {
for p in &[10usize, 30, 100] {
let pi = compute_pi(*p);
let pi_str = pi.to_string();
let reparsed = DBig::from_str(&pi_str).expect("reparse pi should succeed");
assert_eq!(
pi.to_string(),
reparsed.to_string(),
"pi round-trip @ precision {p}"
);
let e = compute_e(*p);
let e_str = e.to_string();
let re = DBig::from_str(&e_str).expect("reparse e should succeed");
assert_eq!(
e.to_string(),
re.to_string(),
"e round-trip @ precision {p}"
);
}
}
mod special_values_scoped {
use super::*;
#[test]
fn parsing_inf_is_an_error() {
assert!(DBig::from_str("inf").is_err());
assert!(DBig::from_str("Inf").is_err());
assert!(DBig::from_str("-inf").is_err());
assert!(DBig::from_str("Infinity").is_err());
}
#[test]
fn parsing_nan_is_an_error() {
assert!(DBig::from_str("nan").is_err());
assert!(DBig::from_str("NaN").is_err());
assert!(DBig::from_str("NAN").is_err());
}
#[test]
fn ln_of_zero_returns_precision_error() {
let zero = DBig::from_str("0.0").expect("parse 0.0");
match ln(&zero, 20) {
Err(OxiNumError::Precision(_)) => {}
other => panic!("ln(0) should be Precision error; got {other:?}"),
}
}
#[test]
fn ln_of_negative_returns_precision_error() {
let neg = DBig::from_str("-1.0").expect("parse -1.0");
match ln(&neg, 20) {
Err(OxiNumError::Precision(_)) => {}
other => panic!("ln(-1) should be Precision error; got {other:?}"),
}
}
#[test]
fn sqrt_of_negative_returns_precision_error() {
let neg = DBig::from_str("-1.0").expect("parse -1.0");
match sqrt(&neg, 20) {
Err(OxiNumError::Precision(_)) => {}
other => panic!("sqrt(-1) should be Precision error; got {other:?}"),
}
}
#[test]
fn pow_with_nonpositive_base_returns_precision_error() {
let zero = DBig::from_str("0.0").expect("parse 0.0");
let one = DBig::from_str("1.0").expect("parse 1.0");
match pow(&zero, &one, 20) {
Err(OxiNumError::Precision(_)) => {}
other => panic!("pow(0, 1) should be Precision error; got {other:?}"),
}
let neg = DBig::from_str("-2.0").expect("parse -2.0");
match pow(&neg, &one, 20) {
Err(OxiNumError::Precision(_)) => {}
other => panic!("pow(-2, 1) should be Precision error; got {other:?}"),
}
}
#[test]
fn zero_precision_argument_is_an_error() {
let one = DBig::from_str("1.0").expect("parse 1.0");
assert!(ln(&one, 0).is_err());
assert!(sqrt(&one, 0).is_err());
assert!(pow(&one, &one, 0).is_err());
}
#[test]
fn infinity_sentinel_is_not_exposed_through_public_api() {
let a = DBig::from_str("1e100").expect("parse 1e100");
let b = DBig::from_str("1e100").expect("parse 1e100");
let sum = &a + &b;
let s = sum.to_string();
assert!(
!s.to_lowercase().contains("inf") && !s.to_lowercase().contains("nan"),
"sum should be a finite decimal, got {s:?}"
);
}
}
fn pool() -> Vec<&'static str> {
vec![
"0.0",
"1.0",
"-1.0",
"0.1",
"-3.7",
"42.0",
"0.000001",
"-987654.321",
"3.141592653589793",
"-2.718281828459045",
"1.0e10",
"-1.0e-10",
"0.5",
"-0.25",
"12345.6789",
]
}
fn lit() -> impl Strategy<Value = DBig> {
let p = pool();
let len = p.len();
(0usize..len).prop_map(move |i| DBig::from_str(p[i]).expect("pool literal parses"))
}
fn at_precision(x: &DBig, prec: usize) -> DBig {
x.clone().with_precision(prec).value()
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 64,
.. ProptestConfig::default()
})]
#[test]
fn addition_commutativity(a in lit(), b in lit()) {
let ab = at_precision(&(&a + &b), 30);
let ba = at_precision(&(&b + &a), 30);
prop_assert_eq!(ab.to_string(), ba.to_string());
}
#[test]
fn multiplication_commutativity(a in lit(), b in lit()) {
let ab = at_precision(&(&a * &b), 30);
let ba = at_precision(&(&b * &a), 30);
prop_assert_eq!(ab.to_string(), ba.to_string());
}
#[test]
fn addition_near_associativity(
a in lit(), b in lit(), c in lit(),
) {
let ap = at_precision(&a, 30);
let bp = at_precision(&b, 30);
let cp = at_precision(&c, 30);
let lhs = at_precision(&(&(&ap + &bp) + &cp), 30);
let rhs = at_precision(&(&ap + &(&bp + &cp)), 30);
let diff = &lhs - &rhs;
let bound = DBig::from_str("1e-8").expect("parse 1e-8");
let neg_bound = DBig::from_str("-1e-8").expect("parse -1e-8");
prop_assert!(
diff <= bound && diff >= neg_bound,
"associativity diff = {} (a={}, b={}, c={})",
diff, a, b, c
);
}
#[test]
fn sqrt_of_square_is_abs(x in lit()) {
let xp = at_precision(&x, 30);
let xx = &xp * &xp;
let s = sqrt(&xx, 30).expect("sqrt(x*x) should succeed for finite x");
let abs_str = xp.to_string();
let abs = if let Some(stripped) = abs_str.strip_prefix('-') {
DBig::from_str(stripped).expect("abs string reparses")
} else {
xp.clone()
};
let abs_p = at_precision(&abs, 25);
let s_p = at_precision(&s, 25);
let diff = &abs_p - &s_p;
let bound = DBig::from_str("1e-8").expect("parse 1e-8");
let neg_bound = DBig::from_str("-1e-8").expect("parse -1e-8");
prop_assert!(
diff <= bound && diff >= neg_bound,
"sqrt(x*x) - |x| = {} for x = {}",
diff, x
);
}
}
#[test]
fn cross_validate_addition_matches_dashu() {
let oa = DBig::from_str("3.14").expect("parse 3.14");
let ob = DBig::from_str("2.71").expect("parse 2.71");
let osum = at_precision(&(&oa + &ob), 10);
let da = dashu_float::DBig::from_str("3.14").expect("dashu parse 3.14");
let db = dashu_float::DBig::from_str("2.71").expect("dashu parse 2.71");
let dsum = (&da + &db).with_precision(10).value();
assert_eq!(osum.to_string(), dsum.to_string());
}
#[test]
fn cross_validate_multiplication_matches_dashu() {
let oa = DBig::from_str("12345.6789").expect("parse 12345.6789");
let ob = DBig::from_str("0.0001").expect("parse 0.0001");
let oprod = at_precision(&(&oa * &ob), 20);
let da = dashu_float::DBig::from_str("12345.6789").expect("dashu parse");
let db = dashu_float::DBig::from_str("0.0001").expect("dashu parse");
let dprod = (&da * &db).with_precision(20).value();
assert_eq!(oprod.to_string(), dprod.to_string());
}
#[test]
fn cross_validate_subtraction_matches_dashu() {
let oa = DBig::from_str("1000.0").expect("parse 1000.0");
let ob = DBig::from_str("0.001").expect("parse 0.001");
let odiff = at_precision(&(&oa - &ob), 30);
let da = dashu_float::DBig::from_str("1000.0").expect("dashu parse");
let db = dashu_float::DBig::from_str("0.001").expect("dashu parse");
let ddiff = (&da - &db).with_precision(30).value();
assert_eq!(odiff.to_string(), ddiff.to_string());
}
#[test]
fn cross_validate_division_matches_dashu() {
let oa = DBig::from_str("1.0").expect("parse 1.0");
let ob = DBig::from_str("3.0").expect("parse 3.0");
let oquot = at_precision(&oa, 30) / at_precision(&ob, 30);
let da = dashu_float::DBig::from_str("1.0").expect("dashu parse");
let db = dashu_float::DBig::from_str("3.0").expect("dashu parse");
let dquot = da.with_precision(30).value() / db.with_precision(30).value();
assert_eq!(oquot.to_string(), dquot.to_string());
}
#[test]
fn cross_validate_sqrt_wrapper_matches_dashu_root() {
let two = DBig::from_str("2.0").expect("parse 2.0");
let wrap = sqrt(&two, 20).expect("sqrt(2) via wrapper");
let s = wrap.to_string();
assert!(
s.starts_with("1.4142135623730950488"),
"sqrt(2) leading digits unexpected: got {s}"
);
}