diff --git a/test/csv_test.dart b/test/csv_test.dart new file mode 100644 index 0000000..e95bdf6 --- /dev/null +++ b/test/csv_test.dart @@ -0,0 +1,60 @@ +import 'package:cellar/cellar.dart'; +import 'package:test/test.dart'; + +double numberOf(CellValue value) => (value as NumberValue).value; + +void main() { + group('csv parsing', () { + test('parses quoted fields with embedded commas and quotes', () { + final rows = parseCsv('a,"b,c","he said ""hi"""\n1,2,3'); + expect(rows, [ + ['a', 'b,c', 'he said "hi"'], + ['1', '2', '3'], + ]); + }); + + test('a trailing newline does not create an empty record', () { + final rows = parseCsv('a,b\nc,d\n'); + expect(rows.length, 2); + }); + }); + + group('csv import and compute', () { + test('imports literals and formulas then computes', () { + final sheet = Sheet(); + importCsv(sheet, 'Name,Qty,Price,Total\nApple,3,2,=B2*C2\nPear,4,5,=B3*C3\n,,,=SUM(D2:D3)'); + expect(numberOf(sheet.valueOf('D2')), 6); + expect(numberOf(sheet.valueOf('D3')), 20); + expect(numberOf(sheet.valueOf('D4')), 26); + }); + }); + + group('csv round trip', () { + test('formulas survive export and re-import with equal results', () { + final original = Sheet(); + original.setValue('A1', '10'); + original.setValue('A2', '20'); + original.setValue('A3', '30'); + original.setValue('B1', '=SUM(A1:A3)'); + original.setValue('B2', '=AVERAGE(A1:A3)'); + original.setValue('B3', '=IF(B1>50, "high", "low")'); + + final formulaCsv = exportFormulas(original); + final reloaded = Sheet(); + importCsv(reloaded, formulaCsv); + + expect(exportFormulas(reloaded), formulaCsv); + expect(numberOf(reloaded.valueOf('B1')), 60); + expect(numberOf(reloaded.valueOf('B2')), 20); + expect(reloaded.valueOf('B3'), const TextValue('high')); + }); + + test('value export renders computed numbers', () { + final sheet = Sheet(); + sheet.setValue('A1', '4'); + sheet.setValue('B1', '=A1*A1'); + final valueCsv = exportValues(sheet); + expect(valueCsv, '4,16'); + }); + }); +} diff --git a/test/format_test.dart b/test/format_test.dart new file mode 100644 index 0000000..d55a71c --- /dev/null +++ b/test/format_test.dart @@ -0,0 +1,47 @@ +import 'package:cellar/cellar.dart'; +import 'package:test/test.dart'; + +void main() { + group('number format', () { + test('grouped decimals', () { + const format = NumberFormat.decimal(decimals: 2, thousands: true); + expect(format.apply(const NumberValue(1234567.891)), '1,234,567.89'); + expect(format.apply(const NumberValue(-5.5)), '-5.50'); + }); + + test('percent scales by a hundred', () { + const format = NumberFormat.percent(decimals: 1); + expect(format.apply(const NumberValue(0.1234)), '12.3%'); + }); + + test('currency before and after', () { + const before = NumberFormat.currency(r'$', decimals: 2); + const after = NumberFormat.currency('zl', decimals: 2, before: false, thousands: false); + expect(before.apply(const NumberValue(1234.5)), r'$1,234.50'); + expect(after.apply(const NumberValue(30)), '30.00zl'); + }); + + test('integer with thousands', () { + const format = NumberFormat.integer(thousands: true); + expect(format.apply(const NumberValue(1234567)), '1,234,567'); + }); + + test('non-number values fall through to display', () { + const format = NumberFormat.decimal(decimals: 2); + expect(format.apply(const TextValue('hello')), 'hello'); + expect(format.apply(const ErrorValue(ErrorKind.divZero)), '#DIV/0!'); + }); + + test('fromCode parses common patterns', () { + expect(NumberFormat.fromCode('#,##0.00').apply(const NumberValue(1234.5)), '1,234.50'); + expect(NumberFormat.fromCode('0.0%').apply(const NumberValue(0.25)), '25.0%'); + expect(NumberFormat.fromCode(r'$#,##0.00').apply(const NumberValue(1234.5)), r'$1,234.50'); + }); + + test('toCode round trips', () { + expect(const NumberFormat.decimal(decimals: 2, thousands: true).toCode(), '#,##0.00'); + expect(const NumberFormat.percent(decimals: 1).toCode(), '0.0%'); + expect(const NumberFormat.integer(thousands: true).toCode(), '#,##0'); + }); + }); +} diff --git a/test/table_test.dart b/test/table_test.dart new file mode 100644 index 0000000..166f39a --- /dev/null +++ b/test/table_test.dart @@ -0,0 +1,76 @@ +import 'package:cellar/cellar.dart'; +import 'package:test/test.dart'; + +double num(CellValue value) => (value as NumberValue).value; + +Sheet salesSheet() { + final sheet = Sheet(); + final rows = [ + ['Region', 'Product', 'Amount'], + ['North', 'A', '100'], + ['South', 'A', '150'], + ['North', 'B', '200'], + ['South', 'B', '50'], + ['North', 'A', '75'], + ]; + for (var r = 0; r < rows.length; r++) { + for (var c = 0; c < rows[r].length; c++) { + sheet.setCell(CellAddress(c, r), rows[r][c]); + } + } + return sheet; +} + +void main() { + group('data table', () { + test('reads a header row from a sheet', () { + final table = DataTable.fromSheet(salesSheet()); + expect(table.headers, ['Region', 'Product', 'Amount']); + expect(table.rowCount, 5); + }); + + test('sortBy descending', () { + final sorted = DataTable.fromSheet(salesSheet()).sortBy('Amount', ascending: false); + expect(num(sorted.cell(0, 'Amount')), 200); + expect(num(sorted.cell(4, 'Amount')), 50); + }); + + test('filter by a column predicate', () { + final filtered = DataTable.fromSheet(salesSheet()) + .where('Region', (value) => value.display() == 'North'); + expect(filtered.rowCount, 3); + }); + + test('groupBy with sum and count aggregations', () { + final grouped = DataTable.fromSheet(salesSheet()).groupBy( + ['Region'], + const [ + Aggregation('Amount', Aggregator.sum, 'Total'), + Aggregation('Amount', Aggregator.count, 'Lines'), + ], + ); + expect(grouped.headers, ['Region', 'Total', 'Lines']); + expect(num(grouped.cell(0, 'Total')), 375); + expect(num(grouped.cell(0, 'Lines')), 3); + expect(num(grouped.cell(1, 'Total')), 200); + }); + + test('pivot on region by product', () { + final pivot = DataTable.fromSheet(salesSheet()).pivot( + rowKey: 'Region', + columnKey: 'Product', + valueColumn: 'Amount', + ); + expect(pivot.rowLabels, ['North', 'South']); + expect(pivot.columnLabels, ['A', 'B']); + expect(num(pivot.at('North', 'A')), 175); + expect(num(pivot.at('South', 'B')), 50); + }); + + test('selectColumns projects a subset', () { + final projected = DataTable.fromSheet(salesSheet()).selectColumns(['Product', 'Amount']); + expect(projected.headers, ['Product', 'Amount']); + expect(projected.rows.first.length, 2); + }); + }); +}