Files
improvise/crates/improvise-incremental/tests/zeros.rs
T

105 lines
3.7 KiB
Rust

//! The sign of a zero in `Min` and `Max`, compared by bits. Evidence:
//! `context/test-evidence/improvise-incremental.md`.
use improvise_core::formula::ast::AggFunc;
use improvise_core::model::Model;
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_incremental::{Derived, Eager, Edit, Incremental, Maintained};
fn key(i: usize) -> CellKey {
CellKey::new(vec![
("Region".into(), format!("r{i}")),
("_Measure".into(), "Revenue".into()),
])
}
fn model_of(values: &[f64], func: AggFunc) -> Model {
let mut model = Model::new("zeros");
model.add_category("Region").unwrap();
model.category_mut("_Measure").unwrap().add_item("Revenue");
model.measure_agg.insert("Revenue".into(), func);
for (i, v) in values.iter().enumerate() {
model
.category_mut("Region")
.unwrap()
.add_item(format!("r{i}"));
model.set_cell(key(i), CellValue::Number(*v));
}
model
}
fn bits(b: &dyn Derived) -> Option<u64> {
let at = CellKey::new(vec![("_Measure".into(), "Revenue".into())]);
b.raw_fold(&at).and_then(|v| v.as_f64()).map(f64::to_bits)
}
fn both(values: &[f64], func: AggFunc) -> (Option<u64>, Option<u64>) {
let model = model_of(values, func);
(
bits(&Eager::new(model.clone(), vec![])),
bits(&Incremental::new(model, vec![])),
)
}
#[test]
fn min_and_max_of_negative_zeros_are_negative_zero_in_both_backends() {
let negative = Some((-0.0f64).to_bits());
for func in [AggFunc::Min, AggFunc::Max] {
for values in [&[-0.0][..], &[-0.0, -0.0], &[-0.0, f64::NAN]] {
let got = both(values, func.clone());
assert_eq!(got, (negative, negative), "{func:?} {values:?}");
}
}
assert_eq!(both(&[-0.0, 3.0], AggFunc::Min), (negative, negative));
assert_eq!(both(&[-3.0, -0.0], AggFunc::Max), (negative, negative));
}
#[test]
fn a_positive_zero_stays_positive_and_nonzero_answers_are_untouched() {
let positive = Some(0.0f64.to_bits());
for func in [AggFunc::Min, AggFunc::Max] {
assert_eq!(both(&[0.0], func.clone()), (positive, positive));
}
let two = Some(2.0f64.to_bits());
assert_eq!(both(&[-0.0, 2.0], AggFunc::Max), (two, two));
}
#[test]
fn overwriting_a_zero_with_the_other_zero_moves_the_sign_of_min_and_max() {
for func in [AggFunc::Min, AggFunc::Max] {
let model = model_of(&[0.0], func.clone());
let mut eager = Eager::new(model.clone(), vec![]);
let mut incremental = Incremental::new(model, vec![]);
assert_eq!(bits(&incremental), Some(0.0f64.to_bits()));
for to in [-0.0, 0.0, -0.0] {
let edit = Edit::SetCell(key(0), CellValue::Number(to));
eager.apply(&edit);
incremental.apply(&edit);
assert_eq!(bits(&incremental), bits(&eager), "{func:?} after {to:?}");
assert_eq!(bits(&incremental), Some(to.to_bits()));
}
}
}
#[test]
fn with_both_zeros_min_is_negative_and_max_is_positive_whatever_the_order() {
for values in [[0.0, -0.0], [-0.0, 0.0]] {
let min = Some((-0.0f64).to_bits());
let max = Some(0.0f64.to_bits());
assert_eq!(both(&values, AggFunc::Min), (min, min), "{values:?}");
assert_eq!(both(&values, AggFunc::Max), (max, max), "{values:?}");
}
}
#[test]
fn min_and_max_over_only_nan_are_nan_in_both_backends() {
for func in [AggFunc::Min, AggFunc::Max] {
let (eager, incremental) = both(&[f64::NAN, -f64::NAN], func.clone());
let nan = |b: Option<u64>| b.is_some_and(|b| f64::from_bits(b).is_nan());
assert!(
nan(eager) && nan(incremental),
"{func:?}: {eager:?} {incremental:?}"
);
}
}