fix(numtower): past-K oracle and weighted NumSum properties, negative-power scale, ExactSum value equality (improvise-59nw.1)
Answers the BLOCK on 302594bf (B1-B3) and its should-fix items: the past-K read and Z-set weights are now checked against a BigRational oracle (the review's F7, F8, F12, F14, F15 now fail by name), a negative power of a unit base has its minimal scale, ExactSum compares values, a forced n/d never reads as infinity, and Interim compares exactly and knows zero without storing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
302594bf5a
commit
34ab0fc3d3
@@ -69,6 +69,29 @@ impl Interim {
|
||||
pub fn pow(&self, o: &Interim) -> Result<Interim, DivisionByZero> {
|
||||
power(self.operand(), o.operand())
|
||||
}
|
||||
|
||||
/// Exactly zero; a wide value never is, because zero fits a stored arm.
|
||||
pub fn is_zero(&self) -> bool {
|
||||
match &self.0 {
|
||||
Inner::Stored(n) => n.is_zero(),
|
||||
Inner::Wide(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clause 6's exact order, before the stored bound; `None` only when either is NaN.
|
||||
pub fn cmp_exact(&self, o: &Interim) -> Option<std::cmp::Ordering> {
|
||||
match (&self.0, &o.0) {
|
||||
(Inner::Stored(a), Inner::Stored(b)) => a.cmp_exact(b),
|
||||
_ => match (self.operand().big(), o.operand().big()) {
|
||||
(Some(a), Some(b)) => Some(a.cmp_value(&b)),
|
||||
(Some(a), None) => crate::cmp::exact_against_f64(&a, o.operand().float()),
|
||||
(None, Some(b)) => {
|
||||
crate::cmp::exact_against_f64(&b, self.operand().float()).map(|c| c.reverse())
|
||||
}
|
||||
(None, None) => self.operand().float().partial_cmp(&o.operand().float()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Num {
|
||||
@@ -364,10 +387,13 @@ fn exact_power(a: Operand, n: &BigInt) -> Result<Interim, DivisionByZero> {
|
||||
_ => 0,
|
||||
};
|
||||
let k = n.magnitude();
|
||||
let target = k
|
||||
.to_u32()
|
||||
.and_then(|k| k.checked_mul(base_scale))
|
||||
.map_or(MAX_SCALE, |s| s.min(MAX_SCALE));
|
||||
let target = match n.is_negative() {
|
||||
true => 0,
|
||||
false => k
|
||||
.to_u32()
|
||||
.and_then(|k| k.checked_mul(base_scale))
|
||||
.map_or(MAX_SCALE, |s| s.min(MAX_SCALE)),
|
||||
};
|
||||
let unit = q.den.is_one() && q.num.magnitude().is_one();
|
||||
match (q.num.is_zero(), n.is_negative(), unit) {
|
||||
(_, _, _) if n.is_zero() => Ok(Interim::from(padded(1, 0, 0))),
|
||||
|
||||
@@ -64,6 +64,16 @@ fn mixed(exact: &Num, x: f64) -> Option<Ordering> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wide exact value against an f64, with no fast path: the slow, total form of `mixed`.
|
||||
pub(crate) fn exact_against_f64(q: &BigQ, x: f64) -> Option<Ordering> {
|
||||
match (x.is_nan(), x.is_infinite(), x > 0.0) {
|
||||
(true, _, _) => None,
|
||||
(false, true, true) => Some(Ordering::Less),
|
||||
(false, true, false) => Some(Ordering::Greater),
|
||||
(false, false, _) => Some(q.cmp_value(&BigQ::from_f64(x))),
|
||||
}
|
||||
}
|
||||
|
||||
fn pick<'a>(a: &'a Num, b: &'a Num, want: Ordering) -> &'a Num {
|
||||
match (a.cmp_exact(b), is_nan(a), is_nan(b)) {
|
||||
(_, true, _) => b,
|
||||
|
||||
@@ -19,18 +19,24 @@ pub enum Typing {
|
||||
|
||||
/// The gate: the strings `f64::from_str` accepts and reads as a finite value.
|
||||
pub fn is_number(text: &str, typing: Typing) -> bool {
|
||||
gate(text) || (typing == Typing::Forced && fraction_lexeme(text).is_some())
|
||||
gate(text) || (typing == Typing::Forced && forced_fraction(text).is_some())
|
||||
}
|
||||
|
||||
/// A number if the text is one, read exactly; `None` means the text is text.
|
||||
pub fn parse_number(text: &str, typing: Typing) -> Option<Num> {
|
||||
match (gate(text), typing) {
|
||||
(true, _) => Lexeme::read(text).map(|lx| lx.value(text)),
|
||||
(false, Typing::Forced) => fraction_lexeme(text).map(|q| Num::settle(&q)),
|
||||
(false, Typing::Forced) => forced_fraction(text),
|
||||
(false, Typing::Inferred) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A forced `n/d`; one that rounds to infinity is text, as a finite lexeme never reads as one.
|
||||
fn forced_fraction(text: &str) -> Option<Num> {
|
||||
let n = Num::settle(&fraction_lexeme(text)?);
|
||||
n.to_f64().is_finite().then_some(n)
|
||||
}
|
||||
|
||||
fn gate(text: &str) -> bool {
|
||||
text.parse::<f64>().is_ok_and(f64::is_finite)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const LIMBS: usize = 70;
|
||||
const CHUNK: u64 = 1 << 30;
|
||||
|
||||
/// An exact sum of weighted f64s, rounded once when read (ADR 009).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExactSum {
|
||||
limbs: [i64; LIMBS],
|
||||
pending: u64,
|
||||
@@ -29,6 +29,19 @@ pub struct ExactSum {
|
||||
neg_inf: i128,
|
||||
}
|
||||
|
||||
/// Equal sums, not equal representations: carries propagated, `pending` ignored.
|
||||
impl PartialEq for ExactSum {
|
||||
fn eq(&self, o: &ExactSum) -> bool {
|
||||
let (mut mine, mut theirs) = (self.limbs, o.limbs);
|
||||
propagate(&mut mine);
|
||||
propagate(&mut theirs);
|
||||
let counts = |s: &ExactSum| (s.nan, s.pos_inf, s.neg_inf);
|
||||
mine == theirs && counts(self) == counts(o)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ExactSum {}
|
||||
|
||||
impl Default for ExactSum {
|
||||
fn default() -> Self {
|
||||
ExactSum {
|
||||
|
||||
@@ -161,9 +161,30 @@ fn contagion_is_total_catches_an_integral_inexact_kept_exact() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The scale a decimal power is stored at: minimal, padded toward `min(s·k, 28)` for k > 0 only.
|
||||
fn power_scale(base: &Num, k: i32, v: &BigRational) -> u32 {
|
||||
let scaled = |s: u32| v * BigRational::from_integer(BigInt::from(10).pow(s));
|
||||
let fits_at =
|
||||
|s: u32| scaled(s).is_integer() && scaled(s).to_integer().magnitude().bits() <= 96;
|
||||
let minimal = (0..=28)
|
||||
.find(|&s| scaled(s).is_integer())
|
||||
.expect("a decimal result");
|
||||
let target = match (base.kind(), k > 0) {
|
||||
(NumRef::Exact(d), true) => (d.scale() * k as u32).min(28),
|
||||
_ => 0,
|
||||
};
|
||||
(minimal..=target.max(minimal))
|
||||
.rev()
|
||||
.find(|&s| fits_at(s))
|
||||
.unwrap_or(minimal)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pow_is_exact_for_an_exact_base_and_integer_exponent_when_it_fits() {
|
||||
run((any_num(), -6i32..=6), 256, false, |(a, k)| {
|
||||
let units =
|
||||
prop::sample::select(vec!["1.0", "-1.0", "1.000", "-1", "0.00", "2.0"]).prop_map(dec);
|
||||
let bases = prop_oneof![4 => any_num(), 1 => units];
|
||||
run((bases, -6i32..=6), 256, false, |(a, k)| {
|
||||
let r = a.pow(&Num::exact(Decimal::from(k)));
|
||||
match (q(&a).filter(|_| a.is_exact()), r) {
|
||||
(_, Err(_)) => prop_assert!(a.is_zero() && k < 0, "{a}^{k} refused"),
|
||||
@@ -179,6 +200,10 @@ fn pow_is_exact_for_an_exact_base_and_integer_exponent_when_it_fits() {
|
||||
r.is_exact() || is_nearest(r.to_f64(), &want),
|
||||
"{a}^{k} = {r}"
|
||||
);
|
||||
if let NumRef::Exact(d) = r.kind() {
|
||||
let scale = power_scale(&a, k, &want);
|
||||
prop_assert_eq!(d.scale(), scale, "power scale: {}^{} = {}", a, k, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -186,6 +211,18 @@ fn pow_is_exact_for_an_exact_base_and_integer_exponent_when_it_fits() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_power_has_its_minimal_scale() {
|
||||
let at = |base: &str, k: i64| dec(base).pow(&Num::exact(Decimal::from(k))).unwrap();
|
||||
assert!(same(&at("1.0", -2), &dec("1")), "{}", at("1.0", -2));
|
||||
assert!(same(&at("-1.0", -3), &dec("-1")), "{}", at("-1.0", -3));
|
||||
assert!(same(&at("2.0", -2), &dec("0.25")));
|
||||
assert!(
|
||||
same(&at("-1.0", 3), &dec("-1.000")),
|
||||
"a positive power keeps s·n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pow_decides_its_size_before_computing_and_honours_the_ceiling() {
|
||||
let third = Num::ratio(1, 3).unwrap();
|
||||
@@ -556,21 +593,75 @@ fn term() -> impl Strategy<Value = Num> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Z-set weights: zero, unit, small, wide and both extremes (never `i64::MIN`, so it negates).
|
||||
fn weight() -> impl Strategy<Value = i64> {
|
||||
prop_oneof![
|
||||
3 => Just(1i64),
|
||||
1 => Just(-1i64),
|
||||
1 => Just(0i64),
|
||||
2 => -3i64..=3,
|
||||
2 => (-(1i64 << 40))..(1i64 << 40),
|
||||
1 => prop_oneof![Just(i64::MAX), Just(-i64::MAX), Just((1i64 << 30) - 1)],
|
||||
]
|
||||
}
|
||||
|
||||
/// Many non-decimal denominators, so a list often holds more than K groups, several terms each.
|
||||
const POOL: [i128; 14] = [
|
||||
3,
|
||||
7,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
17,
|
||||
19,
|
||||
23,
|
||||
29,
|
||||
31,
|
||||
37,
|
||||
49,
|
||||
1 << 40,
|
||||
3 * (1 << 33),
|
||||
];
|
||||
|
||||
fn pooled_term() -> impl Strategy<Value = Num> {
|
||||
let pooled = (-1000i128..1000, prop::sample::select(POOL.to_vec()))
|
||||
.prop_map(|(n, d)| Num::ratio(n, d).unwrap());
|
||||
prop_oneof![
|
||||
3 => decimal(),
|
||||
4 => pooled,
|
||||
1 => fraction(),
|
||||
2 => finite_f64().prop_map(Num::inexact),
|
||||
]
|
||||
}
|
||||
|
||||
fn weighted(terms: impl Strategy<Value = Num>) -> impl Strategy<Value = (Num, i64)> {
|
||||
(terms, weight())
|
||||
}
|
||||
|
||||
fn sum_all(xs: &[Num]) -> Num {
|
||||
let mut s = NumSum::new();
|
||||
xs.iter().for_each(|x| s.insert(x));
|
||||
s.read()
|
||||
}
|
||||
|
||||
fn check_order_free(sum: &dyn Fn(&[Num]) -> Num, deterministic: bool) -> Result<(), String> {
|
||||
fn sum_weighted(xs: &[(Num, i64)]) -> Num {
|
||||
let mut s = NumSum::new();
|
||||
xs.iter().for_each(|(x, w)| s.add(x, *w));
|
||||
s.read()
|
||||
}
|
||||
|
||||
type WeightedSum<'a> = &'a dyn Fn(&[(Num, i64)]) -> Num;
|
||||
|
||||
fn check_order_free(sum: WeightedSum, deterministic: bool) -> Result<(), String> {
|
||||
let seed = vec![
|
||||
dec("0.30000000000000004"),
|
||||
dec("1000000000000"),
|
||||
dec("-1000000000000"),
|
||||
(dec("0.30000000000000004"), 1),
|
||||
(dec("1000000000000"), 1),
|
||||
(dec("-1000000000000"), 1),
|
||||
];
|
||||
let cases = prop_oneof![
|
||||
1 => Just(seed),
|
||||
4 => prop::collection::vec(term(), 0..24),
|
||||
4 => prop::collection::vec(weighted(term()), 0..24),
|
||||
2 => prop::collection::vec(weighted(pooled_term()), 0..40),
|
||||
];
|
||||
let shuffled = cases.prop_flat_map(|xs| (Just(xs.clone()), Just(xs).prop_shuffle()));
|
||||
run(shuffled, 256, deterministic, |(xs, ys)| {
|
||||
@@ -582,7 +673,7 @@ fn check_order_free(sum: &dyn Fn(&[Num]) -> Num, deterministic: bool) -> Result<
|
||||
|
||||
#[test]
|
||||
fn the_accumulator_is_independent_of_order() {
|
||||
check_order_free(&sum_all, false).unwrap();
|
||||
check_order_free(&sum_weighted, false).unwrap();
|
||||
let seed = sum_all(&[
|
||||
dec("0.30000000000000004"),
|
||||
dec("1000000000000"),
|
||||
@@ -592,13 +683,14 @@ fn the_accumulator_is_independent_of_order() {
|
||||
}
|
||||
|
||||
/// Clause 8's rival: `Decimal`'s own `+`, demoting to f64 where it overflows.
|
||||
fn decimal_fold(xs: &[Num]) -> Num {
|
||||
xs.iter().fold(Num::exact(Decimal::ZERO), |acc, x| {
|
||||
fn decimal_fold(xs: &[(Num, i64)]) -> Num {
|
||||
xs.iter().fold(Num::exact(Decimal::ZERO), |acc, (x, w)| {
|
||||
let x = x * &Num::exact(Decimal::from(*w));
|
||||
match (acc.kind(), x.kind()) {
|
||||
(NumRef::Exact(a), NumRef::Exact(b)) => {
|
||||
a.checked_add(*b).map_or_else(|| &acc + x, Num::exact)
|
||||
a.checked_add(*b).map_or_else(|| &acc + &x, Num::exact)
|
||||
}
|
||||
_ => &acc + x,
|
||||
_ => &acc + &x,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -610,66 +702,117 @@ fn the_accumulator_is_independent_of_order_catches_decimal_addition() {
|
||||
|
||||
#[test]
|
||||
fn the_accumulator_retracts_exactly() {
|
||||
run(
|
||||
(
|
||||
prop::collection::vec(term(), 0..16),
|
||||
prop::collection::vec(term(), 0..16),
|
||||
),
|
||||
256,
|
||||
false,
|
||||
|(xs, ys)| {
|
||||
let mut s = NumSum::new();
|
||||
xs.iter().chain(&ys).for_each(|x| s.insert(x));
|
||||
ys.iter().rev().for_each(|y| s.retract(y));
|
||||
let fresh = sum_all(&xs);
|
||||
prop_assert!(
|
||||
same(&s.read(), &fresh),
|
||||
"{} after retraction, {} fresh",
|
||||
s.read(),
|
||||
fresh
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
let terms = || prop::collection::vec(weighted(pooled_term()), 0..20);
|
||||
run((terms(), terms()), 256, false, |(xs, ys)| {
|
||||
let mut s = NumSum::new();
|
||||
xs.iter().chain(&ys).for_each(|(x, w)| s.add(x, *w));
|
||||
ys.iter().rev().for_each(|(y, w)| s.add(y, -w));
|
||||
let fresh = sum_weighted(&xs);
|
||||
prop_assert!(
|
||||
same(&s.read(), &fresh),
|
||||
"{} after retraction, {} fresh",
|
||||
s.read(),
|
||||
fresh
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// The oracle's partition of a weighted list, exactly as clause 8 and option (d) state it.
|
||||
struct Expected {
|
||||
decimal_part: BigRational,
|
||||
groups: std::collections::BTreeMap<i128, BigRational>,
|
||||
inexact: std::collections::BTreeMap<u64, i128>,
|
||||
inexact_total: BigRational,
|
||||
}
|
||||
|
||||
fn ratio_of(i: i128) -> BigRational {
|
||||
BigRational::from_integer(BigInt::from(i))
|
||||
}
|
||||
|
||||
fn rounded(v: &BigRational) -> BigRational {
|
||||
BigRational::from_float(v.to_f64().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
impl Expected {
|
||||
fn of(xs: &[(Num, i64)]) -> Expected {
|
||||
let zero = || ratio_of(0);
|
||||
let mut e = Expected {
|
||||
decimal_part: zero(),
|
||||
groups: Default::default(),
|
||||
inexact: Default::default(),
|
||||
inexact_total: zero(),
|
||||
};
|
||||
for (x, w) in xs {
|
||||
let weighted = q(x).unwrap() * ratio_of(*w as i128);
|
||||
match x.kind() {
|
||||
NumRef::Inexact(f) => {
|
||||
*e.inexact.entry(f.to_bits()).or_insert(0) += *w as i128;
|
||||
e.inexact_total += weighted;
|
||||
}
|
||||
NumRef::Fraction(r) if !decimal_denominator(*r.denom()) => {
|
||||
*e.groups.entry(*r.denom()).or_insert_with(zero) += weighted;
|
||||
}
|
||||
_ => e.decimal_part += weighted,
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
fn live(&self) -> impl Iterator<Item = &BigRational> {
|
||||
self.groups.values().filter(|v| !v.is_zero())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_accumulator_reads_the_exact_sum_or_its_stated_roundings() {
|
||||
run(prop::collection::vec(term(), 0..24), 256, false, |xs| {
|
||||
let got = sum_all(&xs);
|
||||
let mut groups = std::collections::BTreeMap::<i128, BigRational>::new();
|
||||
let mut exact = BigRational::from_integer(BigInt::from(0));
|
||||
let mut floats = Vec::new();
|
||||
for x in &xs {
|
||||
match x.kind() {
|
||||
NumRef::Inexact(f) => floats.push(f),
|
||||
NumRef::Fraction(r) if !decimal_denominator(*r.denom()) => {
|
||||
*groups.entry(*r.denom()).or_insert_with(BigRational::zero) += q(x).unwrap();
|
||||
exact += q(x).unwrap();
|
||||
}
|
||||
_ => exact += q(x).unwrap(),
|
||||
}
|
||||
}
|
||||
let live = groups.values().filter(|v| !v.is_zero()).count();
|
||||
match (floats.is_empty(), live <= numtower::K) {
|
||||
(true, true) => {
|
||||
prop_assert_eq!(got.is_exact(), fits(&exact), "{}", got);
|
||||
prop_assert!(!got.is_exact() || q(&got) == Some(exact.clone()), "{got}");
|
||||
prop_assert!(got.is_exact() || is_nearest(got.to_f64(), &exact), "{got}");
|
||||
}
|
||||
let arms = std::cell::Cell::new([0u32; 3]);
|
||||
let lists = prop_oneof![
|
||||
prop::collection::vec(weighted(term()), 0..24),
|
||||
prop::collection::vec(weighted(pooled_term()), 0..40),
|
||||
];
|
||||
run(lists, 512, false, |xs| {
|
||||
let got = sum_weighted(&xs);
|
||||
let e = Expected::of(&xs);
|
||||
let present = e.inexact.values().any(|w| *w != 0);
|
||||
let exact_total = e.live().fold(e.decimal_part.clone(), |s, g| s + g);
|
||||
let mut seen = arms.get();
|
||||
match (present, e.live().count() <= numtower::K) {
|
||||
(false, true) => {
|
||||
let mut total = BigRational::from_float(exact.to_f64().unwrap()).unwrap();
|
||||
floats
|
||||
.iter()
|
||||
.for_each(|f| total += BigRational::from_float(*f).unwrap());
|
||||
seen[0] += 1;
|
||||
prop_assert_eq!(got.is_exact(), fits(&exact_total), "{}", got);
|
||||
prop_assert!(
|
||||
!got.is_exact() || q(&got) == Some(exact_total.clone()),
|
||||
"{got}"
|
||||
);
|
||||
prop_assert!(
|
||||
got.is_exact() || is_nearest(got.to_f64(), &exact_total),
|
||||
"{got}"
|
||||
);
|
||||
}
|
||||
(true, true) => {
|
||||
seen[1] += 1;
|
||||
let total = rounded(&exact_total) + &e.inexact_total;
|
||||
prop_assert!(!got.is_exact() && is_nearest(got.to_f64(), &total), "{got}");
|
||||
}
|
||||
(_, false) => prop_assert!(!got.is_exact(), "past K groups: {got}"),
|
||||
(_, false) => {
|
||||
seen[2] += 1;
|
||||
let groups = e.live().map(rounded).fold(ratio_of(0), |s, g| s + g);
|
||||
let total = groups + rounded(&e.decimal_part) + &e.inexact_total;
|
||||
let near = is_nearest(got.to_f64(), &total);
|
||||
prop_assert!(!got.is_exact() && near, "past K groups: {got} for {total}");
|
||||
}
|
||||
}
|
||||
arms.set(seen);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let seen = arms.get();
|
||||
assert!(
|
||||
seen.iter().all(|n| *n >= 20),
|
||||
"every branch must be reached often: {seen:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn decimal_denominator(d: i128) -> bool {
|
||||
@@ -1085,3 +1228,140 @@ fn the_canonical_text_reads_back_as_the_same_number() {
|
||||
let ten35 = Num::ratio(10i128.pow(35), 1).unwrap();
|
||||
assert_eq!(ten35.to_string(), format!("{}/1", 10i128.pow(35)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_sums_are_equal_when_their_values_are() {
|
||||
let of = |terms: &[(f64, i64)]| {
|
||||
let mut s = numtower::ExactSum::new();
|
||||
terms.iter().for_each(|(x, w)| s.add(*x, *w));
|
||||
s
|
||||
};
|
||||
assert_eq!(
|
||||
of(&[(0.5, 1), (0.5, 1)]),
|
||||
of(&[(1.0, 1)]),
|
||||
"0.5 + 0.5 = 1.0"
|
||||
);
|
||||
assert_eq!(
|
||||
of(&[(3.25, 1), (3.25, -1)]),
|
||||
numtower::ExactSum::new(),
|
||||
"insert then retract"
|
||||
);
|
||||
let big = (1i64 << 30) - 1;
|
||||
assert_eq!(
|
||||
of(&[(1.5, big), (0.25, 2)]),
|
||||
of(&[(0.25, 2), (1.5, big)]),
|
||||
"order"
|
||||
);
|
||||
assert_ne!(of(&[(1.0, 1)]), of(&[(1.0, 2)]));
|
||||
assert_ne!(
|
||||
of(&[(f64::NAN, 1)]),
|
||||
of(&[]),
|
||||
"a counted NaN is part of the value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_forced_fraction_never_reads_as_infinity() {
|
||||
let huge = format!("{}/1", "9".repeat(400));
|
||||
assert_eq!(
|
||||
parse_number(&huge, Typing::Forced).map(|n| n.to_string()),
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
!is_number(&huge, Typing::Forced),
|
||||
"the gate and the parse agree"
|
||||
);
|
||||
let negative = format!("-{huge}");
|
||||
assert!(parse_number(&negative, Typing::Forced).is_none());
|
||||
let tiny = format!("1/{}", "9".repeat(400));
|
||||
let zero = parse_number(&tiny, Typing::Forced).unwrap();
|
||||
assert!(
|
||||
!zero.is_exact() && zero.to_f64() == 0.0,
|
||||
"an underflow is a finite zero, as 1e-999"
|
||||
);
|
||||
assert!(is_number(&tiny, Typing::Forced));
|
||||
}
|
||||
|
||||
type InterimCmp<'a> = &'a dyn Fn(&Interim, &Interim) -> Option<Ordering>;
|
||||
|
||||
fn check_interim_order(cmp: InterimCmp, deterministic: bool) -> Result<(), String> {
|
||||
let eps = Interim::from(Num::ratio(1, 3 * 10i128.pow(30)).unwrap());
|
||||
run(
|
||||
(
|
||||
wide_fraction(),
|
||||
wide_fraction(),
|
||||
wide_fraction(),
|
||||
wide_fraction(),
|
||||
),
|
||||
256,
|
||||
deterministic,
|
||||
|(x1, x2, y1, y2)| {
|
||||
let a = &Interim::from(x1.clone()) * &Interim::from(x2.clone());
|
||||
let av = q(&x1).unwrap() * q(&x2).unwrap();
|
||||
let near = av.to_f64().unwrap();
|
||||
let eps_v =
|
||||
BigRational::new(BigInt::from(1), BigInt::from(3) * BigInt::from(10).pow(30));
|
||||
let others = [
|
||||
(
|
||||
&Interim::from(y1.clone()) * &Interim::from(y2.clone()),
|
||||
q(&y1).unwrap() * q(&y2).unwrap(),
|
||||
),
|
||||
(&a + &eps, &av + &eps_v),
|
||||
(&a - &eps, &av - &eps_v),
|
||||
(a.clone(), av.clone()),
|
||||
(
|
||||
Interim::from(Num::inexact(near)),
|
||||
BigRational::from_float(near).unwrap(),
|
||||
),
|
||||
(
|
||||
Interim::from(Num::inexact(near.next_up())),
|
||||
BigRational::from_float(near.next_up()).unwrap(),
|
||||
),
|
||||
];
|
||||
for (b, bv) in &others {
|
||||
prop_assert_eq!(
|
||||
cmp(&a, b),
|
||||
Some(av.cmp(bv)),
|
||||
"interim order: {:?} vs {:?}",
|
||||
a,
|
||||
b
|
||||
);
|
||||
prop_assert_eq!(
|
||||
cmp(b, &a),
|
||||
Some(bv.cmp(&av)),
|
||||
"interim order: {:?} vs {:?}",
|
||||
b,
|
||||
a
|
||||
);
|
||||
}
|
||||
let twin = a.clone();
|
||||
prop_assert!(
|
||||
!a.is_zero() && (&a - &twin).is_zero(),
|
||||
"interim zero: {a:?}"
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_interim_compares_exactly_without_storing() {
|
||||
check_interim_order(&Interim::cmp_exact, false).unwrap();
|
||||
assert_eq!(
|
||||
Interim::from(Num::inexact(f64::NAN)).cmp_exact(&Interim::from(dec("1"))),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Storing first: what `=` would do if `Interim` could not compare.
|
||||
fn store_then_compare(a: &Interim, b: &Interim) -> Option<Ordering> {
|
||||
a.clone().store().cmp_exact(&b.clone().store())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_interim_compares_exactly_without_storing_catches_storing_first() {
|
||||
assert_caught(
|
||||
check_interim_order(&store_then_compare, true),
|
||||
"interim order",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user