jupyter-ijavascript-utils 1.46.0 → 1.48.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/DOCS.md CHANGED
@@ -74,7 +74,11 @@ Give it a try here:
74
74
  [![Binder:what can I do with this](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/paulroth3d/jupyter-ijavascript-utils/main?labpath=example.ipynb)
75
75
 
76
76
  ## What's New
77
-
77
+ * 1.48 - Correct table rendering html if filter was used ({@link https://github.com/paulroth3d/jupyter-ijavascript-utils/issues/64|#64})
78
+ * 1.47
79
+ * allow conversion from a Collection of Objects to Arrays and back with object. ({@link module:object.objectCollectionFromArray|object.objectCollectionFromArray}, {@link module:object.objectCollectionToArray|object.objectCollectionToArray})
80
+ * Easier iterating over values and peeking with array with {@link module:array~PeekableArrayIterator|PeekableArrayIterator}
81
+ * Support for delayed and asynchronous chaining of functions ({@link module:array.delayedFn|delayedFn}, {@link module:array.chainFunctions|chainFunctions}, etc)
78
82
  * 1.46 - Make it easier to extract data from "hard-spaced arrays" - {@link module:array.multiLineSubstr}, {@link module:array.multiStepReduce}
79
83
  * 1.45 - more ways to understand the data - {@link module:aggregate.coalesce|aggregate.coalesce()}, convert properties to arrow/dot notation / reverse it {@link module:object.flatten|object.flatten()} / {@link module:object.expand|object.expand()} and {@link module:object.isObject|object.isObject()}
80
84
  * 1.43 - esm module fix since still not supported yet in ijavascript
package/Dockerfile CHANGED
@@ -1,3 +1,3 @@
1
1
  # syntax=docker/dockerfile:1
2
2
 
3
- FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.46.0
3
+ FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.48.0
package/README.md CHANGED
@@ -54,7 +54,12 @@ This is not intended to be the only way to accomplish many of these tasks, and a
54
54
  ![Screenshot of example notebook](docResources/img/mainExampleNotebook.png)
55
55
 
56
56
  # What's New
57
- * 1.46 - Make it easier to extract data from "hard-spaced arrays" - {@link module:array.multiLineSubstr}, {@link module:array.multiStepReduce}
57
+ * 1.48 - Correct table rendering html if filter was used (#46)
58
+ * 1.47 -
59
+ * allow conversion from a Collection of Objects to Arrays and back with object. (objectCollectionFromArray, objectCollectionToArray)
60
+ * Easier iterating over values and peeking with array with PeekableArrayIterator
61
+ * Support for delayed and asynchronous chaining of functions (array.delayedFn, chainFunction, etc)
62
+ * 1.46 - Make it easier to extract data from "hard-spaced arrays" - (array.multiLineSubstr, array.multiStepReduce)
58
63
  * 1.45 - more ways to understand the data - aggregate.coalesce(), convert properties to arrow/dot notation / reverse it : object.flatten() / object.expand() and Object.isObject()
59
64
  * 1.43 - esm module fix since still not supported yet in ijavascript
60
65
  * 1.41 - object.propertyInherit - to simplify inheriting values from one record to the next
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "Utilities for working with iJavaScript - a Jupyter Kernel",
5
5
  "homepage": "https://jupyter-ijavascript-utils.onrender.com/",
6
6
  "license": "MIT",
@@ -56,7 +56,6 @@
56
56
  "plantuml-encoder": "^1.4.0",
57
57
  "promise-sequential": "^1.1.1",
58
58
  "svgdom": "0.1.16",
59
- "taffydb": "^2.7.3",
60
59
  "uuid": "^8.3.2",
61
60
  "vega": "^5.22.1",
62
61
  "vega-datasets": "2.3.0",
@@ -337,8 +337,6 @@ class TableGenerator {
337
337
  this.#isTransposed = false;
338
338
  }
339
339
 
340
- //-- GETTER SETTERS
341
-
342
340
  /**
343
341
  * Assigns the data to be used in generating the table.
344
342
  * @param {Array} collection -
@@ -365,6 +363,182 @@ class TableGenerator {
365
363
  return this;
366
364
  }
367
365
 
366
+ /**
367
+ * Assigns the data by importing in a collections of objects.
368
+ *
369
+ * Note: this is the default functionality / syntatic sugar - as data is expected as a collection of objects.
370
+ * @param {Object[]} collection -
371
+ * @returns {TableGenerator}
372
+ * @example
373
+ *
374
+ * dataSet = [{temp: 37, type: 'C'}, {temp: 310, type: 'K'}, {temp: 98, type: 'F'}];
375
+ *
376
+ * //-- simple example where the temp property is converted, and type property overwritten
377
+ * new TableGenerator()
378
+ * .data(dataSet)
379
+ * .generateMarkdown()
380
+ *
381
+ * //-- gives
382
+ * temp | type
383
+ * ---- | ----
384
+ * 37 | C
385
+ * 310 | K
386
+ * 98 | F
387
+ */
388
+ fromObjectCollection(data) {
389
+ this.data(data);
390
+ return this;
391
+ }
392
+
393
+ /**
394
+ * Assigns the data by importing a 2 dimensional array.
395
+ *
396
+ * If headers are not provided, then the first row of the collection is assumed.
397
+ *
398
+ * If there is no header provided (by default) - then the first row is assumed.
399
+ *
400
+ * ```
401
+ * dataSet = [ [ 'temp', 'type' ], [ 37, 'C' ], [ 310, 'K' ], [ 98, 'F' ] ];
402
+ *
403
+ * new TableGenerator()
404
+ * .fromArray(dataSet)
405
+ * .generateMarkdown();
406
+ * ```
407
+ *
408
+ * temp | type
409
+ * ---- | ----
410
+ * 37 | C
411
+ * 310 | K
412
+ * 98 | F
413
+ *
414
+ * However, if there is a header provided, it assumes there is none in teh first row.
415
+ *
416
+ * ```
417
+ * headers = [ 'temp', 'type' ];
418
+ * dataSet = [[ 37, 'C' ], [ 310, 'K' ], [ 98, 'F' ] ];
419
+ *
420
+ * new TableGenerator()
421
+ * .fromArray(dataSet)
422
+ * .generateMarkdown();
423
+ * ```
424
+ *
425
+ * temp | type
426
+ * ---- | ----
427
+ * 37 | C
428
+ * 310 | K
429
+ * 98 | F
430
+ *
431
+ * @param {Array<Array>} collection -
432
+ * @returns {TableGenerator}
433
+ * @see {TableGenerator.data}
434
+ * @see {TableGenerator.fromList}
435
+ */
436
+ fromArray(arrayCollection, headers = null) {
437
+ if (!arrayCollection) {
438
+ this.data(null);
439
+ return this;
440
+ }
441
+ this.data(ObjectUtils.objectCollectionFromArray(arrayCollection, headers));
442
+
443
+ return this;
444
+ }
445
+
446
+ /**
447
+ * Assigns the data from a single 1 dimensional array.
448
+ *
449
+ * Is syntatic sugar to simply wrap the 1 dimensional array into a 2 dimensional array.
450
+ *
451
+ * ```
452
+ * let precip = [
453
+ * 1, 0, 2, 3, 4
454
+ * ];
455
+ *
456
+ * utils.table().fromList(precip).render()
457
+ * ```
458
+ *
459
+ * _
460
+ * --
461
+ * 1
462
+ * 0
463
+ * 2
464
+ * 3
465
+ * 4
466
+ *
467
+ * @param {Array} array1d
468
+ * @returns {TableGenerator}
469
+ * @see {TableGenerator.fromArray}
470
+ */
471
+ fromList(array1d) {
472
+ if (!array1d) {
473
+ this.data(null);
474
+ return this;
475
+ }
476
+ this.data(array1d.map((v) => ({ _: v })));
477
+ return this;
478
+ }
479
+
480
+ /**
481
+ * Initializes the data in the tableGenerator with an object holding
482
+ * 1d tensor properties.
483
+ *
484
+ * ```
485
+ * dfObject = {
486
+ * id: [
487
+ * 1, 0, 2, 3, 4,
488
+ * 5, 6, 8, 7
489
+ * ],
490
+ * city: [
491
+ * 'Seattle', 'Seattle',
492
+ * 'Seattle', 'New York',
493
+ * 'New York', 'New York',
494
+ * 'Chicago', 'Chicago',
495
+ * 'Chicago'
496
+ * ],
497
+ * month: [
498
+ * 'Aug', 'Apr',
499
+ * 'Dec', 'Apr',
500
+ * 'Aug', 'Dec',
501
+ * 'Apr', 'Dec',
502
+ * 'Aug'
503
+ * ],
504
+ * precip: [
505
+ * 0.87, 2.68, 5.31,
506
+ * 3.94, 4.13, 3.58,
507
+ * 3.62, 2.56, 3.98
508
+ * ]
509
+ * }
510
+ *
511
+ * utils.table().fromDataFrameObject(dfObject).render()
512
+ * ```
513
+ *
514
+ * id|city |month|precip
515
+ * --|-- |-- |--
516
+ * 1 |Seattle |Aug |0.87
517
+ * 0 |Seattle |Apr |2.68
518
+ * 2 |Seattle |Dec |5.31
519
+ * 3 |New York|Apr |3.94
520
+ * 4 |New York|Aug |4.13
521
+ * 5 |New York|Dec |3.58
522
+ * 6 |Chicago |Apr |3.62
523
+ * 8 |Chicago |Dec |2.56
524
+ * 7 |Chicago |Aug |3.98
525
+ *
526
+ * @param {Object} dataFrameObject - DataFrame with 1d tensor properties
527
+ * @returns {TableGenerator}
528
+ * @see https://danfo.jsdata.org/api-reference/dataframe/creating-a-dataframe#creating-a-dataframe-from-an-object
529
+ * @see {TableGenerator.fromList}
530
+ * @see {TableGenerator.fromObjectCollection}
531
+ * @see {TableGenerator.data}
532
+ */
533
+ fromDataFrameObject(dataFrameObject) {
534
+ if (!dataFrameObject) {
535
+ this.data(null);
536
+ return this;
537
+ }
538
+ this.data(ObjectUtils.objectCollectionFromDataFrameObject(dataFrameObject));
539
+ return this;
540
+ }
541
+
368
542
  /**
369
543
  * Augments data with additional fields
370
544
  *
@@ -1276,7 +1450,14 @@ class TableGenerator {
1276
1450
 
1277
1451
  const printBody = (collection) => collection
1278
1452
  .map((dataRow, rowIndex) => {
1279
- const record = this.#data[rowIndex];
1453
+ let record;
1454
+ if (this.#filterFn) {
1455
+ record = results.headers.reduce((result, header, headerIndex) => ObjectUtils.assign(result, header, dataRow[headerIndex]), {});
1456
+ } else if (this.#offset) {
1457
+ record = this.#data[rowIndex + this.#offset];
1458
+ } else {
1459
+ record = this.#data[rowIndex];
1460
+ }
1280
1461
  const rowStyle = !styleRowFn ? null : styleRowFn({ rowIndex, row: dataRow, record }) || '';
1281
1462
 
1282
1463
  return `<tr ${printInlineCSS(rowStyle)}>\n\t`
@@ -1507,7 +1688,26 @@ class TableGenerator {
1507
1688
  */
1508
1689
  generateArray2() {
1509
1690
  const results = this.prepare();
1510
- return [[...results.headers], ...results.data];
1691
+ return [results.headers, ...results.data];
1692
+ }
1693
+
1694
+ generateObjectCollection() {
1695
+ return ObjectUtils.objectCollectionFromArray(this.generateArray2());
1696
+ }
1697
+
1698
+ generateDataFrameObject() {
1699
+ const prepResults = this.prepare();
1700
+ const results = {};
1701
+ const createFrameList = () => new Array(prepResults.data.length).fill(undefined);
1702
+ prepResults.headers.forEach((header) => ObjectUtils.assign(results, header, createFrameList()));
1703
+
1704
+ prepResults.data.forEach((row, rowIndex) => {
1705
+ row.forEach((value, valIndex) => {
1706
+ results[prepResults.headers[valIndex]][rowIndex] = value;
1707
+ });
1708
+ });
1709
+
1710
+ return results;
1511
1711
  }
1512
1712
 
1513
1713
  static hasRenderedCSS = false;
package/src/array.js CHANGED
@@ -41,6 +41,12 @@ require('./_types/global');
41
41
  * * {@link module:array.applyArrayValues|array.applyArrayValues} - applies a value / multiple values deeply into an array safely
42
42
  * * Understanding Values
43
43
  * * {@link module:array.isMultiDimensional|array.isMultiDimensional} - determines if an array is multi-dimensional
44
+ * * Custom Iterators
45
+ * * {@link module:array~PeekableArrayIterator|PeekableArrayIterator} - Iterator that lets you peek ahead while not moving the iterator.
46
+ * * Iterating over values
47
+ * * {@link module:array.delayedFn|delayedFn} - Similar to Function.bind() - you specify a function and arguments only to be called when you ask
48
+ * * {@link module:array.chainFunctions|chainFunctions} - Chain a set of functions to be called one after another.
49
+ * * {@link module:array.asyncWaitAndChain|asyncWaitAndChain} - Chains a set of functions to run one after another, but with a delay between.
44
50
  *
45
51
  * @module array
46
52
  * @exports array
@@ -478,6 +484,7 @@ module.exports.applyArrayValues = function applyArrayValues(collection, path, va
478
484
  * @param {Number} length - the length of the new array
479
485
  * @param {any} defaultValue - the new value to put in each cell
480
486
  * @see {@link module:array.arrange} for values based on the index
487
+ * @see https://stackoverflow.com/questions/35578478/array-prototype-fill-with-object-passes-reference-and-not-new-instance
481
488
  * @returns {Array} - an array of length size with default values
482
489
  */
483
490
  module.exports.size = function size(length, defaultValue) {
@@ -1097,3 +1104,247 @@ module.exports.multiStepReduce = function multiStepReduce(list, fn, initialValue
1097
1104
  });
1098
1105
  return results;
1099
1106
  };
1107
+
1108
+ /**
1109
+ * Create an iterator for an array that allows for peeking next values.
1110
+ *
1111
+ * @see https://www.npmjs.com/package/peekable-array-iterator
1112
+ * @example
1113
+ *
1114
+ * source = [0, 1, 2, 3, 4, 5];
1115
+ *
1116
+ * // also quite helpful for document.querySelector(...)
1117
+ * itr = new utils.array.PeekableArrayIterator(source);
1118
+ *
1119
+ * console.log(itr.next()); // { done: false, value: 0 }
1120
+ *
1121
+ * //-- peek without moving the iterator
1122
+ * const peekItr = itr.peek();
1123
+ * console.log(peekItr.next()); // { done: false, value: 1 }
1124
+ * console.log(peekItr.next()); // { done: false, value: 2 }
1125
+ * console.log(peekItr.next()); // { done: false, value: 3 }
1126
+ * console.log(peekItr.next()); // { done: false, value: 4 }
1127
+ * console.log(peekItr.next()); // { done: true, value: 5 }
1128
+ *
1129
+ * //-- move the main iterator
1130
+ * console.log(itr.next()); // { done: false, value: 1 }
1131
+ *
1132
+ * Of course, for each will always work
1133
+ * or
1134
+ * for (let i of new utils.array.PeekableArrayIterator(list)) {
1135
+ * console.log(i);
1136
+ * }
1137
+ * // 1\n2\n3\n4\n5
1138
+ */
1139
+ class PeekableArrayIterator {
1140
+ /**
1141
+ * Constructor
1142
+ *
1143
+ * @param {Iterable} array - something we can iterate over
1144
+ * @param {Number} start - the starting index
1145
+ */
1146
+ constructor(source, start = -1) {
1147
+ this.array = Array.isArray(source) ? source : [...source];
1148
+ this.i = start;
1149
+ }
1150
+
1151
+ [Symbol.iterator]() { return this; }
1152
+
1153
+ /* eslint-disable wrap-iife */
1154
+ next() {
1155
+ const self = this;
1156
+ this.peek = (function* peek() {
1157
+ for (let peekI = self.i + 1; peekI < self.array.length; peekI += 1) {
1158
+ yield self.array[peekI];
1159
+ }
1160
+ return undefined;
1161
+ })();
1162
+
1163
+ this.i += 1;
1164
+ return { done: this.i >= this.array.length - 1, value: this.array[this.i] };
1165
+ }
1166
+ /* eslint-enable wrap-iife */
1167
+ }
1168
+ module.exports.PeekableArrayIterator = PeekableArrayIterator;
1169
+
1170
+ /**
1171
+ * Defines a function and arguments that will only be called,
1172
+ * only when the delayedFunction is called.
1173
+ *
1174
+ * (Similar to Function.bind - but supports Function.call(1,2,3) or Function.apply([1,2,3]) syntax)
1175
+ *
1176
+ * Example, these are equivalent, but the delayedFn does not mess with `this`
1177
+ *
1178
+ * sayFn = (...rest) => console.log(...rest);
1179
+ * arguments = [0, 1, 2, 3, 4, 5];
1180
+ *
1181
+ * Spy(sayFn);
1182
+ *
1183
+ * delayedFnA = sayFn.bind(globalThis, arguments);
1184
+ * ...
1185
+ * sayFn.calledCount; // 0
1186
+ * delayedFnA(); // consoles: [0, 1, 2, 3, 4, 5];
1187
+ * sayFn.calledCount; // 1
1188
+ *
1189
+ * delayedFnB = utils.array.delayedFn(sayFn, 0, 1, 2, 3, 4, 5);
1190
+ * ...
1191
+ * delayedFnB(); // consoles: [0, 1, 2, 3, 4, 5];
1192
+ *
1193
+ * @param {Function} fn - Function to be executed at a later time
1194
+ * @param {...any} rest - arguments to be passed to fn
1195
+ * @returns {Function} - function that can be called to execute fn with the arguments
1196
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
1197
+ */
1198
+ module.exports.delayedFn = (fn, ...rest) => () => {
1199
+ const cleanRest = rest.length === 1 && Array.isArray(rest[0]) ? rest[0] : rest;
1200
+ return fn.apply(this, cleanRest);
1201
+ };
1202
+
1203
+ /**
1204
+ * Chain a set of functions to be called one after another
1205
+ * (supports functions returning promises)
1206
+ *
1207
+ * This is especially helpful if calls need to be rate limited to only having 1 occur at a time.
1208
+ *
1209
+ * The resulting promise will return a list, with each entry corresponding with the row of arguments sent.
1210
+ *
1211
+ * ```
1212
+ * const sumValues = (...rest) => rest.reduce((result, val) => result + val, 0);
1213
+ *
1214
+ * const arguments = [
1215
+ * [1],
1216
+ * [1, 1],
1217
+ * [1, 1, 2],
1218
+ * [1, 1, 2, 3]
1219
+ * ];
1220
+ *
1221
+ * utils.array.chainFunctions(sumValues, arguments)
1222
+ * .then((results) => console.log(`fibonnacci numbers: ${results}`));
1223
+ * // fibonacci numbers: [1, 2, 4, 7]
1224
+ * ```
1225
+ *
1226
+ * @param {Function} fn - the function to be called
1227
+ * @param {Array<any[]>} rows - Array where each row are arguments to be applied to fn
1228
+ * @returns {Promise<any>} - promise that will resolve when the last delayed function finishes
1229
+ * @see {@link https://rxjs.dev/guide/overview|rxjs} if you would like to have more than one active at a time.
1230
+ * @see {@link module:array.asyncWaitAndChain|asyncWaitAndChain} - if you would like a delay between executions
1231
+ */
1232
+ module.exports.chainFunctions = (fn, rows) => {
1233
+ const delayedFunctions = rows.map((val) => ArrayUtils.delayedFn(fn, val));
1234
+ const delayedIterator = delayedFunctions.values();
1235
+ const answers = [];
1236
+ let isFirstCall = true;
1237
+
1238
+ return new Promise((resolve, reject) => {
1239
+ const callNext = (result) => {
1240
+ if (isFirstCall) {
1241
+ isFirstCall = false;
1242
+ } else {
1243
+ answers.push(result);
1244
+ }
1245
+ try {
1246
+ const nextVal = delayedIterator.next();
1247
+ const { value: delayedFunction, done } = nextVal;
1248
+ if (!done) {
1249
+ const fnResult = delayedFunction();
1250
+ if (fnResult instanceof Promise) {
1251
+ fnResult.then(callNext);
1252
+ } else {
1253
+ callNext(fnResult);
1254
+ }
1255
+ } else {
1256
+ resolve(answers);
1257
+ }
1258
+ } catch (err) {
1259
+ reject(err);
1260
+ }
1261
+ };
1262
+ return callNext();
1263
+ });
1264
+ };
1265
+
1266
+ /**
1267
+ * Executes a function with arguments after a few second delay.
1268
+ *
1269
+ * @param {Number} seconds - number of seconds to wait before calling
1270
+ * @param {Function} fn - Function to call
1271
+ * @param {...any} rest - arguments to send to the function when it is executed.
1272
+ * @returns {@Promise<any>} - that then executes when the timer is up
1273
+ * @private
1274
+ */
1275
+ const asyncWaitThenRun = (seconds, fn, ...rest) => new Promise(
1276
+ (resolve, reject) => {
1277
+ setTimeout(() => {
1278
+ try {
1279
+ const results = fn(...rest);
1280
+ if (results instanceof Promise) {
1281
+ results.then((promiseResults) => {
1282
+ resolve(promiseResults);
1283
+ });
1284
+ } else {
1285
+ resolve(results);
1286
+ }
1287
+ } catch (err) {
1288
+ reject(err);
1289
+ }
1290
+ }, seconds * 1000);
1291
+ }
1292
+ );
1293
+
1294
+ /**
1295
+ * Similar to chainFunctions - in that only one delayed function will occur at a time,
1296
+ * but adds a delay between calls.
1297
+ *
1298
+ * This also supports functions returning promises.
1299
+ *
1300
+ *
1301
+ * ```
1302
+ * const sumValues = (...rest) => rest.reduce((result, val) => result + val, 0);
1303
+ *
1304
+ * const arguments = [
1305
+ * [1],
1306
+ * [1, 1],
1307
+ * [1, 1, 2],
1308
+ * [1, 1, 2, 3]
1309
+ * ];
1310
+ *
1311
+ * utils.array.asyncWaitAndChain(3, sumValues, arguments)
1312
+ * .then((results) => console.log(`fibonnacci numbers: ${results}`));
1313
+ * // fibonacci numbers: [1, 2, 4, 7], but took 9 seconds to accomplish
1314
+ * ```
1315
+ *
1316
+ * @param {Number} seconds - number of seconds to delay between each execution
1317
+ * @param {Function} fn - function to be called for each row of rows
1318
+ * @param {Array<any[]>} rows - Array where each row are arguments to be applied to fn
1319
+ * @returns {Promise<any>} - promise that will resolve when the last delayed function finishes
1320
+ * @see {@link module:array.chainFunctions|chainFunctions} - to execute methods right after each other.
1321
+ * @see {@link https://rxjs.dev/guide/overview|rxjs} if you would like to execute more than one at a time.
1322
+ */
1323
+ // eslint-disable-next-line no-unused-vars
1324
+ module.exports.asyncWaitAndChain = (seconds, fn, rows) => {
1325
+ const delayedFunctions = rows.map((val) => ArrayUtils.delayedFn(fn, val));
1326
+ const delayedIterator = delayedFunctions.values();
1327
+ const answers = [];
1328
+ let isFirstCall = true;
1329
+
1330
+ return new Promise((resolve, reject) => {
1331
+ const callNext = (result) => {
1332
+ if (isFirstCall) {
1333
+ isFirstCall = false;
1334
+ } else {
1335
+ answers.push(result);
1336
+ }
1337
+ const nextVal = delayedIterator.next();
1338
+ const { value: delayedFunction, done } = nextVal;
1339
+ if (!done) {
1340
+ return asyncWaitThenRun(seconds, delayedFunction)
1341
+ .then(callNext)
1342
+ .catch((err) => {
1343
+ reject(err);
1344
+ });
1345
+ }
1346
+ resolve(answers);
1347
+ };
1348
+ return callNext();
1349
+ });
1350
+ };
package/src/chain.js CHANGED
@@ -13,7 +13,6 @@
13
13
  * * {@link ChainContainer#close|.close()} - gets the value of the current chain
14
14
  * * {@link ChainContainer#chain|.chain(function)} - where it is passed the value, and returns a new Chain with that value.
15
15
  * * {@link ChainContainer#errorHandler|.errorHandler(fn)} - custom function called if an error is ever thrown
16
- * * {@link ChainContainer#debug|.debug()} - console.logs the current value, and continues the chain with that value
17
16
  *
18
17
  * Along with methods that can iterate on each element, assuming the value in the chain is an Array.
19
18
  *
@@ -32,10 +31,12 @@
32
31
  *
33
32
  * There may be times you want to run side effects, or replace the value entirely. (This isn't common, but may be useful on occasion)
34
33
  *
34
+ * * {@link ChainContainer#debug|.debug()} - continues with the current value, but executes a console.log first
35
35
  * * {@link ChainContainer#execute|.execute(function)} - where it calls a function, but doesn't pass on the result.
36
36
  * <br /> (This is useful for side-effects, like writing to files)
37
37
  * * {@link ChainContainer#replace|.replace(value)} - replaces the value in the chain with a literal value,
38
38
  * regardless of the previous value.
39
+ * * {@link ChainContainer#toArray|.toArray()} - assuming the current value is an interatable, converts it to an array.
39
40
  *
40
41
  * For example:
41
42
  *
@@ -526,6 +527,25 @@ class ChainContainer {
526
527
  return this.value;
527
528
  }
528
529
 
530
+ /**
531
+ * Converts the current value to an array to be further chained.
532
+ * (Assumes it is iteratable);
533
+ *
534
+ * Example:
535
+ *
536
+ * ```
537
+ * new Chain(document.querySelectorAll('div'))
538
+ * .toArray()
539
+ * .execute((passedValue) => Array.isArray(passedValue))
540
+ * .close(); // [... list of div elements on the page]
541
+ * ```
542
+ *
543
+ * @returns {ChainContainer}
544
+ */
545
+ toArray() {
546
+ return new ChainContainer(Array.from(this.value));
547
+ }
548
+
529
549
  //-- private methods
530
550
 
531
551
  /**
package/src/format.js CHANGED
@@ -1068,6 +1068,12 @@ module.exports.limitLines = function limitLines(str, toLine, fromLine, lineSepar
1068
1068
  : JSON.stringify(str || '', FormatUtils.mapReplacer, 2);
1069
1069
  const cleanLine = lineSeparator || '\n';
1070
1070
 
1071
+ if (!toLine) {
1072
+ return cleanStr.split(cleanLine)
1073
+ .slice(fromLine || 0)
1074
+ .join(cleanLine);
1075
+ }
1076
+
1071
1077
  return cleanStr.split(cleanLine)
1072
1078
  .slice(fromLine || 0, toLine)
1073
1079
  .join(cleanLine);
package/src/object.js CHANGED
@@ -52,6 +52,11 @@ const FormatUtils = require('./format');
52
52
  * * Create Map of objects by key
53
53
  * * {@link module:object.mapByProperty|mapByProperty()} -
54
54
  * * {@link module:group.by|group(collection, accessor)}
55
+ * * Convert collections of objects
56
+ * * {@link module:object.objectCollectionFromArray|objectCollectionFromArray} - convert rows/columns 2d array to objects
57
+ * * {@link module:object.objectCollectionToArray} - convert objects to a rows/columns 2d array
58
+ * * {@link module:object.objectCollectionFromDataFrameObject} - convert tensor object with each field as 1d array of values
59
+ * * {@link module:object.objectCollectionToDataFrameObject} - convert objects from a tensor object
55
60
  *
56
61
  * @module object
57
62
  * @exports object
@@ -1550,7 +1555,7 @@ module.exports.setPropertyDefaults = function setPropertyDefaults(targetObject,
1550
1555
  * @param {Object[]} objCollection - object or multiple objects that should have properties formatted
1551
1556
  * @param {Function} formattingFn - function to apply to all the properties specified
1552
1557
  * @param {...any} propertiesToFormat - list of properties to apply the formatting function
1553
- * @returns {Object[] - clone of objCollection with properties mapped
1558
+ * @returns {Object[]} - clone of objCollection with properties mapped
1554
1559
  */
1555
1560
  module.exports.mapProperties = function mapProperties(objCollection, formattingFn, ...propertiesToFormat) {
1556
1561
  const cleanCollection = !Array.isArray(objCollection)
@@ -1860,3 +1865,235 @@ module.exports.union = function union(source1, source2) {
1860
1865
 
1861
1866
  return results;
1862
1867
  };
1868
+
1869
+ /**
1870
+ * Converts a 2d array to a collection of objects.
1871
+ *
1872
+ * For Example:
1873
+ * ```
1874
+ * weather = [
1875
+ * [ 'id', 'city', 'month', 'precip' ],
1876
+ * [ 1, 'Seattle', 'Aug', 0.87 ],
1877
+ * [ 0, 'Seattle', 'Apr', 2.68 ],
1878
+ * [ 2, 'Seattle', 'Dec', 5.31 ]
1879
+ * ]
1880
+ *
1881
+ * utils.object.objectCollectionFromArray(weather);
1882
+ * // [
1883
+ * // { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
1884
+ * // { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
1885
+ * // { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 }
1886
+ * // ];
1887
+ * ```
1888
+ *
1889
+ * Note that the headers can be optionally provided separately.
1890
+ *
1891
+ * ```
1892
+ * weather = [
1893
+ * [ 1, 'Seattle', 'Aug', 0.87 ],
1894
+ * [ 0, 'Seattle', 'Apr', 2.68 ],
1895
+ * [ 2, 'Seattle', 'Dec', 5.31 ]
1896
+ * ];
1897
+ * headers = [ 'id', 'city', 'month', 'precip' ];
1898
+ *
1899
+ * utils.object.objectCollectionFromArray(weather, headers);
1900
+ * // [
1901
+ * // { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
1902
+ * // { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
1903
+ * // { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 }
1904
+ * // ];
1905
+ * ```
1906
+ *
1907
+ * @param {Array<Array>} arrayCollection - 2d collection of values
1908
+ * @param {String[]} [headers] - Optional set of headers to use if not present in first row 0
1909
+ * @returns {Object[]} - list of objects
1910
+ * @see {@link module:object.objectCollectionFromArray|objectCollectionFromArray}
1911
+ * @see {@link module:object.objectCollectionToArray|objectCollectionToArray}
1912
+ * @see {@link module:object.objectCollectionFromDataFrameObject|objectCollectionFromDataFrameObject}
1913
+ * @see {@link module:object.objectCollectionToDataFrameObject|objectCollectionToDataFrameObject}
1914
+ */
1915
+ module.exports.objectCollectionFromArray = function objectCollectionFromArray(arrayCollection, headers = null) {
1916
+ let cleanHeaders;
1917
+ let cleanValues;
1918
+
1919
+ if (!Array.isArray(arrayCollection)) throw Error('objectCollectionFromArray: expected collection to be a 2 dimensional array');
1920
+
1921
+ if (!headers) {
1922
+ const [arrayCollectionHeaders, ...arrayCollectionValues] = arrayCollection;
1923
+ cleanHeaders = arrayCollectionHeaders;
1924
+ cleanValues = arrayCollectionValues;
1925
+ } else {
1926
+ cleanHeaders = headers;
1927
+ cleanValues = arrayCollection;
1928
+ }
1929
+
1930
+ /* eslint-disable arrow-body-style */
1931
+ const newData = cleanValues.map((row) => {
1932
+ return cleanHeaders.reduce((result, header, index) => {
1933
+ return ObjectUtils.assign(result, header, row[index]);
1934
+ }, {});
1935
+ });
1936
+ /* eslint-enable arrow-body-style */
1937
+
1938
+ return newData;
1939
+ };
1940
+
1941
+ /**
1942
+ * Converts a 2d array to a collection of objects.
1943
+ *
1944
+ * For Example:
1945
+ * ```
1946
+ * weather = [
1947
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
1948
+ * { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
1949
+ * { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 }
1950
+ * ];
1951
+ *
1952
+ * utils.object.objectCollectionToArray(weather);
1953
+ * // [
1954
+ * // [ 'id', 'city', 'month', 'precip' ],
1955
+ * // [ 1, 'Seattle', 'Aug', 0.87 ],
1956
+ * // [ 0, 'Seattle', 'Apr', 2.68 ],
1957
+ * // [ 2, 'Seattle', 'Dec', 5.31 ]
1958
+ * // ]
1959
+ * ```
1960
+ *
1961
+ * @param {Array<Array>} arrayCollection - 2d collection of values
1962
+ * @param {String[]?} headers - Optional set of headers to use if not present in first row 0
1963
+ * @returns {Object[]} - list of objects
1964
+ * @see {@link module:object.objectCollectionFromArray|objectCollectionFromArray}
1965
+ * @see {@link module:object.objectCollectionToArray|objectCollectionToArray}
1966
+ * @see {@link module:object.objectCollectionFromDataFrameObject|objectCollectionFromDataFrameObject}
1967
+ * @see {@link module:object.objectCollectionToDataFrameObject|objectCollectionToDataFrameObject}
1968
+ */
1969
+ module.exports.objectCollectionToArray = function objectCollectionToArray(objectCollection) {
1970
+ if (!Array.isArray(objectCollection)) throw Error('objectCollectionToArray: expected collection to be a collection of objects');
1971
+
1972
+ //-- create the result array in advance for performance
1973
+ // https://stackoverflow.com/questions/35578478/array-prototype-fill-with-object-passes-reference-and-not-new-instance
1974
+
1975
+ const keys = ObjectUtils.keys(objectCollection);
1976
+
1977
+ const finalResult = new Array(objectCollection.length + 1);
1978
+ finalResult[0] = keys;
1979
+
1980
+ for (let i = 1; i <= objectCollection.length; i += 1) {
1981
+ finalResult[i] = new Array(keys.length);
1982
+ }
1983
+
1984
+ objectCollection.forEach((obj, objIndex) => {
1985
+ const rowResult = finalResult[objIndex + 1];
1986
+ keys.forEach((key, keyIndex) => {
1987
+ rowResult[keyIndex] = obj[key];
1988
+ });
1989
+ });
1990
+ return finalResult;
1991
+ };
1992
+
1993
+ /**
1994
+ * Convert a DataFrame Object into a collection of objects.
1995
+ *
1996
+ * This uses properties with 1d tensor lists
1997
+ * and converts them to a list of objects.
1998
+ *
1999
+ * ```
2000
+ * const weather = {
2001
+ * id: [1, 0, 2],
2002
+ * city: ['Seattle', 'Seattle', 'Seattle'],
2003
+ * month: ['Aug', 'Apr', 'Dec'],
2004
+ * precip: [0.87, 2.68, 5.31]
2005
+ * };
2006
+ *
2007
+ * ObjectUtils.objectCollectionFromDataFrameObject(weather);
2008
+ * // [
2009
+ * // { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
2010
+ * // { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
2011
+ * // { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 }
2012
+ * // ];
2013
+ * ```
2014
+ *
2015
+ * @param {Object} dataFrameObject - Object with properties holding 1d tensor arrays
2016
+ * @returns {Object[]} - collection of objects
2017
+ * @see {@link module:object.objectCollectionFromArray|objectCollectionFromArray}
2018
+ * @see {@link module:object.objectCollectionToArray|objectCollectionToArray}
2019
+ * @see {@link module:object.objectCollectionFromDataFrameObject|objectCollectionFromDataFrameObject}
2020
+ * @see {@link module:object.objectCollectionToDataFrameObject|objectCollectionToDataFrameObject}
2021
+ * @see {@link https://danfo.jsdata.org/api-reference/dataframe/creating-a-dataframe#creating-a-dataframe-from-an-object|Danfo DataFrame Objects}
2022
+ */
2023
+ module.exports.objectCollectionFromDataFrameObject = function objectCollectionFromDataFrameObject(dataFrameObject) {
2024
+ if (!dataFrameObject) return [];
2025
+ if ((typeof dataFrameObject) !== 'object') throw Error('objectCollectionFromDataFrameObject must be passed an object with properties holding 1d tensors');
2026
+
2027
+ const fields = ObjectUtils.keys(dataFrameObject);
2028
+
2029
+ if (fields.length < 1) return [];
2030
+
2031
+ const len = fields.reduce((maxLen, field) => {
2032
+ const list = dataFrameObject[field];
2033
+ if (!Array.isArray(list)) return maxLen;
2034
+
2035
+ const listLength = list?.length || 0;
2036
+ return listLength > maxLen ? listLength : maxLen;
2037
+ }, -1);
2038
+
2039
+ // console.log(`fieldLength:${len}`);
2040
+ const results = new Array(len).fill(0).map((_) => ({}));
2041
+
2042
+ fields.forEach((field) => {
2043
+ const fieldArray = dataFrameObject[field];
2044
+ if (Array.isArray(fieldArray)) {
2045
+ fieldArray.forEach((fieldValue, rowIndex) => {
2046
+ results[rowIndex][field] = fieldValue;
2047
+ });
2048
+ }
2049
+ });
2050
+
2051
+ return results;
2052
+ };
2053
+
2054
+ /**
2055
+ * Convert a DataFrame Object into a collection of objects.
2056
+ *
2057
+ * This uses properties with 1d tensor lists
2058
+ * and converts them to a list of objects.
2059
+ *
2060
+ * ```
2061
+ * const weather = [
2062
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
2063
+ * { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
2064
+ * { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 }
2065
+ * ];
2066
+ *
2067
+ * ObjectUtils.objectCollectionToDataFrameObject(weather);
2068
+ * // {
2069
+ * // id: [1, 0, 2],
2070
+ * // city: ['Seattle', 'Seattle', 'Seattle'],
2071
+ * // month: ['Aug', 'Apr', 'Dec'],
2072
+ * // precip: [0.87, 2.68, 5.31]
2073
+ * // };
2074
+ * ```
2075
+ *
2076
+ * @param {Object} dataFrameObject - Object with properties holding 1d tensor arrays
2077
+ * @returns {Object[]} - collection of objects
2078
+ * @see {@link module:object.objectCollectionFromArray|objectCollectionFromArray}
2079
+ * @see {@link module:object.objectCollectionToArray|objectCollectionToArray}
2080
+ * @see {@link module:object.objectCollectionFromDataFrameObject|objectCollectionFromDataFrameObject}
2081
+ * @see {@link module:object.objectCollectionToDataFrameObject|objectCollectionToDataFrameObject}
2082
+ * @see {@link https://danfo.jsdata.org/api-reference/dataframe/creating-a-dataframe#creating-a-dataframe-from-an-object|Danfo DataFrame Objects}
2083
+ */
2084
+ module.exports.objectCollectionToDataFrameObject = function objectCollectionToDataFrameObject(objectCollection, fields = null) {
2085
+ const dataFrameObject = {};
2086
+
2087
+ const cleanFields = fields || ObjectUtils.keys(objectCollection);
2088
+ const length = objectCollection?.length || 0;
2089
+
2090
+ cleanFields.forEach((field) => {
2091
+ const fieldData = new Array(length).fill(undefined);
2092
+ dataFrameObject[field] = fieldData;
2093
+
2094
+ objectCollection.forEach((obj, index) => {
2095
+ fieldData[index] = obj[field];
2096
+ });
2097
+ });
2098
+ return dataFrameObject;
2099
+ };