use std::collections::HashMap;
use crate::fx::FxHashMap;
use crate::messages::{raise, Refusal};
use crate::refers::{self, quote_sheet};
use crate::sheet::{cell_ref, shared_masters, strip_future_prefixes, Content, Workbook};
use super::eval::{Computed, Ctx, Env, Library, Reason};
use super::formula::{self, Expr, Read};
use super::graph::{self, areas_at, cells_in, dependencies, is_formula, CellId, Extent, Names};
#[derive(Clone, Debug)]
pub struct Shape {
pub text: String,
pub expr: Result<Expr, String>,
pub anchor: CellId,
missing: Vec<String>,
risky: bool,
}
impl Shape {
pub fn unread_at(&self, d_row: i64, d_col: i64) -> Option<String> {
if !self.risky || (d_row == 0 && d_col == 0) || self.expr.is_err() {
return None;
}
formula::parse(&refers::shift_a1_references(&self.text, d_row, d_col)).err()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Place {
pub shape: usize,
pub d_row: i64,
pub d_col: i64,
}
#[derive(Clone, Debug, Default)]
pub struct Shapes {
shapes: Vec<Shape>,
by_r1c1: HashMap<String, usize>,
by_shared: HashMap<(usize, u32), usize>,
places: FxHashMap<CellId, Place>,
unread: FxHashMap<CellId, String>,
order: Vec<CellId>,
cycles: Vec<Vec<CellId>>,
}
impl Shapes {
pub fn new() -> Shapes {
Shapes::default()
}
pub fn of(
model: &Workbook,
sheets: usize,
library: &Library,
names: &Names,
extents: &[Extent],
) -> Shapes {
let mut s = Shapes::new();
for (si, sheet) in model.sheets.iter().enumerate().take(sheets) {
let masters = shared_masters(sheet);
for (&(row, col), cell) in &sheet.cells {
s.place_with((si, row, col), &cell.content, &masters, library);
}
}
s.plan(model, names, extents);
s
}
pub fn replace(&mut self, model: &Workbook, id: CellId, library: &Library) {
self.places.remove(&id);
self.unread.remove(&id);
let Some(sheet) = model.sheets.get(id.0) else {
return;
};
let Some(cell) = sheet.cells.get(&(id.1, id.2)) else {
return;
};
if !is_formula(&cell.content) {
return;
}
let masters = shared_masters(sheet);
self.place_with(id, &cell.content, &masters, library);
}
fn place_with(
&mut self,
id: CellId,
content: &Content,
masters: &HashMap<u32, (&str, u32, u32)>,
library: &Library,
) {
let (si, row, col) = id;
let (shape, anchor) = match content {
Content::SharedMaster { si: index, .. } | Content::SharedChild { si: index } => {
let Some(&(text, m_row, m_col)) = masters.get(index) else {
return;
};
let anchor = (si, m_row, m_col);
let key = (si, *index);
let shape = match self.by_shared.get(&key) {
Some(&s) => s,
None => {
let s = self.add(strip_future_prefixes(text), anchor, library);
self.by_shared.insert(key, s);
s
}
};
(shape, anchor)
}
Content::Formula(t) | Content::DynamicFormula(t) => {
let text = strip_future_prefixes(t);
let key = refers::r1c1(&text, row, col);
match self.by_r1c1.get(&key) {
Some(&s) => (s, self.shapes[s].anchor),
None => {
let s = self.add(text, id, library);
self.by_r1c1.insert(key, s);
(s, id)
}
}
}
_ => return,
};
let (d_row, d_col) = (
i64::from(row) - i64::from(anchor.1),
i64::from(col) - i64::from(anchor.2),
);
self.places.insert(
id,
Place {
shape,
d_row,
d_col,
},
);
match self.shapes[shape].unread_at(d_row, d_col) {
Some(construct) => self.unread.insert(id, construct),
None => self.unread.remove(&id),
};
}
fn add(&mut self, text: String, anchor: CellId, library: &Library) -> usize {
let expr = formula::parse(&text);
let mut missing: Vec<String> = formula::calls(&text)
.into_iter()
.filter(|name| !library.has(name))
.collect();
let risky = refers::scan(&text).iter().any(|r| {
r.kind == refers::Kind::ThreeD
|| (r.kind == refers::Kind::Broken && r.shape != refers::Shape::None)
});
missing.sort_unstable();
missing.dedup();
self.shapes.push(Shape {
text,
expr,
anchor,
missing,
risky,
});
self.shapes.len() - 1
}
pub fn plan(&mut self, model: &Workbook, names: &Names, extents: &[Extent]) {
let mut ids: Vec<CellId> = self.places.keys().copied().collect();
ids.sort_unstable();
let index: HashMap<CellId, usize> =
ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
let mut holds = vec![false; model.sheets.len()];
for id in &ids {
holds[id.0] = true;
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); ids.len()];
let mut deps: Vec<CellId> = Vec::new();
for (vi, id) in ids.iter().enumerate() {
deps.clear();
let p = self.places[id];
let shape = &self.shapes[p.shape];
let read = !self.unread.contains_key(id);
match &shape.expr {
Ok(e) if read => e.each_read(&mut |r| match r {
Read::Ref(re) => {
let Ok(areas) = areas_at(model, id.0, re, p.d_row, p.d_col) else {
return;
};
for a in &areas {
if !holds.get(a.sheet).copied().unwrap_or(false) {
continue;
}
let sheet = &model.sheets[a.sheet];
let extent = extents.get(a.sheet).copied().flatten();
for ((row, col), cell) in cells_in(sheet, extent, a) {
if is_formula(&cell.content) {
deps.push((a.sheet, row, col));
}
}
}
}
Read::Name { name, sheet } => {
let scope = sheet.and_then(|s| model.find_sheet(s));
if let Some(t) = names.resolve(name, scope, id.0) {
dependencies(
model,
names,
extents,
scope.unwrap_or(id.0),
t,
&mut deps,
);
}
}
}),
_ => {
let moved = refers::shift_a1_references(&shape.text, p.d_row, p.d_col);
dependencies(model, names, extents, id.0, &moved, &mut deps);
}
}
let edges = &mut adj[vi];
for d in &deps {
if let Some(&di) = index.get(d) {
edges.push(di);
}
}
edges.sort_unstable();
edges.dedup();
}
let components = graph::tarjan(&adj);
self.order.clear();
self.cycles.clear();
for comp in components {
if comp.len() == 1 && !adj[comp[0]].contains(&comp[0]) {
self.order.push(ids[comp[0]]);
} else {
let mut cells: Vec<CellId> = comp.iter().map(|&i| ids[i]).collect();
cells.sort_unstable();
self.cycles.push(cells);
}
}
self.cycles.sort();
}
pub fn order(&self) -> &[CellId] {
&self.order
}
pub fn cycles(&self) -> &[Vec<CellId>] {
&self.cycles
}
pub fn formulas(&self) -> usize {
self.places.len()
}
pub fn shapes(&self) -> usize {
self.shapes.len()
}
pub fn holds(&self, id: CellId) -> bool {
self.places.contains_key(&id)
}
pub fn place(&self, id: CellId) -> Option<Place> {
self.places.get(&id).copied()
}
pub fn shape(&self, index: usize) -> Option<&Shape> {
self.shapes.get(index)
}
pub fn evaluate(&self, env: &dyn Env, library: &Library, id: CellId) -> Computed {
let Some(p) = self.places.get(&id) else {
return Computed::NotComputed(Reason::Construct(String::new()));
};
let shape = &self.shapes[p.shape];
if let Some(construct) = self.unread.get(&id) {
return Computed::NotComputed(Reason::Construct(construct.clone()));
}
match &shape.expr {
Err(construct) => Computed::NotComputed(Reason::Construct(construct.clone())),
Ok(e) => {
let ctx = Ctx::at(env, library, p.d_row, p.d_col);
match ctx.eval(e).and_then(|arg| ctx.one(&arg)) {
Ok(v) => Computed::Value(v.settled()),
Err(reason) => Computed::NotComputed(reason),
}
}
}
}
pub fn first_refusal(
&self,
model: &Workbook,
only: Option<&[CellId]>,
) -> Option<(CellId, Refusal)> {
if let Some(cycle) = self.cycles.first() {
let cells: Vec<String> = cycle.iter().map(|id| spell_cell(model, *id)).collect();
return Some((
cycle[0],
raise("calc-cycle", &[("cells", &cells.join(", "))]),
));
}
let mut ids: Vec<CellId> = match only {
Some(cells) => cells
.iter()
.copied()
.filter(|id| self.places.contains_key(id))
.collect(),
None => self.places.keys().copied().collect(),
};
ids.sort_unstable();
for id in &ids {
let shape = &self.shapes[self.places[id].shape];
if let Some(name) = shape.missing.first() {
return Some((
*id,
raise(
"calc-function-not-computed",
&[("cell", &spell_cell(model, *id)), ("function", name)],
),
));
}
}
for id in &ids {
let p = self.places[id];
let shape = &self.shapes[p.shape];
let label = match (&shape.expr, self.unread.get(id).cloned()) {
(_, Some(unread)) => Some(unread),
(Err(construct), None) if p.d_row == 0 && p.d_col == 0 => Some(construct.clone()),
(Err(construct), None) => {
let moved = refers::shift_a1_references(&shape.text, p.d_row, p.d_col);
Some(
formula::parse(&moved)
.err()
.unwrap_or_else(|| construct.clone()),
)
}
(Ok(_), None) => None,
};
if let Some(label) = label {
return Some((
*id,
raise(
"calc-construct-not-read",
&[("cell", &spell_cell(model, *id)), ("construct", &label)],
),
));
}
}
None
}
}
pub fn spell_cell(model: &Workbook, id: CellId) -> String {
let sheet = model
.sheets
.get(id.0)
.map(|s| s.name.as_str())
.unwrap_or("");
format!("{}!{}", quote_sheet(sheet), cell_ref(id.1, id.2))
}