test(io): add csv, format and table tests
This commit is contained in:
1 parent
55a8b173ae
commit
b7f56e623d
3 files changed
+183
No files matched your search
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user