Files
2026-10-15 11:57:09 +02:00

90 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { evaluateFormula, parseFormula, FormulaError, show } from '../src/lib/formula.js';
const vars = { Price: 12.5, Qty: 4, Name: 'Cleanser', Brand: 'Balea', Opened: '2026-03-01', Expiry: '2026-09-01', Done: true, Tags: ['a', 'b'], Empty: null };
const ctx = { today: '2026-03-31', prop: (n) => { if (!(n in vars)) throw new FormulaError(`Unknown property "${n}"`); return vars[n]; } };
const ev = (s) => evaluateFormula(s, ctx);
test('arithmetic, precedence and properties', () => {
assert.equal(ev('1 + 2 * 3'), 7);
assert.equal(ev('(1 + 2) * 3'), 9);
assert.equal(ev('2 ^ 3 ^ 2'), 512); // right-associative
assert.equal(ev('-2 ^ 2'), -4); // as in maths: the power first
assert.equal(ev('prop("Price") * prop("Qty")'), 50);
assert.equal(ev('{Price} * {Qty}'), 50);
assert.equal(ev('10 % 4'), 2);
assert.equal(ev('1 / 4'), 0.25);
assert.equal(ev('prop("Empty") + 5'), 5); // empty counts as 0
assert.equal(show(ev('0.1 + 0.2')), '0.3');
});
test('comparisons, logic and if', () => {
assert.equal(ev('3 > 2 and 2 >= 2'), true);
assert.equal(ev('1 > 2 or not false'), true);
assert.equal(ev('!(1 == 1)'), false);
assert.equal(ev('prop("Name") == "Cleanser"'), true);
assert.equal(ev('prop("Empty") == ""'), true);
assert.equal(ev('if(prop("Price") > 10, "pricey", "cheap")'), 'pricey');
assert.equal(ev('if(false, 1)'), null);
assert.equal(ev('if(prop("Empty"), "x", "y")'), 'y');
assert.equal(ev('and(true, 1, "x")'), true);
assert.equal(ev('if(true, 1, 1 / 0)'), 1); // the other branch is not evaluated
});
test('text and lists', () => {
assert.equal(ev('concat(prop("Brand"), " – ", prop("Name"))'), 'Balea – Cleanser');
assert.equal(ev('"a" + "b"'), 'ab');
assert.equal(ev('"Total: " + 5'), 'Total: 5');
assert.equal(ev('upper(prop("Name"))'), 'CLEANSER');
assert.equal(ev('length(prop("Tags"))'), 2);
assert.equal(ev('join(prop("Tags"), "/")'), 'a/b');
assert.equal(ev('contains(prop("Tags"), "B")'), true);
assert.equal(ev('contains("Hello", "ell")'), true);
assert.equal(ev('replace("a-b-c", "-", "+")'), 'a+b+c');
});
test('numbers functions', () => {
assert.equal(ev('round(2.567, 1)'), 2.6);
assert.equal(ev('round(2.5)'), 3);
assert.equal(ev('floor(2.9) + ceil(2.1)'), 5);
assert.equal(ev('abs(-3)'), 3);
assert.equal(ev('min(3, 1, 2)'), 1);
assert.equal(ev('max(prop("Tags"), 5)'.replace('prop("Tags"), ', '')), 5);
assert.equal(ev('sum(1, 2, 3)'), 6);
assert.equal(ev('average(2, 4)'), 3);
assert.equal(ev('toNumber("€12.50")'), 12.5);
});
test('dates', () => {
assert.equal(ev('today()'), '2026-03-31');
assert.equal(ev('dateBetween(prop("Expiry"), today(), "days")'), 154);
assert.equal(ev('dateBetween(prop("Expiry"), prop("Opened"), "months")'), 6);
assert.equal(ev('dateBetween(today(), prop("Expiry"), "days")'), -154);
assert.equal(ev('dateAdd(prop("Opened"), 12, "months")'), '2027-03-01');
assert.equal(ev('dateAdd("2026-01-31", 1, "months")'), '2026-02-28');
assert.equal(ev('dateAdd(prop("Opened"), 2, "weeks")'), '2026-03-15');
assert.equal(ev('prop("Expiry") - prop("Opened")'), 184); // date - date = days
assert.equal(ev('prop("Opened") + 30'), '2026-03-31'); // date + number = days later
assert.equal(ev('prop("Expiry") > today()'), true);
assert.equal(ev('year(prop("Opened")) * 100 + month(prop("Opened"))'), 202603);
assert.equal(ev('formatDate(prop("Opened"), "MMM D, YYYY")'), 'Mar 1, 2026');
});
test('mistakes are reported, not thrown as anything else', () => {
const msg = (s) => { try { ev(s); return null; } catch (e) { assert.ok(e instanceof FormulaError, s + ' -> ' + e); return e.message; } };
assert.match(msg('1 +'), /ends too soon/);
assert.match(msg('1 / 0'), /Division by zero/);
assert.match(msg('prop("Nope")'), /Unknown property/);
assert.match(msg('nope(1)'), /no function/);
assert.match(msg('Price * 2'), /not a function or a value/);
assert.match(msg('"abc" * 2'), /can't be used as a number/);
assert.match(msg('(1 + 2'), /Expected “\)”/);
assert.match(msg('1 $ 2'), /Unexpected/);
assert.match(msg('"open'), /not closed/);
assert.match(msg(''), /empty/);
assert.match(msg('dateBetween("x", today(), "days")'), /can't be used as a date/);
assert.match(msg('dateBetween(today(), today(), "decades")'), /Unknown unit/);
assert.throws(() => parseFormula('1 +'), FormulaError);
});