mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 12:00:01 -06:00
269 lines
9.3 KiB
Rust
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();
|
|
}
|
|
}
|