@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +149 -12
- package/dist/index.js +804 -155
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runFilter } from '../filter';
|
|
3
|
-
import { runTransforms } from '../index';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate monthly rows: one row per month for `months` months starting from `startDate`.
|
|
7
|
-
*/
|
|
8
|
-
function monthlyData(startDate: string, months: number) {
|
|
9
|
-
const rows = [];
|
|
10
|
-
const start = new Date(startDate);
|
|
11
|
-
for (let i = 0; i < months; i++) {
|
|
12
|
-
const d = new Date(start);
|
|
13
|
-
d.setMonth(d.getMonth() + i);
|
|
14
|
-
rows.push({ date: d.toISOString().slice(0, 10), value: i + 1 });
|
|
15
|
-
}
|
|
16
|
-
return rows;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generate daily rows: one row per day for `days` days starting from `startDate`.
|
|
21
|
-
*/
|
|
22
|
-
function dailyData(startDate: string, days: number) {
|
|
23
|
-
const rows = [];
|
|
24
|
-
const start = new Date(startDate);
|
|
25
|
-
for (let i = 0; i < days; i++) {
|
|
26
|
-
const d = new Date(start);
|
|
27
|
-
d.setDate(d.getDate() + i);
|
|
28
|
-
rows.push({ date: d.toISOString().slice(0, 10), value: i + 1 });
|
|
29
|
-
}
|
|
30
|
-
return rows;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('relative-time filter', () => {
|
|
34
|
-
it('last 1 year from max: 60 monthly rows, gte anchor max offset -1 year returns ~12 rows', () => {
|
|
35
|
-
const data = monthlyData('2020-01-01', 60); // Jan 2020 through Dec 2024
|
|
36
|
-
const result = runFilter(data, {
|
|
37
|
-
field: 'date',
|
|
38
|
-
gte: { anchor: 'max', offset: -1, unit: 'year' },
|
|
39
|
-
});
|
|
40
|
-
// Max date is 2024-12-01, minus 1 year = 2023-12-01
|
|
41
|
-
// Should include Dec 2023 through Dec 2024 = 13 rows
|
|
42
|
-
expect(result.length).toBeGreaterThanOrEqual(12);
|
|
43
|
-
expect(result.length).toBeLessThanOrEqual(13);
|
|
44
|
-
// All returned dates should be >= 2023-12-01
|
|
45
|
-
for (const row of result) {
|
|
46
|
-
expect(new Date(row.date as string).getTime()).toBeGreaterThanOrEqual(
|
|
47
|
-
new Date('2023-12-01').getTime(),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('last 3 years from max: 60 monthly rows, offset -3 returns ~36 rows', () => {
|
|
53
|
-
const data = monthlyData('2020-01-01', 60);
|
|
54
|
-
const result = runFilter(data, {
|
|
55
|
-
field: 'date',
|
|
56
|
-
gte: { anchor: 'max', offset: -3, unit: 'year' },
|
|
57
|
-
});
|
|
58
|
-
// Max is 2024-12-01, minus 3 years = 2021-12-01
|
|
59
|
-
// Dec 2021 through Dec 2024 = 37 rows
|
|
60
|
-
expect(result.length).toBeGreaterThanOrEqual(36);
|
|
61
|
-
expect(result.length).toBeLessThanOrEqual(37);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('last 30 days: 90 daily rows, offset -30 day returns 30-31 rows', () => {
|
|
65
|
-
const data = dailyData('2024-01-01', 90);
|
|
66
|
-
const result = runFilter(data, {
|
|
67
|
-
field: 'date',
|
|
68
|
-
gte: { anchor: 'max', offset: -30, unit: 'day' },
|
|
69
|
-
});
|
|
70
|
-
expect(result.length).toBeGreaterThanOrEqual(30);
|
|
71
|
-
expect(result.length).toBeLessThanOrEqual(31);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('anchor min + offset: lte anchor min offset +2 year returns rows within 2 years of earliest', () => {
|
|
75
|
-
const data = monthlyData('2020-01-01', 60);
|
|
76
|
-
const result = runFilter(data, {
|
|
77
|
-
field: 'date',
|
|
78
|
-
lte: { anchor: 'min', offset: 2, unit: 'year' },
|
|
79
|
-
});
|
|
80
|
-
// Min is 2020-01-01, plus 2 years = 2022-01-01
|
|
81
|
-
// Jan 2020 through Jan 2022 = 25 rows
|
|
82
|
-
expect(result.length).toBeGreaterThanOrEqual(24);
|
|
83
|
-
expect(result.length).toBeLessThanOrEqual(25);
|
|
84
|
-
for (const row of result) {
|
|
85
|
-
expect(new Date(row.date as string).getTime()).toBeLessThanOrEqual(
|
|
86
|
-
new Date('2022-01-01').getTime(),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('quarter unit: offset -4 quarter returns last ~4 quarters', () => {
|
|
92
|
-
// Generate quarterly data (every 3 months)
|
|
93
|
-
const data = [];
|
|
94
|
-
const start = new Date('2020-01-01');
|
|
95
|
-
for (let i = 0; i < 20; i++) {
|
|
96
|
-
const d = new Date(start);
|
|
97
|
-
d.setMonth(d.getMonth() + i * 3);
|
|
98
|
-
data.push({ date: d.toISOString().slice(0, 10), value: i + 1 });
|
|
99
|
-
}
|
|
100
|
-
const result = runFilter(data, {
|
|
101
|
-
field: 'date',
|
|
102
|
-
gte: { anchor: 'max', offset: -4, unit: 'quarter' },
|
|
103
|
-
});
|
|
104
|
-
// 4 quarters back from max = ~4-5 rows of quarterly data
|
|
105
|
-
expect(result.length).toBeGreaterThanOrEqual(4);
|
|
106
|
-
expect(result.length).toBeLessThanOrEqual(5);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('non-ISO dates: data with timestamp numbers resolves correctly', () => {
|
|
110
|
-
// Use numeric timestamps instead of date strings
|
|
111
|
-
const baseDate = new Date('2024-01-01').getTime();
|
|
112
|
-
const dayMs = 86400000;
|
|
113
|
-
const data = Array.from({ length: 90 }, (_, i) => ({
|
|
114
|
-
date: baseDate + i * dayMs,
|
|
115
|
-
value: i,
|
|
116
|
-
}));
|
|
117
|
-
const result = runFilter(data, {
|
|
118
|
-
field: 'date',
|
|
119
|
-
gte: { anchor: 'max', offset: -30, unit: 'day' },
|
|
120
|
-
});
|
|
121
|
-
expect(result.length).toBeGreaterThanOrEqual(30);
|
|
122
|
-
expect(result.length).toBeLessThanOrEqual(31);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('empty dataset returns [] without throwing', () => {
|
|
126
|
-
const result = runFilter([], {
|
|
127
|
-
field: 'date',
|
|
128
|
-
gte: { anchor: 'max', offset: -1, unit: 'year' },
|
|
129
|
-
});
|
|
130
|
-
expect(result).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('single row: returns that row when anchor matches', () => {
|
|
134
|
-
const data = [{ date: '2024-06-15', value: 42 }];
|
|
135
|
-
const result = runFilter(data, {
|
|
136
|
-
field: 'date',
|
|
137
|
-
gte: { anchor: 'max', offset: -1, unit: 'year' },
|
|
138
|
-
});
|
|
139
|
-
// The single row is both min and max; subtracting 1 year puts the threshold before it
|
|
140
|
-
expect(result).toHaveLength(1);
|
|
141
|
-
expect(result[0].value).toBe(42);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('chained transforms: calculate first, then relative filter works on post-transform data', () => {
|
|
145
|
-
const data = monthlyData('2020-01-01', 60);
|
|
146
|
-
const result = runTransforms(data, [
|
|
147
|
-
// Copy date field to a new field (identity via multiply by 1 won't work for dates,
|
|
148
|
-
// so we just verify the filter operates on the same data)
|
|
149
|
-
{
|
|
150
|
-
filter: {
|
|
151
|
-
field: 'date',
|
|
152
|
-
gte: { anchor: 'max', offset: -1, unit: 'year' },
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
]);
|
|
156
|
-
expect(result.length).toBeGreaterThanOrEqual(12);
|
|
157
|
-
expect(result.length).toBeLessThanOrEqual(13);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('range predicate with two relative refs', () => {
|
|
161
|
-
const data = monthlyData('2020-01-01', 60);
|
|
162
|
-
const maxDate = new Date(data[data.length - 1].date as string);
|
|
163
|
-
const loBound = new Date(maxDate);
|
|
164
|
-
loBound.setFullYear(loBound.getFullYear() - 2);
|
|
165
|
-
const hiBound = new Date(maxDate);
|
|
166
|
-
hiBound.setFullYear(hiBound.getFullYear() - 1);
|
|
167
|
-
|
|
168
|
-
const result = runFilter(data, {
|
|
169
|
-
field: 'date',
|
|
170
|
-
range: [
|
|
171
|
-
{ anchor: 'max', offset: -2, unit: 'year' },
|
|
172
|
-
{ anchor: 'max', offset: -1, unit: 'year' },
|
|
173
|
-
],
|
|
174
|
-
});
|
|
175
|
-
expect(result.length).toBeGreaterThanOrEqual(12);
|
|
176
|
-
expect(result.length).toBeLessThanOrEqual(13);
|
|
177
|
-
for (const row of result) {
|
|
178
|
-
const ts = new Date(row.date as string).getTime();
|
|
179
|
-
expect(ts).toBeGreaterThanOrEqual(loBound.getTime());
|
|
180
|
-
expect(ts).toBeLessThanOrEqual(hiBound.getTime());
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('logical AND with relative ref and static filter: both applied', () => {
|
|
185
|
-
const data = monthlyData('2020-01-01', 60).map((r, i) => ({
|
|
186
|
-
...r,
|
|
187
|
-
category: i % 2 === 0 ? 'A' : 'B',
|
|
188
|
-
}));
|
|
189
|
-
const result = runFilter(data, {
|
|
190
|
-
and: [
|
|
191
|
-
{ field: 'date', gte: { anchor: 'max', offset: -1, unit: 'year' } },
|
|
192
|
-
{ field: 'category', equal: 'A' },
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
// ~12-13 rows in last year, roughly half are category A
|
|
196
|
-
expect(result.length).toBeGreaterThanOrEqual(5);
|
|
197
|
-
expect(result.length).toBeLessThanOrEqual(7);
|
|
198
|
-
for (const row of result) {
|
|
199
|
-
expect(row.category).toBe('A');
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runFilter } from '../filter';
|
|
3
|
-
|
|
4
|
-
describe('runFilter', () => {
|
|
5
|
-
const data = [
|
|
6
|
-
{ name: 'Alice', age: 25, city: 'NYC' },
|
|
7
|
-
{ name: 'Bob', age: 30, city: 'LA' },
|
|
8
|
-
{ name: 'Carol', age: 35, city: 'NYC' },
|
|
9
|
-
{ name: 'Dave', age: 40, city: 'LA' },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
it('filters by field equality', () => {
|
|
13
|
-
const result = runFilter(data, { field: 'city', equal: 'NYC' });
|
|
14
|
-
expect(result).toHaveLength(2);
|
|
15
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Carol']);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('filters by numeric comparison', () => {
|
|
19
|
-
const result = runFilter(data, { field: 'age', gt: 30 });
|
|
20
|
-
expect(result).toHaveLength(2);
|
|
21
|
-
expect(result.map((r) => r.name)).toEqual(['Carol', 'Dave']);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('filters with logical AND', () => {
|
|
25
|
-
const result = runFilter(data, {
|
|
26
|
-
and: [
|
|
27
|
-
{ field: 'city', equal: 'LA' },
|
|
28
|
-
{ field: 'age', gte: 35 },
|
|
29
|
-
],
|
|
30
|
-
});
|
|
31
|
-
expect(result).toHaveLength(1);
|
|
32
|
-
expect(result[0].name).toBe('Dave');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('filters with logical OR', () => {
|
|
36
|
-
const result = runFilter(data, {
|
|
37
|
-
or: [
|
|
38
|
-
{ field: 'name', equal: 'Alice' },
|
|
39
|
-
{ field: 'name', equal: 'Dave' },
|
|
40
|
-
],
|
|
41
|
-
});
|
|
42
|
-
expect(result).toHaveLength(2);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('returns empty array when no rows match', () => {
|
|
46
|
-
const result = runFilter(data, { field: 'age', gt: 100 });
|
|
47
|
-
expect(result).toHaveLength(0);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('returns all rows when all match', () => {
|
|
51
|
-
const result = runFilter(data, { field: 'age', gt: 0 });
|
|
52
|
-
expect(result).toHaveLength(4);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('handles empty data', () => {
|
|
56
|
-
const result = runFilter([], { field: 'age', gt: 0 });
|
|
57
|
-
expect(result).toHaveLength(0);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runFold } from '../fold';
|
|
3
|
-
|
|
4
|
-
describe('runFold', () => {
|
|
5
|
-
const data = [
|
|
6
|
-
{ country: 'US', gold: 10, silver: 20, bronze: 30 },
|
|
7
|
-
{ country: 'UK', gold: 5, silver: 15, bronze: 25 },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
it('folds two columns with default key/value names', () => {
|
|
11
|
-
const result = runFold(data, {
|
|
12
|
-
fold: ['gold', 'silver'],
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
expect(result).toHaveLength(4); // 2 rows x 2 fold fields
|
|
16
|
-
expect(result[0]).toEqual({ country: 'US', bronze: 30, key: 'gold', value: 10 });
|
|
17
|
-
expect(result[1]).toEqual({ country: 'US', bronze: 30, key: 'silver', value: 20 });
|
|
18
|
-
expect(result[2]).toEqual({ country: 'UK', bronze: 25, key: 'gold', value: 5 });
|
|
19
|
-
expect(result[3]).toEqual({ country: 'UK', bronze: 25, key: 'silver', value: 15 });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('folds three columns', () => {
|
|
23
|
-
const result = runFold(data, {
|
|
24
|
-
fold: ['gold', 'silver', 'bronze'],
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(result).toHaveLength(6); // 2 rows x 3 fold fields
|
|
28
|
-
// First row's fold outputs
|
|
29
|
-
expect(result[0].key).toBe('gold');
|
|
30
|
-
expect(result[0].value).toBe(10);
|
|
31
|
-
expect(result[1].key).toBe('silver');
|
|
32
|
-
expect(result[1].value).toBe(20);
|
|
33
|
-
expect(result[2].key).toBe('bronze');
|
|
34
|
-
expect(result[2].value).toBe(30);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('uses custom as names', () => {
|
|
38
|
-
const result = runFold(data, {
|
|
39
|
-
fold: ['gold', 'silver'],
|
|
40
|
-
as: ['medal', 'count'],
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
expect(result[0].medal).toBe('gold');
|
|
44
|
-
expect(result[0].count).toBe(10);
|
|
45
|
-
expect(result[1].medal).toBe('silver');
|
|
46
|
-
expect(result[1].count).toBe(20);
|
|
47
|
-
// Default key/value shouldn't be present
|
|
48
|
-
expect(result[0].key).toBeUndefined();
|
|
49
|
-
expect(result[0].value).toBeUndefined();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('preserves non-fold fields', () => {
|
|
53
|
-
const result = runFold(data, {
|
|
54
|
-
fold: ['gold'],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// country and bronze are non-fold fields
|
|
58
|
-
expect(result[0].country).toBe('US');
|
|
59
|
-
expect(result[0].bronze).toBe(30);
|
|
60
|
-
// gold should not be a direct field (it's now key/value)
|
|
61
|
-
expect(result[0].gold).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('handles undefined fold field values', () => {
|
|
65
|
-
const sparse = [{ name: 'test', a: 1 }]; // no 'b' field
|
|
66
|
-
const result = runFold(sparse, {
|
|
67
|
-
fold: ['a', 'b'],
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result).toHaveLength(2);
|
|
71
|
-
expect(result[0].value).toBe(1);
|
|
72
|
-
expect(result[1].value).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('handles empty data', () => {
|
|
76
|
-
const result = runFold([], { fold: ['gold', 'silver'] });
|
|
77
|
-
expect(result).toHaveLength(0);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runTransforms } from '../index';
|
|
3
|
-
|
|
4
|
-
describe('runTransforms', () => {
|
|
5
|
-
it('runs transforms in order', () => {
|
|
6
|
-
const data = [
|
|
7
|
-
{ name: 'Alice', value: 10 },
|
|
8
|
-
{ name: 'Bob', value: 20 },
|
|
9
|
-
{ name: 'Carol', value: 30 },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
// First filter, then calculate on the filtered result
|
|
13
|
-
const result = runTransforms(data, [
|
|
14
|
-
{ filter: { field: 'value', gte: 15 } },
|
|
15
|
-
{ calculate: { op: '*', field: 'value', value: 2 }, as: 'doubled' },
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
expect(result).toHaveLength(2);
|
|
19
|
-
expect(result[0].name).toBe('Bob');
|
|
20
|
-
expect(result[0].doubled).toBe(40);
|
|
21
|
-
expect(result[1].name).toBe('Carol');
|
|
22
|
-
expect(result[1].doubled).toBe(60);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('applies filter after calculate', () => {
|
|
26
|
-
const data = [{ value: 3 }, { value: 7 }, { value: 12 }];
|
|
27
|
-
|
|
28
|
-
// Calculate first, then filter on the calculated field
|
|
29
|
-
const result = runTransforms(data, [
|
|
30
|
-
{ calculate: { op: '*', field: 'value', value: 10 }, as: 'scaled' },
|
|
31
|
-
{ filter: { field: 'scaled', gt: 50 } },
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
expect(result).toHaveLength(2);
|
|
35
|
-
expect(result[0].scaled).toBe(70);
|
|
36
|
-
expect(result[1].scaled).toBe(120);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('chains bin and filter', () => {
|
|
40
|
-
const data = [{ value: 5 }, { value: 15 }, { value: 25 }, { value: 35 }];
|
|
41
|
-
|
|
42
|
-
const result = runTransforms(data, [
|
|
43
|
-
{ bin: { step: 10, extent: [0, 40] }, field: 'value', as: 'binned' },
|
|
44
|
-
{ filter: { field: 'binned', gte: 20 } },
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
expect(result).toHaveLength(2);
|
|
48
|
-
expect(result[0].value).toBe(25);
|
|
49
|
-
expect(result[1].value).toBe(35);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('chains timeUnit and filter', () => {
|
|
53
|
-
const data = [
|
|
54
|
-
{ date: new Date(2024, 0, 15) }, // January
|
|
55
|
-
{ date: new Date(2024, 5, 15) }, // June
|
|
56
|
-
{ date: new Date(2024, 11, 15) }, // December
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const result = runTransforms(data, [
|
|
60
|
-
{ timeUnit: 'month' as const, field: 'date', as: 'month' },
|
|
61
|
-
{ filter: { field: 'month', gte: 5 } }, // June onwards (0-indexed)
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
expect(result).toHaveLength(2);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns original data for empty transform array', () => {
|
|
68
|
-
const data = [{ x: 1 }];
|
|
69
|
-
const result = runTransforms(data, []);
|
|
70
|
-
expect(result).toEqual(data);
|
|
71
|
-
expect(result).toBe(data); // Same reference since no transforms applied
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('handles multiple filters in sequence', () => {
|
|
75
|
-
const data = [
|
|
76
|
-
{ x: 1, y: 10 },
|
|
77
|
-
{ x: 5, y: 20 },
|
|
78
|
-
{ x: 8, y: 30 },
|
|
79
|
-
{ x: 12, y: 40 },
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
const result = runTransforms(data, [
|
|
83
|
-
{ filter: { field: 'x', gt: 3 } },
|
|
84
|
-
{ filter: { field: 'y', lt: 35 } },
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
// x>3 keeps: {5,20}, {8,30}, {12,40}
|
|
88
|
-
// y<35 keeps: {5,20}, {8,30}
|
|
89
|
-
expect(result).toHaveLength(2);
|
|
90
|
-
expect(result[0]).toEqual({ x: 5, y: 20 });
|
|
91
|
-
expect(result[1]).toEqual({ x: 8, y: 30 });
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { evaluatePredicate } from '../predicates';
|
|
3
|
-
|
|
4
|
-
describe('evaluatePredicate', () => {
|
|
5
|
-
describe('FieldPredicate: equal', () => {
|
|
6
|
-
it('matches equal string value', () => {
|
|
7
|
-
expect(evaluatePredicate({ name: 'Alice' }, { field: 'name', equal: 'Alice' })).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('rejects non-equal value', () => {
|
|
11
|
-
expect(evaluatePredicate({ name: 'Bob' }, { field: 'name', equal: 'Alice' })).toBe(false);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('matches equal numeric value', () => {
|
|
15
|
-
expect(evaluatePredicate({ age: 30 }, { field: 'age', equal: 30 })).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('matches string value against numeric predicate via loose equality', () => {
|
|
19
|
-
// CSV/JSON parsing often produces string values for numeric fields
|
|
20
|
-
expect(evaluatePredicate({ id: '181' }, { field: 'id', equal: 181 })).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('matches numeric value against string predicate via loose equality', () => {
|
|
24
|
-
expect(evaluatePredicate({ id: 181 }, { field: 'id', equal: '181' })).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('FieldPredicate: lt/lte/gt/gte', () => {
|
|
29
|
-
it('lt: value less than threshold', () => {
|
|
30
|
-
expect(evaluatePredicate({ v: 5 }, { field: 'v', lt: 10 })).toBe(true);
|
|
31
|
-
expect(evaluatePredicate({ v: 10 }, { field: 'v', lt: 10 })).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('lte: value less than or equal', () => {
|
|
35
|
-
expect(evaluatePredicate({ v: 10 }, { field: 'v', lte: 10 })).toBe(true);
|
|
36
|
-
expect(evaluatePredicate({ v: 11 }, { field: 'v', lte: 10 })).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('gt: value greater than threshold', () => {
|
|
40
|
-
expect(evaluatePredicate({ v: 15 }, { field: 'v', gt: 10 })).toBe(true);
|
|
41
|
-
expect(evaluatePredicate({ v: 10 }, { field: 'v', gt: 10 })).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('gte: value greater than or equal', () => {
|
|
45
|
-
expect(evaluatePredicate({ v: 10 }, { field: 'v', gte: 10 })).toBe(true);
|
|
46
|
-
expect(evaluatePredicate({ v: 9 }, { field: 'v', gte: 10 })).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('FieldPredicate: range', () => {
|
|
51
|
-
it('includes values within range', () => {
|
|
52
|
-
expect(evaluatePredicate({ v: 5 }, { field: 'v', range: [1, 10] })).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('includes boundary values', () => {
|
|
56
|
-
expect(evaluatePredicate({ v: 1 }, { field: 'v', range: [1, 10] })).toBe(true);
|
|
57
|
-
expect(evaluatePredicate({ v: 10 }, { field: 'v', range: [1, 10] })).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('excludes values outside range', () => {
|
|
61
|
-
expect(evaluatePredicate({ v: 0 }, { field: 'v', range: [1, 10] })).toBe(false);
|
|
62
|
-
expect(evaluatePredicate({ v: 11 }, { field: 'v', range: [1, 10] })).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('FieldPredicate: oneOf', () => {
|
|
67
|
-
it('matches values in the set', () => {
|
|
68
|
-
expect(evaluatePredicate({ c: 'red' }, { field: 'c', oneOf: ['red', 'blue'] })).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('rejects values not in the set', () => {
|
|
72
|
-
expect(evaluatePredicate({ c: 'green' }, { field: 'c', oneOf: ['red', 'blue'] })).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('matches string value against numeric oneOf via loose equality', () => {
|
|
76
|
-
expect(evaluatePredicate({ id: '181' }, { field: 'id', oneOf: [181, 200] })).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('rejects value not in numeric oneOf set', () => {
|
|
80
|
-
expect(evaluatePredicate({ id: '999' }, { field: 'id', oneOf: [181, 200] })).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('FieldPredicate: valid', () => {
|
|
85
|
-
it('valid=true passes for normal values', () => {
|
|
86
|
-
expect(evaluatePredicate({ v: 42 }, { field: 'v', valid: true })).toBe(true);
|
|
87
|
-
expect(evaluatePredicate({ v: 'hello' }, { field: 'v', valid: true })).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('valid=true rejects null/undefined/NaN', () => {
|
|
91
|
-
expect(evaluatePredicate({ v: null }, { field: 'v', valid: true })).toBe(false);
|
|
92
|
-
expect(evaluatePredicate({ v: undefined }, { field: 'v', valid: true })).toBe(false);
|
|
93
|
-
expect(evaluatePredicate({ v: NaN }, { field: 'v', valid: true })).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('valid=false passes for null/undefined/NaN', () => {
|
|
97
|
-
expect(evaluatePredicate({ v: null }, { field: 'v', valid: false })).toBe(true);
|
|
98
|
-
expect(evaluatePredicate({ v: NaN }, { field: 'v', valid: false })).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('valid=false rejects normal values', () => {
|
|
102
|
-
expect(evaluatePredicate({ v: 42 }, { field: 'v', valid: false })).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('LogicalAnd', () => {
|
|
107
|
-
it('passes when all conditions match', () => {
|
|
108
|
-
const pred = {
|
|
109
|
-
and: [
|
|
110
|
-
{ field: 'v', gt: 5 },
|
|
111
|
-
{ field: 'v', lt: 15 },
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
expect(evaluatePredicate({ v: 10 }, pred)).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('fails when any condition fails', () => {
|
|
118
|
-
const pred = {
|
|
119
|
-
and: [
|
|
120
|
-
{ field: 'v', gt: 5 },
|
|
121
|
-
{ field: 'v', lt: 15 },
|
|
122
|
-
],
|
|
123
|
-
};
|
|
124
|
-
expect(evaluatePredicate({ v: 20 }, pred)).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('LogicalOr', () => {
|
|
129
|
-
it('passes when any condition matches', () => {
|
|
130
|
-
const pred = {
|
|
131
|
-
or: [
|
|
132
|
-
{ field: 'c', equal: 'red' },
|
|
133
|
-
{ field: 'c', equal: 'blue' },
|
|
134
|
-
],
|
|
135
|
-
};
|
|
136
|
-
expect(evaluatePredicate({ c: 'blue' }, pred)).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('fails when no condition matches', () => {
|
|
140
|
-
const pred = {
|
|
141
|
-
or: [
|
|
142
|
-
{ field: 'c', equal: 'red' },
|
|
143
|
-
{ field: 'c', equal: 'blue' },
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
expect(evaluatePredicate({ c: 'green' }, pred)).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('LogicalNot', () => {
|
|
151
|
-
it('inverts a passing condition', () => {
|
|
152
|
-
expect(evaluatePredicate({ v: 5 }, { not: { field: 'v', gt: 10 } })).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('inverts a failing condition', () => {
|
|
156
|
-
expect(evaluatePredicate({ v: 15 }, { not: { field: 'v', gt: 10 } })).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('nested logical combinators', () => {
|
|
161
|
-
it('handles and inside or', () => {
|
|
162
|
-
const pred = {
|
|
163
|
-
or: [
|
|
164
|
-
{
|
|
165
|
-
and: [
|
|
166
|
-
{ field: 'x', gt: 0 },
|
|
167
|
-
{ field: 'x', lt: 10 },
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
{ field: 'x', equal: 100 },
|
|
171
|
-
],
|
|
172
|
-
};
|
|
173
|
-
expect(evaluatePredicate({ x: 5 }, pred)).toBe(true);
|
|
174
|
-
expect(evaluatePredicate({ x: 100 }, pred)).toBe(true);
|
|
175
|
-
expect(evaluatePredicate({ x: 50 }, pred)).toBe(false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('handles not inside and', () => {
|
|
179
|
-
const pred = {
|
|
180
|
-
and: [{ field: 'x', gt: 0 }, { not: { field: 'x', equal: 5 } }],
|
|
181
|
-
};
|
|
182
|
-
expect(evaluatePredicate({ x: 3 }, pred)).toBe(true);
|
|
183
|
-
expect(evaluatePredicate({ x: 5 }, pred)).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('edge cases', () => {
|
|
188
|
-
it('missing field returns true for no-op predicate', () => {
|
|
189
|
-
// A field predicate with no comparison operators defaults to true
|
|
190
|
-
expect(evaluatePredicate({ other: 1 }, { field: 'v' })).toBe(true);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
});
|