@tim-code/my-util 0.5.15 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.5.15",
3
+ "version": "0.6.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/array.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * @param {Iterable} iterable
6
6
  * @param {number=} chunkSize If not provided, returns an array consisting of one chunk, which has all the elements of input iterable.
7
7
  * This has the same effect as passing a chunk size that is greater than the number of elements in the iterable.
8
- * @returns {Array}
8
+ * @returns {Array<Array>}
9
9
  */
10
10
  export function chunk(iterable, chunkSize = Infinity) {
11
11
  if (chunkSize !== Infinity && (chunkSize <= 0 || !Number.isInteger(chunkSize))) {
package/src/find.js CHANGED
@@ -181,7 +181,7 @@ export function findClosestGTE(array, desired, { key, cutoff = Infinity } = {})
181
181
  * Find the closest element in an array. If there is a tie, then returns the first matching element by order in the array.
182
182
  * If some values are undefined or null, they will be ignored. If no element is found, returns undefined.
183
183
  * If using for strings, need to specify different values for "cutoff" and "comparator".
184
- * "~" and "" are good cutoff string values for gt/gte and lt/lte respectively.
184
+ * "~" and "" are good cutoff string values for gt/gte and lt/lte respectively.
185
185
  * @template T, V
186
186
  * @param {Array<T>} array
187
187
  * @param {V} value The desired value to search for
@@ -191,8 +191,8 @@ export function findClosestGTE(array, desired, { key, cutoff = Infinity } = {})
191
191
  * If a function, called with the element, index and array (same as .map() callback) to produce the value to sort on.
192
192
  * @param {string=} options.comparator "diff", "lt", "lte", "gt", "gte", "eq". Default is "diff" which implies T is number.
193
193
  * @param {V=} options.cutoff If specified, sets a initial constraint on how close the found value must be.
194
- * If used with lt, lte, value must be greater than or equal to cutoff.
195
- * If used with gt, gte, value must be less than or equal to cutoff.
194
+ * If used with lt/lte, value must be greater than or equal to cutoff.
195
+ * If used with gt/gte, value must be less than or equal to cutoff.
196
196
  * If used with diff, value's difference with desired must be less than or equal to cutoff.
197
197
  * No effect with eq.
198
198
  * @returns {T|undefined}
package/src/math.js CHANGED
@@ -14,8 +14,8 @@ export function mod(number, modulus) {
14
14
  /**
15
15
  * Given two points, returns a function
16
16
  * This function, given an "x" value, returns a "y" value that is on the same line as the first two points.
17
- * @param {[number, number]} firstPoint
18
- * @param {[number, number]} secondPoint
17
+ * @param {[number, number]} point1
18
+ * @param {[number, number]} point2
19
19
  * @returns {Function}
20
20
  */
21
21
  export function line([x1, y1], [x2, y2]) {
@@ -29,8 +29,8 @@ export function line([x1, y1], [x2, y2]) {
29
29
  * Calculate the sum of values in an array.
30
30
  * @template T
31
31
  * @param {Array<T>} array
32
- * @param {Object} [options]
33
- * @param {string|number|((element: T, index: number, array: Array<T>) => number)=} options.key
32
+ * @param {Object} $1
33
+ * @param {string|number|Function=} $1.key Can specify a key of the object or a function.
34
34
  * @returns {number}
35
35
  */
36
36
  export function sum(array, { key } = {}) {
@@ -55,8 +55,8 @@ export function sum(array, { key } = {}) {
55
55
  * Calculate the average (mean) of values in an array.
56
56
  * @template T
57
57
  * @param {Array<T>} array
58
- * @param {Object} [options]
59
- * @param {string|number|((element: T, index: number, array: Array<T>) => number)=} options.key
58
+ * @param {Object} $1
59
+ * @param {string|number|Function=} $1.key Can specify a key of the object or a function.
60
60
  * @returns {number}
61
61
  * @throws {Error} If the array is empty.
62
62
  */
@@ -71,8 +71,8 @@ export function average(array, { key } = {}) {
71
71
  * Calculate the variance of values in an array.
72
72
  * @template T
73
73
  * @param {Array<T>} array
74
- * @param {Object} [options]
75
- * @param {string|number|((element: T, index: number, array: Array<T>) => number)=} options.key
74
+ * @param {Object} $1
75
+ * @param {string|number|Function=} $1.key Can specify a key of the object or a function.
76
76
  * @returns {number}
77
77
  * @throws {Error} If the array is empty.
78
78
  */
@@ -145,7 +145,8 @@ export function range(start, end, increment = 1) {
145
145
  }
146
146
 
147
147
  /**
148
- * Check if the argument is a number (typeof is "number" and not NaN).
148
+ * Check if the argument is a number.
149
+ * This excludes Infinity and NaN, but otherwise is equivalent to `typeof number === "number"`.
149
150
  * @param {any} number
150
151
  * @returns {boolean}
151
152
  */
@@ -163,9 +164,11 @@ export function isNumber(number) {
163
164
  * @param {string|number|Function=} $1.key Can specify a key of the object to sort on or a function.
164
165
  * @param {Function=} $1.method Method to use to choose which element when the percentile index is a fractional value.
165
166
  * Default is Math.round.
167
+ * @param {Function=} $.labeller Function that returns a quantile label given a fractional value (i.e. 33.3...).
168
+ * Default is Math.round.
166
169
  * @returns {Object|undefined} Returns undefined is array is empty
167
170
  */
168
- export function quantiles(array, { N, key, method = Math.round }) {
171
+ export function quantiles(array, { N, key, method = Math.round, labeller = Math.round }) {
169
172
  if (!(N > 0) || !Number.isInteger(N)) {
170
173
  throw new Error("N must be a positive integer")
171
174
  }
@@ -177,7 +180,7 @@ export function quantiles(array, { N, key, method = Math.round }) {
177
180
  for (let i = 0; i <= N; i++) {
178
181
  const percentile = i / N
179
182
  const percentileIndex = method(percentile * (sorted.length - 1))
180
- const label = method(i * (100 / N))
183
+ const label = labeller(i * (100 / N))
181
184
  result[label] = sorted[percentileIndex]
182
185
  }
183
186
  return result
package/src/math.test.js CHANGED
@@ -345,6 +345,9 @@ describe("isNumber", () => {
345
345
  })
346
346
 
347
347
  describe("quantiles", () => {
348
+ // ISSUE: JSDoc for labeller parameter uses "$.labeller" instead of "$1.labeller", which is inconsistent with the other params.
349
+ // ISSUE: If labeller maps multiple percent values to the same label, later entries overwrite earlier ones without warning.
350
+
348
351
  it("maps 0..100 deciles for an already sorted array of length 11 (default rounding)", () => {
349
352
  const arr = Array.from({ length: 11 }, (_, i) => i)
350
353
  const result = quantiles(arr, { N: 10 })
@@ -454,4 +457,24 @@ describe("quantiles", () => {
454
457
  expect(() => quantiles([1, 2, 3], { N: 0 })).toThrow("N must be a positive integer")
455
458
  expect(() => quantiles([1, 2, 3], { N: 2.5 })).toThrow("N must be a positive integer")
456
459
  })
460
+
461
+ it("supports a custom labeller (Math.floor) independent of the index method", () => {
462
+ const arr = Array.from({ length: 11 }, (_, i) => i) // 0..10
463
+ const result = quantiles(arr, { N: 3, labeller: Math.floor }) // labels 0,33,66,100
464
+ expect(result[0]).toBe(0)
465
+ expect(result[33]).toBe(3) // index still uses default Math.round
466
+ expect(result[66]).toBe(7) // label differs from default (which would be 67)
467
+ expect(result[100]).toBe(10)
468
+ })
469
+
470
+ it("supports a custom labeller that produces string labels", () => {
471
+ const arr = Array.from({ length: 9 }, (_, i) => i) // 0..8
472
+ const labeller = (p) => `Q${Math.round(p / 25)}`
473
+ const result = quantiles(arr, { N: 4, labeller })
474
+ expect(result.Q0).toBe(0)
475
+ expect(result.Q1).toBe(2)
476
+ expect(result.Q2).toBe(4)
477
+ expect(result.Q3).toBe(6)
478
+ expect(result.Q4).toBe(8)
479
+ })
457
480
  })
package/src/object.js CHANGED
@@ -10,7 +10,8 @@ export function isObject(thing) {
10
10
  /**
11
11
  * Creates a new object with values created by calling callback on each of argument's values.
12
12
  * @param {Object} object
13
- * @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
13
+ * @param {Function} callback (value, key, object) => newValue
14
+ * Note if not changing value, should return value.
14
15
  * @returns {Object}
15
16
  */
16
17
  export function mapValues(object, callback) {
@@ -25,7 +26,8 @@ export function mapValues(object, callback) {
25
26
  /**
26
27
  * Mutates the passed in object by calling callback on each of its values.
27
28
  * @param {Object} object
28
- * @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
29
+ * @param {Function} callback (value, key, object) => newValue
30
+ * Note if not changing value, should return value.
29
31
  * @returns {Object}
30
32
  */
31
33
  export function mutateValues(object, callback) {
@@ -83,6 +85,7 @@ export function like(template) {
83
85
  /**
84
86
  * Copies the source recursively.
85
87
  * Does not preserve constructors of source or constructors of its keys' values.
88
+ * Preserves distinction between an array and an object i.e. `[1]` and `{"0": 1}`.
86
89
  * @template T
87
90
  * @param {T} source
88
91
  * @returns {T}
@@ -105,9 +108,9 @@ export function deepCopy(source) {
105
108
  * the source object's value is a non-array object, recursively merges the source into the target.
106
109
  * Otherwise, assigns source's key's value into the target's key.
107
110
  * This means that arrays are never merged into arrays or other objects.
108
- * @param {Object} target The target object that will receive the merged properties.
109
- * @param {...Object} sources The source objects whose properties will be merged into the target.
110
- * @returns {Object} The target object with the merged properties from all source objects.
111
+ * @param {Object} target The target object that will receive the merged properties
112
+ * @param {...Object} sources The source objects whose properties will be merged into the target
113
+ * @returns {Object} The target object with the merged properties from all source objects
111
114
  */
112
115
  export function deepMerge(target, ...sources) {
113
116
  for (const source of sources) {
@@ -136,7 +139,7 @@ export function deepMerge(target, ...sources) {
136
139
 
137
140
  /**
138
141
  * Merges a deep copy of each source object into target. See deepCopy() and deepMerge() documentation for caveats.
139
- * @param {Object} target The target object that will receive the merged properties.
142
+ * @param {Object} target The target object that will receive the merged properties
140
143
  * @param {...Object} sources The source objects whose properties will be merged into the returned object
141
144
  * @returns {Object}
142
145
  */
@@ -151,11 +154,11 @@ export function deepMergeCopy(target, ...sources) {
151
154
  * Objects and arrays are compared recursively by their properties and elements.
152
155
  * Primitives are compared with strict equality.
153
156
  * Caveats:
154
- * Does not check class: [1] is considered equal to {0: 1}.
155
- * Any Symbol keys in the arguments are ignored (Object.keys only returns string keys).
156
- * @param {any} a The first value to compare.
157
- * @param {any} b The second value to compare.
158
- * @returns {boolean} True if the values are deeply equal, false otherwise.
157
+ * Does not check class: `[1]` is considered equal to `{"0": 1}`.
158
+ * Any `Symbol` keys in the arguments are ignored (Object.keys only returns string keys).
159
+ * @param {any} a The first value to compare
160
+ * @param {any} b The second value to compare
161
+ * @returns {boolean} True if the values are deeply equal, false otherwise
159
162
  */
160
163
  export function deepEqual(a, b) {
161
164
  if (a === b) {
package/src/time.js CHANGED
@@ -5,13 +5,13 @@ import { mod } from "./math.js"
5
5
  * @param {Object} $1
6
6
  * @param {boolean=} $1.floorMinute If true, floors to the nearest minute. If false, floors to the nearest second.
7
7
  * @param {number=} $1.timestamp Unix timestamp to use instead of current time.
8
- * @param {string=} $1.timezone Timezone to use instead of Eastern time.
8
+ * @param {string=} $1.timeZone Time zone to use instead of Eastern time. A falsy value corresponds to local time.
9
9
  * @returns {Object} { timestamp, date: YYYY-MM-DD, time: HH:mm:ss }
10
10
  */
11
11
  export function getEasternTime({
12
12
  floorMinute = false,
13
13
  timestamp = undefined,
14
- timezone = "America/New_York",
14
+ timeZone = "America/New_York",
15
15
  } = {}) {
16
16
  if (!timestamp) {
17
17
  timestamp = new Date().getTime() / 1000
@@ -19,7 +19,7 @@ export function getEasternTime({
19
19
  timestamp = floorMinute ? Math.floor(timestamp / 60) * 60 : Math.floor(timestamp)
20
20
  // 'en-CA' (English - Canada) formats dates as YYYY-MM-DD and times in 24-hour format by default
21
21
  const string = new Date(timestamp * 1000).toLocaleString("en-CA", {
22
- ...(timezone ? { timeZone: timezone } : {}),
22
+ ...(timeZone ? { timeZone } : {}),
23
23
  hour12: false,
24
24
  year: "numeric",
25
25
  month: "2-digit",
@@ -37,11 +37,11 @@ export function getEasternTime({
37
37
  * @param {Object} $1
38
38
  * @param {boolean=} $1.floorMinute If true, floors to the nearest minute. If false, floors to the nearest second.
39
39
  * @param {number=} $1.timestamp Unix timestamp to use instead of current time.
40
- * @param {string=} $1.timezone Timezone to use instead of local time. undefined corresponds to "America/New_York" and "" (falsy) corresponds to local time.
40
+ * @param {string=} $1.timeZone Time zone to use instead of local time. A falsy value (default) corresponds to local time.
41
41
  * @returns {Object} { timestamp, date, time, minute, datetime }
42
42
  */
43
- export function getTime({ floorMinute, timestamp, timezone = "" } = {}) {
44
- return getEasternTime({ floorMinute, timestamp, timezone })
43
+ export function getTime({ floorMinute, timestamp, timeZone = false } = {}) {
44
+ return getEasternTime({ floorMinute, timestamp, timeZone })
45
45
  }
46
46
 
47
47
  /**
@@ -82,7 +82,7 @@ export function getMinute(time) {
82
82
  * @param {string} string
83
83
  * @returns {boolean}
84
84
  */
85
- export function isDate(string) {
85
+ export function isDateString(string) {
86
86
  const match = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/u.exec(string)
87
87
  if (!match) {
88
88
  return false
@@ -96,15 +96,33 @@ export function isDate(string) {
96
96
  )
97
97
  }
98
98
 
99
+ /**
100
+ * @deprecated Prefer isDateString()
101
+ * @param {string} string
102
+ * @returns {boolean}
103
+ */
104
+ export function isDate(string) {
105
+ return isDateString(string)
106
+ }
107
+
99
108
  /**
100
109
  * Checks if the string represent a valid HH:mm:ss time.
101
110
  * @param {string} string
102
111
  * @returns {boolean}
103
112
  */
104
- export function isTime(string) {
113
+ export function isTimeString(string) {
105
114
  return /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/u.test(string)
106
115
  }
107
116
 
117
+ /**
118
+ * @deprecated Prefer isTimeString()
119
+ * @param {string} string
120
+ * @returns {boolean}
121
+ */
122
+ export function isTime(string) {
123
+ return isTimeString(string)
124
+ }
125
+
108
126
  /**
109
127
  * Checks if a number is a Unix timestamp (i.e. in seconds).
110
128
  * Would not validate timestamps set very far into the future.
@@ -173,7 +191,8 @@ export function getTimeRange(start, end, { hours = 0, minutes = 1 } = {}) {
173
191
  /**
174
192
  * Adds a number of days to a date string.
175
193
  * @param {string} dateString YYYY-MM-DD
176
- * @param {number} days
194
+ * @param {Object} $1
195
+ * @param {number} $1.days
177
196
  * @returns {string}
178
197
  */
179
198
  export function addDays(dateString, { days = 0 } = {}) {
package/src/time.test.js CHANGED
@@ -4,14 +4,15 @@ import {
4
4
  addTime,
5
5
  getDateRange,
6
6
  getDayIndexInWeek,
7
- // getDayOfWeek, // removed, replaced by getDayIndexInWeek
8
7
  getEasternTime,
9
8
  getMinute,
10
9
  getStartOfWeek,
11
10
  getTime,
12
11
  getTimeRange,
13
12
  isDate,
13
+ isDateString,
14
14
  isTime,
15
+ isTimeString,
15
16
  isUnixTimestamp,
16
17
  today,
17
18
  } from "./time.js"
@@ -54,15 +55,15 @@ describe("getEasternTime", () => {
54
55
  expect(def.timestamp).toEqual(explicit.timestamp)
55
56
  })
56
57
 
57
- test("respects timezone parameter", () => {
58
+ test("respects timeZone parameter", () => {
58
59
  // 2024-06-01T12:34:56Z (UTC)
59
60
  const ts = 1717245296
60
61
  // New York (EDT, UTC-4)
61
- const eastern = getEasternTime({ timestamp: ts, timezone: "America/New_York" })
62
+ const eastern = getEasternTime({ timestamp: ts, timeZone: "America/New_York" })
62
63
  // Los Angeles (PDT, UTC-7)
63
- const pacific = getEasternTime({ timestamp: ts, timezone: "America/Los_Angeles" })
64
+ const pacific = getEasternTime({ timestamp: ts, timeZone: "America/Los_Angeles" })
64
65
  // UTC
65
- const utc = getEasternTime({ timestamp: ts, timezone: "UTC" })
66
+ const utc = getEasternTime({ timestamp: ts, timeZone: "UTC" })
66
67
  expect(eastern.time).not.toBe(pacific.time)
67
68
  expect(eastern.time).not.toBe(utc.time)
68
69
  expect(pacific.time).not.toBe(utc.time)
@@ -71,11 +72,11 @@ describe("getEasternTime", () => {
71
72
  expect(utc.time.startsWith("12:34")).toBe(true) // UTC
72
73
  })
73
74
 
74
- test("returns local time if timezone is empty string", () => {
75
- // If timezone is falsy (""), should use local time zone
75
+ test("returns local time if timeZone is empty string", () => {
76
+ // If timeZone is falsy (""), should use local time zone
76
77
  // We'll compare to Date.toLocaleString with no timeZone option
77
78
  const ts = 1717245296
78
- const local = getEasternTime({ timestamp: ts, timezone: "" })
79
+ const local = getEasternTime({ timestamp: ts, timeZone: "" })
79
80
  const expected = new Date(ts * 1000).toLocaleString("en-US", {
80
81
  hour12: false,
81
82
  year: "numeric",
@@ -141,8 +142,8 @@ describe("getTime", () => {
141
142
  expect(result).not.toHaveProperty("datetime")
142
143
  })
143
144
 
144
- test("defaults to local time if timezone is not provided", () => {
145
- // getTime({}) should use timezone = false, which disables the timeZone option and uses local
145
+ test("defaults to local time if timeZone is not provided", () => {
146
+ // getTime({}) should use timeZone = false, which disables the timeZone option and uses local
146
147
  const ts = 1717245296
147
148
  const local = getTime({ timestamp: ts })
148
149
  const expected = new Date(ts * 1000).toLocaleString("en-US", {
@@ -161,11 +162,11 @@ describe("getTime", () => {
161
162
  expect(local.time).toBe(time)
162
163
  })
163
164
 
164
- test("passes timezone through to getEasternTime", () => {
165
- // Should match getEasternTime with same timezone
165
+ test("passes timeZone through to getEasternTime", () => {
166
+ // Should match getEasternTime with same timeZone
166
167
  const ts = 1717245296
167
- const pacific = getTime({ timestamp: ts, timezone: "America/Los_Angeles" })
168
- const ref = getEasternTime({ timestamp: ts, timezone: "America/Los_Angeles" })
168
+ const pacific = getTime({ timestamp: ts, timeZone: "America/Los_Angeles" })
169
+ const ref = getEasternTime({ timestamp: ts, timeZone: "America/Los_Angeles" })
169
170
  expect(pacific).toEqual(ref)
170
171
  })
171
172
 
@@ -208,7 +209,7 @@ describe("getDayIndexInWeek", () => {
208
209
 
209
210
  test("throws on invalid date strings", () => {
210
211
  expect(() => getDayIndexInWeek("not-a-date")).toThrow(/invalid date/u)
211
- // don't worry about bad dates like the following; can be caught with isDate()
212
+ // don't worry about bad dates like the following; can be caught with isDateString()
212
213
  expect(getDayIndexInWeek("2024-02-31")).toBe(6)
213
214
  })
214
215
  })
@@ -234,7 +235,19 @@ describe("getMinute", () => {
234
235
  })
235
236
  })
236
237
 
237
- describe("isDate", () => {
238
+ describe("isDateString", () => {
239
+ test("validates correct dates", () => {
240
+ expect(isDateString("2024-06-01")).toBe(true)
241
+ expect(isDateString("1999-12-31")).toBe(true)
242
+ })
243
+
244
+ test("rejects invalid dates and formats", () => {
245
+ expect(isDateString("2024-02-31")).toBe(false)
246
+ expect(isDateString("2024/06/01")).toBe(false)
247
+ })
248
+ })
249
+
250
+ describe("isDate (deprecated wrapper)", () => {
238
251
  test("returns true for valid YYYY-MM-DD dates", () => {
239
252
  expect(isDate("2024-06-01")).toBe(true)
240
253
  expect(isDate("1999-12-31")).toBe(true)
@@ -266,7 +279,19 @@ describe("isDate", () => {
266
279
  })
267
280
  })
268
281
 
269
- describe("isTime", () => {
282
+ describe("isTimeString", () => {
283
+ test("validates correct times", () => {
284
+ expect(isTimeString("00:00:00")).toBe(true)
285
+ expect(isTimeString("23:59:59")).toBe(true)
286
+ })
287
+
288
+ test("rejects invalid times and formats", () => {
289
+ expect(isTimeString("24:00:00")).toBe(false)
290
+ expect(isTimeString("12:34")).toBe(false)
291
+ })
292
+ })
293
+
294
+ describe("isTime (deprecated wrapper)", () => {
270
295
  test("returns true for valid HH:mm:ss times", () => {
271
296
  expect(isTime("00:00:00")).toBe(true)
272
297
  expect(isTime("23:59:59")).toBe(true)