aeac86ddd2
.base model (filters, formulas, properties, views; unknown keys kept in order; limits and invalid-value errors), evaluator with the D5 type rules, lazy if and boolean operators, memoised formulas with cycle errors, depth cap 200, string/number/object/list/date/link/file/regexp functions with lambdas (regex through notescraft-regex), and the map model (Web Mercator, tile ids, viewport tiles, grid clustering, coordinate and colour parsing, OSM attribution constants; Paris z10 is tile 518/352). Bases tasks 2, 5, 6 (Claude Sonnet) and 8 (MiniMax M3.1 Flash, OpenCode Zen free route). Verified: notescraft-bases suites all pass, clippy -D warnings, xtask layers and tree clean. Known gaps: toFixed rounds ties to even, list.unique is quadratic. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
123 lines
5.7 KiB
Rust
123 lines
5.7 KiB
Rust
//! List functions, lambdas (`value index acc`), and the regexp functions.
|
|
|
|
mod common;
|
|
|
|
use common::{TestRow, cases, ev, is_error, list, n, run, s};
|
|
use notescraft_bases::Value;
|
|
|
|
fn nums(items: &[f64]) -> Value {
|
|
list(items.iter().map(|x| n(*x)).collect())
|
|
}
|
|
|
|
#[test]
|
|
fn list_functions() {
|
|
cases(&[
|
|
("[1, 2, 3].length", n(3.0)),
|
|
("[1, 2, 3].contains(2)", Value::Bool(true)),
|
|
("[1, 2, 3].contains(5)", Value::Bool(false)),
|
|
("[\"a\"].contains(\"a\")", Value::Bool(true)),
|
|
("[1, 2, 3].containsAll(2, 3)", Value::Bool(true)),
|
|
("[1, 2, 3].containsAll(2, 4)", Value::Bool(false)),
|
|
("[1, 2, 3].containsAny(3, 4)", Value::Bool(true)),
|
|
("[1, 2, 3].containsAny(4)", Value::Bool(false)),
|
|
("[1, [2, 3]].flat()", nums(&[1.0, 2.0, 3.0])),
|
|
("[1, 2, 3].isEmpty()", Value::Bool(false)),
|
|
("[].isEmpty()", Value::Bool(true)),
|
|
("[1, 2, 3].join(\",\")", s("1,2,3")),
|
|
("[\"a\", \"b\"].join(\" - \")", s("a - b")),
|
|
("[1, 2, 3].reverse()", nums(&[3.0, 2.0, 1.0])),
|
|
("[1, 2, 3, 4].slice(1, 3)", nums(&[2.0, 3.0])),
|
|
("[1, 2, 3, 4].slice(2)", nums(&[3.0, 4.0])),
|
|
("[1, 2, 3, 4].slice(-2)", nums(&[3.0, 4.0])),
|
|
("[3, 1, 2].sort()", nums(&[1.0, 2.0, 3.0])),
|
|
("[\"c\", \"a\", \"b\"].sort()", list(vec![s("a"), s("b"), s("c")])),
|
|
("[2, \"a\", null, 1].sort()", list(vec![Value::Null, n(1.0), n(2.0), s("a")])),
|
|
("[1, 2, 2, 3, 1].unique()", nums(&[1.0, 2.0, 3.0])),
|
|
("null.contains(1)", Value::Null),
|
|
("null.join(\",\")", Value::Null),
|
|
("null.isEmpty()", Value::Bool(true)),
|
|
("[1, 2, 3][0]", n(1.0)),
|
|
("[1, 2, 3][5]", Value::Null),
|
|
("[1, 2, 3][-1]", Value::Null),
|
|
]);
|
|
for src in ["[1].contains()", "[1].join(1)", "[1].slice(\"a\")", "[1].sort(1)", "[1].nosuch()", "5.flat()", "[1].reverse(1)"] {
|
|
assert!(is_error(&ev(src)), "{src}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn lambdas_bind_value_index_and_acc() {
|
|
cases(&[
|
|
("[1, 2, 3, 4].filter(value > 2)", nums(&[3.0, 4.0])),
|
|
("[1, 2, 3, 4].filter(index % 2 == 0)", nums(&[1.0, 3.0])),
|
|
("[1, 2, 3, 4].map(value + 1)", nums(&[2.0, 3.0, 4.0, 5.0])),
|
|
("[10, 20].map(value + index)", nums(&[10.0, 21.0])),
|
|
("[1, 2, 3].reduce(acc + value, 0)", n(6.0)),
|
|
("[].reduce(acc + value, 7)", n(7.0)),
|
|
("[3, \"x\", 9, 4].filter(value.isType(\"number\")).reduce(if(acc == null || value > acc, value, acc), null)", n(9.0)),
|
|
("[1, 2].map([value, index])", list(vec![nums(&[1.0, 0.0]), nums(&[2.0, 1.0])])),
|
|
("[[1, 2], [3]].map(value.map(value * 2))", list(vec![nums(&[2.0, 4.0]), nums(&[6.0])])),
|
|
("[[1, 2], [3]].map(value.reduce(acc + value, 0))", nums(&[3.0, 3.0])),
|
|
("null.filter(value)", Value::Null),
|
|
("null.map(value)", Value::Null),
|
|
("[\"a\", \"\"].filter(value)", list(vec![s("a")])),
|
|
]);
|
|
for src in ["5.filter(value)", "[1].filter()", "[1].map(1 / 0)", "[1, 2].reduce(acc + value)", "[1].reduce(acc / 0, 1)", "[1].filter(1 / 0)"] {
|
|
assert!(is_error(&ev(src)), "{src}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_variables_do_not_leak() {
|
|
let row = TestRow::new("a.md").prop("value", n(100.0));
|
|
assert_eq!(run("value", &row, &[], None, None), n(100.0));
|
|
assert_eq!(run("[1].map(value)", &row, &[], None, None), nums(&[1.0]));
|
|
assert_eq!(run("[1].map(note.value)", &row, &[], None, None), nums(&[100.0]));
|
|
assert_eq!(run("index", &row, &[], None, None), Value::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn lambdas_can_use_formulas_and_properties() {
|
|
let row = TestRow::new("a.md").prop("tags", list(vec![s("x"), s("y")])).prop("min", n(2.0));
|
|
let formulas = [("big", "value > min")];
|
|
assert_eq!(run("[1, 2, 3].filter(value > min)", &row, &formulas, None, None), nums(&[3.0]));
|
|
assert_eq!(run("tags.map(value.upper())", &row, &formulas, None, None).type_name(), "error");
|
|
assert_eq!(run("tags.map(value + \"!\").join(\" \")", &row, &formulas, None, None), s("x! y!"));
|
|
}
|
|
|
|
#[test]
|
|
fn regexp_functions() {
|
|
cases(&[
|
|
("/abc/.matches(\"abcde\")", Value::Bool(true)),
|
|
("/^abc$/.matches(\"abcde\")", Value::Bool(false)),
|
|
("/ABC/i.matches(\"abcde\")", Value::Bool(true)),
|
|
("/ABC/.matches(\"abcde\")", Value::Bool(false)),
|
|
("/abc/.matches(null)", Value::Bool(false)),
|
|
("\"a:b:c:d\".replace(/:/, \"-\")", s("a-b:c:d")),
|
|
("\"a:b:c:d\".replace(/:/g, \"-\")", s("a-b-c-d")),
|
|
("\"John Smith\".replace(/(\\w+) (\\w+)/, \"$2, $1\")", s("Smith, John")),
|
|
("\"abc\".replace(/x/g, \"-\")", s("abc")),
|
|
("\"a,b,c,d\".split(/,/, 3)", list(vec![s("a"), s("b"), s("c")])),
|
|
("\"a1b22c\".split(/\\d+/)", list(vec![s("a"), s("b"), s("c")])),
|
|
("null.replace(/a/, \"b\")", Value::Null),
|
|
]);
|
|
for src in ["/a/.matches(1)", "/(/.matches(\"a\")", "\"a\".replace(/a/, 1)", "/a/.nosuch()", "\"a\".split(/a/, \"x\")"] {
|
|
assert!(is_error(&ev(src)), "{src}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pathological_regexes_are_bounded() {
|
|
let hostile = "a".repeat(5000);
|
|
let row = TestRow::new("a.md").prop("text", s(&format!("{hostile}!")));
|
|
let started = std::time::Instant::now();
|
|
assert_eq!(run("/(a+)+$/.matches(text)", &row, &[], None, None), Value::Bool(false));
|
|
assert_eq!(run("/(a|aa)+$/.matches(text)", &row, &[], None, None), Value::Bool(false));
|
|
assert!(started.elapsed().as_secs() < 5);
|
|
// 1 MiB of text against a big program passes the work bound of the engine: an error, not a hang
|
|
let big = "a".repeat(1024 * 1024);
|
|
let row = TestRow::new("a.md").prop("text", s(&big));
|
|
let value = run("/(?:a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z){1,50}!/.matches(text)", &row, &[], None, None);
|
|
assert!(matches!(value, Value::Bool(false) | Value::Error(_)), "{value:?}");
|
|
}
|