Files
gridcraft/crates/engine/src/pivot/items.rs
T

269 lines
9.3 KiB
Rust

use super::{CellError, HashMap, MONTHS, Ordering, PivotDateGroup, PivotField, PivotFunc, PivotSort, Source, Value, Workbook, datetime_from_serial};
// ---------------------------------------------------------------- items
#[derive(Clone, Debug)]
pub(super) struct Item {
pub(super) value: Value,
pub(super) label: String,
class: u8,
num: f64,
text: String,
first: usize,
/// The value is a number shown with the source format.
pub(super) formatted: bool,
}
#[derive(Default)]
pub(super) struct FieldItems {
items: Vec<Item>,
index: HashMap<String, u32>,
}
fn cmp_items(a: &Item, b: &Item) -> Ordering {
a.class.cmp(&b.class).then(a.num.total_cmp(&b.num)).then_with(|| a.text.cmp(&b.text))
}
/// (identity key, item) for a source value under a field's date grouping.
fn make_item(wb: &Workbook, v: &Value, group: PivotDateGroup, fmt: &str, first: usize) -> (String, Item) {
let it = |value: Value, label: String, class: u8, num: f64, formatted: bool| Item {
text: label.to_lowercase(),
value,
label,
class,
num,
first,
formatted,
};
match v {
Value::Empty => ("b".into(), it(Value::text("(blank)"), "(blank)".into(), 4, 0.0, false)),
Value::Number(n) => {
if group != PivotDateGroup::None
&& let Some(dt) = datetime_from_serial(wb.date_system, *n)
{
let mon = MONTHS.get(dt.month.saturating_sub(1) as usize).copied().unwrap_or("Jan");
let (label, key) = match group {
PivotDateGroup::Years => (dt.year.to_string(), dt.year as f64),
PivotDateGroup::Quarters => {
let q = (dt.month.saturating_sub(1)) / 3 + 1;
(format!("Qtr{q}"), q as f64)
}
PivotDateGroup::Months => (mon.to_string(), dt.month as f64),
_ => (format!("{}-{mon}", dt.day), (dt.month * 100 + dt.day) as f64),
};
return (format!("g{key}"), it(Value::text(label.as_str()), label, 0, key, false));
}
let label = crate::display::format(v, fmt, wb).text;
(format!("n{}", n.to_bits()), it(v.clone(), label, 0, *n, true))
}
Value::Text(t) => (format!("t{}", t.to_lowercase()), it(v.clone(), t.to_string(), 1, 0.0, false)),
Value::Bool(b) => {
let l = if *b { "TRUE" } else { "FALSE" };
(format!("l{l}"), it(Value::Bool(*b), l.into(), 2, *b as u8 as f64, false))
}
Value::Error(e) => (format!("e{}", e.as_str()), it(v.clone(), e.as_str().into(), 3, 0.0, false)),
Value::Array(_) => make_item(wb, &v.scalar(), group, fmt, first),
}
}
impl FieldItems {
pub(super) fn intern(&mut self, wb: &Workbook, v: &Value, group: PivotDateGroup, fmt: &str, first: usize) -> u32 {
let (key, item) = make_item(wb, v, group, fmt, first);
if let Some(i) = self.index.get(&key) {
return *i;
}
let i = self.items.len() as u32;
self.items.push(item);
self.index.insert(key, i);
i
}
pub(super) fn get(&self, i: u32) -> Option<&Item> {
self.items.get(i as usize)
}
pub(super) fn label(&self, i: u32) -> String {
self.get(i).map(|x| x.label.clone()).unwrap_or_default()
}
}
/// The label of a source value under a date grouping (for filters and field lists).
pub fn item_label(wb: &Workbook, v: &Value, group: PivotDateGroup, fmt: &str) -> String {
make_item(wb, v, group, fmt, 0).1.label
}
/// Distinct item labels of a source column, sorted like the field list shows them.
pub fn distinct_labels(wb: &Workbook, src: &Source, col: usize, group: PivotDateGroup, limit: usize) -> Vec<String> {
let mut fi = FieldItems::default();
let fmt = src.formats.get(col).map(String::as_str).unwrap_or("General");
for (i, r) in src.rows.iter().enumerate() {
if let Some(v) = r.get(col) {
fi.intern(wb, v, group, fmt, i);
}
if fi.items.len() > 100_000 {
break;
}
}
let mut items = fi.items;
items.sort_by(cmp_items);
items.into_iter().take(limit).map(|i| i.label).collect()
}
// ---------------------------------------------------------------- aggregation
#[derive(Clone, Debug, Default)]
pub(super) struct Acc {
count: u64,
nums: u64,
sum: f64,
prod: f64,
min: f64,
max: f64,
mean: f64,
m2: f64,
err: Option<CellError>,
}
impl Acc {
pub(super) fn add(&mut self, v: &Value) {
match v {
Value::Empty => {}
Value::Number(n) => {
let n = *n;
self.count += 1;
self.nums += 1;
self.sum += n;
if self.nums == 1 {
self.prod = n;
self.min = n;
self.max = n;
} else {
self.prod *= n;
self.min = self.min.min(n);
self.max = self.max.max(n);
}
let d = n - self.mean;
self.mean += d / self.nums as f64;
self.m2 += d * (n - self.mean);
}
Value::Error(e) => {
self.count += 1;
if self.err.is_none() {
self.err = Some(*e);
}
}
_ => self.count += 1,
}
}
pub(super) fn result(&self, f: PivotFunc) -> Value {
let n = self.nums as f64;
match f {
PivotFunc::Count => return Value::number(self.count as f64),
PivotFunc::CountNumbers => return Value::number(n),
_ => {}
}
if let Some(e) = self.err {
return Value::Error(e);
}
let div0 = Value::Error(CellError::Div0);
match f {
PivotFunc::Sum => Value::number(self.sum),
PivotFunc::Average => {
if self.nums == 0 {
div0
} else {
Value::number(self.sum / n)
}
}
PivotFunc::Max => Value::number(if self.nums == 0 { 0.0 } else { self.max }),
PivotFunc::Min => Value::number(if self.nums == 0 { 0.0 } else { self.min }),
PivotFunc::Product => Value::number(if self.nums == 0 { 0.0 } else { self.prod }),
PivotFunc::StdDev | PivotFunc::Var => {
if self.nums < 2 {
div0
} else {
let var = (self.m2 / (n - 1.0)).max(0.0);
Value::number(if f == PivotFunc::Var { var } else { var.sqrt() })
}
}
PivotFunc::StdDevP | PivotFunc::VarP => {
if self.nums < 1 {
div0
} else {
let var = (self.m2 / n).max(0.0);
Value::number(if f == PivotFunc::VarP { var } else { var.sqrt() })
}
}
PivotFunc::Count | PivotFunc::CountNumbers => Value::Empty,
}
}
}
/// Excel's caption verb for a function ("Sum of Sales").
pub fn func_caption(f: PivotFunc) -> &'static str {
match f {
PivotFunc::Sum => "Sum",
PivotFunc::Count => "Count",
PivotFunc::Average => "Average",
PivotFunc::Max => "Max",
PivotFunc::Min => "Min",
PivotFunc::Product => "Product",
PivotFunc::CountNumbers => "Count Numbers",
PivotFunc::StdDev => "StdDev",
PivotFunc::StdDevP => "StdDevp",
PivotFunc::Var => "Var",
PivotFunc::VarP => "Varp",
}
}
// ---------------------------------------------------------------- trees
#[derive(Clone, Debug)]
pub(super) struct Node {
pub(super) item: u32,
pub(super) children: Vec<Node>,
}
pub(super) fn build_tree(paths: &[&[u32]], depth: usize, fields: &[&PivotField], items: &[FieldItems]) -> Vec<Node> {
let (Some(field), Some(fi)) = (fields.get(depth), items.get(depth)) else { return vec![] };
let mut order: Vec<u32> = Vec::new();
let mut groups: HashMap<u32, Vec<&[u32]>> = HashMap::new();
for p in paths {
if let Some(&it) = p.get(depth) {
let g = groups.entry(it).or_default();
if g.is_empty() {
order.push(it);
}
g.push(p);
}
}
let mut nodes: Vec<Node> = order
.into_iter()
.map(|it| {
let ps = groups.remove(&it).unwrap_or_default();
Node { item: it, children: build_tree(&ps, depth + 1, fields, items) }
})
.collect();
let by = |a: &Node, b: &Node| match (fi.get(a.item), fi.get(b.item)) {
(Some(x), Some(y)) => match field.sort {
PivotSort::Asc => cmp_items(x, y),
PivotSort::Desc => cmp_items(y, x),
PivotSort::None => x.first.cmp(&y.first),
},
_ => Ordering::Equal,
};
nodes.sort_by(by);
nodes
}
pub(super) fn is_collapsed(field: &PivotField, label: &str) -> bool {
field.collapsed_items.iter().any(|c| c.eq_ignore_ascii_case(label))
}
pub(super) fn collect_siblings(nodes: &[Node], prefix: &mut Vec<u32>, out: &mut HashMap<Vec<u32>, Vec<u32>>) {
out.insert(prefix.clone(), nodes.iter().map(|n| n.item).collect());
for n in nodes {
prefix.push(n.item);
collect_siblings(&n.children, prefix, out);
prefix.pop();
}
}