@byloth/core 2.0.0-rc.9 → 2.0.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.
Files changed (47) hide show
  1. package/dist/core.js +4087 -621
  2. package/dist/core.js.map +1 -1
  3. package/dist/core.umd.cjs +2 -2
  4. package/dist/core.umd.cjs.map +1 -1
  5. package/package.json +17 -13
  6. package/src/core/types.ts +41 -0
  7. package/src/helpers.ts +11 -2
  8. package/src/index.ts +12 -9
  9. package/src/models/aggregators/aggregated-async-iterator.ts +920 -21
  10. package/src/models/aggregators/aggregated-iterator.ts +838 -22
  11. package/src/models/aggregators/reduced-iterator.ts +827 -11
  12. package/src/models/aggregators/types.ts +153 -10
  13. package/src/models/callbacks/callable-object.ts +42 -6
  14. package/src/models/callbacks/index.ts +2 -2
  15. package/src/models/callbacks/publisher.ts +160 -4
  16. package/src/models/callbacks/switchable-callback.ts +230 -23
  17. package/src/models/callbacks/types.ts +16 -0
  18. package/src/models/exceptions/core.ts +132 -3
  19. package/src/models/exceptions/index.ts +405 -13
  20. package/src/models/index.ts +4 -8
  21. package/src/models/iterators/smart-async-iterator.ts +827 -22
  22. package/src/models/iterators/smart-iterator.ts +755 -20
  23. package/src/models/iterators/types.ts +268 -9
  24. package/src/models/json/json-storage.ts +508 -110
  25. package/src/models/json/types.ts +10 -1
  26. package/src/models/promises/deferred-promise.ts +85 -5
  27. package/src/models/promises/index.ts +1 -3
  28. package/src/models/promises/smart-promise.ts +272 -4
  29. package/src/models/promises/timed-promise.ts +43 -1
  30. package/src/models/promises/types.ts +84 -2
  31. package/src/models/timers/clock.ts +109 -19
  32. package/src/models/timers/countdown.ts +176 -21
  33. package/src/models/timers/game-loop.ts +266 -0
  34. package/src/models/timers/index.ts +2 -1
  35. package/src/models/types.ts +6 -5
  36. package/src/utils/async.ts +43 -0
  37. package/src/utils/curve.ts +85 -0
  38. package/src/utils/date.ts +204 -10
  39. package/src/utils/dom.ts +16 -2
  40. package/src/utils/index.ts +3 -2
  41. package/src/utils/iterator.ts +200 -17
  42. package/src/utils/math.ts +55 -3
  43. package/src/utils/random.ts +139 -2
  44. package/src/utils/string.ts +11 -0
  45. package/src/models/game-loop.ts +0 -83
  46. package/src/models/promises/long-running-task.ts +0 -294
  47. package/src/models/promises/thenable.ts +0 -97
@@ -0,0 +1,266 @@
1
+ import type { Interval } from "../../core/types.js";
2
+ import { isBrowser } from "../../helpers.js";
3
+
4
+ import Publisher from "../callbacks/publisher.js";
5
+ import { FatalErrorException, RuntimeException } from "../exceptions/index.js";
6
+ import type { Callback } from "../types.js";
7
+
8
+ interface GameLoopEventMap
9
+ {
10
+ start: () => void;
11
+ stop: () => void;
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ [key: string]: Callback<any[], any>;
15
+ }
16
+
17
+ /**
18
+ * A class representing a {@link https://en.wikipedia.org/wiki/Video_game_programming#Game_structure|game loop} pattern
19
+ * that allows to run a function at a specific frame rate.
20
+ *
21
+ * In a browser environment, it uses the native {@link requestAnimationFrame}
22
+ * function to run the callback at the refresh rate of the screen.
23
+ * In a non-browser environment, however, it uses the {@link setInterval}
24
+ * function to run the callback at the specified fixed interval of time.
25
+ *
26
+ * Every time the callback is executed, it receives the
27
+ * elapsed time since the start of the game loop.
28
+ * It's also possible to subscribe to the `start` & `stop` events to receive notifications when they occur.
29
+ *
30
+ * ```ts
31
+ * const loop = new GameLoop((elapsedTime: number) =>
32
+ * {
33
+ * console.log(`The game loop has been running for ${elapsedTime}ms.`);
34
+ * });
35
+ *
36
+ * loop.onStart(() => { console.log("The game loop has started."); });
37
+ * loop.onStop(() => { console.log("The game loop has stopped."); });
38
+ *
39
+ * loop.start();
40
+ * ```
41
+ */
42
+ export default class GameLoop
43
+ {
44
+ /**
45
+ * The handle of the interval or the animation frame, depending on the environment.
46
+ * It's used to stop the game loop when the {@link GameLoop._stop} method is called.
47
+ */
48
+ protected _handle?: number | Interval;
49
+
50
+ /**
51
+ * The time when the game loop has started.
52
+ * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
53
+ * of the start of the game loop, it's also used to calculate the elapsed time.
54
+ *
55
+ * This protected property is the only one that can be modified directly by the derived classes.
56
+ * If you're looking for the public and readonly property, use the {@link GameLoop.startTime} getter instead.
57
+ */
58
+ protected _startTime: number;
59
+
60
+ /**
61
+ * The time when the game loop has started.
62
+ * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
63
+ * of the start of the game loop, it's also used to calculate the elapsed time.
64
+ */
65
+ public get startTime(): number
66
+ {
67
+ return this._startTime;
68
+ }
69
+
70
+ /**
71
+ * A flag indicating whether the game loop is currently running or not.
72
+ *
73
+ * This protected property is the only one that can be modified directly by the derived classes.
74
+ * If you're looking for the public and readonly property, use the {@link GameLoop.isRunning} getter instead.
75
+ */
76
+ protected _isRunning: boolean;
77
+
78
+ /**
79
+ * A flag indicating whether the game loop is currently running or not.
80
+ */
81
+ public get isRunning(): boolean
82
+ {
83
+ return this._isRunning;
84
+ }
85
+
86
+ /**
87
+ * The elapsed time since the start of the game loop.
88
+ * It's calculated as the difference between the current time and the {@link GameLoop.startTime}.
89
+ */
90
+ public get elapsedTime(): number
91
+ {
92
+ return performance.now() - this._startTime;
93
+ }
94
+
95
+ /**
96
+ * The {@link Publisher} object that will be used to publish the events of the game loop.
97
+ */
98
+ protected _publisher: Publisher<GameLoopEventMap>;
99
+
100
+ /**
101
+ * The internal method actually responsible for starting the game loop.
102
+ *
103
+ * Depending on the current environment, it could use the
104
+ * {@link requestAnimationFrame} or the {@link setInterval} function.
105
+ */
106
+ protected _start: () => void;
107
+
108
+ /**
109
+ * The internal method actually responsible for stopping the game loop.
110
+ *
111
+ * Depending on the current environment, it could use the
112
+ * {@link cancelAnimationFrame} or the {@link clearInterval} function.
113
+ */
114
+ protected _stop: () => void;
115
+
116
+ /**
117
+ * Initializes a new instance of the {@link GameLoop} class.
118
+ *
119
+ * ---
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const loop = new GameLoop((elapsedTime: number) => { [...] });
124
+ * ```
125
+ *
126
+ * ---
127
+ *
128
+ * @param callback The function that will be executed at each iteration of the game loop.
129
+ * @param msIfNotBrowser
130
+ * The interval in milliseconds that will be used if the current environment isn't a browser. Default is `40`.
131
+ */
132
+ public constructor(callback: FrameRequestCallback, msIfNotBrowser = 40)
133
+ {
134
+ this._startTime = 0;
135
+ this._isRunning = false;
136
+
137
+ if (isBrowser)
138
+ {
139
+ this._start = () =>
140
+ {
141
+ callback(this.elapsedTime);
142
+
143
+ this._handle = window.requestAnimationFrame(this._start);
144
+ };
145
+
146
+ this._stop = () => window.cancelAnimationFrame(this._handle as number);
147
+ }
148
+ else
149
+ {
150
+ // eslint-disable-next-line no-console
151
+ console.warn(
152
+ "Not a browser environment detected. " +
153
+ `Using setInterval@${msIfNotBrowser}ms instead of requestAnimationFrame...`
154
+ );
155
+
156
+ this._start = () =>
157
+ {
158
+ this._handle = setInterval(() => callback(this.elapsedTime), msIfNotBrowser);
159
+ };
160
+
161
+ this._stop = () => clearInterval(this._handle as Interval);
162
+ }
163
+
164
+ this._publisher = new Publisher();
165
+ }
166
+
167
+ /**
168
+ * Starts the execution of the game loop.
169
+ *
170
+ * If the game loop is already running, a {@link RuntimeException} will be thrown.
171
+ *
172
+ * ---
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * loop.onStart(() => { [...] }); // This callback will be executed.
177
+ * loop.start();
178
+ * ```
179
+ *
180
+ * ---
181
+ *
182
+ * @param elapsedTime The elapsed time to set as default when the game loop starts. Default is `0`.
183
+ */
184
+ public start(elapsedTime = 0): void
185
+ {
186
+ if (this._isRunning) { throw new RuntimeException("The game loop has already been started."); }
187
+
188
+ this._startTime = performance.now() - elapsedTime;
189
+ this._start();
190
+ this._isRunning = true;
191
+
192
+ this._publisher.publish("start");
193
+ }
194
+
195
+ /**
196
+ * Stops the execution of the game loop.
197
+ *
198
+ * If the game loop hasn't yet started, a {@link RuntimeException} will be thrown.
199
+ *
200
+ * ---
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * loop.onStop(() => { [...] }); // This callback will be executed.
205
+ * loop.stop();
206
+ * ```
207
+ */
208
+ public stop(): void
209
+ {
210
+ if (!(this._isRunning))
211
+ {
212
+ throw new RuntimeException("The game loop had already stopped or hadn't yet started.");
213
+ }
214
+ if (!(this._handle)) { throw new FatalErrorException(); }
215
+
216
+ this._stop();
217
+ this._handle = undefined;
218
+ this._isRunning = false;
219
+
220
+ this._publisher.publish("stop");
221
+ }
222
+
223
+ /**
224
+ * Subscribes to the `start` event of the game loop.
225
+ *
226
+ * ---
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * loop.onStart(() => { console.log("The game loop has started."); });
231
+ * ```
232
+ *
233
+ * ---
234
+ *
235
+ * @param callback The function that will be executed when the game loop starts.
236
+ *
237
+ * @returns A function that can be used to unsubscribe from the event.
238
+ */
239
+ public onStart(callback: () => void): () => void
240
+ {
241
+ return this._publisher.subscribe("start", callback);
242
+ }
243
+
244
+ /**
245
+ * Subscribes to the `stop` event of the game loop.
246
+ *
247
+ * ---
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * loop.onStop(() => { console.log("The game loop has stopped."); });
252
+ * ```
253
+ *
254
+ * ---
255
+ *
256
+ * @param callback The function that will be executed when the game loop stops.
257
+ *
258
+ * @returns A function that can be used to unsubscribe from the event.
259
+ */
260
+ public onStop(callback: () => void): () => void
261
+ {
262
+ return this._publisher.subscribe("stop", callback);
263
+ }
264
+
265
+ public readonly [Symbol.toStringTag]: string = "GameLoop";
266
+ }
@@ -1,4 +1,5 @@
1
1
  import Clock from "./clock.js";
2
2
  import Countdown from "./countdown.js";
3
+ import GameLoop from "./game-loop.js";
3
4
 
4
- export { Clock, Countdown };
5
+ export { Clock, Countdown, GameLoop };
@@ -1,9 +1,10 @@
1
1
  export type {
2
2
  KeyedIteratee,
3
+ AsyncKeyedIteratee,
3
4
  MaybeAsyncKeyedIteratee,
4
- KeyedTypeGuardIteratee,
5
- MaybeAsyncKeyedTypeGuardIteratee,
5
+ KeyedTypeGuardPredicate,
6
6
  KeyedReducer,
7
+ AsyncKeyedReducer,
7
8
  MaybeAsyncKeyedReducer
8
9
 
9
10
  } from "./aggregators/types.js";
@@ -13,10 +14,11 @@ export type {
13
14
  AsyncGeneratorFunction,
14
15
  MaybeAsyncGeneratorFunction,
15
16
  Iteratee,
17
+ AsyncIteratee,
16
18
  MaybeAsyncIteratee,
17
- TypeGuardIteratee,
18
- MaybeAsyncTypeGuardIteratee,
19
+ TypeGuardPredicate,
19
20
  Reducer,
21
+ AsyncReducer,
20
22
  MaybeAsyncReducer,
21
23
  IteratorLike,
22
24
  AsyncIteratorLike,
@@ -32,7 +34,6 @@ export type {
32
34
  } from "./json/types.js";
33
35
 
34
36
  export type {
35
- LongRunningTaskOptions,
36
37
  MaybePromise,
37
38
  FulfilledHandler,
38
39
  RejectedHandler,
@@ -1,13 +1,56 @@
1
+ /**
2
+ * Returns a promise that resolves after a certain number of milliseconds.
3
+ * It can be used to pause or delay the execution of an asynchronous function.
4
+ *
5
+ * ```ts
6
+ * doSomething();
7
+ * await delay(1_000);
8
+ * doSomethingElse();
9
+ * ```
10
+ *
11
+ * @param milliseconds The number of milliseconds to wait before resolving the promise.
12
+ *
13
+ * @returns A {@link Promise} that resolves after the specified number of milliseconds.
14
+ */
1
15
  export function delay(milliseconds: number): Promise<void>
2
16
  {
3
17
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
4
18
  }
5
19
 
20
+ /**
21
+ * Returns a promise that resolves on the next animation frame.
22
+ * It can be used to synchronize operations with the browser's rendering cycle.
23
+ *
24
+ * ```ts
25
+ * const $el = document.querySelector(".element");
26
+ *
27
+ * $el.classList.add("animate");
28
+ * await nextAnimationFrame();
29
+ * $el.style.opacity = "1";
30
+ * ```
31
+ *
32
+ * @returns A {@link Promise} that resolves on the next animation frame.
33
+ */
6
34
  export function nextAnimationFrame(): Promise<void>
7
35
  {
8
36
  return new Promise((resolve) => requestAnimationFrame(() => resolve()));
9
37
  }
10
38
 
39
+ /**
40
+ * Returns a promise that resolves on the next microtask.
41
+ * It can be used to yield to the event loop in long-running operations to prevent blocking the main thread.
42
+ *
43
+ * ```ts
44
+ * for (let i = 0; i < 100_000_000; i += 1)
45
+ * {
46
+ * doSomething(i);
47
+ *
48
+ * if (i % 100 === 0) await yieldToEventLoop();
49
+ * }
50
+ * ```
51
+ *
52
+ * @returns A {@link Promise} that resolves on the next microtask.
53
+ */
11
54
  export function yieldToEventLoop(): Promise<void>
12
55
  {
13
56
  return new Promise((resolve) => setTimeout(resolve));
@@ -0,0 +1,85 @@
1
+ import { SmartIterator, ValueException } from "../models/index.js";
2
+
3
+ /**
4
+ * A utility class that provides a set of methods to generate sequences of numbers following specific curves.
5
+ * It can be used to generate sequences of values that can be
6
+ * used in animations, transitions and other different scenarios.
7
+ *
8
+ * It cannot be instantiated directly.
9
+ */
10
+ export default class Curve
11
+ {
12
+ /**
13
+ * Generates a given number of values following a linear curve.
14
+ * The values are equally spaced and normalized between 0 and 1.
15
+ *
16
+ * ---
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * for (const value of Curve.Linear(5))
21
+ * {
22
+ * console.log(value); // 0, 0.25, 0.5, 0.75, 1
23
+ * }
24
+ * ```
25
+ *
26
+ * ---
27
+ *
28
+ * @param values The number of values to generate.
29
+ *
30
+ * @returns A {@link SmartIterator} object that generates the values following a linear curve.
31
+ */
32
+ public static Linear(values: number): SmartIterator<number>
33
+ {
34
+ const steps = (values - 1);
35
+
36
+ return new SmartIterator<number>(function* ()
37
+ {
38
+ for (let index = 0; index < values; index += 1) { yield index / steps; }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Generates a given number of values following an exponential curve.
44
+ * The values are equally spaced and normalized between 0 and 1.
45
+ *
46
+ * ---
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * for (const value of Curve.Exponential(6))
51
+ * {
52
+ * console.log(value); // 0, 0.04, 0.16, 0.36, 0.64, 1
53
+ * }
54
+ * ```
55
+ *
56
+ * ---
57
+ *
58
+ * @param values The number of values to generate.
59
+ * @param base
60
+ * The base of the exponential curve. Default is `2`.
61
+ *
62
+ * Also note that:
63
+ * - If it's equal to `1`, the curve will be linear.
64
+ * - If it's included between `0` and `1`, the curve will be logarithmic.
65
+ *
66
+ * The base cannot be negative. If so, a {@link ValueException} will be thrown.
67
+ *
68
+ * @returns A {@link SmartIterator} object that generates the values following an exponential curve.
69
+ */
70
+ public static Exponential(values: number, base = 2): SmartIterator<number>
71
+ {
72
+ if (base < 0) { throw new ValueException("The base of the exponential curve cannot be negative."); }
73
+
74
+ const steps = (values - 1);
75
+
76
+ return new SmartIterator<number>(function* ()
77
+ {
78
+ for (let index = 0; index < values; index += 1) { yield Math.pow(index / steps, base); }
79
+ });
80
+ }
81
+
82
+ private constructor() { /* ... */ }
83
+
84
+ public readonly [Symbol.toStringTag]: string = "Curve";
85
+ }
package/src/utils/date.ts CHANGED
@@ -1,50 +1,244 @@
1
1
 
2
- import { SmartIterator } from "../models/index.js";
2
+ import { RangeException, SmartIterator } from "../models/index.js";
3
3
 
4
+ /**
5
+ * An enumeration that represents the time units and their conversion factors.
6
+ * It can be used as utility to express time values in a more
7
+ * readable way or to convert time values between different units.
8
+ *
9
+ * ```ts
10
+ * setTimeout(() => { [...] }, 5 * TimeUnit.Minute);
11
+ * ```
12
+ */
4
13
  export enum TimeUnit
5
14
  {
6
15
  /* eslint-disable @typescript-eslint/prefer-literal-enum-member */
7
16
 
17
+ /**
18
+ * A millisecond: the base time unit.
19
+ */
8
20
  Millisecond = 1,
9
- Second = 1000,
21
+
22
+ /**
23
+ * A second: 1000 milliseconds.
24
+ */
25
+ Second = 1_000,
26
+
27
+ /**
28
+ * A minute: 60 seconds.
29
+ */
10
30
  Minute = 60 * Second,
31
+
32
+ /**
33
+ * An hour: 60 minutes.
34
+ */
11
35
  Hour = 60 * Minute,
36
+
37
+ /**
38
+ * A day: 24 hours.
39
+ */
12
40
  Day = 24 * Hour,
41
+
42
+ /**
43
+ * A week: 7 days.
44
+ */
13
45
  Week = 7 * Day,
46
+
47
+ /**
48
+ * A month: 30 days.
49
+ */
14
50
  Month = 30 * Day,
51
+
52
+ /**
53
+ * A year: 365 days.
54
+ */
15
55
  Year = 365 * Day
16
56
  }
17
57
 
58
+ /**
59
+ * An enumeration that represents the days of the week.
60
+ * It can be used as utility to identify the days of the week when working with dates.
61
+ *
62
+ * ```ts
63
+ * const today = new Date();
64
+ * if (today.getUTCDay() === WeekDay.Sunday)
65
+ * {
66
+ * // Today is Sunday. Do something...
67
+ * }
68
+ * ```
69
+ */
70
+ export enum WeekDay
71
+ {
72
+ /**
73
+ * Sunday
74
+ */
75
+ Sunday = 0,
76
+
77
+ /**
78
+ * Monday
79
+ */
80
+ Monday = 1,
81
+
82
+ /**
83
+ * Tuesday
84
+ */
85
+ Tuesday = 2,
86
+
87
+ /**
88
+ * Wednesday
89
+ */
90
+ Wednesday = 3,
91
+
92
+ /**
93
+ * Thursday
94
+ */
95
+ Thursday = 4,
96
+
97
+ /**
98
+ * Friday
99
+ */
100
+ Friday = 5,
101
+
102
+ /**
103
+ * Saturday
104
+ */
105
+ Saturday = 6
106
+ }
107
+
108
+ /**
109
+ * An utility function that calculates the difference between two dates.
110
+ * The difference can be expressed in different time units.
111
+ *
112
+ * ```ts
113
+ * const start = new Date("2025-01-01");
114
+ * const end = new Date("2025-01-31");
115
+ *
116
+ * dateDifference(start, end, TimeUnit.Minute); // 43200
117
+ * ```
118
+ *
119
+ * @param start The start date.
120
+ * @param end The end date.
121
+ * @param unit The time unit to express the difference. `TimeUnit.Day` by default.
122
+ *
123
+ * @returns The difference between the two dates in the specified time unit.
124
+ */
18
125
  export function dateDifference(start: string | Date, end: string | Date, unit = TimeUnit.Day): number
19
126
  {
127
+ let _round: (value: number) => number;
128
+
20
129
  start = new Date(start);
21
130
  end = new Date(end);
22
131
 
23
- return Math.floor((end.getTime() - start.getTime()) / unit);
132
+ if (start < end) { _round = Math.floor; }
133
+ else { _round = Math.ceil; }
134
+
135
+ return _round((end.getTime() - start.getTime()) / unit);
24
136
  }
25
137
 
26
- export function dateRange(start: string | Date, end: string | Date, offset = TimeUnit.Day): SmartIterator<Date>
138
+ /**
139
+ * An utility function that generates an iterator over a range of dates.
140
+ * The step between the dates can be expressed in different time units.
141
+ *
142
+ * ```ts
143
+ * const start = new Date("2025-01-01");
144
+ * const end = new Date("2025-01-31");
145
+ *
146
+ * for (const date of dateRange(start, end, TimeUnit.Week))
147
+ * {
148
+ * date.toISOString().slice(8, 10); // "01", "08", "15", "22", "29"
149
+ * }
150
+ * ```
151
+ *
152
+ * @param start The start date (included).
153
+ * @param end
154
+ * The end date (excluded).
155
+ *
156
+ * Must be greater than the start date. Otherwise, a {@link RangeException} will be thrown.
157
+ *
158
+ * @param step The time unit to express the step between the dates. `TimeUnit.Day` by default.
159
+ *
160
+ * @returns A {@link SmartIterator} object that generates the dates in the range.
161
+ */
162
+ export function dateRange(start: string | Date, end: string | Date, step = TimeUnit.Day): SmartIterator<Date>
27
163
  {
28
- start = new Date(start);
29
- end = new Date(end);
164
+ if (start >= end) { throw new RangeException("The end date must be greater than the start date."); }
30
165
 
31
166
  return new SmartIterator<Date>(function* ()
32
167
  {
33
- const endTime = end.getTime();
168
+ const endTime = new Date(end).getTime();
34
169
 
35
- let unixTime: number = start.getTime();
170
+ let unixTime: number = new Date(start).getTime();
36
171
  while (unixTime < endTime)
37
172
  {
38
173
  yield new Date(unixTime);
39
174
 
40
- unixTime += offset;
175
+ unixTime += step;
41
176
  }
42
177
  });
43
178
  }
44
179
 
180
+ /**
181
+ * An utility function that rounds a date to the nearest time unit.
182
+ * The rounding can be expressed in different time units.
183
+ *
184
+ * ```ts
185
+ * const date = new Date("2025-01-01T12:34:56.789Z");
186
+ *
187
+ * dateRound(date, TimeUnit.Hour); // 2025-01-01T12:00:00.000Z
188
+ * ```
189
+ *
190
+ * @param date The date to round.
191
+ * @param unit
192
+ * The time unit to express the rounding. `TimeUnit.Day` by default.
193
+ *
194
+ * Must be greater than a millisecond and less than or equal to a day.
195
+ * Otherwise, a {@link RangeException} will be thrown.
196
+ *
197
+ * @returns The rounded date.
198
+ */
45
199
  export function dateRound(date: string | Date, unit = TimeUnit.Day): Date
46
200
  {
47
- date = new Date(date);
201
+ if (unit <= TimeUnit.Millisecond)
202
+ {
203
+ throw new RangeException(
204
+ "Rounding a timestamp by milliseconds or less makes no sense." +
205
+ "Use the timestamp value directly instead."
206
+ );
207
+ }
208
+ if (unit > TimeUnit.Day)
209
+ {
210
+ throw new RangeException(
211
+ "Rounding by more than a day leads to unexpected results. " +
212
+ "Consider using other methods to round dates by weeks, months or years."
213
+ );
214
+ }
48
215
 
216
+ date = new Date(date);
49
217
  return new Date(Math.floor(date.getTime() / unit) * unit);
50
218
  }
219
+
220
+ /**
221
+ * An utility function that gets the week of a date.
222
+ * The first day of the week can be optionally specified.
223
+ *
224
+ * ```ts
225
+ * const date = new Date("2025-01-01");
226
+ *
227
+ * getWeek(date, WeekDay.Monday); // 2024-12-30
228
+ * ```
229
+ *
230
+ * @param date The date to get the week of.
231
+ * @param firstDay The first day of the week. `WeekDay.Sunday` by default.
232
+ *
233
+ * @returns The first day of the week of the specified date.
234
+ */
235
+ export function getWeek(date: string | Date, firstDay = WeekDay.Sunday): Date
236
+ {
237
+ date = new Date(date);
238
+
239
+ const startCorrector = 7 - firstDay;
240
+ const weekDayIndex = (date.getUTCDay() + startCorrector) % 7;
241
+ const firstDayTime = date.getTime() - (TimeUnit.Day * weekDayIndex);
242
+
243
+ return dateRound(new Date(firstDayTime));
244
+ }