@rokkit/data 1.0.0-next.100 → 1.0.0-next.105

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/data",
3
- "version": "1.0.0-next.100",
3
+ "version": "1.0.0-next.105",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -11,16 +11,10 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
- "devDependencies": {
15
- "@sveltejs/vite-plugin-svelte": "^3.1.2",
16
- "@testing-library/svelte": "^5.2.1",
17
- "@types/ramda": "^0.30.2",
18
- "@vitest/coverage-v8": "^2.1.1",
19
- "@vitest/ui": "~2.1.1",
20
- "typescript": "^5.6.2",
21
- "vite": "^5.4.6",
22
- "vitest": "~2.1.1",
23
- "shared-config": "1.0.0-next.100"
14
+ "scripts": {
15
+ "prepublishOnly": "tsc --project tsconfig.build.json",
16
+ "clean": "rm -rf dist",
17
+ "build": "bun clean && bun prepublishOnly"
24
18
  },
25
19
  "files": [
26
20
  "src/**/*.js",
@@ -37,20 +31,10 @@
37
31
  },
38
32
  "dependencies": {
39
33
  "@lukeed/uuid": "^2.0.1",
34
+ "@rokkit/core": "1.0.0-next.104",
40
35
  "d3-array": "^3.2.4",
41
36
  "d3-collection": "^1.0.7",
42
37
  "ramda": "^0.30.1",
43
- "svelte": "^4.2.19",
44
- "@rokkit/core": "1.0.0-next.100"
45
- },
46
- "scripts": {
47
- "format": "prettier --write .",
48
- "lint": "eslint --fix .",
49
- "test:ci": "vitest run",
50
- "test:ui": "vitest --ui",
51
- "test": "vitest",
52
- "coverage": "vitest run --coverage",
53
- "latest": "pnpm upgrade --latest && pnpm test:ci",
54
- "release": "pnpm publish --access public"
38
+ "svelte": "^5.10.0"
55
39
  }
56
- }
40
+ }
package/src/dataset.js CHANGED
@@ -3,54 +3,6 @@ import { descending } from 'd3-array'
3
3
  import { pickAllowedConfig, defaultConfig, includeAll } from './constants'
4
4
  import { deriveSortableColumn } from './infer'
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
53
- }
54
6
 
55
7
  /**
56
8
  * Adds a filter to the dataset using the provided condition. This filter is applied
@@ -252,19 +204,6 @@ function nestedJoin(first, second, condition, key = 'children') {
252
204
  return result
253
205
  }
254
206
 
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
207
  /**
269
208
  * Drops the specified keys from the data.
270
209
  *
@@ -342,6 +281,19 @@ function renameKeysUsingMap(lookup) {
342
281
  .reduce((acc, [k, v]) => ({ ...acc, [lookup[k] || k]: v }), {})
343
282
  }
344
283
 
284
+ /**
285
+ * Renames the keys of the data based on the provided mapping, or function
286
+ * that returns the new key. If the new key already exists, the value is overwritten.
287
+ *
288
+ * @param {Array<Object>} data - The data to rename the keys of.
289
+ * @param {Object|Function} how - The mapping of old keys to new keys, or a function that performs renaming.
290
+ * returns Array<Object> - The updated data with renamed keys.
291
+ */
292
+ function renameKeys(data, how) {
293
+ const rename = typeof how === 'function' ? how : renameKeysUsingMap(how)
294
+ return data.map(rename)
295
+ }
296
+
345
297
  /**
346
298
  * Sorts data by the specified columns.
347
299
  *
@@ -378,6 +330,21 @@ function selectKeys(data, config, ...cols) {
378
330
  return result
379
331
  }
380
332
 
333
+ /**
334
+ * Returns the default aggregator which rolls up the columns other than the group_by columns into a single object.
335
+ *
336
+ * @param {import('./types').Metadata} metadata - The metadata for the columns to be aggregated.
337
+ * @param {Object} config - The configuration used to build the aggregator.
338
+ *
339
+ * @returns {Object} An object containing the default aggregator for the specified metadata and configuration.
340
+ */
341
+ export function defaultAggregator(config) {
342
+ return {
343
+ mapper: omit(config.group_by),
344
+ reducers: [{ field: config.children, formula: identity }]
345
+ }
346
+ }
347
+
381
348
  /**
382
349
  * Summarizes the dataset by the specified columns.
383
350
  * @param {import('./types').dataset} df - The dataset object to summarize.
@@ -413,16 +380,50 @@ function rollup(data, config) {
413
380
  }
414
381
 
415
382
  /**
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.
383
+ * Dataset is a collection of data with a set of operations that can be performed on it.
384
+ * @param {Array} data - The data to be stored in the dataset.
385
+ * @param {Object} options - The configuration options for the dataset.
386
+ * @returns {Object} - An object with a set of operations that can be performed on the dataset.
422
387
  */
423
- export function defaultAggregator(config) {
424
- return {
425
- mapper: omit(config.group_by),
426
- reducers: [{ field: config.children, formula: identity }]
388
+ // eslint-disable-next-line max-lines-per-function
389
+ export function dataset(data, options = {}) {
390
+ const config = { ...clone(defaultConfig), ...options }
391
+
392
+ const actions = {
393
+ // configuration
394
+ override: (props) => dataset(data, { ...config, ...pickAllowedConfig(props) }),
395
+ where: (condition) => dataset(data, where(config, condition)),
396
+ groupBy: (...fields) => dataset(data, groupBy(config, ...fields)),
397
+ alignBy: (...fields) => dataset(data, alignBy(config, ...fields)),
398
+ using: (template) => dataset(data, usingTemplate(config, template)),
399
+ summarize: (from, fields) => dataset(data, summarize(config, from, fields)),
400
+ // alter keys
401
+ rename: (how) => dataset(renameKeys(data, how)),
402
+ drop: (...fields) => dataset(dropKeys(data, ...fields)),
403
+ // alter rows
404
+ sortBy: (...fields) => dataset(sortDataBy(data, ...fields)),
405
+ delete: () => dataset(deleteRows(data, config.filter || includeAll)),
406
+ update: (value) => dataset(updateRows(data, config.filter || identity, value)),
407
+ fillNA: (value) => dataset(fillNA(data, value)),
408
+ // transform data
409
+ apply: (callback) => dataset(data.map(callback)),
410
+ rollup: () => dataset(rollup(data, config)),
411
+ select: (...cols) => selectKeys(data, config, ...cols),
412
+ // set operations
413
+ union: (other) => dataset(data.concat(other)),
414
+ minus: (other) => dataset(data.filter((d) => !other.find((x) => equals(x, d)))),
415
+ intersect: (other) => dataset(data.filter((d) => other.find((x) => equals(x, d)))),
416
+ // joins
417
+ innerJoin: (other, condition) => dataset(innerJoin(data, other.select(), condition)),
418
+ leftJoin: (other, condition) => dataset(leftJoin(data, other.select(), condition)),
419
+ rightJoin: (other, condition) => dataset(leftJoin(other.select(), data, condition)),
420
+ fullJoin: (other, condition) => dataset(fullJoin(data, other.select(), condition)),
421
+ crossJoin: (other) => dataset(crossJoin(data, other.select())),
422
+ semiJoin: (other, condition) => dataset(semiJoin(data, other.select(), condition)),
423
+ antiJoin: (other, condition) => dataset(antiJoin(data, other.select(), condition)),
424
+ nestedJoin: (other, condition) =>
425
+ dataset(nestedJoin(data, other.select(), condition, config.children))
427
426
  }
427
+
428
+ return actions
428
429
  }
package/src/filter.js CHANGED
@@ -1,5 +1,22 @@
1
1
  import { filterOperations } from './constants'
2
2
 
3
+ /**
4
+ * Checks if a row matches the given value based on the specified operator.
5
+ * Supports both standard and negated operators by checking the operator string
6
+ * for a leading '!' character, which indicates negation.
7
+ *
8
+ * @param {Object} row - The row object in which to search for the value match.
9
+ * @param {any} value - The value to match against the row's properties.
10
+ * @param {string} operator - The operator used to compare the row's properties to the value.
11
+ * When operator starts with '!', it negates the result of the comparison.
12
+ * @returns {boolean} - Returns true if the row matches the condition; otherwise, returns false.
13
+ */
14
+ function findMatchingRow(row, value, operator) {
15
+ const op = operator.startsWith('!') ? operator.slice(1) : operator
16
+ const matched = Object.keys(row).some((key) => filterOperations[op](row[key], value))
17
+
18
+ return operator.startsWith('!') ? !matched : matched
19
+ }
3
20
  /**
4
21
  * Filters an array of objects based on a specified condition defined in the options.
5
22
  * If a column is provided, it filters based on the value of that column. Otherwise, it filters
@@ -22,24 +39,6 @@ export function filterObjectArray(data, options) {
22
39
  return data.filter((row) => findMatchingRow(row, value, operator))
23
40
  }
24
41
 
25
- /**
26
- * Checks if a row matches the given value based on the specified operator.
27
- * Supports both standard and negated operators by checking the operator string
28
- * for a leading '!' character, which indicates negation.
29
- *
30
- * @param {Object} row - The row object in which to search for the value match.
31
- * @param {any} value - The value to match against the row's properties.
32
- * @param {string} operator - The operator used to compare the row's properties to the value.
33
- * When operator starts with '!', it negates the result of the comparison.
34
- * @returns {boolean} - Returns true if the row matches the condition; otherwise, returns false.
35
- */
36
- function findMatchingRow(row, value, operator) {
37
- const op = operator.startsWith('!') ? operator.slice(1) : operator
38
- const matched = Object.keys(row).some((key) => filterOperations[op](row[key], value))
39
-
40
- return operator.startsWith('!') ? !matched : matched
41
- }
42
-
43
42
  /**
44
43
  * Applies multiple filtering conditions to an array of objects. This function iterates through each filter option,
45
44
  * successively narrowing down the data.
package/src/formatter.js CHANGED
@@ -1,20 +1,4 @@
1
1
  import { identity } from 'ramda'
2
-
3
- const formatterMap = {
4
- currency: getCurrencyFormatter,
5
- integer: (language) => (val) => val.toLocaleString(language, { maximumFractionDigits: 0 }),
6
- number: (language, decimalPlaces) => (val) =>
7
- val.toLocaleString(language, {
8
- minimumFractionDigits: decimalPlaces,
9
- maximumFractionDigits: decimalPlaces
10
- }),
11
- date: (language) => (val) => val.toLocaleDateString(language),
12
- time: (language) => (val) => val.toLocaleTimeString(language),
13
- object: () => JSON.stringify,
14
- array: () => JSON.stringify, // Combined formatter for 'object' and 'array'
15
- ellipsis: () => () => '...' // Static formatter, no need for parameters
16
- }
17
-
18
2
  /**
19
3
  * Returns a currency formatter function that formats a value as a currency string.
20
4
  * @param {string} language - The IETF language tag used for locale-specific formatting.
@@ -39,6 +23,21 @@ function getCurrencyFormatter(language, decimalPlaces) {
39
23
  return formatter
40
24
  }
41
25
 
26
+ const formatterMap = {
27
+ currency: getCurrencyFormatter,
28
+ integer: (language) => (val) => val.toLocaleString(language, { maximumFractionDigits: 0 }),
29
+ number: (language, decimalPlaces) => (val) =>
30
+ val.toLocaleString(language, {
31
+ minimumFractionDigits: decimalPlaces,
32
+ maximumFractionDigits: decimalPlaces
33
+ }),
34
+ date: (language) => (val) => val.toLocaleDateString(language),
35
+ time: (language) => (val) => val.toLocaleTimeString(language),
36
+ object: () => JSON.stringify,
37
+ array: () => JSON.stringify, // Combined formatter for 'object' and 'array'
38
+ ellipsis: () => () => '...' // Static formatter, no need for parameters
39
+ }
40
+
42
41
  /**
43
42
  * Creates a formatter function that formats a value according to the specified type and optional locale settings.
44
43
  * Supported types include 'default' (no formatting), 'integer', 'number', 'date', 'time',, 'object', 'array', 'currency' & 'ellipsis''
package/src/infer.js CHANGED
@@ -7,27 +7,30 @@ import { typeOf } from './utils'
7
7
  import { deriveColumnProperties } from './metadata'
8
8
 
9
9
  /**
10
- * Derives the sorting information for a column based on the type of the input value.
10
+ * Validates an array to ensure it can be used to create a sorting configuration.
11
11
  *
12
- * @param {import('./types').SortableColumn} value - The value that determines the sorting behavior of the column.
13
- * @returns {import('./types').ColumnSorter} An object containing the 'name' of the column and a 'sorter' function.
14
- * @throws {Error} Throws an error if the value type is not supported or the value format is invalid.
12
+ * @param {Array} value - The array to validate.
13
+ * @throws {Error} Throws an error if the array does not correspond to a [string, boolean] pair.
15
14
  */
16
- export function deriveSortableColumn(value) {
17
- const type = typeOf(value)
18
-
19
- const configGenerators = {
20
- array: arrayConfigGenerator,
21
- object: objectConfigGenerator,
22
- string: stringConfigGenerator
15
+ function arrayValidator(value) {
16
+ if (value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'boolean') {
17
+ throw new Error('Array should be a pair of [string, boolean]')
23
18
  }
19
+ }
24
20
 
25
- const configGenerator = configGenerators[type]
26
- if (!configGenerator) {
27
- throw new Error(`Invalid value type: ${type}`)
21
+ /**
22
+ * Validates an object to ensure it has the necessary properties for creating a sorting configuration.
23
+ *
24
+ * @param {Object} value - The object to validate.
25
+ * @throws {Error} Throws an error if the object does not have a 'name' property or has an invalid 'sorter'.
26
+ */
27
+ function objectValidator(value) {
28
+ if (typeof value.name !== 'string') {
29
+ throw new Error('The property "name" is required and must be a string')
30
+ }
31
+ if (value.sorter !== undefined && typeof value.sorter !== 'function') {
32
+ throw new Error('Sorter should be a function')
28
33
  }
29
-
30
- return configGenerator(value)
31
34
  }
32
35
 
33
36
  /**
@@ -63,32 +66,28 @@ function stringConfigGenerator(value) {
63
66
  }
64
67
 
65
68
  /**
66
- * Validates an array to ensure it can be used to create a sorting configuration.
69
+ * Derives the sorting information for a column based on the type of the input value.
67
70
  *
68
- * @param {Array} value - The array to validate.
69
- * @throws {Error} Throws an error if the array does not correspond to a [string, boolean] pair.
71
+ * @param {import('./types').SortableColumn} value - The value that determines the sorting behavior of the column.
72
+ * @returns {import('./types').ColumnSorter} An object containing the 'name' of the column and a 'sorter' function.
73
+ * @throws {Error} Throws an error if the value type is not supported or the value format is invalid.
70
74
  */
71
- function arrayValidator(value) {
72
- if (value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'boolean') {
73
- throw new Error('Array should be a pair of [string, boolean]')
74
- }
75
- }
75
+ export function deriveSortableColumn(value) {
76
+ const type = typeOf(value)
76
77
 
77
- /**
78
- * Validates an object to ensure it has the necessary properties for creating a sorting configuration.
79
- *
80
- * @param {Object} value - The object to validate.
81
- * @throws {Error} Throws an error if the object does not have a 'name' property or has an invalid 'sorter'.
82
- */
83
- function objectValidator(value) {
84
- if (typeof value.name !== 'string') {
85
- throw new Error('The property "name" is required and must be a string')
78
+ const configGenerators = {
79
+ array: arrayConfigGenerator,
80
+ object: objectConfigGenerator,
81
+ string: stringConfigGenerator
86
82
  }
87
- if (value.sorter !== undefined && typeof value.sorter !== 'function') {
88
- throw new Error('Sorter should be a function')
83
+
84
+ const configGenerator = configGenerators[type]
85
+ if (!configGenerator) {
86
+ throw new Error(`Invalid value type: ${type}`)
89
87
  }
90
- }
91
88
 
89
+ return configGenerator(value)
90
+ }
92
91
  /**
93
92
  * Creates a deep scan sample object that contains a union of all keys from all
94
93
  * objects in the array, pulling the values from the last item that contains the key.
@@ -96,7 +95,8 @@ function objectValidator(value) {
96
95
  * @param {Array<Object>} data - An array of objects to deep scan and collect sample values.
97
96
  * @returns {Object} A sample object consolidating all unique keys with their latest non-null values.
98
97
  */
99
- export function getDeepScanSample(data) {
98
+ export function getDeepScanSample(data, deepScan = true) {
99
+ if (!deepScan) return [data[0]]
100
100
  return data.reduce(
101
101
  (acc, cur) => ({
102
102
  ...acc,
@@ -178,31 +178,6 @@ export function inferDataType(values) {
178
178
  return type
179
179
  }
180
180
 
181
- /**
182
- * Derives column metadata from the data to be used in a tabular component.
183
- *
184
- * @param {Array} data - The data to derive column metadata from.
185
- * @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
186
- */
187
- export function deriveMetadata(dataArray, options = {}) {
188
- let { columns = [] } = options
189
- if (dataArray.length === 0) return columns
190
- const { actions, language } = { ...defaultViewOptions, ...options }
191
-
192
- if (columns.length === 0) {
193
- columns = deriveColumns(dataArray, options)
194
- } else {
195
- columns.forEach((col) => {
196
- // if (col.sortable && !col.order) col.order = 'none'
197
- if (!col.fields) col.fields = { text: col.name }
198
- })
199
- }
200
- columns = addFormatters(columns, language)
201
- if (actions.length > 0) columns = deriveActions(columns, actions)
202
-
203
- return columns
204
- }
205
-
206
181
  /**
207
182
  * Adds a formatter function to each column in the array based on the column type. If a formatter
208
183
  * function is already present, it will not be overwritten. The formatter is created based on the
@@ -261,3 +236,40 @@ export function deriveActions(columns, input) {
261
236
  }))
262
237
  return [...actionColumns, ...columns]
263
238
  }
239
+
240
+ /**
241
+ * Derives column metadata from the data to be used in a tabular component.
242
+ * @param {Array<import('./types').ColumnMetadata>} columns
243
+ * @param {Array} dataArray - The data to derive column metadata from.
244
+ * @param {Object} [options] - The options to use when deriving column metadata.
245
+ * @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
246
+ */
247
+ function deriveColumnMetadata(columns, dataArray, options) {
248
+ if (columns.length === 0) {
249
+ columns = deriveColumns(dataArray, options)
250
+ } else {
251
+ columns.forEach((col) => {
252
+ // if (col.sortable && !col.order) col.order = 'none'
253
+ if (!col.fields) col.fields = { text: col.name }
254
+ })
255
+ }
256
+ return columns
257
+ }
258
+ /**
259
+ * Derives column metadata from the data to be used in a tabular component.
260
+ *
261
+ * @param {Array} data - The data to derive column metadata from.
262
+ * @param {Object} [options] - The options to use when deriving column metadata.
263
+ * @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
264
+ */
265
+ export function deriveMetadata(dataArray, options = {}) {
266
+ let { columns = [] } = options
267
+ if (dataArray.length === 0) return columns
268
+ const { actions, language } = { ...defaultViewOptions, ...options }
269
+
270
+ columns = deriveColumnMetadata(columns, dataArray, options)
271
+ columns = addFormatters(columns, language)
272
+ if (actions.length > 0) columns = deriveActions(columns, actions)
273
+
274
+ return columns
275
+ }
package/src/model.js CHANGED
@@ -2,6 +2,50 @@ import { identity, clone as copy } from 'ramda'
2
2
  import { getDeepScanSample } 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 ? getDeepScanSample(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
@@ -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/renamer.js CHANGED
@@ -9,39 +9,25 @@ import { omit, identity } from 'ramda'
9
9
  * @property {string} [separator='_'] - The separator to use between the key and the prefix or suffix.
10
10
  * @property {Array} [keys] - The keys to rename.
11
11
  */
12
-
13
12
  /**
14
- * A generator function that produces a function to rename keys.
15
- *
16
- * @param {OptionsToRenameKeys} options - Options to rename keys
17
- * @returns {Object} - A renamer object.
18
- *
19
- * Example:
13
+ * Renames the keys of an object using a lookup table.
20
14
  *
21
- * renamer({ prefix: 'a', keys: ['a', 'b'] }).get()
22
- * renamer({ prefix: 'a', separator: '-' }).get()
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()
15
+ * @param {Object} lookup - The lookup table.
16
+ * @returns {Function} - A function that takes an object and renames the keys using the lookup table.
30
17
  */
31
- export function renamer(options = {}) {
32
- const { prefix, suffix, separator = '_', keys = [] } = options
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
- }
18
+ function usingLookup(lookup) {
19
+ return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [lookup[key]]: row[key] }), {})
43
20
  }
44
21
 
22
+ /**
23
+ * Renames the keys of an object using a function.
24
+ *
25
+ * @param {Function} rename - The function to rename the keys.
26
+ * @returns {Function} - A function that takes an object and renames the keys using the function.
27
+ */
28
+ function forObject(rename) {
29
+ return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
30
+ }
45
31
  /**
46
32
  * Generates a renamer function which adds a prefix or suffix to a string.
47
33
  *
@@ -78,21 +64,33 @@ function getObjectRenamer(rename, keys) {
78
64
  }
79
65
 
80
66
  /**
81
- * Renames the keys of an object using a lookup table.
67
+ * A generator function that produces a function to rename keys.
82
68
  *
83
- * @param {Object} lookup - The lookup table.
84
- * @returns {Function} - A function that takes an object and renames the keys using the lookup table.
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.
69
+ * @param {OptionsToRenameKeys} options - Options to rename keys
70
+ * @returns {Object} - A renamer object.
92
71
  *
93
- * @param {Function} rename - The function to rename the keys.
94
- * @returns {Function} - A function that takes an object and renames the keys using the function.
72
+ * Example:
73
+ *
74
+ * renamer({ prefix: 'a', keys: ['a', 'b'] }).get()
75
+ * renamer({ prefix: 'a', separator: '-' }).get()
76
+ * renamer().setPrefix('a')
77
+ * .setSeparator('-')
78
+ * .setKeys(['a', 'b'])
79
+ * .get()
80
+ * renamer().setPrefix('a').get()
81
+ * renamer().setSuffix('b').get()
82
+ * renamer().setKeys(['a', 'b']).get()
95
83
  */
96
- function forObject(rename) {
97
- return (row) => Object.keys(row).reduce((acc, key) => ({ ...acc, [rename(key)]: row[key] }), {})
84
+ export function renamer(options = {}) {
85
+ const { prefix, suffix, separator = '_', keys = [] } = options
86
+ const rename = getRenamer(prefix, suffix, separator)
87
+ const renameObject = getObjectRenamer(rename, keys)
88
+
89
+ return {
90
+ get: () => ({ rename, renameObject }),
91
+ setPrefix: (value) => renamer({ ...omit(['suffix'], options), prefix: value }),
92
+ setSuffix: (value) => renamer({ ...omit(['prefix'], options), suffix: value }),
93
+ setKeys: (value) => renamer({ ...options, keys: value }),
94
+ setSeparator: (value) => renamer({ ...options, separator: value })
95
+ }
98
96
  }
package/src/rollup.js CHANGED
@@ -1,27 +1,5 @@
1
1
  import { omit, pick, mergeLeft, map, difference, uniq, pipe } from 'ramda'
2
2
 
3
- /**
4
- * Groups data by specified keys.
5
- *
6
- * @param {Array} data - An array of data points.
7
- * @param {Array} groupByKeys - Keys to group the data by.
8
- * @param {Array} summaries - An array of summary objects to define aggregation operations.
9
- * @returns {Object} Grouped data object with aggregation placeholders.
10
- */
11
- export function groupDataByKeys(data, groupByKeys, summaries) {
12
- const keysExtractor = pick(groupByKeys)
13
- const groupedData = data.reduce((grouped, row) => {
14
- const key = JSON.stringify(keysExtractor(row))
15
- if (!grouped[key]) {
16
- grouped[key] = { ...keysExtractor(row), ...initialValues(summaries) }
17
- }
18
- addToSummaries(grouped[key], row, summaries)
19
- return grouped
20
- }, {})
21
-
22
- return Object.values(groupedData)
23
- }
24
-
25
3
  /**
26
4
  * Sets initial values for summaries.
27
5
  *
@@ -49,6 +27,28 @@ export function addToSummaries(group, row, summaries) {
49
27
  })
50
28
  }
51
29
 
30
+ /**
31
+ * Groups data by specified keys.
32
+ *
33
+ * @param {Array} data - An array of data points.
34
+ * @param {Array} groupByKeys - Keys to group the data by.
35
+ * @param {Array} summaries - An array of summary objects to define aggregation operations.
36
+ * @returns {Object} Grouped data object with aggregation placeholders.
37
+ */
38
+ export function groupDataByKeys(data, groupByKeys, summaries) {
39
+ const keysExtractor = pick(groupByKeys)
40
+ const groupedData = data.reduce((grouped, row) => {
41
+ const key = JSON.stringify(keysExtractor(row))
42
+ if (!grouped[key]) {
43
+ grouped[key] = { ...keysExtractor(row), ...initialValues(summaries) }
44
+ }
45
+ addToSummaries(grouped[key], row, summaries)
46
+ return grouped
47
+ }, {})
48
+
49
+ return Object.values(groupedData)
50
+ }
51
+
52
52
  /**
53
53
  * Fills grouped data with aligned data if specified in configuration.
54
54
  *
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2022 Jerry Thomas
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.