901 lines
33 KiB
Rust
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(¬e, &[], 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());
|
|
}
|
|
}
|