bare-script 2.3.2 → 3.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/lib/data.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  /** @module lib/data */
5
5
 
6
+ import {valueBoolean, valueCompare, valueJSON, valueParseDatetime, valueParseNumber} from './value.js';
6
7
  import {evaluateExpression} from './runtime.js';
7
- import {jsonStringifySortKeys} from 'schema-markdown/lib/encode.js';
8
8
  import {parseExpression} from './parser.js';
9
9
  import {parseSchemaMarkdown} from 'schema-markdown/lib/parser.js';
10
10
  import {validateType} from 'schema-markdown/lib/schema.js';
@@ -31,7 +31,7 @@ export function parseCSV(text) {
31
31
  const rows = lines.filter((line) => !line.match(rCSVBlankLine)).map((line) => {
32
32
  const row = [];
33
33
  let linePart = line;
34
- while (linePart !== '') {
34
+ while (linePart !== null) {
35
35
  // Quoted field?
36
36
  const mQuoted = linePart.match(rCSVQuotedField) ?? linePart.match(rCSVQuotedFieldEnd);
37
37
  if (mQuoted !== null) {
@@ -43,7 +43,7 @@ export function parseCSV(text) {
43
43
  // Non-quoted field
44
44
  const ixComma = linePart.indexOf(',');
45
45
  row.push(ixComma !== -1 ? linePart.slice(0, ixComma) : linePart);
46
- linePart = (ixComma !== -1 ? linePart.slice(ixComma + 1) : '');
46
+ linePart = (ixComma !== -1 ? linePart.slice(ixComma + 1) : null);
47
47
  }
48
48
  return row;
49
49
  });
@@ -51,11 +51,11 @@ export function parseCSV(text) {
51
51
  // Assemble the data rows
52
52
  const result = [];
53
53
  if (rows.length >= 2) {
54
- const [fields] = rows;
54
+ const fields = rows[0].map((field) => field.trim());
55
55
  for (let ixLine = 1; ixLine < rows.length; ixLine += 1) {
56
56
  const row = rows[ixLine];
57
57
  result.push(Object.fromEntries(fields.map(
58
- (field, ixField) => [field, ixField < row.length ? row[ixField] : 'null']
58
+ (field, ixField) => [field, ixField < row.length ? row[ixField] : null]
59
59
  )));
60
60
  }
61
61
  }
@@ -83,15 +83,28 @@ export function validateData(data, csv = false) {
83
83
  const types = {};
84
84
  for (const row of data) {
85
85
  for (const [field, value] of Object.entries(row)) {
86
- if (!(field in types)) {
87
- if (typeof value === 'number') {
86
+ if ((types[field] ?? null) === null) {
87
+ if (typeof value === 'boolean') {
88
+ types[field] = 'boolean';
89
+ } if (typeof value === 'number') {
88
90
  types[field] = 'number';
89
91
  } else if (value instanceof Date) {
90
92
  types[field] = 'datetime';
91
- } else if (typeof value === 'string' && (!csv || value !== 'null')) {
92
- if (parseDatetime(value) !== null) {
93
+ } else if (typeof value === 'string') {
94
+ // If we aren't parsing CSV strings, its just a string
95
+ if (!csv) {
96
+ types[field] = 'string';
97
+
98
+ // If its the null string we can't determine the type yet
99
+ } else if (value === '' || value === 'null') {
100
+ types[field] = null;
101
+
102
+ // Can the string be parsed into another type?
103
+ } else if (valueParseDatetime(value) !== null) {
93
104
  types[field] = 'datetime';
94
- } else if (csv && parseNumber(value) !== null) {
105
+ } else if (value === 'true' || value === 'false') {
106
+ types[field] = 'boolean';
107
+ } else if (valueParseNumber(value) !== null) {
95
108
  types[field] = 'number';
96
109
  } else {
97
110
  types[field] = 'string';
@@ -101,13 +114,25 @@ export function validateData(data, csv = false) {
101
114
  }
102
115
  }
103
116
 
104
- // Validate field values
117
+ // Set the type for fields with undetermined type
118
+ for (const [field, fieldType] of Object.entries(types)) {
119
+ if (fieldType === null) {
120
+ types[field] = 'string';
121
+ }
122
+ }
123
+
124
+ // Helper to format and raise validation errors
105
125
  const throwFieldError = (field, fieldType, fieldValue) => {
106
- throw new Error(`Invalid "${field}" field value ${JSON.stringify(fieldValue)}, expected type ${fieldType}`);
126
+ throw new Error(`Invalid "${field}" field value ${valueJSON(fieldValue)}, expected type ${fieldType}`);
107
127
  };
128
+
129
+ // Validate field values
108
130
  for (const row of data) {
109
131
  for (const [field, value] of Object.entries(row)) {
110
- const fieldType = types[field];
132
+ const fieldType = types[field] ?? null;
133
+ if (fieldType === null) {
134
+ continue;
135
+ }
111
136
 
112
137
  // Null string?
113
138
  if (csv && value === 'null') {
@@ -116,9 +141,14 @@ export function validateData(data, csv = false) {
116
141
  // Number field
117
142
  } else if (fieldType === 'number') {
118
143
  if (csv && typeof value === 'string') {
119
- const numberValue = parseNumber(value);
120
- if (numberValue === null) {
121
- throwFieldError(field, fieldType, value);
144
+ let numberValue;
145
+ if (value === '') {
146
+ numberValue = null;
147
+ } else {
148
+ numberValue = valueParseNumber(value);
149
+ if (numberValue === null) {
150
+ throwFieldError(field, fieldType, value);
151
+ }
122
152
  }
123
153
  row[field] = numberValue;
124
154
  } else if (value !== null && typeof value !== 'number') {
@@ -127,16 +157,38 @@ export function validateData(data, csv = false) {
127
157
 
128
158
  // Datetime field
129
159
  } else if (fieldType === 'datetime') {
130
- if (typeof value === 'string') {
131
- const datetimeValue = parseDatetime(value);
132
- if (datetimeValue === null) {
133
- throwFieldError(field, fieldType, value);
160
+ if (csv && typeof value === 'string') {
161
+ let datetimeValue;
162
+ if (value === '') {
163
+ datetimeValue = null;
164
+ } else {
165
+ datetimeValue = valueParseDatetime(value);
166
+ if (datetimeValue === null) {
167
+ throwFieldError(field, fieldType, value);
168
+ }
134
169
  }
135
170
  row[field] = datetimeValue;
136
171
  } else if (value !== null && !(value instanceof Date)) {
137
172
  throwFieldError(field, fieldType, value);
138
173
  }
139
174
 
175
+ // Boolean field
176
+ } else if (fieldType === 'boolean') {
177
+ if (csv && typeof value === 'string') {
178
+ let booleanValue;
179
+ if (value === '') {
180
+ booleanValue = null;
181
+ } else {
182
+ booleanValue = (value === 'true' ? true : (value === 'false' ? false : null));
183
+ if (booleanValue === null) {
184
+ throwFieldError(field, fieldType, value);
185
+ }
186
+ }
187
+ row[field] = booleanValue;
188
+ } else if (value !== null && typeof value !== 'boolean') {
189
+ throwFieldError(field, fieldType, value);
190
+ }
191
+
140
192
  // String field
141
193
  } else {
142
194
  if (value !== null && typeof value !== 'string') {
@@ -150,47 +202,23 @@ export function validateData(data, csv = false) {
150
202
  }
151
203
 
152
204
 
153
- function parseNumber(text) {
154
- const value = Number.parseFloat(text);
155
- if (Number.isNaN(value) || !Number.isFinite(value)) {
156
- return null;
157
- }
158
- return value;
159
- }
160
-
161
-
162
- export function parseDatetime(text) {
163
- const mDate = text.match(rDate);
164
- if (mDate !== null) {
165
- const year = Number.parseInt(mDate.groups.year, 10);
166
- const month = Number.parseInt(mDate.groups.month, 10);
167
- const day = Number.parseInt(mDate.groups.day, 10);
168
- return new Date(year, month - 1, day);
169
- } else if (rDatetime.test(text)) {
170
- return new Date(text);
171
- }
172
- return null;
173
- }
174
-
175
- const rDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
176
- const rDatetime = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/;
177
-
178
-
179
205
  /**
180
206
  * Join two data arrays
181
207
  *
182
208
  * @param {Object} leftData - The left data array
183
209
  * @param {Object} rightData - The left data array
184
- * @param {string} joinExpr - The join [expression]{@link https://craigahobbs.github.io/bare-script/language/#expressions}
185
- * @param {?string} [rightExpr = null] - The right join [expression]{@link https://craigahobbs.github.io/bare-script/language/#expressions}
210
+ * @param {string} joinExpr - The join [expression](./language/#expressions)
211
+ * @param {?string} [rightExpr = null] - The right join [expression](./language/#expressions)
186
212
  * @param {boolean} [isLeftJoin = false] - If true, perform a left join (always include left row)
187
213
  * @param {?Object} [variables = null] - Additional variables for expression evaluation
188
- * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
214
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/options~ExecuteScriptOptions}
189
215
  * @returns {Object[]} The joined data array
190
216
  */
191
217
  export function joinData(leftData, rightData, joinExpr, rightExpr = null, isLeftJoin = false, variables = null, options = null) {
192
218
  // Compute the map of row field name to joined row field name
193
219
  const leftNames = {};
220
+ const rightNamesRaw = {};
221
+ const rightNames = {};
194
222
  for (const row of leftData) {
195
223
  for (const fieldName of Object.keys(row)) {
196
224
  if (!(fieldName in leftNames)) {
@@ -198,24 +226,26 @@ export function joinData(leftData, rightData, joinExpr, rightExpr = null, isLeft
198
226
  }
199
227
  }
200
228
  }
201
- const rightNames = {};
202
229
  for (const row of rightData) {
203
230
  for (const fieldName of Object.keys(row)) {
204
231
  if (!(fieldName in rightNames)) {
205
- if (!(fieldName in leftNames)) {
206
- rightNames[fieldName] = fieldName;
207
- } else {
208
- let uniqueName = fieldName;
209
- let ixUnique = 2;
210
- do {
211
- uniqueName = `${fieldName}${ixUnique}`;
212
- ixUnique += 1;
213
- } while (uniqueName in leftNames || uniqueName in rightNames);
214
- rightNames[fieldName] = uniqueName;
215
- }
232
+ rightNamesRaw[fieldName] = fieldName;
216
233
  }
217
234
  }
218
235
  }
236
+ for (const fieldName of Object.keys(rightNamesRaw)) {
237
+ if (!(fieldName in leftNames)) {
238
+ rightNames[fieldName] = fieldName;
239
+ } else {
240
+ let ixUnique = 2;
241
+ let uniqueName = `${fieldName}${ixUnique}`;
242
+ while (uniqueName in leftNames || uniqueName in rightNames || uniqueName in rightNamesRaw) {
243
+ ixUnique += 1;
244
+ uniqueName = `${fieldName}${ixUnique}`;
245
+ }
246
+ rightNames[fieldName] = uniqueName;
247
+ }
248
+ }
219
249
 
220
250
  // Create the evaluation options object
221
251
  let evalOptions = options;
@@ -235,7 +265,7 @@ export function joinData(leftData, rightData, joinExpr, rightExpr = null, isLeft
235
265
  // Bucket the right rows by the right expression value
236
266
  const rightCategoryRows = {};
237
267
  for (const rightRow of rightData) {
238
- const categoryKey = jsonStringifySortKeys(evaluateExpression(rightExpression, evalOptions, rightRow));
268
+ const categoryKey = valueJSON(evaluateExpression(rightExpression, evalOptions, rightRow));
239
269
  if (!(categoryKey in rightCategoryRows)) {
240
270
  rightCategoryRows[categoryKey] = [];
241
271
  }
@@ -245,7 +275,7 @@ export function joinData(leftData, rightData, joinExpr, rightExpr = null, isLeft
245
275
  // Join the left with the right
246
276
  const data = [];
247
277
  for (const leftRow of leftData) {
248
- const categoryKey = jsonStringifySortKeys(evaluateExpression(leftExpression, evalOptions, leftRow));
278
+ const categoryKey = valueJSON(evaluateExpression(leftExpression, evalOptions, leftRow));
249
279
  if (categoryKey in rightCategoryRows) {
250
280
  for (const rightRow of rightCategoryRows[categoryKey]) {
251
281
  const joinRow = {...leftRow};
@@ -270,7 +300,7 @@ export function joinData(leftData, rightData, joinExpr, rightExpr = null, isLeft
270
300
  * @param {string} fieldName - The calculated field name
271
301
  * @param {string} expr - The calculated field expression
272
302
  * @param {?Object} [variables = null] - Additional variables for expression evaluation
273
- * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
303
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/options~ExecuteScriptOptions}
274
304
  * @returns {Object[]} The updated data array
275
305
  */
276
306
  export function addCalculatedField(data, fieldName, expr, variables = null, options = null) {
@@ -292,6 +322,7 @@ export function addCalculatedField(data, fieldName, expr, variables = null, opti
292
322
  for (const row of data) {
293
323
  row[fieldName] = evaluateExpression(calcExpr, evalOptions, row);
294
324
  }
325
+
295
326
  return data;
296
327
  }
297
328
 
@@ -300,9 +331,9 @@ export function addCalculatedField(data, fieldName, expr, variables = null, opti
300
331
  * Filter data rows
301
332
  *
302
333
  * @param {Object[]} data - The data array
303
- * @param {string} expr - The boolean filter [expression]{@link https://craigahobbs.github.io/bare-script/language/#expressions}
334
+ * @param {string} expr - The boolean filter [expression](./language/#expressions)
304
335
  * @param {?Object} [variables = null] - Additional variables for expression evaluation
305
- * @param {?Object} [options = null] - The [script execution options]{@link module:lib/runtime~ExecuteScriptOptions}
336
+ * @param {?Object} [options = null] - The [script execution options]{@link module:lib/options~ExecuteScriptOptions}
306
337
  * @returns {Object[]} The filtered data array
307
338
  */
308
339
  export function filterData(data, expr, variables = null, options = null) {
@@ -324,7 +355,7 @@ export function filterData(data, expr, variables = null, options = null) {
324
355
 
325
356
  // Filter the data
326
357
  for (const row of data) {
327
- if (evaluateExpression(filterExpr, evalOptions, row)) {
358
+ if (valueBoolean(evaluateExpression(filterExpr, evalOptions, row))) {
328
359
  result.push(row);
329
360
  }
330
361
  }
@@ -333,80 +364,16 @@ export function filterData(data, expr, variables = null, options = null) {
333
364
  }
334
365
 
335
366
 
336
- // The aggregation model
337
- export const aggregationTypes = parseSchemaMarkdown(`\
338
- group "Aggregation"
339
-
340
-
341
- # A data aggregation specification
342
- struct Aggregation
343
-
344
- # The aggregation category fields
345
- optional string[len > 0] categories
346
-
347
- # The aggregation measures
348
- AggregationMeasure[len > 0] measures
349
-
350
-
351
- # An aggregation measure specification
352
- struct AggregationMeasure
353
-
354
- # The aggregation measure field
355
- string field
356
-
357
- # The aggregation function
358
- AggregationFunction function
359
-
360
- # The aggregated-measure field name
361
- optional string name
362
-
363
-
364
- # An aggregation function
365
- enum AggregationFunction
366
-
367
- # The average of the measure's values
368
- average
369
-
370
- # The count of the measure's values
371
- count
372
-
373
- # The greatest of the measure's values
374
- max
375
-
376
- # The least of the measure's values
377
- min
378
-
379
- # The standard deviation of the measure's values
380
- stddev
381
-
382
- # The sum of the measure's values
383
- sum
384
- `);
385
-
386
-
387
- /**
388
- * Validate an aggregation model
389
- *
390
- * @param {Object} aggregation - The
391
- * [aggregation model]{@link https://craigahobbs.github.io/bare-script/library/model.html#var.vName='Aggregation'}
392
- * @returns {Object} The validated
393
- * [aggregation model]{@link https://craigahobbs.github.io/bare-script/library/model.html#var.vName='Aggregation'}
394
- * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
395
- */
396
- export function validateAggregation(aggregation) {
397
- return validateType(aggregationTypes, 'Aggregation', aggregation);
398
- }
399
-
400
-
401
367
  /**
402
368
  * Aggregate data rows
403
369
  *
404
370
  * @param {Object[]} data - The data array
405
- * @param {Object} aggregation - The
406
- * [aggregation model]{@link https://craigahobbs.github.io/bare-script/library/model.html#var.vName='Aggregation'}
371
+ * @param {Object} aggregation - The [aggregation model](./library/model.html#var.vName='Aggregation')
407
372
  * @returns {Object[]} The aggregated data array
408
373
  */
409
374
  export function aggregateData(data, aggregation) {
375
+ // Validate the aggregation model
376
+ validateType(aggregationTypes, 'Aggregation', aggregation);
410
377
  const categories = aggregation.categories ?? null;
411
378
 
412
379
  // Create the aggregate rows
@@ -417,7 +384,7 @@ export function aggregateData(data, aggregation) {
417
384
 
418
385
  // Get or create the aggregate row
419
386
  let aggregateRow;
420
- const rowKey = (categoryValues !== null ? jsonStringifySortKeys(categoryValues) : '');
387
+ const rowKey = (categoryValues !== null ? valueJSON(categoryValues) : '');
421
388
  if (rowKey in categoryRows) {
422
389
  aggregateRow = categoryRows[rowKey];
423
390
  } else {
@@ -474,6 +441,57 @@ export function aggregateData(data, aggregation) {
474
441
  }
475
442
 
476
443
 
444
+ // The aggregation model
445
+ export const aggregationTypes = parseSchemaMarkdown(`\
446
+ group "Aggregation"
447
+
448
+
449
+ # A data aggregation specification
450
+ struct Aggregation
451
+
452
+ # The aggregation category fields
453
+ optional string[len > 0] categories
454
+
455
+ # The aggregation measures
456
+ AggregationMeasure[len > 0] measures
457
+
458
+
459
+ # An aggregation measure specification
460
+ struct AggregationMeasure
461
+
462
+ # The aggregation measure field
463
+ string field
464
+
465
+ # The aggregation function
466
+ AggregationFunction function
467
+
468
+ # The aggregated-measure field name
469
+ optional string name
470
+
471
+
472
+ # An aggregation function
473
+ enum AggregationFunction
474
+
475
+ # The average of the measure's values
476
+ average
477
+
478
+ # The count of the measure's values
479
+ count
480
+
481
+ # The greatest of the measure's values
482
+ max
483
+
484
+ # The least of the measure's values
485
+ min
486
+
487
+ # The standard deviation of the measure's values
488
+ stddev
489
+
490
+ # The sum of the measure's values
491
+ sum
492
+ `);
493
+
494
+
477
495
  /**
478
496
  * Sort data rows
479
497
  *
@@ -489,7 +507,7 @@ export function sortData(data, sorts) {
489
507
  const [field, desc = false] = sort;
490
508
  const value1 = row1[field] ?? null;
491
509
  const value2 = row2[field] ?? null;
492
- const compare = compareValues(value1, value2);
510
+ const compare = valueCompare(value1, value2);
493
511
  return desc ? -compare : compare;
494
512
  }, 0));
495
513
  }
@@ -509,7 +527,7 @@ export function topData(data, count, categoryFields = null) {
509
527
  const categoryOrder = [];
510
528
  for (const row of data) {
511
529
  const categoryKey = categoryFields === null ? ''
512
- : jsonStringifySortKeys(categoryFields.map((field) => (field in row ? row[field] : null)));
530
+ : valueJSON(categoryFields.map((field) => (field in row ? row[field] : null)));
513
531
  if (!(categoryKey in categoryRows)) {
514
532
  categoryRows[categoryKey] = [];
515
533
  categoryOrder.push(categoryKey);
@@ -528,24 +546,3 @@ export function topData(data, count, categoryFields = null) {
528
546
  }
529
547
  return dataTop;
530
548
  }
531
-
532
-
533
- /**
534
- * Compare two data values
535
- *
536
- * @param {*} value1 - The first value
537
- * @param {*} value2 - The second value
538
- * @returns {number} -1 if the first value is less, 1 if the first value is greater, and 0 if they are equal
539
- */
540
- export function compareValues(value1, value2) {
541
- if (value1 === null) {
542
- return value2 === null ? 0 : -1;
543
- } else if (value2 === null) {
544
- return 1;
545
- } else if (value1 instanceof Date) {
546
- const time1 = value1.getTime();
547
- const time2 = value2.getTime();
548
- return time1 < time2 ? -1 : (time1 === time2 ? 0 : 1);
549
- }
550
- return value1 < value2 ? -1 : (value1 === value2 ? 0 : 1);
551
- }