@naturalcycles/js-lib 14.97.0 → 14.98.1

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.
@@ -1,14 +1,14 @@
1
1
  import { _assert } from '../error/assert'
2
- import { Sequence } from '../seq/seq'
3
- import { END, IsoDateString, UnixTimestampNumber } from '../types'
2
+ import { IsoDateString, UnixTimestampNumber } from '../types'
4
3
  import { LocalTime } from './localTime'
5
4
 
6
5
  export type LocalDateUnit = 'year' | 'month' | 'day'
7
6
  export type Inclusiveness = '()' | '[]' | '[)' | '(]'
8
7
 
9
- const m31 = new Set<number>([1, 3, 5, 7, 8, 10, 12])
8
+ const MDAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
9
+ const DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
10
10
 
11
- export type LocalDateConfig = LocalDate | string
11
+ export type LocalDateConfig = LocalDate | IsoDateString
12
12
 
13
13
  /* eslint-disable no-dupe-class-members */
14
14
 
@@ -61,8 +61,13 @@ export class LocalDate {
61
61
  if (!d) return null
62
62
  if (d instanceof LocalDate) return d
63
63
 
64
- // todo: explore more performant options
65
- const [year, month, day] = d.slice(0, 10).split('-').map(Number)
64
+ // const [year, month, day] = d.slice(0, 10).split('-').map(Number)
65
+ const matches = DATE_REGEX.exec(d.slice(0, 10))
66
+ if (!matches) return null
67
+
68
+ const year = Number(matches[1])
69
+ const month = Number(matches[2])
70
+ const day = Number(matches[3])
66
71
 
67
72
  if (
68
73
  !year ||
@@ -79,6 +84,11 @@ export class LocalDate {
79
84
  return new LocalDate(year, month, day)
80
85
  }
81
86
 
87
+ // Can use just .toString()
88
+ // static parseToString(d: LocalDateConfig): IsoDateString {
89
+ // return typeof d === 'string' ? d : d.toString()
90
+ // }
91
+
82
92
  static isValid(iso: string | undefined | null): boolean {
83
93
  return this.parseOrNull(iso) !== null
84
94
  }
@@ -117,64 +127,30 @@ export class LocalDate {
117
127
  }
118
128
 
119
129
  static range(
120
- minIncl: LocalDateConfig,
121
- maxExcl: LocalDateConfig,
130
+ min: LocalDateConfig,
131
+ max: LocalDateConfig,
132
+ incl: Inclusiveness = '[)',
122
133
  step = 1,
123
134
  stepUnit: LocalDateUnit = 'day',
124
135
  ): LocalDate[] {
125
- const days: LocalDate[] = []
126
- let current = LocalDate.of(minIncl).startOf(stepUnit)
127
- const max = LocalDate.of(maxExcl).startOf(stepUnit)
128
-
129
- do {
130
- days.push(current)
131
- current = current.add(step, stepUnit)
132
- } while (current.isBefore(max))
133
-
134
- return days
135
- }
136
+ const dates: LocalDate[] = []
137
+ const $min = LocalDate.of(min)
138
+ const $max = LocalDate.of(max).startOf(stepUnit)
136
139
 
137
- static rangeSeq(
138
- minIncl: LocalDateConfig,
139
- maxExcl: LocalDateConfig,
140
- step = 1,
141
- stepUnit: LocalDateUnit = 'day',
142
- ): Sequence<LocalDate> {
143
- const min = LocalDate.of(minIncl).startOf(stepUnit)
144
- const max = LocalDate.of(maxExcl).startOf(stepUnit)
145
- return Sequence.create(min, d => {
146
- const next = d.add(step, stepUnit)
147
- return next.isAfter(max) ? END : next
148
- })
149
- }
150
-
151
- static rangeString(
152
- minIncl: LocalDateConfig,
153
- maxExcl: LocalDateConfig,
154
- step = 1,
155
- stepUnit: LocalDateUnit = 'day',
156
- ): IsoDateString[] {
157
- return LocalDate.range(minIncl, maxExcl, step, stepUnit).map(ld => ld.toString())
158
- }
140
+ let current = $min.startOf(stepUnit)
141
+ if (current.isAfter($min, incl[0] === '[')) {
142
+ // ok
143
+ } else {
144
+ current.add(1, stepUnit, true)
145
+ }
159
146
 
160
- static rangeIncl(
161
- minIncl: LocalDateConfig,
162
- maxIncl: LocalDateConfig,
163
- step = 1,
164
- stepUnit: LocalDateUnit = 'day',
165
- ): LocalDate[] {
166
- return LocalDate.range(minIncl, LocalDate.of(maxIncl).add(1, stepUnit), step, stepUnit)
167
- }
147
+ const incl2 = incl[1] === ']'
148
+ while (current.isBefore($max, incl2)) {
149
+ dates.push(current)
150
+ current = current.add(step, stepUnit)
151
+ }
168
152
 
169
- static rangeInclString(
170
- minIncl: LocalDateConfig,
171
- maxIncl: LocalDateConfig,
172
- step = 1,
173
- stepUnit: LocalDateUnit = 'day',
174
- ): IsoDateString[] {
175
- return LocalDate.range(minIncl, LocalDate.of(maxIncl).add(1, stepUnit), step, stepUnit).map(
176
- ld => ld.toString(),
177
- )
153
+ return dates
178
154
  }
179
155
 
180
156
  get(unit: LocalDateUnit): number {
@@ -216,16 +192,18 @@ export class LocalDate {
216
192
  return this.$day === d.$day && this.$month === d.$month && this.$year === d.$year
217
193
  }
218
194
 
219
- isBefore(d: LocalDateConfig): boolean {
220
- return this.cmp(d) === -1
195
+ isBefore(d: LocalDateConfig, inclusive = false): boolean {
196
+ const r = this.cmp(d)
197
+ return r === -1 || (r === 0 && inclusive)
221
198
  }
222
199
 
223
200
  isSameOrBefore(d: LocalDateConfig): boolean {
224
201
  return this.cmp(d) <= 0
225
202
  }
226
203
 
227
- isAfter(d: LocalDateConfig): boolean {
228
- return this.cmp(d) === 1
204
+ isAfter(d: LocalDateConfig, inclusive = false): boolean {
205
+ const r = this.cmp(d)
206
+ return r === 1 || (r === 0 && inclusive)
229
207
  }
230
208
 
231
209
  isSameOrAfter(d: LocalDateConfig): boolean {
@@ -317,26 +295,31 @@ export class LocalDate {
317
295
  }
318
296
 
319
297
  // check day overflow
320
- let monLen = LocalDate.getMonthLength($year, $month)
321
- while ($day > monLen) {
322
- $day -= monLen
323
- $month += 1
324
- if ($month > 12) {
325
- $year += 1
326
- $month -= 12
327
- }
328
-
329
- monLen = LocalDate.getMonthLength($year, $month)
330
- }
331
- while ($day < 1) {
332
- $day += monLen
333
- $month -= 1
334
- if ($month < 1) {
335
- $year -= 1
336
- $month += 12
298
+ if (unit === 'day') {
299
+ if ($day < 1) {
300
+ while ($day < 1) {
301
+ $month -= 1
302
+ if ($month < 1) {
303
+ $year -= 1
304
+ $month += 12
305
+ }
306
+
307
+ $day += LocalDate.getMonthLength($year, $month)
308
+ }
309
+ } else {
310
+ let monLen = LocalDate.getMonthLength($year, $month)
311
+
312
+ while ($day > monLen) {
313
+ $day -= monLen
314
+ $month += 1
315
+ if ($month > 12) {
316
+ $year += 1
317
+ $month -= 12
318
+ }
319
+
320
+ monLen = LocalDate.getMonthLength($year, $month)
321
+ }
337
322
  }
338
-
339
- monLen = LocalDate.getMonthLength($year, $month)
340
323
  }
341
324
 
342
325
  // check month overflow
@@ -388,13 +371,11 @@ export class LocalDate {
388
371
 
389
372
  static getMonthLength(year: number, month: number): number {
390
373
  if (month === 2) return this.isLeapYear(year) ? 29 : 28
391
- return m31.has(month) ? 31 : 30
374
+ return MDAYS[month]!
392
375
  }
393
376
 
394
377
  static isLeapYear(year: number): boolean {
395
- if (year % 4 !== 0) return false
396
- if (year % 100 !== 0) return true
397
- return year % 400 === 0
378
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
398
379
  }
399
380
 
400
381
  clone(): LocalDate {
@@ -62,7 +62,7 @@ export class LocalTime {
62
62
  } else if (typeof d === 'number') {
63
63
  date = new Date(d * 1000)
64
64
  } else {
65
- date = new Date(d)
65
+ date = new Date(d.slice(0, 19))
66
66
  }
67
67
 
68
68
  // validation
@@ -78,6 +78,32 @@ export class LocalTime {
78
78
  return new LocalTime(date, false)
79
79
  }
80
80
 
81
+ static parseToDate(d: LocalTimeConfig): Date {
82
+ if (d instanceof LocalTime) return d.$date
83
+ if (d instanceof Date) return d
84
+
85
+ const date = typeof d === 'number' ? new Date(d * 1000) : new Date(d)
86
+
87
+ if (isNaN(date.getDate())) {
88
+ throw new TypeError(`Cannot parse "${d}" to Date`)
89
+ }
90
+
91
+ return date
92
+ }
93
+
94
+ static parseToUnixTimestamp(d: LocalTimeConfig): UnixTimestampNumber {
95
+ if (typeof d === 'number') return d
96
+ if (d instanceof LocalTime) return d.unix()
97
+
98
+ const date = d instanceof Date ? d : new Date(d)
99
+
100
+ if (isNaN(date.getDate())) {
101
+ throw new TypeError(`Cannot parse "${d}" to UnixTimestamp`)
102
+ }
103
+
104
+ return date.valueOf() / 1000
105
+ }
106
+
81
107
  static isValid(d: LocalTimeConfig | undefined | null): boolean {
82
108
  return this.parseOrNull(d) !== null
83
109
  }
@@ -205,7 +231,7 @@ export class LocalTime {
205
231
  }
206
232
 
207
233
  diff(other: LocalTimeConfig, unit: LocalTimeUnit): number {
208
- const date2 = LocalTime.of(other).$date
234
+ const date2 = LocalTime.parseToDate(other)
209
235
 
210
236
  if (unit === 'year') {
211
237
  return this.$date.getFullYear() - date2.getFullYear()
@@ -295,16 +321,18 @@ export class LocalTime {
295
321
  return this.cmp(d) === 0
296
322
  }
297
323
 
298
- isBefore(d: LocalTimeConfig): boolean {
299
- return this.cmp(d) === -1
324
+ isBefore(d: LocalTimeConfig, inclusive = false): boolean {
325
+ const r = this.cmp(d)
326
+ return r === -1 || (r === 0 && inclusive)
300
327
  }
301
328
 
302
329
  isSameOrBefore(d: LocalTimeConfig): boolean {
303
330
  return this.cmp(d) <= 0
304
331
  }
305
332
 
306
- isAfter(d: LocalTimeConfig): boolean {
307
- return this.cmp(d) === 1
333
+ isAfter(d: LocalTimeConfig, inclusive = false): boolean {
334
+ const r = this.cmp(d)
335
+ return r === 1 || (r === 0 && inclusive)
308
336
  }
309
337
 
310
338
  isSameOrAfter(d: LocalTimeConfig): boolean {
@@ -326,7 +354,7 @@ export class LocalTime {
326
354
  */
327
355
  cmp(d: LocalTimeConfig): -1 | 0 | 1 {
328
356
  const t1 = this.$date.valueOf()
329
- const t2 = LocalTime.of(d).$date.valueOf()
357
+ const t2 = LocalTime.parseToDate(d).valueOf()
330
358
  if (t1 === t2) return 0
331
359
  return t1 < t2 ? -1 : 1
332
360
  }
@@ -355,8 +383,8 @@ export class LocalTime {
355
383
  }
356
384
  }
357
385
 
358
- fromNow(now: LocalTimeConfig = LocalTime.now()): string {
359
- const msDiff = LocalTime.of(now).unixMillis() - this.unixMillis()
386
+ fromNow(now: LocalTimeConfig = new Date()): string {
387
+ const msDiff = LocalTime.parseToDate(now).valueOf() - this.$date.valueOf()
360
388
 
361
389
  if (msDiff === 0) return 'now'
362
390
 
@@ -499,5 +527,3 @@ export class LocalTime {
499
527
  export function localTime(d?: LocalTimeConfig): LocalTime {
500
528
  return d ? LocalTime.of(d) : LocalTime.now()
501
529
  }
502
-
503
- // todo: range
@@ -0,0 +1,104 @@
1
+ import { UnixTimestampNumber } from '../types'
2
+ import { Inclusiveness } from './localDate'
3
+ import { LocalTime, LocalTimeConfig } from './localTime'
4
+
5
+ export type TimeIntervalConfig = TimeInterval | TimeIntervalString
6
+ export type TimeIntervalString = string
7
+
8
+ /**
9
+ * Class that supports an "interval of time" between 2 timestamps - start and end.
10
+ * Example: `1649267185/1649267187`.
11
+ *
12
+ * @experimental
13
+ */
14
+ export class TimeInterval {
15
+ private constructor(private $start: UnixTimestampNumber, private $end: UnixTimestampNumber) {}
16
+
17
+ static of(start: LocalTimeConfig, end: LocalTimeConfig): TimeInterval {
18
+ return new TimeInterval(
19
+ LocalTime.parseToUnixTimestamp(start),
20
+ LocalTime.parseToUnixTimestamp(end),
21
+ )
22
+ }
23
+
24
+ get start(): UnixTimestampNumber {
25
+ return this.$start
26
+ }
27
+
28
+ get end(): UnixTimestampNumber {
29
+ return this.$end
30
+ }
31
+
32
+ get startTime(): LocalTime {
33
+ return LocalTime.of(this.$start)
34
+ }
35
+
36
+ get endTime(): LocalTime {
37
+ return LocalTime.of(this.$end)
38
+ }
39
+
40
+ /**
41
+ * Parses string like `1649267185/1649267187` into a TimeInterval.
42
+ */
43
+ static parse(d: TimeIntervalConfig): TimeInterval {
44
+ if (d instanceof TimeInterval) return d
45
+
46
+ const [start, end] = d.split('/').map(Number)
47
+
48
+ if (!end || !start) {
49
+ throw new Error(`Cannot parse "${d}" into TimeInterval`)
50
+ }
51
+
52
+ return new TimeInterval(start, end)
53
+ }
54
+
55
+ isSame(d: TimeIntervalConfig): boolean {
56
+ return this.cmp(d) === 0
57
+ }
58
+
59
+ isBefore(d: TimeIntervalConfig, inclusive = false): boolean {
60
+ const r = this.cmp(d)
61
+ return r === -1 || (r === 0 && inclusive)
62
+ }
63
+
64
+ isSameOrBefore(d: TimeIntervalConfig): boolean {
65
+ return this.cmp(d) <= 0
66
+ }
67
+
68
+ isAfter(d: TimeIntervalConfig, inclusive = false): boolean {
69
+ const r = this.cmp(d)
70
+ return r === 1 || (r === 0 && inclusive)
71
+ }
72
+
73
+ isSameOrAfter(d: TimeIntervalConfig): boolean {
74
+ return this.cmp(d) >= 0
75
+ }
76
+
77
+ includes(d: LocalTimeConfig, incl: Inclusiveness = '[)'): boolean {
78
+ d = LocalTime.parseToUnixTimestamp(d)
79
+ if (d < this.$start || (d === this.$start && incl[0] === '(')) return false
80
+ if (d > this.$end || (d === this.$end && incl[1] === ')')) return false
81
+ return true
82
+ }
83
+
84
+ /**
85
+ * TimeIntervals compare by start date.
86
+ * If it's the same - then by end date.
87
+ */
88
+ cmp(d: TimeIntervalConfig): -1 | 0 | 1 {
89
+ d = TimeInterval.parse(d)
90
+ if (this.$start > d.$start) return 1
91
+ if (this.$start < d.$start) return -1
92
+ if (this.$end > d.$end) return 1
93
+ if (this.$end < d.$end) return -1
94
+ return 0
95
+ }
96
+
97
+ toString(): TimeIntervalString {
98
+ return [this.$start, this.$end].join('/')
99
+ }
100
+
101
+ toJSON(): TimeIntervalString {
102
+ return this.toString()
103
+ }
104
+ }
package/src/index.ts CHANGED
@@ -160,13 +160,17 @@ export * from './string/leven'
160
160
  export * from './datetime/localDate'
161
161
  export * from './datetime/localTime'
162
162
  export * from './datetime/dateInterval'
163
+ export * from './datetime/timeInterval'
163
164
  import { LocalDateConfig, LocalDateUnit, Inclusiveness } from './datetime/localDate'
164
165
  import { LocalTimeConfig, LocalTimeUnit, LocalTimeComponents } from './datetime/localTime'
165
166
  import { DateIntervalConfig, DateIntervalString } from './datetime/dateInterval'
167
+ import { TimeIntervalConfig, TimeIntervalString } from './datetime/timeInterval'
166
168
 
167
169
  export type {
168
170
  DateIntervalConfig,
169
171
  DateIntervalString,
172
+ TimeIntervalConfig,
173
+ TimeIntervalString,
170
174
  LocalDateConfig,
171
175
  LocalDateUnit,
172
176
  Inclusiveness,
@@ -365,7 +365,9 @@ export class JsonSchemaObjectBuilder<T extends AnyObject> extends JsonSchemaAnyB
365
365
  return this
366
366
  }
367
367
 
368
- baseDBEntity<ID = string>(idType = 'string'): JsonSchemaObjectBuilder<T & BaseDBEntity<ID>> {
368
+ baseDBEntity<ID extends string | number = string>(
369
+ idType = 'string',
370
+ ): JsonSchemaObjectBuilder<T & BaseDBEntity<ID>> {
369
371
  Object.assign(this.schema.properties, {
370
372
  id: { type: idType },
371
373
  created: { type: 'number', format: 'unixTimestamp' },
@@ -375,7 +377,9 @@ export class JsonSchemaObjectBuilder<T extends AnyObject> extends JsonSchemaAnyB
375
377
  return this
376
378
  }
377
379
 
378
- savedDBEntity<ID = string>(idType = 'string'): JsonSchemaObjectBuilder<T & SavedDBEntity<ID>> {
380
+ savedDBEntity<ID extends string | number = string>(
381
+ idType = 'string',
382
+ ): JsonSchemaObjectBuilder<T & SavedDBEntity<ID>> {
379
383
  return this.baseDBEntity(idType).addRequired(['id', 'created', 'updated']) as any
380
384
  }
381
385
 
@@ -12,6 +12,13 @@ export function _average(values: number[]): number {
12
12
  return values.reduce((a, b) => a + b) / values.length
13
13
  }
14
14
 
15
+ /**
16
+ * Same as _average, but safely returns null if input array is empty or nullish.
17
+ */
18
+ export function _averageOrNull(values: number[] | undefined | null): number | null {
19
+ return values?.length ? values.reduce((a, b) => a + b) / values.length : null
20
+ }
21
+
15
22
  /**
16
23
  * valuesArray and weightsArray length is expected to be the same.
17
24
  */
package/src/math/sma.ts CHANGED
@@ -15,6 +15,10 @@ export class SimpleMovingAverage {
15
15
  */
16
16
  avg = 0
17
17
 
18
+ /**
19
+ * Push new value.
20
+ * Returns newly calculated average (using newly pushed value).
21
+ */
18
22
  push(n: number): number {
19
23
  this.data[this.nextIndex] = n
20
24
  this.nextIndex =
package/src/types.ts CHANGED
@@ -6,7 +6,7 @@ import { Merge, Promisable } from './typeFest'
6
6
  * Alternative: Record<string, T | undefined>
7
7
  */
8
8
  export interface StringMap<T = string> {
9
- [k: string]: T | undefined
9
+ [k: string | number]: T | undefined
10
10
  }
11
11
 
12
12
  /**
@@ -29,15 +29,50 @@ export interface CreatedUpdated {
29
29
  updated: number
30
30
  }
31
31
 
32
- export interface CreatedUpdatedId<ID = string> extends CreatedUpdated {
32
+ export interface CreatedUpdatedId<ID extends string | number = string | number>
33
+ extends CreatedUpdated {
33
34
  id: ID
34
35
  }
35
36
 
36
- export interface ObjectWithId<ID = string> {
37
+ export interface ObjectWithId<ID extends string | number = string | number> {
37
38
  id: ID
38
39
  }
39
40
 
40
- export interface AnyObjectWithId<ID = string> extends AnyObject, ObjectWithId<ID> {}
41
+ export interface AnyObjectWithId<ID extends string | number = string | number>
42
+ extends AnyObject,
43
+ ObjectWithId<ID> {}
44
+
45
+ /**
46
+ * Base interface for any Entity that was saved to DB.
47
+ */
48
+ export interface SavedDBEntity<ID extends string | number = string> {
49
+ id: ID
50
+
51
+ /**
52
+ * unixTimestamp of when the entity was first created (in the DB).
53
+ */
54
+ created: UnixTimestampNumber
55
+
56
+ /**
57
+ * unixTimestamp of when the entity was last updated (in the DB).
58
+ */
59
+ updated: UnixTimestampNumber
60
+ }
61
+
62
+ /**
63
+ * Base interface for any Entity that can be saved to DB.
64
+ * This interface fits when entity was NOT YET saved to DB,
65
+ * hence `id`, `created` and `updated` fields CAN BE undefined (yet).
66
+ * When it's known to be saved - `SavedDBEntity` interface can be used instead.
67
+ */
68
+ export type BaseDBEntity<ID extends string | number = string> = Partial<SavedDBEntity<ID>>
69
+
70
+ export type Saved<T extends Partial<ObjectWithId>> = Merge<
71
+ T,
72
+ SavedDBEntity<Exclude<T['id'], undefined>>
73
+ >
74
+
75
+ export type Unsaved<T extends ObjectWithId> = Merge<T, BaseDBEntity<T['id']>>
41
76
 
42
77
  /**
43
78
  * Convenience type shorthand.
@@ -181,34 +216,6 @@ export type UnixTimestamp = number
181
216
  */
182
217
  export type Integer = number
183
218
 
184
- /**
185
- * Base interface for any Entity that was saved to DB.
186
- */
187
- export interface SavedDBEntity<ID = string> {
188
- id: ID
189
-
190
- /**
191
- * unixTimestamp of when the entity was first created (in the DB).
192
- */
193
- created: UnixTimestampNumber
194
-
195
- /**
196
- * unixTimestamp of when the entity was last updated (in the DB).
197
- */
198
- updated: UnixTimestampNumber
199
- }
200
-
201
- /**
202
- * Base interface for any Entity that can be saved to DB.
203
- * This interface fits when entity was NOT YET saved to DB,
204
- * hence `id`, `created` and `updated` fields CAN BE undefined (yet).
205
- * When it's known to be saved - `SavedDBEntity` interface can be used instead.
206
- */
207
- export type BaseDBEntity<ID = string> = Partial<SavedDBEntity<ID>>
208
-
209
- export type Saved<E, ID = string> = Merge<E, SavedDBEntity<ID>>
210
- export type Unsaved<E, ID = string> = Merge<E, BaseDBEntity<ID>>
211
-
212
219
  /**
213
220
  * Named type for JSON.parse / JSON.stringify second argument
214
221
  */