test(formula): add lexer and parser tests

This commit is contained in:
dualfroz committed 2026-08-27 22:40:00 +02:00
1 parent 7a2a2e9881
commit 47ea8bb501
1 file changed
+202
+202
View File
@@ -0,0 +1,202 @@
import 'package:cellar/cellar.dart';
import 'package:test/test.dart';
double numberOf(CellValue value) => (value as NumberValue).value;
CellValue evalFormula(String formula) {
final sheet = Sheet();
sheet.setValue('Z100', formula);
return sheet.valueOf('Z100');
}
void main() {
group('arithmetic and precedence', () {
test('multiplication binds tighter than addition', () {
expect(numberOf(evalFormula('=1+2*3')), 7);
});
test('parentheses override precedence', () {
expect(numberOf(evalFormula('=(1+2)*3')), 9);
});
test('exponent is right associative', () {
expect(numberOf(evalFormula('=2^3^2')), 512);
});
test('unary minus binds looser than exponent', () {
expect(numberOf(evalFormula('=-2^2')), -4);
});
test('division yields a fraction', () {
expect(numberOf(evalFormula('=10/4')), 2.5);
});
test('percent operator is modulo', () {
expect(numberOf(evalFormula('=10%3')), 1);
});
test('comparison has the lowest precedence', () {
expect(evalFormula('=1+1=2'), const BooleanValue(true));
});
test('inequality returns a boolean', () {
expect(evalFormula('=5>=5'), const BooleanValue(true));
expect(evalFormula('=2<>2'), const BooleanValue(false));
});
test('string concatenation with ampersand', () {
expect(evalFormula('="a"&"b"&"c"'), const TextValue('abc'));
});
});
group('built-in functions', () {
late Sheet sheet;
setUp(() {
sheet = Sheet();
sheet.setValue('A1', '1');
sheet.setValue('A2', '2');
sheet.setValue('A3', '3');
});
test('SUM over a range', () {
sheet.setValue('B1', '=SUM(A1:A3)');
expect(numberOf(sheet.valueOf('B1')), 6);
});
test('SUM mixes range and scalar arguments', () {
sheet.setValue('B1', '=SUM(A1:A3, 10, 4)');
expect(numberOf(sheet.valueOf('B1')), 20);
});
test('AVERAGE and AVG alias', () {
sheet.setValue('B1', '=AVERAGE(A1:A3)');
sheet.setValue('B2', '=AVG(A1:A3)');
expect(numberOf(sheet.valueOf('B1')), 2);
expect(numberOf(sheet.valueOf('B2')), 2);
});
test('MIN and MAX', () {
sheet.setValue('B1', '=MIN(A1:A3)');
sheet.setValue('B2', '=MAX(A1:A3)');
expect(numberOf(sheet.valueOf('B1')), 1);
expect(numberOf(sheet.valueOf('B2')), 3);
});
test('COUNT counts only numbers while COUNTA counts non-blank', () {
sheet.setValue('C1', '10');
sheet.setValue('C2', 'hello');
sheet.setValue('C4', 'TRUE');
sheet.setValue('D1', '=COUNT(C1:C4)');
sheet.setValue('D2', '=COUNTA(C1:C4)');
expect(numberOf(sheet.valueOf('D1')), 1);
expect(numberOf(sheet.valueOf('D2')), 3);
});
test('IF chooses a branch', () {
sheet.setValue('B1', '=IF(A3>A1,"big","small")');
expect(sheet.valueOf('B1'), const TextValue('big'));
});
test('nested functions', () {
sheet.setValue('B1', '=IF(SUM(A1:A3)=6, MAX(A1:A3)*2, 0)');
expect(numberOf(sheet.valueOf('B1')), 6);
});
test('math helpers', () {
expect(numberOf(evalFormula('=ABS(-4.5)')), 4.5);
expect(numberOf(evalFormula('=ROUND(3.14159, 2)')), 3.14);
expect(numberOf(evalFormula('=FLOOR(7.8)')), 7);
expect(numberOf(evalFormula('=CEIL(7.2)')), 8);
expect(numberOf(evalFormula('=MOD(10, 3)')), 1);
expect(numberOf(evalFormula('=POWER(2, 10)')), 1024);
expect(numberOf(evalFormula('=SQRT(144)')), 12);
expect(numberOf(evalFormula('=INT(9.99)')), 9);
});
test('text helpers', () {
expect(evalFormula('=CONCAT("foo","bar")'), const TextValue('foobar'));
expect(numberOf(evalFormula('=LEN("hello")')), 5);
expect(evalFormula('=LEFT("spreadsheet", 6)'), const TextValue('spread'));
expect(evalFormula('=RIGHT("spreadsheet", 5)'), const TextValue('sheet'));
expect(evalFormula('=MID("spreadsheet", 7, 5)'), const TextValue('sheet'));
expect(evalFormula('=UPPER("cellar")'), const TextValue('CELLAR'));
expect(evalFormula('=LOWER("CELLAR")'), const TextValue('cellar'));
expect(evalFormula('=TRIM(" a b ")'), const TextValue('a b'));
});
test('logical helpers', () {
expect(evalFormula('=AND(TRUE, 1=1, 2>1)'), const BooleanValue(true));
expect(evalFormula('=OR(FALSE, 1=2, 3>1)'), const BooleanValue(true));
expect(evalFormula('=NOT(1=1)'), const BooleanValue(false));
});
test('INDEX into a range', () {
sheet.setValue('B1', '=INDEX(A1:A3, 2)');
expect(numberOf(sheet.valueOf('B1')), 2);
});
test('INDEX out of range returns #REF!', () {
sheet.setValue('B1', '=INDEX(A1:A3, 9)');
expect(sheet.valueOf('B1'), const ErrorValue(ErrorKind.ref));
});
test('IFERROR replaces an error', () {
expect(numberOf(evalFormula('=IFERROR(1/0, 42)')), 42);
expect(numberOf(evalFormula('=IFERROR(5, 42)')), 5);
});
test('ISERROR detects errors', () {
expect(evalFormula('=ISERROR(1/0)'), const BooleanValue(true));
expect(evalFormula('=ISERROR(1+1)'), const BooleanValue(false));
});
});
group('errors', () {
test('division by zero', () {
expect(evalFormula('=1/0'), const ErrorValue(ErrorKind.divZero));
});
test('reference error literal propagates through arithmetic', () {
expect(evalFormula('=#REF!'), const ErrorValue(ErrorKind.ref));
expect(evalFormula('=#REF!+1'), const ErrorValue(ErrorKind.ref));
});
test('unknown function name', () {
expect(evalFormula('=NOPE(1)'), const ErrorValue(ErrorKind.name));
});
test('bare unknown name', () {
expect(evalFormula('=SOMENAME'), const ErrorValue(ErrorKind.name));
});
test('square root of a negative is #NUM!', () {
expect(evalFormula('=SQRT(-1)'), const ErrorValue(ErrorKind.numError));
});
test('error propagates from a referenced cell', () {
final sheet = Sheet();
sheet.setValue('A1', '=1/0');
sheet.setValue('B1', '=A1+5');
expect(sheet.valueOf('B1'), const ErrorValue(ErrorKind.divZero));
});
});
group('address helpers', () {
test('column label round trips', () {
expect(columnLabel(0), 'A');
expect(columnLabel(25), 'Z');
expect(columnLabel(26), 'AA');
expect(columnLabel(27), 'AB');
expect(columnLabel(701), 'ZZ');
expect(columnLabel(702), 'AAA');
});
test('parse A1 style references', () {
expect(CellAddress.parse('A1'), const CellAddress(0, 0));
expect(CellAddress.parse('B2'), const CellAddress(1, 1));
expect(CellAddress.parse('AA10').col, 26);
expect(CellAddress.parse('AA10').row, 9);
});
});
}