@tim-code/my-util 0.7.2 → 0.8.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.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/time.js CHANGED
@@ -1,24 +1,34 @@
1
1
  import { mod } from "./math.js"
2
2
 
3
+ // `floorMinute` at first glance seems ugly and something like `floor: "minute"` seems better.
4
+ // However, practically and realistically, it doesn't seem like we need other values for floor besides "second" and "minute".
5
+ // So, then it seems more problematic to rely on an exact string to be passed... probably would need to throw on some other value.
6
+ // We don't really ever want to disable flooring because we want timestamp, date, and time returned to represent the same thing.
3
7
  /**
4
8
  * Gets various ways of representing the current time in EDT. Floors to nearest second by default.
5
9
  * @param {Object} $1
6
- * @param {boolean=} $1.floorMinute If true, floors to the nearest minute. If false, floors to the nearest second.
7
- * @param {number=} $1.timestamp Unix timestamp to use instead of current time.
8
- * @param {string=} $1.timeZone Time zone to use instead of Eastern time. A falsy value corresponds to local time.
10
+ * @param {number=} $1.timestamp Unix timestamp to use instead of now (in seconds).
11
+ * @param {Date=} $1.dateInstance Alternative to specifying timestamp that is a Date instance.
12
+ * This can be used to specify UTC: `getEasternTime({ dateInstance: new Date(utc) })`.
13
+ * @param {boolean=} $1.floorMinute If true, floors to the nearest minute. If false, floors to the nearest second (default).
14
+ * @param {string=} $1.timeZone Time zone to use instead of Eastern time. A falsy, not-undefined value corresponds to local time.
9
15
  * @returns {Object} { timestamp, date: YYYY-MM-DD, time: HH:mm:ss }
16
+ * If invalid timestamp or dateInstance specified: { timestamp: NaN|Infinity, date: "Invalid date", time: undefined }
10
17
  */
11
18
  export function getEasternTime({
19
+ dateInstance = undefined,
20
+ timestamp = (dateInstance ?? new Date()).getTime() / 1000,
12
21
  floorMinute = false,
13
- timestamp = undefined,
14
22
  timeZone = "America/New_York",
15
23
  } = {}) {
16
- if (!timestamp) {
17
- timestamp = new Date().getTime() / 1000
24
+ if (floorMinute) {
25
+ timestamp = Math.floor(timestamp / 60) * 60
26
+ } else {
27
+ timestamp = Math.floor(timestamp)
18
28
  }
19
- timestamp = floorMinute ? Math.floor(timestamp / 60) * 60 : Math.floor(timestamp)
29
+ const flooredDateInstance = new Date(timestamp * 1000)
20
30
  // 'en-CA' (English - Canada) formats dates as YYYY-MM-DD and times in 24-hour format by default
21
- const string = new Date(timestamp * 1000).toLocaleString("en-CA", {
31
+ const string = flooredDateInstance.toLocaleString("en-CA", {
22
32
  ...(timeZone ? { timeZone } : {}),
23
33
  hour12: false,
24
34
  year: "numeric",
@@ -35,13 +45,26 @@ export function getEasternTime({
35
45
  /**
36
46
  * Gets various ways of representing the current time in local timezone. Floors to nearest second by default.
37
47
  * @param {Object} $1
48
+ * @param {number=} $1.timestamp Unix timestamp to use instead of now (in seconds).
49
+ * @param {Date=} $1.dateInstance Alternative to specifying timestamp that is a Date instance.
50
+ * This can be used to specify UTC: `getEasternTime({ dateInstance: new Date(utc) })`.
38
51
  * @param {boolean=} $1.floorMinute If true, floors to the nearest minute. If false, floors to the nearest second.
39
- * @param {number=} $1.timestamp Unix timestamp to use instead of current time.
40
- * @param {string=} $1.timeZone Time zone to use instead of local time. A falsy value (default) corresponds to local time.
41
- * @returns {Object} { timestamp, date, time, minute, datetime }
52
+ * @param {string=} $1.timeZone Time zone to use instead of local time. A falsy value corresponds to local time (default) .
53
+ * @returns {Object} { timestamp, date: YYYY-MM-DD, time: HH:mm:ss }
54
+ * If invalid timestamp or dateInstance specified: { timestamp: NaN|Infinity, date: "Invalid date", time: undefined }
42
55
  */
43
- export function getTime({ floorMinute, timestamp, timeZone = false } = {}) {
44
- return getEasternTime({ floorMinute, timestamp, timeZone })
56
+ export function getTime({ timestamp, dateInstance, floorMinute, timeZone = false } = {}) {
57
+ return getEasternTime({ timestamp, dateInstance, floorMinute, timeZone })
58
+ }
59
+
60
+ /**
61
+ * Get Unix timestamp for a UTC date string.
62
+ * @param {string} utc UTC date time: "YYYY-MM-DDTHH:mm:ssZ" or with UTC offset
63
+ * @returns {number} Seconds since epoch
64
+ */
65
+ export function getUnixTimestamp(utc) {
66
+ const timestamp = Math.floor(new Date(utc).getTime() / 1000)
67
+ return timestamp
45
68
  }
46
69
 
47
70
  /**
@@ -97,30 +120,41 @@ export function isDateString(string) {
97
120
  }
98
121
 
99
122
  /**
100
- * @deprecated Prefer isDateString()
123
+ * Checks if the string represents a valid HH:mm:ss time.
124
+ * This will return false for times like "24:00:00".
125
+ * Does not handle milliseconds.
101
126
  * @param {string} string
102
127
  * @returns {boolean}
103
128
  */
104
- export function isDate(string) {
105
- return isDateString(string)
129
+ export function isTimeString(string) {
130
+ return /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/u.test(string)
106
131
  }
107
132
 
108
133
  /**
109
- * Checks if the string represent a valid HH:mm:ss time.
134
+ * Checks if the string represents a valid date time string similar to YYYY-MM-DDTHH:mm:ss.
135
+ * Does not handle milliseconds.
110
136
  * @param {string} string
137
+ * @param {Object} $1
138
+ * @param {string=} $1.separator Can specify a different string separator between date and time. Defaults to "T".
111
139
  * @returns {boolean}
112
140
  */
113
- export function isTimeString(string) {
114
- return /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/u.test(string)
141
+ export function isDateTimeString(string, { separator = "T" } = {}) {
142
+ const [date, time] = string.split(separator)
143
+ return isDateString(date) && isTimeString(time)
115
144
  }
116
145
 
117
146
  /**
118
- * @deprecated Prefer isTimeString()
147
+ * Checks if the string represents a valid UTC date time similar to YYYY-MM-DDTHH:mm:ssZ.
148
+ * Does not handle milliseconds or UTC offsets.
119
149
  * @param {string} string
120
150
  * @returns {boolean}
121
151
  */
122
- export function isTime(string) {
123
- return isTimeString(string)
152
+ export function isUTCString(string) {
153
+ if (!string.endsWith("Z")) {
154
+ return false
155
+ }
156
+ const datetime = string.slice(0, string.length - 1)
157
+ return isDateTimeString(datetime)
124
158
  }
125
159
 
126
160
  /**
package/src/time.test.js CHANGED
@@ -9,14 +9,20 @@ import {
9
9
  getStartOfWeek,
10
10
  getTime,
11
11
  getTimeRange,
12
- isDate,
12
+ getUnixTimestamp,
13
13
  isDateString,
14
- isTime,
14
+ isDateTimeString,
15
15
  isTimeString,
16
+ isUTCString,
16
17
  isUnixTimestamp,
17
18
  today,
18
19
  } from "./time.js"
19
20
 
21
+ // Exported functions:
22
+ // getEasternTime, getTime, getUnixTimestamp, today, getDayIndexInWeek, getMinute,
23
+ // isDateString, isTimeString, isDateTimeString, isUTCString, isUnixTimestamp,
24
+ // addTime, getTimeRange, addDays, getDateRange, getStartOfWeek
25
+
20
26
  describe("getEasternTime", () => {
21
27
  test("returns correct structure and types", () => {
22
28
  const result = getEasternTime()
@@ -47,6 +53,22 @@ describe("getEasternTime", () => {
47
53
  expect(floored.time.endsWith(":00")).toBe(true)
48
54
  })
49
55
 
56
+ test("accepts dateInstance and matches equivalent timestamp", () => {
57
+ const ts = 1717245296 // 2024-06-01T12:34:56Z
58
+ const di = new Date(ts * 1000)
59
+ const a = getEasternTime({ dateInstance: di, timeZone: "UTC" })
60
+ const b = getEasternTime({ timestamp: ts, timeZone: "UTC" })
61
+ expect(a).toEqual(b)
62
+ })
63
+
64
+ test("dateInstance path respects floorMinute", () => {
65
+ const ts = 1717245296 // ...:56
66
+ const di = new Date(ts * 1000)
67
+ const floored = getEasternTime({ dateInstance: di, floorMinute: true, timeZone: "UTC" })
68
+ expect(floored.timestamp % 60).toBe(0)
69
+ expect(floored.time).toBe("12:34:00")
70
+ })
71
+
50
72
  test("default parameters yield consistent output", () => {
51
73
  const def = getEasternTime()
52
74
  const explicit = getEasternTime({ floorMinute: false })
@@ -170,6 +192,14 @@ describe("getTime", () => {
170
192
  expect(pacific).toEqual(ref)
171
193
  })
172
194
 
195
+ test("accepts dateInstance and matches equivalent timestamp", () => {
196
+ const ts = 1717245296
197
+ const di = new Date(ts * 1000)
198
+ const a = getTime({ dateInstance: di, timeZone: "UTC" })
199
+ const b = getTime({ timestamp: ts, timeZone: "UTC" })
200
+ expect(a).toEqual(b)
201
+ })
202
+
173
203
  test("floors to minute if floorMinute is true", () => {
174
204
  const ts = 1717245296
175
205
  const floored = getTime({ timestamp: ts, floorMinute: true })
@@ -178,6 +208,26 @@ describe("getTime", () => {
178
208
  })
179
209
  })
180
210
 
211
+ describe("getUnixTimestamp", () => {
212
+ test("parses Zulu (UTC) timestamp", () => {
213
+ expect(getUnixTimestamp("2024-06-01T12:34:56Z")).toBe(1717245296)
214
+ })
215
+
216
+ test("parses timestamp with positive offset", () => {
217
+ const expected = Date.UTC(2024, 5, 1, 10, 34, 56) / 1000 // 12:34:56+02:00 == 10:34:56Z
218
+ expect(getUnixTimestamp("2024-06-01T12:34:56+02:00")).toBe(expected)
219
+ })
220
+
221
+ test("floors fractional seconds", () => {
222
+ // 999ms floors down to the same second
223
+ expect(getUnixTimestamp("1970-01-01T00:00:00.999Z")).toBe(0)
224
+ })
225
+
226
+ test("returns NaN for invalid date string", () => {
227
+ expect(Number.isNaN(getUnixTimestamp("not-a-date"))).toBe(true)
228
+ })
229
+ })
230
+
181
231
  describe("today", () => {
182
232
  test("returns today's date in YYYY-MM-DD format", () => {
183
233
  const expected = getTime().date
@@ -214,7 +264,6 @@ describe("getDayIndexInWeek", () => {
214
264
  })
215
265
  })
216
266
 
217
- // New tests for getMinute
218
267
  describe("getMinute", () => {
219
268
  test("extracts minute from HH:mm:ss", () => {
220
269
  expect(getMinute("12:34:56")).toBe(34)
@@ -247,67 +296,51 @@ describe("isDateString", () => {
247
296
  })
248
297
  })
249
298
 
250
- describe("isDate (deprecated wrapper)", () => {
251
- test("returns true for valid YYYY-MM-DD dates", () => {
252
- expect(isDate("2024-06-01")).toBe(true)
253
- expect(isDate("1999-12-31")).toBe(true)
299
+ describe("isTimeString", () => {
300
+ test("validates correct times", () => {
301
+ expect(isTimeString("00:00:00")).toBe(true)
302
+ expect(isTimeString("23:59:59")).toBe(true)
254
303
  })
255
304
 
256
- test("returns false for invalid dates (e.g., 2024-02-31)", () => {
257
- expect(isDate("2024-02-31")).toBe(false)
258
- expect(isDate("2023-04-31")).toBe(false)
305
+ test("rejects invalid times and formats", () => {
306
+ expect(isTimeString("24:00:00")).toBe(false)
307
+ expect(isTimeString("12:34")).toBe(false)
259
308
  })
309
+ })
260
310
 
261
- test("returns false for invalid formats", () => {
262
- expect(isDate("2024/06/01")).toBe(false)
263
- expect(isDate("06-01-2024")).toBe(false)
264
- expect(isDate("2024-6-1")).toBe(false)
265
- expect(isDate("20240601")).toBe(false)
266
- expect(isDate("abcd-ef-gh")).toBe(false)
311
+ describe("isDateTimeString", () => {
312
+ test("valid with default T separator", () => {
313
+ expect(isDateTimeString("2024-06-01T12:34:56")).toBe(true)
267
314
  })
268
315
 
269
- test("returns false for impossible months and days", () => {
270
- expect(isDate("2024-00-10")).toBe(false)
271
- expect(isDate("2024-13-10")).toBe(false)
272
- expect(isDate("2024-01-00")).toBe(false)
273
- expect(isDate("2024-01-32")).toBe(false)
316
+ test("rejects invalid combinations", () => {
317
+ expect(isDateTimeString("2024-06-01T12:34")).toBe(false) // missing seconds
318
+ expect(isDateTimeString("2024-06-01T24:00:00")).toBe(false) // invalid hour
319
+ expect(isDateTimeString("2024-02-31T12:00:00")).toBe(false) // invalid date
320
+ expect(isDateTimeString("2024-06-01 12:34:56")).toBe(false) // wrong separator
274
321
  })
275
322
 
276
- test("returns true for leap day", () => {
277
- expect(isDate("2024-02-29")).toBe(true)
278
- expect(isDate("2023-02-29")).toBe(false)
323
+ test("supports custom separator", () => {
324
+ expect(isDateTimeString("2024-06-01 12:34:56", { separator: " " })).toBe(true)
325
+ expect(isDateTimeString("2024-06-01T12:34:56", { separator: " " })).toBe(false)
279
326
  })
280
327
  })
281
328
 
282
- describe("isTimeString", () => {
283
- test("validates correct times", () => {
284
- expect(isTimeString("00:00:00")).toBe(true)
285
- expect(isTimeString("23:59:59")).toBe(true)
329
+ describe("isUTCString", () => {
330
+ test("validates proper Zulu UTC format", () => {
331
+ expect(isUTCString("2024-06-01T12:34:56Z")).toBe(true)
286
332
  })
287
333
 
288
- test("rejects invalid times and formats", () => {
289
- expect(isTimeString("24:00:00")).toBe(false)
290
- expect(isTimeString("12:34")).toBe(false)
334
+ test("rejects strings without trailing Z", () => {
335
+ expect(isUTCString("2024-06-01T12:34:56")).toBe(false)
291
336
  })
292
- })
293
337
 
294
- describe("isTime (deprecated wrapper)", () => {
295
- test("returns true for valid HH:mm:ss times", () => {
296
- expect(isTime("00:00:00")).toBe(true)
297
- expect(isTime("23:59:59")).toBe(true)
298
- expect(isTime("12:34:56")).toBe(true)
299
- })
300
-
301
- test("returns false for invalid times", () => {
302
- expect(isTime("24:00:00")).toBe(false)
303
- expect(isTime("12:60:00")).toBe(false)
304
- expect(isTime("12:00:60")).toBe(false)
305
- expect(isTime("1:00:00")).toBe(false)
306
- expect(isTime("12:0:00")).toBe(false)
307
- expect(isTime("12:00:0")).toBe(false)
308
- expect(isTime("12:00")).toBe(false)
309
- expect(isTime("120000")).toBe(false)
310
- expect(isTime("ab:cd:ef")).toBe(false)
338
+ test("rejects invalid time/date and unsupported variants", () => {
339
+ expect(isUTCString("2024-06-01T24:00:00Z")).toBe(false) // invalid hour
340
+ expect(isUTCString("2024-02-31T12:00:00Z")).toBe(false) // invalid date
341
+ expect(isUTCString("2024-06-01T12:34:56+00:00")).toBe(false) // offset not supported
342
+ expect(isUTCString("2024-06-01T12:34:56.123Z")).toBe(false) // milliseconds not supported
343
+ expect(isUTCString("2024-06-01 12:34:56Z")).toBe(false) // wrong separator
311
344
  })
312
345
  })
313
346
 
@@ -428,6 +461,14 @@ describe("getTimeRange", () => {
428
461
  test("handles input with no seconds", () => {
429
462
  expect(getTimeRange("12:00", "12:02")).toEqual(["12:00:00", "12:01:00", "12:02:00"])
430
463
  })
464
+
465
+ // ISSUE: getTimeRange allows a zero step, which would otherwise loop forever; it's capped internally at 1440 iterations.
466
+ test("caps the number of results at MINUTES_IN_DAY when step is zero", () => {
467
+ const result = getTimeRange("12:00:00", "12:00:00", { hours: 0, minutes: 0 })
468
+ expect(result.length).toBe(24 * 60)
469
+ expect(result[0]).toBe("12:00:00")
470
+ expect(result[result.length - 1]).toBe("12:00:00")
471
+ })
431
472
  })
432
473
 
433
474
  describe("addDays", () => {
@@ -519,7 +560,6 @@ describe("getDateRange", () => {
519
560
  })
520
561
  })
521
562
 
522
- // Tests for getStartOfWeek (newly exported function)
523
563
  describe("getStartOfWeek", () => {
524
564
  test("returns same date if input is Sunday", () => {
525
565
  expect(getStartOfWeek("2024-06-02")).toBe("2024-06-02") // Sunday