Files
improvise/crates/improvise-core/src/model/source.rs
T
2026-10-10 19:52:55 -07:00

901 lines
33 KiB
Rust

//! Where cell values are read from: a read-only trait, and the aggregate it answers in.
//! Reasoning: `context/code/source.md` (ADR 003, ADR 009 clause 2; improvise-qddo).
use super::cell::{CellChange, CellKey, CellValue, DataStore};
use crate::formula::AggFunc;
use numtower::ExactSum;
/// The stored cells one read asks about: those that contain every pair of `base`, and, for each
/// span, one of its listed items. `exact` admits only a cell with no other coordinate.
#[derive(Debug, Clone, Copy)]
pub struct Select<'a> {
pub base: &'a [(String, String)],
pub spans: &'a [(&'a str, Vec<&'a str>)],
pub exact: bool,
}
impl<'a> Select<'a> {
/// Every cell containing `base`.
pub fn matching(base: &'a [(String, String)]) -> Self {
Select {
base,
spans: &[],
exact: false,
}
}
/// Whether the cell at `key` is one this select names.
pub fn admits(&self, key: &CellKey) -> bool {
let spanned = |(cat, items): &(&str, Vec<&str>)| {
key.get(cat).is_some_and(|held| items.contains(&held))
};
key.matches_partial(self.base)
&& self.spans.iter().all(spanned)
&& (!self.exact || key.0.len() == self.base.len() + self.spans.len())
}
/// Every cell containing `base` and one listed item of each span.
pub fn spanning(
base: &'a [(String, String)],
spans: &'a [(&'a str, Vec<&'a str>)],
exact: bool,
) -> Self {
Select { base, spans, exact }
}
}
/// What a source reports of the numbers in a read: count, exact sum, extremes. It merges.
/// Why it is enough: `context/code/source.md`.
#[derive(Debug, Clone, Default)]
pub struct Numbers {
count: u64,
sum: ExactSum,
least: Option<f64>,
greatest: Option<f64>,
}
impl Numbers {
pub fn of(values: impl IntoIterator<Item = f64>) -> Self {
let mut numbers = Numbers::default();
values.into_iter().for_each(|x| numbers.add(x));
numbers
}
/// Numbers from what an engine computed: how many, their exact sum and the least and
/// greatest that are not NaN. A state `add` cannot reach is refused, not repaired.
pub fn from_parts(
count: u64,
sum: ExactSum,
least: Option<f64>,
greatest: Option<f64>,
) -> Result<Self, BadParts> {
let bad = |why| Err(BadParts(why));
if count == 0 && sum != ExactSum::new() {
return bad("a sum with no numbers");
}
match (least, greatest) {
(None, None) if count > 0 && !sum.value().is_nan() => {
return bad("no extremes but a sum that is not NaN");
}
(None, None) => {}
(Some(_), Some(_)) if count == 0 => return bad("extremes with no numbers"),
(Some(l), Some(g)) if l.is_nan() || g.is_nan() => return bad("a NaN extreme"),
(Some(l), Some(g)) if l.total_cmp(&g).is_gt() => return bad("least above greatest"),
(Some(_), Some(_)) => {}
_ => return bad("one extreme without the other"),
}
Ok(Numbers {
count,
sum,
least,
greatest,
})
}
pub fn add(&mut self, x: f64) {
self.count += 1;
self.sum.add(x, 1);
if !x.is_nan() {
self.least = Some(self.least.map_or(x, |l| order_min(l, x)));
self.greatest = Some(self.greatest.map_or(x, |g| order_max(g, x)));
}
}
pub fn merge(&mut self, other: &Numbers) {
self.count += other.count;
self.sum.absorb(&other.sum);
self.least = merged(self.least, other.least, order_min);
self.greatest = merged(self.greatest, other.greatest, order_max);
}
pub fn count(&self) -> u64 {
self.count
}
/// The sum correctly rounded once, signed as IEEE signs a zero total; `+0.0` over none.
pub fn total(&self) -> f64 {
self.sum.value()
}
/// `None` over no numbers: an empty read is absent, not zero. `Min` and `Max` skip NaN, and
/// are NaN when nothing else is left; zeros order by sign.
pub fn reduce(&self, func: &AggFunc) -> Option<f64> {
(self.count > 0).then(|| match func {
AggFunc::Sum => self.sum.value(),
AggFunc::Avg => self.sum.mean(self.count),
AggFunc::Min => self.least.unwrap_or(f64::NAN),
AggFunc::Max => self.greatest.unwrap_or(f64::NAN),
AggFunc::Count => self.count as f64,
})
}
}
fn order_min(a: f64, b: f64) -> f64 {
if b.total_cmp(&a).is_lt() { b } else { a }
}
fn order_max(a: f64, b: f64) -> f64 {
if b.total_cmp(&a).is_gt() { b } else { a }
}
fn merged(a: Option<f64>, b: Option<f64>, pick: fn(f64, f64) -> f64) -> Option<f64> {
match (a, b) {
(Some(x), Some(y)) => Some(pick(x, y)),
(x, y) => x.or(y),
}
}
/// Parts of a summary that no sequence of values could have produced.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BadParts(pub &'static str);
/// What the values that are not numbers agree on: nothing held, one value, or disagreement.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Agreement {
#[default]
None,
One(CellValue),
Mixed,
}
/// A read of cells as a source reports it: the numbers, and whether the rest agree on one
/// value. It reduces as raw values do (core.md §2); none at all is absent.
#[derive(Debug, Clone, Default)]
pub struct Summary {
numbers: Numbers,
others: Agreement,
}
impl Summary {
pub fn of<'a>(values: impl IntoIterator<Item = &'a CellValue>) -> Self {
let mut summary = Summary::default();
values.into_iter().for_each(|v| summary.add(v));
summary
}
/// A summary from what an engine computed: the numbers, and what the rest agree on.
pub fn from_parts(numbers: Numbers, others: Agreement) -> Result<Self, BadParts> {
match &others {
Agreement::One(v) if v.as_f64().is_some() => Err(BadParts("a number among the others")),
_ => Ok(Summary { numbers, others }),
}
}
pub fn add(&mut self, value: &CellValue) {
match value.as_f64() {
Some(x) => self.numbers.add(x),
None => self.add_other(value),
}
}
fn add_other(&mut self, value: &CellValue) {
self.others = match std::mem::take(&mut self.others) {
Agreement::None => Agreement::One(value.clone()),
Agreement::One(held) if held == *value => Agreement::One(held),
_ => Agreement::Mixed,
};
}
pub fn merge(&mut self, other: &Summary) {
self.numbers.merge(&other.numbers);
match &other.others {
Agreement::None => {}
Agreement::One(v) => self.add_other(v),
Agreement::Mixed => self.others = Agreement::Mixed,
}
}
/// Only the numbers: what a formula-level aggregate pools.
pub fn numbers(&self) -> &Numbers {
&self.numbers
}
pub fn reduce(&self, func: &AggFunc) -> Option<CellValue> {
match (&self.others, self.numbers.count()) {
(Agreement::None, _) => self.numbers.reduce(func).map(CellValue::Number),
(Agreement::One(only), 0) => Some(only.clone()),
_ => Some(CellValue::Error("mixed".to_string())),
}
}
}
/// A source a model can be over and a backend can hold at once. A source that changes behind
/// its readers (a database) does so through interior mutability of its own.
pub type SharedSource = std::sync::Arc<dyn CellSource + Send + Sync>;
/// A source that reports its changes; its cells are the state as of its last drain.
/// A drain is destructive: a feed has one drainer (`context/code/source.md`).
pub trait ChangeSource: CellSource {
/// The changes since the last drain, oldest first; reads see them from now on.
fn drain_changes(&self) -> Vec<CellChange>;
}
/// A change-reporting source a backend and a model share.
pub type SharedFeed = std::sync::Arc<dyn ChangeSource + Send + Sync>;
/// The read side of cell storage (ADR 003). Nothing here writes, so a source that cannot be
/// written to is not one that fails at runtime: it has no method to fail in.
pub trait CellSource {
/// The value stored at exactly `key`.
fn value_at(&self, key: &CellKey) -> Option<CellValue>;
/// The summary of the values `select` names.
fn summarize(&self, select: &Select) -> Summary;
/// The keys of the cells containing every pair of `partial`.
fn keys<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = CellKey> + 'a>;
/// The cells containing every pair of `partial`, with their values.
fn cells<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = (CellKey, CellValue)> + 'a>;
/// How many cells `select` names, leaving out those holding any of the `excluding` pairs.
fn count(&self, select: &Select, excluding: &[(String, String)]) -> usize;
/// The key of every cell with the categories in `without` removed, each once, in the order
/// first seen: the stems a formula is evaluated at, as `SELECT DISTINCT` gives them.
fn distinct_without(&self, without: &[String]) -> Vec<CellKey>;
}
/// Whether the cell at `key` holds any of the `pairs`.
fn holds_any(key: &CellKey, pairs: &[(String, String)]) -> bool {
pairs
.iter()
.any(|(cat, item)| key.get(cat) == Some(item.as_str()))
}
impl CellSource for DataStore {
fn value_at(&self, key: &CellKey) -> Option<CellValue> {
self.get(key).cloned()
}
fn summarize(&self, select: &Select) -> Summary {
let values = match (select.spans.is_empty(), select.exact) {
(true, true) => Vec::from_iter(self.get(&CellKey::new(select.base.to_vec()))),
(true, false) => self.matching_values(select.base),
(false, _) => self.spanned_values(select.base, select.spans, select.exact),
};
Summary::of(values)
}
fn keys<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = CellKey> + 'a> {
match partial.is_empty() {
true => Box::new(self.iter_cells().map(|(key, _)| key)),
false => Box::new(self.matching_cells(partial).into_iter().map(|(key, _)| key)),
}
}
fn cells<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = (CellKey, CellValue)> + 'a> {
match partial.is_empty() {
true => Box::new(self.iter_cells().map(|(k, v)| (k, v.clone()))),
false => Box::new(
self.matching_cells(partial)
.into_iter()
.map(|(k, v)| (k, v.clone())),
),
}
}
fn count(&self, select: &Select, excluding: &[(String, String)]) -> usize {
let named = self.keys(select.base).filter(|key| select.admits(key));
named.filter(|key| !holds_any(key, excluding)).count()
}
fn distinct_without(&self, without: &[String]) -> Vec<CellKey> {
let mut seen = std::collections::HashSet::new();
let stems = self
.iter_cells()
.map(|(key, _)| without.iter().fold(key, |k, cat| k.without(cat)));
stems.filter(|stem| seen.insert(stem.clone())).collect()
}
}
/// A table of rows read by scanning, the way a query engine with no index would. A second
/// implementation of [`CellSource`] that shares no code with [`DataStore`] but the summary.
#[cfg(any(test, feature = "test-support"))]
#[derive(Debug, Clone, Default)]
pub struct TableSource {
pub rows: Vec<(CellKey, CellValue)>,
}
#[cfg(any(test, feature = "test-support"))]
impl TableSource {
fn holds(key: &CellKey, select: &Select) -> bool {
let has = |(cat, item): &(String, String)| key.get(cat) == Some(item.as_str());
let spanned = |(cat, items): &(&str, Vec<&str>)| {
key.get(cat).is_some_and(|held| items.contains(&held))
};
select.base.iter().all(has)
&& select.spans.iter().all(spanned)
&& (!select.exact || key.0.len() == select.base.len() + select.spans.len())
}
}
#[cfg(any(test, feature = "test-support"))]
impl CellSource for TableSource {
fn value_at(&self, key: &CellKey) -> Option<CellValue> {
self.rows
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.clone())
}
fn summarize(&self, select: &Select) -> Summary {
let held = self.rows.iter().filter(|(key, _)| Self::holds(key, select));
Summary::of(held.map(|(_, v)| v))
}
fn keys<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = CellKey> + 'a> {
Box::new(self.cells(partial).map(|(key, _)| key))
}
fn cells<'a>(
&'a self,
partial: &'a [(String, String)],
) -> Box<dyn Iterator<Item = (CellKey, CellValue)> + 'a> {
let select = Select::matching(partial);
let held = self.rows.iter();
Box::new(
held.filter(move |(key, _)| Self::holds(key, &select))
.cloned(),
)
}
fn count(&self, select: &Select, excluding: &[(String, String)]) -> usize {
let has = |key: &CellKey, (cat, item): &(String, String)| {
key.0.iter().any(|(c, i)| c == cat && i == item)
};
let named = self.rows.iter().filter(|(key, _)| Self::holds(key, select));
named
.filter(|(key, _)| !excluding.iter().any(|pair| has(key, pair)))
.count()
}
fn distinct_without(&self, without: &[String]) -> Vec<CellKey> {
let mut stems: Vec<CellKey> = Vec::new();
for (key, _) in &self.rows {
let kept = key.0.iter().filter(|(c, _)| !without.contains(c));
let stem = CellKey::new(kept.cloned().collect());
if !stems.contains(&stem) {
stems.push(stem);
}
}
stems
}
}
#[cfg(test)]
mod tests {
use super::*;
const FUNCS: [AggFunc; 5] = [
AggFunc::Sum,
AggFunc::Avg,
AggFunc::Min,
AggFunc::Max,
AggFunc::Count,
];
fn pool() -> Vec<CellValue> {
let text = |s: &str| CellValue::Text(s.into());
vec![
CellValue::Number(1.5),
CellValue::Number(-0.0),
CellValue::Number(0.0),
CellValue::Number(f64::NAN),
CellValue::Number(f64::INFINITY),
CellValue::Number(1e16),
CellValue::Number(1.0),
CellValue::Number(-1e16),
CellValue::Number(0.1),
CellValue::Number(0.2),
text("a"),
text("b"),
CellValue::Error("e".into()),
]
}
/// Every sequence of the pool up to `len` values.
fn sequences(len: usize) -> Vec<Vec<CellValue>> {
let mut all: Vec<Vec<CellValue>> = vec![vec![]];
let mut last = all.clone();
for _ in 0..len {
let extend = |s: &Vec<CellValue>| {
pool()
.into_iter()
.map(|v| [s.clone(), vec![v]].concat())
.collect::<Vec<_>>()
};
last = last.iter().flat_map(extend).collect();
all.extend(last.clone());
}
all
}
/// The reduction as it was before the source moved it, kept to compare against.
fn reference(values: &[&CellValue], func: &AggFunc) -> Option<CellValue> {
let numbers: Vec<f64> = values.iter().filter_map(|v| v.as_f64()).collect();
values.split_first().map(|(first, rest)| {
if numbers.len() == values.len() {
let mut sum = ExactSum::new();
numbers.iter().for_each(|v| sum.add(*v, 1));
let extreme = |order: fn(&f64, &f64) -> std::cmp::Ordering| {
let finite = numbers.iter().copied().filter(|v| !v.is_nan());
finite.min_by(order).unwrap_or(f64::NAN)
};
CellValue::Number(match func {
AggFunc::Sum => sum.value(),
AggFunc::Avg => sum.mean(numbers.len() as u64),
AggFunc::Min => extreme(f64::total_cmp),
AggFunc::Max => extreme(|a, b| b.total_cmp(a)),
AggFunc::Count => numbers.len() as f64,
})
} else if rest.iter().all(|v| v == first) {
(*first).clone()
} else {
CellValue::Error("mixed".to_string())
}
})
}
fn same(a: &Option<CellValue>, b: &Option<CellValue>) -> bool {
match (a, b) {
(Some(CellValue::Number(x)), Some(CellValue::Number(y))) => {
x.to_bits() == y.to_bits() || (x.is_nan() && y.is_nan())
}
_ => a == b,
}
}
#[test]
fn a_summary_reduces_as_the_values_did() {
for values in sequences(3) {
let refs: Vec<&CellValue> = values.iter().collect();
for func in &FUNCS {
let want = reference(&refs, func);
let got = Summary::of(&values).reduce(func);
assert!(
same(&want, &got),
"{func:?} of {values:?}: {want:?} != {got:?}"
);
}
}
}
#[test]
fn a_merge_of_summaries_is_the_summary_of_the_union() {
let all = sequences(2);
for (a, b) in all.iter().flat_map(|a| all.iter().map(move |b| (a, b))) {
let mut merged = Summary::of(a);
merged.merge(&Summary::of(b));
let union: Vec<&CellValue> = a.iter().chain(b).collect();
for func in &FUNCS {
let (want, got) = (reference(&union, func), merged.reduce(func));
assert!(
same(&want, &got),
"{func:?} of {a:?} + {b:?}: {want:?} != {got:?}"
);
}
}
}
#[test]
fn numbers_pool_what_the_formula_aggregate_pools() {
let xs = [1e16, 1.0, -1e16, 0.1, 0.2, -0.0, 0.0, 3.0];
for split in 0..=xs.len() {
let mut pooled = Numbers::of(xs[..split].iter().copied());
pooled.merge(&Numbers::of(xs[split..].iter().copied()));
let whole = Numbers::of(xs);
for func in &FUNCS {
let (a, b) = (pooled.reduce(func), whole.reduce(func));
assert_eq!(
a.map(f64::to_bits),
b.map(f64::to_bits),
"{func:?} split at {split}"
);
}
assert_eq!(
pooled.total().to_bits(),
whole.total().to_bits(),
"split at {split}"
);
}
assert_eq!(Numbers::default().total().to_bits(), 0.0f64.to_bits());
assert_eq!(Numbers::default().reduce(&AggFunc::Sum), None);
}
#[test]
fn the_exact_sum_of_a_pool_of_cancelling_values_is_not_the_sum_of_rounded_parts() {
let rounded = [1e16, 1.0].iter().sum::<f64>() + [-1e16].iter().sum::<f64>();
assert_eq!(
rounded, 0.0,
"the fixture is inexact: rounded parts lose the 1"
);
for (first, second) in [([1e16, 1.0], [-1e16, 0.0]), ([-1e16, 0.0], [1e16, 1.0])] {
let mut pooled = Numbers::of(first);
pooled.merge(&Numbers::of(second));
assert_eq!(pooled.total(), 1.0, "{first:?} then {second:?}");
}
}
fn pair(c: &str, i: &str) -> (String, String) {
(c.to_string(), i.to_string())
}
/// Rows of several key shapes over three categories, with text among the numbers.
fn rows() -> Vec<(CellKey, CellValue)> {
let mut rows = Vec::new();
let records = [("E", "A"), ("E", "B"), ("W", "A"), ("W", "B")];
for (n, (r, p)) in records.iter().enumerate() {
for (m, v) in [("Rev", n as f64 + 1.0), ("Cost", 0.5 * n as f64)] {
let key = CellKey::new(vec![pair("R", r), pair("P", p), pair("_Measure", m)]);
rows.push((key, CellValue::Number(v)));
}
}
let note = CellKey::new(vec![pair("R", "E"), pair("_Measure", "Note")]);
rows.push((note, CellValue::Text("x".into())));
let lone = CellKey::new(vec![pair("R", "W"), pair("_Measure", "Rev")]);
rows.push((lone, CellValue::Number(100.0)));
rows
}
fn store_of(rows: &[(CellKey, CellValue)]) -> DataStore {
let mut store = DataStore::new();
rows.iter()
.for_each(|(k, v)| store.set(k.clone(), v.clone()));
store
}
type Spans = Vec<(String, Vec<String>)>;
/// Every subset of every stored key's pairs as a base, each with no span, with one
/// category spanned over each of several item lists, and with two spanned.
fn selects(rows: &[(CellKey, CellValue)]) -> Vec<(Vec<(String, String)>, Spans)> {
let lists: Vec<Vec<String>> = [
vec!["E"],
vec!["E", "W"],
vec!["A", "B"],
vec!["Rev", "Note"],
vec!["Cost", "Rev", "Note"],
vec!["none"],
]
.iter()
.map(|l| l.iter().map(|s| s.to_string()).collect())
.collect();
let mut out = Vec::new();
for (key, _) in rows {
for mask in 0..(1u32 << key.0.len()) {
let keep = |(i, _): &(usize, &(String, String))| mask >> i & 1 == 1;
let base: Vec<_> = key.0.iter().enumerate().filter(keep).collect();
let base: Vec<_> = base.into_iter().map(|(_, p)| p.clone()).collect();
out.push((base.clone(), vec![]));
let all = ["R", "P", "_Measure"].map(String::from);
let free: Vec<_> = all
.iter()
.filter(|c| base.iter().all(|(b, _)| b != *c))
.collect();
for cat in free.iter().copied() {
for list in &lists {
out.push((base.clone(), vec![(cat.clone(), list.clone())]));
for other in free.iter().copied().filter(|o| *o != cat) {
let both = vec![
(cat.clone(), list.clone()),
(other.clone(), lists[1].clone()),
];
out.push((base.clone(), both));
}
}
}
}
}
out
}
#[test]
fn the_store_and_a_scanned_table_answer_every_select_alike() {
let rows = rows();
let (store, table) = (store_of(&rows), TableSource { rows: rows.clone() });
let mut asked = 0;
for (base, spans) in selects(&rows) {
let spans: Vec<(&str, Vec<&str>)> = spans
.iter()
.map(|(c, i)| (c.as_str(), i.iter().map(String::as_str).collect()))
.collect();
for exact in [false, true] {
let select = Select::spanning(&base, &spans, exact);
for func in &FUNCS {
let a = store.summarize(&select).reduce(func);
let b = table.summarize(&select).reduce(func);
assert!(
same(&a, &b),
"{func:?} {base:?} {spans:?} exact={exact}: {a:?} != {b:?}"
);
asked += 1;
}
}
}
assert!(asked > 5000, "the enumeration shrank to {asked}");
}
#[test]
fn the_store_and_a_scanned_table_list_the_same_cells() {
let rows = rows();
let (store, table) = (store_of(&rows), TableSource { rows: rows.clone() });
for (base, _) in selects(&rows) {
let sorted = |source: &dyn CellSource| {
let mut cells: Vec<_> = source.cells(&base).collect();
cells.sort_by(|a, b| a.0.cmp(&b.0));
cells
};
assert_eq!(sorted(&store), sorted(&table), "cells under {base:?}");
let mut keys: Vec<_> = store.keys(&base).collect();
keys.sort();
let listed: Vec<_> = sorted(&table).into_iter().map(|c| c.0).collect();
assert_eq!(keys, listed, "keys under {base:?}");
}
for (key, value) in &rows {
assert_eq!(store.value_at(key).as_ref(), Some(value));
assert_eq!(table.value_at(key).as_ref(), Some(value));
}
let absent = CellKey::new(vec![pair("R", "nowhere")]);
assert_eq!(
(store.value_at(&absent), table.value_at(&absent)),
(None, None)
);
}
#[test]
fn an_exact_select_with_no_span_names_one_cell() {
let store = store_of(&rows());
let note = vec![pair("R", "E"), pair("_Measure", "Note")];
let one = store.summarize(&Select::spanning(&note, &[], true));
assert_eq!(one.reduce(&AggFunc::Sum), Some(CellValue::Text("x".into())));
let wide = vec![pair("R", "E")];
let loose = store.summarize(&Select::matching(&wide));
let mixed = Some(CellValue::Error("mixed".into()));
assert_eq!(
loose.reduce(&AggFunc::Sum),
mixed,
"E holds text and numbers"
);
let exact = store.summarize(&Select::spanning(&wide, &[], true));
assert_eq!(
exact.reduce(&AggFunc::Sum),
None,
"no cell is keyed by R=E alone"
);
}
/// Every way to hide items of the fixture: none, each item of each category, and two at once.
fn hidden_sets() -> Vec<Vec<(String, String)>> {
let items = [("R", "E"), ("R", "W"), ("P", "A"), ("_Measure", "Note")];
let mut sets = vec![vec![]];
for (i, a) in items.iter().enumerate() {
sets.push(vec![pair(a.0, a.1)]);
for b in &items[i + 1..] {
sets.push(vec![pair(a.0, a.1), pair(b.0, b.1)]);
}
}
sets
}
#[test]
fn the_store_and_a_scanned_table_count_every_select_alike() {
let rows = rows();
let (store, table) = (store_of(&rows), TableSource { rows: rows.clone() });
let (mut asked, mut nonzero) = (0, 0);
for (base, spans) in selects(&rows) {
let spans: Vec<(&str, Vec<&str>)> = spans
.iter()
.map(|(c, i)| (c.as_str(), i.iter().map(String::as_str).collect()))
.collect();
for exact in [false, true] {
let select = Select::spanning(&base, &spans, exact);
for hidden in hidden_sets() {
let (a, b) = (store.count(&select, &hidden), table.count(&select, &hidden));
assert_eq!(a, b, "{base:?} {spans:?} exact={exact} hiding {hidden:?}");
asked += 1;
nonzero += usize::from(a > 0);
}
}
}
assert!(asked > 20_000, "the enumeration shrank to {asked}");
assert!(nonzero > 2_000, "{nonzero} of {asked} counts were not zero");
}
#[test]
fn hiding_an_item_removes_exactly_the_cells_that_hold_it() {
let rows = rows();
let store = store_of(&rows);
let none = Select::matching(&[]);
assert_eq!(store.count(&none, &[]), rows.len());
let east = [pair("R", "E")];
let held = rows.iter().filter(|(k, _)| k.get("R") == Some("E")).count();
assert_eq!(store.count(&none, &east), rows.len() - held);
assert_eq!(
store.count(&Select::matching(&east), &east),
0,
"a named hidden item matches nothing"
);
}
#[test]
fn the_store_and_a_scanned_table_list_the_same_stems() {
let rows = rows();
let (store, table) = (store_of(&rows), TableSource { rows: rows.clone() });
let cats = ["R", "P", "_Measure", "Nowhere"].map(String::from);
let mut asked = 0;
for mask in 0..(1u32 << cats.len()) {
let without: Vec<String> = cats
.iter()
.enumerate()
.filter(|(i, _)| mask >> i & 1 == 1)
.map(|(_, c)| c.clone())
.collect();
let (a, b) = (
store.distinct_without(&without),
table.distinct_without(&without),
);
assert_eq!(a, b, "without {without:?}, in first-seen order");
let mut sorted = a.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), a.len(), "each stem once");
asked += a.len();
}
assert!(asked > 40, "only {asked} stems were compared");
}
/// The parts an engine would compute for `values`, found without `Summary::of`.
fn parts_of(values: &[CellValue]) -> Summary {
let numbers: Vec<f64> = values.iter().filter_map(|v| v.as_f64()).collect();
let mut sum = ExactSum::new();
numbers.iter().for_each(|x| sum.add(*x, 1));
let finite = || numbers.iter().copied().filter(|x| !x.is_nan());
let least = finite().min_by(f64::total_cmp);
let greatest = finite().max_by(f64::total_cmp);
let rest: Vec<&CellValue> = values.iter().filter(|v| v.as_f64().is_none()).collect();
let agreement = match rest.split_first() {
None => Agreement::None,
Some((first, others)) if others.iter().all(|v| v == first) => {
Agreement::One((*first).clone())
}
Some(_) => Agreement::Mixed,
};
let parts = Numbers::from_parts(numbers.len() as u64, sum, least, greatest).unwrap();
Summary::from_parts(parts, agreement).unwrap()
}
#[test]
fn a_summary_built_from_an_engines_parts_reduces_as_one_built_from_the_values() {
for values in sequences(3) {
let refs: Vec<&CellValue> = values.iter().collect();
for func in &FUNCS {
let want = reference(&refs, func);
let got = parts_of(&values).reduce(func);
assert!(
same(&want, &got),
"{func:?} of {values:?}: {want:?} != {got:?}"
);
}
}
}
#[test]
fn summaries_built_from_parts_merge_as_the_union_does() {
let all = sequences(2);
for (a, b) in all.iter().flat_map(|a| all.iter().map(move |b| (a, b))) {
let mut merged = parts_of(a);
merged.merge(&Summary::of(b));
let mut both = parts_of(a);
both.merge(&parts_of(b));
let union: Vec<&CellValue> = a.iter().chain(b).collect();
for func in &FUNCS {
let want = reference(&union, func);
assert!(
same(&want, &merged.reduce(func)),
"{func:?} of {a:?} + {b:?}"
);
assert!(
same(&want, &both.reduce(func)),
"{func:?} of {a:?} + {b:?}, both from parts"
);
}
}
}
#[test]
fn parts_no_sequence_of_values_could_produce_are_refused() {
let sum = ExactSum::new;
let refused = |r: Result<Numbers, BadParts>| r.expect_err("must be refused").0;
assert_eq!(
refused(Numbers::from_parts(0, sum(), Some(1.0), Some(1.0))),
"extremes with no numbers"
);
assert_eq!(
refused(Numbers::from_parts(2, sum(), Some(f64::NAN), Some(1.0))),
"a NaN extreme"
);
assert_eq!(
refused(Numbers::from_parts(2, sum(), Some(2.0), Some(1.0))),
"least above greatest"
);
assert_eq!(
refused(Numbers::from_parts(2, sum(), Some(0.0), Some(-0.0))),
"least above greatest"
);
assert_eq!(
refused(Numbers::from_parts(2, sum(), Some(1.0), None)),
"one extreme without the other"
);
let mut one = ExactSum::new();
one.add(1.0, 1);
assert_eq!(
refused(Numbers::from_parts(0, one.clone(), None, None)),
"a sum with no numbers"
);
assert_eq!(
refused(Numbers::from_parts(2, one, None, None)),
"no extremes but a sum that is not NaN"
);
let mut nan = ExactSum::new();
nan.add(f64::NAN, 1);
let ok = Numbers::from_parts(2, nan, None, None).unwrap();
assert!(
ok.reduce(&AggFunc::Min).unwrap().is_nan(),
"all NaN: no extremes, a count"
);
let number = Agreement::One(CellValue::Number(1.0));
let bad = Summary::from_parts(ok.clone(), number).expect_err("a number is not an other");
assert_eq!(bad.0, "a number among the others");
assert!(Summary::from_parts(ok, Agreement::One(CellValue::Text("t".into()))).is_ok());
}
#[test]
fn keys_of_a_store_are_its_cells_keys_without_the_values() {
let rows = rows();
let store = store_of(&rows);
let all: Vec<CellKey> = store.keys(&[]).collect();
assert_eq!(all, rows.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>());
let east = vec![pair("R", "E")];
let under: Vec<CellKey> = store.keys(&east).collect();
let want: Vec<CellKey> = store.cells(&east).map(|c| c.0).collect();
assert_eq!(under, want);
assert!(!under.is_empty());
}
}