jupyter-ijavascript-utils 1.56.0 → 1.58.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 +9 -0
- package/Dockerfile +1 -1
- package/README.md +9 -0
- package/package.json +1 -1
- package/src/array.js +15 -8
- package/src/date.js +3 -2
- package/src/file.js +102 -13
- package/src/hashMap.js +3 -4
- package/src/ijs.js +106 -3
- package/src/object.js +215 -5
package/DOCS.md
CHANGED
|
@@ -74,6 +74,15 @@ Give it a try here:
|
|
|
74
74
|
[](https://mybinder.org/v2/gh/paulroth3d/jupyter-ijavascript-utils/main?labpath=example.ipynb)
|
|
75
75
|
|
|
76
76
|
## What's New
|
|
77
|
+
* 1.58 -
|
|
78
|
+
* #91 - add {@link module:object.splitIntoDatums|splitIntoDatums(collection, fieldsToSplitBy)} - to make working with datum libraries - like vega-lite - easier
|
|
79
|
+
* #92 - make using the cache easier for {@link module:file.useCache|file.useCache()} - as you can say to use the cache, and it will still work if the cache file does not exist yet.
|
|
80
|
+
* #93 / #94 - bug fixes
|
|
81
|
+
* 1.57 - #86 - include examples on binning based on time series {@link https://github.com/paulroth3d/jupyter-ijavascript-utils/issues/86|see more here}
|
|
82
|
+
* #87 - add in {@link module:file.fileExists|file.fileExists} to make things easier for new people getting started
|
|
83
|
+
* #89 - allow {@link module:array.resize|array.resize} to work with defaults, if zipping arrays of different sizes
|
|
84
|
+
* #90 - correct issue with timezone offsets, so it is no longer possible to get a timezone offset +24:00 from any of the localizations - like {@link module:date.toLocalISO|date.toLocalISO}
|
|
85
|
+
* #92 - make it easier to handle big expensive calculations with a cache, see {@link module:ijs.useCache|ijs.useCache()} and {@link module:file.useCache|file.useCache()}
|
|
77
86
|
* 1.56 - #84 (object.renamePropertiesFromList), #82 (date.getWeekday)
|
|
78
87
|
* 1.55 - #76, #77, #74, #78
|
|
79
88
|
* 1.54 - additional Date logic, and formatting. #70 #71 #72
|
package/Dockerfile
CHANGED
package/README.md
CHANGED
|
@@ -54,6 +54,15 @@ This is not intended to be the only way to accomplish many of these tasks, and a
|
|
|
54
54
|

|
|
55
55
|
|
|
56
56
|
# What's New
|
|
57
|
+
* 1.58 -
|
|
58
|
+
* #91 - add object.splitIntoDatums - to make working with datum libraries - like vega-lite - easier
|
|
59
|
+
* #92 - make using the cache easier - as you can say to use the cache, and it will still work if the cache file does not exist yet.
|
|
60
|
+
* #93 / #94 - bug fixes
|
|
61
|
+
* 1.57 - #86 - include examples on binning based on time series
|
|
62
|
+
* #87 - add in fileExists to make things easier for new people getting started
|
|
63
|
+
* #89 - allow resizing arrays with defaults, if zipping arrays of different sizes
|
|
64
|
+
* #90 - correct issue with timezone offsets, so it is no longer possible to get a timezone offset +24:00
|
|
65
|
+
* #92 - make it easier to handle big expensive calculations with a cache
|
|
57
66
|
* 1.56 - #84 (object.renamePropertiesFromList), #82 (date.getWeekday)
|
|
58
67
|
* 1.55 - #76, #77, #74, #78
|
|
59
68
|
* 1.54 - additional Date logic, and formatting. #70 #71 #72
|
package/package.json
CHANGED
package/src/array.js
CHANGED
|
@@ -1460,18 +1460,25 @@ module.exports.asyncWaitAndChain = (seconds, fn, rows) => {
|
|
|
1460
1460
|
*
|
|
1461
1461
|
* utils.array.resize(categoryValues, 2); // ['rock', 'paper']
|
|
1462
1462
|
* utils.array.resize(categoryValues, 7); // ['rock', 'paper', 'scissors',
|
|
1463
|
-
*
|
|
1463
|
+
* undefined, undefined, undefined, undefined];
|
|
1464
1464
|
* ```
|
|
1465
1465
|
*
|
|
1466
1466
|
* @param {Array} sourceList - array of values
|
|
1467
|
-
* @param {Number} length - new
|
|
1467
|
+
* @param {Number} length - new length of the list
|
|
1468
1468
|
*/
|
|
1469
|
-
module.exports.resize = function resize(sourceList, length) {
|
|
1469
|
+
module.exports.resize = function resize(sourceList, length, defaultValue = undefined) {
|
|
1470
1470
|
if (!sourceList || !Array.isArray(sourceList)) return [];
|
|
1471
1471
|
if (length < 1 || sourceList.length < 1) return [];
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
.
|
|
1472
|
+
|
|
1473
|
+
const result = new Array(length)
|
|
1474
|
+
.fill(defaultValue);
|
|
1475
|
+
|
|
1476
|
+
sourceList.forEach((val, index) => {
|
|
1477
|
+
if (index >= length) return;
|
|
1478
|
+
result[index] = val;
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
return result;
|
|
1475
1482
|
};
|
|
1476
1483
|
|
|
1477
1484
|
/**
|
|
@@ -1532,9 +1539,9 @@ module.exports.zip = function zip(arrayLeft, arrayRight, ...rest) {
|
|
|
1532
1539
|
if (cleanLeft.length === 0 && cleanRight.length === 0) {
|
|
1533
1540
|
result = [[]];
|
|
1534
1541
|
} else if (cleanLeft.length === 0) {
|
|
1535
|
-
result = cleanRight.map((val) => Array.isArray(val) ? val : [val]);
|
|
1542
|
+
result = cleanRight.map((val) => Array.isArray(val) ? val : (typeof val === 'string') ? [val] : [...val]);
|
|
1536
1543
|
} else if (cleanRight.length === 0) {
|
|
1537
|
-
result = cleanLeft.map((val) => Array.isArray(val) ? val : [val]);
|
|
1544
|
+
result = cleanLeft.map((val) => Array.isArray(val) ? val : (typeof val === 'string') ? [val] : [...val]);
|
|
1538
1545
|
} else {
|
|
1539
1546
|
const cleanLeftLen = cleanLeft.length;
|
|
1540
1547
|
const cleanRightLen = cleanRight.length;
|
package/src/date.js
CHANGED
|
@@ -258,7 +258,7 @@ module.exports.getTimezoneEntry = function getTimezoneEntry(timezoneStr) {
|
|
|
258
258
|
}, {});
|
|
259
259
|
// const impactedDate = new Date(d.toLocaleString('en-US', { timezone: timezoneStr }));
|
|
260
260
|
const dateStr = `${dm.year}-${DateUtils.padTime(dm.month)}-${DateUtils.padTime(dm.day)}T${
|
|
261
|
-
DateUtils.padTime(dm.hour)}:${DateUtils.padTime(dm.minute)}:${DateUtils.padTime(dm.second)}.${
|
|
261
|
+
DateUtils.padTime(dm.hour % 24)}:${DateUtils.padTime(dm.minute)}:${DateUtils.padTime(dm.second)}.${
|
|
262
262
|
DateUtils.padTime(dm.fractionalSecond, 3)}`;
|
|
263
263
|
|
|
264
264
|
return dateStr;
|
|
@@ -266,7 +266,8 @@ module.exports.getTimezoneEntry = function getTimezoneEntry(timezoneStr) {
|
|
|
266
266
|
|
|
267
267
|
const impactedDate = getOffset(d);
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
//-- avoid +24:00 or -24:00 - the number of seconds within a day
|
|
270
|
+
const diff = (d.getTime() - impactedDate.getTime()) % 86400000;
|
|
270
271
|
|
|
271
272
|
const diffSign = diff > 0 ? '-' : '+';
|
|
272
273
|
let remainder = DateUtils.divideRemainder(Math.abs(diff), DateUtils.TIME.HOUR);
|
package/src/file.js
CHANGED
|
@@ -29,6 +29,9 @@ const logger = require('./logger');
|
|
|
29
29
|
* * {@link module:file.matchFiles|matchFiles(path, matchingFn)} - find files or directories based on type of file or name
|
|
30
30
|
* * checking files exist
|
|
31
31
|
* * {@link module:file.checkFile|checkFile(...paths)} - check if a file at a path exists
|
|
32
|
+
* * {@link module:file.fileExists|fileExists(filePath)} - check if a single file at a path exists
|
|
33
|
+
* * using a cache for long running executions
|
|
34
|
+
* * {@link module:file.useCache|file.useCache()} - perform an expensive calculation and write to a cache, or read from the cache transparently
|
|
32
35
|
*
|
|
33
36
|
* ---
|
|
34
37
|
*
|
|
@@ -96,6 +99,8 @@ const FileUtil = module.exports;
|
|
|
96
99
|
*
|
|
97
100
|
* @param {string} filePath - path of the file to load
|
|
98
101
|
* @param {Object} fsOptions - options to pass for fsRead (ex: { encoding: 'utf-8' })
|
|
102
|
+
* @param {Function} fsOptions.reviver - reviver to use when writing the JSON
|
|
103
|
+
* @param {String} fsOptions.encoding - the encoding to write the JSON out with
|
|
99
104
|
* @example
|
|
100
105
|
* const weather = [
|
|
101
106
|
* { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
|
|
@@ -222,17 +227,20 @@ module.exports.readFile = function readFile(filePath, fsOptions = {}) {
|
|
|
222
227
|
* @param {Boolean} fsOptions.append - if true, will append the text to the file
|
|
223
228
|
* @param {Boolean} fsOptions.prefix - string to add before writing the json, like an opening bracket '[' or comma ','
|
|
224
229
|
* @param {Boolean} fsOptions.prefix - string to add before writing the json, like a closing bracket ']'
|
|
230
|
+
* @param {Function} fsOptions.replacer - function to use when writing JSON passed to stringify
|
|
225
231
|
* @param {String} fsOptions.encoding - encoding to use when writing the file.
|
|
226
232
|
* @see {@link module:file.readJSON|readJSON(filePath, fsOptions)} - for reading
|
|
227
233
|
*/
|
|
228
234
|
module.exports.writeJSON = function writeJSON(filePath, contents, fsOptions = {}) {
|
|
229
235
|
//-- if it isn't desired, simply pass as a string.
|
|
230
|
-
const jsonContents = JSON.stringify(contents, null, 2);
|
|
231
236
|
const optionsDefaults = { encoding: 'utf-8' };
|
|
232
237
|
const cleanedOptions = { ...optionsDefaults, ...fsOptions };
|
|
233
238
|
const isAppend = cleanedOptions.append === true;
|
|
234
239
|
const prefix = cleanedOptions.prefix || '';
|
|
235
240
|
const suffix = cleanedOptions.suffix || '';
|
|
241
|
+
const replacer = cleanedOptions.replacer || null;
|
|
242
|
+
const spacing = cleanedOptions.spacing || 2;
|
|
243
|
+
const jsonContents = JSON.stringify(contents, replacer, spacing);
|
|
236
244
|
|
|
237
245
|
// const resolvedPath = path.resolve(filePath);
|
|
238
246
|
try {
|
|
@@ -465,23 +473,104 @@ module.exports.checkFile = function checkFile(...files) {
|
|
|
465
473
|
return notFoundFiles;
|
|
466
474
|
};
|
|
467
475
|
|
|
468
|
-
|
|
469
|
-
*
|
|
470
|
-
* @param {String
|
|
471
|
-
* @
|
|
476
|
+
/**
|
|
477
|
+
* Checks if a single file exists
|
|
478
|
+
* @param {String} filePath - path to check if the file exists.
|
|
479
|
+
* @returns {Boolean} - if the file exists (true) or not (false)
|
|
480
|
+
* @see {@link module:file.checkFile|file.checkFile} - if checking multiple files
|
|
472
481
|
*/
|
|
482
|
+
module.exports.fileExists = function fileExists(filePath) {
|
|
483
|
+
const resolvedPath = path.resolve(filePath);
|
|
484
|
+
return fs.existsSync(resolvedPath);
|
|
485
|
+
};
|
|
486
|
+
|
|
473
487
|
/*
|
|
474
|
-
|
|
475
|
-
|
|
488
|
+
//-- not needed - dates already serialize to iso Strings
|
|
489
|
+
module.exports.cacheSerializer = (key, value) => {
|
|
490
|
+
if (key && (key === 'date' || key.endsWith('_date')) && (value instanceof Date)) {
|
|
491
|
+
return value.toISOString();
|
|
492
|
+
}
|
|
493
|
+
return value;
|
|
494
|
+
};
|
|
495
|
+
*/
|
|
476
496
|
|
|
477
|
-
|
|
497
|
+
module.exports.cacheDeserializer = (key, value) => {
|
|
498
|
+
if (key && (key === 'date' || key.endsWith('_date') || key.endsWith('Date'))) {
|
|
499
|
+
return new Date(value);
|
|
500
|
+
}
|
|
501
|
+
return value;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* For very long or time-intensive executions, sometimes it is better to cache the results
|
|
506
|
+
* than to execute them every single time.
|
|
507
|
+
*
|
|
508
|
+
* Note that this works synchronously, and can be easier to use than if promises are involved.
|
|
509
|
+
*
|
|
510
|
+
* As opposed to {@link module:ijs.useCache|ijs.useCache} - which works with promises.
|
|
511
|
+
*
|
|
512
|
+
* ```
|
|
513
|
+
* shouldWrite = true; /// we will write to the cache with the results from the execution
|
|
514
|
+
* expensiveResults = utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
|
|
515
|
+
* const data = d3.csvParse(utils.file.readFile('./someFile.csv'))
|
|
516
|
+
* .map(obj => ({ ...obj, date: Date.parse(obj.epoch) }));
|
|
517
|
+
*
|
|
518
|
+
* const earliestDate = utils.date.startOfDay( utils.agg.min(data, 'date') );
|
|
519
|
+
* const lastDate = utils.date.endOfDay( utils.agg.max(data, 'date') );
|
|
520
|
+
*
|
|
521
|
+
* // binning or lots of other things.
|
|
522
|
+
*
|
|
523
|
+
* return finalResults;
|
|
524
|
+
* });
|
|
525
|
+
*
|
|
526
|
+
* expensiveresults.length = 1023424;
|
|
527
|
+
* ```
|
|
528
|
+
*
|
|
529
|
+
* but sometimes I would rather just skip to the end
|
|
530
|
+
*
|
|
531
|
+
* ```
|
|
532
|
+
* shouldWrite = false; /// we will read from the cache instead,
|
|
533
|
+
* // everything else remains the same
|
|
534
|
+
*
|
|
535
|
+
* expensiveResults = utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
|
|
536
|
+
* const data = d3.csvParse(utils.file.readFile('./someFile.csv'))
|
|
537
|
+
* .map(obj => ({ ...obj, date: Date.parse(obj.epoch) }));
|
|
538
|
+
*
|
|
539
|
+
* //-- function can remain untouched,
|
|
540
|
+
* //-- BUT nothing in here will be executed
|
|
541
|
+
* //-- since we are reading from the cache
|
|
542
|
+
* });
|
|
543
|
+
*
|
|
544
|
+
* //-- completely transparent to the runner
|
|
545
|
+
* expensiveresults.length = 1023424;
|
|
546
|
+
* ```
|
|
547
|
+
*
|
|
548
|
+
* @param {Boolean} shouldWrite - whether we should write to the cache (true) or read from the cache (false)
|
|
549
|
+
* @param {String} cachePath - Path to the cache folder, ex: './cache'
|
|
550
|
+
* @param {String} cacheFile - Filename of the cache file to use for this execution, ex: 'ExecutionsPerMin.js'
|
|
551
|
+
* @param {Function} expensiveFn - function that returns the results to be stored in the cache
|
|
552
|
+
* @param {Object} fsOptions - options to use when writing or reading files
|
|
553
|
+
* @returns {any} - either the deserialized json from the cache or the results from the expensive function
|
|
554
|
+
* @see {@link module:file.readJSON|file.readJSON} - reads a local JSON file
|
|
555
|
+
* @see {@link module:file.writeJSON|file.writeJSON} - writes to a JSON file
|
|
556
|
+
* @see {@link module:ijs.useCache|ijs.useCache} - similar idea - but supports promises
|
|
557
|
+
*/
|
|
558
|
+
module.exports.useCache = function useCache(shouldWrite, cachePath, cacheFile, expensiveFn, fsOptions = null) {
|
|
559
|
+
const ensureEndsWithSlash = (str) => str.endsWith('/') ? str : `${str}/`;
|
|
560
|
+
const cacheFilePath = `${ensureEndsWithSlash(cachePath)}${cacheFile}`;
|
|
478
561
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
562
|
+
const cacheExists = FileUtil.fileExists(cacheFilePath);
|
|
563
|
+
|
|
564
|
+
if (cacheExists && !shouldWrite) {
|
|
565
|
+
const cleanOptions = { ...fsOptions, reviver: FileUtil.cacheDeserializer };
|
|
566
|
+
const results = FileUtil.readJSON(cacheFilePath, cleanOptions);
|
|
567
|
+
return results;
|
|
483
568
|
}
|
|
484
569
|
|
|
570
|
+
const results = expensiveFn();
|
|
571
|
+
|
|
572
|
+
const cleanOptions = fsOptions; // FileUtil.cacheSerializer not needed
|
|
573
|
+
FileUtil.writeJSON(cacheFilePath, results, cleanOptions);
|
|
574
|
+
|
|
485
575
|
return results;
|
|
486
576
|
};
|
|
487
|
-
*/
|
package/src/hashMap.js
CHANGED
|
@@ -83,12 +83,11 @@ module.exports.add = function add(map, key, value) {
|
|
|
83
83
|
* @param {any} functor.value - the first argument is the current value
|
|
84
84
|
* @param {any} functor.key - the second argument is the key passed
|
|
85
85
|
* @param {any} functor.map - the third argument is the map being acted upon
|
|
86
|
-
* @param {any} [setKey = null] - optional separate key to use to set the updated value
|
|
87
86
|
* @returns {Map}
|
|
88
87
|
*/
|
|
89
|
-
module.exports.getSet = function getSet(map, key, functor
|
|
90
|
-
const
|
|
91
|
-
map.set(
|
|
88
|
+
module.exports.getSet = function getSet(map, key, functor) {
|
|
89
|
+
const currentValue = map.has(key) ? map.get(key) : undefined;
|
|
90
|
+
map.set(key, functor(currentValue, key, map));
|
|
92
91
|
return map;
|
|
93
92
|
};
|
|
94
93
|
module.exports.update = module.exports.getSet;
|
package/src/ijs.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const uuid = require('uuid').v4;
|
|
4
4
|
|
|
5
|
+
const FileUtil = require('./file');
|
|
6
|
+
|
|
5
7
|
require('./_types/global');
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -24,9 +26,11 @@ require('./_types/global');
|
|
|
24
26
|
* * {@link module:ijs.markdown|ijs.markdown} - Render output as markdown
|
|
25
27
|
* * {@link module:ijs.htmlScript|ijs.htmlScript} - Leverage external libraries like D3, Leaflet, etc.
|
|
26
28
|
* * Printing
|
|
27
|
-
* * {@link module:ijs.
|
|
29
|
+
* * {@link module:ijs.clearOutput|ijs.clearOutput} - clears the output to declutter results (like importing libraries, or functions)
|
|
28
30
|
* * {@link module:ijs.initializePageBreaks|ijs.initializePageBreaks} - call at least once to allow pageBreaks when rendering PDFs
|
|
29
31
|
* * {@link module:ijs.printPageBreak|ijs.printPageBreak} - call to print a page break when rendering PDFs
|
|
32
|
+
* * using a cache for long running executions
|
|
33
|
+
* * {@link module:ijs.useCache|ijs.useCache()} - perform an expensive calculation and write to a cache, or read from the cache transparently
|
|
30
34
|
*
|
|
31
35
|
* For example:
|
|
32
36
|
*
|
|
@@ -588,7 +592,7 @@ module.exports.htmlScript = function htmlScripts(
|
|
|
588
592
|
* (This is useful to put after importing libraries,
|
|
589
593
|
* or defining a list of functions)
|
|
590
594
|
*/
|
|
591
|
-
module.exports.
|
|
595
|
+
module.exports.clearOutput = function clearOutput(outputText = '') {
|
|
592
596
|
//-- you must be in iJavaScript container to rendeer
|
|
593
597
|
const context = IJSUtils.detectContext();
|
|
594
598
|
|
|
@@ -598,7 +602,7 @@ module.exports.noOutputNeeded = function clearOutput(outputText = '') {
|
|
|
598
602
|
|
|
599
603
|
context.$$.text(outputText);
|
|
600
604
|
};
|
|
601
|
-
module.exports.
|
|
605
|
+
module.exports.noOutputNeeded = module.exports.clearOutput;
|
|
602
606
|
|
|
603
607
|
/**
|
|
604
608
|
* Required to be called first - in order to write page-breaks in the html results.
|
|
@@ -662,3 +666,102 @@ module.exports.printPageBreak = function printPageBreak() {
|
|
|
662
666
|
|
|
663
667
|
context.$$.html('<div class="pagebreak"></div>');
|
|
664
668
|
};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* For very long or time-intensive executions, sometimes it is better to cache the results
|
|
672
|
+
* than to execute them every single time.
|
|
673
|
+
*
|
|
674
|
+
* Note that this supports promises, and can be a bit harder to understand.
|
|
675
|
+
*
|
|
676
|
+
* As opposed to {@link module:file.useCache|file.useCache} - which works synchronously
|
|
677
|
+
*
|
|
678
|
+
* ```
|
|
679
|
+
* shouldWrite = true; /// we will write to the cache with the results from the execution
|
|
680
|
+
* utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
|
|
681
|
+
* //-- all the items to cache
|
|
682
|
+
* return Promise.resolve()
|
|
683
|
+
* .then(() => ajax.retrieve(...))
|
|
684
|
+
* .then((results) => {
|
|
685
|
+
* const data = results
|
|
686
|
+
* .map(obj => ({ ...obj, date: Date.parse(obj.epoch) }));
|
|
687
|
+
* .. other things to do
|
|
688
|
+
* return data;
|
|
689
|
+
* });
|
|
690
|
+
* })
|
|
691
|
+
* //-- using the information AFTER the expensive function or retrieval from cache
|
|
692
|
+
* //-- this can ALSO be run in a subsequence cell
|
|
693
|
+
* .then((results) => {
|
|
694
|
+
* expensiveresults = results;
|
|
695
|
+
* conosole.log(`expensiveResults.length: ${results.length}`);
|
|
696
|
+
* });
|
|
697
|
+
* ```
|
|
698
|
+
*
|
|
699
|
+
* but sometimes I would rather just skip to the end
|
|
700
|
+
*
|
|
701
|
+
* ```
|
|
702
|
+
* shouldWrite = false; /// we will read from the cache instead,
|
|
703
|
+
* // everything else remains the same
|
|
704
|
+
*
|
|
705
|
+
* utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
|
|
706
|
+
* //-- all the items to cache
|
|
707
|
+
* return Promise.resolve()
|
|
708
|
+
* ... blah blah - none of this will get executed
|
|
709
|
+
* })
|
|
710
|
+
* //-- using the information AFTER the expensive function or retrieval from cache
|
|
711
|
+
* //-- this can ALSO be run in a subsequence cell
|
|
712
|
+
* .then((results) => {
|
|
713
|
+
* expensiveresults = results;
|
|
714
|
+
* conosole.log(`expensiveResults.length: ${results.length}`);
|
|
715
|
+
* });
|
|
716
|
+
*
|
|
717
|
+
* //-- completely transparent to the runner
|
|
718
|
+
* expensiveresults.length = 1023424;
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @param {Boolean} shouldWrite - whether we should write to the cache (true) or read from the cache (false)
|
|
722
|
+
* @param {String} cachePath - Path to the cache folder, ex: './cache'
|
|
723
|
+
* @param {String} cacheFile - Filename of the cache file to use for this execution, ex: 'ExecutionsPerMin.js'
|
|
724
|
+
* @param {Function} expensiveFn - function that returns the results to be stored in the cache
|
|
725
|
+
* @param {Object} fsOptions - options to use when writing or reading files
|
|
726
|
+
* @returns {any} - either the deserialized json from the cache or the results from the expensive function
|
|
727
|
+
* @see {@link module:file.readJSON|file.readJSON} - reads a local JSON file
|
|
728
|
+
* @see {@link module:file.writeJSON|file.writeJSON} - writes to a JSON file
|
|
729
|
+
* @see {@link module:file.useCache|file.useCache} - can be much easier than using promises
|
|
730
|
+
*/
|
|
731
|
+
module.exports.useCache = async function useCache(shouldWrite, cachePath, cacheFile, expensiveFn, fsOptions = null) {
|
|
732
|
+
const ensureEndsWithSlash = (str) => str.endsWith('/') ? str : `${str}/`;
|
|
733
|
+
const cacheFilePath = `${ensureEndsWithSlash(cachePath)}${cacheFile}`;
|
|
734
|
+
const context = IJSUtils.detectContext();
|
|
735
|
+
|
|
736
|
+
if (!shouldWrite) {
|
|
737
|
+
const cleanOptions = { ...fsOptions, formatter: FileUtil.cacheDeserializer };
|
|
738
|
+
//-- we read the info and be done with it
|
|
739
|
+
//context.console.log(`before retrieving cache:${cacheFilePath}`);
|
|
740
|
+
const results = FileUtil.readJSON(cacheFilePath, cleanOptions);
|
|
741
|
+
//context.console.log(`after retrieving cache`);
|
|
742
|
+
return Promise.resolve(results);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (!context) {
|
|
746
|
+
throw (Error('IJSUtils.async must be run within iJavaScript. Otherwise, use normal async methods'));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
context.$$.async();
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
context.console.log('before starting expensive fn');
|
|
753
|
+
const results = await expensiveFn(context.$$, context.console);
|
|
754
|
+
context.console.log('AFTER starting expensive fn');
|
|
755
|
+
|
|
756
|
+
FileUtil.writeJSON(cacheFilePath, results);
|
|
757
|
+
// context.$$.sendResult('success');
|
|
758
|
+
return results;
|
|
759
|
+
} catch (err) {
|
|
760
|
+
context.console.error('error occurred');
|
|
761
|
+
context.console.error(err);
|
|
762
|
+
context.$$.sendResult(err);
|
|
763
|
+
|
|
764
|
+
// return Promise.reject(err);
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
};
|
package/src/object.js
CHANGED
|
@@ -26,6 +26,7 @@ const FormatUtils = require('./format');
|
|
|
26
26
|
* * {@link module:object.extractObjectProperties|extractObjectProperties(list, propertyNameOrFnMap)} - extracts multiple propertie or fn across all objects in list.
|
|
27
27
|
* * Apply deep values safely
|
|
28
28
|
* * {@link module:object.assign|objAssign(object, property, value)} - Applies properties to an object in functional programming style.
|
|
29
|
+
* * {@link module:object.getSet|getSet(object, property, functor)} - calls a function with the current value on an object, allowing for decrementing/incrementing/etc.
|
|
29
30
|
* * {@link module:object.augment|augment(object, augmentFn)} - Applies properties to an object similar to Map
|
|
30
31
|
* * {@link module:object.assignEntities|objAssignEntities(object, [property, value])} - Applies properties to an object using Array values - [key,value]
|
|
31
32
|
* * {@link module:object.setPropertyDefaults|setPropertyDefaults()} - sets values for objects that don't currently have the property
|
|
@@ -50,13 +51,14 @@ const FormatUtils = require('./format');
|
|
|
50
51
|
* * {@link module:object.flatten|flatten()} - creates dot notation properties (similar to arrow notation) of all child objects.
|
|
51
52
|
* * {@link module:object.expand|expand()} - expands dot notation properties onto sub children (inverse of flatten)
|
|
52
53
|
* * Create Map of objects by key
|
|
53
|
-
* * {@link module:object.mapByProperty|mapByProperty()}
|
|
54
|
-
* * {@link module:group.by|group(collection, accessor)}
|
|
54
|
+
* * {@link module:object.mapByProperty|mapByProperty()}
|
|
55
|
+
* * {@link module:group.by|group.by(collection, accessor)}
|
|
55
56
|
* * Convert collections of objects
|
|
56
57
|
* * {@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
|
|
58
|
+
* * {@link module:object.objectCollectionToArray|objectCollectionToArray} - convert objects to a rows/columns 2d array
|
|
59
|
+
* * {@link module:object.objectCollectionFromDataFrameObject|objectCollectionFromDataFrameObject} - convert tensor object with each field as 1d array of values
|
|
60
|
+
* * {@link module:object.objectCollectionToDataFrameObject|objectCollectionToDataFrameObject} - convert objects from a tensor object
|
|
61
|
+
* * {@link module:object.splitIntoDatums|splitIntoDatums(object, fieldsToSplitBy)} - separate objects into series by fields
|
|
60
62
|
*
|
|
61
63
|
* @module object
|
|
62
64
|
* @exports object
|
|
@@ -169,6 +171,53 @@ module.exports.assignEntities = function objAssignEntities(obj, entities) {
|
|
|
169
171
|
};
|
|
170
172
|
module.exports.objAssignEntities = module.exports.assignEntities;
|
|
171
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Use this for times where you want to update a value
|
|
176
|
+
*
|
|
177
|
+
*
|
|
178
|
+
* ```
|
|
179
|
+
* key = 'somethingToIncrement';
|
|
180
|
+
* defaultValue = null;
|
|
181
|
+
*
|
|
182
|
+
* const initialObject = {};
|
|
183
|
+
* initialObject[key] = defaultValue;
|
|
184
|
+
*
|
|
185
|
+
* // { somethingToIncrement: null }
|
|
186
|
+
*
|
|
187
|
+
* const functor = (value) => { //, key, map) => {
|
|
188
|
+
* if (!value) return 1;
|
|
189
|
+
* return value + 1;
|
|
190
|
+
* };
|
|
191
|
+
*
|
|
192
|
+
* utils.object.getSet(initialObject, key, functor);
|
|
193
|
+
*
|
|
194
|
+
* // equivalent to
|
|
195
|
+
* // intitialObject[key] = intitialObject[key] ? initialObject[key] + 1 : 1`
|
|
196
|
+
* // but in a way that is repeatable across many values
|
|
197
|
+
*
|
|
198
|
+
* utils.object.getSet(initialObject, key, functor);
|
|
199
|
+
* utils.object.getSet(initialObject, key, functor);
|
|
200
|
+
* utils.object.getSet(initialObject, key, functor);
|
|
201
|
+
* utils.object.getSet(initialObject, key, functor);
|
|
202
|
+
*
|
|
203
|
+
* initialObject.get(key); // 5
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @param {Map} map - map to get and set values from
|
|
207
|
+
* @param {any} key - they key to GET and SET the value (unless setKey is provided)
|
|
208
|
+
* @param {Function} functor - the function called with the arguments below - returning the value to set
|
|
209
|
+
* @param {any} functor.value - the first argument is the current value
|
|
210
|
+
* @param {any} functor.key - the second argument is the key passed
|
|
211
|
+
* @param {any} functor.map - the third argument is the map being acted upon
|
|
212
|
+
* @returns {Map}
|
|
213
|
+
*/
|
|
214
|
+
module.exports.getSet = function getSet(obj, field, functor) {
|
|
215
|
+
const currentValue = Object.hasOwn(obj, field) ? obj[field] : undefined;
|
|
216
|
+
obj[field] = functor(currentValue, field, obj);
|
|
217
|
+
return obj;
|
|
218
|
+
};
|
|
219
|
+
module.exports.update = module.exports.getSet;
|
|
220
|
+
|
|
172
221
|
/**
|
|
173
222
|
* Runs a map over a collection, and adds properties the the objects.
|
|
174
223
|
*
|
|
@@ -283,6 +332,78 @@ module.exports.keys = function keys(objOrArray = {}, maxRows = -1) {
|
|
|
283
332
|
return Array.from(result);
|
|
284
333
|
};
|
|
285
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Identifies which keys provided are also in objOrArray.
|
|
337
|
+
*
|
|
338
|
+
* ```
|
|
339
|
+
* dataSet = [
|
|
340
|
+
* { first: 'john', last: 'McCartney' },
|
|
341
|
+
* { first: 'ringo', last: 'Starr' }
|
|
342
|
+
* ];
|
|
343
|
+
*
|
|
344
|
+
* utils.object.keysWithinList(dataSet, 'first', 'last', 'favouriteColor');
|
|
345
|
+
* // ['first', 'last'] // no favouriteColor defined
|
|
346
|
+
* ```
|
|
347
|
+
*
|
|
348
|
+
* Note you can also pass the list of keys as an array in the first argument
|
|
349
|
+
*
|
|
350
|
+
* ```
|
|
351
|
+
* fieldsToCheck = ['first', 'last', 'favouriteColor'];
|
|
352
|
+
* utils.object.keysWithinList(dataSet, fieldsToCheck);
|
|
353
|
+
* // ['first', 'last']
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* @param {Object|Object[]} objOrArray - object or list of objects to identify the keys they have
|
|
357
|
+
* @param {...String} listOfKeys - a list of keys to check if they are defined within objOrArray
|
|
358
|
+
* @returns {String[]} - list of keys that are both in the objOrArray or within listOfKeys
|
|
359
|
+
*/
|
|
360
|
+
module.exports.keysWithinList = function keysWithinList(objOrArray, ...listOfKeys) {
|
|
361
|
+
const cleanListOfKeys = listOfKeys.length > 0 && Array.isArray(listOfKeys[0])
|
|
362
|
+
? listOfKeys[0]
|
|
363
|
+
: listOfKeys;
|
|
364
|
+
|
|
365
|
+
const keySet = new Set(ObjectUtils.keys(objOrArray));
|
|
366
|
+
const keyIntersection = cleanListOfKeys.filter((keyToTest) => keySet.has(keyToTest));
|
|
367
|
+
return keyIntersection;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Identifies which other keys are defined that are not in the list provided.
|
|
372
|
+
*
|
|
373
|
+
* This is quite helpful for dynamic APIs.
|
|
374
|
+
*
|
|
375
|
+
* ```
|
|
376
|
+
* dataSet = [
|
|
377
|
+
* { first: 'john', last: 'McCartney', favouriteColor: 'blue' },
|
|
378
|
+
* { first: 'ringo', last: 'Starr', favouriteColor: 'red' }
|
|
379
|
+
* ];
|
|
380
|
+
*
|
|
381
|
+
* utils.object.keysNotInList(dataSet, 'first', 'last', 'favouriteColor');
|
|
382
|
+
* // ['favouriteColor']
|
|
383
|
+
* ```
|
|
384
|
+
*
|
|
385
|
+
* Note you can also pass the list of keys as an array in the first argument
|
|
386
|
+
*
|
|
387
|
+
* ```
|
|
388
|
+
* fieldsToCheck = ['first', 'last', 'favouriteColor'];
|
|
389
|
+
* utils.object.keysNotInList(dataSet, fieldsToCheck);
|
|
390
|
+
* // ['favouriteColor']
|
|
391
|
+
* ```
|
|
392
|
+
*
|
|
393
|
+
* @param {Object|Object[]} objOrArray - object or list of objects to identify the keys they have
|
|
394
|
+
* @param {...String} listOfKeys - a list of keys to check if they are defined within objOrArray
|
|
395
|
+
* @returns {String[]} - list of keys that are both in the objOrArray or within listOfKeys
|
|
396
|
+
*/
|
|
397
|
+
module.exports.keysNotInList = function keysNotInList(objOrArray, ...listOfKeys) {
|
|
398
|
+
const setOfKeysToCheck = listOfKeys.length > 0 && Array.isArray(listOfKeys[0])
|
|
399
|
+
? new Set(listOfKeys[0])
|
|
400
|
+
: new Set(listOfKeys);
|
|
401
|
+
|
|
402
|
+
const keys = ObjectUtils.keys(objOrArray);
|
|
403
|
+
const keyIntersection = keys.filter((keyToTest) => !setOfKeysToCheck.has(keyToTest));
|
|
404
|
+
return keyIntersection;
|
|
405
|
+
};
|
|
406
|
+
|
|
286
407
|
/**
|
|
287
408
|
* Cleans all the properties of the array of objects in place (does not make Copies)
|
|
288
409
|
*
|
|
@@ -2122,3 +2243,92 @@ module.exports.objectCollectionToDataFrameObject = function objectCollectionToDa
|
|
|
2122
2243
|
});
|
|
2123
2244
|
return dataFrameObject;
|
|
2124
2245
|
};
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* Some charting software (such as vega-lite) does not allow a single object to be used
|
|
2249
|
+
* for multiple line series.
|
|
2250
|
+
*
|
|
2251
|
+
* This is intended to help with that.
|
|
2252
|
+
*
|
|
2253
|
+
* ```
|
|
2254
|
+
* [
|
|
2255
|
+
* { category: 'A', source: 'chicago', x: 0.1, y: 0.6, z: 0.9 },
|
|
2256
|
+
* { category: 'B', source: 'springfield', x: 0.7, y: 0.2, z: 1.1 },
|
|
2257
|
+
* { category: 'C', source: 'winnetka', x: 0.6, y: 0.1, z: 0.2 }
|
|
2258
|
+
* ]
|
|
2259
|
+
* ```
|
|
2260
|
+
*
|
|
2261
|
+
* must have a separate object for each x, y and z field for the A category.
|
|
2262
|
+
*
|
|
2263
|
+
* ```
|
|
2264
|
+
* utils.object.splitIntoDatums(category, ['x', 'y', 'z']);
|
|
2265
|
+
* [
|
|
2266
|
+
* { category: 'A', source: 'chicago', series: 'x', value: 0.1 },
|
|
2267
|
+
* { category: 'A', source: 'chicago', series: 'y', value: 0.6 },
|
|
2268
|
+
* { category: 'A', source: 'chicago', series: 'z', value: 0.9 },
|
|
2269
|
+
* { category: 'B', source: 'springfield', series: 'x', value: 0.7 },
|
|
2270
|
+
* { category: 'B', source: 'springfield', series: 'y', value: 0.2 },
|
|
2271
|
+
* { category: 'B', source: 'springfield', series: 'z', value: 1.1 },
|
|
2272
|
+
* { category: 'C', source: 'winnetka', series: 'x', value: 0.6 },
|
|
2273
|
+
* { category: 'C', source: 'winnetka', series: 'y', value: 0.1 },
|
|
2274
|
+
* { category: 'C', source: 'winnetka', series: 'z', value: 0.2 }
|
|
2275
|
+
* ]
|
|
2276
|
+
* ```
|
|
2277
|
+
*
|
|
2278
|
+
* note that the fields NOT within the list of fields specified, are preserved
|
|
2279
|
+
* in the denormalized objects...
|
|
2280
|
+
*
|
|
2281
|
+
* while the fields listed are put into separate objects.
|
|
2282
|
+
*
|
|
2283
|
+
* You can specify which fields that are generated in those new objects
|
|
2284
|
+
*
|
|
2285
|
+
* ```
|
|
2286
|
+
* utils.object.splitIntoDatums(category, ['x', 'y', 'z'], 'group', 'val');
|
|
2287
|
+
* [
|
|
2288
|
+
* { category: 'A', source: 'chicago', group: 'x', val: 0.1 },
|
|
2289
|
+
* { category: 'A', source: 'chicago', group: 'y', val: 0.6 },
|
|
2290
|
+
* { category: 'A', source: 'chicago', group: 'z', val: 0.9 },
|
|
2291
|
+
* { category: 'B', source: 'springfield', group: 'x', val: 0.7 },
|
|
2292
|
+
* { category: 'B', source: 'springfield', group: 'y', val: 0.2 },
|
|
2293
|
+
* { category: 'B', source: 'springfield', group: 'z', val: 1.1 },
|
|
2294
|
+
* { category: 'C', source: 'winnetka', group: 'x', val: 0.6 },
|
|
2295
|
+
* { category: 'C', source: 'winnetka', group: 'y', val: 0.1 },
|
|
2296
|
+
* { category: 'C', source: 'winnetka', group: 'z', val: 0.2 }
|
|
2297
|
+
* ]
|
|
2298
|
+
* ```
|
|
2299
|
+
*
|
|
2300
|
+
* @param {Object[]} objectCollection - collection
|
|
2301
|
+
* @param {String[]} keysForEachSeries - fields that will each be a series spread across datum records
|
|
2302
|
+
* @param {String} [seriesFieldName='series'] - the name of the field to indicate which series this datum is in
|
|
2303
|
+
* @param {String} [valueFieldName='value'] - the name of the value for that datum
|
|
2304
|
+
* @returns {Object} - list of objects x keysForEachSeries.length, where field within keysForEachSeries
|
|
2305
|
+
* are then individually assigned to a value, and series - to indicate which field is used.
|
|
2306
|
+
*/
|
|
2307
|
+
module.exports.splitIntoDatums = function splitIntoDatums(
|
|
2308
|
+
collection,
|
|
2309
|
+
keysToSplit,
|
|
2310
|
+
seriesFieldName = 'series',
|
|
2311
|
+
valueFieldName = 'value'
|
|
2312
|
+
) {
|
|
2313
|
+
const keysToKeep = ObjectUtils.keysNotInList(collection, keysToSplit);
|
|
2314
|
+
const keysToSeparate = ObjectUtils.keysWithinList(collection, keysToSplit);
|
|
2315
|
+
const rowLength = keysToSeparate.length;
|
|
2316
|
+
|
|
2317
|
+
const results = new Array(collection.length * rowLength).fill(null);
|
|
2318
|
+
collection.forEach((obj, objIndex) => {
|
|
2319
|
+
const basis = {};
|
|
2320
|
+
keysToKeep.forEach((key) => {
|
|
2321
|
+
basis[key] = obj[key];
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
keysToSeparate.forEach((key, keyIndex) => {
|
|
2325
|
+
const newRecord = { ...basis };
|
|
2326
|
+
newRecord[seriesFieldName] = key;
|
|
2327
|
+
newRecord[valueFieldName] = obj[key];
|
|
2328
|
+
results[objIndex * rowLength + keyIndex] = newRecord;
|
|
2329
|
+
});
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// return ({keysForSplitting, keysForKeeping});
|
|
2333
|
+
return results;
|
|
2334
|
+
};
|