@blwatkins/utils 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +79 -0
- package/_dist/index.d.mts +413 -0
- package/_dist/index.mjs +679 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Brittni Watkins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# TypeScript Utilities
|
|
2
|
+
|
|
3
|
+
A growing toolkit of reusable, domain-agnostic TypeScript and JavaScript utilities for everyday development.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- [Latest Release](https://blwatkins.github.io/typescript-utils/doc/index.html)
|
|
8
|
+
- [Documentation by Version Number](https://blwatkins.github.io/typescript-utils/releases.html)
|
|
9
|
+
|
|
10
|
+
## License
|
|
11
|
+
|
|
12
|
+
The source code of this project is licensed under the [MIT License](https://opensource.org/license/mit).
|
|
13
|
+
The full text of the license is included with the project source code.
|
|
14
|
+
|
|
15
|
+
## Project Status Badges
|
|
16
|
+
|
|
17
|
+
### [npm](https://www.npmjs.com/package/@blwatkins/utils)
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
### [Socket](https://socket.dev/npm/package/@blwatkins/utils)
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
### [Bundlephobia](https://bundlephobia.com/package/@blwatkins/utils)
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
### [Package Phobia](https://packagephobia.com/result?p=%40blwatkins%2Futils)
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
### [GitHub](https://github.com/blwatkins/typescript-utils)
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+

|
|
51
|
+

|
|
52
|
+

|
|
53
|
+

|
|
54
|
+

|
|
55
|
+

|
|
56
|
+

|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
### GitHub Actions
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+

|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
## Sources and Technical Notes
|
|
66
|
+
|
|
67
|
+
- [Demonstrated Portfolio Skills](https://blwatkins.github.io/typescript-utils/portfolio-skills.html)
|
|
68
|
+
- [Resources and References](https://blwatkins.github.io/typescript-utils/resources-and-references.html)
|
|
69
|
+
|
|
70
|
+
## Thank Yous
|
|
71
|
+
|
|
72
|
+
A huge thank you to all the open source contributors who have made this project possible by creating and maintaining the libraries and tools used in this project, and to the open source community for fostering collaboration and innovation.
|
|
73
|
+
|
|
74
|
+
A special thank you to all the educators, mentors, and content creators who have shared their knowledge and expertise in the fields of web development and computer science.
|
|
75
|
+
Thank you for giving me the tools, resources, opportunities, support, and inspiration to learn and grow as a developer.
|
|
76
|
+
|
|
77
|
+
----
|
|
78
|
+
|
|
79
|
+
Copyright © 2024-2026 Brittni Watkins.
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
//#region src/discriminator/discriminated.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* TypeBox schema for validating that an object implements the {@link Discriminated} type.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
declare const discriminatedSchema: Type.TObject<{
|
|
11
|
+
/**
|
|
12
|
+
* The discriminator value that identifies the type of a {@link Discriminated} object.
|
|
13
|
+
* This value must be unique across all registered discriminators.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
discriminator: Type.TReadonly<Type.TString>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Discriminated objects can be type checked using the discriminator registry.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
* @since 0.1.0
|
|
25
|
+
*/
|
|
26
|
+
type Discriminated = Static<typeof discriminatedSchema>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/discriminator/discriminator-registry.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* A type guard function that checks if an input is of a specific {@link Discriminated} type.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
type TypeGuard<T extends Discriminated> = (input: unknown) => input is T;
|
|
36
|
+
/**
|
|
37
|
+
* A registration for a discriminator to the {@link DiscriminatorRegistry}.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
interface DiscriminatorRegistration {
|
|
43
|
+
/**
|
|
44
|
+
* The discriminator value that identifies the type of a {@link Discriminated} object.
|
|
45
|
+
* This value must be unique across all registered discriminators.
|
|
46
|
+
*
|
|
47
|
+
* @readonly
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
* @type {string}
|
|
50
|
+
*/
|
|
51
|
+
readonly discriminator: string;
|
|
52
|
+
/**
|
|
53
|
+
* A method that validates whether an input matches the type associated with the {@link discriminator}.
|
|
54
|
+
*
|
|
55
|
+
* @param {unknown} input - The input to validate.
|
|
56
|
+
*
|
|
57
|
+
* @returns {boolean} - `true` if the input matches the type associated with the discriminator, `false` otherwise.
|
|
58
|
+
*
|
|
59
|
+
* @readonly
|
|
60
|
+
* @since 0.1.0
|
|
61
|
+
* @type {(input: unknown) => boolean}
|
|
62
|
+
*/
|
|
63
|
+
readonly validator: (input: unknown) => boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Static registry for managing discriminators and their associated type guards.
|
|
67
|
+
* Discriminators are used to identify the type of a {@link Discriminated} object and validate it using a registered type guard function.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
declare class DiscriminatorRegistry {
|
|
73
|
+
#private;
|
|
74
|
+
/**
|
|
75
|
+
* @throws {Error} DiscriminatorRegistry is a static class and cannot be instantiated.
|
|
76
|
+
*
|
|
77
|
+
* @private
|
|
78
|
+
*/
|
|
79
|
+
private constructor();
|
|
80
|
+
/**
|
|
81
|
+
* Checks if a discriminator is already registered.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} discriminator - The discriminator value to check.
|
|
84
|
+
* @returns {boolean} - `true` if the discriminator is registered, `false` otherwise.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
*/
|
|
89
|
+
static has(discriminator: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Registers a new discriminator and its associated validation function.
|
|
92
|
+
*
|
|
93
|
+
* @param {DiscriminatorRegistration} registration - The registration details for the discriminator.
|
|
94
|
+
* @returns {TypeGuard<T>} - A type guard function for the registered type.
|
|
95
|
+
*
|
|
96
|
+
* @throws {TypeError} If the given input is not an object.
|
|
97
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.discriminator} is not a non-empty single line trimmed string.
|
|
98
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.validator} property is not a function.
|
|
99
|
+
* @throws {Error} If the {@link DiscriminatorRegistration.discriminator} is already registered.
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
*/
|
|
104
|
+
static register<T extends Discriminated>(registration: DiscriminatorRegistration): TypeGuard<T>;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/number/number-utility.d.ts
|
|
108
|
+
/**
|
|
109
|
+
* Static properties and methods for validating number types.
|
|
110
|
+
*
|
|
111
|
+
* @since 0.1.0
|
|
112
|
+
*/
|
|
113
|
+
declare class NumberUtility {
|
|
114
|
+
/**
|
|
115
|
+
* @throws {Error} - NumberUtility is a static class and cannot be instantiated.
|
|
116
|
+
*
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
private constructor();
|
|
120
|
+
/**
|
|
121
|
+
* Is the given input a finite number?
|
|
122
|
+
*
|
|
123
|
+
* @param {unknown} input
|
|
124
|
+
*
|
|
125
|
+
* @returns {input is number} `true` when the input is a finite number; `false` otherwise.
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
* @since 0.1.0
|
|
129
|
+
*/
|
|
130
|
+
static isFiniteNumber(input: unknown): input is number;
|
|
131
|
+
/**
|
|
132
|
+
* Is the given input an integer?
|
|
133
|
+
*
|
|
134
|
+
* @param {unknown} input
|
|
135
|
+
*
|
|
136
|
+
* @returns {input is number} `true` when the input is an integer; `false` otherwise.
|
|
137
|
+
*
|
|
138
|
+
* @public
|
|
139
|
+
* @since 0.1.0
|
|
140
|
+
*/
|
|
141
|
+
static isInteger(input: unknown): input is number;
|
|
142
|
+
/**
|
|
143
|
+
* Is the given input a positive integer?
|
|
144
|
+
*
|
|
145
|
+
* @param {unknown} input
|
|
146
|
+
* @param {boolean} zeroInclusive - `true` if zero should be considered a valid input.
|
|
147
|
+
* `false` if zero should be considered an invalid input.
|
|
148
|
+
*
|
|
149
|
+
* @returns {input is number} `true` if the given input is a positive integer, or zero when `zeroInclusive` is `true`; `false` otherwise.
|
|
150
|
+
*
|
|
151
|
+
* @public
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
static isPositiveInteger(input: unknown, zeroInclusive?: boolean): input is number;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/random/seeded-random/seeded-random-number-generator.d.ts
|
|
158
|
+
/**
|
|
159
|
+
* Deterministic seeded pseudorandom number generator.
|
|
160
|
+
* This generator utilizes the xoshiro128** algorithm, which is a pseudorandom number generator suitable for general-purpose use.
|
|
161
|
+
*
|
|
162
|
+
* @since 0.1.0
|
|
163
|
+
*/
|
|
164
|
+
declare class SeededRandomNumberGenerator {
|
|
165
|
+
#private;
|
|
166
|
+
/**
|
|
167
|
+
* @param {[number, number, number, number]} state - Initial 128-bit state.
|
|
168
|
+
* Must be an array with 4 32-bit unsigned integers, where at least one element is greater than 0.
|
|
169
|
+
*
|
|
170
|
+
* @throws {TypeError} If state is not an array with 4 elements.
|
|
171
|
+
* @throws {RangeError} If each element of state is not a 32-bit unsigned integer.
|
|
172
|
+
* @throws {RangeError} If state does not have at least one element that is greater than 0.
|
|
173
|
+
*
|
|
174
|
+
* @public
|
|
175
|
+
* @since 0.1.0
|
|
176
|
+
*/
|
|
177
|
+
constructor(state: [number, number, number, number]);
|
|
178
|
+
/**
|
|
179
|
+
* @remarks This method advances the internal 128-bit xoshiro128** state by one step.
|
|
180
|
+
* Successive calls produce an independent, uniformly distributed sequence.
|
|
181
|
+
*
|
|
182
|
+
* @returns {number} - The next pseudorandom float in the range [0, 1).
|
|
183
|
+
*
|
|
184
|
+
* @public
|
|
185
|
+
* @since 0.1.0
|
|
186
|
+
*/
|
|
187
|
+
next(): number;
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/random/seeded-random/random-number-generator-factory.d.ts
|
|
191
|
+
/**
|
|
192
|
+
* A static factory class for creating a {@link SeededRandomNumberGenerator} object.
|
|
193
|
+
*
|
|
194
|
+
* @since 0.1.0
|
|
195
|
+
*/
|
|
196
|
+
declare class RandomNumberGeneratorFactory {
|
|
197
|
+
#private;
|
|
198
|
+
/**
|
|
199
|
+
* @throws {Error} - RandomNumberGeneratorFactory is a static class and cannot be instantiated.
|
|
200
|
+
*
|
|
201
|
+
* @private
|
|
202
|
+
* @since 0.1.0
|
|
203
|
+
*/
|
|
204
|
+
private constructor();
|
|
205
|
+
/**
|
|
206
|
+
* Build a {@link SeededRandomNumberGenerator} object with the given seed, namespace, and version.
|
|
207
|
+
*
|
|
208
|
+
* @see {@link SeedVersions.size}
|
|
209
|
+
* @see {@link SeedVersions.isValidIndex}
|
|
210
|
+
*
|
|
211
|
+
* @param {string} seed - The primary input to determine the random number sequence.
|
|
212
|
+
* @param {string|undefined} namespace - Namespace to create different sequences from the same seed.
|
|
213
|
+
* @param {number|undefined} version - The {@link SeedVersions} index to use for selecting the offsets for hashing.
|
|
214
|
+
* Changing the version number will result in a different sequence of random numbers for the same seed and namespace.
|
|
215
|
+
*
|
|
216
|
+
* @returns {SeededRandomNumberGenerator}
|
|
217
|
+
*
|
|
218
|
+
* @throws {TypeError} - When the given seed is not a string.
|
|
219
|
+
* @throws {TypeError} - When the given namespace is not a string.
|
|
220
|
+
* @throws {TypeError} - When the given version is not an integer.
|
|
221
|
+
* @throws {RangeError} - When the given version is not a valid {@link SeedVersions} index.
|
|
222
|
+
*
|
|
223
|
+
* @public
|
|
224
|
+
* @since 0.1.0
|
|
225
|
+
*/
|
|
226
|
+
static build(seed: string, namespace?: string, version?: number): SeededRandomNumberGenerator;
|
|
227
|
+
/**
|
|
228
|
+
* Build a {@link SeededRandomNumberGenerator} object with the given seed and namespace from an asynchronous hashing algorithm.
|
|
229
|
+
*
|
|
230
|
+
* @remarks This method relies on the Web Crypto API via `crypto.subtle`.
|
|
231
|
+
* In Node.js environments, ensure you are using a version where the Web Crypto API is available.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} seed - The primary input to determine the random number sequence.
|
|
234
|
+
* @param {string|undefined} namespace - Namespace to create different sequences from the same seed.
|
|
235
|
+
*
|
|
236
|
+
* @returns {Promise<SeededRandomNumberGenerator>}
|
|
237
|
+
*
|
|
238
|
+
* @throws {TypeError} - When the given seed is not a string.
|
|
239
|
+
* @throws {TypeError} - When the given namespace is not a string.
|
|
240
|
+
*
|
|
241
|
+
* @public
|
|
242
|
+
* @since 0.1.0
|
|
243
|
+
*/
|
|
244
|
+
static asyncBuild(seed: string, namespace?: string): Promise<SeededRandomNumberGenerator>;
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/random/seeded-random/seed-versions.d.ts
|
|
248
|
+
/**
|
|
249
|
+
* A seed version defines a specific set of offsets for the FNV-1a hashing algorithm.
|
|
250
|
+
* A different set of offsets results in a different hash for the same input, which results in a different initial state for the seeded random number generator, which results in a different sequence of pseudorandom numbers.
|
|
251
|
+
*
|
|
252
|
+
* @since 0.1.0
|
|
253
|
+
*/
|
|
254
|
+
interface SeedVersion {
|
|
255
|
+
/**
|
|
256
|
+
* A collection of offset values for the FNV-1a hashing algorithm.
|
|
257
|
+
* Each offset value should be a 32-bit unsigned integer.
|
|
258
|
+
*
|
|
259
|
+
* @readonly
|
|
260
|
+
* @since 0.1.0
|
|
261
|
+
*/
|
|
262
|
+
readonly offsets: readonly [number, number, number, number];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* A static class for accessing different seed versions.
|
|
266
|
+
* Each seed version index is guaranteed to always return the same seed version object, so that the same seed and version will always produce the same sequence of pseudorandom numbers.
|
|
267
|
+
*
|
|
268
|
+
* @since 0.1.0
|
|
269
|
+
*/
|
|
270
|
+
declare class SeedVersions {
|
|
271
|
+
/**
|
|
272
|
+
* @throws {Error} - SeedVersions is a static class and cannot be instantiated.
|
|
273
|
+
*
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
private constructor();
|
|
277
|
+
/**
|
|
278
|
+
* @returns {number} - The total number of seed versions that currently exist.
|
|
279
|
+
*
|
|
280
|
+
* @public
|
|
281
|
+
* @since 0.1.0
|
|
282
|
+
*/
|
|
283
|
+
static get size(): number;
|
|
284
|
+
/**
|
|
285
|
+
* Is the given index a valid seed version?
|
|
286
|
+
*
|
|
287
|
+
* @param {number} index - The index to check.
|
|
288
|
+
*
|
|
289
|
+
* @returns {boolean}
|
|
290
|
+
*
|
|
291
|
+
* @public
|
|
292
|
+
* @since 0.1.0
|
|
293
|
+
*/
|
|
294
|
+
static isValidIndex(index: number): boolean;
|
|
295
|
+
/**
|
|
296
|
+
* @param {number} index - The index of the seed version to retrieve.
|
|
297
|
+
* Must be a valid {@link SeedVersions} index.
|
|
298
|
+
*
|
|
299
|
+
* @returns {SeedVersion} - The seed version with the given index.
|
|
300
|
+
*
|
|
301
|
+
* @throws {RangeError} - If the index is not a valid seed version index.
|
|
302
|
+
*
|
|
303
|
+
* @public
|
|
304
|
+
* @since 0.1.0
|
|
305
|
+
*/
|
|
306
|
+
static getVersion(index: number): SeedVersion;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/string/string-utility.d.ts
|
|
310
|
+
/**
|
|
311
|
+
* Static properties and methods for validating string types.
|
|
312
|
+
*
|
|
313
|
+
* @since 0.1.0
|
|
314
|
+
*/
|
|
315
|
+
declare class StringUtility {
|
|
316
|
+
/**
|
|
317
|
+
* @throws {Error} - StringUtility is a static class and cannot be instantiated.
|
|
318
|
+
*
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
private constructor();
|
|
322
|
+
/**
|
|
323
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
324
|
+
*
|
|
325
|
+
* @returns {RegExp} Regular expression pattern for validating single-line lowercase strings.
|
|
326
|
+
*
|
|
327
|
+
* @public
|
|
328
|
+
* @since 0.1.0
|
|
329
|
+
*/
|
|
330
|
+
static get singleLineLowercaseTrimmedPattern(): RegExp;
|
|
331
|
+
/**
|
|
332
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
333
|
+
*
|
|
334
|
+
* @returns {RegExp} Regular expression pattern for validating single-line uppercase strings.
|
|
335
|
+
*
|
|
336
|
+
* @public
|
|
337
|
+
* @since 0.1.0
|
|
338
|
+
*/
|
|
339
|
+
static get singleLineUppercaseTrimmedPattern(): RegExp;
|
|
340
|
+
/**
|
|
341
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
342
|
+
*
|
|
343
|
+
* @returns {RegExp} Regular expression pattern for validating single-line mixed-case strings.
|
|
344
|
+
*
|
|
345
|
+
* @public
|
|
346
|
+
* @since 0.1.0
|
|
347
|
+
*/
|
|
348
|
+
static get singleLineTrimmedPattern(): RegExp;
|
|
349
|
+
/**
|
|
350
|
+
* Is the given input a string?
|
|
351
|
+
*
|
|
352
|
+
* @param {unknown} input
|
|
353
|
+
*
|
|
354
|
+
* @returns {input is string}
|
|
355
|
+
*
|
|
356
|
+
* @public
|
|
357
|
+
* @since 0.1.0
|
|
358
|
+
*/
|
|
359
|
+
static isString(input: unknown): input is string;
|
|
360
|
+
/**
|
|
361
|
+
* Is the given input a non-empty string?
|
|
362
|
+
* Non-empty strings must contain at least one non-whitespace character.
|
|
363
|
+
*
|
|
364
|
+
* @param {unknown} input
|
|
365
|
+
*
|
|
366
|
+
* @returns {boolean}
|
|
367
|
+
*
|
|
368
|
+
* @public
|
|
369
|
+
* @since 0.1.0
|
|
370
|
+
*/
|
|
371
|
+
static isNonEmptyString(input: unknown): boolean;
|
|
372
|
+
/**
|
|
373
|
+
* Is the given input a single-line lowercase string that is trimmed (no leading or trailing whitespace)?
|
|
374
|
+
*
|
|
375
|
+
* @see {@link StringUtility.singleLineLowercaseTrimmedPattern}
|
|
376
|
+
*
|
|
377
|
+
* @param {unknown} input
|
|
378
|
+
*
|
|
379
|
+
* @returns {boolean}
|
|
380
|
+
*
|
|
381
|
+
* @public
|
|
382
|
+
* @since 0.1.0
|
|
383
|
+
*/
|
|
384
|
+
static isSingleLineLowercaseTrimmedString(input: unknown): boolean;
|
|
385
|
+
/**
|
|
386
|
+
* Is the given input a single-line uppercase string that is trimmed (no leading or trailing whitespace)?
|
|
387
|
+
*
|
|
388
|
+
* @see {@link StringUtility.singleLineUppercaseTrimmedPattern}
|
|
389
|
+
*
|
|
390
|
+
* @param {unknown} input
|
|
391
|
+
*
|
|
392
|
+
* @returns {boolean}
|
|
393
|
+
*
|
|
394
|
+
* @public
|
|
395
|
+
* @since 0.1.0
|
|
396
|
+
*/
|
|
397
|
+
static isSingleLineUppercaseTrimmedString(input: unknown): boolean;
|
|
398
|
+
/**
|
|
399
|
+
* Is the given input a single-line string that is trimmed (no leading or trailing whitespace)?
|
|
400
|
+
*
|
|
401
|
+
* @see {@link StringUtility.singleLineTrimmedPattern}
|
|
402
|
+
*
|
|
403
|
+
* @param {unknown} input
|
|
404
|
+
*
|
|
405
|
+
* @returns {boolean}
|
|
406
|
+
*
|
|
407
|
+
* @public
|
|
408
|
+
* @since 0.1.0
|
|
409
|
+
*/
|
|
410
|
+
static isSingleLineTrimmedString(input: unknown): boolean;
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
export { Discriminated, DiscriminatorRegistration, DiscriminatorRegistry, NumberUtility, RandomNumberGeneratorFactory, SeedVersion, SeedVersions, SeededRandomNumberGenerator, StringUtility, TypeGuard, discriminatedSchema };
|
package/_dist/index.mjs
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
//#region src/discriminator/discriminated.ts
|
|
4
|
+
/**
|
|
5
|
+
* TypeBox schema for validating that an object implements the {@link Discriminated} type.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
const discriminatedSchema = Type.Object({
|
|
11
|
+
/**
|
|
12
|
+
* The discriminator value that identifies the type of a {@link Discriminated} object.
|
|
13
|
+
* This value must be unique across all registered discriminators.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
* @type {string}
|
|
17
|
+
*/
|
|
18
|
+
discriminator: Type.Readonly(Type.String()) });
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/string/string-utility.ts
|
|
22
|
+
const RegularExpressions = {
|
|
23
|
+
singleLineLowercaseTrimmed: /^(?!\s)(?!.*\s$)(?!.*\p{Lu})(?!.* {2})[^\t\r\n]+$/u,
|
|
24
|
+
singleLineUppercaseTrimmed: /^(?!\s)(?!.*\s$)(?!.*\p{Ll})(?!.* {2})[^\t\r\n]+$/u,
|
|
25
|
+
singleLineTrimmed: /^(?!\s)(?!.*\s$)(?!.* {2})[^\t\r\n]+$/
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Static properties and methods for validating string types.
|
|
29
|
+
*
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
var StringUtility = class StringUtility {
|
|
33
|
+
/**
|
|
34
|
+
* @throws {Error} - StringUtility is a static class and cannot be instantiated.
|
|
35
|
+
*
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
constructor() {
|
|
39
|
+
throw new Error("StringUtility is a static class and cannot be instantiated.");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
43
|
+
*
|
|
44
|
+
* @returns {RegExp} Regular expression pattern for validating single-line lowercase strings.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
static get singleLineLowercaseTrimmedPattern() {
|
|
50
|
+
return RegularExpressions.singleLineLowercaseTrimmed;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
54
|
+
*
|
|
55
|
+
* @returns {RegExp} Regular expression pattern for validating single-line uppercase strings.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
static get singleLineUppercaseTrimmedPattern() {
|
|
61
|
+
return RegularExpressions.singleLineUppercaseTrimmed;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @remarks This expression does not allow tab breaks, new lines, leading whitespace, trailing whitespace, or consecutive spaces within the string.
|
|
65
|
+
*
|
|
66
|
+
* @returns {RegExp} Regular expression pattern for validating single-line mixed-case strings.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
static get singleLineTrimmedPattern() {
|
|
72
|
+
return RegularExpressions.singleLineTrimmed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Is the given input a string?
|
|
76
|
+
*
|
|
77
|
+
* @param {unknown} input
|
|
78
|
+
*
|
|
79
|
+
* @returns {input is string}
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
* @since 0.1.0
|
|
83
|
+
*/
|
|
84
|
+
static isString(input) {
|
|
85
|
+
return typeof input === "string";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Is the given input a non-empty string?
|
|
89
|
+
* Non-empty strings must contain at least one non-whitespace character.
|
|
90
|
+
*
|
|
91
|
+
* @param {unknown} input
|
|
92
|
+
*
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
* @since 0.1.0
|
|
97
|
+
*/
|
|
98
|
+
static isNonEmptyString(input) {
|
|
99
|
+
return StringUtility.isString(input) && input.trim().length > 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Is the given input a single-line lowercase string that is trimmed (no leading or trailing whitespace)?
|
|
103
|
+
*
|
|
104
|
+
* @see {@link StringUtility.singleLineLowercaseTrimmedPattern}
|
|
105
|
+
*
|
|
106
|
+
* @param {unknown} input
|
|
107
|
+
*
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
* @since 0.1.0
|
|
112
|
+
*/
|
|
113
|
+
static isSingleLineLowercaseTrimmedString(input) {
|
|
114
|
+
return StringUtility.isString(input) && StringUtility.singleLineLowercaseTrimmedPattern.test(input);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Is the given input a single-line uppercase string that is trimmed (no leading or trailing whitespace)?
|
|
118
|
+
*
|
|
119
|
+
* @see {@link StringUtility.singleLineUppercaseTrimmedPattern}
|
|
120
|
+
*
|
|
121
|
+
* @param {unknown} input
|
|
122
|
+
*
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*
|
|
125
|
+
* @public
|
|
126
|
+
* @since 0.1.0
|
|
127
|
+
*/
|
|
128
|
+
static isSingleLineUppercaseTrimmedString(input) {
|
|
129
|
+
return StringUtility.isString(input) && StringUtility.singleLineUppercaseTrimmedPattern.test(input);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Is the given input a single-line string that is trimmed (no leading or trailing whitespace)?
|
|
133
|
+
*
|
|
134
|
+
* @see {@link StringUtility.singleLineTrimmedPattern}
|
|
135
|
+
*
|
|
136
|
+
* @param {unknown} input
|
|
137
|
+
*
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
* @since 0.1.0
|
|
142
|
+
*/
|
|
143
|
+
static isSingleLineTrimmedString(input) {
|
|
144
|
+
return StringUtility.isString(input) && StringUtility.singleLineTrimmedPattern.test(input);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/discriminator/discriminator-registry.ts
|
|
150
|
+
/**
|
|
151
|
+
* Static registry for managing discriminators and their associated type guards.
|
|
152
|
+
* Discriminators are used to identify the type of a {@link Discriminated} object and validate it using a registered type guard function.
|
|
153
|
+
*
|
|
154
|
+
* @public
|
|
155
|
+
* @since 0.1.0
|
|
156
|
+
*/
|
|
157
|
+
var DiscriminatorRegistry = class DiscriminatorRegistry {
|
|
158
|
+
/**
|
|
159
|
+
* A map of discriminator values to their corresponding validation functions.
|
|
160
|
+
*
|
|
161
|
+
* @readonly
|
|
162
|
+
* @private
|
|
163
|
+
* @type {Map<string, (input: unknown) => boolean>}
|
|
164
|
+
*/
|
|
165
|
+
static #discriminators = /* @__PURE__ */ new Map();
|
|
166
|
+
/**
|
|
167
|
+
* @throws {Error} DiscriminatorRegistry is a static class and cannot be instantiated.
|
|
168
|
+
*
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
constructor() {
|
|
172
|
+
throw new Error("DiscriminatorRegistry is a static class and cannot be instantiated.");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Checks if a discriminator is already registered.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} discriminator - The discriminator value to check.
|
|
178
|
+
* @returns {boolean} - `true` if the discriminator is registered, `false` otherwise.
|
|
179
|
+
*
|
|
180
|
+
* @public
|
|
181
|
+
* @since 0.1.0
|
|
182
|
+
*/
|
|
183
|
+
static has(discriminator) {
|
|
184
|
+
return DiscriminatorRegistry.#discriminators.has(discriminator);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Registers a new discriminator and its associated validation function.
|
|
188
|
+
*
|
|
189
|
+
* @param {DiscriminatorRegistration} registration - The registration details for the discriminator.
|
|
190
|
+
* @returns {TypeGuard<T>} - A type guard function for the registered type.
|
|
191
|
+
*
|
|
192
|
+
* @throws {TypeError} If the given input is not an object.
|
|
193
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.discriminator} is not a non-empty single line trimmed string.
|
|
194
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.validator} property is not a function.
|
|
195
|
+
* @throws {Error} If the {@link DiscriminatorRegistration.discriminator} is already registered.
|
|
196
|
+
*
|
|
197
|
+
* @public
|
|
198
|
+
* @since 0.1.0
|
|
199
|
+
*/
|
|
200
|
+
static register(registration) {
|
|
201
|
+
DiscriminatorRegistry.#validateRegistration(registration);
|
|
202
|
+
DiscriminatorRegistry.#discriminators.set(registration.discriminator, registration.validator);
|
|
203
|
+
return (input) => {
|
|
204
|
+
return DiscriminatorRegistry.#validate(input, registration.discriminator);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validates a discriminator registration object to ensure it has the required properties and that the discriminator value is unique.
|
|
209
|
+
*
|
|
210
|
+
* @see {@link StringUtility.isSingleLineTrimmedString}
|
|
211
|
+
*
|
|
212
|
+
* @param {unknown} input - The input to validate.
|
|
213
|
+
*
|
|
214
|
+
* @returns {void}
|
|
215
|
+
*
|
|
216
|
+
* @throws {TypeError} If the given input is not an object.
|
|
217
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.discriminator} is not a non-empty single line trimmed string.
|
|
218
|
+
* @throws {TypeError} If the {@link DiscriminatorRegistration.validator} property is not a function.
|
|
219
|
+
* @throws {Error} If the {@link DiscriminatorRegistration.discriminator} is already registered.
|
|
220
|
+
*
|
|
221
|
+
* @private
|
|
222
|
+
*/
|
|
223
|
+
static #validateRegistration(input) {
|
|
224
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new TypeError("Registration must be an object.");
|
|
225
|
+
const registration = input;
|
|
226
|
+
if (!StringUtility.isSingleLineTrimmedString(registration.discriminator)) throw new TypeError(`Discriminator '${registration.discriminator}' must be a non-empty single line trimmed string.`);
|
|
227
|
+
if (typeof registration.validator !== "function") throw new TypeError(`Discriminator '${registration.discriminator}' must have a validator function.`);
|
|
228
|
+
if (DiscriminatorRegistry.has(registration.discriminator)) throw new Error(`Discriminator '${registration.discriminator}' is already registered.`);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Validates an input against a specific discriminator.
|
|
232
|
+
*
|
|
233
|
+
* @param {unknown} input - The input to validate.
|
|
234
|
+
* @param {string} discriminator - The discriminator value to check.
|
|
235
|
+
*
|
|
236
|
+
* @returns {boolean} - `true` if the input matches the type associated with the discriminator, `false` otherwise.
|
|
237
|
+
*
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
static #validate(input, discriminator) {
|
|
241
|
+
if (!DiscriminatorRegistry.#isDiscriminated(input, discriminator)) return false;
|
|
242
|
+
const validator = DiscriminatorRegistry.#discriminators.get(discriminator);
|
|
243
|
+
if (validator) return validator(input);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Is the given input an object with a discriminator property that matches the given discriminator value?
|
|
248
|
+
*
|
|
249
|
+
* @param {unknown} input - The input to check.
|
|
250
|
+
* @param {string} discriminator - The discriminator value to match.
|
|
251
|
+
*
|
|
252
|
+
* @returns {boolean} - `true` if the input is an object with a discriminator property that matches the given discriminator value, `false` otherwise.
|
|
253
|
+
*
|
|
254
|
+
* @private
|
|
255
|
+
*/
|
|
256
|
+
static #isDiscriminated(input, discriminator) {
|
|
257
|
+
if (input && typeof input === "object") return input.discriminator === discriminator;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/number/number-utility.ts
|
|
264
|
+
/**
|
|
265
|
+
* Static properties and methods for validating number types.
|
|
266
|
+
*
|
|
267
|
+
* @since 0.1.0
|
|
268
|
+
*/
|
|
269
|
+
var NumberUtility = class NumberUtility {
|
|
270
|
+
/**
|
|
271
|
+
* @throws {Error} - NumberUtility is a static class and cannot be instantiated.
|
|
272
|
+
*
|
|
273
|
+
* @private
|
|
274
|
+
*/
|
|
275
|
+
constructor() {
|
|
276
|
+
throw new Error("NumberUtility is a static class and cannot be instantiated.");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Is the given input a finite number?
|
|
280
|
+
*
|
|
281
|
+
* @param {unknown} input
|
|
282
|
+
*
|
|
283
|
+
* @returns {input is number} `true` when the input is a finite number; `false` otherwise.
|
|
284
|
+
*
|
|
285
|
+
* @public
|
|
286
|
+
* @since 0.1.0
|
|
287
|
+
*/
|
|
288
|
+
static isFiniteNumber(input) {
|
|
289
|
+
return Number.isFinite(input);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Is the given input an integer?
|
|
293
|
+
*
|
|
294
|
+
* @param {unknown} input
|
|
295
|
+
*
|
|
296
|
+
* @returns {input is number} `true` when the input is an integer; `false` otherwise.
|
|
297
|
+
*
|
|
298
|
+
* @public
|
|
299
|
+
* @since 0.1.0
|
|
300
|
+
*/
|
|
301
|
+
static isInteger(input) {
|
|
302
|
+
return Number.isInteger(input);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Is the given input a positive integer?
|
|
306
|
+
*
|
|
307
|
+
* @param {unknown} input
|
|
308
|
+
* @param {boolean} zeroInclusive - `true` if zero should be considered a valid input.
|
|
309
|
+
* `false` if zero should be considered an invalid input.
|
|
310
|
+
*
|
|
311
|
+
* @returns {input is number} `true` if the given input is a positive integer, or zero when `zeroInclusive` is `true`; `false` otherwise.
|
|
312
|
+
*
|
|
313
|
+
* @public
|
|
314
|
+
* @since 0.1.0
|
|
315
|
+
*/
|
|
316
|
+
static isPositiveInteger(input, zeroInclusive) {
|
|
317
|
+
if (!NumberUtility.isInteger(input)) return false;
|
|
318
|
+
if (zeroInclusive === true) return input >= 0;
|
|
319
|
+
return input > 0;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/random/seeded-random/seed-versions.ts
|
|
325
|
+
/**
|
|
326
|
+
* @remarks Once a seed version has been published, it should <b>NEVER</b> be changed or updated.
|
|
327
|
+
* The order of seed versions should <b>NEVER</b> be changed.
|
|
328
|
+
* New seed versions can only be added to the end of the array.
|
|
329
|
+
* Each element in the offsets array should be unique.
|
|
330
|
+
*
|
|
331
|
+
* @constant
|
|
332
|
+
*/
|
|
333
|
+
const seedVersions = [{ offsets: Object.freeze([
|
|
334
|
+
1779033703,
|
|
335
|
+
3144134277,
|
|
336
|
+
1013904242,
|
|
337
|
+
2773480762
|
|
338
|
+
]) }, { offsets: Object.freeze([
|
|
339
|
+
2166136261,
|
|
340
|
+
55548468,
|
|
341
|
+
2712847316,
|
|
342
|
+
1584364171
|
|
343
|
+
]) }];
|
|
344
|
+
/**
|
|
345
|
+
* A static class for accessing different seed versions.
|
|
346
|
+
* Each seed version index is guaranteed to always return the same seed version object, so that the same seed and version will always produce the same sequence of pseudorandom numbers.
|
|
347
|
+
*
|
|
348
|
+
* @since 0.1.0
|
|
349
|
+
*/
|
|
350
|
+
var SeedVersions = class SeedVersions {
|
|
351
|
+
/**
|
|
352
|
+
* @throws {Error} - SeedVersions is a static class and cannot be instantiated.
|
|
353
|
+
*
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
constructor() {
|
|
357
|
+
throw new Error("SeedVersions is a static class and cannot be instantiated.");
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* @returns {number} - The total number of seed versions that currently exist.
|
|
361
|
+
*
|
|
362
|
+
* @public
|
|
363
|
+
* @since 0.1.0
|
|
364
|
+
*/
|
|
365
|
+
static get size() {
|
|
366
|
+
return seedVersions.length;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Is the given index a valid seed version?
|
|
370
|
+
*
|
|
371
|
+
* @param {number} index - The index to check.
|
|
372
|
+
*
|
|
373
|
+
* @returns {boolean}
|
|
374
|
+
*
|
|
375
|
+
* @public
|
|
376
|
+
* @since 0.1.0
|
|
377
|
+
*/
|
|
378
|
+
static isValidIndex(index) {
|
|
379
|
+
return NumberUtility.isPositiveInteger(index, true) && index < seedVersions.length;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* @param {number} index - The index of the seed version to retrieve.
|
|
383
|
+
* Must be a valid {@link SeedVersions} index.
|
|
384
|
+
*
|
|
385
|
+
* @returns {SeedVersion} - The seed version with the given index.
|
|
386
|
+
*
|
|
387
|
+
* @throws {RangeError} - If the index is not a valid seed version index.
|
|
388
|
+
*
|
|
389
|
+
* @public
|
|
390
|
+
* @since 0.1.0
|
|
391
|
+
*/
|
|
392
|
+
static getVersion(index) {
|
|
393
|
+
if (!SeedVersions.isValidIndex(index)) throw new RangeError(`SeedVersion ${index} does not exist`);
|
|
394
|
+
return seedVersions[index];
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/random/seeded-random/seeded-random-number-generator.ts
|
|
400
|
+
/**
|
|
401
|
+
* Deterministic seeded pseudorandom number generator.
|
|
402
|
+
* This generator utilizes the xoshiro128** algorithm, which is a pseudorandom number generator suitable for general-purpose use.
|
|
403
|
+
*
|
|
404
|
+
* @since 0.1.0
|
|
405
|
+
*/
|
|
406
|
+
var SeededRandomNumberGenerator = class SeededRandomNumberGenerator {
|
|
407
|
+
/**
|
|
408
|
+
* Internal xoshiro128** state (4 x 32-bit unsigned integers).
|
|
409
|
+
*
|
|
410
|
+
* @private
|
|
411
|
+
* @readonly
|
|
412
|
+
* @type {[number, number, number, number]}
|
|
413
|
+
*/
|
|
414
|
+
#state;
|
|
415
|
+
/**
|
|
416
|
+
* @param {[number, number, number, number]} state - Initial 128-bit state.
|
|
417
|
+
* Must be an array with 4 32-bit unsigned integers, where at least one element is greater than 0.
|
|
418
|
+
*
|
|
419
|
+
* @throws {TypeError} If state is not an array with 4 elements.
|
|
420
|
+
* @throws {RangeError} If each element of state is not a 32-bit unsigned integer.
|
|
421
|
+
* @throws {RangeError} If state does not have at least one element that is greater than 0.
|
|
422
|
+
*
|
|
423
|
+
* @public
|
|
424
|
+
* @since 0.1.0
|
|
425
|
+
*/
|
|
426
|
+
constructor(state) {
|
|
427
|
+
this.#validateState(state);
|
|
428
|
+
this.#state = [
|
|
429
|
+
state[0],
|
|
430
|
+
state[1],
|
|
431
|
+
state[2],
|
|
432
|
+
state[3]
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* @remarks This method advances the internal 128-bit xoshiro128** state by one step.
|
|
437
|
+
* Successive calls produce an independent, uniformly distributed sequence.
|
|
438
|
+
*
|
|
439
|
+
* @returns {number} - The next pseudorandom float in the range [0, 1).
|
|
440
|
+
*
|
|
441
|
+
* @public
|
|
442
|
+
* @since 0.1.0
|
|
443
|
+
*/
|
|
444
|
+
next() {
|
|
445
|
+
const result = SeededRandomNumberGenerator.#rotl(Math.imul(this.#state[1], 5), 7);
|
|
446
|
+
const output = (Math.imul(result, 9) >>> 0) / 4294967296;
|
|
447
|
+
const t = this.#state[1] << 9 >>> 0;
|
|
448
|
+
this.#state[2] ^= this.#state[0];
|
|
449
|
+
this.#state[3] ^= this.#state[1];
|
|
450
|
+
this.#state[1] ^= this.#state[2];
|
|
451
|
+
this.#state[0] ^= this.#state[3];
|
|
452
|
+
this.#state[2] ^= t;
|
|
453
|
+
this.#state[3] = SeededRandomNumberGenerator.#rotl(this.#state[3], 11);
|
|
454
|
+
return output;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Rotates the bits of a 32-bit unsigned integer left by k positions.
|
|
458
|
+
*
|
|
459
|
+
* @param {number} x - The number to rotate. Must be a 32-bit unsigned integer.
|
|
460
|
+
* @param {number} k - The number of bits to rotate.
|
|
461
|
+
*
|
|
462
|
+
* @returns {number}
|
|
463
|
+
*
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
466
|
+
static #rotl(x, k) {
|
|
467
|
+
return (x << k | x >>> 32 - k) >>> 0;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* The maximum valid value in the state array.
|
|
471
|
+
*
|
|
472
|
+
* @returns {number} 0xFFFFFFFF
|
|
473
|
+
*
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
static get #maxStateValue() {
|
|
477
|
+
return 4294967295;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Validate that state is an array with 4 32-bit unsigned integers, where at least one element is greater than 0.
|
|
481
|
+
*
|
|
482
|
+
* @param {[number, number, number, number]} state - The state to validate.
|
|
483
|
+
*
|
|
484
|
+
* @throws {TypeError} If state is not an array with 4 elements.
|
|
485
|
+
* @throws {RangeError} If each element of state is not a 32-bit unsigned integer
|
|
486
|
+
* @throws {RangeError} If state does not have at least one element that is greater than 0.
|
|
487
|
+
*
|
|
488
|
+
* @private
|
|
489
|
+
*/
|
|
490
|
+
#validateState(state) {
|
|
491
|
+
if (!Array.isArray(state) || state.length !== 4) throw new TypeError("State must be an array with 4 elements.");
|
|
492
|
+
for (const value of state) if (!NumberUtility.isPositiveInteger(value, true) || value > SeededRandomNumberGenerator.#maxStateValue) throw new RangeError("Elements of state must be 32-bit unsigned integers (maximum value 0xFFFFFFFF).");
|
|
493
|
+
if (state[0] === 0 && state[1] === 0 && state[2] === 0 && state[3] === 0) throw new RangeError("State must have at least one element that is greater than 0.");
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/random/seeded-random/random-number-generator-factory.ts
|
|
499
|
+
/**
|
|
500
|
+
* @type {TextEncoder}
|
|
501
|
+
*/
|
|
502
|
+
const textEncoder = new TextEncoder();
|
|
503
|
+
/**
|
|
504
|
+
* A static factory class for creating a {@link SeededRandomNumberGenerator} object.
|
|
505
|
+
*
|
|
506
|
+
* @since 0.1.0
|
|
507
|
+
*/
|
|
508
|
+
var RandomNumberGeneratorFactory = class RandomNumberGeneratorFactory {
|
|
509
|
+
/**
|
|
510
|
+
* @throws {Error} - RandomNumberGeneratorFactory is a static class and cannot be instantiated.
|
|
511
|
+
*
|
|
512
|
+
* @private
|
|
513
|
+
* @since 0.1.0
|
|
514
|
+
*/
|
|
515
|
+
constructor() {
|
|
516
|
+
throw new Error("RandomNumberGeneratorFactory is a static class and cannot be instantiated.");
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Prime number for FNV-1a hashing algorithm.
|
|
520
|
+
* This number is an algorithmic constant; it must not change.
|
|
521
|
+
*
|
|
522
|
+
* @returns {number} - 0x01000193
|
|
523
|
+
*
|
|
524
|
+
* @private
|
|
525
|
+
*/
|
|
526
|
+
static get #fnvPrime() {
|
|
527
|
+
return 16777619;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Build a {@link SeededRandomNumberGenerator} object with the given seed, namespace, and version.
|
|
531
|
+
*
|
|
532
|
+
* @see {@link SeedVersions.size}
|
|
533
|
+
* @see {@link SeedVersions.isValidIndex}
|
|
534
|
+
*
|
|
535
|
+
* @param {string} seed - The primary input to determine the random number sequence.
|
|
536
|
+
* @param {string|undefined} namespace - Namespace to create different sequences from the same seed.
|
|
537
|
+
* @param {number|undefined} version - The {@link SeedVersions} index to use for selecting the offsets for hashing.
|
|
538
|
+
* Changing the version number will result in a different sequence of random numbers for the same seed and namespace.
|
|
539
|
+
*
|
|
540
|
+
* @returns {SeededRandomNumberGenerator}
|
|
541
|
+
*
|
|
542
|
+
* @throws {TypeError} - When the given seed is not a string.
|
|
543
|
+
* @throws {TypeError} - When the given namespace is not a string.
|
|
544
|
+
* @throws {TypeError} - When the given version is not an integer.
|
|
545
|
+
* @throws {RangeError} - When the given version is not a valid {@link SeedVersions} index.
|
|
546
|
+
*
|
|
547
|
+
* @public
|
|
548
|
+
* @since 0.1.0
|
|
549
|
+
*/
|
|
550
|
+
static build(seed, namespace, version) {
|
|
551
|
+
RandomNumberGeneratorFactory.#validateBuildInputs(seed, namespace, version);
|
|
552
|
+
const input = RandomNumberGeneratorFactory.#buildInputString(seed, namespace);
|
|
553
|
+
return new SeededRandomNumberGenerator(RandomNumberGeneratorFactory.#generateFnvHashState(input, version));
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Build a {@link SeededRandomNumberGenerator} object with the given seed and namespace from an asynchronous hashing algorithm.
|
|
557
|
+
*
|
|
558
|
+
* @remarks This method relies on the Web Crypto API via `crypto.subtle`.
|
|
559
|
+
* In Node.js environments, ensure you are using a version where the Web Crypto API is available.
|
|
560
|
+
*
|
|
561
|
+
* @param {string} seed - The primary input to determine the random number sequence.
|
|
562
|
+
* @param {string|undefined} namespace - Namespace to create different sequences from the same seed.
|
|
563
|
+
*
|
|
564
|
+
* @returns {Promise<SeededRandomNumberGenerator>}
|
|
565
|
+
*
|
|
566
|
+
* @throws {TypeError} - When the given seed is not a string.
|
|
567
|
+
* @throws {TypeError} - When the given namespace is not a string.
|
|
568
|
+
*
|
|
569
|
+
* @public
|
|
570
|
+
* @since 0.1.0
|
|
571
|
+
*/
|
|
572
|
+
static async asyncBuild(seed, namespace) {
|
|
573
|
+
RandomNumberGeneratorFactory.#validateBuildInputs(seed, namespace);
|
|
574
|
+
const input = RandomNumberGeneratorFactory.#buildInputString(seed, namespace);
|
|
575
|
+
return new SeededRandomNumberGenerator(await RandomNumberGeneratorFactory.#generateSha256HashState(input));
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* @see {@link SeedVersions.isValidIndex}
|
|
579
|
+
*
|
|
580
|
+
* @param {string} seed - seed to validate
|
|
581
|
+
* @param {string|undefined} namespace - namespace to validate
|
|
582
|
+
* @param {number|undefined} version - version to validate
|
|
583
|
+
*
|
|
584
|
+
* @throws {TypeError} - When the given seed is not a string.
|
|
585
|
+
* @throws {TypeError} - When the given namespace is not a string.
|
|
586
|
+
* @throws {TypeError} - When the given version is not an integer.
|
|
587
|
+
* @throws {RangeError} - When the given version is not a valid {@link SeedVersions} index.
|
|
588
|
+
*
|
|
589
|
+
* @private
|
|
590
|
+
*/
|
|
591
|
+
static #validateBuildInputs(seed, namespace, version) {
|
|
592
|
+
if (!StringUtility.isString(seed)) throw new TypeError("Seed must be a string.");
|
|
593
|
+
if (namespace !== void 0 && !StringUtility.isString(namespace)) throw new TypeError("Namespace must be a string.");
|
|
594
|
+
if (version !== void 0 && !NumberUtility.isInteger(version)) throw new TypeError("Version must be an integer.");
|
|
595
|
+
if (version !== void 0 && !SeedVersions.isValidIndex(version)) throw new RangeError("Version must be a valid seed versions index.");
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Build the hash algorithm input string from the given seed and namespace.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} seed
|
|
601
|
+
* @param {string|undefined} namespace - Optional namespace to create different sequences from the same seed.
|
|
602
|
+
*
|
|
603
|
+
* @returns {string}
|
|
604
|
+
*
|
|
605
|
+
* @private
|
|
606
|
+
*/
|
|
607
|
+
static #buildInputString(seed, namespace) {
|
|
608
|
+
if (StringUtility.isString(namespace)) return `${namespace}\x00${seed}`;
|
|
609
|
+
else return seed;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Create a state array from the given input using the FNV-1a hashing algorithm.
|
|
613
|
+
* The state is generated by hashing the input string with four different offsets, which are determined by the given version number.
|
|
614
|
+
*
|
|
615
|
+
* @see {@link SeedVersions.size}
|
|
616
|
+
* @see {@link SeedVersions.isValidIndex}
|
|
617
|
+
*
|
|
618
|
+
* @param {string} input - Input to be hashed and converted into the initial state of the random number generator.
|
|
619
|
+
* @param {number} version - The {@link SeedVersions} index to use for selecting the offsets for hashing.
|
|
620
|
+
* Changing the version number will result in a different sequence of random numbers for the same input.
|
|
621
|
+
* Default value is 0.
|
|
622
|
+
*
|
|
623
|
+
* @returns {[number, number, number, number]}
|
|
624
|
+
*
|
|
625
|
+
* @throws {TypeError} - When the given input is not a string.
|
|
626
|
+
* @throws {TypeError} - When the given version is not an integer.
|
|
627
|
+
* @throws {RangeError} - When the given version is not a valid {@link SeedVersions} index.
|
|
628
|
+
*
|
|
629
|
+
* @private
|
|
630
|
+
*/
|
|
631
|
+
static #generateFnvHashState(input, version = 0) {
|
|
632
|
+
RandomNumberGeneratorFactory.#validateBuildInputs(input, void 0, version);
|
|
633
|
+
const bytes = textEncoder.encode(input);
|
|
634
|
+
const [o0, o1, o2, o3] = SeedVersions.getVersion(version).offsets;
|
|
635
|
+
const hash = (offset) => {
|
|
636
|
+
let h = offset;
|
|
637
|
+
for (const byte of bytes) {
|
|
638
|
+
h ^= byte;
|
|
639
|
+
h = Math.imul(h, RandomNumberGeneratorFactory.#fnvPrime);
|
|
640
|
+
}
|
|
641
|
+
return h >>> 0;
|
|
642
|
+
};
|
|
643
|
+
return [
|
|
644
|
+
hash(o0),
|
|
645
|
+
hash(o1),
|
|
646
|
+
hash(o2),
|
|
647
|
+
hash(o3)
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Create a state array from the given input using the SHA-256 hashing algorithm.
|
|
652
|
+
*
|
|
653
|
+
* @remarks This method hashes the given input with SHA-256 and folds the 256-bit output into 128 bits by XOR-ing the two 128-bit halves together, fully utilizing all output bits.<hr/>
|
|
654
|
+
* This method relies on the Web Crypto API via `crypto.subtle`.
|
|
655
|
+
* In Node.js environments, ensure you are using a version where the Web Crypto API is available.
|
|
656
|
+
*
|
|
657
|
+
* @param {string} input - Input to be hashed and converted into the initial state of the random number generator.
|
|
658
|
+
*
|
|
659
|
+
* @returns {[number, number, number, number]}
|
|
660
|
+
*
|
|
661
|
+
* @throws {TypeError} - When the given input is not a string.
|
|
662
|
+
*
|
|
663
|
+
* @private
|
|
664
|
+
*/
|
|
665
|
+
static async #generateSha256HashState(input) {
|
|
666
|
+
RandomNumberGeneratorFactory.#validateBuildInputs(input);
|
|
667
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
668
|
+
const v = new DataView(hashBuffer);
|
|
669
|
+
return [
|
|
670
|
+
(v.getUint32(0, false) ^ v.getUint32(16, false)) >>> 0,
|
|
671
|
+
(v.getUint32(4, false) ^ v.getUint32(20, false)) >>> 0,
|
|
672
|
+
(v.getUint32(8, false) ^ v.getUint32(24, false)) >>> 0,
|
|
673
|
+
(v.getUint32(12, false) ^ v.getUint32(28, false)) >>> 0
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
//#endregion
|
|
679
|
+
export { DiscriminatorRegistry, NumberUtility, RandomNumberGeneratorFactory, SeedVersions, SeededRandomNumberGenerator, StringUtility, discriminatedSchema };
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blwatkins/utils",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "A growing toolkit of reusable, domain-agnostic TypeScript and JavaScript utilities for everyday development.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Brittni Watkins",
|
|
9
|
+
"url": "https://blwatkins.github.io/"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://blwatkins.github.io/typescript-utils/",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/blwatkins/typescript-utils.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/blwatkins/typescript-utils/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": "^22.22.0 || >=24"
|
|
22
|
+
},
|
|
23
|
+
"types": "./_dist/index.d.mts",
|
|
24
|
+
"module": "./_dist/index.mjs",
|
|
25
|
+
"main": "./_dist/index.mjs",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./_dist/index.d.mts",
|
|
29
|
+
"default": "./_dist/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"./_dist",
|
|
34
|
+
"./LICENSE",
|
|
35
|
+
"./package.json",
|
|
36
|
+
"./README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"lint:js": "eslint -c eslint.config.js.mjs .",
|
|
40
|
+
"lint:ts": "eslint -c eslint.config.ts.mjs .",
|
|
41
|
+
"lint:all": "npm run lint:js && npm run lint:ts",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"test:ui": "vitest --ui",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"docs": "typedoc",
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"prepack": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"typebox": "^1.3.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
55
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
56
|
+
"@types/node": "^26.0.1",
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
58
|
+
"@vitest/ui": "^4.1.9",
|
|
59
|
+
"eslint": "^10.6.0",
|
|
60
|
+
"eslint-plugin-es-x": "^9.7.0",
|
|
61
|
+
"globals": "^17.7.0",
|
|
62
|
+
"tsdown": "^0.22.3",
|
|
63
|
+
"typedoc": "^0.28.19",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"typescript-eslint": "^8.62.1",
|
|
66
|
+
"vitest": "^4.1.9"
|
|
67
|
+
},
|
|
68
|
+
"keywords": [
|
|
69
|
+
"typescript",
|
|
70
|
+
"javascript",
|
|
71
|
+
"utility",
|
|
72
|
+
"utilities",
|
|
73
|
+
"utils",
|
|
74
|
+
"type-guard",
|
|
75
|
+
"type-guards",
|
|
76
|
+
"validation",
|
|
77
|
+
"validator",
|
|
78
|
+
"discriminator",
|
|
79
|
+
"number",
|
|
80
|
+
"string",
|
|
81
|
+
"random",
|
|
82
|
+
"seeded-random",
|
|
83
|
+
"prng",
|
|
84
|
+
"pseudorandom",
|
|
85
|
+
"deterministic",
|
|
86
|
+
"xoshiro",
|
|
87
|
+
"esm"
|
|
88
|
+
]
|
|
89
|
+
}
|