@tim-code/my-util 0.2.4 → 0.2.6

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.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
package/src/object.js CHANGED
@@ -55,3 +55,37 @@ export function like(template) {
55
55
  return true
56
56
  }
57
57
  }
58
+
59
+ /**
60
+ * Deeply merges one or more source objects into a target object.
61
+ * If the property values are objects, they are merged recursively.
62
+ * Non-object properties (including arrays) are directly assigned to the target.
63
+ * @param {Object} target The target object that will receive the merged properties.
64
+ * @param {...Object} sources The source objects whose properties will be merged into the target.
65
+ * @returns {Object} The target object with the merged properties from all source objects.
66
+ */
67
+ export function deepMerge(target, ...sources) {
68
+ for (const source of sources) {
69
+ const keys = Object.keys(source)
70
+ for (const key of keys) {
71
+ if (!Object.hasOwn(source, key)) {
72
+ continue
73
+ }
74
+ const targetValue = target[key]
75
+ const sourceValue = source[key]
76
+ if (Array.isArray(sourceValue)) {
77
+ target[key] = sourceValue
78
+ } else if (
79
+ targetValue &&
80
+ typeof targetValue === "object" &&
81
+ sourceValue &&
82
+ typeof sourceValue === "object"
83
+ ) {
84
+ deepMerge(targetValue, sourceValue)
85
+ } else {
86
+ target[key] = sourceValue
87
+ }
88
+ }
89
+ }
90
+ return target
91
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  import { jest } from "@jest/globals"
3
- import { deleteUndefinedValues, like, mutateValues, via } from "./object.js"
3
+ import { deepMerge, deleteUndefinedValues, like, mutateValues, via } from "./object.js"
4
4
 
5
5
  describe("mutateValues", () => {
6
6
  it("mutates values in the object using the callback", () => {
@@ -159,3 +159,101 @@ describe("contains", () => {
159
159
  expect(fn(obj)).toBe(false)
160
160
  })
161
161
  })
162
+
163
+ describe("deepMerge", () => {
164
+ it("merges flat objects", () => {
165
+ const target = { a: 1, b: 2 }
166
+ const source = { b: 3, c: 4 }
167
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 3, c: 4 })
168
+ expect(target).toEqual({ a: 1, b: 3, c: 4 })
169
+ })
170
+
171
+ it("deeply merges nested objects", () => {
172
+ const target = { a: { x: 1, y: 2 }, b: 2 }
173
+ const source = { a: { y: 3, z: 4 }, b: 5 }
174
+ expect(deepMerge(target, source)).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 5 })
175
+ expect(target).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 5 })
176
+ })
177
+
178
+ it("overwrites arrays instead of merging them", () => {
179
+ const target = { arr: [1, 2], a: 1 }
180
+ const source = { arr: [3, 4] }
181
+ expect(deepMerge(target, source)).toEqual({ arr: [3, 4], a: 1 })
182
+ })
183
+
184
+ it("merges multiple sources left-to-right", () => {
185
+ const target = { a: 1 }
186
+ const s1 = { b: 2 }
187
+ const s2 = { a: 3, c: 4 }
188
+ expect(deepMerge(target, s1, s2)).toEqual({ a: 3, b: 2, c: 4 })
189
+ })
190
+
191
+ it("does not merge non-object values, just assigns", () => {
192
+ const target = { a: { x: 1 } }
193
+ const source = { a: 2 }
194
+ expect(deepMerge(target, source)).toEqual({ a: 2 })
195
+ })
196
+
197
+ it("handles empty sources", () => {
198
+ const target = { a: 1 }
199
+ expect(deepMerge(target)).toEqual({ a: 1 })
200
+ expect(deepMerge(target, {})).toEqual({ a: 1 })
201
+ })
202
+
203
+ it("handles empty target", () => {
204
+ const target = {}
205
+ const source = { a: 1 }
206
+ expect(deepMerge(target, source)).toEqual({ a: 1 })
207
+ })
208
+
209
+ it("does not merge inherited properties from sources", () => {
210
+ const proto = { x: 1 }
211
+ const source = Object.create(proto)
212
+ source.a = 2
213
+ const target = {}
214
+ expect(deepMerge(target, source)).toEqual({ a: 2 })
215
+ expect("x" in target).toBe(false)
216
+ })
217
+
218
+ it("merges deeply with multiple sources", () => {
219
+ const target = { a: { x: 1 } }
220
+ const s1 = { a: { y: 2 } }
221
+ const s2 = { a: { z: 3 } }
222
+ expect(deepMerge(target, s1, s2)).toEqual({ a: { x: 1, y: 2, z: 3 } })
223
+ })
224
+
225
+ it("does not merge if source value is null", () => {
226
+ const target = { a: { x: 1 } }
227
+ const source = { a: null }
228
+ expect(deepMerge(target, source)).toEqual({ a: null })
229
+ })
230
+
231
+ it("does not merge if target value is null", () => {
232
+ const target = { a: null }
233
+ const source = { a: { x: 1 } }
234
+ expect(deepMerge(target, source)).toEqual({ a: { x: 1 } })
235
+ })
236
+
237
+ it("does not merge arrays deeply", () => {
238
+ const target = { a: [1, 2, 3] }
239
+ const source = { a: [4, 5] }
240
+ expect(deepMerge(target, source)).toEqual({ a: [4, 5] })
241
+ })
242
+
243
+ it("does not merge non-enumerable properties from source", () => {
244
+ const target = {}
245
+ const source = {}
246
+ Object.defineProperty(source, "hidden", {
247
+ value: 123,
248
+ enumerable: false,
249
+ })
250
+ expect(deepMerge(target, source)).toEqual({})
251
+ expect("hidden" in target).toBe(false)
252
+ })
253
+
254
+ it("returns the target object", () => {
255
+ const target = { a: 1 }
256
+ const result = deepMerge(target, { b: 2 })
257
+ expect(result).toBe(target)
258
+ })
259
+ })
package/src/time.js CHANGED
@@ -53,19 +53,27 @@ export function today() {
53
53
  }
54
54
 
55
55
  /**
56
- * Get the day of the week from a YYYY-MM-DD string.
56
+ * Get the day of the week index from a YYYY-MM-DD string.
57
57
  * @param {string=} string YYYY-MM-DD
58
- * @returns {string} "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"
58
+ * @returns {string} 0, 1, 2, 3, 4, 5, 6; 0 is Sunday and, 1 is Monday, ... 6 is Saturday
59
59
  */
60
- export function getDayOfWeek(string = today()) {
60
+ export function getDayIndexInWeek(string = today()) {
61
61
  const [year, month, day] = string.split("-").map(Number)
62
- const dayOfWeek = new Date(year, month - 1, day)
63
- .toLocaleDateString("en-US", { weekday: "long" })
64
- .toLowerCase()
65
- if (dayOfWeek === "invalid date") {
66
- throw new Error(`invalid date passed: ${string}`)
62
+ const index = new Date(year, month - 1, day).getDay()
63
+ if (isNaN(index)) {
64
+ throw new Error(`invalid date: ${string}`)
67
65
  }
68
- return dayOfWeek
66
+ return index
67
+ }
68
+
69
+ /**
70
+ * Get the minute from a time string.
71
+ * @param {string} time HH:mm or HH:mm::ss
72
+ * @returns {number} Between 0 and 59 inclusive
73
+ */
74
+ export function getMinute(time) {
75
+ const minute = Number(time.split(":")[1])
76
+ return minute
69
77
  }
70
78
 
71
79
  /**
package/src/time.test.js CHANGED
@@ -3,8 +3,10 @@ import {
3
3
  addDays,
4
4
  addTime,
5
5
  getDateRange,
6
- getDayOfWeek,
6
+ getDayIndexInWeek,
7
+ // getDayOfWeek, // removed, replaced by getDayIndexInWeek
7
8
  getEasternTime,
9
+ getMinute,
8
10
  getTime,
9
11
  getTimeRange,
10
12
  isDate,
@@ -13,14 +15,12 @@ import {
13
15
  today,
14
16
  } from "./time.js"
15
17
 
16
- // getEasternTime changed: now accepts a "timezone" param and no longer returns "minute" or "datetime"
17
18
  describe("getEasternTime", () => {
18
19
  test("returns correct structure and types", () => {
19
20
  const result = getEasternTime()
20
21
  expect(typeof result.timestamp).toBe("number")
21
22
  expect(typeof result.date).toBe("string")
22
23
  expect(typeof result.time).toBe("string")
23
- // minute and datetime are no longer returned
24
24
  expect(result).not.toHaveProperty("minute")
25
25
  expect(result).not.toHaveProperty("datetime")
26
26
  })
@@ -53,7 +53,6 @@ describe("getEasternTime", () => {
53
53
  expect(def.timestamp).toEqual(explicit.timestamp)
54
54
  })
55
55
 
56
- // New: test timezone override
57
56
  test("respects timezone parameter", () => {
58
57
  // 2024-06-01T12:34:56Z (UTC)
59
58
  const ts = 1717245296
@@ -66,7 +65,6 @@ describe("getEasternTime", () => {
66
65
  expect(eastern.time).not.toBe(pacific.time)
67
66
  expect(eastern.time).not.toBe(utc.time)
68
67
  expect(pacific.time).not.toBe(utc.time)
69
- // Should match expected hour offset
70
68
  expect(eastern.time.startsWith("08:34")).toBe(true) // EDT
71
69
  expect(pacific.time.startsWith("05:34")).toBe(true) // PDT
72
70
  expect(utc.time.startsWith("12:34")).toBe(true) // UTC
@@ -93,7 +91,6 @@ describe("getEasternTime", () => {
93
91
  expect(local.time).toBe(time)
94
92
  })
95
93
 
96
- // DST boundary tests
97
94
  test("handles DST start (spring forward) correctly", () => {
98
95
  // In 2024, DST starts in US/Eastern at 2024-03-10 02:00:00 local time (clocks jump to 03:00:00)
99
96
  // 2024-03-10T06:59:59Z = 1:59:59 EST (should be 01:59:59)
@@ -133,7 +130,6 @@ describe("getEasternTime", () => {
133
130
  })
134
131
  })
135
132
 
136
- // New tests for getTime (newly exported function)
137
133
  describe("getTime", () => {
138
134
  test("returns same structure as getEasternTime", () => {
139
135
  const result = getTime({})
@@ -188,30 +184,52 @@ describe("today", () => {
188
184
  })
189
185
  })
190
186
 
191
- describe("getDayOfWeek", () => {
192
- test("returns correct day of week for known dates", () => {
193
- expect(getDayOfWeek("2024-06-01")).toBe("saturday")
194
- expect(getDayOfWeek("2024-06-02")).toBe("sunday")
195
- expect(getDayOfWeek("2024-06-03")).toBe("monday")
196
- expect(getDayOfWeek("2024-06-04")).toBe("tuesday")
197
- expect(getDayOfWeek("2024-06-05")).toBe("wednesday")
198
- expect(getDayOfWeek("2024-06-06")).toBe("thursday")
199
- expect(getDayOfWeek("2024-06-07")).toBe("friday")
187
+ describe("getDayIndexInWeek", () => {
188
+ test("returns correct index for known dates", () => {
189
+ expect(getDayIndexInWeek("2024-06-02")).toBe(0)
190
+ expect(getDayIndexInWeek("2024-06-03")).toBe(1)
191
+ expect(getDayIndexInWeek("2024-06-04")).toBe(2)
192
+ expect(getDayIndexInWeek("2024-06-05")).toBe(3)
193
+ expect(getDayIndexInWeek("2024-06-06")).toBe(4)
194
+ expect(getDayIndexInWeek("2024-06-07")).toBe(5)
195
+ expect(getDayIndexInWeek("2024-06-08")).toBe(6)
200
196
  })
201
197
 
202
- test("returns correct day for leap day", () => {
203
- expect(getDayOfWeek("2024-02-29")).toBe("thursday")
198
+ test("returns correct index for leap day", () => {
199
+ // 2024-02-29 is Thursday (4)
200
+ expect(getDayIndexInWeek("2024-02-29")).toBe(4)
204
201
  })
205
202
 
206
203
  test("defaults to today() if no argument is given", () => {
207
- // Should match today()'s day of week
208
204
  const todayDate = today()
209
- expect(getDayOfWeek()).toBe(getDayOfWeek(todayDate))
205
+ expect(getDayIndexInWeek()).toBe(getDayIndexInWeek(todayDate))
210
206
  })
211
207
 
212
- test("handles invalid date strings (returns 'invalid date')", () => {
213
- expect(() => getDayOfWeek("not-a-date")).toThrow(/invalid date/u)
214
- expect(getDayOfWeek("2024-02-31")).toBe("saturday")
208
+ test("throws on invalid date strings", () => {
209
+ expect(() => getDayIndexInWeek("not-a-date")).toThrow(/invalid date/u)
210
+ // don't worry about bad dates like the following; can be caught with isDate()
211
+ expect(getDayIndexInWeek("2024-02-31")).toBe(6)
212
+ })
213
+ })
214
+
215
+ // New tests for getMinute
216
+ describe("getMinute", () => {
217
+ test("extracts minute from HH:mm:ss", () => {
218
+ expect(getMinute("12:34:56")).toBe(34)
219
+ expect(getMinute("00:01:00")).toBe(1)
220
+ expect(getMinute("23:59:59")).toBe(59)
221
+ })
222
+
223
+ test("extracts minute from HH:mm", () => {
224
+ expect(getMinute("12:34")).toBe(34)
225
+ expect(getMinute("00:01")).toBe(1)
226
+ expect(getMinute("23:59")).toBe(59)
227
+ })
228
+
229
+ test("handles single-digit minutes", () => {
230
+ expect(getMinute("12:07:00")).toBe(7)
231
+ expect(getMinute("12:7:00")).toBe(7)
232
+ expect(getMinute("12:7")).toBe(7)
215
233
  })
216
234
  })
217
235
 
@@ -327,27 +345,22 @@ describe("addTime", () => {
327
345
  expect(addTime("9:8", { hours: 0, minutes: 0 })).toBe("09:08:00")
328
346
  })
329
347
 
330
- // Edge case: negative minutes that require multiple hour underflows
331
348
  test("handles large negative minutes", () => {
332
349
  expect(addTime("05:10:00", { minutes: -130 })).toBe("03:00:00")
333
350
  })
334
351
 
335
- // Edge case: large positive minutes that require multiple hour rollovers
336
352
  test("handles large positive minutes", () => {
337
353
  expect(addTime("05:10:00", { minutes: 130 })).toBe("07:20:00")
338
354
  })
339
355
 
340
- // Edge case: input with seconds omitted
341
356
  test("handles input with no seconds", () => {
342
357
  expect(addTime("12:34", { minutes: 0 })).toBe("12:34:00")
343
358
  })
344
359
 
345
- // Edge case: input with all zeros
346
360
  test("handles midnight", () => {
347
361
  expect(addTime("00:00:00", { hours: 0, minutes: 0 })).toBe("00:00:00")
348
362
  })
349
363
 
350
- // New: test default parameters (no options argument)
351
364
  test("handles missing options argument (all defaults)", () => {
352
365
  expect(addTime("12:34:56")).toBe("12:34:56")
353
366
  expect(addTime("05:10")).toBe("05:10:00")
@@ -419,14 +432,12 @@ describe("addDays", () => {
419
432
  expect(addDays("2024-01-01", { days: -1 })).toBe("2023-12-31")
420
433
  })
421
434
 
422
- // DST boundary: adding days across US DST start (spring forward)
423
435
  test("adds days across DST start (spring forward) (object param)", () => {
424
436
  expect(addDays("2024-03-09", { days: 1 })).toBe("2024-03-10")
425
437
  expect(addDays("2024-03-09", { days: 2 })).toBe("2024-03-11")
426
438
  expect(addDays("2024-03-10", { days: -1 })).toBe("2024-03-09")
427
439
  })
428
440
 
429
- // DST boundary: adding days across US DST end (fall back)
430
441
  test("adds days across DST end (fall back) (object param)", () => {
431
442
  expect(addDays("2024-11-02", { days: 1 })).toBe("2024-11-03")
432
443
  expect(addDays("2024-11-02", { days: 2 })).toBe("2024-11-04")