use gents_derives::TS;
use logisheets_base::{BlockId, CellId, CellValue, TextId};
use crate::block_manager::schema_manager::field_type::DimOrder;
use crate::block_manager::schema_manager::schema::{Schema, SchemaTrait};
use crate::calc_engine::calculator::funcs::condition::{match_condition, parse_condition};
use crate::errors::Error;
use super::Workbook;
const MAX_PIVOT_KEYS: usize = 1000;
const MAX_PIVOT_FIELDS: usize = 200;
#[derive(Debug, Clone, TS)]
#[ts(file_name = "pivot_excel_note.ts", rename_all = "camelCase")]
pub struct PivotExcelNote {
pub expressible: bool,
pub reason: Option<String>,
}
#[derive(Debug, Clone, TS)]
#[ts(file_name = "pivot_plan.ts", rename_all = "camelCase")]
pub struct PivotPlan {
pub keys: Vec<String>,
pub fields: Vec<String>,
pub current_keys: Vec<String>,
pub current_fields: Vec<String>,
pub missing_keys: Vec<String>,
pub missing_fields: Vec<String>,
pub extra_keys: Vec<String>,
pub extra_fields: Vec<String>,
pub unassigned_records: usize,
pub is_stale: bool,
}
impl Workbook {
pub fn pivot_plan(
&self,
sheet_idx: usize,
block_id: BlockId,
) -> crate::errors::Result<PivotPlan> {
let status = self.status();
let sheet_id = status
.sheet_info_manager
.get_sheet_id(sheet_idx)
.ok_or(Error::UnavailableSheetIdx(sheet_idx))?;
let place = status.navigator.get_block_place(&sheet_id, &block_id)?;
let pivot = place.pivot.as_ref().ok_or_else(|| {
Error::PayloadError(format!(
"block {block_id} on sheet {sheet_idx} is not a pivot, so it has no shape to plan"
))
})?;
let source = place
.analyzes
.ok_or_else(|| Error::PayloadError(format!("pivot {block_id} analyses nothing")))?;
let (current_keys, current_derived, declared, declared_measures) =
current_shape(status, sheet_id, block_id);
plan_from(
status,
sheet_id,
source,
pivot,
current_keys,
current_derived,
declared,
declared_measures,
)
}
pub fn pivot_excel_note(
&self,
sheet_idx: usize,
block_id: BlockId,
) -> crate::errors::Result<PivotExcelNote> {
let status = self.status();
let sheet_id = status
.sheet_info_manager
.get_sheet_id(sheet_idx)
.ok_or(Error::UnavailableSheetIdx(sheet_idx))?;
Ok(
match crate::file_saver::pivot_ooxml::expressibility(
sheet_id,
block_id,
&status.navigator,
&status.block_schema_manager,
) {
Some(Err(why)) => PivotExcelNote {
expressible: false,
reason: Some(why.reason().to_string()),
},
_ => PivotExcelNote {
expressible: true,
reason: None,
},
},
)
}
pub fn pivot_plan_for(
&self,
sheet_idx: usize,
source_block: BlockId,
spec: &crate::block_manager::schema_manager::field_type::PivotSpecParts,
) -> crate::errors::Result<PivotPlan> {
use crate::block_manager::schema_manager::field_type::PivotSpec;
let status = self.status();
let sheet_id = status
.sheet_info_manager
.get_sheet_id(sheet_idx)
.ok_or(Error::UnavailableSheetIdx(sheet_idx))?;
let pivot = PivotSpec::from_parts(spec).ok_or_else(|| {
Error::PayloadError(
"that is not a usable pivot recipe: it needs a row dimension, a measure, and one of SUM / COUNT / AVERAGE / MIN / MAX"
.to_string(),
)
})?;
plan_from(
status,
sheet_id,
source_block,
&pivot,
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
)
}
}
fn plan_from(
status: &crate::controller::status::Status,
sheet_id: logisheets_base::SheetId,
source: BlockId,
pivot: &crate::block_manager::schema_manager::field_type::PivotSpec,
current_keys: Vec<String>,
current_fields: Vec<String>,
declared_fields: Vec<String>,
declared_measures: Vec<String>,
) -> crate::errors::Result<PivotPlan> {
let distinct =
|field: &str, limit: usize, what: &str| -> crate::errors::Result<(Vec<String>, usize)> {
distinct_values(status, sheet_id, source, field, pivot, limit, what)
};
check_measures(status, sheet_id, source, pivot, &declared_measures)?;
let (keys, blank_rows) = distinct(&pivot.row_dim, MAX_PIVOT_KEYS, "rows")?;
let (fields, blank_cols) = match &pivot.col_dim {
Some(col_dim) => distinct(col_dim, MAX_PIVOT_FIELDS, "columns")?,
None => (Vec::new(), 0),
};
let diff = |want: &[String], have: &[String]| -> Vec<String> {
want.iter().filter(|v| !have.contains(v)).cloned().collect()
};
let missing_keys = diff(&keys, ¤t_keys);
let extra_keys = diff(¤t_keys, &keys);
let (missing_fields, extra_fields) = match &pivot.col_dim {
Some(_) => (
diff(&fields, ¤t_fields),
diff(¤t_fields, &fields),
),
None => (Vec::new(), Vec::new()),
};
let is_stale = !missing_keys.is_empty()
|| !extra_keys.is_empty()
|| !missing_fields.is_empty()
|| !extra_fields.is_empty();
let mut fields = fields;
fields.extend(declared_fields.iter().cloned());
Ok(PivotPlan {
keys,
fields,
current_keys,
current_fields,
missing_keys,
missing_fields,
extra_keys,
extra_fields,
unassigned_records: blank_rows.max(blank_cols),
is_stale,
})
}
fn distinct_values(
status: &crate::controller::status::Status,
sheet_id: logisheets_base::SheetId,
block: BlockId,
field: &str,
spec: &crate::block_manager::schema_manager::field_type::PivotSpec,
limit: usize,
what: &str,
) -> crate::errors::Result<(Vec<String>, usize)> {
let schema = status
.block_schema_manager
.schemas
.get(&(sheet_id, block))
.ok_or_else(|| {
Error::PayloadError(format!(
"the pivot's source block {block} has no schema, so it has no fields to group by"
))
})?;
if matches!(schema, Schema::RandomSchema(_)) {
return Err(Error::PayloadError(
"cannot pivot a random-schema block: it has no fields".to_string(),
));
}
let field_id = schema.resolve_field_id(field).ok_or_else(|| {
Error::PayloadError(format!(
"the pivot groups by \"{field}\", which block {block} does not have"
))
})?;
let bp = status.navigator.get_block_place(&sheet_id, &block)?;
let text_fetcher = |id: TextId| status.text_id_manager.get_string(&id).unwrap_or_default();
let filters = spec
.filters
.iter()
.map(|f| {
let fid = schema.resolve_field_id(&f.field).ok_or_else(|| {
Error::PayloadError(format!(
"the pivot filters on \"{}\", which block {block} does not have",
f.field
))
})?;
Ok((fid, f.criteria.clone()))
})
.collect::<crate::errors::Result<Vec<_>>>()?;
let mut seen: Vec<String> = Vec::new();
let mut blank = 0usize;
'records: for key_cell in schema.get_all_key_cell_ids(block, bp) {
let read = |fid| {
schema
.partially_resolve_by_field_id(key_cell, fid)
.and_then(|c| status.container.get_cell(sheet_id, &CellId::BlockCell(c)))
.map(|c| c.value.clone())
.unwrap_or(CellValue::Blank)
};
for (fid, criteria) in &filters {
let v = read(*fid);
let cond = match parse_condition(criteria) {
Some(c) => c,
None => continue 'records,
};
let v = crate::calc_engine::calculator::calc_vertex::Value::from_cell_value(
v,
&|id: &TextId| status.text_id_manager.get_string(id),
);
if !match_condition(&cond, &v) {
continue 'records;
}
}
let value = read(field_id);
let text = value.to_string(&text_fetcher);
if text.is_empty() {
blank += 1;
continue;
}
if !seen.contains(&text) {
if seen.len() == limit {
return Err(Error::PayloadError(format!(
"\"{field}\" has more than {limit} distinct values, which is too many \
for a pivot's {what}. Group it into coarser buckets first."
)));
}
seen.push(text);
}
}
match spec.order {
DimOrder::Ascending => seen.sort_by(|a, b| super::sort_block::cmp_display_strings(a, b)),
DimOrder::FirstSeen => {}
DimOrder::Custom => {
let rank = |v: &String| spec.order_values.iter().position(|o| o == v);
seen.sort_by(|a, b| match (rank(a), rank(b)) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => super::sort_block::cmp_display_strings(a, b),
});
}
}
Ok((seen, blank))
}
fn current_shape(
status: &crate::controller::status::Status,
sheet_id: logisheets_base::SheetId,
block_id: BlockId,
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
let Some(schema) = status
.block_schema_manager
.schemas
.get(&(sheet_id, block_id))
else {
return (Vec::new(), Vec::new(), Vec::new(), Vec::new());
};
let Ok(bp) = status.navigator.get_block_place(&sheet_id, &block_id) else {
return (Vec::new(), Vec::new(), Vec::new(), Vec::new());
};
let text_fetcher = |id: TextId| status.text_id_manager.get_string(&id).unwrap_or_default();
let keys = schema
.get_all_key_cell_ids(block_id, bp)
.into_iter()
.filter_map(|c| {
let v = status
.container
.get_cell(sheet_id, &CellId::BlockCell(c))?
.value
.clone();
let t = v.to_string(&text_fetcher);
(!t.is_empty()).then_some(t)
})
.collect();
let mut derived = Vec::new();
let mut declared = Vec::new();
let mut declared_measures = Vec::new();
let mut sort =
|name: &String,
is_key: bool,
column: Option<&crate::block_manager::schema_manager::field_type::PivotColumn>| {
if is_key {
return;
}
match column {
Some(c) => {
declared.push(name.clone());
if let Some(m) = &c.measure {
declared_measures.push(m.clone());
}
}
None => derived.push(name.clone()),
}
};
match schema {
Schema::RowSchema(s) => s
.fields
.iter()
.for_each(|(n, e)| sort(n, e.field_axis_id == s.key, e.pivot_column.as_ref())),
Schema::ColSchema(s) => s
.fields
.iter()
.for_each(|(n, e)| sort(n, e.field_axis_id == s.key, e.pivot_column.as_ref())),
Schema::RandomSchema(_) => {}
}
(keys, derived, declared, declared_measures)
}
fn check_measures(
status: &crate::controller::status::Status,
sheet_id: logisheets_base::SheetId,
source: BlockId,
pivot: &crate::block_manager::schema_manager::field_type::PivotSpec,
declared_measures: &[String],
) -> crate::errors::Result<()> {
use crate::block_manager::schema_manager::schema::SchemaTrait;
let Some(schema) = status.block_schema_manager.schemas.get(&(sheet_id, source)) else {
return Ok(());
};
for measure in std::iter::once(&pivot.measure).chain(declared_measures) {
if schema.resolve_field_id(measure).is_none() {
return Err(Error::PayloadError(format!(
"the pivot measures \"{measure}\", which block {source} does not have"
)));
}
}
Ok(())
}