1use crate::base::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
26use crate::model::{RepeatOf, Workbook};
27use serde_json::{Map, Value as Json};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Place {
32 pub sheet: usize,
33 pub range: RangeRef,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum ResolveError {
39 Edge(String),
41 Bad(String),
43}
44
45impl ResolveError {
46 pub fn message(&self) -> &str {
47 match self {
48 ResolveError::Edge(m) | ResolveError::Bad(m) => m,
49 }
50 }
51}
52
53impl std::fmt::Display for ResolveError {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.write_str(self.message())
56 }
57}
58
59type R<T> = Result<T, ResolveError>;
60
61fn bad<T>(msg: impl Into<String>) -> R<T> {
62 Err(ResolveError::Bad(msg.into()))
63}
64
65#[derive(Clone, Copy, Debug)]
67pub struct Env<'a> {
68 pub wb: &'a Workbook,
69 pub active_sheet: usize,
71 pub inputs: &'a [(String, Place, bool)],
73 pub output: Option<Place>,
74 pub item: Option<(RepeatOf, Place)>,
76}
77
78const REF_KEYS: &[&str] = &[
82 "active",
83 "anchor",
84 "area",
85 "areas",
86 "at",
87 "cell",
88 "cells",
89 "changing",
90 "copyTo",
91 "destination",
92 "from",
93 "fromCell",
94 "location",
95 "objective",
96 "over",
97 "range",
98 "ranges",
99 "reference",
100 "resultCells",
101 "source",
102 "sources",
103 "target",
104 "to",
105 "variables",
106];
107
108pub fn is_ref_key(key: &str) -> bool {
110 let k = key.to_ascii_lowercase();
111 REF_KEYS.iter().any(|r| r.eq_ignore_ascii_case(key)) || ["cell", "cells", "range", "ranges", "ref"].iter().any(|end| k.ends_with(end))
112}
113
114fn is_general_key(key: &str) -> bool {
118 ["area", "areas", "at", "destination", "from", "location", "source", "sources", "target", "to"].iter().any(|k| k.eq_ignore_ascii_case(key))
119}
120
121fn is_bare_item_word(text: &str) -> bool {
123 let t = text.trim();
124 t.eq_ignore_ascii_case("row") || t.eq_ignore_ascii_case("column")
125}
126
127const MAX_JSON_DEPTH: usize = 64;
129
130pub fn is_token(text: &str, env: &Env) -> bool {
133 starts_like_token(text, |base| env.inputs.iter().any(|(n, ..)| n.eq_ignore_ascii_case(base)))
134}
135
136pub fn reads_as_token(text: &str, inputs: &[&str]) -> bool {
138 starts_like_token(text, |base| inputs.iter().any(|n| n.eq_ignore_ascii_case(base)))
139}
140
141fn starts_like_token(text: &str, is_input: impl Fn(&str) -> bool) -> bool {
142 let t = text.trim();
143 if t.get(..5).is_some_and(|h| h.eq_ignore_ascii_case("name:")) {
144 return true;
145 }
146 let n: usize = t.chars().take_while(|c| c.is_alphanumeric() || *c == '_').map(char::len_utf8).sum();
147 let base = t.get(..n).unwrap_or("");
148 if t.get(n..).unwrap_or("").trim_start().starts_with('!') {
149 return false;
150 }
151 is_builtin(base) || is_input(base)
152}
153
154fn is_builtin(base: &str) -> bool {
155 ["Output", "Row", "Column"].iter().any(|b| b.eq_ignore_ascii_case(base))
156}
157
158pub fn resolve_ref(text: &str, env: &Env) -> R<Place> {
160 let t = text.trim();
161 if is_token(t, env) {
162 return resolve_token(t, env);
163 }
164 plain_ref(t, env)
165}
166
167fn plain_ref(t: &str, env: &Env) -> R<Place> {
169 let (sheet, body) = crate::engine::cmd::split_sheet(t);
170 let si = match &sheet {
171 Some(name) => env.wb.sheet_index(name).ok_or_else(|| ResolveError::Bad(format!("There is no sheet named '{name}'.")))?,
172 None => env.active_sheet,
173 };
174 if let Some(range) = RangeRef::parse(body) {
175 return Ok(Place { sheet: si, range });
176 }
177 if sheet.is_none() {
178 return named(t, env);
179 }
180 bad(format!("'{t}' isn't a reference."))
181}
182
183fn named(name: &str, env: &Env) -> R<Place> {
185 if let Some(n) = env.wb.name(name, env.active_sheet) {
186 let f = n.formula.trim().trim_start_matches('=');
189 let (sheet, body) = crate::engine::cmd::split_sheet(f);
190 let si = match sheet {
191 Some(s) => env.wb.sheet_index(&s),
192 None => n.scope.or(Some(env.active_sheet)),
193 };
194 if let (Some(sheet), Some(range)) = (si, RangeRef::parse(&body.replace('$', ""))) {
195 return Ok(Place { sheet, range });
196 }
197 return bad(format!("The name '{name}' doesn't refer to a range."));
198 }
199 if let Some((si, ti)) = env.wb.table(name)
200 && let Some(t) = env.wb.sheet(si).and_then(|sh| sh.tables.get(ti))
201 {
202 let Some(range) = t.data_range() else {
205 return bad(format!("The table '{name}' has no data rows."));
206 };
207 return Ok(Place { sheet: si, range });
208 }
209 bad(format!("There is no name or table called '{name}'."))
210}
211
212const METHODS: &[&str] = &["col", "row", "cols", "rows", "cell", "offset", "resize", "data", "below", "right"];
214
215fn name_end(run: &str, env: &Env) -> usize {
219 let fits = |name: &str| {
221 !name.is_empty()
222 && run.get(..name.len()).is_some_and(|head| head.eq_ignore_ascii_case(name))
223 && run.get(name.len()..).is_some_and(|rest| rest.is_empty() || rest.starts_with('.'))
224 };
225 let names = env.wb.names.iter().filter(|n| n.scope.is_none() || n.scope == Some(env.active_sheet)).map(|n| n.name.as_str());
226 let tables = env.wb.sheets.iter().flat_map(|sh| sh.tables.iter().map(|t| t.name.as_str()));
227 if let Some(end) = names.chain(tables).filter(|n| fits(n)).map(str::len).max() {
228 return end;
229 }
230 run.match_indices('.')
231 .map(|(i, _)| i)
232 .find(|&i| {
233 let next = run.get(i + 1..).unwrap_or("").split('.').next().unwrap_or("");
234 METHODS.iter().any(|m| m.eq_ignore_ascii_case(next))
235 })
236 .unwrap_or(run.len())
237}
238
239pub fn resolve_token(text: &str, env: &Env) -> R<Place> {
241 let t = text.trim();
242 let mut p = Parser { s: t, at: 0 };
243 let (mut place, mut header) = p.base(env)?;
244 let mut names_row = header.then_some(place.range.start.row);
246 while p.eat('.') {
247 let name = p.ident();
248 if name.is_empty() {
249 return bad(format!("'{t}': expected a method name after '.'."));
250 }
251 let args = if p.eat('(') { p.args(t)? } else { vec![] };
252 let r = place.range;
253 let method = name.to_ascii_lowercase();
254 let range = match (method.as_str(), args.as_slice()) {
255 ("col", [Arg::Int(n)]) => sub(r, None, Some((*n, *n)), t)?,
256 ("col", [Arg::Text(h)]) => {
257 let Some(row) = names_row else {
258 return bad(format!("'{t}': col(\"{h}\") needs an input with a header row; use col(n)."));
259 };
260 let c = header_col(env, place, row, h).ok_or_else(|| ResolveError::Bad(format!("'{t}': no column is headed '{h}'.")))?;
261 sub(r, None, Some((c, c)), t)?
262 }
263 ("row", [Arg::Int(n)]) => sub(r, Some((*n, *n)), None, t)?,
264 ("cols", [Arg::Span(a, b)]) => sub(r, None, Some(span(*a, *b, i64::from(r.width()))), t)?,
265 ("rows", [Arg::Span(a, b)]) => sub(r, Some(span(*a, *b, i64::from(r.height()))), None, t)?,
266 ("cell", [Arg::Int(row), Arg::Int(col)]) => {
267 let c = shift(r.start, row - 1, col - 1, t)?;
268 RangeRef::cell(c)
269 }
270 ("offset", [Arg::Int(dr), Arg::Int(dc)]) => RangeRef::new(shift(r.start, *dr, *dc, t)?, shift(r.end, *dr, *dc, t)?),
271 ("resize", [Arg::Int(h), Arg::Int(w)]) => {
272 if *h < 1 || *w < 1 {
273 return bad(format!("'{t}': resize needs at least 1 row and 1 column."));
274 }
275 RangeRef::new(r.start, shift(r.start, h - 1, w - 1, t)?)
276 }
277 ("data", []) => {
278 if header {
279 if r.height() < 2 {
281 return bad(format!("'{t}': the range is only a header row, with no data under it."));
282 }
283 RangeRef::new(CellRef::new(r.start.row + 1, r.start.col), r.end)
284 } else {
285 r
286 }
287 }
288 ("below", []) | ("below", [Arg::Int(_)]) => {
289 let n = count_arg(&args, t)?;
290 let top = shift(CellRef::new(r.end.row, r.start.col), 1, 0, t)?;
291 RangeRef::new(top, shift(CellRef::new(r.end.row, r.end.col), n, 0, t)?)
292 }
293 ("right", []) | ("right", [Arg::Int(_)]) => {
294 let n = count_arg(&args, t)?;
295 let left = shift(CellRef::new(r.start.row, r.end.col), 0, 1, t)?;
296 RangeRef::new(left, shift(r.end, 0, n, t)?)
297 }
298 _ => {
299 return bad(format!("'{t}': '{name}' with these arguments isn't a token method ({}).", METHODS.join(", ")));
300 }
301 };
302 if !matches!(method.as_str(), "col" | "cols") {
306 header = false;
307 }
308 if method != "data" {
309 names_row = None;
310 }
311 place = Place { sheet: place.sheet, range };
312 }
313 p.skip_ws();
314 if p.at < p.s.len() {
315 return bad(format!("'{t}' isn't a token: unexpected '{}'.", p.s.get(p.at..).unwrap_or("")));
316 }
317 Ok(place)
318}
319
320fn count_arg(args: &[Arg], t: &str) -> R<i64> {
322 match args.first() {
323 Some(Arg::Int(n)) if *n < 1 => bad(format!("'{t}': the count must be at least 1.")),
324 Some(Arg::Int(n)) => Ok(*n),
325 _ => Ok(1),
326 }
327}
328
329fn span(a: Option<i64>, b: Option<i64>, len: i64) -> (i64, i64) {
331 (a.unwrap_or(1), b.unwrap_or(len))
332}
333
334fn sub(r: RangeRef, rows: Option<(i64, i64)>, cols: Option<(i64, i64)>, t: &str) -> R<RangeRef> {
336 let (h, w) = (i64::from(r.height()), i64::from(r.width()));
337 let (r0, r1) = rows.unwrap_or((1, h));
338 let (c0, c1) = cols.unwrap_or((1, w));
339 if r0 < 1 || c0 < 1 || r1 < r0 || c1 < c0 || r1 > h || c1 > w {
340 return bad(format!("'{t}' is outside the range it starts from ({} rows by {} columns).", h, w));
341 }
342 Ok(RangeRef::new(shift(r.start, r0 - 1, c0 - 1, t)?, shift(r.start, r1 - 1, c1 - 1, t)?))
343}
344
345fn shift(c: CellRef, dr: i64, dc: i64, t: &str) -> R<CellRef> {
346 let row = i64::from(c.row).checked_add(dr);
347 let col = i64::from(c.col).checked_add(dc);
348 match (row, col) {
349 (Some(row), Some(col)) if (0..i64::from(MAX_ROWS)).contains(&row) && (0..i64::from(MAX_COLS)).contains(&col) => {
350 Ok(CellRef::new(row as u32, col as u32))
351 }
352 (Some(row), Some(_)) if row < 0 => Err(ResolveError::Edge(format!("'{t}' runs past the top of the sheet."))),
353 (Some(row), Some(_)) if row >= i64::from(MAX_ROWS) => Err(ResolveError::Edge(format!("'{t}' runs past the last row of the sheet."))),
354 _ => Err(ResolveError::Edge(format!("'{t}' runs past the edge of the sheet."))),
355 }
356}
357
358fn header_col(env: &Env, place: Place, row: u32, h: &str) -> Option<i64> {
360 let sh = env.wb.sheet(place.sheet)?;
361 let r = place.range;
362 (r.start.col..=r.end.col)
363 .position(|c| crate::engine::display::value_text(env.wb, sh, CellRef::new(row, c)).trim().eq_ignore_ascii_case(h.trim()))
366 .map(|i| i as i64 + 1)
367}
368
369#[derive(Debug)]
370enum Arg {
371 Int(i64),
372 Span(Option<i64>, Option<i64>),
373 Text(String),
374}
375
376struct Parser<'a> {
377 s: &'a str,
378 at: usize,
379}
380
381impl Parser<'_> {
382 fn rest(&self) -> &str {
383 self.s.get(self.at..).unwrap_or("")
384 }
385 fn skip_ws(&mut self) {
386 let n = self.rest().len() - self.rest().trim_start().len();
387 self.at += n;
388 }
389 fn eat(&mut self, c: char) -> bool {
390 self.skip_ws();
391 if self.rest().starts_with(c) {
392 self.at += c.len_utf8();
393 true
394 } else {
395 false
396 }
397 }
398 fn ident(&mut self) -> String {
399 self.skip_ws();
400 let n: usize = self.rest().chars().take_while(|c| c.is_alphanumeric() || *c == '_').map(char::len_utf8).sum();
401 let out = self.rest().get(..n).unwrap_or("").to_string();
402 self.at += n;
403 out
404 }
405 fn int(&mut self) -> Option<i64> {
406 self.skip_ws();
407 let neg = self.rest().starts_with('-');
408 let start = self.at + usize::from(neg);
409 let digits: usize = self.s.get(start..).unwrap_or("").chars().take_while(char::is_ascii_digit).count();
410 if digits == 0 || digits > 9 {
411 return None;
412 }
413 let n: i64 = self.s.get(start..start + digits)?.parse().ok()?;
414 self.at = start + digits;
415 Some(if neg { -n } else { n })
416 }
417 fn base(&mut self, env: &Env) -> R<(Place, bool)> {
419 self.skip_ws();
420 if self.rest().get(..5).is_some_and(|h| h.eq_ignore_ascii_case("name:")) {
421 self.at += 5;
422 let n: usize = self.rest().chars().take_while(|c| c.is_alphanumeric() || matches!(c, '_' | '\\' | '.')).map(char::len_utf8).sum();
425 let run = self.rest().get(..n).unwrap_or("").to_string();
426 let end = name_end(&run, env);
427 let name = run.get(..end).unwrap_or("");
428 if name.is_empty() {
429 return bad("'Name:' needs a defined name after it.");
430 }
431 self.at += end;
432 return Ok((named(name, env)?, false));
433 }
434 let base = self.ident();
435 if base.eq_ignore_ascii_case("Output") {
436 return env.output.map(|p| (p, false)).ok_or_else(|| ResolveError::Bad("'Output' is used, but the macro has no output anchor.".into()));
437 }
438 if base.eq_ignore_ascii_case("Row") || base.eq_ignore_ascii_case("Column") {
439 return match env.item {
440 Some((_, p)) => Ok((p, false)),
441 None => bad(format!("'{base}' only means something inside a Repeat step.")),
442 };
443 }
444 match env.inputs.iter().find(|(n, ..)| n.eq_ignore_ascii_case(&base)) {
445 Some((_, p, header)) => Ok((*p, *header)),
446 None => bad(format!("'{base}' isn't an input of this macro.")),
447 }
448 }
449 fn args(&mut self, t: &str) -> R<Vec<Arg>> {
450 let mut out = Vec::new();
451 if self.eat(')') {
452 return Ok(out);
453 }
454 loop {
455 self.skip_ws();
456 if self.rest().starts_with('"') {
457 self.at += 1;
458 let n = self.rest().find('"').ok_or_else(|| ResolveError::Bad(format!("'{t}': a quote isn't closed.")))?;
459 out.push(Arg::Text(self.rest().get(..n).unwrap_or("").to_string()));
460 self.at += n + 1;
461 } else {
462 let a = self.int();
463 self.skip_ws();
464 if self.rest().starts_with("..") {
465 self.at += 2;
466 let b = self.int();
467 out.push(Arg::Span(a, b));
468 } else {
469 out.push(Arg::Int(a.ok_or_else(|| ResolveError::Bad(format!("'{t}': expected a whole number.")))?));
470 }
471 }
472 if out.len() > 4 {
473 return bad(format!("'{t}': too many arguments."));
474 }
475 if self.eat(')') {
476 return Ok(out);
477 }
478 if !self.eat(',') {
479 return bad(format!("'{t}': expected ',' or ')'."));
480 }
481 }
482 }
483}
484
485pub fn a1(place: Place, env: &Env) -> String {
487 if place.sheet == env.active_sheet {
488 return place.range.a1();
489 }
490 let name = env.wb.sheet(place.sheet).map(|s| s.name.as_str()).unwrap_or("");
491 format!("{}!{}", quote_sheet(name), place.range.a1())
492}
493
494pub fn quote_sheet(name: &str) -> String {
496 crate::base::quote_sheet(name)
499}
500
501pub fn resolve_params(params: &Json, env: &Env) -> R<Json> {
503 walk(params, None, env, 0)
504}
505
506fn walk(v: &Json, key: Option<&str>, env: &Env, depth: usize) -> R<Json> {
507 if depth > MAX_JSON_DEPTH {
508 return bad("The step's parameters nest too deeply.");
509 }
510 Ok(match v {
511 Json::Object(o) => {
512 if o.len() == 1
513 && let Some(Json::String(t)) = o.get("$ref")
514 {
515 return Ok(Json::String(a1(resolve_ref(t, env)?, env)));
516 }
517 if o.len() == 1
519 && let Some(Json::String(t)) = o.get("$text")
520 {
521 return Ok(Json::String(t.clone()));
522 }
523 let mut out = Map::new();
524 for (k, x) in o {
525 out.insert(k.clone(), walk(x, Some(k), env, depth + 1)?);
526 }
527 Json::Object(out)
528 }
529 Json::Array(a) => Json::Array(a.iter().map(|x| walk(x, key, env, depth + 1)).collect::<R<Vec<_>>>()?),
531 Json::String(s) => {
532 if key.is_some_and(|k| is_ref_key(k) && !(is_general_key(k) && is_bare_item_word(s))) && is_token(s, env) {
533 Json::String(a1(resolve_token(s, env)?, env))
534 } else {
535 Json::String(interpolate(s, env)?)
536 }
537 }
538 other => other.clone(),
539 })
540}
541
542pub fn interpolate(s: &str, env: &Env) -> R<String> {
545 if !s.contains("{{") {
546 return Ok(s.to_string());
547 }
548 let mut out = String::with_capacity(s.len());
549 let mut rest = s;
550 while let Some(i) = rest.find("{{") {
551 out.push_str(rest.get(..i).unwrap_or(""));
552 if rest.get(i..i + 4) == Some("{{{{") {
553 out.push_str("{{");
554 rest = rest.get(i + 4..).unwrap_or("");
555 continue;
556 }
557 let after = rest.get(i + 2..).unwrap_or("");
558 let Some(j) = after.find("}}") else {
559 out.push_str(rest.get(i..).unwrap_or(""));
560 return Ok(out);
561 };
562 let token = after.get(..j).unwrap_or("");
563 out.push_str(&a1(resolve_ref(token, env)?, env));
564 rest = after.get(j + 2..).unwrap_or("");
565 }
566 out.push_str(rest);
567 Ok(out)
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use serde_json::json;
574
575 fn wb() -> Workbook {
576 let mut wb = Workbook::new();
577 wb.sheets.push(std::sync::Arc::new(crate::model::Sheet::new("My Data")));
578 let sh = wb.sheet_mut(0).unwrap();
579 for (i, h) in ["Name", "Amount", "Date"].iter().enumerate() {
580 sh.set_value(CellRef::new(1, 1 + i as u32), crate::base::Value::text(*h));
581 }
582 wb.names.push(crate::model::DefinedName {
583 name: "Total".into(),
584 scope: None,
585 formula: "Sheet1!$F$20".into(),
586 comment: String::new(),
587 hidden: false,
588 });
589 wb.names.push(crate::model::DefinedName {
590 name: "Rates".into(),
591 scope: None,
592 formula: "'My Data'!$A$1:$A$5".into(),
593 comment: String::new(),
594 hidden: false,
595 });
596 wb
597 }
598
599 fn place(a1: &str) -> Place {
600 Place { sheet: 0, range: RangeRef::parse(a1).unwrap() }
601 }
602
603 fn check(env: &Env, token: &str, want: &str) {
604 match resolve_ref(token, env) {
605 Ok(p) => assert_eq!(a1(p, env), want, "{token}"),
606 Err(e) => panic!("{token}: {e}"),
607 }
608 }
609
610 #[test]
611 fn a_formula_header_matches_while_formulas_are_shown() {
612 let mut wb = wb();
615 let sh = wb.sheet_mut(0).unwrap();
616 let formula = Some(std::sync::Arc::new(crate::model::Formula::new("\"Amo\"&\"unt\"")));
617 sh.set_cell(
618 CellRef::new(1, 2),
619 crate::model::Cell { value: crate::base::Value::text("Amount"), formula, style: Default::default(), py: None },
620 );
621 sh.show_formulas = true;
622 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
623 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
624 check(&env, "Input.col(\"Amount\")", "C2:C11");
625 }
626
627 #[test]
628 fn every_token_resolves() {
629 let wb = wb();
630 let inputs = vec![
632 ("Input".to_string(), place("B2:D11"), true),
633 ("Other".to_string(), Place { sheet: 1, range: RangeRef::parse("C3:C4").unwrap() }, false),
634 ];
635 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: Some(place("G2")), item: Some((RepeatOf::Rows, place("B5:D5"))) };
636 for (token, want) in [
637 ("Input", "B2:D11"),
638 ("input", "B2:D11"),
639 ("Input.col(2)", "C2:C11"),
640 ("Input.col(\"Amount\")", "C2:C11"),
641 ("Input.col(\"date\").data", "D3:D11"),
642 ("Input.cols(2..3).data", "C3:D11"),
643 ("Input.col(2).data.col(1)", "C3:C11"),
644 ("Input.data", "B3:D11"),
645 ("Input.data.col(2)", "C3:C11"),
646 ("Input.data.col(\"Amount\")", "C3:C11"),
647 ("Input.row(1)", "B2:D2"),
648 ("Input.rows(2..)", "B3:D11"),
649 ("Input.rows(2..4)", "B3:D5"),
650 ("Input.rows(..2)", "B2:D3"),
651 ("Input.cols(2..3)", "C2:D11"),
652 ("Input.cell(1,1)", "B2"),
653 ("Input.cell(3,2)", "C4"),
654 ("Input.cell(0,1)", "B1"),
655 ("Input.cell(12,3)", "D13"),
656 ("Input.offset(0,4)", "F2:H11"),
657 ("Input.offset(-1,-1)", "A1:C10"),
658 ("Input.cell(1,1).resize(2,3)", "B2:D3"),
659 ("Input.below()", "B12:D12"),
660 ("Input.below(2)", "B12:D13"),
661 ("Input.col(2).below", "C12"),
662 ("Input.right", "E2:E11"),
663 (" Input . col( 2 ) ", "C2:C11"),
664 ("Output", "G2"),
665 ("Output.offset(1,0)", "G3"),
666 ("Row", "B5:D5"),
667 ("Row.cell(1,2)", "C5"),
668 ("Column", "B5:D5"),
669 ("Name:Total", "F20"),
670 ("Name:Rates", "'My Data'!A1:A5"),
671 ("Name:Rates.cell(2,1)", "'My Data'!A2"),
672 ("Other", "'My Data'!C3:C4"),
673 ("B7", "B7"),
674 ("'My Data'!A1:B2", "'My Data'!A1:B2"),
675 ("Total", "F20"),
676 ] {
677 check(&env, token, want);
678 }
679 }
680
681 #[test]
682 fn bad_tokens_say_why() {
683 let wb = wb();
684 let inputs = vec![("Input".to_string(), place("B2:D11"), false), ("Headed".to_string(), place("B2:D11"), true)];
686 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
687 for (token, needle, edge) in [
688 ("Input.col(4)", "outside the range", false),
689 ("Input.col(0)", "outside the range", false),
690 ("Input.rows(5..2)", "outside the range", false),
691 ("Headed.col(\"Nope\")", "no column is headed", false),
692 ("Input.col(\"Name\")", "needs an input with a header row", false),
693 ("Headed.col(2).col(\"Amount\")", "needs an input with a header row", false),
694 ("Name:Rates.col(\"Name\")", "needs an input with a header row", false),
695 ("Input.colour(1)", "isn't a token method", false),
696 ("Input.col(1", "expected", false),
697 ("Input.col(x)", "whole number", false),
698 ("Input.col(99999999999)", "whole number", false),
699 ("Input.", "method name", false),
700 ("Input junk", "unexpected", false),
701 ("Input.resize(0,1)", "at least 1 row", false),
702 ("Output", "no output anchor", false),
703 ("Row", "inside a Repeat", false),
704 ("Name:", "needs a defined name", false),
705 ("Name:Nothing", "no name or table", false),
706 ("Nowhere!A1", "no sheet named", false),
707 ("Input.offset(-2,0)", "past the top", true),
708 ("Input.offset(0,-2)", "edge of the sheet", true),
709 ("Input.offset(1048570,0)", "past the last row", true),
710 ("Input.cell(1,16384)", "edge of the sheet", true),
711 ] {
712 let e = resolve_ref(token, &env).unwrap_err();
713 assert!(e.message().contains(needle), "{token}: {e}");
714 assert_eq!(matches!(e, ResolveError::Edge(_)), edge, "{token}");
715 }
716 }
717
718 #[test]
719 fn params_resolve_tokens_in_reference_keys_and_braces_anywhere() {
720 let wb = wb();
721 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
722 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: Some(place("F2")), item: None };
723 let p = json!({
724 "cell": "Output",
725 "range": "Input.col(2)",
726 "ranges": ["Input.row(1)", "A1"],
727 "input": "Row",
728 "value": "Input",
729 "formula": "=SUM({{Input.data.col(2)}})/{{Name:Total}}",
730 "nested": {"target": {"$ref": "Input.cell(1,1)"}, "n": 3, "flag": true},
731 "text": {"$ref": "Output"},
732 "type": "column",
733 "sheet": "Input",
734 "rowInputCell": "Input.cell(1,1)",
735 "unclosed": "a {{b"
736 });
737 let r = resolve_params(&p, &env).unwrap();
738 assert_eq!(
739 r,
740 json!({
741 "cell": "F2",
742 "range": "C2:C11",
743 "ranges": ["B2:D2", "A1"],
744 "input": "Row",
745 "value": "Input",
746 "formula": "=SUM(C3:C11)/F20",
747 "nested": {"target": "B2", "n": 3, "flag": true},
748 "text": "F2",
749 "type": "column",
750 "sheet": "Input",
751 "rowInputCell": "B2",
752 "unclosed": "a {{b"
753 })
754 );
755 let mut deep = json!(1);
757 for _ in 0..100 {
758 deep = json!([deep]);
759 }
760 assert!(resolve_params(&deep, &env).is_err());
761 assert!(resolve_params(&json!({"cell": "Input.col(9)"}), &env).is_err());
762 }
763
764 #[test]
765 fn references_on_sheets_named_like_tokens_stay_references() {
766 let mut wb = Workbook::new();
769 for name in ["Output", "Row", "Column", "Input", "Rates"] {
770 wb.sheets.push(std::sync::Arc::new(crate::model::Sheet::new(name)));
771 }
772 let inputs = vec![("Input".to_string(), place("B2:D11"), true), ("Rates".to_string(), place("F2:F5"), false)];
773 for output in [None, Some(place("H2"))] {
774 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output, item: Some((RepeatOf::Rows, place("B5:D5"))) };
775 for (sheet, i) in [("Output", 1), ("Row", 2), ("Column", 3), ("Input", 4), ("Rates", 5)] {
776 for text in [format!("{sheet}!A1:B2"), format!("'{sheet}'!A1:B2")] {
777 let want = Place { sheet: i, range: RangeRef::parse("A1:B2").unwrap() };
778 assert_eq!(resolve_ref(&text, &env), Ok(want), "{text}");
779 let a = a1(want, &env);
780 let p = json!({"range": text, "target": {"$ref": text}, "formula": format!("=SUM({{{{{text}}}}})")});
781 let r = resolve_params(&p, &env).unwrap();
782 assert_eq!(r, json!({"range": text, "target": a, "formula": format!("=SUM({a})")}), "{text}");
784 }
785 }
786 assert!(!is_token("Output !A1", &env));
787 check(&env, "Input.col(2)", "C2:C11");
789 check(&env, "Rates", "F2:F5");
790 check(&env, "Row", "B5:D5");
791 }
792 }
793
794 #[test]
795 fn defined_names_can_hold_periods() {
796 let mut wb = wb();
799 for (name, formula) in [("Sales", "Sheet1!$A$1"), ("Sales.Q1", "Sheet1!$B$2:$C$5"), ("Data.row", "'My Data'!$E$1:$E$3")] {
800 wb.names.push(crate::model::DefinedName {
801 name: name.into(),
802 scope: None,
803 formula: formula.into(),
804 comment: String::new(),
805 hidden: false,
806 });
807 }
808 let env = Env { wb: &wb, active_sheet: 0, inputs: &[], output: None, item: None };
809 for (token, want) in [
810 ("Name:Sales.Q1", "B2:C5"),
811 ("Name:sales.q1", "B2:C5"),
812 ("Name:Sales.Q1.cell(1,1)", "B2"),
813 ("Name:Sales.Q1.col(2).row(4)", "C5"),
814 ("Name:Sales", "A1"),
815 ("Name:Sales.offset(1,0)", "A2"),
816 ("Name:Data.row", "'My Data'!E1:E3"),
817 ("Name:Data.row.row(2)", "'My Data'!E2"),
818 ("Name:Data.row.cell(1,1)", "'My Data'!E1"),
819 ("Sales.Q1", "B2:C5"),
820 ] {
821 check(&env, token, want);
822 }
823 let p = json!({
825 "range": "Name:Sales.Q1",
826 "cell": "Name:Sales.Q1.cell(1,1)",
827 "a": {"$ref": "Name:Sales.Q1"},
828 "b": {"$ref": "Name:Sales.Q1.cell(2,2)"},
829 "formula": "=SUM({{Name:Sales.Q1}})+{{Name:Sales.Q1.cell(1,2)}}"
830 });
831 let r = resolve_params(&p, &env).unwrap();
832 assert_eq!(r, json!({"range": "B2:C5", "cell": "B2", "a": "B2:C5", "b": "C3", "formula": "=SUM(B2:C5)+C2"}));
833 for (token, needle) in [
835 ("Name:Nope.Q1", "no name or table called 'Nope.Q1'"),
836 ("Name:Nope.cell(1,1)", "no name or table called 'Nope'"),
837 ("Name:Sales.Q1.bogus(1)", "'bogus' with these arguments isn't a token method"),
838 ("Name:Sales.Q1.cell(1", "expected"),
839 ("Name:Sales.Q1.", "method name"),
840 ("Name:.cell(1,1)", "needs a defined name"),
841 ] {
842 let e = resolve_ref(token, &env).unwrap_err();
843 assert!(e.message().contains(needle), "{token}: {e}");
844 let e = resolve_params(&json!({"formula": format!("={{{{{token}}}}}")}), &env).unwrap_err();
845 assert!(e.message().contains(needle), "{token} in braces: {e}");
846 }
847 }
848
849 #[test]
850 fn sheet_names_are_quoted_when_needed() {
851 for (name, want) in [
854 ("Sheet1", "Sheet1"),
855 ("My Data", "'My Data'"),
856 ("O'Hara", "'O''Hara'"),
857 ("2024", "'2024'"),
858 ("", "''"),
859 ("Q1", "'Q1'"),
860 ("R1C1", "'R1C1'"),
861 ("TRUE", "'TRUE'"),
862 ("Sales.Q1", "Sales.Q1"),
863 ] {
864 assert_eq!(quote_sheet(name), want, "{name}");
865 }
866 }
867
868 #[test]
869 fn a_bare_row_or_column_under_a_general_key_is_text() {
870 let wb = wb();
874 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
875 for item in [None, Some((RepeatOf::Rows, place("B5:D5")))] {
876 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: Some(place("F2")), item };
877 let p = json!({"word": "Colum", "to": "Column", "area": "row", "from": "Input", "at": "Output"});
878 let r = resolve_params(&p, &env).unwrap();
879 assert_eq!(r, json!({"word": "Colum", "to": "Column", "area": "row", "from": "B2:D11", "at": "F2"}), "{item:?}");
880 }
881 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: Some((RepeatOf::Rows, place("B5:D5"))) };
882 let p = json!({"to": "Row.col(2)", "range": "Row", "area": {"$ref": "Row"}, "text": "{{Row}}"});
883 let r = resolve_params(&p, &env).unwrap();
884 assert_eq!(r, json!({"to": "C5", "range": "B5:D5", "area": "B5:D5", "text": "B5:D5"}));
885 }
886
887 #[test]
888 fn a_header_row_alone_and_a_table_without_data_rows_are_refused() {
889 let mut wb = wb();
890 let sh = wb.sheet_mut(0).unwrap();
891 sh.tables.push(crate::model::Table {
892 id: 1,
893 name: "Empty".into(),
894 range: RangeRef::parse("H1:J1").unwrap(),
895 header_row: true,
896 totals_row: false,
897 columns: Vec::new(),
898 style: String::new(),
899 banded_rows: false,
900 banded_cols: false,
901 first_col: false,
902 last_col: false,
903 filter_button: false,
904 filter: Vec::new(),
905 table_type: None,
906 connection_id: None,
907 });
908 let inputs = vec![("Input".to_string(), place("B2:D2"), true), ("Plain".to_string(), place("B2:D2"), false)];
909 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
910 assert!(resolve_ref("Input.data", &env).unwrap_err().to_string().contains("only a header row"));
911 assert!(resolve_ref("Input.data.col(2)", &env).unwrap_err().to_string().contains("only a header row"));
912 check(&env, "Plain.data", "B2:D2");
913 check(&env, "Input.col(2)", "C2");
914 assert!(resolve_ref("Name:Empty", &env).unwrap_err().to_string().contains("no data rows"));
915 }
916
917 #[test]
918 fn doubled_braces_are_literal_braces() {
919 let wb = wb();
920 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
921 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
922 assert_eq!(interpolate("Dear {{{{name}},", &env).unwrap(), "Dear {{name}},");
923 assert_eq!(interpolate("{{{{ {{Input.cell(1,1)}} }}", &env).unwrap(), "{{ B2 }}");
924 assert!(interpolate("Dear {{name}},", &env).is_err(), "an unknown token is still an error");
925 }
926
927 #[test]
928 fn text_marked_as_text_is_never_a_token() {
929 let wb = wb();
932 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
933 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
934 for text in ["Output", "Input", "Name:Nowhere", "Row", "Dear {{name}}"] {
935 let p = json!({"range": {"$text": text}, "input": {"$text": text}});
936 assert_eq!(resolve_params(&p, &env).unwrap(), json!({"range": text, "input": text}), "{text}");
937 assert_ne!(resolve_params(&json!({"range": text}), &env).ok(), Some(json!({"range": text})), "{text}");
938 assert!(reads_as_token(text, &["Input"]) || text.contains("{{"), "{text}");
939 }
940 assert!(!reads_as_token("Output!A1", &["Input"]) && !reads_as_token("B2", &["Input"]));
941 }
942
943 #[test]
944 fn a_defined_name_on_a_sheet_with_a_dollar_sign_resolves() {
945 let mut wb = wb();
946 wb.sheets.push(std::sync::Arc::new(crate::model::Sheet::new("US$")));
947 wb.names.push(crate::model::DefinedName {
948 name: "Dollars".into(),
949 scope: None,
950 formula: "'US$'!$F$20".into(),
951 comment: String::new(),
952 hidden: false,
953 });
954 let env = Env { wb: &wb, active_sheet: 0, inputs: &[], output: None, item: None };
955 check(&env, "Name:Dollars", "'US$'!F20");
956 }
957
958 #[test]
959 fn below_and_right_refuse_a_count_under_one() {
960 let wb = wb();
961 let inputs = vec![("Input".to_string(), place("B2:D11"), true)];
962 let env = Env { wb: &wb, active_sheet: 0, inputs: &inputs, output: None, item: None };
963 check(&env, "Input.below", "B12:D12");
964 check(&env, "Input.below(2)", "B12:D13");
965 check(&env, "Input.right(1)", "E2:E11");
966 for bad in ["Input.below(0)", "Input.below(-3)", "Input.right(0)"] {
967 assert!(resolve_ref(bad, &env).unwrap_err().to_string().contains("at least 1"), "{bad}");
968 }
969 }
970}