@rokkit/data 1.0.0-next.98 → 1.0.1
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/README.md +147 -35
- package/package.json +20 -29
- package/src/constants.js +9 -4
- package/src/dataset.js +242 -333
- package/src/dataset.svelte.js +181 -0
- package/src/filter.js +17 -18
- package/src/formatter.js +16 -17
- package/src/index.js +12 -0
- package/src/infer.js +179 -65
- package/src/join.js +157 -0
- package/src/metadata.js +3 -3
- package/src/model.js +48 -48
- package/src/parser.js +42 -42
- package/src/pipeline.js +97 -0
- package/src/renamer.js +48 -43
- package/src/rollup.js +71 -74
- package/src/view.js +1 -2
- package/LICENSE +0 -21
package/src/join.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins two datasets together based on a condition. Result includes all rows from the first
|
|
3
|
+
* dataset and matching rows from the second dataset. In case of multiple matches, all
|
|
4
|
+
* combinations are returned. When combining the rows, the columns from the first dataset take
|
|
5
|
+
* precedence.
|
|
6
|
+
*
|
|
7
|
+
* inner: only the rows that have a match in both datasets.
|
|
8
|
+
* outer: all rows from the first dataset and matching rows from the second dataset.
|
|
9
|
+
*
|
|
10
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
11
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
12
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
13
|
+
* @returns {Array<Object>} - An object with the inner and outer join result.
|
|
14
|
+
*/
|
|
15
|
+
function joinData(first, second, condition) {
|
|
16
|
+
const inner = []
|
|
17
|
+
const outer = []
|
|
18
|
+
|
|
19
|
+
for (const f of first) {
|
|
20
|
+
let matched = false
|
|
21
|
+
for (const s of second) {
|
|
22
|
+
if (condition(f, s)) {
|
|
23
|
+
inner.push({ ...s, ...f })
|
|
24
|
+
matched = true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!matched) outer.push(f)
|
|
28
|
+
}
|
|
29
|
+
return { inner, outer }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Filters the results of the first dataset based on the condition using the second dataset.
|
|
34
|
+
*
|
|
35
|
+
* @param {Array<Object>} first - The first dataset to filter.
|
|
36
|
+
* @param {Array<Object>} second - The second dataset to filter by.
|
|
37
|
+
* @param {Function} condition - The condition to filter the first dataset by.
|
|
38
|
+
* @returns {Array<Object>} - The filtered dataset.
|
|
39
|
+
*/
|
|
40
|
+
export function antiJoin(first, second, condition) {
|
|
41
|
+
return first.filter((f) => !second.find((s) => condition(f, s)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Joins two datasets together based on a condition. Result includes all rows from the first
|
|
46
|
+
* dataset and matching rows from the second dataset. In case of multiple matches, all
|
|
47
|
+
* combinations are returned. When combining the rows, the columns from the first dataset take
|
|
48
|
+
* precedence.
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
51
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
52
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
53
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
54
|
+
*/
|
|
55
|
+
export function innerJoin(first, second, condition) {
|
|
56
|
+
const { inner } = joinData(first, second, condition)
|
|
57
|
+
return inner
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Performs a left join on two datasets based on a condition.
|
|
62
|
+
*
|
|
63
|
+
* - Result includes all rows from the first dataset and matching rows from the second dataset.
|
|
64
|
+
* - In case of multiple matches, all combinations are returned.
|
|
65
|
+
* - When combining the rows, the columns from the first dataset take precedence.
|
|
66
|
+
* - If there is no match in the second dataset, only the row from the first dataset is returned.
|
|
67
|
+
*
|
|
68
|
+
* This can be used to perform a right join by swapping the datasets.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
71
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
72
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
73
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
74
|
+
*/
|
|
75
|
+
export function leftJoin(first, second, condition) {
|
|
76
|
+
const { inner, outer } = joinData(first, second, condition)
|
|
77
|
+
return inner.concat(outer)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Performs a right join on two datasets based on a condition.
|
|
82
|
+
*
|
|
83
|
+
* - Result includes all rows from both datasets.
|
|
84
|
+
* - In case of multiple matches, all combinations are returned.
|
|
85
|
+
* - When combining the rows, the columns from the second dataset take precedence.
|
|
86
|
+
* - If there is no match in the first dataset, only the row from the second dataset is returned.
|
|
87
|
+
*
|
|
88
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
89
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
90
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
91
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
92
|
+
*/
|
|
93
|
+
export function rightJoin(first, second, condition) {
|
|
94
|
+
return leftJoin(second, first, condition)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Performs a full join on two datasets based on a condition.
|
|
99
|
+
*
|
|
100
|
+
* - Result includes all rows from both datasets.
|
|
101
|
+
* - In case of multiple matches, all combinations are returned.
|
|
102
|
+
* - When combining the rows, the columns from the second dataset take precedence.
|
|
103
|
+
* - If there is no match in the first dataset, only the row from the second dataset is returned.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
106
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
107
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
108
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
109
|
+
*/
|
|
110
|
+
export function fullJoin(first, second, condition) {
|
|
111
|
+
const { inner, outer } = joinData(first, second, condition)
|
|
112
|
+
const rightOuter = antiJoin(second, first, condition)
|
|
113
|
+
return inner.concat(outer).concat(rightOuter)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
|
|
118
|
+
*
|
|
119
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
120
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
121
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
122
|
+
*/
|
|
123
|
+
export function crossJoin(first, second) {
|
|
124
|
+
return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Performs a semi join on two datasets based on a condition. Result includes all rows from the first
|
|
129
|
+
* dataset that have a match in the second dataset.
|
|
130
|
+
*
|
|
131
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
132
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
133
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
134
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
135
|
+
*/
|
|
136
|
+
export function semiJoin(first, second, condition) {
|
|
137
|
+
return first.filter((f) => second.find((s) => condition(f, s)))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Performs a nested join on two datasets based on a condition. Result includes all rows from the first
|
|
142
|
+
* dataset that have a match in the second dataset. The result is nested with the matching rows from the
|
|
143
|
+
* second dataset.
|
|
144
|
+
*
|
|
145
|
+
* @param {Array<Object>} first - The first dataset to join.
|
|
146
|
+
* @param {Array<Object>} second - The second dataset to join.
|
|
147
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
148
|
+
* @param {String} [key='children'] - The key to nest the matching rows under.
|
|
149
|
+
* @returns {Array<Object>} - The joined dataset.
|
|
150
|
+
*/
|
|
151
|
+
export function nestedJoin(first, second, condition, key = 'children') {
|
|
152
|
+
const result = first.map((f) => ({
|
|
153
|
+
...f,
|
|
154
|
+
[key]: second.filter((s) => condition(f, s))
|
|
155
|
+
}))
|
|
156
|
+
return result
|
|
157
|
+
}
|
package/src/metadata.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultPathOptions } from './constants'
|
|
2
|
-
import {
|
|
2
|
+
import { getSample } from './infer'
|
|
3
3
|
import { typeOf } from './utils'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -88,8 +88,8 @@ export function deriveColumnProperties(sample, options) {
|
|
|
88
88
|
export function deriveColumnMetadata(data, options = {}) {
|
|
89
89
|
const { metadata, deepScan = false } = options
|
|
90
90
|
if (Array.isArray(metadata) && metadata.length > 0) return metadata
|
|
91
|
-
if (data.length === 0) return []
|
|
92
|
-
const sample =
|
|
91
|
+
// if (data.length === 0) return []
|
|
92
|
+
const sample = getSample(data, deepScan)
|
|
93
93
|
return Object.entries(sample).map(([name, value]) => ({ name, type: typeOf(value) }))
|
|
94
94
|
}
|
|
95
95
|
|
package/src/model.js
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
const copy = (val) => structuredClone(val)
|
|
2
|
+
import { getSample } from './infer'
|
|
3
3
|
import { typeOf } from './utils'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Derives a model from the input data
|
|
7
|
+
*
|
|
8
|
+
* @param {Array|Object} data - the data to derive the model from
|
|
9
|
+
* @param {Boolean} sparse - whether the data set contains sparse data
|
|
10
|
+
* @returns {Array} - the derived model
|
|
11
|
+
*/
|
|
12
|
+
function deriveModel(data, sparse = false) {
|
|
13
|
+
const result = []
|
|
14
|
+
let item = data
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(data)) item = sparse ? getSample(data) : data[0]
|
|
17
|
+
|
|
18
|
+
const kv = Object.entries(item)
|
|
19
|
+
kv.forEach(([key, value]) => {
|
|
20
|
+
result.push({
|
|
21
|
+
name: key,
|
|
22
|
+
type: typeOf(value)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merges two models, returning a new model
|
|
30
|
+
*
|
|
31
|
+
* @param {Array} first - the first model
|
|
32
|
+
* @param {Array} second - the second model
|
|
33
|
+
* @param {Boolean} override - whether to override the type of the first model
|
|
34
|
+
* @returns {Array} - the merged model
|
|
35
|
+
*/
|
|
36
|
+
function mergeModels(first, second, override = false) {
|
|
37
|
+
second.forEach(({ name, type }) => {
|
|
38
|
+
const existing = first.find((x) => x.name === name)
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (existing.type !== type) existing.mixedTypes = true
|
|
41
|
+
if (override) existing.type = type
|
|
42
|
+
} else {
|
|
43
|
+
first.push({ name, type })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return first
|
|
47
|
+
}
|
|
48
|
+
|
|
5
49
|
/**
|
|
6
50
|
* A model is a representation of a dataset
|
|
7
51
|
* @returns an object with methods to manipulate the model
|
|
@@ -23,8 +67,8 @@ export function model() {
|
|
|
23
67
|
* Renames the reference names using a renamer
|
|
24
68
|
* @param {Function} rename - the renamer function
|
|
25
69
|
*/
|
|
26
|
-
renameUsing: (rename
|
|
27
|
-
if (rename
|
|
70
|
+
renameUsing: (rename) => {
|
|
71
|
+
if (rename) {
|
|
28
72
|
data = data.map((x) => ({ ...x, name: rename(x.name) }))
|
|
29
73
|
}
|
|
30
74
|
return actions
|
|
@@ -54,47 +98,3 @@ export function model() {
|
|
|
54
98
|
}
|
|
55
99
|
return actions
|
|
56
100
|
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Derives a model from the input data
|
|
60
|
-
*
|
|
61
|
-
* @param {Array|Object} data - the data to derive the model from
|
|
62
|
-
* @param {Boolean} sparse - whether the data set contains sparse data
|
|
63
|
-
* @returns {Array} - the derived model
|
|
64
|
-
*/
|
|
65
|
-
function deriveModel(data, sparse = false) {
|
|
66
|
-
const result = []
|
|
67
|
-
let item = data
|
|
68
|
-
|
|
69
|
-
if (Array.isArray(data)) item = sparse ? getDeepScanSample(data) : data[0]
|
|
70
|
-
|
|
71
|
-
const kv = Object.entries(item)
|
|
72
|
-
kv.forEach(([key, value]) => {
|
|
73
|
-
result.push({
|
|
74
|
-
name: key,
|
|
75
|
-
type: typeOf(value)
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
return result
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Merges two models, returning a new model
|
|
83
|
-
*
|
|
84
|
-
* @param {Array} first - the first model
|
|
85
|
-
* @param {Array} second - the second model
|
|
86
|
-
* @param {Boolean} override - whether to override the type of the first model
|
|
87
|
-
* @returns {Array} - the merged model
|
|
88
|
-
*/
|
|
89
|
-
function mergeModels(first, second, override = false) {
|
|
90
|
-
second.forEach(({ name, type }) => {
|
|
91
|
-
const existing = first.find((x) => x.name === name)
|
|
92
|
-
if (existing) {
|
|
93
|
-
if (existing.type !== type) existing.mixedTypes = true
|
|
94
|
-
if (override) existing.type = type
|
|
95
|
-
} else {
|
|
96
|
-
first.push({ name, type })
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
return first
|
|
100
|
-
}
|
package/src/parser.js
CHANGED
|
@@ -28,6 +28,48 @@ export function getRegex() {
|
|
|
28
28
|
return new RegExp(pattern, 'gm')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Replaces various shorthand operators with their standardized equivalent for filtering.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} operator - The operator to be replaced.
|
|
35
|
+
* @returns {string} - The replaced operator string.
|
|
36
|
+
*/
|
|
37
|
+
function replaceOperators(operator) {
|
|
38
|
+
return operator.replace(':', '~*').replace('=>', '>=').replace('=<', '<=')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Removes double quotes from the start and end of a string.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} str - The input string from which the surrounding quotes should be removed.
|
|
45
|
+
* @returns {string} - The unquoted string.
|
|
46
|
+
*/
|
|
47
|
+
function removeQuotes(str) {
|
|
48
|
+
const quoteMatch = str.match(/^"([^"]+)"$/)
|
|
49
|
+
return quoteMatch ? quoteMatch[1] : str
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Processes the filter value provided, converting it into a format suitable for filtering.
|
|
54
|
+
* If the operator includes a tilde (~), the value is converted to a RegExp.
|
|
55
|
+
*
|
|
56
|
+
* @param {string|number} value - The value to be processed. It could be a string needing quote removal or a number that needs parsing.
|
|
57
|
+
* @param {string} operator - The operator that determines the type of processing to be applied to the value.
|
|
58
|
+
* @returns {string|number|RegExp} - The processed value, which may be a RegExp if the operator involves pattern matching.
|
|
59
|
+
*/
|
|
60
|
+
function processValue(value, operator) {
|
|
61
|
+
// If the value can be parsed as an integer, do so. Otherwise, strip quotes if present.
|
|
62
|
+
|
|
63
|
+
value = !isNaN(parseInt(value)) ? parseInt(value) : removeQuotes(value)
|
|
64
|
+
// If the operator includes a tilde (~), treat the value as a regular expression,
|
|
65
|
+
// case-insensitive if '*' is present, sensitive otherwise.
|
|
66
|
+
if (operator.includes('~')) {
|
|
67
|
+
value = operator.includes('*') ? new RegExp(value, 'i') : new RegExp(value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
/**
|
|
32
74
|
* Parses a search string and extracts filters based on a predefined regular expression pattern.
|
|
33
75
|
* Each filter captures column, operator, and value components. Any remaining text that
|
|
@@ -67,45 +109,3 @@ export function parseFilters(string) {
|
|
|
67
109
|
|
|
68
110
|
return results
|
|
69
111
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Replaces various shorthand operators with their standardized equivalent for filtering.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} operator - The operator to be replaced.
|
|
75
|
-
* @returns {string} - The replaced operator string.
|
|
76
|
-
*/
|
|
77
|
-
function replaceOperators(operator) {
|
|
78
|
-
return operator.replace(':', '~*').replace('=>', '>=').replace('=<', '<=')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Processes the filter value provided, converting it into a format suitable for filtering.
|
|
83
|
-
* If the operator includes a tilde (~), the value is converted to a RegExp.
|
|
84
|
-
*
|
|
85
|
-
* @param {string|number} value - The value to be processed. It could be a string needing quote removal or a number that needs parsing.
|
|
86
|
-
* @param {string} operator - The operator that determines the type of processing to be applied to the value.
|
|
87
|
-
* @returns {string|number|RegExp} - The processed value, which may be a RegExp if the operator involves pattern matching.
|
|
88
|
-
*/
|
|
89
|
-
function processValue(value, operator) {
|
|
90
|
-
// If the value can be parsed as an integer, do so. Otherwise, strip quotes if present.
|
|
91
|
-
|
|
92
|
-
value = !isNaN(parseInt(value)) ? parseInt(value) : removeQuotes(value)
|
|
93
|
-
// If the operator includes a tilde (~), treat the value as a regular expression,
|
|
94
|
-
// case-insensitive if '*' is present, sensitive otherwise.
|
|
95
|
-
if (operator.includes('~')) {
|
|
96
|
-
value = operator.includes('*') ? new RegExp(value, 'i') : new RegExp(value)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return value
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Removes double quotes from the start and end of a string.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} str - The input string from which the surrounding quotes should be removed.
|
|
106
|
-
* @returns {string} - The unquoted string.
|
|
107
|
-
*/
|
|
108
|
-
function removeQuotes(str) {
|
|
109
|
-
const quoteMatch = str.match(/^"([^"]+)"$/)
|
|
110
|
-
return quoteMatch ? quoteMatch[1] : str
|
|
111
|
-
}
|
package/src/pipeline.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pipeline execution logic used by both DataSet and ReactiveDataSet.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a plain `.js` file so it can be imported from both regular modules
|
|
5
|
+
* and `.svelte.js` files without triggering rune compilation errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deriveSortableColumn } from './infer.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sort an array in-place by one or more field specs.
|
|
12
|
+
* @param {any[]} data - Array to sort (mutated in place).
|
|
13
|
+
* @param {...string|Object|Array} fields - Sort field specs (same as DataSet.sortBy).
|
|
14
|
+
* @returns {any[]}
|
|
15
|
+
*/
|
|
16
|
+
export function sortDataByFields(data, ...fields) {
|
|
17
|
+
const sorters = fields.map(deriveSortableColumn)
|
|
18
|
+
data.sort((a, b) => {
|
|
19
|
+
let result = 0
|
|
20
|
+
for (let i = 0; i < sorters.length && result === 0; i++) {
|
|
21
|
+
const { name, sorter } = sorters[i]
|
|
22
|
+
result = sorter(a[name], b[name])
|
|
23
|
+
}
|
|
24
|
+
return result
|
|
25
|
+
})
|
|
26
|
+
return data
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Execute a list of pipeline ops in a single fused pass over `data`.
|
|
31
|
+
*
|
|
32
|
+
* Ops:
|
|
33
|
+
* { type: 'filter', fn } — keep rows where fn(row) is truthy
|
|
34
|
+
* { type: 'map', fn } — transform row via fn(row)
|
|
35
|
+
* { type: 'sort', fields } — sort the result by fields
|
|
36
|
+
*
|
|
37
|
+
* Consecutive filter/map ops are merged into one loop.
|
|
38
|
+
* Sort ops are applied after the linear pass.
|
|
39
|
+
*
|
|
40
|
+
* @param {any[]} data
|
|
41
|
+
* @param {Array<{type:string, fn?:Function, fields?:string[]}>} ops
|
|
42
|
+
* @returns {any[]}
|
|
43
|
+
*/
|
|
44
|
+
function applyOpsToRow(row, linearOps) {
|
|
45
|
+
let cur = row
|
|
46
|
+
for (const op of linearOps) {
|
|
47
|
+
if (op.type === 'filter') {
|
|
48
|
+
if (!op.fn(cur)) return null
|
|
49
|
+
} else {
|
|
50
|
+
cur = op.fn(cur)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return cur
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyLinearOps(data, linearOps) {
|
|
57
|
+
const out = []
|
|
58
|
+
for (const row of data) {
|
|
59
|
+
const result = applyOpsToRow(row, linearOps)
|
|
60
|
+
if (result !== null) out.push(result)
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectLinearOps(ops, i) {
|
|
66
|
+
const linearOps = []
|
|
67
|
+
while (i < ops.length && ops[i].type !== 'sort') linearOps.push(ops[i++])
|
|
68
|
+
return { linearOps, i }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectSortFields(ops, i) {
|
|
72
|
+
const sortFields = []
|
|
73
|
+
while (i < ops.length && ops[i].type === 'sort') sortFields.push(...ops[i++].fields)
|
|
74
|
+
return { sortFields, i }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function executeFused(data, ops) {
|
|
78
|
+
if (ops.length === 0) return data
|
|
79
|
+
|
|
80
|
+
let result = data
|
|
81
|
+
let i = 0
|
|
82
|
+
|
|
83
|
+
while (i < ops.length) {
|
|
84
|
+
const linear = collectLinearOps(ops, i)
|
|
85
|
+
i = linear.i
|
|
86
|
+
if (linear.linearOps.length > 0) result = applyLinearOps(result, linear.linearOps)
|
|
87
|
+
|
|
88
|
+
const sorted = collectSortFields(ops, i)
|
|
89
|
+
i = sorted.i
|
|
90
|
+
if (sorted.sortFields.length > 0) {
|
|
91
|
+
result = [...result]
|
|
92
|
+
sortDataByFields(result, ...sorted.sortFields)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
package/src/renamer.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
const identity = (x) => x
|
|
2
|
+
|
|
3
|
+
function omit(keys, obj) {
|
|
4
|
+
const skip = new Set(keys)
|
|
5
|
+
const out = {}
|
|
6
|
+
for (const k of Object.keys(obj)) if (!skip.has(k)) out[k] = obj[k]
|
|
7
|
+
return out
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
*
|
|
@@ -9,39 +16,25 @@ import { omit, identity } from 'ramda'
|
|
|
9
16
|
* @property {string} [separator='_'] - The separator to use between the key and the prefix or suffix.
|
|
10
17
|
* @property {Array} [keys] - The keys to rename.
|
|
11
18
|
*/
|
|
12
|
-
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param {OptionsToRenameKeys} options - Options to rename keys
|
|
17
|
-
* @returns {Object} - A renamer object.
|
|
18
|
-
*
|
|
19
|
-
* Example:
|
|
20
|
+
* Renames the keys of an object using a lookup table.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* renamer().setPrefix('a')
|
|
24
|
-
* .setSeparator('-')
|
|
25
|
-
* .setKeys(['a', 'b'])
|
|
26
|
-
* .get()
|
|
27
|
-
* renamer().setPrefix('a').get()
|
|
28
|
-
* renamer().setSuffix('b').get()
|
|
29
|
-
* renamer().setKeys(['a', 'b']).get()
|
|
22
|
+
* @param {Object} lookup - The lookup table.
|
|
23
|
+
* @returns {Function} - A function that takes an object and renames the keys using the lookup table.
|
|
30
24
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const rename = getRenamer(prefix, suffix, separator)
|
|
34
|
-
const renameObject = getObjectRenamer(rename, keys)
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
get: () => ({ rename, renameObject }),
|
|
38
|
-
setPrefix: (value) => renamer({ ...omit(['suffix'], options), prefix: value }),
|
|
39
|
-
setSuffix: (value) => renamer({ ...omit(['prefix'], options), suffix: value }),
|
|
40
|
-
setKeys: (value) => renamer({ ...options, keys: value }),
|
|
41
|
-
setSeparator: (value) => renamer({ ...options, separator: value })
|
|
42
|
-
}
|
|
25
|
+
function usingLookup(lookup) {
|
|
26
|
+
return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [lookup[key]]: row[key] }), {})
|
|
43
27
|
}
|
|
44
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Renames the keys of an object using a function.
|
|
31
|
+
*
|
|
32
|
+
* @param {Function} rename - The function to rename the keys.
|
|
33
|
+
* @returns {Function} - A function that takes an object and renames the keys using the function.
|
|
34
|
+
*/
|
|
35
|
+
function forObject(rename) {
|
|
36
|
+
return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
|
|
37
|
+
}
|
|
45
38
|
/**
|
|
46
39
|
* Generates a renamer function which adds a prefix or suffix to a string.
|
|
47
40
|
*
|
|
@@ -78,21 +71,33 @@ function getObjectRenamer(rename, keys) {
|
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
/**
|
|
81
|
-
*
|
|
74
|
+
* A generator function that produces a function to rename keys.
|
|
82
75
|
*
|
|
83
|
-
* @param {
|
|
84
|
-
* @returns {
|
|
85
|
-
*/
|
|
86
|
-
function usingLookup(lookup) {
|
|
87
|
-
return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [lookup[key]]: row[key] }), {})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Renames the keys of an object using a function.
|
|
76
|
+
* @param {OptionsToRenameKeys} options - Options to rename keys
|
|
77
|
+
* @returns {Object} - A renamer object.
|
|
92
78
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
79
|
+
* Example:
|
|
80
|
+
*
|
|
81
|
+
* renamer({ prefix: 'a', keys: ['a', 'b'] }).get()
|
|
82
|
+
* renamer({ prefix: 'a', separator: '-' }).get()
|
|
83
|
+
* renamer().setPrefix('a')
|
|
84
|
+
* .setSeparator('-')
|
|
85
|
+
* .setKeys(['a', 'b'])
|
|
86
|
+
* .get()
|
|
87
|
+
* renamer().setPrefix('a').get()
|
|
88
|
+
* renamer().setSuffix('b').get()
|
|
89
|
+
* renamer().setKeys(['a', 'b']).get()
|
|
95
90
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
91
|
+
export function renamer(options = {}) {
|
|
92
|
+
const { prefix, suffix, separator = '_', keys = [] } = options
|
|
93
|
+
const rename = getRenamer(prefix, suffix, separator)
|
|
94
|
+
const renameObject = getObjectRenamer(rename, keys)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
get: () => ({ rename, renameObject }),
|
|
98
|
+
setPrefix: (value) => renamer({ ...omit(['suffix'], options), prefix: value }),
|
|
99
|
+
setSuffix: (value) => renamer({ ...omit(['prefix'], options), suffix: value }),
|
|
100
|
+
setKeys: (value) => renamer({ ...options, keys: value }),
|
|
101
|
+
setSeparator: (value) => renamer({ ...options, separator: value })
|
|
102
|
+
}
|
|
98
103
|
}
|