jupyter-ijavascript-utils 1.50.0 → 1.51.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/Dockerfile CHANGED
@@ -1,3 +1,3 @@
1
1
  # syntax=docker/dockerfile:1
2
2
 
3
- FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.50.0
3
+ FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.51.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.50.0",
3
+ "version": "1.51.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
@@ -13,6 +13,7 @@ require('./_types/global');
13
13
  * * {@link module:array.arrange|array.arrange(size, start, step)} - generate array of a size, and INCREASING default value
14
14
  * * {@link module:array.arrangeMulti|array.arrangeMulti(n, m, ...)} - generate a multi-dimensional array
15
15
  * * {@link module:array.clone|array.clone(array)} - deep clones arrays
16
+ * * {@link module:array.zip|array.zip(arrayleft, arrayRight)} - zips two arrays to join values at the same index together.
16
17
  * * Sorting
17
18
  * * {@link module:array.createSort|array.createSort(sortIndex, sortIndex, ...)} - generates a sorting function
18
19
  * * {@link module:array.SORT_ASCENDING|array.SORT_ASCENDING} - common ascending sorting function for array.sort()
@@ -21,6 +22,7 @@ require('./_types/global');
21
22
  * * Rearrange Array
22
23
  * * {@link module:array.reshape|array.reshape} - reshapes an array to a size of rows and columns
23
24
  * * {@link module:array.transpose|array.transpose} - transposes (flips - the array along the diagonal)
25
+ * * {@link module:array.resize|array.resize} - repeats or truncates to change the size of an array.
24
26
  * * Picking Values
25
27
  * * {@link module:array.peekFirst|array.peekFirst} - peeks at the first value in the list
26
28
  * * {@link module:array.peekLast|array.peekLast} - peeks at the last value in the list
@@ -36,6 +38,8 @@ require('./_types/global');
36
38
  * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring|Substring}
37
39
  * from a multi-line string or array of strings
38
40
  * * {@link module:array.multiStepReduce|array.multiStepReduce} - Performs reduce, and returns the value of reduce at each step
41
+ * * {@link module:array.extractFromHardSpacedTable|array.extractFromHardSpacedTable} - Extract values where each line has no delimiter,
42
+ * but instead a column index (ex: column 13)
39
43
  * * Applying a value
40
44
  * * {@link module:array.applyArrayValue|array.applyArrayValue} - applies a value deeply into an array safely
41
45
  * * {@link module:array.applyArrayValues|array.applyArrayValues} - applies a value / multiple values deeply into an array safely
@@ -607,8 +611,8 @@ module.exports.transpose = function transpose(matrix) {
607
611
  };
608
612
 
609
613
  /**
610
- * Resizes an NxM dimensional array by number of columns
611
- * @param {any[]} sourceArray - an array to resize
614
+ * Re-shapes an NxM dimensional array by number of columns
615
+ * @param {any[]} sourceArray - an array to reshape
612
616
  * @param {Number} numColumns - number of columns
613
617
  * @returns {any[][]} - 2 dimensinal array
614
618
  * @example
@@ -618,14 +622,14 @@ module.exports.transpose = function transpose(matrix) {
618
622
  * 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
619
623
  * ]
620
624
  *
621
- * //-- resize the 1d array based on 3 columns
625
+ * //-- reshape the 1d array based on 3 columns
622
626
  * newArray = utils.array.reshape(baseArray, 3)
623
627
  * [ [ 0, 1, 2 ],
624
628
  * [ 3, 4, 5 ],
625
629
  * [ 6, 7, 8 ],
626
630
  * [ 9, 10, 11 ] ];
627
631
  *
628
- * //-- now resize the 4x3 array to 3x4
632
+ * //-- now reshape the 4x3 array to 3x4
629
633
  * utils.array.reshape(newArray, 4);
630
634
  * [ [ 0, 1, 2, 3 ],
631
635
  * [ 4, 5, 6, 7 ],
@@ -1447,3 +1451,118 @@ module.exports.asyncWaitAndChain = (seconds, fn, rows) => {
1447
1451
  return callNext();
1448
1452
  });
1449
1453
  };
1454
+
1455
+ /**
1456
+ * Resizes an array - if shorter (truncates), if longer cycles values.
1457
+ *
1458
+ * ```
1459
+ * categoryValues = ['rock', 'paper', 'scissors'];
1460
+ *
1461
+ * utils.array.resize(categoryValues, 2); // ['rock', 'paper']
1462
+ * utils.array.resize(categoryValues, 7); // ['rock', 'paper', 'scissors',
1463
+ * 'rock', 'paper', 'scissors', 'rock];
1464
+ * ```
1465
+ *
1466
+ * @param {Array} sourceList - array of values
1467
+ * @param {Number} length - new number of items in the list
1468
+ */
1469
+ module.exports.resize = function resize(sourceList, length) {
1470
+ if (!sourceList || !Array.isArray(sourceList)) return [];
1471
+ if (length < 1 || sourceList.length < 1) return [];
1472
+ return new Array(length)
1473
+ .fill(0)
1474
+ .map((_, index) => sourceList[index % sourceList.length]);
1475
+ };
1476
+
1477
+ /**
1478
+ * Combines arrays together by joining the values at the same index.
1479
+ *
1480
+ * Similar to Panda's zip
1481
+ *
1482
+ * This can be very helpful for joining multiple value lists.
1483
+ *
1484
+ * ```
1485
+ * first = ['john', 'paul', 'george', 'ringo'];
1486
+ * last = ['lennon', 'mccartney', 'harrison', 'starr'];
1487
+ * phrase = ['imagine', 'yesterday', 'taxman', 'walrus'];
1488
+ *
1489
+ * names = utils.array.zip(first, last);
1490
+ * // [['john', 'lennon'], ['paul', 'mccartney'],
1491
+ * // ['george', 'harrison'], ['ringo', 'starr']];
1492
+ * ```
1493
+ *
1494
+ * You can also zip together existing arrays
1495
+ *
1496
+ * ```
1497
+ * utils.array.zip(names, phrase);
1498
+ * // [['john', 'lennon', 'imagine'],
1499
+ * // ['paul', 'mccartney', 'yesterday'],
1500
+ * // ['george', 'harrison', 'taxman'],
1501
+ * // ['ringo', 'starr', 'walrus']]
1502
+ * ```
1503
+ *
1504
+ * or you can zip them together all at once
1505
+ *
1506
+ * ```
1507
+ * utils.array.zip(first, last, phrase);
1508
+ * // [['john', 'lennon', 'imagine'],
1509
+ * // ['paul', 'mccartney', 'yesterday'],
1510
+ * // ['george', 'harrison', 'taxman'],
1511
+ * // ['ringo', 'starr', 'walrus']]
1512
+ * ```
1513
+ *
1514
+ * @param {Array} arrayLeft - one array to combine with the array on the right
1515
+ * @param {Array} arrayRight - another array to combine at the same indices on the left
1516
+ * @param {...any} rest - additional arrays to combine
1517
+ * @returns {Array<Array>}
1518
+ */
1519
+ module.exports.zip = function zip(arrayLeft, arrayRight, ...rest) {
1520
+ if (!arrayLeft || !arrayLeft[Symbol.iterator]) {
1521
+ throw new Error('zip: left must be iterable');
1522
+ }
1523
+ if (!arrayRight || !arrayRight[Symbol.iterator]) {
1524
+ throw new Error('zip: right must be iterable');
1525
+ }
1526
+
1527
+ const cleanLeft = Array.isArray(arrayLeft) ? arrayLeft : [...arrayLeft];
1528
+ const cleanRight = Array.isArray(arrayRight) ? arrayRight : [...arrayRight];
1529
+
1530
+ let result;
1531
+
1532
+ if (cleanLeft.length === 0 && cleanRight.length === 0) {
1533
+ result = [[]];
1534
+ } else if (cleanLeft.length === 0) {
1535
+ result = cleanRight.map((val) => Array.isArray(val) ? val : [val]);
1536
+ } else if (cleanRight.length === 0) {
1537
+ result = cleanLeft.map((val) => Array.isArray(val) ? val : [val]);
1538
+ } else {
1539
+ const cleanLeftLen = cleanLeft.length;
1540
+ const cleanRightLen = cleanRight.length;
1541
+ const zipLen = Math.min(cleanLeftLen, cleanRightLen);
1542
+
1543
+ result = new Array(zipLen).fill(0);
1544
+
1545
+ for (let i = 0; i < zipLen; i += 1) {
1546
+ const leftVal = cleanLeft[i];
1547
+ const rightVal = cleanRight[i];
1548
+ const leftValArray = Array.isArray(leftVal);
1549
+ const rightValArray = Array.isArray(rightVal);
1550
+ if (leftValArray && rightValArray) {
1551
+ result[i] = [...leftVal, ...rightVal];
1552
+ } else if (leftValArray) {
1553
+ result[i] = [...leftVal, rightVal];
1554
+ } else if (rightValArray) {
1555
+ result[i] = [leftVal, ...rightVal];
1556
+ } else {
1557
+ result[i] = [leftVal, rightVal];
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ if (rest && rest.length > 0) {
1563
+ const [newRight, ...newRest] = rest;
1564
+ result = ArrayUtils.zip(result, newRight, ...newRest);
1565
+ }
1566
+
1567
+ return result;
1568
+ };
package/src/color.js CHANGED
@@ -21,6 +21,8 @@
21
21
  *
22
22
  * See other common libraries for working with color on NPM:
23
23
  * like [d3/color](https://d3js.org/d3-color)
24
+ * or [d3-scale-chromatic scales](https://d3js.org/d3-scale-chromatic)
25
+ * or [d3-color-interpolation](https://d3js.org/d3-interpolate/color)
24
26
  *
25
27
  * * Parsing color formats
26
28
  * * {@link module:color.parse|color.parse(string|array|object, optionalAlpha 0-1)} - intelligently parse any of the types to an array format
@@ -41,6 +43,8 @@
41
43
  * with properties: {r:Number[0-255], g: Number[0-255], b: Number[0-255], a: Number[0-1]}
42
44
  * * interpolate
43
45
  * * {@link module:color.interpolate|color.interpolate(fromColor, toColor, percent, formatType)} - gradually converts one color to another
46
+ * * {@link module:color.interpolator|color.interpolator} - create a function you can then call with a percentage over and over again.
47
+ * * {@link module:color.generateSequence|color.generateSequence} - generate a sequence of colors from one to another, in X number of steps
44
48
  * * {@link module:color.interpolationStrategy|color.interpolationStrategy} - the function to use for interpolation,
45
49
  * a function of signature (fromColor:Number[0-255], toColor:Number[0-255], percentage:Number[0-1]):Number[0-255]
46
50
  * * {@link module:color.INTERPOLATION_STRATEGIES|color.INTERPOLATION_STRATEGIES} - a list of strategies for interpolation you can choose from
@@ -568,6 +572,7 @@ module.exports.convert = function convert(target, formatType = ColorUtils.defaul
568
572
  * @see {@link module:color.interpolationStrategy|color.interpolationStrategy} - the default interpolation
569
573
  * used to calculate how the percentages come up with the color
570
574
  * @see {@link module:color.defaultFormat|color.defaultFormat} - the default format to use if not specified
575
+ * @see {@link module:format.mapArrayDomain|format.mapArrayDomain}
571
576
  */
572
577
  module.exports.interpolate = function interpolate(
573
578
  fromColor,
@@ -588,3 +593,97 @@ module.exports.interpolate = function interpolate(
588
593
  newColor[2] = Math.round(newColor[2]);
589
594
  return ColorUtils.convert(newColor, formatType);
590
595
  };
596
+
597
+ /**
598
+ * Curried function for color interpolation, so only the percent between [0-1] inclusive is needed.
599
+ *
600
+ * Meaning that you can do something like this:
601
+ *
602
+ * ```
603
+ * black = `#000000`;
604
+ * white = `#FFFFFF`;
605
+ *
606
+ * colorFn = utils.color.interpolator(black, white);
607
+ *
608
+ * colorFn(0); // '#000000';
609
+ * colorFn(0.5); // '#808080';
610
+ * colorFn(1); // '#FFFFFF;
611
+ *
612
+ * ```
613
+ *
614
+ * Instead of something like this with the interpolate function
615
+ *
616
+ * ```
617
+ * utils.color.interpolate(black, white, 0); // `#000000`
618
+ * utils.color.interpolate(black, white, 0.5); // `#808080`
619
+ * utils.color.interpolate(black, white, 1); // `#FFFFFF`
620
+ * ```
621
+ *
622
+ * @param {string|array|object} fromColor -the color to interpolate from
623
+ * @param {string|array|object} toColor - the color to interpolate to
624
+ * @param {Number} percent - value from 0-1 on where we should be on the sliding scale
625
+ * @param {Function} [interpolationFn = ColorUtils.interpolationStrategy] - function of
626
+ * signature (fromVal:Number[0-255], toVal:Number[0-255], pct:Number[0-1]):Number[0-255]
627
+ * @param {String} [formatType = ColorUtils.defaultFormat] - the format to convert the result to
628
+ * @returns {Function} - of signature: (Number) => {string|array|object}
629
+ * @see {@link module:color.interpolate|color.interpolate} - as this is a curried version of that function.
630
+ * @see {@link module:format.mapArrayDomain|format.mapArrayDomain}
631
+ */
632
+ module.exports.interpolator = function interpolator(
633
+ fromColor,
634
+ toColor,
635
+ interpolationFn = ColorUtils.interpolationStrategy,
636
+ formatType = ColorUtils.defaultFormat
637
+ ) {
638
+ return function interpolatorImpl(pct) {
639
+ return ColorUtils.interpolate(fromColor, toColor, pct, interpolationFn, formatType);
640
+ };
641
+ };
642
+
643
+ /**
644
+ * Generates a sequential array of colors interpolating fromColor to toColor,
645
+ *
646
+ * ```
647
+ * black = `#000000`;
648
+ * white = `#FFFFFF`;
649
+ *
650
+ * categoricalColors = utils.color.generateSequence(black, white, 5);
651
+ * // [' #000000' ,' #404040' ,' #808080' ,' #bfbfbf' ,' #ffffff' ]
652
+ * ```
653
+ *
654
+ * @param {string|array|object} fromColor -the color to interpolate from
655
+ * @param {string|array|object} toColor - the color to interpolate to
656
+ * @param {Number} lengthOfSequence - how many steps in the sequence to generate
657
+ * @param {Function} [interpolationFn = ColorUtils.interpolationStrategy] - function of
658
+ * signature (fromVal:Number[0-255], toVal:Number[0-255], pct:Number[0-1]):Number[0-255]
659
+ * @param {String} [formatType = ColorUtils.defaultFormat] - the format to convert the result to
660
+ * @returns {Function} - of signature: (Number) => {string|array|object}
661
+ * @see {@link module:color.interpolate|color.interpolate} - as this is a curried version of that function.
662
+ */
663
+ module.exports.generateSequence = function generateSequence(
664
+ fromColor,
665
+ toColor,
666
+ lengthOfSequence,
667
+ interpolationFn = ColorUtils.interpolationStrategy,
668
+ formatType = ColorUtils.defaultFormat
669
+ ) {
670
+ if (lengthOfSequence <= 0) return [];
671
+ const cleanLengthOSequence = Math.floor(lengthOfSequence);
672
+ const maxIndex = cleanLengthOSequence - 1;
673
+ const result = new Array(lengthOfSequence).fill(0)
674
+ .map((_, index) => index / maxIndex)
675
+ .map((pct) => ColorUtils.interpolate(fromColor, toColor, pct, interpolationFn, formatType));
676
+ return result;
677
+ };
678
+
679
+ /**
680
+ * Simple sequence of colors to use when plotting categorical values.
681
+ *
682
+ * Used based on the Tableau color scheme.
683
+ *
684
+ * For example:
685
+ *
686
+ * ```
687
+ * utils.color.SEQUENCE[0]
688
+ */
689
+ ColorUtils.SEQUENCE = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f', '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab'];
package/src/date.js ADDED
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Utility methods for working with dates and date ranges
3
+ *
4
+
5
+ * * Valiate
6
+ * * {@link module:date.isValid|date.isValid(date)} - whether the date provided is an invalid date
7
+ * * Parse
8
+ * * {@link module:date.parse|date.parse(String)} - parse a date and throw an exception if it is not a valid date
9
+ * * TimeZones
10
+ * * {@link module:date.getTimezoneOffset|date.getTimezoneOffset(String)} - gets the number of milliseconds offset for a given timezone
11
+ * * {@link module:date.correctForTimezone|date.correctForTimezone(Date, String)} - meant to correct a date already off from UTC to the correct time
12
+ * * {@link module:date.epochShift|date.epochShift(Date, String)} - offsets a date from UTC to a given time amount
13
+ * - knowing some methods might behave incorrectly
14
+ * * Add
15
+ * * {@link module:date.add|date.add(Date, {days, hours, minutes, seconds)} - shift a date by a given amount
16
+ * * {@link module:date.endOfDay|date.endOfDay(Date)} - finds the end of day UTC for a given date
17
+ * * {@link module:date.startOfDay|date.startOfDay(Date)} - finds the end of day UTC for a given date
18
+ *
19
+ * --------
20
+ *
21
+ * See other libraries for more complete functionality:
22
+ *
23
+ * * [Luxon](https://moment.github.io/luxon/index.html) - successor to [Moment.js](https://momentjs.com/)
24
+ * * [date-fns-tz](https://github.com/marnusw/date-fns-tz) extension for [date-fns](https://date-fns.org/)
25
+ *
26
+ * also watch the [TC39 Temporal Proposal](https://github.com/tc39/proposal-temporal)
27
+ * - also found under caniuse: https://caniuse.com/temporal
28
+ *
29
+ * @module date
30
+ * @exports date
31
+ * @see {@link https://stackoverflow.com/questions/15141762/how-to-initialize-a-javascript-date-to-a-particular-time-zone}
32
+ * @see {@link https://www.youtube.com/watch?v=2rnIHsqABfM&t=750s|epochShifting}
33
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getTimeZones|MDN TimeZone Names}
34
+ */
35
+ module.exports = {};
36
+ const DateUtils = module.exports;
37
+
38
+ /**
39
+ * Collection of time durations in milliseconds
40
+ */
41
+ module.exports.TIME = { MILLI: 1 };
42
+ module.exports.TIME.SECOND = DateUtils.TIME.MILLI * 1000;
43
+ module.exports.TIME.MINUTE = DateUtils.TIME.SECOND * 60;
44
+ module.exports.TIME.HOUR = DateUtils.TIME.MINUTE * 60;
45
+ module.exports.TIME.DAY = DateUtils.TIME.HOUR * 24;
46
+
47
+ module.exports.padTime = function padTime(num, size = 2) {
48
+ return String(num).padStart(size, '0');
49
+ };
50
+
51
+ /**
52
+ * Simple check on whether the a JavaScript Date object is - or is not - an 'Invalid Date' instance.
53
+ *
54
+ * ```
55
+ * d = new Date('2024-12-1');
56
+ * utils.date.isValid(d); // true
57
+ *
58
+ * d = new Date('2024-12-1T');
59
+ * utils.date.isValid(d); // false
60
+ *
61
+ * d = new Date('some string');
62
+ * utils.date.isValid(d); // false
63
+ * ```
64
+ *
65
+ * @param {Date} testDate - JavaScript date to validate
66
+ * @returns {boolean} - whether the Date object is an 'invalid date' instance
67
+ */
68
+ module.exports.isValid = (testDate) => {
69
+ if (!testDate) return false;
70
+ if (!(testDate instanceof Date)) return false;
71
+ return !Number.isNaN(testDate.getTime());
72
+ };
73
+
74
+ /**
75
+ * Harshly parses a JavaScript Date.
76
+ *
77
+ * If the testValue is null, undefined then the same value is returned.
78
+ *
79
+ * if the testValue is a valid Date - then the parsed Date object is returned.
80
+ *
81
+ * If the testValue is not a valid Date, then throws an Error.
82
+ *
83
+ * ```
84
+ * d = utils.date.parse('2024-12-01'); // returns Date object
85
+ * d = utils.date.parse(0); // returns Date object
86
+ *
87
+ * d = utils.date.parse(null); // returns null
88
+ *
89
+ * @param {String} dateStr - value passed to Date.parse
90
+ * @returns {Date}
91
+ * @see {@link module:date.isValid|date.isValid} - in checking for invalid dates
92
+ */
93
+ module.exports.parse = (dateStr) => {
94
+ if (dateStr === undefined || dateStr === null) return dateStr;
95
+ const result = new Date(Date.parse(dateStr));
96
+ if (!DateUtils.isValid(result)) {
97
+ throw new Error(`Could not parse date: ${dateStr}`);
98
+ }
99
+ return result;
100
+ };
101
+
102
+ module.exports.timeZoneOffsets = new Map();
103
+
104
+ /**
105
+ * Determines the number of milliseconds difference between
106
+ * a given timezone and UTC.
107
+ *
108
+ * (Note: these values are cached, and optimized for repeated use on the same value)
109
+ *
110
+ * See {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|the list of TZ database time zones}
111
+ * for the full list of options.
112
+ *
113
+ * @param {String} timeZoneStr - a timezone string like "America/Toronto"
114
+ * @returns {Number} - the number of milliseconds between UTC and that timezone
115
+ */
116
+ module.exports.getTimezoneOffset = function getTimezoneOffset(timeZoneStr) {
117
+ if (DateUtils.timeZoneOffsets.has(timeZoneStr)) {
118
+ return DateUtils.timeZoneOffsets.get(timeZoneStr);
119
+ }
120
+ const d = new Date();
121
+
122
+ const format = new Intl.DateTimeFormat('en-us', {
123
+ year: 'numeric',
124
+ month: 'numeric',
125
+ day: 'numeric',
126
+ hour: 'numeric',
127
+ minute: 'numeric',
128
+ second: 'numeric',
129
+ hour12: false,
130
+ fractionalSecondDigits: 3,
131
+ timeZone: timeZoneStr
132
+ });
133
+ const dm = format.formatToParts(d)
134
+ .filter(({ type }) => type !== 'literal')
135
+ .reduce((result, { type, value }) => {
136
+ // eslint-disable-next-line no-param-reassign
137
+ result[type] = value;
138
+ return result;
139
+ }, {});
140
+ // const impactedDate = new Date(d.toLocaleString('en-US', { timeZone: timeZoneStr }));
141
+ const dateStr = `${dm.year}-${DateUtils.padTime(dm.month)}-${DateUtils.padTime(dm.day)}T${
142
+ DateUtils.padTime(dm.hour)}:${DateUtils.padTime(dm.minute)}:${DateUtils.padTime(dm.second)}.${
143
+ DateUtils.padTime(dm.fractionalSecond, 3)}`;
144
+ const impactedDate = new Date(dateStr);
145
+
146
+ const diff = d.getTime() - impactedDate.getTime();
147
+
148
+ DateUtils.timeZoneOffsets.set(timeZoneStr, diff);
149
+
150
+ return diff;
151
+ };
152
+
153
+ /**
154
+ * JavaScript always stores dates in UTC, but the data you imported may have lost the timezone information.
155
+ *
156
+ * Use this to correct the timezone to the correct time UTC.
157
+ *
158
+ * For Example:
159
+ *
160
+ * ```
161
+ * // the date we originally pulled from the database
162
+ * // but thetimezone of the database was america/Toronto but not in UTC
163
+ * dateStr = '2024-12-06 18:00';
164
+ *
165
+ * // we may have done this:
166
+ * myDate = new Date(dateStr);
167
+ *
168
+ * // but now calling toISOString() is incorrect
169
+ * myDate.toISOString(); // '2024-12-06T18:00:00.0000'
170
+ *
171
+ * // it should be:
172
+ * correctedDate = utils.date.correctForTimezone('america/Toronto');
173
+ * correctedDate.toISOString(); // '2024-12-06T13:00:00.00.000' -- the correct time UTC
174
+ * ```
175
+ *
176
+ * See {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|the list of TZ database time zones}
177
+ * for the full list of timezone options.
178
+ *
179
+ * @param {Date} date - the date to be corrected in a new instance
180
+ * @param {String} timeZoneStr - tz database name for the timezone
181
+ * @returns {Date} - copy of the date corrected
182
+ */
183
+ module.exports.correctForTimezone = function correctForTimezone(date, timeZoneStr) {
184
+ const offsetMilli = DateUtils.getTimezoneOffset(timeZoneStr);
185
+ return new Date(date.getTime() + offsetMilli);
186
+ };
187
+
188
+ /**
189
+ * Epoch shift a date, so the utcDate is no longer correct,
190
+ * but many other functions behave closer to expected.
191
+ *
192
+ * See {@link https://stackoverflow.com/a/15171030|here why this might not be what you want}
193
+ *
194
+ * @param {Date} date - date to shift
195
+ * @param {String} timeZoneStr - the tz database name of the timezone
196
+ * @returns {Date}
197
+ */
198
+ module.exports.epochShift = function epochShift(date, timeZoneStr) {
199
+ const offsetMilli = DateUtils.getTimezoneOffset(timeZoneStr);
200
+ return new Date(date.getTime() - offsetMilli);
201
+ };
202
+
203
+ /**
204
+ * Clones a date.
205
+ *
206
+ * (Doesn't seem needed currently)
207
+ *
208
+ * (NOTE: the timezone information is lost)
209
+ *
210
+ * @param {Date} targetDate - the date to be cloned
211
+ * @returns {Date}
212
+ */
213
+ /*
214
+ module.exports.clone = function clone(targetDate) {
215
+ return new Date(targetDate.getTime());
216
+ };
217
+ */
218
+
219
+ /**
220
+ * Adds an amount to a date: days, hours, minutes, seconds
221
+ *
222
+ * ```
223
+ * d = new Date('2024-12-26 6:00:00');
224
+ * d30 = utils.date.add(d, { minutes: 30 }); // Date('2024-12-26 6:00:00')
225
+ * ```
226
+ *
227
+ * @param {Date} dateValue - date to add to
228
+ * @param {Object} options - options of what to add
229
+ * @param {Number} [options.days=0] - number of days to add
230
+ * @param {Number} [options.minutes=0] - number of minutes to add
231
+ * @param {Number} [options.hours=0] - number of minutes to add
232
+ * @param {Number} [options.seconds=0] - number of seconds to add
233
+ * @returns {Date} -
234
+ */
235
+ module.exports.add = function add(dateValue, options = null) {
236
+ if (!options) return dateValue;
237
+
238
+ const { days = 0, minutes = 0, hours = 0, seconds = 0 } = options;
239
+ return new Date(dateValue.getTime()
240
+ + DateUtils.TIME.DAY * days
241
+ + DateUtils.TIME.HOUR * hours
242
+ + DateUtils.TIME.MINUTE * minutes
243
+ + DateUtils.TIME.SECOND * seconds);
244
+ };
245
+
246
+ /**
247
+ * Creates a new date that is at the end of the day (in UTC)
248
+ *
249
+ * ```
250
+ * d = new Date('2024-12-26 6:00:00');
251
+ * dEnd = utils.date.endOfDay(d); // Date('2024-12-26 23:59:59.9999')
252
+ * ```
253
+ *
254
+ * @param {Date} dateValue - Date where only the year,month,day is used
255
+ * @returns {Date} - new date set to the end of the day for dateValue's date
256
+ */
257
+ module.exports.endOfDay = function endOfDay(dateValue) {
258
+ const startDate = Math.floor(dateValue.getTime() / DateUtils.TIME.DAY) * DateUtils.TIME.DAY;
259
+ return new Date(startDate + DateUtils.TIME.DAY - 1);
260
+ };
261
+
262
+ /**
263
+ * Creates a new date that is at the start of the day (in UTC)
264
+ *
265
+ * ```
266
+ * d = new Date('2024-12-26 6:00:00');
267
+ * dEnd = utils.date.startOfDay(d); // Date('2024-12-26 0:00:00.0000')
268
+ * ```
269
+ *
270
+ * @param {Date} dateValue - Date where only the year,month,day is used
271
+ * @returns {Date} - new date set to the end of the day for dateValue's date
272
+ */
273
+ module.exports.startOfDay = function endOfDay(dateValue) {
274
+ const startDate = Math.floor(dateValue.getTime() / DateUtils.TIME.DAY) * DateUtils.TIME.DAY;
275
+ return new Date(startDate);
276
+ };
277
+
278
+ /**
279
+ * Represents a Range between two times
280
+ */
281
+ class DateRange {
282
+ /**
283
+ * The starting date
284
+ * @type {Date}
285
+ */
286
+ startDate;
287
+
288
+ /**
289
+ * The ending date
290
+ * @type {Date}
291
+ */
292
+ endDate;
293
+
294
+ /**
295
+ * @param {Date} startDate - the starting datetime of the range
296
+ * @param {Date} endDate - the ending datetime of the range
297
+ */
298
+ constructor(startDate, endDate) {
299
+ this.reinitialize(startDate, endDate);
300
+ }
301
+
302
+ /**
303
+ * Reinitializes the object
304
+ *
305
+ * (Sometimes useful for shifting times after the fact)
306
+ *
307
+ * @param {Date} startDate - the starting date
308
+ * @param {Date} endDate - the ending date
309
+ */
310
+ reinitialize(startDate, endDate) {
311
+ if (startDate > endDate) {
312
+ this.startDate = endDate;
313
+ this.endDate = startDate;
314
+ } else {
315
+ this.startDate = startDate;
316
+ this.endDate = endDate;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Creates a DateRange based on the start and end of the day UTC.
322
+ *
323
+ * This is very useful for determining overlapping dates.
324
+ *
325
+ * @param {Date} targetDate - date to use to find the start and end UTC for
326
+ * @returns {DateRange}
327
+ */
328
+ static startAndEndOfDay(targetDate) {
329
+ const startDate = DateUtils.startOfDay(targetDate);
330
+ const endDate = DateUtils.endOfDay(targetDate);
331
+ return new DateRange(startDate, endDate);
332
+ }
333
+
334
+ /**
335
+ * Whether this dateRange overlaps with a target dateRange.
336
+ * @param {DateRange} targetDateRange - dateRange to compare
337
+ * @returns {Boolean}
338
+ * @example
339
+ * overlapA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
340
+ * overlapB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
341
+ * overlapC = new Date(Date.UTC(2024, 12, 26, 14, 0, 0));
342
+ * overlapD = new Date(Date.UTC(2024, 12, 26, 15, 0, 0));
343
+ *
344
+ * rangeBefore = new utils.DateRange(overlapA, overlapB);
345
+ * rangeAfter = new utils.DateRange(overlapC, overlapD);
346
+ *
347
+ * rangeBefore.overlaps(rangeAfter); // false
348
+ * rangeAfter.overlaps(rangeBefore); // false
349
+ *
350
+ * rangeBefore = new utils.DateRange(overlapA, overlapC);
351
+ * rangeAfter = new utils.DateRange(overlapB, overlapD);
352
+ *
353
+ * rangeBefore.overlaps(rangeAfter); // true
354
+ * rangeAfter.overlaps(rangeBefore); // true
355
+ */
356
+ overlaps(targetDateRange) {
357
+ return (this.endDate > targetDateRange.startDate
358
+ && this.startDate < targetDateRange.endDate);
359
+ }
360
+
361
+ /**
362
+ * Determines if a datetime is within the range
363
+ *
364
+ * @param {Date} dateToCheck - the value to test if it is within the date range
365
+ * @returns {Boolean} - if the value is within the range (true) or not (false)
366
+ *
367
+ * @example
368
+ * withinA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
369
+ * withinB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
370
+ * withinC = new Date(Date.UTC(2024, 12, 26, 14, 0, 0));
371
+ * withinD = new Date(Date.UTC(2024, 12, 26, 15, 0, 0));
372
+ *
373
+ * range = new utils.DateRange(withinB, withinD);
374
+ * range.contains(withinA); // false - it was before the range
375
+ *
376
+ * range.contains(withinB); // true
377
+ * range.contains(withinC); // true
378
+ * range.contains(withinD); // true
379
+ *
380
+ */
381
+ contains(dateToCheck) {
382
+ const testTime = dateToCheck.getTime();
383
+ return testTime >= this.startDate.getTime() && testTime <= this.endDate.getTime();
384
+ }
385
+
386
+ /**
387
+ * Determines the millisecond duration between the end and start time.
388
+ *
389
+ * ```
390
+ * durationA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
391
+ * durationB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
392
+ * range = new utils.DateRange(durationA, durationB);
393
+ *
394
+ * range.durationString(); // 1 hour in milliseconds; 1000 * 60 * 60;
395
+ * ```
396
+ *
397
+ * @returns {Number}
398
+ */
399
+ duration() {
400
+ return this.endDate.getTime() - this.startDate.getTime();
401
+ }
402
+
403
+ /**
404
+ * Determines the duration in a clear and understandable string;
405
+ *
406
+ * ```
407
+ * durationA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
408
+ * durationB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
409
+ * range = new utils.DateRange(durationA, durationB);
410
+ *
411
+ * range.durationString(); // '0 days, 1 hours, 0 minutes, 0.0 seconds';
412
+ * ```
413
+ *
414
+ * @returns {String}
415
+ */
416
+ durationString() {
417
+ const dur = this.duration();
418
+ const divideRemainder = (val, denominator) => ({ value: Math.floor(val / denominator), remainder: val % denominator });
419
+ let result = divideRemainder(dur, DateUtils.TIME.DAY);
420
+ const days = result.value;
421
+ result = divideRemainder(result.remainder, DateUtils.TIME.HOUR);
422
+ const hours = result.value;
423
+ result = divideRemainder(result.remainder, DateUtils.TIME.MINUTE);
424
+ const minutes = result.value;
425
+ result = divideRemainder(result.remainder, DateUtils.TIME.SECOND);
426
+ const seconds = result.value;
427
+ const milli = result.remainder;
428
+ return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds}.${milli} seconds`;
429
+ }
430
+
431
+ /**
432
+ * Determines the duration in days:hours:minutes:seconds.milliseconds
433
+ *
434
+ * ```
435
+ * durationA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
436
+ * durationB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
437
+ * range = new utils.DateRange(durationA, durationB);
438
+ *
439
+ * range.durationString(); // '0:01:00:00.0000';
440
+ * ```
441
+ *
442
+ * @returns {String}
443
+ */
444
+ durationISO() {
445
+ const dur = this.duration();
446
+ const divideRemainder = (val, denominator) => ({ value: Math.floor(val / denominator), remainder: val % denominator });
447
+ let result = divideRemainder(dur, DateUtils.TIME.DAY);
448
+ const days = String(result.value);
449
+ result = divideRemainder(result.remainder, DateUtils.TIME.HOUR);
450
+ const hours = String(result.value).padStart(2, '0');
451
+ result = divideRemainder(result.remainder, DateUtils.TIME.MINUTE);
452
+ const minutes = String(result.value).padStart(2, '0');
453
+ result = divideRemainder(result.remainder, DateUtils.TIME.SECOND);
454
+ const seconds = String(result.value).padStart(2, '0');
455
+ const milli = String(result.remainder).padStart(4, '0');
456
+ return `${days}:${hours}:${minutes}:${seconds}.${milli}`;
457
+ }
458
+
459
+ /**
460
+ * Determines if both the startDate and endDate are valid dates.
461
+ *
462
+ * @returns {Boolean}
463
+ */
464
+ isValid() {
465
+ return DateUtils.isValid(this.startDate) && DateUtils.isValid(this.endDate);
466
+ }
467
+
468
+ /**
469
+ * Converts the daterange to a string value
470
+ *
471
+ * ```
472
+ * durationA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
473
+ * durationB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
474
+ * range = new utils.DateRange(durationA, durationB);
475
+ *
476
+ * range.toString(); // '2025-01-26T12:00:00.000Z to 2025-01-26T13:00:00.000Z';
477
+ * ```
478
+ *
479
+ * @returns {String}
480
+ */
481
+ toString() {
482
+ return `${this.startDate.toISOString()} to ${this.endDate.toISOString()}`;
483
+ }
484
+
485
+ /**
486
+ * Converts the daterange to a local string value
487
+ *
488
+ * ```
489
+ * durationA = new Date(Date.UTC(2024, 12, 26, 12, 0, 0));
490
+ * durationB = new Date(Date.UTC(2024, 12, 26, 13, 0, 0));
491
+ * range = new utils.DateRange(durationA, durationB);
492
+ *
493
+ * range.toLocaleString(); // '1/26/2025, 12:00:00 PM to 1/26/2025, 1:00:00 PM'
494
+ * ```
495
+ *
496
+ * @returns {String}
497
+ */
498
+ toLocaleString() {
499
+ return `${this.startDate.toLocaleString()} to ${this.endDate.toLocaleString()}`;
500
+ }
501
+ }
502
+
503
+ module.exports.DateRange = DateRange;
package/src/format.js CHANGED
@@ -1347,3 +1347,19 @@ module.exports.extractWords = function extractWords(strToExtractFrom, additional
1347
1347
 
1348
1348
  return cleanStrings.reduce((result, str) => [...result, ...((str || '').match(regex) || [])], []);
1349
1349
  };
1350
+
1351
+ /**
1352
+ * A function that returns the value provided
1353
+ *
1354
+ * ```
1355
+ * alwaysBlack = utils.format.constantFn('#000000');
1356
+ * alwaysBlack(); // '#000000'
1357
+ * alwaysBlack(); // '#000000'
1358
+ * ```
1359
+ *
1360
+ * @param {any} val - Any value
1361
+ * @returns {Function} - a function that accepts no parameters, and always returns value provided
1362
+ */
1363
+ module.exports.constantFn = function constantFn(val) {
1364
+ return () => val;
1365
+ };
package/src/hashMap.js CHANGED
@@ -216,3 +216,27 @@ module.exports.fromObject = function fromObject(target) {
216
216
  return [...Object.keys(target)]
217
217
  .reduce((result, key) => HashMapUtil.add(result, key, target[key]), new Map());
218
218
  };
219
+
220
+ /**
221
+ * Simple and safe Map accessing function.
222
+ *
223
+ * ```
224
+ * styleMap = new Map(['1', 'background-color: #FF0000'], ['2', 'background-color: #00FF00']]);
225
+ * styleFn = utils.map.mappingFn(styleMap, 'background-color: #aaaaaa');
226
+ *
227
+ * styleFn('1'); // 'background-color: #FF0000';
228
+ * styleFn('2'); // 'background-color: #00FF00';
229
+ * styleFn('somethingElse'); // 'background-color: #aaaaaa' - because it was not found
230
+ *
231
+ * @param {Map} map - map to use when checking the subsequent function
232
+ * @param {any} [defaultValue=''] - default value to return if key is not found
233
+ * @returns {Function} - (key) => map.get(key) || defaultValue
234
+ */
235
+ module.exports.mappingFn = function mappingFn(map, defaultValue = '') {
236
+ return function mappingFnImpl(key) {
237
+ if (map.has(key)) {
238
+ return map.get(key);
239
+ }
240
+ return defaultValue;
241
+ };
242
+ };
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ const base64 = require('./base64');
4
4
  const chain = require('./chain');
5
5
  const color = require('./color');
6
6
  const datasets = require('./datasets');
7
+ const date = require('./date');
7
8
  const describe = require('./describe');
8
9
  const group = require('./group');
9
10
  const hashMap = require('./hashMap');
@@ -50,6 +51,9 @@ module.exports = {
50
51
  /** @see {@link module:datasets} */
51
52
  datasets,
52
53
  dataset: datasets,
54
+ /** @see {@link module:date} */
55
+ date,
56
+ DateRange: date.DateRange,
53
57
  /** @see {@link module:describe} */
54
58
  describe,
55
59
  /** @see {@link module:file} */
package/src/object.js CHANGED
@@ -217,6 +217,7 @@ module.exports.augment = function augment(objCollection, mappingFn, inPlace = fa
217
217
  * @param {Function | String} propertyOrFn - Name of the property or Function to return a value
218
218
  * @returns {Map<String, Object>} - map using the propertyName as the key
219
219
  * @see {@link module:group.by|group(collection, propertyOrFn)} - if there is a possibility the records are not unique
220
+ * @see {@link module:object.join|object.join()} - join two objects by a shared index
220
221
  * @example
221
222
  * const data = [{ id: '123', name: 'jim' },
222
223
  * { id: '456', name: 'mary' },
@@ -1248,6 +1249,7 @@ module.exports.generateSchema = function generateSchema(targetObj) {
1248
1249
  * @param {Function} joinFn - function to call each time an objectArray object, has an indexField found in targetMap <br />
1249
1250
  * Signature: `(sourceObj:Object, mappedObject:Object) => {Object}`
1250
1251
  * @returns {Array<Object>} - Array of results returned from `joinFn`
1252
+ * @see {@link module:object.mapByProperty|object.mapByProperty}
1251
1253
  */
1252
1254
  module.exports.join = function join(objectArray, indexField, targetMap, joinFn) {
1253
1255
  const cleanArray = !objectArray