Files
gridcraft/crates/model/src/cell.rs
T
nihilist-narwhal c650c2828f Legacy array formulas: fill the range they were entered in
A Ctrl+Shift+Enter array formula was evaluated like a dynamic array:

- Read from a file, the cached values in its range were kept as
  constants, so the formula showed #SPILL! (any multi-cell array
  formula saved by Excel or LibreOffice).
- Entered over C2:C4 with a two-value result, C4 stayed blank where
  Excel shows #N/A, and a single-cell array formula spilled.
- On save, the values of its other cells were not cached.

The result is now fitted to the range: a single row or column repeats
across it, cells past the result show #N/A, and a larger result is cut
off. Reading drops the cached values inside the range, entering clears
the range, and saving caches its values. The formula bar shows the
formula in braces, {=B2:B3*2}.
2026-10-10 01:53:55 -05:00

87 lines
3.1 KiB
Rust

//! Cells.
use std::sync::Arc;
use gridcraft_core::{RangeRef, Value};
use gridcraft_formula::Expr;
use serde::{Deserialize, Serialize};
use crate::style::StyleId;
/// A parsed formula. `text` is the canonical formula text without the leading `=`.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Formula {
pub text: String,
#[serde(skip)]
pub expr: Option<Arc<Expr>>,
/// Legacy array formula (Ctrl+Shift+Enter) covering this range; the formula lives in the
/// top-left cell.
pub array: Option<RangeRef>,
}
impl Formula {
/// Parses `text` (with or without `=`). Unparseable formulas keep their text and evaluate to
/// `#NAME?`.
pub fn new(text: &str) -> Formula {
let body = text.strip_prefix('=').unwrap_or(text);
match gridcraft_formula::parse(body) {
Ok(e) => Formula { text: gridcraft_formula::print(&e), expr: Some(Arc::new(e)), array: None },
Err(_) => Formula { text: body.to_string(), expr: None, array: None },
}
}
pub fn from_expr(e: Expr) -> Formula {
Formula { text: gridcraft_formula::print(&e), expr: Some(Arc::new(e)), array: None }
}
/// The parsed expression, re-parsing after deserialization.
pub fn expr(&self) -> Option<Expr> {
self.expr.as_deref().cloned().or_else(|| gridcraft_formula::parse(&self.text).ok())
}
/// The parsed expression without copying it (re-parses after deserialization).
pub fn expr_arc(&self) -> Option<Arc<Expr>> {
self.expr.clone().or_else(|| gridcraft_formula::parse(&self.text).ok().map(Arc::new))
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Cell {
/// Constant value, or the cached result of the formula.
pub value: Value,
pub formula: Option<Arc<Formula>>,
pub style: StyleId,
}
impl Cell {
pub fn value(v: Value) -> Cell {
Cell { value: v, formula: None, style: StyleId::DEFAULT }
}
pub fn formula(f: Formula) -> Cell {
Cell { value: Value::Empty, formula: Some(Arc::new(f)), style: StyleId::DEFAULT }
}
/// No value, no formula and the default style: storing it is pointless.
pub fn is_blank(&self) -> bool {
self.value.is_empty() && self.formula.is_none() && self.style == StyleId::DEFAULT
}
/// What the formula bar shows.
pub fn input_text(&self) -> String {
if let Some(f) = &self.formula {
return format!("={}", f.text);
}
match &self.value {
Value::Text(t) => {
// Text that would parse as something else is shown with its apostrophe by Excel
// only when entered that way; we show it plainly.
t.to_string()
}
v => v.display(),
}
}
/// What the formula bar shows when not editing: [`input_text`](Self::input_text), with a
/// legacy array formula in braces (`{=A1:A2*2}`).
pub fn bar_text(&self) -> String {
match &self.formula {
Some(f) if f.array.is_some() => format!("{{={}}}", f.text),
_ => self.input_text(),
}
}
}