9f035f918e
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
105 lines
3.7 KiB
Rust
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:?}"
|
|
);
|
|
}
|
|
}
|