Defined names with relative references
A name whose references have no $ was evaluated as if they were absolute. In Excel such references are relative: defined from B2, =Sheet1!A2 means "the cell to the left", and the file stores it relative to A1 (Sheet1!XFD1, wrapping around the sheet's edge). Name formulas with relative references are now moved by the using cell's offset from A1, wrapping at the edges, and formulas.defineName takes relative references as relative to the active cell, as Excel's Name Manager does, and stores them relative to A1.
This commit is contained in:
parent
743ca809cc
commit
683ff6003c
|
|
@ -249,6 +249,13 @@ impl<'h> Evaluator<'h> {
|
|||
let scope = def.scope.unwrap_or(self.sheet);
|
||||
return match gridcraft_formula::parse(&text) {
|
||||
Ok(expr) => {
|
||||
// Relative references in a name are relative to A1: used here, they move by
|
||||
// this cell's offset from it.
|
||||
let expr = if gridcraft_formula::adjust::has_relative_refs(&expr) {
|
||||
gridcraft_formula::adjust::shift_relative_wrapping(expr, self.at.row as i64, self.at.col as i64)
|
||||
} else {
|
||||
expr
|
||||
};
|
||||
let saved = self.sheet;
|
||||
// Unqualified references in a name refer to the scope sheet.
|
||||
self.sheet = scope;
|
||||
|
|
|
|||
|
|
@ -261,6 +261,32 @@ fn references_functions() {
|
|||
assert_eq!(t.num("B6"), 105.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_names() {
|
||||
let mut t = T::new();
|
||||
// As files store them: relative to A1. `Left` is the cell to the left, `Above3` the three
|
||||
// cells above, `Mixed` column A of the same row.
|
||||
for (name, formula) in [("Left", "Sheet1!XFD1"), ("Above3", "Sheet1!A1048574:A1048576"), ("Mixed", "Sheet1!$A1")] {
|
||||
t.wb.names.push(gridcraft_model::DefinedName {
|
||||
name: name.into(),
|
||||
scope: None,
|
||||
formula: formula.into(),
|
||||
comment: String::new(),
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
t.calc.rebuild(&t.wb);
|
||||
for r in 1..=5 {
|
||||
t.set(&format!("A{r}"), &(r * 10).to_string());
|
||||
}
|
||||
t.set("B2", "=Left");
|
||||
assert_eq!(t.num("B2"), 20.0);
|
||||
t.set("B4", "=Left+Mixed");
|
||||
assert_eq!(t.num("B4"), 80.0);
|
||||
t.set("A6", "=SUM(Above3)");
|
||||
assert_eq!(t.num("A6"), 120.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spills() {
|
||||
let mut t = T::new();
|
||||
|
|
|
|||
|
|
@ -253,7 +253,15 @@ fn define_name(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
Some(sh) => Some(d.wb.sheet_index(sh).ok_or_else(|| bad("formulas.defineName", "no such sheet"))?),
|
||||
};
|
||||
let refers = match str_param(p, "refersTo") {
|
||||
Some(r) => r.trim_start_matches('=').to_string(),
|
||||
// As in Excel, relative references are relative to the active cell; names keep them
|
||||
// relative to A1, as files do.
|
||||
Some(r) => match gridcraft_formula::parse(r.trim_start_matches('=')) {
|
||||
Ok(e) if gridcraft_formula::adjust::has_relative_refs(&e) => {
|
||||
let at = d.selection.active;
|
||||
gridcraft_formula::print(&gridcraft_formula::adjust::shift_relative_wrapping(e, -(at.row as i64), -(at.col as i64)))
|
||||
}
|
||||
_ => r.trim_start_matches('=').to_string(),
|
||||
},
|
||||
None => {
|
||||
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
|
||||
let r = d.selection.current();
|
||||
|
|
@ -564,3 +572,27 @@ fn calc_options(s: &mut Session, p: &Json) -> Result<Json> {
|
|||
Ok(json!({"mode": format!("{:?}", cx.wb.calc.mode)}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn names_with_relative_references() {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3]]})).unwrap();
|
||||
// Defined from B2, `=Sheet1!A2` is "the cell to the left".
|
||||
s.execute("selection.set", json!({"range": "B2"})).unwrap();
|
||||
let r = s.execute("formulas.defineName", json!({"name": "LeftCell", "refersTo": "=Sheet1!A2"})).unwrap();
|
||||
assert_eq!(r["refersTo"], "=Sheet1!XFD1");
|
||||
s.execute("cell.set", json!({"cell": "B1", "input": "=LeftCell"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "B3", "input": "=LeftCell*10"})).unwrap();
|
||||
let v = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["value"].clone();
|
||||
assert_eq!(v(&mut s, "B1"), json!(1.0));
|
||||
assert_eq!(v(&mut s, "B3"), json!(30.0));
|
||||
// Absolute references are kept as they are.
|
||||
let r = s.execute("formulas.defineName", json!({"name": "First", "refersTo": "=Sheet1!$A$1"})).unwrap();
|
||||
assert_eq!(r["refersTo"], "=Sheet1!$A$1");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,35 @@ pub fn shift_relative(e: Expr, dr: i64, dc: i64) -> Expr {
|
|||
})
|
||||
}
|
||||
|
||||
/// Defined names: relative parts move by `(dr, dc)`, wrapping around the sheet's edges. Files
|
||||
/// store a name's relative references as seen from A1 (a name for "the cell to the left" defined
|
||||
/// in B1 is `Sheet1!XFD1`), and a use in another cell moves them by that cell's offset from A1.
|
||||
pub fn shift_relative_wrapping(e: Expr, dr: i64, dc: i64) -> Expr {
|
||||
let wrap = |v: u32, abs: bool, d: i64, max: u32| if abs { v } else { (v as i64 + d).rem_euclid(max as i64) as u32 };
|
||||
let anchor = |a: Anchor| Anchor { row: wrap(a.row, a.row_abs, dr, MAX_ROWS), col: wrap(a.col, a.col_abs, dc, MAX_COLS), ..a };
|
||||
e.map(&mut |x| match x {
|
||||
Expr::Ref(r) => {
|
||||
let kind = match r.kind {
|
||||
RefKind::Cell(a) => RefKind::Cell(anchor(a)),
|
||||
RefKind::Range(a, b) => RefKind::Range(anchor(a), anchor(b)),
|
||||
RefKind::Rows(r0, a0, r1, a1) => RefKind::Rows(wrap(r0, a0, dr, MAX_ROWS), a0, wrap(r1, a1, dr, MAX_ROWS), a1),
|
||||
RefKind::Cols(c0, a0, c1, a1) => RefKind::Cols(wrap(c0, a0, dc, MAX_COLS), a0, wrap(c1, a1, dc, MAX_COLS), a1),
|
||||
};
|
||||
Expr::Ref(Reference { sheet: r.sheet, kind })
|
||||
}
|
||||
other => other,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the formula has a relative (no `$`) row or column anywhere.
|
||||
pub fn has_relative_refs(e: &Expr) -> bool {
|
||||
e.references().iter().any(|r| match r.kind {
|
||||
RefKind::Cell(a) => !a.row_abs || !a.col_abs,
|
||||
RefKind::Range(a, b) => !a.row_abs || !a.col_abs || !b.row_abs || !b.col_abs,
|
||||
RefKind::Rows(_, a0, _, a1) | RefKind::Cols(_, a0, _, a1) => !a0 || !a1,
|
||||
})
|
||||
}
|
||||
|
||||
fn shift_anchor(a: &Anchor, dr: i64, dc: i64) -> Option<Anchor> {
|
||||
let row = if a.row_abs { a.row as i64 } else { a.row as i64 + dr };
|
||||
let col = if a.col_abs { a.col as i64 } else { a.col as i64 + dc };
|
||||
|
|
|
|||
Loading…
Reference in New Issue