@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,73 @@
1
+ import { LocalDateUnit, LocalDate, LocalDateConfig } from '../datetime/localDate'
2
+
3
+ export class LazyLocalDate {
4
+ constructor(private str: string) {}
5
+
6
+ private ld?: LocalDate
7
+
8
+ eq(d: LocalDateConfig): boolean {
9
+ if (typeof d === 'string') return d === this.str
10
+ this.ld ||= LocalDate.of(this.str)
11
+ return this.ld.isSame(d)
12
+ }
13
+
14
+ lt(d: LocalDateConfig): boolean {
15
+ return this.cmp(d) === -1
16
+ }
17
+
18
+ lte(d: LocalDateConfig): boolean {
19
+ return this.cmp(d) <= 0
20
+ }
21
+
22
+ gt(d: LocalDateConfig): boolean {
23
+ return this.cmp(d) === 1
24
+ }
25
+
26
+ gte(d: LocalDateConfig): boolean {
27
+ return this.cmp(d) >= 0
28
+ }
29
+
30
+ cmp(d: LocalDateConfig): -1 | 0 | 1 {
31
+ if (typeof d === 'string') {
32
+ return this.str < d ? -1 : this.str > d ? 1 : 0
33
+ }
34
+
35
+ this.ld ||= LocalDate.of(this.str)
36
+ return this.ld.cmp(d)
37
+ }
38
+
39
+ absDiff(d: LocalDateConfig, unit: LocalDateUnit): number {
40
+ return Math.abs(this.diff(d, unit))
41
+ }
42
+
43
+ diff(d: LocalDateConfig, unit: LocalDateUnit): number {
44
+ this.ld ||= LocalDate.of(this.str)
45
+ return this.ld.diff(d, unit)
46
+ }
47
+
48
+ add(num: number, unit: LocalDateUnit): LocalDate {
49
+ this.ld ||= LocalDate.of(this.str)
50
+ return this.ld.add(num, unit)
51
+ }
52
+
53
+ subtract(num: number, unit: LocalDateUnit): LocalDate {
54
+ return this.add(-num, unit)
55
+ }
56
+
57
+ clone(): LazyLocalDate {
58
+ return new LazyLocalDate(this.str)
59
+ }
60
+
61
+ toDate(): Date {
62
+ this.ld ||= LocalDate.of(this.str)
63
+ return this.ld.toDate()
64
+ }
65
+
66
+ toString(): string {
67
+ return this.str
68
+ }
69
+
70
+ toJSON(): string {
71
+ return this.str
72
+ }
73
+ }
@@ -0,0 +1,385 @@
1
+ import { _assert } from '../error/assert'
2
+ import { Sequence } from '../seq/seq'
3
+ import { END, IsoDate } from '../types'
4
+
5
+ export type LocalDateUnit = 'year' | 'month' | 'day'
6
+
7
+ const m31 = new Set<number>([1, 3, 5, 7, 8, 10, 12])
8
+
9
+ export type LocalDateConfig = LocalDate | string
10
+
11
+ /**
12
+ * @experimental
13
+ */
14
+ export class LocalDate {
15
+ private constructor(public year: number, public month: number, public day: number) {}
16
+
17
+ static create(year: number, month: number, day: number): LocalDate {
18
+ return new LocalDate(year, month, day)
19
+ }
20
+
21
+ /**
22
+ * Parses input String into LocalDate.
23
+ * Input can already be a LocalDate - it is returned as-is in that case.
24
+ */
25
+ static of(d: LocalDateConfig): LocalDate {
26
+ const t = this.parseOrNull(d)
27
+
28
+ if (t === null) {
29
+ throw new Error(`Cannot parse "${d}" into LocalDate`)
30
+ }
31
+
32
+ return t
33
+ }
34
+
35
+ static parseCompact(d: string): LocalDate {
36
+ const [year, month, day] = [d.slice(0, 4), d.slice(4, 2), d.slice(6, 2)].map(Number)
37
+
38
+ if (!day || !month || !year) {
39
+ throw new Error(`Cannot parse "${d}" into LocalDate`)
40
+ }
41
+
42
+ return new LocalDate(year, month, day)
43
+ }
44
+
45
+ static fromDate(d: Date): LocalDate {
46
+ return new LocalDate(d.getFullYear(), d.getMonth() + 1, d.getDate())
47
+ }
48
+
49
+ /**
50
+ * Returns null if invalid.
51
+ */
52
+ static parseOrNull(d: LocalDateConfig): LocalDate | null {
53
+ if (d instanceof LocalDate) return d
54
+
55
+ // todo: explore more performant options
56
+ const [year, month, day] = d.slice(0, 10).split('-').map(Number)
57
+
58
+ if (
59
+ !year ||
60
+ !month ||
61
+ month < 1 ||
62
+ month > 12 ||
63
+ !day ||
64
+ day < 1 ||
65
+ day > this.getMonthLength(year, month)
66
+ ) {
67
+ return null
68
+ }
69
+
70
+ return new LocalDate(year, month, day)
71
+ }
72
+
73
+ static isValid(iso: string): boolean {
74
+ return this.parseOrNull(iso) !== null
75
+ }
76
+
77
+ static today(): LocalDate {
78
+ return this.fromDate(new Date())
79
+ }
80
+
81
+ static sort(items: LocalDate[], mutate = false, descending = false): LocalDate[] {
82
+ const mod = descending ? -1 : 1
83
+ return (mutate ? items : [...items]).sort((a, b) => a.cmp(b) * mod)
84
+ }
85
+
86
+ static earliestOrUndefined(items: LocalDate[]): LocalDate | undefined {
87
+ return items.length ? LocalDate.earliest(items) : undefined
88
+ }
89
+
90
+ static earliest(items: LocalDate[]): LocalDate {
91
+ _assert(items.length, 'LocalDate.earliest called on empty array')
92
+
93
+ return items.reduce((min, item) => (min.isSameOrBefore(item) ? min : item))
94
+ }
95
+
96
+ static latestOrUndefined(items: LocalDate[]): LocalDate | undefined {
97
+ return items.length ? LocalDate.latest(items) : undefined
98
+ }
99
+
100
+ static latest(items: LocalDate[]): LocalDate {
101
+ _assert(items.length, 'LocalDate.latest called on empty array')
102
+
103
+ return items.reduce((max, item) => (max.isSameOrAfter(item) ? max : item))
104
+ }
105
+
106
+ static range(
107
+ minIncl: LocalDateConfig,
108
+ maxExcl: LocalDateConfig,
109
+ step = 1,
110
+ stepUnit: LocalDateUnit = 'day',
111
+ ): LocalDate[] {
112
+ const days: LocalDate[] = []
113
+ let current = LocalDate.of(minIncl).startOf(stepUnit)
114
+ const max = LocalDate.of(maxExcl).startOf(stepUnit)
115
+
116
+ do {
117
+ days.push(current)
118
+ current = current.add(step, stepUnit)
119
+ } while (current.isBefore(max))
120
+
121
+ return days
122
+ }
123
+
124
+ static rangeSeq(
125
+ minIncl: LocalDateConfig,
126
+ maxExcl: LocalDateConfig,
127
+ step = 1,
128
+ stepUnit: LocalDateUnit = 'day',
129
+ ): Sequence<LocalDate> {
130
+ const min = LocalDate.of(minIncl).startOf(stepUnit)
131
+ const max = LocalDate.of(maxExcl).startOf(stepUnit)
132
+ return Sequence.create(min, d => {
133
+ const next = d.add(step, stepUnit)
134
+ return next.isAfter(max) ? END : next
135
+ })
136
+ }
137
+
138
+ static rangeString(
139
+ minIncl: LocalDateConfig,
140
+ maxExcl: LocalDateConfig,
141
+ step = 1,
142
+ stepUnit: LocalDateUnit = 'day',
143
+ ): IsoDate[] {
144
+ return LocalDate.range(minIncl, maxExcl, step, stepUnit).map(ld => ld.toString())
145
+ }
146
+
147
+ static rangeIncl(
148
+ minIncl: LocalDateConfig,
149
+ maxIncl: LocalDateConfig,
150
+ step = 1,
151
+ stepUnit: LocalDateUnit = 'day',
152
+ ): LocalDate[] {
153
+ return LocalDate.range(minIncl, LocalDate.of(maxIncl).add(1, stepUnit), step, stepUnit)
154
+ }
155
+
156
+ static rangeInclString(
157
+ minIncl: LocalDateConfig,
158
+ maxIncl: LocalDateConfig,
159
+ step = 1,
160
+ stepUnit: LocalDateUnit = 'day',
161
+ ): IsoDate[] {
162
+ return LocalDate.range(minIncl, LocalDate.of(maxIncl).add(1, stepUnit), step, stepUnit).map(
163
+ ld => ld.toString(),
164
+ )
165
+ }
166
+
167
+ isSame(d: LocalDateConfig): boolean {
168
+ d = LocalDate.of(d)
169
+ return this.day === d.day && this.month === d.month && this.year === d.year
170
+ }
171
+
172
+ isBefore(d: LocalDateConfig): boolean {
173
+ return this.cmp(d) === -1
174
+ }
175
+
176
+ isSameOrBefore(d: LocalDateConfig): boolean {
177
+ return this.cmp(d) <= 0
178
+ }
179
+
180
+ isAfter(d: LocalDateConfig): boolean {
181
+ return this.cmp(d) === 1
182
+ }
183
+
184
+ isSameOrAfter(d: LocalDateConfig): boolean {
185
+ return this.cmp(d) >= 0
186
+ }
187
+
188
+ /**
189
+ * Returns 1 if this > d
190
+ * returns 0 if they are equal
191
+ * returns -1 if this < d
192
+ */
193
+ cmp(d: LocalDateConfig): -1 | 0 | 1 {
194
+ d = LocalDate.of(d)
195
+ if (this.year < d.year) return -1
196
+ if (this.year > d.year) return 1
197
+ if (this.month < d.month) return -1
198
+ if (this.month > d.month) return 1
199
+ if (this.day < d.day) return -1
200
+ if (this.day > d.day) return 1
201
+ return 0
202
+ }
203
+
204
+ /**
205
+ * Same as Math.abs( diff )
206
+ */
207
+ absDiff(d: LocalDateConfig, unit: LocalDateUnit): number {
208
+ return Math.abs(this.diff(d, unit))
209
+ }
210
+
211
+ /**
212
+ * Returns the number of **full** units difference (aka `Math.ceil`).
213
+ *
214
+ * a.diff(b) means "a minus b"
215
+ */
216
+ diff(d: LocalDateConfig, unit: LocalDateUnit): number {
217
+ d = LocalDate.of(d)
218
+
219
+ if (unit === 'year') {
220
+ return this.year - d.year
221
+ }
222
+
223
+ if (unit === 'month') {
224
+ return (this.year - d.year) * 12 + (this.month - d.month)
225
+ }
226
+
227
+ // unit is 'day'
228
+ let days = this.day - d.day
229
+
230
+ if (d.year < this.year) {
231
+ for (let year = d.year; year < this.year; year++) {
232
+ days += LocalDate.getYearLength(year)
233
+ }
234
+ } else if (this.year < d.year) {
235
+ for (let year = this.year; year < d.year; year++) {
236
+ days -= LocalDate.getYearLength(year)
237
+ }
238
+ }
239
+
240
+ if (d.month < this.month) {
241
+ for (let month = d.month; month < this.month; month++) {
242
+ days += LocalDate.getMonthLength(this.year, month)
243
+ }
244
+ } else if (this.month < d.month) {
245
+ for (let month = this.month; month < d.month; month++) {
246
+ days -= LocalDate.getMonthLength(d.year, month)
247
+ }
248
+ }
249
+
250
+ return days
251
+ }
252
+
253
+ add(num: number, unit: LocalDateUnit, mutate = false): LocalDate {
254
+ let { day, month, year } = this
255
+
256
+ if (unit === 'day') {
257
+ day += num
258
+ } else if (unit === 'month') {
259
+ month += num
260
+ } else if (unit === 'year') {
261
+ year += num
262
+ }
263
+
264
+ // check day overflow
265
+ let monLen = LocalDate.getMonthLength(year, month)
266
+ while (day > monLen) {
267
+ day -= monLen
268
+ month += 1
269
+ if (month > 12) {
270
+ year += 1
271
+ month -= 12
272
+ }
273
+
274
+ monLen = LocalDate.getMonthLength(year, month)
275
+ }
276
+ while (day < 1) {
277
+ day += monLen
278
+ month -= 1
279
+ if (month < 1) {
280
+ year -= 1
281
+ month += 12
282
+ }
283
+
284
+ monLen = LocalDate.getMonthLength(year, month)
285
+ }
286
+
287
+ // check month overflow
288
+ while (month > 12) {
289
+ year += 1
290
+ month -= 12
291
+ }
292
+ while (month < 1) {
293
+ year -= 1
294
+ month += 12
295
+ }
296
+
297
+ if (mutate) {
298
+ this.year = year
299
+ this.month = month
300
+ this.day = day
301
+ return this
302
+ }
303
+
304
+ return new LocalDate(year, month, day)
305
+ }
306
+
307
+ subtract(num: number, unit: LocalDateUnit, mutate = false): LocalDate {
308
+ return this.add(-num, unit, mutate)
309
+ }
310
+
311
+ startOf(unit: LocalDateUnit): LocalDate {
312
+ if (unit === 'day') return this
313
+ if (unit === 'month') return LocalDate.create(this.year, this.month, 1)
314
+ // year
315
+ return LocalDate.create(this.year, 1, 1)
316
+ }
317
+
318
+ endOf(unit: LocalDateUnit): LocalDate {
319
+ if (unit === 'day') return this
320
+ if (unit === 'month')
321
+ return LocalDate.create(
322
+ this.year,
323
+ this.month,
324
+ LocalDate.getMonthLength(this.year, this.month),
325
+ )
326
+ // year
327
+ return LocalDate.create(this.year, 12, 31)
328
+ }
329
+
330
+ static getYearLength(year: number): number {
331
+ return this.isLeapYear(year) ? 366 : 365
332
+ }
333
+
334
+ static getMonthLength(year: number, month: number): number {
335
+ if (month === 2) return this.isLeapYear(year) ? 29 : 28
336
+ return m31.has(month) ? 31 : 30
337
+ }
338
+
339
+ static isLeapYear(year: number): boolean {
340
+ if (year % 4 !== 0) return false
341
+ if (year % 100 !== 0) return true
342
+ return year % 400 === 0
343
+ }
344
+
345
+ clone(): LocalDate {
346
+ return new LocalDate(this.year, this.month, this.day)
347
+ }
348
+
349
+ /**
350
+ * Converts LocalDate into instance of Date.
351
+ * Year, month and day will match.
352
+ * Hour, minute, second, ms will be 0.
353
+ * Timezone will match local timezone.
354
+ */
355
+ toDate(): Date {
356
+ return new Date(this.year, this.month - 1, this.day)
357
+ }
358
+
359
+ toString(): IsoDate {
360
+ return [
361
+ String(this.year).padStart(4, '0'),
362
+ String(this.month).padStart(2, '0'),
363
+ String(this.day).padStart(2, '0'),
364
+ ].join('-')
365
+ }
366
+
367
+ toStringCompact(): string {
368
+ return [
369
+ String(this.year).padStart(4, '0'),
370
+ String(this.month).padStart(2, '0'),
371
+ String(this.day).padStart(2, '0'),
372
+ ].join('')
373
+ }
374
+
375
+ toJSON(): IsoDate {
376
+ return this.toString()
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Shortcut wrapper around `LocalDate.parse` / `LocalDate.today`
382
+ */
383
+ export function localDate(d?: LocalDateConfig): LocalDate {
384
+ return d ? LocalDate.of(d) : LocalDate.today()
385
+ }