jupyter-ijavascript-utils 1.56.0 → 1.57.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,6 +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
+ * 1.57 - #86 - include examples on binning based on time series {@link https://github.com/paulroth3d/jupyter-ijavascript-utils/issues/86|see more here}
78
+ * #87 - add in {@link module:file.fileExists|file.fileExists} to make things easier for new people getting started
79
+ * #89 - allow {@link module:array.resize|array.resize} to work with defaults, if zipping arrays of different sizes
80
+ * #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}
81
+ * #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
82
  * 1.56 - #84 (object.renamePropertiesFromList), #82 (date.getWeekday)
78
83
  * 1.55 - #76, #77, #74, #78
79
84
  * 1.54 - additional Date logic, and formatting. #70 #71 #72
package/Dockerfile CHANGED
@@ -1,3 +1,3 @@
1
1
  # syntax=docker/dockerfile:1
2
2
 
3
- FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.55.0
3
+ FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.57.0
package/README.md CHANGED
@@ -54,6 +54,11 @@ 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.57 - #86 - include examples on binning based on time series
58
+ * #87 - add in fileExists to make things easier for new people getting started
59
+ * #89 - allow resizing arrays with defaults, if zipping arrays of different sizes
60
+ * #90 - correct issue with timezone offsets, so it is no longer possible to get a timezone offset +24:00
61
+ * #92 - make it easier to handle big expensive calculations with a cache
57
62
  * 1.56 - #84 (object.renamePropertiesFromList), #82 (date.getWeekday)
58
63
  * 1.55 - #76, #77, #74, #78
59
64
  * 1.54 - additional Date logic, and formatting. #70 #71 #72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.56.0",
3
+ "version": "1.57.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",
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
- * 'rock', 'paper', 'scissors', 'rock];
1463
+ * undefined, undefined, undefined, undefined];
1464
1464
  * ```
1465
1465
  *
1466
1466
  * @param {Array} sourceList - array of values
1467
- * @param {Number} length - new number of items in the list
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
- return new Array(length)
1473
- .fill(0)
1474
- .map((_, index) => sourceList[index % sourceList.length]);
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
@@ -266,7 +266,8 @@ module.exports.getTimezoneEntry = function getTimezoneEntry(timezoneStr) {
266
266
 
267
267
  const impactedDate = getOffset(d);
268
268
 
269
- const diff = d.getTime() - impactedDate.getTime();
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.formatter - formatter 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 },
@@ -117,7 +122,11 @@ const FileUtil = module.exports;
117
122
  module.exports.readJSON = function readJSON(filePath, fsOptions = {}) {
118
123
  const resolvedPath = path.resolve(filePath);
119
124
  const optionsDefaults = { encoding: 'utf-8' };
120
- const cleanedOptions = { ...optionsDefaults, ...fsOptions };
125
+ let cleanedOptions = { ...optionsDefaults, ...fsOptions };
126
+
127
+ //-- unfortunately we cannot pass the formatter in addition, it must replace
128
+ //-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
129
+ if (cleanedOptions.formatter) cleanedOptions = cleanedOptions.formatter;
121
130
 
122
131
  /** @type {string} */
123
132
  let result;
@@ -227,12 +236,14 @@ module.exports.readFile = function readFile(filePath, fsOptions = {}) {
227
236
  */
228
237
  module.exports.writeJSON = function writeJSON(filePath, contents, fsOptions = {}) {
229
238
  //-- if it isn't desired, simply pass as a string.
230
- const jsonContents = JSON.stringify(contents, null, 2);
231
239
  const optionsDefaults = { encoding: 'utf-8' };
232
240
  const cleanedOptions = { ...optionsDefaults, ...fsOptions };
233
241
  const isAppend = cleanedOptions.append === true;
234
242
  const prefix = cleanedOptions.prefix || '';
235
243
  const suffix = cleanedOptions.suffix || '';
244
+ const formatter = cleanedOptions.formatter || null;
245
+ const spacing = cleanedOptions.spacing || 2;
246
+ const jsonContents = JSON.stringify(contents, formatter, spacing);
236
247
 
237
248
  // const resolvedPath = path.resolve(filePath);
238
249
  try {
@@ -465,23 +476,103 @@ module.exports.checkFile = function checkFile(...files) {
465
476
  return notFoundFiles;
466
477
  };
467
478
 
468
- /*
469
- * Execute an async function if any of the files do not exist
470
- * @param {String[]} filePaths - list of paths of files to check that they exist
471
- * @param {*} fnIfFailed - async function tha will run - but only if any of the files are not found.
479
+ /**
480
+ * Checks if a single file exists
481
+ * @param {String} filePath - path to check if the file exists.
482
+ * @returns {Boolean} - if the file exists (true) or not (false)
483
+ * @see {@link module:file.checkFile|file.checkFile} - if checking multiple files
472
484
  */
485
+ module.exports.fileExists = function fileExists(filePath) {
486
+ const resolvedPath = path.resolve(filePath);
487
+ return fs.existsSync(resolvedPath);
488
+ };
489
+
473
490
  /*
474
- module.exports.ifNotExists = async function ifNotExists(filePaths, fnIfFailed) {
475
- const filesNotFound = FileUtil.checkFile(filePaths);
491
+ //-- not needed - dates already serialize to iso Strings
492
+ module.exports.cacheSerializer = (key, value) => {
493
+ if (key && (key === 'date' || key.endsWith('_date')) && (value instanceof Date)) {
494
+ return value.toISOString();
495
+ }
496
+ return value;
497
+ };
498
+ */
476
499
 
477
- let results;
500
+ module.exports.cacheDeserializer = (key, value) => {
501
+ if (key && (key === 'date' || key.endsWith('_date'))) {
502
+ return new Date(value);
503
+ }
504
+ return value;
505
+ };
478
506
 
479
- if (filesNotFound) {
480
- results = await fnIfFailed(filesNotFound);
481
- } else {
482
- results = null;
507
+ /**
508
+ * For very long or time-intensive executions, sometimes it is better to cache the results
509
+ * than to execute them every single time.
510
+ *
511
+ * Note that this works synchronously, and can be easier to use than if promises are involved.
512
+ *
513
+ * As opposed to {@link module:ijs.useCache|ijs.useCache} - which works with promises.
514
+ *
515
+ * ```
516
+ * shouldWrite = true; /// we will write to the cache with the results from the execution
517
+ * expensiveResults = utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
518
+ * const data = d3.csvParse(utils.file.readFile('./someFile.csv'))
519
+ * .map(obj => ({ ...obj, date: Date.parse(obj.epoch) }));
520
+ *
521
+ * const earliestDate = utils.date.startOfDay( utils.agg.min(data, 'date') );
522
+ * const lastDate = utils.date.endOfDay( utils.agg.max(data, 'date') );
523
+ *
524
+ * // binning or lots of other things.
525
+ *
526
+ * return finalResults;
527
+ * });
528
+ *
529
+ * expensiveresults.length = 1023424;
530
+ * ```
531
+ *
532
+ * but sometimes I would rather just skip to the end
533
+ *
534
+ * ```
535
+ * shouldWrite = false; /// we will read from the cache instead,
536
+ * // everything else remains the same
537
+ *
538
+ * expensiveResults = utils.file.useCache(shouldWrite, './cache', 'expensive.json', () => {
539
+ * const data = d3.csvParse(utils.file.readFile('./someFile.csv'))
540
+ * .map(obj => ({ ...obj, date: Date.parse(obj.epoch) }));
541
+ *
542
+ * //-- function can remain untouched,
543
+ * //-- BUT nothing in here will be executed
544
+ * //-- since we are reading from the cache
545
+ * });
546
+ *
547
+ * //-- completely transparent to the runner
548
+ * expensiveresults.length = 1023424;
549
+ * ```
550
+ *
551
+ * @param {Boolean} shouldWrite - whether we should write to the cache (true) or read from the cache (false)
552
+ * @param {String} cachePath - Path to the cache folder, ex: './cache'
553
+ * @param {String} cacheFile - Filename of the cache file to use for this execution, ex: 'ExecutionsPerMin.js'
554
+ * @param {Function} expensiveFn - function that returns the results to be stored in the cache
555
+ * @param {Object} fsOptions - options to use when writing or reading files
556
+ * @returns {any} - either the deserialized json from the cache or the results from the expensive function
557
+ * @see {@link module:file.readJSON|file.readJSON} - reads a local JSON file
558
+ * @see {@link module:file.writeJSON|file.writeJSON} - writes to a JSON file
559
+ * @see {@link module:ijs.useCache|ijs.useCache} - similar idea - but supports promises
560
+ */
561
+ module.exports.useCache = function useCache(shouldWrite, cachePath, cacheFile, expensiveFn, fsOptions = null) {
562
+ const ensureEndsWithSlash = (str) => str.endsWith('/') ? str : `${str}/`;
563
+ const cacheFilePath = `${ensureEndsWithSlash(cachePath)}${cacheFile}`;
564
+
565
+ if (!shouldWrite) {
566
+ const cleanOptions = { ...fsOptions, formatter: FileUtil.cacheDeserializer };
567
+ const results = FileUtil.readJSON(cacheFilePath, cleanOptions);
568
+ return results;
483
569
  }
484
570
 
571
+ const results = expensiveFn();
572
+
573
+ const cleanOptions = { ...fsOptions, formatter: null }; // FileUtil.cacheSerializer not needed
574
+
575
+ FileUtil.writeJSON(cacheFilePath, results, cleanOptions);
576
+
485
577
  return results;
486
578
  };
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, setKey = null) {
90
- const cleanSetKey = setKey || key;
91
- map.set(cleanSetKey, functor(map.get(key), key, map));
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
  /**
@@ -27,6 +29,8 @@ require('./_types/global');
27
29
  * * {@link module:ijs.noOutputNeeded|ijs.noOutputNeeded} - 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
  *
@@ -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
@@ -169,6 +170,53 @@ module.exports.assignEntities = function objAssignEntities(obj, entities) {
169
170
  };
170
171
  module.exports.objAssignEntities = module.exports.assignEntities;
171
172
 
173
+ /**
174
+ * Use this for times where you want to update a value
175
+ *
176
+ *
177
+ * ```
178
+ * key = 'somethingToIncrement';
179
+ * defaultValue = null;
180
+ *
181
+ * const initialObject = {};
182
+ * initialObject[key] = defaultValue;
183
+ *
184
+ * // { somethingToIncrement: null }
185
+ *
186
+ * const functor = (value) => { //, key, map) => {
187
+ * if (!value) return 1;
188
+ * return value + 1;
189
+ * };
190
+ *
191
+ * utils.object.getSet(initialObject, key, functor);
192
+ *
193
+ * // equivalent to
194
+ * // intitialObject[key] = intitialObject[key] ? initialObject[key] + 1 : 1`
195
+ * // but in a way that is repeatable across many values
196
+ *
197
+ * utils.object.getSet(initialObject, key, functor);
198
+ * utils.object.getSet(initialObject, key, functor);
199
+ * utils.object.getSet(initialObject, key, functor);
200
+ * utils.object.getSet(initialObject, key, functor);
201
+ *
202
+ * initialObject.get(key); // 5
203
+ * ```
204
+ *
205
+ * @param {Map} map - map to get and set values from
206
+ * @param {any} key - they key to GET and SET the value (unless setKey is provided)
207
+ * @param {Function} functor - the function called with the arguments below - returning the value to set
208
+ * @param {any} functor.value - the first argument is the current value
209
+ * @param {any} functor.key - the second argument is the key passed
210
+ * @param {any} functor.map - the third argument is the map being acted upon
211
+ * @returns {Map}
212
+ */
213
+ module.exports.getSet = function getSet(obj, field, functor) {
214
+ const currentValue = Object.hasOwn(obj, field) ? obj[field] : undefined;
215
+ obj[field] = functor(currentValue, field, obj);
216
+ return obj;
217
+ };
218
+ module.exports.update = module.exports.getSet;
219
+
172
220
  /**
173
221
  * Runs a map over a collection, and adds properties the the objects.
174
222
  *