use crate::data::aggregator::AggregatorKind;
use crate::data::column::ColumnMeta;
use crate::data::dataframe::DataFrame;
use crate::types::ColumnType;
use polars::prelude::*;
#[derive(Clone, Debug)]
pub struct AggSpec {
pub col: String,
pub kind: AggregatorKind,
}
impl AggSpec {
pub fn output_name(&self) -> String {
if self.col == "*" {
self.kind.name().to_string()
} else {
format!("{}:{}", self.col, self.kind.name())
}
}
}
fn build_aggregates(
df: &DataFrame,
agg: &[AggSpec],
) -> Result<(Vec<Expr>, Vec<ColumnMeta>), String> {
let mut exprs = Vec::with_capacity(agg.len());
let mut metas = Vec::with_capacity(agg.len());
for spec in agg {
if spec.col == "*" {
if spec.kind != AggregatorKind::Count {
return Err(format!(
"'{}' needs a column; only 'count' works with '*'",
spec.kind.name()
));
}
let alias = spec.output_name();
exprs.push(len().alias(alias.as_str()));
let mut meta = ColumnMeta::new(alias);
meta.col_type = ColumnType::Integer;
metas.push(meta);
continue;
}
let source = &df.columns[df.column_index(&spec.col)?];
if !spec.kind.is_compatible(source.col_type) {
return Err(format!(
"Cannot compute {} over '{}': the column is {}, and {} needs a numeric one",
spec.kind.name(),
spec.col,
source.col_type.name(),
spec.kind.name()
));
}
let expr = spec.kind.to_expr(&spec.col).ok_or_else(|| {
format!(
"'{}' is not available as a group aggregate",
spec.kind.name()
)
})?;
let alias = spec.output_name();
exprs.push(expr.alias(alias.as_str()));
let mut meta = ColumnMeta::new(alias);
if spec.kind.preserves_col_type() {
meta.col_type = source.col_type;
meta.currency = source.currency;
meta.precision = source.precision;
} else {
meta.col_type = ColumnType::Integer;
}
metas.push(meta);
}
Ok((exprs, metas))
}
pub fn group_by(df: &DataFrame, by: &[String], agg: &[AggSpec]) -> Result<DataFrame, String> {
if by.is_empty() {
return Err(
"grouping needs at least one column to group by — for a grand total, total() it"
.to_string(),
);
}
if agg.is_empty() {
return Err("grouping needs at least one aggregate".to_string());
}
let mut metas: Vec<ColumnMeta> = Vec::with_capacity(by.len() + agg.len());
for name in by {
metas.push(df.columns[df.column_index(name)?].clone());
}
let (exprs, agg_metas) = build_aggregates(df, agg)?;
metas.extend(agg_metas);
let grouped = df
.get_visible_df()?
.lazy()
.group_by_stable(
by.iter()
.map(|s| crate::data::column_expr(s.as_str()))
.collect::<Vec<_>>(),
)
.agg(exprs)
.collect()
.map_err(|e| format!("group_by failed: {}", e))?;
Ok(DataFrame::from_parts(grouped, metas))
}
pub fn total(df: &DataFrame, agg: &[AggSpec]) -> Result<DataFrame, String> {
if agg.is_empty() {
return Err("a total needs at least one aggregate".to_string());
}
let (exprs, metas) = build_aggregates(df, agg)?;
let totalled = df
.get_visible_df()?
.lazy()
.select(exprs)
.collect()
.map_err(|e| format!("aggregate failed: {}", e))?;
Ok(DataFrame::from_parts(totalled, metas))
}
pub fn frequency(df: &DataFrame, by: &[String], agg: &[AggSpec]) -> Result<DataFrame, String> {
if by.is_empty() {
return Err("a frequency table needs at least one column to count by".to_string());
}
let group_indices: Vec<usize> = by
.iter()
.map(|name| df.column_index(name))
.collect::<Result<_, _>>()?;
let mut aggregated: Vec<(usize, Vec<AggregatorKind>)> = Vec::new();
for spec in agg {
let idx = df.column_index(&spec.col)?;
let source = &df.columns[idx];
if !spec.kind.is_compatible(source.col_type) {
return Err(format!(
"Cannot compute {} over '{}': the column is {}, and {} needs a numeric one",
spec.kind.name(),
spec.col,
source.col_type.name(),
spec.kind.name()
));
}
match aggregated.iter_mut().find(|(i, _)| *i == idx) {
Some((_, kinds)) => kinds.push(spec.kind),
None => aggregated.push((idx, vec![spec.kind])),
}
}
let (pdf, metas) = if group_indices.len() == 1 {
df.build_frequency_table(group_indices[0], &aggregated)?
} else {
df.build_multi_frequency_table(&group_indices, &aggregated)?
};
let mut result = DataFrame::from_parts(pdf, metas);
if let Some(bar) = result.columns.iter().position(|c| c.name == "Bar") {
result.drop_column(bar)?;
}
Ok(result)
}