@naturalcycles/js-lib 14.88.0 → 14.91.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.
@@ -0,0 +1,401 @@
1
+ import { _assert } from '../error/assert'
2
+ import { IsoDateTime, UnixTimestamp } from '../types'
3
+
4
+ export type LocalTimeUnit = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'
5
+
6
+ export type LocalTimeConfig = LocalTime | Date | IsoDateTime | UnixTimestamp
7
+
8
+ export interface LocalTimeComponents {
9
+ year: number
10
+ month: number
11
+ day: number
12
+ hour: number
13
+ minute: number
14
+ second: number
15
+ }
16
+
17
+ /* eslint-disable no-dupe-class-members */
18
+
19
+ // Design choices:
20
+ // No milliseconds
21
+ // No timezone support, ISO8601 is parsed as LocalDateTime, discarding Timezone information
22
+ // Formats as unix timestamp, ISO8601 or "pretty string"
23
+ // toString and .toJSON formats as unix timestamp
24
+ // No "unixMillis", just pure unixtimestamp
25
+ // .valueOf returns unix timestamp (no millis)
26
+ // Prevents dayjs(undefined) being dayjs.now()
27
+ // Validates on parse, throws if invalid. Doesn't allow invalid objects
28
+ /**
29
+ * @experimental
30
+ */
31
+ export class LocalTime {
32
+ private constructor(private $date: Date) {}
33
+
34
+ /**
35
+ * Parses input String into LocalDate.
36
+ * Input can already be a LocalDate - it is returned as-is in that case.
37
+ */
38
+ static of(d: LocalTimeConfig): LocalTime {
39
+ const t = this.parseOrNull(d)
40
+
41
+ if (t === null) {
42
+ throw new TypeError(`Cannot parse "${d}" into LocalTime`)
43
+ }
44
+
45
+ return t
46
+ }
47
+
48
+ /**
49
+ * Returns null if invalid
50
+ */
51
+ static parseOrNull(d: LocalTimeConfig): LocalTime | null {
52
+ if (d instanceof LocalTime) return d
53
+
54
+ let date
55
+
56
+ if (d instanceof Date) {
57
+ date = d
58
+ } else if (typeof d === 'number') {
59
+ date = new Date(d * 1000)
60
+ } else {
61
+ date = new Date(d)
62
+ }
63
+
64
+ // validation
65
+ if (isNaN(date.getDate())) {
66
+ // throw new TypeError(`Cannot parse "${d}" into LocalTime`)
67
+ return null
68
+ }
69
+
70
+ return new LocalTime(date)
71
+ }
72
+
73
+ static isValid(d: LocalTimeConfig): boolean {
74
+ return this.parseOrNull(d) !== null
75
+ }
76
+
77
+ static unix(ts: UnixTimestamp): LocalTime {
78
+ return new LocalTime(new Date(ts * 1000))
79
+ }
80
+
81
+ static now(): LocalTime {
82
+ return this.of(new Date())
83
+ }
84
+
85
+ static fromComponents(
86
+ c: { year: number; month: number } & Partial<LocalTimeComponents>,
87
+ ): LocalTime {
88
+ return new LocalTime(new Date(c.year, c.month - 1, c.day, c.hour, c.minute, c.second))
89
+ }
90
+
91
+ get(unit: LocalTimeUnit): number {
92
+ if (unit === 'year') {
93
+ return this.$date.getFullYear()
94
+ }
95
+ if (unit === 'month') {
96
+ return this.$date.getMonth() + 1
97
+ }
98
+ if (unit === 'day') {
99
+ return this.$date.getDate()
100
+ }
101
+ if (unit === 'hour') {
102
+ return this.$date.getHours()
103
+ }
104
+ if (unit === 'minute') {
105
+ return this.$date.getMinutes()
106
+ }
107
+ // second
108
+ return this.$date.getSeconds()
109
+ }
110
+
111
+ set(unit: LocalTimeUnit, v: number, mutate = false): LocalTime {
112
+ const t = mutate ? this : this.clone()
113
+
114
+ if (unit === 'year') {
115
+ t.$date.setFullYear(v)
116
+ } else if (unit === 'month') {
117
+ t.$date.setMonth(v - 1)
118
+ } else if (unit === 'day') {
119
+ t.$date.setDate(v)
120
+ } else if (unit === 'hour') {
121
+ t.$date.setHours(v)
122
+ } else if (unit === 'minute') {
123
+ t.$date.setMinutes(v)
124
+ } else if (unit === 'second') {
125
+ t.$date.setSeconds(v)
126
+ }
127
+
128
+ return t
129
+ }
130
+
131
+ year(): number
132
+ year(v: number): LocalTime
133
+ year(v?: number): number | LocalTime {
134
+ return v === undefined ? this.$date.getFullYear() : this.set('year', v)
135
+ }
136
+ month(): number
137
+ month(v: number): LocalTime
138
+ month(v?: number): number | LocalTime {
139
+ return v === undefined ? this.$date.getMonth() + 1 : this.set('month', v)
140
+ }
141
+ date(): number
142
+ date(v: number): LocalTime
143
+ date(v?: number): number | LocalTime {
144
+ return v === undefined ? this.$date.getDate() : this.set('day', v)
145
+ }
146
+ hour(): number
147
+ hour(v: number): LocalTime
148
+ hour(v?: number): number | LocalTime {
149
+ return v === undefined ? this.$date.getHours() : this.set('hour', v)
150
+ }
151
+ minute(): number
152
+ minute(v: number): LocalTime
153
+ minute(v?: number): number | LocalTime {
154
+ return v === undefined ? this.$date.getMinutes() : this.set('minute', v)
155
+ }
156
+ second(): number
157
+ second(v: number): LocalTime
158
+ second(v?: number): number | LocalTime {
159
+ return v === undefined ? this.$date.getSeconds() : this.set('second', v)
160
+ }
161
+
162
+ setComponents(c: Partial<LocalTimeComponents>, mutate = false): LocalTime {
163
+ const d = mutate ? this.$date : new Date(this.$date)
164
+
165
+ if (c.year) {
166
+ d.setFullYear(c.year)
167
+ }
168
+ if (c.month) {
169
+ d.setMonth(c.month - 1)
170
+ }
171
+ if (c.day) {
172
+ d.setDate(c.day)
173
+ }
174
+ if (c.hour !== undefined) {
175
+ d.setHours(c.hour)
176
+ }
177
+ if (c.minute !== undefined) {
178
+ d.setMinutes(c.minute)
179
+ }
180
+ if (c.second !== undefined) {
181
+ d.setSeconds(c.second)
182
+ }
183
+
184
+ return mutate ? this : new LocalTime(d)
185
+ }
186
+
187
+ add(num: number, unit: LocalTimeUnit, mutate = false): LocalTime {
188
+ return this.set(unit, this.get(unit) + num, mutate)
189
+ }
190
+
191
+ subtract(num: number, unit: LocalTimeUnit, mutate = false): LocalTime {
192
+ return this.add(-num, unit, mutate)
193
+ }
194
+
195
+ absDiff(other: LocalTimeConfig, unit: LocalTimeUnit): number {
196
+ return Math.abs(this.diff(other, unit))
197
+ }
198
+
199
+ diff(other: LocalTimeConfig, unit: LocalTimeUnit): number {
200
+ const date2 = LocalTime.of(other).$date
201
+
202
+ if (unit === 'year') {
203
+ return this.$date.getFullYear() - date2.getFullYear()
204
+ }
205
+ if (unit === 'month') {
206
+ return (
207
+ (this.$date.getFullYear() - date2.getFullYear()) * 12 +
208
+ this.$date.getMonth() -
209
+ date2.getMonth()
210
+ )
211
+ }
212
+
213
+ const secDiff = (this.$date.valueOf() - date2.valueOf()) / 1000
214
+ let r
215
+
216
+ if (unit === 'day') {
217
+ r = secDiff / (24 * 60 * 60)
218
+ } else if (unit === 'hour') {
219
+ r = secDiff / (60 * 60)
220
+ } else if (unit === 'minute') {
221
+ r = secDiff / 60
222
+ } else {
223
+ // unit === 'second'
224
+ r = secDiff
225
+ }
226
+
227
+ r = r < 0 ? -Math.floor(-r) : Math.floor(r)
228
+ if (Object.is(r, -0)) return 0
229
+ return r
230
+ }
231
+
232
+ startOf(unit: LocalTimeUnit, mutate = false): LocalTime {
233
+ if (unit === 'second') return this
234
+
235
+ if (mutate) {
236
+ const d = this.$date
237
+ d.setSeconds(0)
238
+ if (unit === 'minute') return this
239
+ d.setMinutes(0)
240
+ if (unit === 'hour') return this
241
+ d.setHours(0)
242
+ if (unit === 'day') return this
243
+ d.setDate(0)
244
+ if (unit === 'month') return this
245
+ d.setMonth(0)
246
+ return this
247
+ }
248
+
249
+ const c = this.components()
250
+
251
+ c.second = 0
252
+ if (unit === 'year') {
253
+ c.month = c.day = 1
254
+ c.hour = c.minute = 0
255
+ } else if (unit === 'month') {
256
+ c.day = 1
257
+ c.hour = c.minute = 0
258
+ } else if (unit === 'day') {
259
+ c.hour = c.minute = 0
260
+ } else if (unit === 'hour') {
261
+ c.minute = 0
262
+ }
263
+
264
+ return LocalTime.fromComponents(c)
265
+ }
266
+
267
+ static sort(items: LocalTime[], mutate = false, descending = false): LocalTime[] {
268
+ const mod = descending ? -1 : 1
269
+ return (mutate ? items : [...items]).sort((a, b) => {
270
+ const v1 = a.$date.valueOf()
271
+ const v2 = b.$date.valueOf()
272
+ if (v1 === v2) return 0
273
+ return (v1 < v2 ? -1 : 1) * mod
274
+ })
275
+ }
276
+
277
+ static earliestOrUndefined(items: LocalTime[]): LocalTime | undefined {
278
+ return items.length ? LocalTime.earliest(items) : undefined
279
+ }
280
+
281
+ static earliest(items: LocalTime[]): LocalTime {
282
+ _assert(items.length, 'LocalTime.earliest called on empty array')
283
+
284
+ return items.reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
285
+ }
286
+
287
+ static latestOrUndefined(items: LocalTime[]): LocalTime | undefined {
288
+ return items.length ? LocalTime.latest(items) : undefined
289
+ }
290
+
291
+ static latest(items: LocalTime[]): LocalTime {
292
+ _assert(items.length, 'LocalTime.latest called on empty array')
293
+
294
+ return items.reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
295
+ }
296
+
297
+ isSame(d: LocalTimeConfig): boolean {
298
+ return this.cmp(d) === 0
299
+ }
300
+
301
+ isBefore(d: LocalTimeConfig): boolean {
302
+ return this.cmp(d) === -1
303
+ }
304
+
305
+ isSameOrBefore(d: LocalTimeConfig): boolean {
306
+ return this.cmp(d) <= 0
307
+ }
308
+
309
+ isAfter(d: LocalTimeConfig): boolean {
310
+ return this.cmp(d) === 1
311
+ }
312
+
313
+ isSameOrAfter(d: LocalTimeConfig): boolean {
314
+ return this.cmp(d) >= 0
315
+ }
316
+
317
+ /**
318
+ * Returns 1 if this > d
319
+ * returns 0 if they are equal
320
+ * returns -1 if this < d
321
+ */
322
+ cmp(d: LocalTimeConfig): -1 | 0 | 1 {
323
+ const t1 = this.$date.valueOf()
324
+ const t2 = LocalTime.of(d).$date.valueOf()
325
+ if (t1 === t2) return 0
326
+ return t1 < t2 ? -1 : 1
327
+ }
328
+
329
+ // todo: endOf
330
+
331
+ components(): LocalTimeComponents {
332
+ return {
333
+ year: this.$date.getFullYear(),
334
+ month: this.$date.getMonth() + 1,
335
+ day: this.$date.getDate(),
336
+ hour: this.$date.getHours(),
337
+ minute: this.$date.getMinutes(),
338
+ second: this.$date.getSeconds(),
339
+ }
340
+ }
341
+
342
+ getDate(): Date {
343
+ return this.$date
344
+ }
345
+
346
+ clone(): LocalTime {
347
+ return new LocalTime(new Date(this.$date))
348
+ }
349
+
350
+ unix(): UnixTimestamp {
351
+ return Math.floor(this.$date.valueOf() / 1000)
352
+ }
353
+
354
+ valueOf(): UnixTimestamp {
355
+ return Math.floor(this.$date.valueOf() / 1000)
356
+ }
357
+
358
+ toISO8601(): IsoDateTime {
359
+ return this.$date.toISOString().slice(0, 19)
360
+ }
361
+
362
+ toPretty(seconds = true): IsoDateTime {
363
+ return this.$date
364
+ .toISOString()
365
+ .slice(0, seconds ? 19 : 16)
366
+ .split('T')
367
+ .join(' ')
368
+ }
369
+
370
+ /**
371
+ * Returns e.g: `19840621_1705`
372
+ */
373
+ toStringCompact(seconds = false): string {
374
+ return [
375
+ String(this.$date.getFullYear()).padStart(4, '0'),
376
+ String(this.$date.getMonth() + 1).padStart(2, '0'),
377
+ String(this.$date.getDate()).padStart(2, '0'),
378
+ '_',
379
+ String(this.$date.getHours()).padStart(2, '0'),
380
+ String(this.$date.getMinutes()).padStart(2, '0'),
381
+ seconds ? String(this.$date.getSeconds()).padStart(2, '0') : '',
382
+ ].join('')
383
+ }
384
+
385
+ toString(): string {
386
+ return String(this.unix())
387
+ }
388
+
389
+ toJSON(): UnixTimestamp {
390
+ return this.unix()
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Shortcut wrapper around `LocalDate.parse` / `LocalDate.today`
396
+ */
397
+ export function localTime(d?: LocalTimeConfig): LocalTime {
398
+ return d ? LocalTime.of(d) : LocalTime.now()
399
+ }
400
+
401
+ // todo: range
package/src/index.ts CHANGED
@@ -156,8 +156,17 @@ import { PQueue, PQueueCfg } from './promise/pQueue'
156
156
  export * from './seq/seq'
157
157
  export * from './math/stack.util'
158
158
  export * from './string/leven'
159
+ export * from './datetime/localDate'
160
+ export * from './datetime/localTime'
161
+ import { LocalDateConfig, LocalDateUnit } from './datetime/localDate'
162
+ import { LocalTimeConfig, LocalTimeUnit, LocalTimeComponents } from './datetime/localTime'
159
163
 
160
164
  export type {
165
+ LocalDateConfig,
166
+ LocalDateUnit,
167
+ LocalTimeConfig,
168
+ LocalTimeUnit,
169
+ LocalTimeComponents,
161
170
  AbortableMapper,
162
171
  AbortablePredicate,
163
172
  AbortableAsyncPredicate,