1use vla_lang::calc::graph::cells_in;
39use vla_lang::calc::library::{language, number_arg, numbers};
40use vla_lang::calc::value::{round15, text_to_number};
41use vla_lang::calc::{Area, Arg, Ctx, ErrorKind, Expr, Library, Reason, Value};
42use vla_lang::intrinsics::fold;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct Entry {
47 pub name: &'static str,
48 pub evidence: &'static str,
50}
51
52pub const SUBSET: &[Entry] = &[
54 Entry {
55 name: "IF",
56 evidence: "Enron 2, EUSES 2; SPEC.md s13",
57 },
58 Entry {
59 name: "AND",
60 evidence: "EUSES 6; SPEC.md s13",
61 },
62 Entry {
63 name: "OR",
64 evidence: "EUSES 9; SPEC.md s13",
65 },
66 Entry {
67 name: "NOT",
68 evidence: "SPEC.md s13",
69 },
70 Entry {
71 name: "SUM",
72 evidence: "Enron 1, EUSES 1; SPEC.md s13",
73 },
74 Entry {
75 name: "MIN",
76 evidence: "EUSES 11; SPEC.md s13",
77 },
78 Entry {
79 name: "MAX",
80 evidence: "EUSES 13; SPEC.md s13",
81 },
82 Entry {
83 name: "ABS",
84 evidence: "SPEC.md s13",
85 },
86 Entry {
87 name: "INT",
88 evidence: "SPEC.md s13",
89 },
90 Entry {
91 name: "MOD",
92 evidence: "SPEC.md s13: every counter wraps",
93 },
94 Entry {
95 name: "ROW",
96 evidence: "SPEC.md s13: a cell painting at the mouse",
97 },
98 Entry {
99 name: "COLUMN",
100 evidence: "SPEC.md s13: a cell painting at the mouse",
101 },
102 Entry {
103 name: "CHOOSE",
104 evidence: "SPEC.md s13: a score until INDEX comes",
105 },
106 Entry {
107 name: "SIN",
108 evidence: "SPEC.md s13: a slow oscillator",
109 },
110 Entry {
111 name: "AVERAGE",
112 evidence: "Enron 3, EUSES 8",
113 },
114 Entry {
115 name: "ROUND",
116 evidence: "Enron 5, EUSES 3",
117 },
118 Entry {
119 name: "SUMIF",
120 evidence: "Enron 11",
121 },
122 Entry {
123 name: "COUNTIF",
124 evidence: "EUSES 7",
125 },
126 Entry {
127 name: "ISNUMBER",
128 evidence: "EUSES 12",
129 },
130 Entry {
131 name: "ISBLANK",
132 evidence: "EUSES 15",
133 },
134 Entry {
135 name: "IFS",
136 evidence: "scripts/reflect/saved.xlsx, the first Excel-saved fixture",
137 },
138];
139
140pub const NAMED_NOT_COMPUTED: &[(&str, &str)] = &[
144 ("VLOOKUP", "KERNEL.8, the lookups"),
145 ("INDEX", "KERNEL.8, the lookups"),
146 ("MATCH", "KERNEL.8, the lookups"),
147 ("LOOKUP", "KERNEL.8, the lookups"),
148 ("SUBTOTAL", "KERNEL.8, the lookups"),
149 ("DAVERAGE", "KERNEL.8, the database functions"),
150 ("CONCATENATE", "KERNEL.8, the text functions"),
151 ("MONTH", "KERNEL.8, the dates"),
152 ("NOW", "volatile: an engine refuses it, a door shows it"),
153 ("OFFSET", "unreadable by the reader's rule"),
154 ("INDIRECT", "unreadable by the reader's rule"),
155 (
156 "HYPERLINK",
157 "a link, not a value; the formula sink refuses it",
158 ),
159];
160
161pub fn library() -> Library {
163 let mut lib = language();
164 lib.register_form("IFS", ifs);
165 lib.register("AVERAGE", average);
166 lib.register("ROUND", round);
167 lib.register("SUMIF", sumif);
168 lib.register("COUNTIF", countif);
169 lib.register("ISNUMBER", isnumber);
170 lib.register("ISBLANK", isblank);
171 lib
172}
173
174fn ifs<'e>(ctx: &Ctx<'_>, args: &'e [Expr]) -> Result<Arg<'e>, Reason> {
178 if args.len() < 2 || args.len() % 2 == 1 {
179 return Ok(Arg::Scalar(Value::Error(ErrorKind::NA)));
180 }
181 for pair in args.chunks(2) {
182 match ctx.truth(&pair[0])? {
183 Ok(true) => return ctx.eval(&pair[1]),
184 Ok(false) => {}
185 Err(k) => return Ok(Arg::Scalar(Value::Error(k))),
186 }
187 }
188 Ok(Arg::Scalar(Value::Error(ErrorKind::NA)))
189}
190
191fn average(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
193 Ok(match numbers(ctx, args)? {
194 Err(k) => Value::Error(k),
195 Ok(ns) if ns.is_empty() => Value::Error(ErrorKind::Div0),
196 Ok(ns) => Value::Number(ns.iter().sum::<f64>() / ns.len() as f64),
197 })
198}
199
200fn round(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
206 if args.len() != 2 {
207 return Ok(Value::Error(ErrorKind::Value));
208 }
209 let x = match number_arg(ctx, args, 0)? {
210 Ok(n) => n,
211 Err(k) => return Ok(Value::Error(k)),
212 };
213 let digits = match number_arg(ctx, args, 1)? {
214 Ok(n) => n.trunc(),
215 Err(k) => return Ok(Value::Error(k)),
216 };
217 let rounded = if digits >= 0.0 {
218 let m = 10f64.powi(digits.min(300.0) as i32);
219 round15(x * m).round() / m
220 } else {
221 let p = 10f64.powi((-digits).min(300.0) as i32);
222 round15(x / p).round() * p
223 };
224 Ok(vla_lang::calc::eval::finite(rounded))
225}
226
227#[derive(Clone, Debug, PartialEq)]
229enum Criterion {
230 Eq(Target),
231 Ne(Target),
232 Lt(Target),
233 Gt(Target),
234 Le(Target),
235 Ge(Target),
236}
237
238#[derive(Clone, Debug, PartialEq)]
239enum Target {
240 Number(f64),
241 Text(String),
243 Bool(bool),
244 Error(ErrorKind),
245 Blank,
246}
247
248fn criterion(v: &Value) -> Criterion {
254 let text = match v {
255 Value::Number(n) => return Criterion::Eq(Target::Number(*n)),
256 Value::Bool(b) => return Criterion::Eq(Target::Bool(*b)),
257 Value::Error(k) => return Criterion::Eq(Target::Error(*k)),
258 Value::Empty => return Criterion::Eq(Target::Blank),
259 Value::Text(t) => t.as_str(),
260 };
261 let (op, rest): (fn(Target) -> Criterion, &str) = if let Some(r) = text.strip_prefix("<>") {
262 (Criterion::Ne, r)
263 } else if let Some(r) = text.strip_prefix("<=") {
264 (Criterion::Le, r)
265 } else if let Some(r) = text.strip_prefix(">=") {
266 (Criterion::Ge, r)
267 } else if let Some(r) = text.strip_prefix('<') {
268 (Criterion::Lt, r)
269 } else if let Some(r) = text.strip_prefix('>') {
270 (Criterion::Gt, r)
271 } else if let Some(r) = text.strip_prefix('=') {
272 (Criterion::Eq, r)
273 } else {
274 (Criterion::Eq, text)
275 };
276 let target = if rest.is_empty() {
277 Target::Blank
278 } else if let Some(n) = text_to_number(rest) {
279 Target::Number(n)
280 } else if let Some(k) = ErrorKind::parse(rest) {
281 Target::Error(k)
282 } else {
283 match fold(rest).as_str() {
284 "true" => Target::Bool(true),
285 "false" => Target::Bool(false),
286 _ => Target::Text(fold(rest)),
287 }
288 };
289 op(target)
290}
291
292fn meets(cell: &Value, c: &Criterion) -> bool {
297 use std::cmp::Ordering::*;
298 let ordering = |target: &Target| -> Option<std::cmp::Ordering> {
299 match (cell, target) {
300 (Value::Number(x), Target::Number(n)) => x.partial_cmp(n),
301 (Value::Text(t), Target::Number(n)) => {
302 text_to_number(t).and_then(|x| (x == *n).then_some(Equal))
303 }
304 (Value::Text(t), Target::Text(pattern)) => {
305 if wildcard(pattern, &fold(t)) {
306 Some(Equal)
307 } else {
308 Some(fold(t).cmp(pattern))
309 }
310 }
311 (Value::Bool(b), Target::Bool(want)) => Some(b.cmp(want)),
312 (Value::Error(k), Target::Error(want)) => (k == want).then_some(Equal),
313 (Value::Empty, Target::Blank) => Some(Equal),
314 (Value::Text(t), Target::Blank) if t.is_empty() => Some(Equal),
315 _ => None,
316 }
317 };
318 match c {
319 Criterion::Eq(t) => ordering(t) == Some(Equal),
320 Criterion::Ne(t) => ordering(t) != Some(Equal),
321 Criterion::Lt(t) => matches!(
322 (ordering(t), t),
323 (Some(Less), Target::Number(_) | Target::Text(_))
324 ),
325 Criterion::Gt(t) => matches!(
326 (ordering(t), t),
327 (Some(Greater), Target::Number(_) | Target::Text(_))
328 ),
329 Criterion::Le(t) => matches!(
330 (ordering(t), t),
331 (
332 Some(Less) | Some(Equal),
333 Target::Number(_) | Target::Text(_)
334 )
335 ),
336 Criterion::Ge(t) => matches!(
337 (ordering(t), t),
338 (
339 Some(Greater) | Some(Equal),
340 Target::Number(_) | Target::Text(_)
341 )
342 ),
343 }
344}
345
346fn wildcard(pattern: &str, text: &str) -> bool {
349 fn go(p: &[char], t: &[char]) -> bool {
350 match p.split_first() {
351 None => t.is_empty(),
352 Some(('*', rest)) => (0..=t.len()).any(|i| go(rest, &t[i..])),
353 Some(('?', rest)) => !t.is_empty() && go(rest, &t[1..]),
354 Some(('~', rest)) => match (rest.split_first(), t.split_first()) {
355 (Some((want, rest2)), Some((have, t2))) => want == have && go(rest2, t2),
356 (None, _) => t.is_empty(),
357 _ => false,
358 },
359 Some((want, rest)) => match t.split_first() {
360 Some((have, t2)) => want == have && go(rest, t2),
361 None => false,
362 },
363 }
364 }
365 let p: Vec<char> = pattern.chars().collect();
366 let t: Vec<char> = text.chars().collect();
367 if !p.iter().any(|c| matches!(c, '*' | '?' | '~')) {
368 return p == t;
369 }
370 go(&p, &t)
371}
372
373fn one_area(arg: Option<&Arg>) -> Option<Area> {
375 match arg {
376 Some(Arg::Area { areas, .. }) if areas.len() == 1 => Some(areas[0]),
377 _ => None,
378 }
379}
380
381fn countif(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
385 if args.len() != 2 {
386 return Ok(Value::Error(ErrorKind::Value));
387 }
388 let Some(area) = one_area(args.first()) else {
389 return Ok(Value::Error(ErrorKind::Value));
390 };
391 let c = criterion(&ctx.one(&args[1])?);
392 let model = ctx.env.model();
393 let Some(sheet) = model.sheets.get(area.sheet) else {
394 return Ok(Value::Error(ErrorKind::Ref));
395 };
396 let mut count = 0u64;
397 let mut present = 0u64;
398 for ((row, col), _) in cells_in(sheet, ctx.env.extent(area.sheet), &area) {
399 let v = ctx.env.read((area.sheet, row, col))?;
400 if v != Value::Empty {
401 present += 1;
402 if meets(&v, &c) {
403 count += 1;
404 }
405 }
406 }
407 let cells = u64::from(area.bottom - area.top + 1) * u64::from(area.right - area.left + 1);
408 if meets(&Value::Empty, &c) {
409 count += cells - present;
410 }
411 Ok(Value::Number(count as f64))
412}
413
414fn sumif(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
419 if args.len() < 2 || args.len() > 3 {
420 return Ok(Value::Error(ErrorKind::Value));
421 }
422 let Some(area) = one_area(args.first()) else {
423 return Ok(Value::Error(ErrorKind::Value));
424 };
425 let c = criterion(&ctx.one(&args[1])?);
426 let sum_area = match args.get(2) {
427 None => area,
428 Some(a) => match one_area(Some(a)) {
429 Some(s) => Area {
430 sheet: s.sheet,
431 top: s.top,
432 left: s.left,
433 bottom: s.top.saturating_add(area.bottom - area.top),
434 right: s.left.saturating_add(area.right - area.left),
435 },
436 None => return Ok(Value::Error(ErrorKind::Value)),
437 },
438 };
439 let model = ctx.env.model();
440 let Some(sum_sheet) = model.sheets.get(sum_area.sheet) else {
441 return Ok(Value::Error(ErrorKind::Ref));
442 };
443 let mut total = 0.0;
446 for ((row, col), _) in cells_in(sum_sheet, ctx.env.extent(sum_area.sheet), &sum_area) {
447 let value = ctx.env.read((sum_area.sheet, row, col))?;
448 let n = match value {
449 Value::Number(n) => n,
450 Value::Error(k) => return Ok(Value::Error(k)),
451 _ => continue,
452 };
453 let at = (
454 area.sheet,
455 area.top + (row - sum_area.top),
456 area.left + (col - sum_area.left),
457 );
458 let tested = ctx.env.read(at)?;
459 if meets(&tested, &c) {
460 total += n;
461 }
462 }
463 Ok(vla_lang::calc::eval::finite(total))
464}
465
466fn isnumber(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
469 if args.len() != 1 {
470 return Ok(Value::Error(ErrorKind::Value));
471 }
472 Ok(Value::Bool(matches!(ctx.one(&args[0])?, Value::Number(_))))
473}
474
475fn isblank(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
478 if args.len() != 1 {
479 return Ok(Value::Error(ErrorKind::Value));
480 }
481 Ok(Value::Bool(ctx.one(&args[0])? == Value::Empty))
482}
483
484pub fn subset_names() -> Vec<&'static str> {
486 SUBSET.iter().map(|e| e.name).collect()
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use vla_lang::calc::library::NAMES as LANGUAGE_NAMES;
493 use vla_lang::calc::{Calc, Computed};
494 use vla_lang::sheet::{parse_a1_range, Cell, Content, Workbook};
495
496 fn model(rows: &[(&str, &str, &str)]) -> Workbook {
497 let mut wb = Workbook::new();
498 for (sheet, addr, text) in rows {
499 let i = wb.ensure_sheet(sheet);
500 let r = parse_a1_range(addr).unwrap();
501 let content = if let Some(f) = text.strip_prefix('=') {
502 Content::Formula(f.to_string())
503 } else if let Ok(n) = text.parse::<f64>() {
504 Content::Number(n)
505 } else if *text == "TRUE" || *text == "FALSE" {
506 Content::Bool(*text == "TRUE")
507 } else {
508 Content::Text(text.to_string())
509 };
510 wb.sheets[i].set(r.top, r.left, Cell { content, style: 0 });
511 }
512 wb
513 }
514
515 fn at(calc: &Calc<'_>, addr: &str) -> Computed {
516 let r = parse_a1_range(addr).unwrap();
517 calc.computed((0, r.top, r.left)).cloned().unwrap()
518 }
519
520 fn number(calc: &Calc<'_>, addr: &str) -> f64 {
521 match at(calc, addr) {
522 Computed::Value(Value::Number(n)) => n,
523 other => panic!("{addr}: {other:?}"),
524 }
525 }
526
527 #[test]
528 fn the_subset_table_is_the_library_and_the_language_comes_first() {
529 let lib = library();
530 let mut names: Vec<&str> = SUBSET.iter().map(|e| e.name).collect();
531 names.sort_unstable();
532 assert_eq!(
533 lib.names(),
534 names,
535 "every function admitted has its evidence row, and no other"
536 );
537 assert_eq!(
538 &SUBSET[..LANGUAGE_NAMES.len()]
539 .iter()
540 .map(|e| e.name)
541 .collect::<Vec<_>>(),
542 &LANGUAGE_NAMES.to_vec()
543 );
544 for e in SUBSET {
545 assert!(!e.evidence.is_empty(), "{} has no evidence", e.name);
546 }
547 for (name, _) in NAMED_NOT_COMPUTED {
548 assert!(!lib.has(name), "{name} is listed as not computed");
549 }
550 }
551
552 #[test]
553 fn excel_s_functions_compute_as_excel_does() {
554 let wb = model(&[
555 ("S", "A1", "10"),
556 ("S", "A2", "20"),
557 ("S", "A3", "x"),
558 ("S", "A4", "30"),
559 ("S", "B1", "1"),
560 ("S", "B2", "2"),
561 ("S", "B4", "4"),
562 ("S", "C1", "=AVERAGE(A1:A4)"),
563 ("S", "C2", "=AVERAGE(A3)"),
564 ("S", "C3", "=ROUND(2.675,2)"),
565 ("S", "C4", "=ROUND(-2.5,0)"),
566 ("S", "C5", "=ROUND(1234.5678,-2)"),
567 ("S", "C6", "=ROUND(1.234,1.9)"),
568 ("S", "C7", "=SUMIF(A1:A4,\">15\")"),
569 ("S", "C8", "=SUMIF(A1:A4,\">15\",B1:B4)"),
570 ("S", "C9", "=COUNTIF(A1:A4,\">15\")"),
571 ("S", "C10", "=COUNTIF(A1:A4,\"x\")"),
572 ("S", "C11", "=COUNTIF(A1:A6,\"\")"),
573 ("S", "C12", "=COUNTIF(A1:A4,\"<>10\")"),
574 ("S", "C13", "=COUNTIF(A1:A4,\"*\")"),
575 ("S", "C14", "=COUNTIF(A1:A4,10)"),
576 ("S", "C15", "=ISNUMBER(A1)"),
577 ("S", "C16", "=ISNUMBER(A3)"),
578 ("S", "C17", "=ISNUMBER(1/0)"),
579 ("S", "C18", "=ISBLANK(A5)"),
580 ("S", "C19", "=ISBLANK(A1)"),
581 ("S", "C20", "=IFS(A1>15,\"big\",TRUE,\"small\")"),
582 ("S", "C21", "=IFS(A1>15,\"big\")"),
583 ("S", "C22", "=IFS(A3,1,TRUE,2)"),
584 ("S", "C23", "=COUNTIF(B1:B4,\"<>\")"),
585 ("S", "C24", "=COUNTIF(A1:A4,\"?\")"),
586 ("S", "C25", "=SUMIF(A1:A4,\"x\",B1:B4)"),
587 ]);
588 let lib = library();
589 let calc = Calc::run(&wb, &lib);
590 assert_eq!(number(&calc, "C1"), 20.0, "the text is skipped");
591 assert_eq!(
592 at(&calc, "C2"),
593 Computed::Value(Value::Error(ErrorKind::Div0))
594 );
595 assert_eq!(number(&calc, "C3"), 2.68);
596 assert_eq!(number(&calc, "C4"), -3.0);
597 assert_eq!(number(&calc, "C5"), 1200.0);
598 assert_eq!(number(&calc, "C6"), 1.2);
599 assert_eq!(number(&calc, "C7"), 50.0);
600 assert_eq!(number(&calc, "C8"), 6.0);
601 assert_eq!(number(&calc, "C9"), 2.0);
602 assert_eq!(number(&calc, "C10"), 1.0);
603 assert_eq!(number(&calc, "C11"), 2.0, "A5 and A6 are blank");
604 assert_eq!(number(&calc, "C12"), 3.0, "the text counts as not 10");
605 assert_eq!(number(&calc, "C13"), 1.0, "* matches texts alone");
606 assert_eq!(number(&calc, "C14"), 1.0);
607 assert_eq!(at(&calc, "C15"), Computed::Value(Value::Bool(true)));
608 assert_eq!(at(&calc, "C16"), Computed::Value(Value::Bool(false)));
609 assert_eq!(
610 at(&calc, "C17"),
611 Computed::Value(Value::Bool(false)),
612 "an error is not a number and does not propagate"
613 );
614 assert_eq!(at(&calc, "C18"), Computed::Value(Value::Bool(true)));
615 assert_eq!(at(&calc, "C19"), Computed::Value(Value::Bool(false)));
616 assert_eq!(
617 at(&calc, "C20"),
618 Computed::Value(Value::Text("small".to_string()))
619 );
620 assert_eq!(
621 at(&calc, "C21"),
622 Computed::Value(Value::Error(ErrorKind::NA))
623 );
624 assert_eq!(
625 at(&calc, "C22"),
626 Computed::Value(Value::Error(ErrorKind::Value)),
627 "a text condition"
628 );
629 assert_eq!(number(&calc, "C23"), 3.0, "the non-blank cells");
630 assert_eq!(number(&calc, "C24"), 1.0);
631 assert_eq!(number(&calc, "C25"), 0.0, "B3 is absent");
632 }
633
634 #[test]
635 fn criteria_read_as_excel_reads_them() {
636 assert_eq!(
637 criterion(&Value::Text(">=5".into())),
638 Criterion::Ge(Target::Number(5.0))
639 );
640 assert_eq!(
641 criterion(&Value::Text("<>".into())),
642 Criterion::Ne(Target::Blank)
643 );
644 assert_eq!(
645 criterion(&Value::Text("=TRUE".into())),
646 Criterion::Eq(Target::Bool(true))
647 );
648 assert_eq!(
649 criterion(&Value::Text("a*".into())),
650 Criterion::Eq(Target::Text("a*".into()))
651 );
652 assert_eq!(
653 criterion(&Value::Number(3.0)),
654 Criterion::Eq(Target::Number(3.0))
655 );
656 assert!(wildcard("a*", "abc"));
657 assert!(wildcard("a?c", "abc"));
658 assert!(!wildcard("a?c", "abbc"));
659 assert!(wildcard("10~*", "10*"));
660 assert!(wildcard("*", ""));
661 assert!(meets(
662 &Value::Text("5".into()),
663 &Criterion::Eq(Target::Number(5.0))
664 ));
665 assert!(
666 !meets(
667 &Value::Text("5".into()),
668 &Criterion::Gt(Target::Number(1.0))
669 ),
670 "a text is not compared by size against a number"
671 );
672 assert!(meets(&Value::Empty, &Criterion::Ne(Target::Number(5.0))));
673 assert!(meets(
674 &Value::Text("ABC".into()),
675 &Criterion::Eq(Target::Text("abc".into()))
676 ));
677 }
678}