@rokkit/data 1.0.0-next.99 → 1.0.2

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/src/dataset.js CHANGED
@@ -1,116 +1,63 @@
1
- import { equals, identity, pick, omit, clone } from 'ramda'
2
1
  import { descending } from 'd3-array'
3
- import { pickAllowedConfig, defaultConfig, includeAll } from './constants'
4
- import { deriveSortableColumn } from './infer'
2
+ import { pickAllowedConfig, defaultConfig } from './constants'
3
+ import { deriveColumnDefs } from './infer'
4
+ import { executeFused, sortDataByFields } from './pipeline.js'
5
5
  import { groupDataByKeys, fillAlignedData, getAlignGenerator, aggregateData } from './rollup'
6
- /**
7
- * Dataset is a collection of data with a set of operations that can be performed on it.
8
- * @param {Array} data - The data to be stored in the dataset.
9
- * @param {Object} options - The configuration options for the dataset.
10
- * @returns {Object} - An object with a set of operations that can be performed on the dataset.
11
- */
12
- // eslint-disable-next-line max-lines-per-function
13
- export function dataset(data, options = {}) {
14
- const config = { ...clone(defaultConfig), ...options }
15
-
16
- const actions = {
17
- // configuration
18
- override: (props) => dataset(data, { ...config, ...pickAllowedConfig(props) }),
19
- where: (condition) => dataset(data, where(config, condition)),
20
- groupBy: (...fields) => dataset(data, groupBy(config, ...fields)),
21
- alignBy: (...fields) => dataset(data, alignBy(config, ...fields)),
22
- using: (template) => dataset(data, usingTemplate(config, template)),
23
- summarize: (from, fields) => dataset(data, summarize(config, from, fields)),
24
- // alter keys
25
- rename: (how) => dataset(renameKeys(data, how)),
26
- drop: (...fields) => dataset(dropKeys(data, ...fields)),
27
- // alter rows
28
- sortBy: (...fields) => dataset(sortDataBy(data, ...fields)),
29
- delete: () => dataset(deleteRows(data, config.filter || includeAll)),
30
- update: (value) => dataset(updateRows(data, config.filter || identity, value)),
31
- fillNA: (value) => dataset(fillNA(data, value)),
32
- // transform data
33
- apply: (callback) => dataset(data.map(callback)),
34
- rollup: () => dataset(rollup(data, config)),
35
- select: (...cols) => selectKeys(data, config, ...cols),
36
- // set operations
37
- union: (other) => dataset(data.concat(other)),
38
- minus: (other) => dataset(data.filter((d) => !other.find((x) => equals(x, d)))),
39
- intersect: (other) => dataset(data.filter((d) => other.find((x) => equals(x, d)))),
40
- // joins
41
- innerJoin: (other, condition) => dataset(innerJoin(data, other.select(), condition)),
42
- leftJoin: (other, condition) => dataset(leftJoin(data, other.select(), condition)),
43
- rightJoin: (other, condition) => dataset(leftJoin(other.select(), data, condition)),
44
- fullJoin: (other, condition) => dataset(fullJoin(data, other.select(), condition)),
45
- crossJoin: (other) => dataset(crossJoin(data, other.select())),
46
- semiJoin: (other, condition) => dataset(semiJoin(data, other.select(), condition)),
47
- antiJoin: (other, condition) => dataset(antiJoin(data, other.select(), condition)),
48
- nestedJoin: (other, condition) =>
49
- dataset(nestedJoin(data, other.select(), condition, config.children))
50
- }
51
-
52
- return actions
6
+ import {
7
+ leftJoin,
8
+ rightJoin,
9
+ fullJoin,
10
+ innerJoin,
11
+ crossJoin,
12
+ semiJoin,
13
+ antiJoin,
14
+ nestedJoin
15
+ } from './join'
16
+
17
+ // ── Native helpers (replace Ramda) ─────────────────────────────────────────
18
+
19
+ function pickKeys(keys) {
20
+ return (row) => {
21
+ const out = {}
22
+ for (const k of keys) if (k in row) out[k] = row[k]
23
+ return out
24
+ }
53
25
  }
54
26
 
55
- /**
56
- * Adds a filter to the dataset using the provided condition. This filter is applied
57
- * in subsequent operations like select, delete, and update.
58
- *
59
- * @param {Object} config - The configuration object to be updated.
60
- * @param {Function} condition - The condition function to apply.
61
- * @returns {Object} - The updated configuration
62
- */
63
- function where(config, condition) {
64
- config.filter = condition
65
- return config
66
- }
67
- /**
68
- * Groups the dataset by the specified columns.
69
- * @param {Object} config - The configuration object to be updated.
70
- * @param {Object} actions - The actions object for method chaining.
71
- * @param {...string} fields - The columns to group by.
72
- * @returns {Object} - The updated configurations.
73
- */
74
- function groupBy(config, ...fields) {
75
- config.group_by = fields
76
- return config
27
+ function omitKeys(keys) {
28
+ const skip = new Set(keys)
29
+ return (row) => {
30
+ const out = {}
31
+ for (const k of Object.keys(row)) if (!skip.has(k)) out[k] = row[k]
32
+ return out
33
+ }
77
34
  }
78
35
 
79
- /**
80
- * Aligns the columns of the dataset using the provided fields.
81
- *
82
- * @param {Object} config - The configuration object to be updated.
83
- * @param {...string} fields - The fields to align.
84
- * @returns {Object} - The updated configuration.
85
- */
86
- function alignBy(config, ...fields) {
87
- config.align_by = fields
88
- return config
36
+ function deepEqual(a, b) {
37
+ return JSON.stringify(a) === JSON.stringify(b)
89
38
  }
90
- /**
91
- * Sets the template for adding empty rows in the dataset.
92
- * @param {Object} config - The configuration object to be updated.
93
- * @param {Object} template - The template to use for adding empty rows.
94
- * @returns {Object} - The updated configuration.
95
- */
96
- function usingTemplate(config, template) {
97
- config.template = template
98
- return config
39
+
40
+ function cloneConfig(config) {
41
+ return {
42
+ ...config,
43
+ group_by: [...config.group_by],
44
+ align_by: [...config.align_by],
45
+ summaries: [...config.summaries],
46
+ template: { ...config.template }
47
+ }
99
48
  }
100
49
 
50
+ // ───────────────────────────────────────────────────────────────────────────
51
+
101
52
  /**
102
53
  * Adds a summary field to the dataset.
103
- *
104
- * @param {Object} config - The configuration object to be updated.
105
- * @param {string|Array<string>|Function} from - The field or function to fetch data for summary
106
- * @param {string|Object<string:Function>} using - The target field & formula to use for summarizing.
107
- * @returns {Object} - The updated configuration.
108
54
  */
109
55
  function summarize(config, from, using) {
110
- const mapper = typeof from === 'function' ? from : Array.isArray(from) ? pick(from) : pick([from])
56
+ const mapper =
57
+ typeof from === 'function' ? from : Array.isArray(from) ? pickKeys(from) : pickKeys([from])
111
58
  const reducers = []
112
59
 
113
- if (typeof using === 'string') reducers.push({ field: using, formula: identity })
60
+ if (typeof using === 'string') reducers.push({ field: using, formula: (x) => x })
114
61
  if (typeof using === 'object')
115
62
  Object.entries(using).forEach(([field, formula]) => reducers.push({ field, formula }))
116
63
 
@@ -119,181 +66,8 @@ function summarize(config, from, using) {
119
66
  return config
120
67
  }
121
68
 
122
- /**
123
- * Joins two datasets together based on a condition. Result includes all rows from the first
124
- * dataset and matching rows from the second dataset. In case of multiple matches, all
125
- * combinations are returned. When combining the rows, the columns from the first dataset take
126
- * precedence.
127
- *
128
- * inner: only the rows that have a match in both datasets.
129
- * outer: all rows from the first dataset and matching rows from the second dataset.
130
- *
131
- * @param {Array} first - The first dataset to join.
132
- * @param {Array} second - The second dataset to join.
133
- * @param {Function} condition - The condition to join the datasets on.
134
- * @returns {Object} - An object with the inner and outer join result.
135
- */
136
- function joinData(first, second, condition) {
137
- let inner = []
138
- const outer = []
139
-
140
- first.forEach((f) => {
141
- const matches = second.filter((s) => condition(f, s)).map((m) => ({ ...m, ...f }))
142
- inner = inner.concat(matches)
143
- if (matches.length === 0) outer.push(f)
144
- })
145
- return { inner, outer }
146
- }
147
-
148
- /**
149
- * Filters the results of the first dataset based on the condition using the second dataset.
150
- *
151
- * @param {Array} first - The first dataset to filter.
152
- * @param {Array} second - The second dataset to filter by.
153
- * @param {Function} condition - The condition to filter the first dataset by.
154
- * @returns {Array} - The filtered dataset.
155
- */
156
- function antiJoin(first, second, condition) {
157
- return first.filter((f) => !second.find((s) => condition(f, s)))
158
- }
159
-
160
- /**
161
- * Joins two datasets together based on a condition. Result includes all rows from the first
162
- * dataset and matching rows from the second dataset. In case of multiple matches, all
163
- * combinations are returned. When combining the rows, the columns from the first dataset take
164
- * precedence.
165
- *
166
- * @param {Array} first - The first dataset to join.
167
- * @param {Array} second - The second dataset to join.
168
- * @param {Function} condition - The condition to join the datasets on.
169
- * @returns {Array} - The joined dataset.
170
- */
171
- function innerJoin(first, second, condition) {
172
- const { inner } = joinData(first, second, condition)
173
- return inner
174
- }
175
-
176
- /**
177
- * Performs a left join on two datasets based on a condition. Result includes all rows from the first
178
- * dataset and matching rows from the second dataset. In case of multiple matches, all combinations
179
- * are returned. When combining the rows, the columns from the first dataset take precedence.
180
- * If there is no match in the second dataset, only the row from the first dataset is returned.
181
- *
182
- * This can be used to perform a right join by swapping the datasets.
183
- *
184
- * @param {Array} first - The first dataset to join.
185
- * @param {Array} second - The second dataset to join.
186
- * @param {Function} condition - The condition to join the datasets on.
187
- * @returns {Array} - The joined dataset.
188
- */
189
- function leftJoin(first, second, condition) {
190
- const { inner, outer } = joinData(first, second, condition)
191
- return inner.concat(outer)
192
- }
193
-
194
- /**
195
- * Performs a full join on two datasets based on a condition. Result includes all rows from both
196
- * datasets. In case of multiple matches, all combinations are returned. When combining the rows,
197
- * the columns from the first dataset take precedence. If there is no match in the second dataset,
198
- * only the row from the first dataset is returned. If there is no match in the first dataset, only
199
- * the row from the second dataset is returned.
200
- *
201
- * @param {Array} first - The first dataset to join.
202
- * @param {Array} second - The second dataset to join.
203
- * @param {Function} condition - The condition to join the datasets on.
204
- * @returns {Array} - The joined dataset.
205
- */
206
- function fullJoin(first, second, condition) {
207
- const { inner, outer } = joinData(first, second, condition)
208
- const rightOuter = antiJoin(second, first, condition)
209
- return inner.concat(outer).concat(rightOuter)
210
- }
211
-
212
- /**
213
- * Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
214
- *
215
- * @param {Array} first - The first dataset to join.
216
- * @param {Array} second - The second dataset to join.
217
- * @returns {Array} - The joined dataset.
218
- */
219
- function crossJoin(first, second) {
220
- return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
221
- }
222
-
223
- /**
224
- * Performs a semi join on two datasets based on a condition. Result includes all rows from the first
225
- * dataset that have a match in the second dataset.
226
- *
227
- * @param {Array} first - The first dataset to join.
228
- * @param {Array} second - The second dataset to join.
229
- * @param {Function} condition - The condition to join the datasets on.
230
- * @returns {Array} - The joined dataset.
231
- */
232
- function semiJoin(first, second, condition) {
233
- return first.filter((f) => second.find((s) => condition(f, s)))
234
- }
235
-
236
- /**
237
- * Performs a nested join on two datasets based on a condition. Result includes all rows from the first
238
- * dataset that have a match in the second dataset. The result is nested with the matching rows from the
239
- * second dataset.
240
- *
241
- * @param {Array} first - The first dataset to join.
242
- * @param {Array} second - The second dataset to join.
243
- * @param {Function} condition - The condition to join the datasets on.
244
- * @param {String} [key='children'] - The key to nest the matching rows under.
245
- * @returns {Array} - The joined dataset.
246
- */
247
- function nestedJoin(first, second, condition, key = 'children') {
248
- const result = first.map((f) => ({
249
- ...f,
250
- [key]: second.filter((s) => condition(f, s))
251
- }))
252
- return result
253
- }
254
-
255
- /**
256
- * Renames the keys of the data based on the provided mapping, or function
257
- * that returns the new key. If the new key already exists, the value is overwritten.
258
- *
259
- * @param {Array<Object>} data - The data to rename the keys of.
260
- * @param {Object|Function} how - The mapping of old keys to new keys, or a function that performs renaming.
261
- * returns Array<Object> - The updated data with renamed keys.
262
- */
263
- function renameKeys(data, how) {
264
- const rename = typeof how === 'function' ? how : renameKeysUsingMap(how)
265
- return data.map(rename)
266
- }
267
-
268
- /**
269
- * Drops the specified keys from the data.
270
- *
271
- * @param {Array<Object>} data - The data to drop keys from.
272
- * @param {Array<String>} keys - The keys to drop.
273
- * @returns {Array<Object>} - The updated data with the specified keys dropped.
274
- */
275
- function dropKeys(data, ...keys) {
276
- return data.map((row) => omit(keys, row))
277
- }
278
-
279
- /**
280
- * Deletes rows from the data based on the condition
281
- *
282
- * @param {Array<Object>} data - The data to delete rows from.
283
- * @param {Function} condition - The condition to delete rows on.
284
- * @returns {Array<Object>} - The updated data with rows deleted based on the condition.
285
- */
286
- function deleteRows(data, condition) {
287
- return data.filter((row) => !condition(row))
288
- }
289
-
290
69
  /**
291
70
  * Updates rows in the data based on the condition
292
- *
293
- * @param {Array<Object>} data - The data to update rows in.
294
- * @param {Function} filter - The condition to update rows on.
295
- * @param {Object|Function} value - The values to update the rows with.
296
- * @returns {Array<Object>} - The updated data with rows updated based on the condition.
297
71
  */
298
72
  function updateRows(data, filter, value) {
299
73
  if (typeof value !== 'function' && typeof value !== 'object') {
@@ -306,17 +80,8 @@ function updateRows(data, filter, value) {
306
80
 
307
81
  /**
308
82
  * Fills missing values in the data based on the provided value.
309
- *
310
- * @param {Array<Object>} data - The data to fill missing values in.
311
- * @param {Object} value - The value to fill missing values with.
312
- * @returns {Array<Object>} - The updated data with missing values filled.
313
83
  */
314
84
  function fillNA(data, value) {
315
- /**
316
- * Fills missing values in a single row based on the provided value.
317
- *
318
- * @param {Object} row - The row to fill missing values in.
319
- */
320
85
  const fill = (row) => {
321
86
  const filled = {}
322
87
  Object.entries(value).forEach(([k, v]) => {
@@ -331,9 +96,6 @@ function fillNA(data, value) {
331
96
 
332
97
  /**
333
98
  * Returns a function that renames the keys of a single row based on the provided mapping.
334
- *
335
- * @param {Object} lookup - The mapping of old keys to new keys.
336
- * @returns {Function} - The function that renames the keys of a single row.
337
99
  */
338
100
  function renameKeysUsingMap(lookup) {
339
101
  return (row) =>
@@ -343,47 +105,25 @@ function renameKeysUsingMap(lookup) {
343
105
  }
344
106
 
345
107
  /**
346
- * Sorts data by the specified columns.
347
- *
348
- * @param {Array<Object>} data - The data to sort.
349
- * @param {import('./types').SortableColumn} fields - The columns to sort by.
350
- * @returns {Array<Object>} The sorted data
108
+ * Renames the keys of the data based on the provided mapping, or function.
351
109
  */
352
- function sortDataBy(data, ...fields) {
353
- const sorters = fields.map(deriveSortableColumn)
354
-
355
- data.sort((a, b) => {
356
- let result = 0
357
- for (let i = 0; i < sorters.length && result === 0; i++) {
358
- const { name, sorter } = sorters[i]
359
- result = sorter(a[name], b[name])
360
- }
361
- return result
362
- })
363
- return data
110
+ function renameKeys(data, how) {
111
+ const rename = typeof how === 'function' ? how : renameKeysUsingMap(how)
112
+ return data.map(rename)
364
113
  }
365
114
 
366
115
  /**
367
- * Selects the specified columns from the dataset.
368
- *
369
- * @param {Array<Object>} data - The data to select from.
370
- * @param {Object} config - The configuration object.
371
- * @param {Function} config.filter - The filter function to apply to the data.
372
- * @param {Array<String>} cols - The columns to select.
373
- * @returns {Array<Object>} The selected data.
116
+ * Returns the default aggregator which rolls up non-group_by columns into a children array.
374
117
  */
375
- function selectKeys(data, config, ...cols) {
376
- const result = config.filter ? data.filter(config.filter) : data
377
- if (cols.length > 0) return result.map(pick(cols))
378
- return result
118
+ export function defaultAggregator(config) {
119
+ return {
120
+ mapper: omitKeys(config.group_by),
121
+ reducers: [{ field: config.children, formula: (x) => x }]
122
+ }
379
123
  }
380
124
 
381
125
  /**
382
126
  * Summarizes the dataset by the specified columns.
383
- * @param {import('./types').dataset} df - The dataset object to summarize.
384
- * @param {Array} summaries - The columns to summarize.
385
- *
386
- * @returns {import('./types').dataset} The summarized dataset object.
387
127
  */
388
128
  function rollup(data, config) {
389
129
  if (config.group_by.length === 0 && config.summaries.length === 0) {
@@ -392,7 +132,7 @@ function rollup(data, config) {
392
132
  )
393
133
  }
394
134
 
395
- const summaries = clone(config.summaries)
135
+ const summaries = [...config.summaries]
396
136
  const hasAlignBy = config.align_by.length > 0
397
137
  if (summaries.length === 0) summaries.push(defaultAggregator(config))
398
138
 
@@ -402,27 +142,196 @@ function rollup(data, config) {
402
142
  const fillRows = getAlignGenerator(data, config)
403
143
  alignedData = fillAlignedData(alignedData, config, fillRows)
404
144
  alignedData.forEach((group) => {
405
- group[config.children] = sortDataBy(group[config.children], ...config.align_by)
145
+ group[config.children] = sortDataByFields(group[config.children], ...config.align_by)
406
146
  })
407
147
  }
408
148
 
409
- const aggregatedData = aggregateData(alignedData, summaries)
410
- // const newMetadata = buildMetadata(aggregatedData, df.metadata, df.config.group_by, summaries)
411
-
412
- return aggregatedData
149
+ return aggregateData(alignedData, summaries)
413
150
  }
414
151
 
415
- /**
416
- * Returns the default aggregator which rolls up the columns other than the group_by columns into a single object.
417
- *
418
- * @param {import('./types').Metadata} metadata - The metadata for the columns to be aggregated.
419
- * @param {Object} config - The configuration used to build the aggregator.
420
- *
421
- * @returns {Object} An object containing the default aggregator for the specified metadata and configuration.
422
- */
423
- export function defaultAggregator(config) {
424
- return {
425
- mapper: omit(config.group_by),
426
- reducers: [{ field: config.children, formula: identity }]
152
+ export class DataSet {
153
+ #config = cloneConfig(defaultConfig)
154
+ #data = []
155
+ /** @type {Array<{type:string, fn?:Function, fields?:string[]}>} */
156
+ #ops = []
157
+
158
+ constructor(data) {
159
+ this.#data = data
160
+ }
161
+ get data() {
162
+ return this.#data
163
+ }
164
+
165
+ set data(values) {
166
+ this.#data = values
167
+ }
168
+
169
+ // configuration
170
+ set config(props = {}) {
171
+ this.#config = { ...this.#config, ...pickAllowedConfig(props) }
172
+ }
173
+
174
+ override(props) {
175
+ this.#config = { ...this.#config, ...pickAllowedConfig(props) }
176
+ return this
177
+ }
178
+
179
+ // ── Lazy linear ops (fused in select) ──────────────────────────────────
180
+
181
+ where(condition) {
182
+ this.#ops.push({ type: 'filter', fn: condition })
183
+ return this
184
+ }
185
+ apply(callback) {
186
+ this.#ops.push({ type: 'map', fn: callback })
187
+ return this
188
+ }
189
+ sortBy(...fields) {
190
+ this.#ops.push({ type: 'sort', fields })
191
+ return this
192
+ }
193
+
194
+ // ── Eager ops (commit to this.#data immediately) ────────────────────────
195
+
196
+ rename(how) {
197
+ this.#data = renameKeys(this.#data, how)
198
+ return this
199
+ }
200
+ drop(...fields) {
201
+ const omit = omitKeys(fields)
202
+ const newDataSet = new DataSet(this.#data.map(omit))
203
+ newDataSet.config = this.#config
204
+ return newDataSet
205
+ }
206
+ fillNA(value) {
207
+ this.#data = fillNA(this.#data, value)
208
+ return this
209
+ }
210
+
211
+ // ── Mutation ops (flush lazy ops first, then mutate) ───────────────────
212
+
213
+ remove() {
214
+ // Flush pending ops so the filter context is respected
215
+ const pending = this.#ops.splice(0)
216
+ if (pending.length > 0) {
217
+ // The last filter op is the one to use for removal
218
+ const filterOp = [...pending].reverse().find((o) => o.type === 'filter')
219
+ if (filterOp) {
220
+ this.#data = this.#data.filter((row) => !filterOp.fn(row))
221
+ } else {
222
+ this.#data = []
223
+ }
224
+ } else {
225
+ this.#data = []
226
+ }
227
+ return this
228
+ }
229
+ update(value) {
230
+ // Flush pending ops to get the filter
231
+ const pending = this.#ops.splice(0)
232
+ const filterOp = [...pending].reverse().find((o) => o.type === 'filter')
233
+ const filter = filterOp ? filterOp.fn : () => true
234
+ this.#data = updateRows(this.#data, filter, value)
235
+ return this
236
+ }
237
+
238
+ // ── Aggregation ─────────────────────────────────────────────────────────
239
+
240
+ groupBy(...fields) {
241
+ this.#config.group_by = fields
242
+ return this
243
+ }
244
+ alignBy(...fields) {
245
+ this.#config.align_by = fields
246
+ return this
247
+ }
248
+ usingTemplate(template) {
249
+ this.#config.template = template
250
+ return this
251
+ }
252
+ summarize(from, fields) {
253
+ this.config = summarize(this.#config, from, fields)
254
+ return this
255
+ }
256
+ rollup() {
257
+ const data = executeFused(this.#data, this.#ops)
258
+ this.#ops = []
259
+ return new DataSet(rollup(data, this.#config))
260
+ }
261
+
262
+ // ── Terminal ────────────────────────────────────────────────────────────
263
+
264
+ select(...cols) {
265
+ const result = executeFused(this.#data, this.#ops)
266
+ this.#ops = []
267
+ if (cols.length > 0) {
268
+ const pick = pickKeys(cols)
269
+ return result.map(pick)
270
+ }
271
+ return result
272
+ }
273
+
274
+ /**
275
+ * Derive column definitions from the current data.
276
+ *
277
+ * Returns `{ name, type, scale, sortable, filterable, fields, formatter }` for each
278
+ * column, plus any user-supplied properties from `options.enhancements`.
279
+ *
280
+ * @param {Object} [options]
281
+ * @param {Array<Object>} [options.enhancements=[]] - Per-column overrides keyed by name.
282
+ * @param {string} [options.language='en-US'] - Locale for formatters.
283
+ * @param {string} [options.scanMode='fast'] - 'fast' or 'deep'.
284
+ * @returns {Array<Object>}
285
+ */
286
+ columnDefs(options = {}) {
287
+ return deriveColumnDefs(this.#data, options)
288
+ }
289
+
290
+ // ── Set operations ──────────────────────────────────────────────────────
291
+
292
+ union(other) {
293
+ if (Array.isArray(other)) {
294
+ return new DataSet(this.#data.concat(other))
295
+ }
296
+ return new DataSet(this.#data.concat(other.data))
297
+ }
298
+ minus(other) {
299
+ return new DataSet(this.#data.filter((d) => !other.find((x) => deepEqual(x, d))))
300
+ }
301
+ intersect(other) {
302
+ return new DataSet(this.#data.filter((d) => other.find((x) => deepEqual(x, d))))
303
+ }
304
+
305
+ // ── Joins ───────────────────────────────────────────────────────────────
306
+
307
+ innerJoin(other, condition) {
308
+ return new DataSet(innerJoin(this.#data, other.data, condition))
309
+ }
310
+ leftJoin(other, condition) {
311
+ return new DataSet(leftJoin(this.#data, other.data, condition))
427
312
  }
313
+ rightJoin(other, condition) {
314
+ return new DataSet(rightJoin(this.#data, other.data, condition))
315
+ }
316
+ fullJoin(other, condition) {
317
+ return new DataSet(fullJoin(this.#data, other.data, condition))
318
+ }
319
+ crossJoin(other) {
320
+ return new DataSet(crossJoin(this.#data, other.data))
321
+ }
322
+ semiJoin(other, condition) {
323
+ return new DataSet(semiJoin(this.#data, other.data, condition))
324
+ }
325
+ antiJoin(other, condition) {
326
+ return new DataSet(antiJoin(this.#data, other.data, condition))
327
+ }
328
+ nestedJoin(other, condition) {
329
+ return new DataSet(nestedJoin(this.#data, other.data, condition, this.#config.children))
330
+ }
331
+ }
332
+
333
+ export function dataset(data, options = {}) {
334
+ const ds = new DataSet(data)
335
+ ds.config = options
336
+ return ds
428
337
  }