The Anzan language itself
48 scenarios
✔ Operator precedence and associativity 4 ms
- When I calculate "2 + 3 * 4"
- Then the result is "14"
✔ Operator precedence and associativity 2 ms
- When I calculate "2^3^2"
- Then the result is "512"
✔ Operator precedence and associativity 1 ms
- When I calculate "-2^2"
- Then the result is "-4"
✔ Operator precedence and associativity 1 ms
- When I calculate "2^-2"
- Then the result is "0.25"
✔ Operator precedence and associativity 1 ms
- When I calculate "[10, 2][0]^2"
- Then the result is "100"
✔ Operator precedence and associativity 1 ms
- When I calculate "√4 + 5"
- Then the result is "7"
✔ Number literals 1 ms
- When I calculate "1_000 + 0"
- Then the result is "1000"
✔ Number literals 1 ms
- When I calculate "2.5e-3"
- Then the result is "0.0025"
✔ Number literals 1 ms
- When I calculate ".5 + .5"
- Then the result is "1"
✔ Number literals 1 ms
- When I calculate "0xFF"
- Then the result is "255"
✔ Number literals 1 ms
- When I calculate "0b1010"
- Then the result is "10"
✔ Number literals 1 ms
- When I calculate "0xDEAD_BEEF"
- Then the result is "3735928559"
✔ Pinned cell references evaluate like plain ones 3 ms
- Given cell A:1 contains "21"
- When I calculate "$A:$1 * 2"
- Then the result is "42"
✔ Pinned cell references evaluate like plain ones 2 ms
- Given cell A:1 contains "21"
- When I calculate "$A:1 + A:1"
- Then the result is "42"
✔ Pinned cell references evaluate like plain ones 2 ms
- Given cell A:1 contains "21"
- When I calculate "A:$1 * 2"
- Then the result is "42"
✔ A dangling $ is a loud lex error 1 ms
- When I calculate "$"
- Then the calculation fails mentioning "pins a cell reference"
✔ A dangling $ is a loud lex error 1 ms
- When I calculate "$x"
- Then the calculation fails mentioning "pins a cell reference"
✔ A dangling $ is a loud lex error 1 ms
- When I calculate "2 + $rate"
- Then the calculation fails mentioning "pins a cell reference"
✔ A comment-only line is a note, not an error 1 ms
- When I calculate "# the calc below confirms our test"
- Then the result is "# the calc below confirms our test"
✔ A standalone note never touches ans 3 ms
- When I calculate "21 * 2"
- And I calculate "# just thinking out loud"
- And I calculate "ans"
- Then the result is "42"
✔ A trailing comment is stripped before evaluation 1 ms
- When I calculate "5 + 3 # adds them"
- Then the result is "8"
✔ A hash inside a string is not a comment 1 ms
- When I calculate "len("a # b")"
- Then the result is "5"
✔ Malformed programmer literals fail loudly 1 ms
- When I calculate "0xFG"
- Then the calculation fails mentioning "malformed hex"
✔ Malformed programmer literals fail loudly 1 ms
- When I calculate "0x1.5"
- Then the calculation fails mentioning "malformed hex"
✔ Malformed programmer literals fail loudly 1 ms
- When I calculate "0x"
- Then the calculation fails mentioning "needs digits"
✔ Malformed programmer literals fail loudly 1 ms
- When I calculate "0b12"
- Then the calculation fails mentioning "malformed binary"
✔ Implicit multiplication covers parens, names, and constants 2 ms
- When I calculate "x9 = 4"
- And I calculate "(2)(3) + 2x9"
- Then the result is "14"
✔ Modulo is exact 1 ms
- When I calculate "mod(0.3, 0.1)"
- Then the result is "0"
✔ Percent literals 1 ms
- When I calculate "3%"
- Then the result is "0.03"
✔ Percent literals 1 ms
- When I calculate "1 * 3%"
- Then the result is "0.03"
✔ Percent literals 1 ms
- When I calculate "100 + 5%"
- Then the result is "100.05"
✔ Percent literals 1 ms
- When I calculate "50% * 2"
- Then the result is "1"
✔ Percent literals 1 ms
- When I calculate "(2 + 3)%"
- Then the result is "0.05"
✔ A number can't directly follow another value 1 ms
- When I calculate "3 4"
- Then the calculation fails mentioning "operator"
✔ A number can't directly follow another value 1 ms
- When I calculate "3 % 4"
- Then the calculation fails mentioning "operator"
✔ A number can't directly follow another value 1 ms
- When I calculate "(1) 2"
- Then the calculation fails mentioning "operator"
✔ A string is not a condition 1 ms
- When I calculate "if("a", 1, 2)"
- Then the calculation fails mentioning "number"
✔ Empty reductions yield identities 1 ms
- When I calculate "∑_i=1^0(i) + ∏_i=1^0(i)"
- Then the result is "1"
✔ A reduction spanning too many terms is refused 1 ms
- When I calculate "∑_i=1^200000(i)"
- Then the calculation fails mentioning "100,000"
✔ Reserved words refuse assignment 1 ms
- When I calculate "ans = 5"
- Then the calculation fails mentioning "cannot assign"
✔ Reserved words refuse assignment 1 ms
- When I calculate "sigma = 1"
- Then the calculation fails mentioning "cannot assign"
✔ Reserved words refuse assignment 1 ms
- When I calculate "true = 0"
- Then the calculation fails mentioning "cannot assign"
✔ Parameters shadow globals without clobbering them 4 ms
- When I calculate "shade = 10"
- And I calculate "twice(shade) = shade * 2"
- And I calculate "twice(3) + shade"
- Then the result is "16"
✔ Free variables resolve at call time 5 ms
- When I calculate "base = 10"
- And I calculate "above(y) = base + y"
- And I calculate "base = 100"
- And I calculate "above(1)"
- Then the result is "101"
✔ Special forms ship their manual 2 ms
- When I calculate "man if"
- Then documentation is shown mentioning "taken branch"
✔ String escapes are honored 1 ms
- When I calculate "len("a\tb")"
- Then the result is "3"
✔ The word data is still an ordinary name 2 ms
- When I calculate "data = 5"
- And I calculate "data * 2"
- Then the result is "10"
✔ Compact named arguments versus cell references 6 ms
- Given I calculate "data Q { a: Number, age: Number }"
- When I calculate "Q(a: 1, age:36).age"
- Then the result is "36"
- When I calculate "sum(a:1)"
- Then the result is "0"
Calculating in the log
25 scenarios
✔ Everyday calculations are exact 4 ms
- When I calculate "0.1 + 0.2"
- Then the result is "0.3"
✔ Everyday calculations are exact 2 ms
- When I calculate "2(3 + 4)"
- Then the result is "14"
✔ Everyday calculations are exact 0.8 ms
- When I calculate "2^10"
- Then the result is "1024"
✔ Everyday calculations are exact 1 ms
- When I calculate "∑(1, 2, 3)"
- Then the result is "6"
✔ Everyday calculations are exact 0.9 ms
- When I calculate "∑_i=1^10(i^2)"
- Then the result is "385"
✔ Everyday calculations are exact 17 ms
- When I calculate "pmt(0.05/12, 360, 200000)"
- Then the result is "-1073.643246024277969656985158225109053609679713701"
✔ Everyday calculations are exact 1 ms
- When I calculate "margin(100, 80)"
- Then the result is "20"
✔ Everyday calculations are exact 0.9 ms
- When I calculate "date(2026, 6, 6) - date(2026, 1, 1)"
- Then the result is "156"
✔ Everyday calculations are exact 0.8 ms
- When I calculate "if(2 > 1, 10, 20)"
- Then the result is "10"
✔ Everyday calculations are exact 0.7 ms
- When I calculate "gcd(48, 36)"
- Then the result is "12"
✔ A hundred dimes make exactly a dollar bag 0.9 ms
- When I calculate "∑_i=1^100(0.1)"
- Then the result is "10"
✔ The previous answer carries forward 1 ms
- When I calculate "6 * 7"
- And I calculate "ans + 8"
- Then the result is "50"
✔ Variables persist across calculations 1 ms
- When I calculate "rate2026 = 0.0825"
- And I calculate "1200 * rate2026"
- Then the result is "99"
✔ A leading equals sign is tolerated 0.6 ms
- When I calculate "= 1 + 2"
- Then the result is "3"
✔ A failed calculation never clobbers the previous answer 2 ms
- When I calculate "6 * 7"
- And I calculate "1 / 0"
- And I calculate "ans + 0"
- Then the result is "42"
✔ Dividing by zero explains itself 0.7 ms
- When I calculate "1 / 0"
- Then the calculation fails mentioning "division by zero"
✔ Typos are caught, not guessed 0.7 ms
- When I calculate "12 * rte"
- Then the calculation fails mentioning "unknown variable 'rte'"
✔ Comparisons answer 1 or 0 0.7 ms
- When I calculate "2 < 3"
- Then the result is "1"
✔ Comparisons answer 1 or 0 0.7 ms
- When I calculate "3 < 2"
- Then the result is "0"
✔ Comparisons answer 1 or 0 0.7 ms
- When I calculate "3 > 2"
- Then the result is "1"
✔ Comparisons answer 1 or 0 0.7 ms
- When I calculate "2 <= 2"
- Then the result is "1"
✔ Comparisons answer 1 or 0 0.7 ms
- When I calculate "2 >= 3"
- Then the result is "0"
✔ Comparisons answer 1 or 0 0.6 ms
- When I calculate "2 == 2"
- Then the result is "1"
✔ Comparisons answer 1 or 0 0.6 ms
- When I calculate "2 != 2"
- Then the result is "0"
✔ Comparison chains are rejected, not misread 0.6 ms
- When I calculate "1 < 2 < 3"
- Then the calculation fails mentioning "can't be chained"
Data types — declared records with named construction
55 scenarios
✔ Declaring a type registers its constructor 8 ms
- When I calculate "data Person { name: String, age: Number, active: Boolean }"
- Then the result is "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "Ada", age: 36, active: true)"
- Then the result is "Person(name: "Ada", age: 36, active: true)"
✔ Construction is named fields or one map — never positional 13 ms
- Given I calculate "data Pt { x: Number, y: Number }"
- When I calculate "Pt(y: 4, x: 3)"
- Then the result is "Pt(x: 3, y: 4)"
- When I calculate "m = {x: 3, y: 4}"
- And I calculate "Pt(m)"
- Then the result is "Pt(x: 3, y: 4)"
- When I calculate "Pt(3, 4)"
- Then the calculation fails mentioning "takes named fields"
✔ Field types accept any casing and Boolean fields render true/false 5 ms
- When I calculate "data Flag { on: boolean }"
- Then the result is "data Flag { on: Boolean }"
- When I calculate "Flag(on: 1 < 2)"
- Then the result is "Flag(on: true)"
✔ Fields read like map members, by dot or by key 14 ms
- Given I calculate "data Pt { x: Number, y: Number }"
- And I calculate "p = Pt(x: 3, y: 4)"
- When I calculate "sqrt(p.x^2 + p.y^2)"
- Then the result is "5"
- When I calculate "p["y"]"
- Then the result is "4"
- When I calculate "keys(p)"
- Then the result is "["x", "y"]"
- When I calculate "len(p)"
- Then the result is "2"
✔ Records collect into arrays and flow through higher-order functions 15 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- And I calculate "team = [Person(name: "Ada", age: 36, active: true), Person(name: "Grace", age: 30, active: false)]"
- When I calculate "map(x -> x.age, team)"
- Then the result is "[36, 30]"
- When I calculate "sum(map(x -> x.age, team))"
- Then the result is "66"
- When I calculate "filter(x -> x.active, team)"
- Then the result is "[Person(name: "Ada", age: 36, active: true)]"
- When I calculate "map(Person, [{name: "Bo", age: 1, active: true}])"
- Then the result is "[Person(name: "Bo", age: 1, active: true)]"
✔ Instances canonicalize to declaration order and compare deeply 6 ms
- Given I calculate "data Pt { x: Number, y: Number }"
- When I calculate "Pt(y: 2, x: 1) == Pt(x: 1, y: 2)"
- Then the result is "1"
- When I calculate "Pt(x: 1, y: 2) == {x: 1, y: 2}"
- Then the result is "0"
✔ toJson is pretty by default, compact on request, honest about Booleans 11 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- And I calculate "p = Person(name: "Ada", age: 36, active: true)"
- When I calculate "toJson(p)"
- Then the result is ""{\n \"name\": \"Ada\",\n \"age\": 36,\n \"active\": true\n}""
- When I calculate "toJson(p, Json.Compact)"
- Then the result is ""{\"name\":\"Ada\",\"age\":36,\"active\":true}""
- When I calculate "toJson([1, 2])"
- Then the result is ""[\n 1,\n 2\n]""
✔ fromJson parses JSON into values, exactly 17 ms
- When I calculate "fromJson("[1, 2, 3]")"
- Then the result is "[1, 2, 3]"
- When I calculate "fromJson("{\"name\": \"Ada\", \"age\": 36}").age"
- Then the result is "36"
- When I calculate "fromJson("0.30000000000000004") == 0.30000000000000004"
- Then the result is "1"
- When I calculate "fromJson("123456789012345678901234567890.5") * 2"
- Then the result is "246913578024691357802469135781"
- When I calculate "fromJson("true") + fromJson("false")"
- Then the result is "1"
- When I calculate "fromJson("\"\\u0041da\"")"
- Then the result is ""Ada""
✔ fromJson and a constructor re-type a serialized record 8 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- And I calculate "p = Person(name: "Ada", age: 36, active: true)"
- When I calculate "Person(fromJson(toJson(p))) == p"
- Then the result is "1"
- When I calculate "map(Person, fromJson(toJson([p, p])))[1].age"
- Then the result is "36"
✔ fromJson mistakes explain themselves 2 ms
- When I calculate "fromJson("null")"
- Then the calculation fails mentioning "has no Anzan value"
✔ fromJson mistakes explain themselves 2 ms
- When I calculate "fromJson("[1,")"
- Then the calculation fails mentioning "unexpected end"
✔ fromJson mistakes explain themselves 2 ms
- When I calculate "fromJson("[1] junk")"
- Then the calculation fails mentioning "trailing content"
✔ fromJson mistakes explain themselves 2 ms
- When I calculate "fromJson("{\"a\":1,\"a\":2}")"
- Then the calculation fails mentioning "duplicate key"
✔ fromJson mistakes explain themselves 2 ms
- When I calculate "fromJson(5)"
- Then the calculation fails mentioning "wants JSON text"
✔ Json options are named constants, not magic flags 15 ms
- When I calculate "Json.Pretty"
- Then the result is ""pretty""
- When I calculate "toJson([1, 2], Json.Compact)"
- Then the result is ""[1,2]""
- When I calculate "toJson([1, 2], "compact")"
- Then the result is ""[1,2]""
- When I calculate "Json = 5"
- Then the calculation fails mentioning "cannot assign"
- When I calculate "man Json"
- Then documentation is shown mentioning "Formatting options"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "Ada", age: 36)"
- Then the calculation fails mentioning "missing 'active'"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: 7, age: 36, active: true)"
- Then the calculation fails mentioning "'name' of Person is a String"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "A", age: "x", active: true)"
- Then the calculation fails mentioning "'age' of Person is a Number"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "A", age: 36, active: 7)"
- Then the calculation fails mentioning "use true or false"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "A", age: 36, active: true, pet: 1)"
- Then the calculation fails mentioning "no field 'pet'"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "Person(name: "A", name: "B", age: 1, active: 1)"
- Then the calculation fails mentioning "duplicate field"
✔ Construction mistakes explain themselves 4 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "toJson(sqrt)"
- Then the calculation fails mentioning "can't serialize a function"
✔ Construction mistakes explain themselves 4 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "toJson(1, "wat")"
- Then the calculation fails mentioning "unknown toJson option"
✔ Construction mistakes explain themselves 3 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- When I calculate "toJson(1, true)"
- Then the calculation fails mentioning "Json.Pretty or Json.Compact"
✔ Declaration mistakes explain themselves 2 ms
- When I calculate "data person { a: Number }"
- Then the calculation fails mentioning "capital letter"
✔ Declaration mistakes explain themselves 2 ms
- When I calculate "data Bad { a: truthy }"
- Then the calculation fails mentioning "declared data type"
✔ Declaration mistakes explain themselves 2 ms
- When I calculate "data Empty {}"
- Then the calculation fails mentioning "at least one field"
✔ Declaration mistakes explain themselves 2 ms
- When I calculate "data Dup { a: Number, A: Number }"
- Then the calculation fails mentioning "duplicate field"
✔ Declaration mistakes explain themselves 2 ms
- When I calculate "data Abs { a: Number }"
- Then the calculation fails mentioning "built-in"
✔ Types and functions can't share a name, but redeclaring your own type works 13 ms
- Given I calculate "f(x) = x + 1"
- When I calculate "data F { a: Number }"
- Then the calculation fails mentioning "already a function"
- When I calculate "data G { a: Number }"
- And I calculate "g(x) = x"
- Then the calculation fails mentioning "is a data type"
- When I calculate "data G { a: Number, b: Number }"
- And I calculate "G(a: 1, b: 2).b"
- Then the result is "2"
✔ A type documents itself through its trailing comment 3 ms
- When I calculate "data Invoice { total: Number, paid: Boolean } # one customer invoice"
- And I calculate "man Invoice"
- Then documentation is shown mentioning "one customer invoice"
✔ toJson escapes strings and renders empty containers 8 ms
- When I calculate "toJson("a\tb")"
- Then the result is ""\"a\\tb\"""
- When I calculate "toJson([])"
- Then the result is ""[]""
- When I calculate "toJson({})"
- Then the result is ""{}""
✔ A constructor call works in tail position of a recursive function 5 ms
- Given I calculate "data Box { v: Number }"
- And I calculate "wrap(n) = if(n <= 0, Box(v: n), wrap(n - 1))"
- When I calculate "wrap(5).v"
- Then the result is "0"
✔ The explicit formula marker rejects declarations in cells 2 ms
- Given cell A:1 contains "=data Pt { x: Number }"
- Then cell A:1 shows an error mentioning "drop the leading '='"
✔ A plain declaration in a cell is a sheet-scoped 𝑫 type 8 ms
- Given cell A:1 contains "data Pt { x: Number, y: Number }"
- And cell B:1 contains "Pt(x: 3, y: 4).x"
- Then cell A:1 shows "𝑫 Pt"
- And cell B:1 shows "3"
- When I calculate "data Pt { x: Number }"
- Then the calculation fails mentioning "defined in cell Sheet 1!A:1"
✔ Types and record variables survive save and reopen 8 ms
- Given I calculate "data Person { name: String, age: Number, active: Boolean }"
- And I calculate "p = Person(name: "Ada", age: 36, active: true)"
- When the workbook is saved and reopened
- And I calculate "p.age + Person(name: "B", age: 4, active: false).age"
- Then the result is "40"
✔ A typed parameter dispatches a function by argument type 9 ms
- Given I calculate "kind(n: Number) = "number""
- And I calculate "kind(s: String) = "string""
- When I calculate "kind(42)"
- Then the result is ""number""
- When I calculate "kind("hi")"
- Then the result is ""string""
✔ A function can be written for a data type 6 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "midpoint(a: Point, b: Point) = Point(x: (a.x + b.x) / 2, y: (a.y + b.y) / 2)"
- When I calculate "midpoint(Point(x: 0, y: 0), Point(x: 4, y: 10))"
- Then the result is "Point(x: 2, y: 5)"
✔ An operator can be overloaded for a data type 5 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "+(a: Point, b: Point) = Point(x: a.x + b.x, y: a.y + b.y)"
- When I calculate "Point(x: 1, y: 2) + Point(x: 10, y: 20)"
- Then the result is "Point(x: 11, y: 22)"
✔ An overloaded operator can mix a data type and a scalar 6 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "*(a: Point, s: Number) = Point(x: a.x * s, y: a.y * s)"
- When I calculate "Point(x: 1, y: 2) * 3"
- Then the result is "Point(x: 3, y: 6)"
✔ Built-in arithmetic is untouched by an overload 9 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "+(a: Point, b: Point) = Point(x: a.x + b.x, y: a.y + b.y)"
- When I calculate "1 + 2"
- Then the result is "3"
- When I calculate ""Q" + 1"
- Then the result is ""Q1""
✔ An operator overload must involve a data type 2 ms
- When I calculate "+(a: Number, b: Number) = 5"
- Then the calculation fails mentioning "must involve a data type"
✔ Records compare equal by all of their state 10 ms
- Given I calculate "data Point { x: Number, y: Number }"
- When I calculate "Point(x: 1, y: 2) == Point(x: 1, y: 2)"
- Then the result is "1"
- When I calculate "Point(x: 1, y: 2) == Point(x: 9, y: 9)"
- Then the result is "0"
- When I calculate "Point(x: 1, y: 2) != Point(x: 9, y: 9)"
- Then the result is "1"
✔ Operator overloads survive save and reopen 13 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "+(a: Point, b: Point) = Point(x: a.x + b.x, y: a.y + b.y)"
- And I calculate "*(a: Point, s: Number) = Point(x: a.x * s, y: a.y * s)"
- When the workbook is saved and reopened
- And I calculate "(Point(x: 1, y: 1) + Point(x: 2, y: 3)).y"
- Then the result is "4"
- And I calculate "(Point(x: 2, y: 3) * 2).x"
- Then the result is "4"
✔ A data type can have fields of another data type 11 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "data Line { a: Point, b: Point }"
- And I calculate "l = Line(a: Point(x: 1, y: 2), b: Point(x: 3, y: 4))"
- When I calculate "l.b.y"
- Then the result is "4"
- When I calculate "l == Line(a: Point(x: 1, y: 2), b: Point(x: 3, y: 4))"
- Then the result is "1"
✔ A nested field rejects the wrong type 5 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "data Line { a: Point, b: Point }"
- When I calculate "Line(a: 5, b: Point(x: 3, y: 4))"
- Then the calculation fails mentioning "is a Point"
✔ Nested records survive save and reopen 11 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "data Line { a: Point, b: Point }"
- And I calculate "seg = Line(a: Point(x: 1, y: 1), b: Point(x: 4, y: 5))"
- When the workbook is saved and reopened
- And I calculate "seg.b.x - seg.a.x"
- Then the result is "3"
✔ A field can be a list of scalars 8 ms
- Given I calculate "data Tags { items: [String] }"
- When I calculate "Tags(items: ["a", "b", "c"])"
- Then the result is "Tags(items: ["a", "b", "c"])"
- When I calculate "len(Tags(items: ["a", "b"]).items)"
- Then the result is "2"
✔ A field can be a list of records, nested freely 12 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "data Path { points: [Point] }"
- And I calculate "p = Path(points: [Point(x: 1, y: 2), Point(x: 3, y: 4)])"
- When I calculate "p.points[1].y"
- Then the result is "4"
- When I calculate "len(p.points)"
- Then the result is "2"
✔ A field can be a nested list 4 ms
- Given I calculate "data Grid { rows: [[Number]] }"
- When I calculate "Grid(rows: [[1, 2], [3, 4]]).rows[1][0]"
- Then the result is "3"
✔ A field can be a string-keyed map 4 ms
- Given I calculate "data Config { opts: {String: Number} }"
- When I calculate "Config(opts: {a: 1, b: 2}).opts.b"
- Then the result is "2"
✔ List and map fields validate their elements 6 ms
- Given I calculate "data Tags { items: [String] }"
- And I calculate "data Config { opts: {String: Number} }"
- When I calculate "Tags(items: [1, 2])"
- Then the calculation fails mentioning "is a String"
✔ List and map fields validate their elements 5 ms
- Given I calculate "data Tags { items: [String] }"
- And I calculate "data Config { opts: {String: Number} }"
- When I calculate "Tags(items: "x")"
- Then the calculation fails mentioning "is a [String]"
✔ List and map fields validate their elements 5 ms
- Given I calculate "data Tags { items: [String] }"
- And I calculate "data Config { opts: {String: Number} }"
- When I calculate "Config(opts: {a: "x"})"
- Then the calculation fails mentioning "is a Number"
✔ List and map fields survive save and reopen 9 ms
- Given I calculate "data Point { x: Number, y: Number }"
- And I calculate "data Path { points: [Point] }"
- And I calculate "route = Path(points: [Point(x: 1, y: 1), Point(x: 5, y: 9)])"
- When the workbook is saved and reopened
- And I calculate "route.points[1].x"
- Then the result is "5"
Fixed-precision decimals are SQL-style money values
20 scenarios
✔ Fixed-precision decimal construction and arithmetic 4 ms
- When I calculate "Decimal(0.5)"
- Then the result is "Decimal(0.5)"
✔ Fixed-precision decimal construction and arithmetic 1 ms
- When I calculate "Decimal(3.14159)"
- Then the result is "Decimal(3.14159)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(0.5, 2)"
- Then the result is "Decimal(0.50, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(10.5, 5, 2)"
- Then the result is "Decimal(10.50, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(1.005, 5, 2)"
- Then the result is "Decimal(1.00, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(1.005, 5, 2, Rounding.HalfUp)"
- Then the result is "Decimal(1.01, 5, 2, Rounding.HalfUp)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(123, 5, 0)"
- Then the result is "Decimal(123, 5, 0)"
✔ Fixed-precision decimal construction and arithmetic 0.8 ms
- When I calculate "Decimal(2.50, 5, 2) + Decimal(1.25, 5, 2)"
- Then the result is "Decimal(3.75, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 1.0 ms
- When I calculate "Decimal(10.00, 5, 2) - Decimal(0.01, 5, 2)"
- Then the result is "Decimal(9.99, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(2.50, 5, 2) * Decimal(4, 5, 2)"
- Then the result is "Decimal(10.00, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(10, 5, 2) / Decimal(3, 5, 2)"
- Then the result is "Decimal(3.33, 5, 2)"
✔ Fixed-precision decimal construction and arithmetic 0.9 ms
- When I calculate "Decimal(10.00, 5, 2) + 0.005"
- Then the result is "Decimal(10.00, 5, 2)"
✔ Decimal overflow and bad mixes are loud errors 0.9 ms
- When I calculate "Decimal(12345, 4, 0)"
- Then the calculation fails mentioning "exceeds"
✔ Decimal overflow and bad mixes are loud errors 1 ms
- When I calculate "Decimal(999.99, 5, 2) + Decimal(0.01, 5, 2)"
- Then the calculation fails mentioning "exceeds"
✔ Decimal overflow and bad mixes are loud errors 1 ms
- When I calculate "Decimal(1, 5, 2) + Decimal(1, 5, 2, Rounding.HalfUp)"
- Then the calculation fails mentioning "rounding"
✔ Decimal overflow and bad mixes are loud errors 1 ms
- When I calculate "Decimal(5, 5, 2) + Int8(5)"
- Then the calculation fails mentioning "combine"
✔ Decimal overflow and bad mixes are loud errors 0.9 ms
- When I calculate "Decimal(1, 1001, 2)"
- Then the calculation fails mentioning "between 1 and 1000"
✔ A Decimal coerces to its number outside typed arithmetic 0.9 ms
- When I calculate "Decimal(10.50, 5, 2) == 10.5"
- Then the result is "1"
✔ A Decimal coerces to its number outside typed arithmetic 1.0 ms
- When I calculate "Decimal(2.50, 5, 2) < 3"
- Then the result is "1"
✔ A Decimal coerces to its number outside typed arithmetic 1.0 ms
- When I calculate "sum(Decimal(1.5, 5, 2), Decimal(2.5, 5, 2))"
- Then the result is "4"
Error handling — try, iferror, ifpanic, and the Result type
30 scenarios
✔ try turns success and domain errors into a Result 3 ms
- When I calculate "try(2 + 2)"
- Then the result is "Ok(4)"
✔ try turns success and domain errors into a Result 2 ms
- When I calculate "try(1 / 0)"
- Then the result is "Err("division by zero")"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "try("abc"[9])"
- Then the result is "Err("index 9 is out of range (string has 3 characters)")"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "try(fromJson("invalidJson"))"
- Then the result is "Err("fromJson: unexpected character 'i' at character 1")"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "Ok(4)"
- Then the result is "Ok(4)"
✔ try turns success and domain errors into a Result 1.0 ms
- When I calculate "Err("nope")"
- Then the result is "Err("nope")"
✔ try turns success and domain errors into a Result 2 ms
- When I calculate "try(2 + 2) == Ok(4)"
- Then the result is "1"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "Ok(4) == Ok(4)"
- Then the result is "1"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "Ok(4) == Ok(5)"
- Then the result is "0"
✔ try turns success and domain errors into a Result 1 ms
- When I calculate "Ok(4) == Err("x")"
- Then the result is "0"
✔ Result combinators read and default a Result 1 ms
- When I calculate "isOk(try(2 + 2))"
- Then the result is "1"
✔ Result combinators read and default a Result 1 ms
- When I calculate "isError(try(1 / 0))"
- Then the result is "1"
✔ Result combinators read and default a Result 1.0 ms
- When I calculate "isOk(try(1 / 0))"
- Then the result is "0"
✔ Result combinators read and default a Result 1 ms
- When I calculate "isError(try(2 + 2))"
- Then the result is "0"
✔ Result combinators read and default a Result 1.0 ms
- When I calculate "unwrap(try(2 + 2))"
- Then the result is "4"
✔ Result combinators read and default a Result 1 ms
- When I calculate "unwrapOr(try(1 / 0), -1)"
- Then the result is "-1"
✔ Result combinators read and default a Result 1 ms
- When I calculate "unwrapOr(try(2 + 2), -1)"
- Then the result is "4"
✔ Result combinators read and default a Result 1 ms
- When I calculate "unwrapErr(try(1 / 0))"
- Then the result is ""division by zero""
✔ Result combinators read and default a Result 1 ms
- When I calculate "if(isOk(try(9)), unwrap(try(9)), 0)"
- Then the result is "9"
✔ iferror recovers a domain error with a fallback 1.0 ms
- When I calculate "iferror(1 / 0, 0)"
- Then the result is "0"
✔ iferror recovers a domain error with a fallback 1 ms
- When I calculate "iferror(2 + 2, 0)"
- Then the result is "4"
✔ iferror recovers a domain error with a fallback 1 ms
- When I calculate "iferror("x"[9], -1)"
- Then the result is "-1"
✔ iferror does not evaluate its fallback on success 1 ms
- When I calculate "iferror(42, 1 / 0)"
- Then the result is "42"
✔ iferror does not catch a panic (runaway guard) 1 ms
- When I calculate "iferror(sigma_i=1^200000(i), -1)"
- Then the calculation fails mentioning "100,000 terms"
✔ try does not catch a panic either 1 ms
- When I calculate "try(sigma_i=1^200000(i))"
- Then the calculation fails mentioning "100,000 terms"
✔ ifpanic catches a panic and returns its fallback 1 ms
- When I calculate "ifpanic(sigma_i=1^200000(i), -1)"
- Then the result is "-1"
✔ ifpanic does not catch a domain error (wrong tier) 1.0 ms
- When I calculate "ifpanic(1 / 0, -1)"
- Then the calculation fails mentioning "division by zero"
✔ Result mistakes explain themselves 0.9 ms
- When I calculate "unwrap(try(1 / 0))"
- Then the calculation fails mentioning "division by zero"
✔ Result mistakes explain themselves 1.0 ms
- When I calculate "unwrap(5)"
- Then the calculation fails mentioning "Result"
✔ Result mistakes explain themselves 1.0 ms
- When I calculate "unwrapErr(Ok(4))"
- Then the calculation fails mentioning "Ok"
Fixed-width integers are bounded and checked
15 scenarios
✔ Fixed-width construction and checked arithmetic 2 ms
- When I calculate "Int32(27374)"
- Then the result is "Int32(27374)"
✔ Fixed-width construction and checked arithmetic 0.8 ms
- When I calculate "Int(27374, 32)"
- Then the result is "Int32(27374)"
✔ Fixed-width construction and checked arithmetic 0.5 ms
- When I calculate "Int8(5) + Int8(3)"
- Then the result is "Int8(8)"
✔ Fixed-width construction and checked arithmetic 0.5 ms
- When I calculate "Int8(5) + 3"
- Then the result is "Int8(8)"
✔ Fixed-width construction and checked arithmetic 0.5 ms
- When I calculate "Int(100, 8) + Int(100, 16)"
- Then the result is "Int16(200)"
✔ Fixed-width construction and checked arithmetic 0.5 ms
- When I calculate "Int8(2) ^ 3"
- Then the result is "Int8(8)"
✔ Overflow and bad mixes are loud errors, never wraps 0.7 ms
- When I calculate "Int8(200)"
- Then the calculation fails mentioning "out of range"
✔ Overflow and bad mixes are loud errors, never wraps 0.6 ms
- When I calculate "Int8(100) + Int8(100)"
- Then the calculation fails mentioning "out of range"
✔ Overflow and bad mixes are loud errors, never wraps 0.5 ms
- When I calculate "UInt8(0) - 1"
- Then the calculation fails mentioning "out of range"
✔ Overflow and bad mixes are loud errors, never wraps 0.5 ms
- When I calculate "Int8(5) + UInt8(5)"
- Then the calculation fails mentioning "signed and unsigned"
✔ Overflow and bad mixes are loud errors, never wraps 0.5 ms
- When I calculate "Int8(5) + 1.5"
- Then the calculation fails mentioning "whole numbers"
✔ Programmer-mode bitwise preserves the fixed-width type 1 ms
- Given the calculator is in programmer mode
- When I calculate "UInt8(12) & UInt8(10)"
- Then the result is "UInt8(8)"
✔ Programmer-mode bitwise preserves the fixed-width type 0.9 ms
- Given the calculator is in programmer mode
- When I calculate "~UInt8(0)"
- Then the result is "UInt8(255)"
✔ Programmer-mode bitwise preserves the fixed-width type 1.0 ms
- Given the calculator is in programmer mode
- When I calculate "~Int8(0)"
- Then the result is "Int8(-1)"
✔ Programmer-mode bitwise preserves the fixed-width type 1 ms
- Given the calculator is in programmer mode
- When I calculate "UInt8(1) << 4"
- Then the result is "UInt8(16)"
Number formats are presentation, never value
15 scenarios
✔ A formatted cell displays its value in costume 3 ms
- Given cell B:1 contains "1234567.5"
- And cell B:1 is formatted as "number"
- Then cell B:1 displays "1,234,567.50"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "1234.5"
- And cell B:1 is formatted as "dollars"
- Then cell B:1 displays "$1,234.50"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "-1234.5"
- And cell B:1 is formatted as "dollars"
- Then cell B:1 displays "-$1,234.50"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "-2"
- And cell B:1 is formatted as "euros"
- Then cell B:1 displays "-€2.00"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "0.0825"
- And cell B:1 is formatted as "percent"
- Then cell B:1 displays "8.25%"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "1"
- And cell B:1 is formatted as "percent"
- Then cell B:1 displays "100.00%"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "20610"
- And cell B:1 is formatted as "a date"
- Then cell B:1 displays "2026-06-06"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "0"
- And cell B:1 is formatted as "a date"
- Then cell B:1 displays "1970-01-01"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "195"
- And cell B:1 is formatted as "hex"
- Then cell B:1 displays "0xC3"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "-255"
- And cell B:1 is formatted as "hex"
- Then cell B:1 displays "-0xFF"
✔ A formatted cell displays its value in costume 2 ms
- Given cell B:1 contains "195"
- And cell B:1 is formatted as "binary"
- Then cell B:1 displays "0b1100_0011"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "5"
- And cell B:1 is formatted as "binary"
- Then cell B:1 displays "0b101"
✔ A formatted cell displays its value in costume 1 ms
- Given cell B:1 contains "1.5"
- And cell B:1 is formatted as "hex"
- Then cell B:1 displays "1.5"
✔ Formatting never touches the underlying math 3 ms
- Given cell B:1 contains "0.0825"
- And cell B:1 is formatted as "percent"
- And cell B:2 contains "=B:1 * 200"
- Then cell B:1 displays "8.25%"
- And cell B:2 shows "16.5"
✔ Hex format is display-only, like every format 3 ms
- Given cell A:1 contains "=bitOr(0xC0, 0x03)"
- And cell A:1 is formatted as "hex"
- And cell A:2 contains "=A:1 + 1"
- Then cell A:1 displays "0xC3"
- And cell A:2 shows "196"
Defining your own functions
16 scenarios
✔ Define a function and use it 2 ms
- When I calculate "tax(x) = x * 1.0825 # TX sales tax"
- And I calculate "tax(100)"
- Then the result is "108.25"
✔ Functions compose regardless of definition order 1 ms
- When I calculate "g(x) = f(x) + 1"
- And I calculate "f(x) = x * 2"
- And I calculate "g(20)"
- Then the result is "41"
✔ Recursion works 0.7 ms
- When I calculate "fact(n) = if(n <= 1, 1, n * fact(n - 1))"
- And I calculate "fact(10)"
- Then the result is "3628800"
✔ Lambdas are values 0.4 ms
- When I calculate "map(x -> x * 2, [1, 2, 3])"
- Then the result is "[2, 4, 6]"
✔ Structures carry data 0.6 ms
- When I calculate "people = [{name: "Ada", age: 36}, {name: "Bob", age: 32}]"
- And I calculate "people[0].age"
- Then the result is "36"
✔ Machin's 1706 formula recovers pi 2 ms
- When I calculate "arctanInv(x, n) = ∑_k=0^(n)((-1)^k / ((2k + 1) * x^(2k + 1)))"
- And I calculate "16 * arctanInv(5, 40) - 4 * arctanInv(239, 15) - pi"
- Then the result is within "1e-45" of zero
✔ Comments are for humans and ignored by the math 0.4 ms
- When I calculate "6 * 7 # the answer"
- Then the result is "42"
✔ A trailing comment becomes the function's documentation 0.7 ms
- When I calculate "tax(x) = x * 1.0825 # TX sales tax"
- And I calculate "man tax"
- Then documentation is shown mentioning "TX sales tax"
✔ Built-in functions ship their manual 0.4 ms
- When I calculate "man pmt"
- Then documentation is shown mentioning "payment"
✔ Only the taken branch of if() runs 0.3 ms
- When I calculate "if(1, 2, 1/0)"
- Then the result is "2"
✔ Deep recursion is bounded by memory, not a counter 50 ms
- When I calculate "countdown(n) = if(n <= 0, 0, countdown(n - 1) + 1)"
- And I calculate "countdown(2000)"
- Then the result is "2000"
✔ Tail-recursive functions run at constant stack — any depth 4120 ms
- When I calculate "sumTo(n, acc) = if(n <= 0, acc, sumTo(n - 1, acc + n))"
- And I calculate "sumTo(500000, 0)"
- Then the result is "125000250000"
✔ Forgetting a base case fails politely, with a hint 515 ms
- When I calculate "F(n) = F(n - 1) + F(n - 2)"
- And I calculate "f(3)"
- Then the calculation fails mentioning "base case"
✔ Runaway recursion is cut off cleanly, not a crash 4726 ms
- When I calculate "loop(n) = loop(n + 1)"
- And I calculate "loop(1)"
- Then the calculation fails mentioning "nested too deeply"
✔ Built-in names are protected 0.4 ms
- When I calculate "abs(x) = x"
- Then the calculation fails mentioning "built-in"
✔ Function calls are case-insensitive 0.4 ms
- When I calculate "MIN(1, 2) + Sqrt(16)"
- Then the result is "5"
The expanded function library
114 scenarios
✔ Exact combinatorics — BigInt keeps every digit 13 ms
- When I calculate "choose(5, 2)"
- Then the result is "10"
✔ Exact combinatorics — BigInt keeps every digit 7 ms
- When I calculate "choose(52, 5)"
- Then the result is "2598960"
✔ Exact combinatorics — BigInt keeps every digit 10 ms
- When I calculate "choose(100, 50)"
- Then the result is "100891344545564193334812497256"
✔ Exact combinatorics — BigInt keeps every digit 5 ms
- When I calculate "choose(2, 5)"
- Then the result is "0"
✔ Exact combinatorics — BigInt keeps every digit 5 ms
- When I calculate "perm(5, 2)"
- Then the result is "20"
✔ Exact combinatorics — BigInt keeps every digit 5 ms
- When I calculate "perm(10, 3)"
- Then the result is "720"
✔ Exact combinatorics — BigInt keeps every digit 5 ms
- When I calculate "perm(10, 10)"
- Then the result is "3628800"
✔ Array plumbing 5 ms
- When I calculate "sort([3, 1, 2])"
- Then the result is "[1, 2, 3]"
✔ Array plumbing 4 ms
- When I calculate "sort(["pear", "fig", "kiwi"])"
- Then the result is "["fig", "kiwi", "pear"]"
✔ Array plumbing 4 ms
- When I calculate "unique([3, 1, 3, 2, 1])"
- Then the result is "[3, 1, 2]"
✔ Array plumbing 4 ms
- When I calculate "reverse([1, 2, 3])"
- Then the result is "[3, 2, 1]"
✔ Array plumbing 5 ms
- When I calculate "reverse("abc")"
- Then the result is ""cba""
✔ Array plumbing 4 ms
- When I calculate "seq(1, 5)"
- Then the result is "[1, 2, 3, 4, 5]"
✔ Array plumbing 4 ms
- When I calculate "seq(10, 0, -2)"
- Then the result is "[10, 8, 6, 4, 2, 0]"
✔ Array plumbing 4 ms
- When I calculate "sum(map(x -> x^2, seq(1, 10)))"
- Then the result is "385"
✔ Array plumbing 4 ms
- When I calculate "list(1, 2, 3)"
- Then the result is "[1, 2, 3]"
✔ Array plumbing 5 ms
- When I calculate "sumproduct([2, 3], [10, 100])"
- Then the result is "320"
✔ Array plumbing 5 ms
- When I calculate "sumproduct(1, 2, 3, 4, 5, 6)"
- Then the result is "32"
✔ list() turns a range into an array — higher-order functions over cells 18 ms
- Given the sheet contains:
- When I calculate "sum(filter(x -> x > 10, list(A:1..A:4)))"
- Then the result is "42"
- When I calculate "map(x -> x * 2, list(A:1..A:2))"
- Then the result is "[10, 24]"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "variance(2, 4, 4, 4, 5, 5, 7, 9)"
- Then the result is "4.5714285714285714285714285714285714285714285714286"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "mode(1, 2, 2, 3, 3, 3)"
- Then the result is "3"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "mode(4, 9, 4, 9)"
- Then the result is "4"
✔ Statistics depth (sample conventions, full precision) 4 ms
- When I calculate "percentile(15, 20, 35, 40, 50, 0.4)"
- Then the result is "29"
✔ Statistics depth (sample conventions, full precision) 4 ms
- When I calculate "percentile(1, 2, 3, 4, 0.75)"
- Then the result is "3.25"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "percentile(1, 2, 3, 1)"
- Then the result is "3"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "geomean(4, 9)"
- Then the result is "6"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "correl(1, 2, 3, 2, 4, 6)"
- Then the result is "1"
✔ Statistics depth (sample conventions, full precision) 5 ms
- When I calculate "slope(3, 5, 7, 1, 2, 3)"
- Then the result is "2"
✔ Statistics depth (sample conventions, full precision) 4 ms
- When I calculate "intercept(3, 5, 7, 1, 2, 3)"
- Then the result is "1"
✔ Statistics depth (sample conventions, full precision) 4 ms
- When I calculate "forecast(4, 3, 5, 7, 1, 2, 3)"
- Then the result is "9"
✔ Amortization and depreciation 20 ms
- When I calculate "round(ipmt(0.05/12, 1, 360, 200000), 10)"
- Then the result is "-833.3333333333"
✔ Amortization and depreciation 211 ms
- When I calculate "round(ipmt(0.05/12, 360, 360, 200000), 10)"
- Then the result is "-4.4549512283"
✔ Amortization and depreciation 70 ms
- When I calculate "round(cumipmt(0.05/12, 360, 200000, 1, 12), 2)"
- Then the result is "-9932.99"
✔ Amortization and depreciation 32 ms
- When I calculate "round(cumprinc(0.05/12, 360, 200000, 1, 12), 2)"
- Then the result is "-2950.73"
✔ Amortization and depreciation 52 ms
- When I calculate "ipmt(0.05/12, 7, 360, 200000) + ppmt(0.05/12, 7, 360, 200000) == pmt(0.05/12, 360, 200000)"
- Then the result is "1"
✔ Amortization and depreciation 5 ms
- When I calculate "sln(30000, 7500, 10)"
- Then the result is "2250"
✔ Amortization and depreciation 5 ms
- When I calculate "round(syd(30000, 7500, 10, 1), 2)"
- Then the result is "4090.91"
✔ Amortization and depreciation 6 ms
- When I calculate "ddb(30000, 7500, 10, 1)"
- Then the result is "6000"
✔ Amortization and depreciation 5 ms
- When I calculate "ddb(30000, 7500, 10, 10)"
- Then the result is "0"
✔ Amortization and depreciation 6 ms
- When I calculate "round(nominal(effectiveRate(0.06, 12), 12), 10)"
- Then the result is "0.06"
✔ Business days and calendar positions 6 ms
- When I calculate "quarter(date(2026, 6, 6))"
- Then the result is "2"
✔ Business days and calendar positions 5 ms
- When I calculate "quarter(date(2026, 11, 1))"
- Then the result is "4"
✔ Business days and calendar positions 5 ms
- When I calculate "weeknum(date(2026, 1, 1))"
- Then the result is "1"
✔ Business days and calendar positions 8 ms
- When I calculate "weeknum(date(2026, 1, 4))"
- Then the result is "2"
✔ Business days and calendar positions 5 ms
- When I calculate "workday(date(2026, 6, 5), 1) == date(2026, 6, 8)"
- Then the result is "1"
✔ Business days and calendar positions 6 ms
- When I calculate "workday(date(2026, 6, 8), -1) == date(2026, 6, 5)"
- Then the result is "1"
✔ Business days and calendar positions 5 ms
- When I calculate "networkdays(date(2026, 6, 1), date(2026, 6, 30))"
- Then the result is "22"
✔ Business days and calendar positions 6 ms
- When I calculate "networkdays(date(2026, 6, 1), date(2026, 6, 30), date(2026, 6, 19))"
- Then the result is "21"
✔ Business days and calendar positions 7 ms
- When I calculate "networkdays(date(2026, 6, 5), date(2026, 6, 1))"
- Then the result is "-5"
✔ Scientific completions 6 ms
- When I calculate "deg(pi)"
- Then the result is "180"
✔ Scientific completions 6 ms
- When I calculate "deg(pi / 4)"
- Then the result is "45"
✔ Scientific completions 5 ms
- When I calculate "sin(rad(90))"
- Then the result is "1"
✔ Scientific completions 6 ms
- When I calculate "sinh(0)"
- Then the result is "0"
✔ Scientific completions 5 ms
- When I calculate "cosh(0)"
- Then the result is "1"
✔ Scientific completions 5 ms
- When I calculate "tanh(0)"
- Then the result is "0"
✔ Scientific completions 4 ms
- When I calculate "asinh(0)"
- Then the result is "0"
✔ Scientific completions 4 ms
- When I calculate "acosh(1)"
- Then the result is "0"
✔ Scientific completions 4 ms
- When I calculate "atanh(0)"
- Then the result is "0"
✔ atan2 knows its quadrant 13 ms
- When I calculate "atan2(1, 1) - pi/4"
- Then the result is within "1e-15" of zero
- When I calculate "atan2(-1, -1) + 3 * pi/4"
- Then the result is within "1e-15" of zero
✔ Programmer tools — exact at any width 5 ms
- When I calculate "toBase(255, 16)"
- Then the result is ""FF""
✔ Programmer tools — exact at any width 5 ms
- When I calculate "toBase(10, 2)"
- Then the result is ""1010""
✔ Programmer tools — exact at any width 6 ms
- When I calculate "fromBase("ff", 16)"
- Then the result is "255"
✔ Programmer tools — exact at any width 7 ms
- When I calculate "fromBase("1010", 2)"
- Then the result is "10"
✔ Programmer tools — exact at any width 7 ms
- When I calculate "toBase(-255, 16)"
- Then the result is ""-FF""
✔ Programmer tools — exact at any width 5 ms
- When I calculate "fromBase("-ff", 16)"
- Then the result is "-255"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "fromBase(toBase(123456789012345678901234567890, 36), 36)"
- Then the result is "123456789012345678901234567890"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "bitAnd(12, 10)"
- Then the result is "8"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "bitOr(12, 10)"
- Then the result is "14"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "bitXor(12, 10)"
- Then the result is "6"
✔ Programmer tools — exact at any width 6 ms
- When I calculate "bitShift(1, 100)"
- Then the result is "1.267650600228229401496703205376e+30"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "bitShift(256, -4)"
- Then the result is "16"
✔ Programmer tools — exact at any width 8 ms
- When I calculate "bitAnd(0xFF, 0x0F)"
- Then the result is "15"
✔ Programmer tools — exact at any width 5 ms
- When I calculate "toBase(0b1111, 16)"
- Then the result is ""F""
✔ solve() is goal seek in a formula 43 ms
- When I calculate "solve(x -> x^2, 2) - sqrt(2)"
- Then the result is within "1e-12" of zero
- When I calculate "solve(cos, 0, 1) - pi/2"
- Then the result is within "1e-12" of zero
- When I calculate "solve(x -> if(x < 1, -1, 1), 0) - 1"
- Then the result is within "1e-12" of zero
- When I calculate "solve(r -> fv(r, 120, -100), 20000, 0.01) * 12"
- Then the result is within "1e-9" of "0.0958092381724"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "avg(2, 4, 9) == sum(2, 4, 9) / count(2, 4, 9)"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "stdev(2, 4, 4, 7) == sqrt(variance(2, 4, 4, 7))"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "variance(2, 4) == ((2 - 3)^2 + (4 - 3)^2) / 1"
- Then the result is "1"
✔ Derived builtins equal their definitions 11 ms
- When I calculate "cbrt(27) == root(27, 3)"
- Then the result is "1"
✔ Derived builtins equal their definitions 6 ms
- When I calculate "geomean(2, 4, 8) == root(product(2, 4, 8), 3)"
- Then the result is "1"
✔ Derived builtins equal their definitions 6 ms
- When I calculate "percent(8.25) == 8.25 / 100"
- Then the result is "1"
✔ Derived builtins equal their definitions 6 ms
- When I calculate "deg(2) == 2 * 180 / pi"
- Then the result is "1"
✔ Derived builtins equal their definitions 6 ms
- When I calculate "choose(10, 4) == fact(10) / (fact(4) * fact(6))"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "perm(10, 4) == fact(10) / fact(6)"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "lcm(12, 18) == 12 * 18 / gcd(12, 18)"
- Then the result is "1"
✔ Derived builtins equal their definitions 7 ms
- When I calculate "median(4, 1, 3) == sort([4, 1, 3])[1]"
- Then the result is "1"
✔ Derived builtins equal their definitions 6 ms
- When I calculate "sumproduct([2, 3], [10, 100]) == 2 * 10 + 3 * 100"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "npv(0.1, 300, 420) == 300 / 1.1 + 420 / 1.1^2"
- Then the result is "1"
✔ Derived builtins equal their definitions 7 ms
- When I calculate "forecast(4, 3, 5, 7, 1, 2, 3) == intercept(3, 5, 7, 1, 2, 3) + slope(3, 5, 7, 1, 2, 3) * 4"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "quarter(date(2026, 11, 1)) == trunc((month(date(2026, 11, 1)) - 1) / 3) + 1"
- Then the result is "1"
✔ Derived builtins equal their definitions 5 ms
- When I calculate "bitShift(5, 3) == 5 * 2^3"
- Then the result is "1"
✔ The new functions explain their mistakes 5 ms
- When I calculate "choose(-1, 2)"
- Then the calculation fails mentioning "non-negative"
✔ The new functions explain their mistakes 5 ms
- When I calculate "sort([1, "a"])"
- Then the calculation fails mentioning "all numbers or all strings"
✔ The new functions explain their mistakes 5 ms
- When I calculate "seq(1, 10, 0)"
- Then the calculation fails mentioning "can't be 0"
✔ The new functions explain their mistakes 5 ms
- When I calculate "mode(1, 2, 3)"
- Then the calculation fails mentioning "no value repeats"
✔ The new functions explain their mistakes 9 ms
- When I calculate "percentile(1, 2, 3, 1.5)"
- Then the calculation fails mentioning "between 0 and 1"
✔ The new functions explain their mistakes 9 ms
- When I calculate "geomean(4, -9)"
- Then the calculation fails mentioning "positive"
✔ The new functions explain their mistakes 7 ms
- When I calculate "correl(1, 2, 3, 4, 5)"
- Then the calculation fails mentioning "equal-length"
✔ The new functions explain their mistakes 9 ms
- When I calculate "ipmt(0.05, 0, 12, 1000)"
- Then the calculation fails mentioning "1 ≤ per ≤ nper"
✔ The new functions explain their mistakes 7 ms
- When I calculate "syd(30000, 7500, 10, 11)"
- Then the calculation fails mentioning "1 ≤ per ≤ life"
✔ The new functions explain their mistakes 7 ms
- When I calculate "toBase(1.5, 16)"
- Then the calculation fails mentioning "integer"
✔ The new functions explain their mistakes 6 ms
- When I calculate "fromBase("xyz", 16)"
- Then the calculation fails mentioning "not a base-16"
✔ The new functions explain their mistakes 6 ms
- When I calculate "bitAnd(-1, 2)"
- Then the calculation fails mentioning "non-negative"
✔ The new functions explain their mistakes 5 ms
- When I calculate "solve(x -> x^2 + 1, 0)"
- Then the calculation fails mentioning "did not converge"
✔ The new functions explain their mistakes 7 ms
- When I calculate "ipmt(0.05, 1, 0, 1000)"
- Then the calculation fails mentioning "1 ≤ per ≤ nper"
✔ The new functions explain their mistakes 5 ms
- When I calculate "cumipmt(0.05/12, 12, 1000, 5, 2)"
- Then the calculation fails mentioning "1 ≤ start ≤ end"
✔ The new functions explain their mistakes 5 ms
- When I calculate "nominal(-2, 12)"
- Then the calculation fails mentioning "above -100%"
✔ The new functions explain their mistakes 5 ms
- When I calculate "ddb(30000, 7500, 10, 11)"
- Then the calculation fails mentioning "1 ≤ per ≤ life"
✔ The new functions explain their mistakes 6 ms
- When I calculate "sln(1, 1, 0)"
- Then the calculation fails mentioning "can't be 0"
✔ The new functions explain their mistakes 5 ms
- When I calculate "toBase(255, 50)"
- Then the calculation fails mentioning "2–36"
✔ The new functions explain their mistakes 5 ms
- When I calculate "fromBase("", 16)"
- Then the calculation fails mentioning "at least one digit"
✔ The new functions explain their mistakes 6 ms
- When I calculate "bitShift(1, 1.5)"
- Then the calculation fails mentioning "integer"
✔ The new functions explain their mistakes 65 ms
- When I calculate "seq(1, 1000000)"
- Then the calculation fails mentioning "100,000"
✔ The new functions explain their mistakes 5 ms
- When I calculate "workday(date(2026, 1, 1), 200001)"
- Then the calculation fails mentioning "too many"
✔ The new functions explain their mistakes 6 ms
- When I calculate "networkdays(date(1, 1, 1), date(9999, 1, 1))"
- Then the calculation fails mentioning "too many"
The mathematics a user can reach
108 scenarios
✔ Core arithmetic and algebra 15 ms
- When I calculate "1 + 2 * 3"
- Then the result is "7"
✔ Core arithmetic and algebra 9 ms
- When I calculate "(1 + 2) * 3"
- Then the result is "9"
✔ Core arithmetic and algebra 5 ms
- When I calculate "10 / 4"
- Then the result is "2.5"
✔ Core arithmetic and algebra 6 ms
- When I calculate "mod(7, 3)"
- Then the result is "1"
✔ Core arithmetic and algebra 5 ms
- When I calculate "-2^2"
- Then the result is "-4"
✔ Core arithmetic and algebra 5 ms
- When I calculate "2^-1"
- Then the result is "0.5"
✔ Core arithmetic and algebra 6 ms
- When I calculate "abs(-5)"
- Then the result is "5"
✔ Core arithmetic and algebra 4 ms
- When I calculate "min(3, 1, 2)"
- Then the result is "1"
✔ Core arithmetic and algebra 5 ms
- When I calculate "max(3, 1, 2)"
- Then the result is "3"
✔ Core arithmetic and algebra 5 ms
- When I calculate "floor(2.7)"
- Then the result is "2"
✔ Core arithmetic and algebra 4 ms
- When I calculate "floor(-1.5)"
- Then the result is "-2"
✔ Core arithmetic and algebra 5 ms
- When I calculate "ceil(2.1)"
- Then the result is "3"
✔ Core arithmetic and algebra 4 ms
- When I calculate "ceil(-1.5)"
- Then the result is "-1"
✔ Core arithmetic and algebra 4 ms
- When I calculate "trunc(-2.7)"
- Then the result is "-2"
✔ Core arithmetic and algebra 4 ms
- When I calculate "round(2.345, 2)"
- Then the result is "2.34"
✔ Core arithmetic and algebra 4 ms
- When I calculate "round(2.567, 2)"
- Then the result is "2.57"
✔ Core arithmetic and algebra 5 ms
- When I calculate "round(2.5)"
- Then the result is "2"
✔ Core arithmetic and algebra 4 ms
- When I calculate "mod(7, 3)"
- Then the result is "1"
✔ Core arithmetic and algebra 4 ms
- When I calculate "fact(5)"
- Then the result is "120"
✔ Core arithmetic and algebra 4 ms
- When I calculate "fact(20)"
- Then the result is "2432902008176640000"
✔ Core arithmetic and algebra 4 ms
- When I calculate "gcd(12, 18)"
- Then the result is "6"
✔ Core arithmetic and algebra 5 ms
- When I calculate "lcm(4, 6)"
- Then the result is "12"
✔ Core arithmetic and algebra 5 ms
- When I calculate "percent(8.25)"
- Then the result is "0.0825"
✔ Core arithmetic and algebra 5 ms
- When I calculate "sqrt(144)"
- Then the result is "12"
✔ Core arithmetic and algebra 4 ms
- When I calculate "cbrt(27)"
- Then the result is "3"
✔ Core arithmetic and algebra 4 ms
- When I calculate "cbrt(-27)"
- Then the result is "-3"
✔ Core arithmetic and algebra 4 ms
- When I calculate "root(32, 5)"
- Then the result is "2"
✔ Core arithmetic and algebra 4 ms
- When I calculate "pow(4, 0.5)"
- Then the result is "2"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "√16"
- Then the result is "4"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "√(2 + 2)"
- Then the result is "2"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "√2^2"
- Then the result is "2"
✔ Mathematical symbols are first-class 6 ms
- When I calculate "6 × 7 ÷ 2"
- Then the result is "21"
✔ Mathematical symbols are first-class 5 ms
- When I calculate "6 ÷ 2 − 1"
- Then the result is "2"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "3 · 4"
- Then the result is "12"
✔ Mathematical symbols are first-class 6 ms
- When I calculate "π − pi"
- Then the result is "0"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "τ ÷ π"
- Then the result is "2"
✔ Mathematical symbols are first-class 4 ms
- When I calculate "pi"
- Then the result is "3.14159265358979323846264338327950288419716939937510582097494"
✔ Mathematical symbols are first-class 5 ms
- When I calculate "3 ≠ 2"
- Then the result is "1"
✔ Mathematical symbols are first-class 5 ms
- When I calculate "2 ≤ 2"
- Then the result is "1"
✔ Constants are protected 7 ms
- When I calculate "π = 3"
- Then the calculation fails mentioning "cannot assign to 'π'"
✔ Trigonometry (radians) 4 ms
- When I calculate "sin(pi / 2)"
- Then the result is "1"
✔ Trigonometry (radians) 4 ms
- When I calculate "cos(pi)"
- Then the result is "-1"
✔ Trigonometry (radians) 4 ms
- When I calculate "tan(pi / 4)"
- Then the result is "0.9999999999999999"
✔ Trigonometry (radians) 4 ms
- When I calculate "asin(1) * 2"
- Then the result is "3.1415926535897932"
✔ Trigonometry (radians) 4 ms
- When I calculate "acos(0) * 2"
- Then the result is "3.1415926535897932"
✔ Trigonometry (radians) 4 ms
- When I calculate "atan(1) * 4"
- Then the result is "3.1415926535897932"
✔ The degree postfix converts to radians 4 ms
- When I calculate "sin(90°)"
- Then the result is "1"
✔ The degree postfix converts to radians 4 ms
- When I calculate "cos(180°)"
- Then the result is "-1"
✔ The degree postfix converts to radians 4 ms
- When I calculate "90° == pi / 2"
- Then the result is "1"
✔ Exponentials and logarithms 4 ms
- When I calculate "exp(0)"
- Then the result is "1"
✔ Exponentials and logarithms 4 ms
- When I calculate "ln(e)"
- Then the result is "1"
✔ Exponentials and logarithms 4 ms
- When I calculate "log10(1000)"
- Then the result is "3"
✔ Exponentials and logarithms 4 ms
- When I calculate "log(2, 8)"
- Then the result is "3"
✔ Exponentials and logarithms 4 ms
- When I calculate "sin(0)"
- Then the result is "0"
✔ Exponentials and logarithms 4 ms
- When I calculate "cos(0)"
- Then the result is "1"
✔ exp inverts ln to within the Double seam 4 ms
- When I calculate "exp(ln(7))"
- Then the result is within "0.000000000000002" of "7"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "avg(2, 4, 9)"
- Then the result is "5"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "median(1, 9, 5)"
- Then the result is "5"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "median(1, 2, 3, 4)"
- Then the result is "2.5"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "count(1, 2, 3)"
- Then the result is "3"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "product(2, 3, 4)"
- Then the result is "24"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "stdev(2, 4, 4, 4, 5, 5, 7, 9)"
- Then the result is "2.1380899352993950774764278470380281724320113187307"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "avg([2, 4, 9])"
- Then the result is "5"
✔ Statistics over lists, arrays, and ranges 4 ms
- When I calculate "stdev(1, 3) - sqrt(2)"
- Then the result is "0"
✔ Products mirror summation 11 ms
- When I calculate "∏(2, 3, 4)"
- Then the result is "24"
- When I calculate "∏_i=1^5(i)"
- Then the result is "120"
✔ Euler's number from its factorial series 4 ms
- When I calculate "∑_k=0^45(1 / fact(k)) - e"
- Then the result is within "1e-45" of zero
✔ Nicomachus — the sum of cubes is the square of the sum 4 ms
- When I calculate "reduce((a, b) -> a + b, map(x -> x^3, [1,2,3,4,5,6,7,8,9,10]), 0) == (∑_i=1^10(i))^2"
- Then the result is "1"
✔ Three roads to twenty factorial agree exactly 15 ms
- When I calculate "rec(n) = if(n <= 1, 1, n * rec(n - 1))"
- And I calculate "∏_i=1^20(i) == fact(20)"
- Then the result is "1"
- When I calculate "rec(20) == fact(20)"
- Then the result is "1"
✔ Compound growth's product form equals its closed form, exactly 8 ms
- When I calculate "grow(r, n) = ∏_i=1^(n)(1 + r)"
- And I calculate "grow(0.05, 30) == 1.05^30"
- Then the result is "1"
✔ A mortgage payment round-trips the principal to 30 decimal places 38 ms
- When I calculate "pv(0.05/12, 360, pmt(0.05/12, 360, 200000)) - 200000"
- Then the result is within "1e-30" of zero
✔ Finance agrees with Excel (independent cross-validation) 18 ms
- When I calculate "pmt(0.05/12, 360, 200000)"
- Then the result is within "0.01" of "-1073.64"
✔ Finance agrees with Excel (independent cross-validation) 4 ms
- When I calculate "pmt(0, 12, 1200)"
- Then the result is within "0.000001" of "-100"
✔ Finance agrees with Excel (independent cross-validation) 7 ms
- When I calculate "fv(0.06/12, 120, -100)"
- Then the result is within "0.01" of "16387.93"
✔ Finance agrees with Excel (independent cross-validation) 3 ms
- When I calculate "fv(0, 12, -100)"
- Then the result is within "0.000001" of "1200"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "pv(0.04/12, 60, -500)"
- Then the result is within "0.01" of "27149.53"
✔ Finance agrees with Excel (independent cross-validation) 4 ms
- When I calculate "nper(0.05/12, -1073.64, 200000)"
- Then the result is within "0.01" of "360"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "nper(0, -100, 1200)"
- Then the result is within "0.000001" of "12"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "rate(360, -1073.64, 200000) * 12"
- Then the result is within "0.0001" of "0.05"
✔ Finance agrees with Excel (independent cross-validation) 4 ms
- When I calculate "npv(0.1, 3000, 4200, 6800)"
- Then the result is within "0.01" of "11307.29"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "irr(-70000, 12000, 15000, 18000, 21000, 26000)"
- Then the result is within "0.0001" of "0.0866"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "effectiveRate(0.06, 12)"
- Then the result is within "0.000001" of "0.061678"
✔ Finance agrees with Excel (independent cross-validation) 4 ms
- When I calculate "markup(80, 25)"
- Then the result is within "0.000001" of "100"
✔ Finance agrees with Excel (independent cross-validation) 5 ms
- When I calculate "percentOf(30, 120)"
- Then the result is within "0.000001" of "25"
✔ Finance agrees with Excel (independent cross-validation) 4 ms
- When I calculate "percentChange(80, 100)"
- Then the result is within "0.000001" of "25"
✔ A mortgage's lifetime cost, built up across lines 27 ms
- When I calculate "r = 0.05 / 12"
- And I calculate "payment = pmt(r, 360, 200000)"
- And I calculate "payment * 360"
- Then the result is within "0.01" of "-386511.57"
✔ IRR refuses an unsolvable stream 4 ms
- When I calculate "irr(1000, 2000)"
- Then the calculation fails mentioning "both positive and negative"
✔ Finance (spreadsheet sign convention) 4 ms
- When I calculate "fv(0.1, 2, -100)"
- Then the result is "210"
✔ Finance (spreadsheet sign convention) 5 ms
- When I calculate "npv(0, 10, 20, 30)"
- Then the result is "60"
✔ Finance (spreadsheet sign convention) 4 ms
- When I calculate "npv(0.1, 110)"
- Then the result is "100"
✔ Finance (spreadsheet sign convention) 4 ms
- When I calculate "nper(0, -10, 100)"
- Then the result is "10"
✔ Finance (spreadsheet sign convention) 4 ms
- When I calculate "irr(-100, 110)"
- Then the result is "0.1"
✔ Finance (spreadsheet sign convention) 4 ms
- When I calculate "effectiveRate(0.12, 12)"
- Then the result is "0.126825030131969720661201"
✔ Accounting shorthands 4 ms
- When I calculate "margin(100, 80)"
- Then the result is "20"
✔ Accounting shorthands 4 ms
- When I calculate "markup(80, 25)"
- Then the result is "100"
✔ Accounting shorthands 4 ms
- When I calculate "percentOf(50, 200)"
- Then the result is "25"
✔ Accounting shorthands 9 ms
- When I calculate "percentChange(100, 150)"
- Then the result is "50"
✔ Date arithmetic on exact day serials 16 ms
- When I calculate "weekday(date(2026, 6, 6))"
- Then the result is "6"
✔ Date arithmetic on exact day serials 7 ms
- When I calculate "weekday(date(2026, 6, 8))"
- Then the result is "1"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "year(date(2026, 6, 6))"
- Then the result is "2026"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "month(date(2026, 6, 6))"
- Then the result is "6"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "day(date(2026, 6, 6))"
- Then the result is "6"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "days(date(2026, 3, 1), date(2026, 2, 1))"
- Then the result is "28"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "edate(date(2026, 1, 31), 1) == date(2026, 2, 28)"
- Then the result is "1"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "eomonth(date(2024, 2, 1), 0) == date(2024, 2, 29)"
- Then the result is "1"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "day(eomonth(date(1900, 2, 1), 0))"
- Then the result is "28"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "day(eomonth(date(2000, 2, 1), 0))"
- Then the result is "29"
✔ Date arithmetic on exact day serials 4 ms
- When I calculate "days(date(2001, 1, 1), date(2000, 1, 1))"
- Then the result is "366"
✔ Logic composes with everything 4 ms
- When I calculate "and(1 < 2, or(0, not(0)))"
- Then the result is "1"
Input/display modes are dialects over one canonical language
61 scenarios
✔ Programmer mode reads glyphs as bitwise and modulo 17 ms
- Given the calculator is in programmer mode
- When I calculate "5 ^ 3"
- Then the result is "6"
✔ Programmer mode reads glyphs as bitwise and modulo 5 ms
- Given the calculator is in programmer mode
- When I calculate "12 & 10"
- Then the result is "8"
✔ Programmer mode reads glyphs as bitwise and modulo 4 ms
- Given the calculator is in programmer mode
- When I calculate "1 << 4"
- Then the result is "16"
✔ Programmer mode reads glyphs as bitwise and modulo 5 ms
- Given the calculator is in programmer mode
- When I calculate "8 >> 2"
- Then the result is "2"
✔ Programmer mode reads glyphs as bitwise and modulo 3 ms
- Given the calculator is in programmer mode
- When I calculate "17 % 5"
- Then the result is "2"
✔ Programmer mode reads glyphs as bitwise and modulo 4 ms
- Given the calculator is in programmer mode
- When I calculate "pow(2, 10)"
- Then the result is "1024"
✔ Programmer mode reads pipe as bitwise OR 4 ms
- Given the calculator is in programmer mode
- When I calculate "12 | 3"
- Then the result is "15"
✔ Programmer-mode bitwise precedence follows Python 4 ms
- Given the calculator is in programmer mode
- When I calculate "1 & 1 == 1"
- Then the result is "1"
✔ Bitwise glyphs are Programmer-mode only 2 ms
- When I calculate "5 & 3"
- Then the calculation fails mentioning "Programmer-mode operator"
✔ Bitwise glyphs are Programmer-mode only 2 ms
- When I calculate "1 << 2"
- Then the calculation fails mentioning "Programmer-mode operator"
✔ Bitwise glyphs are Programmer-mode only 3 ms
- When I calculate "8 >> 1"
- Then the calculation fails mentioning "Programmer-mode operator"
✔ Scientific mode keeps the Normal arithmetic core 3 ms
- Given the calculator is in scientific mode
- When I calculate "2 ^ 3"
- Then the result is "8"
✔ Scientific mode keeps the Normal arithmetic core 3 ms
- Given the calculator is in scientific mode
- When I calculate "3%"
- Then the result is "0.03"
✔ Scientific mode keeps the Normal arithmetic core 3 ms
- Given the calculator is in scientific mode
- When I calculate "50%"
- Then the result is "0.5"
✔ Scientific mode still rejects the Programmer-only glyphs 3 ms
- Given the calculator is in scientific mode
- When I calculate "5 & 3"
- Then the calculation fails mentioning "Programmer-mode operator"
✔ Scientific mode echoes plain numbers in scientific notation 4 ms
- Given the calculator is in scientific mode
- When I calculate "123456 * 2"
- Then the result is "246912"
- And the log echoes "2.46912e5"
✔ Scientific mode echoes plain numbers in scientific notation 4 ms
- Given the calculator is in scientific mode
- When I calculate "5"
- Then the result is "5"
- And the log echoes "5e0"
✔ Scientific mode echoes plain numbers in scientific notation 5 ms
- Given the calculator is in scientific mode
- When I calculate "1 / 8"
- Then the result is "0.125"
- And the log echoes "1.25e-1"
✔ Scientific mode echoes plain numbers in scientific notation 5 ms
- Given the calculator is in scientific mode
- When I calculate "-2500 * 2"
- Then the result is "-5000"
- And the log echoes "-5e3"
✔ The engineering style snaps the exponent to a multiple of 3 4 ms
- Given the calculator is in scientific mode
- And the scientific style is "eng"
- When I calculate "123456 * 2"
- Then the log echoes "246.912e3"
✔ The engineering style snaps the exponent to a multiple of 3 5 ms
- Given the calculator is in scientific mode
- And the scientific style is "eng"
- When I calculate "0.05"
- Then the log echoes "50e-3"
✔ The engineering style snaps the exponent to a multiple of 3 5 ms
- Given the calculator is in scientific mode
- And the scientific style is "eng"
- When I calculate "5"
- Then the log echoes "5e0"
✔ Money keeps its own display in scientific mode 3 ms
- Given the calculator is in scientific mode
- When I calculate "$10 * 5%"
- Then the log echoes "$0.50"
✔ A grouped number keeps its grouping in scientific mode 3 ms
- Given the calculator is in scientific mode
- When I calculate "138,561 * 9%"
- Then the log echoes "12,470.49"
✔ Degrees work in scientific mode 4 ms
- Given the calculator is in scientific mode
- When I calculate "sin(90°)"
- Then the result is "1"
- And the log echoes "1e0"
✔ The finance mode is retired in favor of core currency 2 ms
- When I calculate ":mode finance"
- Then the calculation fails mentioning "currency now works"
✔ Currency carries through arithmetic 3 ms
- When I calculate "$10"
- Then the result is "Money(10, "USD")"
- And the log echoes "$10.00"
✔ Currency carries through arithmetic 3 ms
- When I calculate "$10 * 5%"
- Then the result is "Money(0.5, "USD")"
- And the log echoes "$0.50"
✔ Currency carries through arithmetic 3 ms
- When I calculate "€10 * .4"
- Then the result is "Money(4, "EUR")"
- And the log echoes "€4.00"
✔ Currency carries through arithmetic 3 ms
- When I calculate "$10 + $5"
- Then the result is "Money(15, "USD")"
- And the log echoes "$15.00"
✔ Currency carries through arithmetic 3 ms
- When I calculate "$10 - $2.50"
- Then the result is "Money(7.5, "USD")"
- And the log echoes "$7.50"
✔ Currency carries through arithmetic 9 ms
- When I calculate "$100 / 4"
- Then the result is "Money(25, "USD")"
- And the log echoes "$25.00"
✔ Currency carries through arithmetic 3 ms
- When I calculate "£1234.567"
- Then the result is "Money(1234.567, "GBP")"
- And the log echoes "£1,234.57"
✔ Currency carries through arithmetic 3 ms
- When I calculate "¥5000 * 2"
- Then the result is "Money(10000, "JPY")"
- And the log echoes "¥10,000.00"
✔ The Money constructor works in normal mode 4 ms
- When I calculate "Money(10, "USD")"
- Then the result is "Money(10, "USD")"
- And the log echoes "$10.00"
✔ An unknown currency code is refused 2 ms
- When I calculate "Money(10, "XYZ")"
- Then the calculation fails mentioning "unknown currency"
✔ An unsupported currency glyph is refused 2 ms
- When I calculate "₫100"
- Then the calculation fails mentioning "unsupported currency"
✔ Percent on a currency amount is refused 2 ms
- When I calculate "$9%"
- Then the calculation fails mentioning "can't apply % to a currency amount"
✔ A plain number absorbs into the currency 2 ms
- When I calculate "$10 + 5"
- Then the log echoes "$15.00"
✔ A plain number absorbs into the currency 2 ms
- When I calculate "5 + $10"
- Then the log echoes "$15.00"
✔ A plain number absorbs into the currency 2 ms
- When I calculate "$10 * 3"
- Then the log echoes "$30.00"
✔ Mixing currencies is refused 2 ms
- When I calculate "$10 + €5"
- Then the calculation fails mentioning "can't mix currencies"
✔ Mixing currencies is refused 2 ms
- When I calculate "€10 - $5"
- Then the calculation fails mentioning "can't mix currencies"
✔ Mixing currencies is refused 2 ms
- When I calculate "$10 * £2"
- Then the calculation fails mentioning "can't mix currencies"
✔ The currency survives every arithmetic operator 2 ms
- When I calculate "$10 * $2"
- Then the log echoes "$20.00"
✔ The currency survives every arithmetic operator 2 ms
- When I calculate "$10 / $2"
- Then the log echoes "$5.00"
✔ The currency survives every arithmetic operator 2 ms
- When I calculate "$10 + $2"
- Then the log echoes "$12.00"
✔ The currency survives every arithmetic operator 1 ms
- When I calculate "$10 - $2"
- Then the log echoes "$8.00"
✔ Negation keeps the currency and puts the sign outside the symbol 3 ms
- When I calculate "-$1234.5"
- Then the result is "Money(-1234.5, "USD")"
- And the log echoes "-$1,234.50"
✔ Grouped numbers echo their grouping 3 ms
- When I calculate "138,561"
- Then the result is "138561"
- And the log echoes "138,561"
✔ Grouped numbers echo their grouping 3 ms
- When I calculate "138,561 * 9%"
- Then the result is "12470.49"
- And the log echoes "12,470.49"
✔ Grouped numbers echo their grouping 3 ms
- When I calculate "1,000 + 1"
- Then the result is "1001"
- And the log echoes "1,001"
✔ Grouped numbers echo their grouping 4 ms
- When I calculate "1,234,567"
- Then the result is "1234567"
- And the log echoes "1,234,567"
✔ Grouped numbers echo their grouping 3 ms
- When I calculate "12,470.49 * 2"
- Then the result is "24940.98"
- And the log echoes "24,940.98"
✔ Grouped numbers echo their grouping 3 ms
- When I calculate "-138,561"
- Then the result is "-138561"
- And the log echoes "-138,561"
✔ Grouped currency composes through a subexpression 3 ms
- When I calculate "$10,000 + ($15,000 * 5%)"
- Then the result is "Money(10750, "USD")"
- And the log echoes "$10,750.00"
✔ The argument separator wins over grouping 2 ms
- When I calculate "max(138,561)"
- Then the result is "561"
✔ Grouping is suppressed inside an array literal 2 ms
- When I calculate "sum([1,500])"
- Then the result is "501"
✔ Malformed thousands groups are refused 2 ms
- When I calculate "1,23"
- Then the calculation fails mentioning "thousands group"
✔ Malformed thousands groups are refused 2 ms
- When I calculate "1,2345"
- Then the calculation fails mentioning "thousands group"
✔ Malformed thousands groups are refused 2 ms
- When I calculate "1234,567"
- Then the calculation fails mentioning "thousands group"
Namespaces group declarations under a qualified name
33 scenarios
✔ A namespace exposes its data types by qualified name 5 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number } }"
- When I calculate "Geo::Point(x: 3, y: 4).x"
- Then the result is "3"
✔ A namespaced record renders and re-parses with its qualified name 2 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number } }"
- When I calculate "Geo::Point(x: 3, y: 4)"
- Then the result is "Geo::Point(x: 3, y: 4)"
✔ A field referencing a sibling type resolves within the namespace 3 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number }; data Line { a: Point, b: Point } }"
- And I calculate "seg = Geo::Line(a: Geo::Point(x: 1, y: 1), b: Geo::Point(x: 4, y: 5))"
- When I calculate "seg.b.x"
- Then the result is "4"
✔ List fields nest namespaced records 5 ms
- Given I calculate "namespace Bits { data Field { name: String, flags: [String] }; data Format { fields: [Field] } }"
- And I calculate "f = Bits::Format(fields: [Bits::Field(name: "owner", flags: ["r", "w", "x"])])"
- When I calculate "len(f.fields)"
- Then the result is "1"
- When I calculate "len(f.fields[0].flags)"
- Then the result is "3"
✔ A namespaced function resolves sibling functions and types unqualified 3 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number }; dist(p: Point) = sqrt(p.x^2 + p.y^2); origin() = Point(x: 0, y: 0) }"
- When I calculate "Geo::dist(Geo::Point(x: 3, y: 4))"
- Then the result is "5"
- When I calculate "Geo::dist(Geo::origin())"
- Then the result is "0"
✔ A namespaced function calls a sibling function unqualified 2 ms
- Given I calculate "namespace M { a(n) = b(n) + 1; b(n) = n * 2 }"
- When I calculate "M::a(10)"
- Then the result is "21"
✔ Namespaced recursion resolves itself through the home namespace 2 ms
- Given I calculate "namespace R { fact(n) = if(n <= 1, 1, n * fact(n - 1)) }"
- When I calculate "R::fact(6)"
- Then the result is "720"
✔ Qualified construction validates and resolves loudly 2 ms
- Given I calculate "namespace Bits { data Field { name: String }; data Format { fields: [Field] } }"
- When I calculate "Bits::Format(fields: [5])"
- Then the calculation fails mentioning "is a Bits::Field"
✔ Qualified construction validates and resolves loudly 2 ms
- Given I calculate "namespace Bits { data Field { name: String }; data Format { fields: [Field] } }"
- When I calculate "Bits::Nope(x: 1)"
- Then the calculation fails mentioning "unknown function"
✔ A namespace needs at least one declaration 1 ms
- When I calculate "namespace Empty { }"
- Then the calculation fails mentioning "at least one declaration"
✔ Namespace members are separated by a semicolon 1.0 ms
- When I calculate "namespace Bad { data A { x: Number } data B { y: Number } }"
- Then the calculation fails mentioning "separate namespace declarations with ';'"
✔ A namespace exposes a constant by qualified name 2 ms
- Given I calculate "namespace Phys { c = 299792458 }"
- When I calculate "Phys::c"
- Then the result is "299792458"
✔ A namespaced function uses a sibling constant unqualified 3 ms
- Given I calculate "namespace Circle { k = 3.14159; area(r) = k * r * r }"
- When I calculate "Circle::area(10)"
- Then the result is "314.159"
✔ An imported namespace's constant resolves unqualified 3 ms
- Given I calculate "namespace Phys { c = 299792458 }"
- And I calculate "import Phys"
- When I calculate "c"
- Then the result is "299792458"
✔ Importing a constant that collides with a global is loud 2 ms
- Given I calculate "speed = 5"
- And I calculate "namespace V { data Q { x: Number }; speed = 10 }"
- When I calculate "import V"
- Then the calculation fails mentioning "would shadow 'speed'"
✔ A nested namespace resolves by its full qualified name 2 ms
- Given I calculate "namespace A { data Point { x: Number, y: Number }; namespace B { dist(p: Point) = sqrt(p.x^2 + p.y^2) } }"
- When I calculate "A::B::dist(A::Point(x: 3, y: 4))"
- Then the result is "5"
✔ An inner member uses a parent constant and function unqualified 1 ms
- Given I calculate "namespace A { base = 10; twice(n) = n * 2; namespace B { f(x) = twice(x) + base } }"
- When I calculate "A::B::f(5)"
- Then the result is "20"
✔ A nested record renders and re-parses with its full qualified name 2 ms
- Given I calculate "namespace A { namespace B { data Point { x: Number, y: Number } } }"
- When I calculate "A::B::Point(x: 3, y: 4)"
- Then the result is "A::B::Point(x: 3, y: 4)"
✔ A nested namespace survives save and reopen 2 ms
- Given I calculate "namespace A { k = 7; namespace B { triple(n) = n * 3 } }"
- When the workbook is saved and reopened
- And I calculate "A::B::triple(A::k)"
- Then the result is "21"
✔ import brings a namespace's members into scope unqualified 4 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number }; dist(p: Point) = sqrt(p.x^2 + p.y^2) }"
- And I calculate "import Geo"
- When I calculate "dist(Point(x: 3, y: 4))"
- Then the result is "5"
- When I calculate "Geo::dist(Geo::Point(x: 6, y: 8))"
- Then the result is "10"
✔ Importing an unknown namespace errors 0.8 ms
- When I calculate "import Nope"
- Then the calculation fails mentioning "no namespace 'Nope'"
✔ Importing a name that collides with a global is loud 2 ms
- Given I calculate "data Pt { x: Number }"
- And I calculate "namespace NS { data Pt { x: Number } }"
- When I calculate "import NS"
- Then the calculation fails mentioning "would shadow 'Pt'"
✔ Importing a name that collides with a builtin is loud 1 ms
- Given I calculate "namespace M2 { sqrt(x) = x * x }"
- When I calculate "import M2"
- Then the calculation fails mentioning "would shadow 'sqrt'"
✔ Re-importing a namespace is a harmless no-op 3 ms
- Given I calculate "namespace Ok { area(r) = 3 * r * r }"
- And I calculate "import Ok"
- And I calculate "import Ok"
- When I calculate "area(2)"
- Then the result is "12"
✔ A builtin is reachable by its module, and the bare name stays global 3 ms
- When I calculate "Core::sqrt(16)"
- Then the result is "4"
- When I calculate "Core::sqrt(16) == sqrt(16)"
- Then the result is "1"
✔ A qualified builtin must belong to that module 0.9 ms
- When I calculate "Finance::sqrt(16)"
- Then the calculation fails mentioning "unknown function 'Finance::sqrt'"
✔ A qualified builtin works as a value 0.8 ms
- When I calculate "map(Core::sqrt, [4, 9, 16])"
- Then the result is "[2, 3, 4]"
✔ Importing a builtin module is a no-op — its members are in the prelude 1 ms
- Given I calculate "import Stats"
- When I calculate "median(1, 2, 3, 4, 5)"
- Then the result is "3"
✔ A namespace and its import survive save and reopen 5 ms
- Given I calculate "namespace Geo { data Point { x: Number, y: Number }; dist(p: Point) = sqrt(p.x^2 + p.y^2) }"
- And I calculate "import Geo"
- When the workbook is saved and reopened
- And I calculate "Geo::dist(Geo::Point(x: 3, y: 4))"
- Then the result is "5"
- When I calculate "dist(Point(x: 6, y: 8))"
- Then the result is "10"
✔ A namespaced constant survives save and reopen 4 ms
- Given I calculate "namespace Phys { c = 299792458; energy(m) = m * c * c }"
- When the workbook is saved and reopened
- And I calculate "Phys::c"
- Then the result is "299792458"
- When I calculate "Phys::energy(2)"
- Then the result is "179751035747363528"
✔ An IPv4 subnetting toolkit composes from namespaces and fixed-width ints 9 ms
- Given I calculate "namespace Net { ip(a,b,c,d) = UInt32(a)*16777216 + UInt32(b)*65536 + UInt32(c)*256 + UInt32(d); mask(p) = UInt32(2^32 - 2^(32 - p)); wildcard(p) = bitNot(mask(p)); network(addr, p) = bitAnd(addr, mask(p)); broadcast(addr, p) = bitOr(addr, wildcard(p)); hosts(p) = 2^(32 - p) - 2 }"
- And I calculate "addr = Net::ip(192, 168, 1, 130)"
- When I calculate "Net::network(addr, 24) == 3232235776"
- Then the result is "1"
- When I calculate "Net::broadcast(addr, 24) == 3232236031"
- Then the result is "1"
- When I calculate "Net::hosts(24)"
- Then the result is "254"
- When I calculate "Net::network(addr, 26) == 3232235904"
- Then the result is "1"
- When I calculate "Net::hosts(26)"
- Then the result is "62"
✔ IPv6 subnetting scales the same toolkit to a 128-bit UInt128 7 ms
- Given I calculate "namespace Net6 { mask(p) = UInt128(2^128 - 2^(128 - p)); network(addr, p) = bitAnd(addr, mask(p)); hosts(p) = 2^(128 - p); hextet(addr, i) = bitAnd(bitShift(addr, -16 * (7 - i)), 65535) }"
- And I calculate "addr = UInt128(2) * 2^112 + UInt128(0xdb8) * 2^96 + UInt128(0xabcd)"
- When I calculate "Net6::hextet(addr, 0) == 2"
- Then the result is "1"
- When I calculate "Net6::hextet(addr, 1) == 3512"
- Then the result is "1"
- When I calculate "Net6::network(addr, 64) == bitAnd(addr, UInt128(2^128 - 2^64))"
- Then the result is "1"
- When I calculate "Net6::hosts(64)"
- Then the result is "18446744073709551616"
✔ A MAC address splits into OUI and device with a UInt64 3 ms
- Given I calculate "mac = UInt64(0x001B44113AB7)"
- When I calculate "bitAnd(bitShift(mac, -24), 0xFFFFFF) == 0x001B44"
- Then the result is "1"
- When I calculate "bitAnd(mac, 0xFFFFFF) == 0x113AB7"
- Then the result is "1"
Inspecting the workbook
13 scenarios
✔ Workbook reports its sheets 2 ms
- Given a sheet named "Budget"
- When I calculate "Workbook.count"
- Then the result is "2"
✔ Worksheet names are readable 0.7 ms
- Given a sheet named "Budget"
- When I calculate "Workbook.worksheets[1].name == "Budget""
- Then the result is "1"
✔ A worksheet is reachable by name 0.6 ms
- Given a sheet named "Budget"
- When I calculate "Workbook.worksheets["Budget"].name == "Budget""
- Then the result is "1"
✔ A negative index counts from the end 0.5 ms
- Given a sheet named "Budget"
- When I calculate "Workbook.worksheets[-1].name == "Budget""
- Then the result is "1"
✔ Reading a cell's value through the object graph 1.0 ms
- Given a sheet named "Budget"
- And cell B:1 on "Budget" contains "1200"
- When I calculate "Workbook.worksheets["Budget"].cell("B", 1).value * 2"
- Then the result is "2400"
✔ A cell value is reachable by position 0.6 ms
- Given cell A:1 contains "=2 + 3"
- When I calculate "Workbook.worksheets[0].cell("A", 1).value"
- Then the result is "5"
✔ The flat cell() accessor reads the active sheet 0.5 ms
- Given cell A:1 contains "42"
- When I calculate "cell("A", 1).value"
- Then the result is "42"
✔ The flat cell() accessor reaches another sheet by name 0.8 ms
- Given a sheet named "Budget"
- And cell A:1 on "Budget" contains "99"
- When I calculate "cell("Budget", "A", 1).value"
- Then the result is "99"
✔ sheetNames() lists every sheet 0.5 ms
- Given a sheet named "Budget"
- When I calculate "len(sheetNames())"
- Then the result is "2"
✔ rowCount() reports the grid size 0.3 ms
- When I calculate "rowCount()"
- Then the result is "1000"
✔ A formula reading a cell through reflection recomputes live 0.6 ms
- Given cell A:1 contains "10"
- And cell B:1 contains "=cell("A", 1).value + 1"
- Then cell B:1 shows "11"
✔ A user's own function shadows a reflection accessor 0.5 ms
- When I calculate "cell(x) = x * 10"
- And I calculate "cell(5)"
- Then the result is "50"
✔ An unknown sheet is reported clearly 0.3 ms
- When I calculate "cell("Nope", "A", 1).value"
- Then the calculation fails mentioning "unknown sheet"
Scripts — multi-line sources split into logical statements
8 scenarios
✔ A pretty multi-line namespace block runs as one statement 0.8 ms
- When I run the script:
- Then the result is "3"
✔ Parentheses continue a statement across lines 0.3 ms
- When I run the script:
- Then the result is "10"
✔ Brackets continue a statement across lines 0.3 ms
- When I run the script:
- Then the result is "3"
✔ Shebang and comment lines pass between statements 0.2 ms
- When I run the script:
- Then the result is "2"
✔ A first-line comment documents a multi-line definition 0.5 ms
- When I run the script:
- When I calculate "man triple"
- Then documentation is shown mentioning "three of x"
✔ Braces inside a string do not open a continuation 0.2 ms
- When I run the script:
- Then the result is "13"
✔ An unclosed block at end of input is a loud error 0.2 ms
- When I run the script:
- Then the calculation fails mentioning "unterminated"
✔ Thousands grouping works inside a continued statement 0.4 ms
- When I run the script:
- Then the result is "12470.49"
- And the log echoes "12,470.49"
Working in the grid
21 scenarios
✔ Cells reference each other 2 ms
- Given cell B:1 contains "1200"
- And cell B:2 contains "=B:1 * 2"
- Then cell B:2 shows "2400"
✔ Labels never become errors 0.8 ms
- Given cell A:1 contains "Q1 revenue"
- Then cell A:1 shows "Q1 revenue"
✔ A comment cell is a note that holds no value 0.5 ms
- Given cell A:1 contains "# tentative — revisit in Q4"
- Then cell A:1 shows "# tentative — revisit in Q4"
✔ A note cell is skipped in ranges and errors when referenced 2 ms
- Given the sheet contains:
- And cell B:4 contains "=sum(B:1..B:3)"
- Then cell B:4 shows "150"
- And cell B:5 contains "=B:2 + 1"
- Then cell B:5 shows an error mentioning "not a number"
✔ A trailing comment on a formula is kept and ignored by evaluation 0.9 ms
- Given cell B:1 contains "200"
- And cell B:2 contains "=B:1 * 1.08 # with sales tax"
- Then cell B:2 shows "216"
✔ Ranges aggregate a column 1.0 ms
- Given the sheet contains:
- And cell B:4 contains "=sum(B:1..B:3)"
- Then cell B:4 shows "400"
✔ A named cell reads like prose 1 ms
- Given cell B:7 contains "0.08"
- And cell B:7 is named "Projected Rate"
- And cell A:1 contains "='Projected Rate' * 100"
- Then cell A:1 shows "8"
✔ Sheet definitions belong to their cells 2 ms
- Given cell A:1 contains "rate = 0.1"
- And cell A:2 contains "=100 * rate"
- Then cell A:1 shows "𝑖 rate"
- And cell A:2 shows "10"
- When I calculate "rate = 0.5"
- Then the calculation fails mentioning "defined in cell"
✔ A function defined in a cell is callable 1 ms
- Given cell A:1 contains "tax(x) = x * 1.0825"
- And cell A:2 contains "=tax(200)"
- Then cell A:1 shows "λ tax(x)"
- And cell A:2 shows "216.5"
✔ A slider is a value with a range 1 ms
- Given cell A:1 contains "r = slider(0.11, 0, 0.2)"
- And cell A:2 contains "=r * 100"
- Then cell A:1 is a slider set to "0.11"
- And cell A:2 shows "11"
✔ The log sees the sheet 1.0 ms
- Given cell B:1 contains "1200"
- When I calculate "B:1 * 2"
- Then the result is "2400"
✔ Explicit markers override auto-detection 1 ms
- Given cell A:1 contains ""123""
- And cell A:2 contains "=12 * rte"
- Then cell A:1 shows "123"
- And cell A:2 shows an error mentioning "unknown variable"
✔ A formula mistake is an error, not a guess 1.0 ms
- Given cell B:1 contains "100"
- And cell B:2 contains "B:1 / 0"
- Then cell B:2 shows an error mentioning "division by zero"
✔ Empty cells read as zero 0.5 ms
- Given cell A:1 contains "=B:9 + 5"
- Then cell A:1 shows "5"
✔ Circular references are caught, not infinite 1 ms
- Given cell A:1 contains "=A:2 + 1"
- And cell A:2 contains "=A:1 + 1"
- Then cell A:1 shows an error mentioning "circular reference"
✔ Referencing a text cell is an error 1 ms
- Given cell A:1 contains "Q1 revenue"
- And cell A:2 contains "=A:1 * 2"
- Then cell A:2 shows an error mentioning "not a number"
✔ The controls family holds values 4 ms
- Given cell A:1 contains "flag = checkbox(true)"
- And cell A:2 contains "n = stepper(5, 1, 20)"
- And cell A:3 contains "region = dropdown("EU", ["EU", "US"])"
- And cell B:1 contains "=if(flag, n * 2, 0)"
- And cell B:2 contains "=if(region == "EU", 1, 2)"
- Then cell B:1 shows "10"
- And cell B:2 shows "1"
✔ A column of checkboxes counts its checked ones 1 ms
- Given the sheet contains:
- And cell C:4 contains "=sum(C:1..C:3)"
- Then cell C:4 shows "2"
✔ Formulas reach across worksheets by name 3 ms
- Given a sheet named "Budget"
- And cell B:1 on "Budget" contains "1200"
- And cell A:1 contains "=Budget!B:1 * 2"
- Then cell A:1 shows "2400"
- When I calculate "sum(Budget!B:1..B:1) + 1"
- Then the result is "1201"
✔ A renamed-away sheet breaks loudly, not silently 0.6 ms
- Given cell A:1 contains "=Nowhere!B:1"
- Then cell A:1 shows an error mentioning "unknown sheet"
✔ A workbook round-trips through its file format 8 ms
- Given a sheet named "Loan"
- And cell B:1 on "Loan" contains "350000"
- And cell A:1 contains "=Loan!B:1 / 100"
- And cell B:7 contains "0.08"
- And cell B:7 is named "Projected Rate"
- And cell A:2 contains "='Projected Rate' * 100"
- When I calculate "growth = 1.5"
- And the workbook is saved and reopened
- Then cell A:1 shows "3500"
- And cell A:2 shows "8"
- When I calculate "growth * 2"
- Then the result is "3"
Strings, arrays, maps, and the functions that work them
45 scenarios
✔ Text and structure functions 6 ms
- When I calculate "len([1, 2, 3])"
- Then the result is "3"
✔ Text and structure functions 4 ms
- When I calculate "len("hello")"
- Then the result is "5"
✔ Text and structure functions 3 ms
- When I calculate "len({a: 1, b: 2})"
- Then the result is "2"
✔ Text and structure functions 2 ms
- When I calculate "first([5, 6, 7])"
- Then the result is "5"
✔ Text and structure functions 3 ms
- When I calculate "last([5, 6, 7])"
- Then the result is "7"
✔ Text and structure functions 3 ms
- When I calculate "keys({name: "Ada", age: 36})"
- Then the result is "["name", "age"]"
✔ Text and structure functions 2 ms
- When I calculate "values({a: 1, b: 2})"
- Then the result is "[1, 2]"
✔ Text and structure functions 2 ms
- When I calculate "sum(values({a: 1, b: 2}))"
- Then the result is "3"
✔ Text and structure functions 2 ms
- When I calculate "concat("Q", 1, "-", 2026)"
- Then the result is ""Q1-2026""
✔ Text and structure functions 2 ms
- When I calculate "concat([1, 2], [3])"
- Then the result is "[1, 2, 3]"
✔ Text and structure functions 2 ms
- When I calculate ""Q" + 1"
- Then the result is ""Q1""
✔ Text and structure functions 2 ms
- When I calculate ""a" + "b" + "c""
- Then the result is ""abc""
✔ Text and structure functions 2 ms
- When I calculate ""abc"[0]"
- Then the result is ""a""
✔ Text and structure functions 2 ms
- When I calculate "slice("hello", 0, 2)"
- Then the result is ""he""
✔ Text and structure functions 2 ms
- When I calculate "slice("hello", 2)"
- Then the result is ""llo""
✔ Text and structure functions 3 ms
- When I calculate "slice("hello", 1, 4)"
- Then the result is ""ell""
✔ Text and structure functions 2 ms
- When I calculate "slice("hello", 0, len("hello"))"
- Then the result is ""hello""
✔ Text and structure functions 2 ms
- When I calculate "slice([1, 2, 3, 4], 1, 3)"
- Then the result is "[2, 3]"
✔ Text and structure functions 2 ms
- When I calculate "slice([1, 2, 3, 4], 2)"
- Then the result is "[3, 4]"
✔ Text and structure functions 2 ms
- When I calculate "split("a,b,c", ",")"
- Then the result is "["a", "b", "c"]"
✔ Text and structure functions 3 ms
- When I calculate "split("nosep", ",")"
- Then the result is "["nosep"]"
✔ Text and structure functions 3 ms
- When I calculate "len(split("a,b,c", ","))"
- Then the result is "3"
✔ Text and structure functions 2 ms
- When I calculate "charCode("A")"
- Then the result is "65"
✔ Text and structure functions 2 ms
- When I calculate "fromCharCode(65)"
- Then the result is ""A""
✔ Text and structure functions 2 ms
- When I calculate "fromCharCode(charCode("z"))"
- Then the result is ""z""
✔ Text and structure functions 2 ms
- When I calculate "[[1, 2], [3, 4]][1][0]"
- Then the result is "3"
✔ Text and structure functions 2 ms
- When I calculate "{a: [1, 2]}.a[1]"
- Then the result is "2"
✔ Text and structure functions 2 ms
- When I calculate "[1, 2] == [1, 2]"
- Then the result is "1"
✔ Text and structure functions 2 ms
- When I calculate "{a: 1, b: 2} == {b: 2, a: 1}"
- Then the result is "1"
✔ Text and structure functions 2 ms
- When I calculate ""x" != 5"
- Then the result is "1"
✔ Structure mistakes explain themselves 2 ms
- When I calculate "[1, 2][5]"
- Then the calculation fails mentioning "out of range"
✔ Structure mistakes explain themselves 2 ms
- When I calculate "{a: 1}.b"
- Then the calculation fails mentioning "no key 'b'"
✔ Structure mistakes explain themselves 2 ms
- When I calculate "first([])"
- Then the calculation fails mentioning "empty array"
✔ Structure mistakes explain themselves 2 ms
- When I calculate "sum(["a"])"
- Then the calculation fails mentioning "works on numbers"
✔ Structure mistakes explain themselves 2 ms
- When I calculate ""a" * 2"
- Then the calculation fails mentioning "expected a number"
✔ Structure mistakes explain themselves 3 ms
- When I calculate "{a: 1, a: 2}"
- Then the calculation fails mentioning "duplicate key"
✔ Structure mistakes explain themselves 3 ms
- When I calculate "slice("abc", 0, 9)"
- Then the calculation fails mentioning "out of range"
✔ Structure mistakes explain themselves 3 ms
- When I calculate "slice("abc", -1, 2)"
- Then the calculation fails mentioning "out of range"
✔ Structure mistakes explain themselves 2 ms
- When I calculate "split("abc", "")"
- Then the calculation fails mentioning "non-empty separator"
✔ Structure mistakes explain themselves 5 ms
- When I calculate "charCode("ab")"
- Then the calculation fails mentioning "single character"
✔ Structure mistakes explain themselves 4 ms
- When I calculate "fromCharCode(-1)"
- Then the calculation fails mentioning "valid character"
✔ charCode enables ordered character-class tests 7 ms
- When I calculate "and(charCode("5") >= charCode("0"), charCode("5") <= charCode("9"))"
- Then the result is "1"
- When I calculate "and(charCode("x") >= charCode("0"), charCode("x") <= charCode("9"))"
- Then the result is "0"
✔ Filter and reduce shape data like a user thinks 7 ms
- When I calculate "filter(x -> x > 1, [1, 2, 3])"
- Then the result is "[2, 3]"
- When I calculate "reduce((a, b) -> a + b, [], 42)"
- Then the result is "42"
✔ Lambdas live in variables and close over parameters 12 ms
- When I calculate "f = x -> x * 2"
- And I calculate "f(21)"
- Then the result is "42"
- When I calculate "scale(arr, n) = map(x -> x * n, arr)"
- And I calculate "scale([1, 2, 3], 10)"
- Then the result is "[10, 20, 30]"
✔ A named function reference follows redefinition 10 ms
- When I calculate "h(x) = x + 1"
- And I calculate "alias = h"
- And I calculate "h(x) = x + 100"
- And I calculate "alias(1)"
- Then the result is "101"