@aidc-toolkit/utility 0.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.
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./iteration.js";
2
+ export * from "./transformer.js";
3
+ export * from "./string.js";
4
+ export * from "./reg_exp.js";
5
+ export * from "./record.js";
6
+ export * from "./character_set.js";
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Iteration source type. The underlying source is an iterable or iterator or a callback to an iterable or iterator.
3
+ */
4
+ export type IterationSource<T> = Iterable<T> | Iterator<T> | (() => (Iterable<T> | Iterator<T>));
5
+
6
+ /**
7
+ * Iteration proxy class for applying a callback for mapping or filtering.
8
+ */
9
+ class IterationProxy<T, U, V> implements IterableIterator<V> {
10
+ /**
11
+ * Proxied iterable iterator.
12
+ */
13
+ private readonly _proxiedIterableIterator: IterableIterator<T>;
14
+
15
+ /**
16
+ * Callback for map or filter.
17
+ */
18
+ private readonly _callback: (element: T, index: number) => U;
19
+
20
+ /**
21
+ * If true, callback is a predicate for a filter.
22
+ */
23
+ private readonly _isPredicate: boolean;
24
+
25
+ /**
26
+ * Index into proxied iterable iterator.
27
+ */
28
+ private _index: number;
29
+
30
+ /**
31
+ * Constructor.
32
+ *
33
+ * @param proxiedIterableIterator
34
+ * Proxied iterable iterator.
35
+ *
36
+ * @param callback
37
+ * Callback for map or filter.
38
+ *
39
+ * @param isPredicate
40
+ * If true, callback is a predicate for a filter.
41
+ */
42
+ constructor(proxiedIterableIterator: IterableIterator<T>, callback: (element: T, index: number) => U, isPredicate: boolean) {
43
+ this._proxiedIterableIterator = proxiedIterableIterator;
44
+ this._callback = callback;
45
+ this._isPredicate = isPredicate;
46
+
47
+ this._index = 0;
48
+ }
49
+
50
+ /**
51
+ * {@link Iterable} interface implementation.
52
+ *
53
+ * @returns
54
+ * Iterable iterator.
55
+ */
56
+ [Symbol.iterator](): IterableIterator<V> {
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * {@link Iterator} interface implementation.
62
+ *
63
+ * @param args
64
+ * Arguments.
65
+ *
66
+ * @returns
67
+ * Next element or number or total number of elements if none.
68
+ */
69
+ next(...args: [] | [undefined]): IteratorResult<V, number> {
70
+ let done = false;
71
+ let value: V | undefined;
72
+
73
+ let callbackDone: boolean;
74
+
75
+ do {
76
+ const proxiedNext = this._proxiedIterableIterator.next(...args);
77
+
78
+ if (!(proxiedNext.done ?? false)) {
79
+ const proxiedValue = proxiedNext.value;
80
+ const callbackValue = this._callback(proxiedValue, this._index++);
81
+
82
+ if (!this._isPredicate) {
83
+ // Types U and V are known to be identical.
84
+ value = callbackValue as unknown as V;
85
+
86
+ callbackDone = true;
87
+ } else {
88
+ callbackDone = callbackValue as boolean;
89
+
90
+ if (callbackDone) {
91
+ // Types T and V are known to be identical.
92
+ value = proxiedValue as unknown as V;
93
+ }
94
+ }
95
+ } else {
96
+ done = true;
97
+ callbackDone = true;
98
+ }
99
+ } while (!callbackDone);
100
+
101
+ return done ?
102
+ {
103
+ done: true,
104
+ value: this._index
105
+ } :
106
+ {
107
+ done: false,
108
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
109
+ value: value!
110
+ };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Iteration helper. Adds array-like functionality through {@link forEach}, {@link map}, {@link filter}, and
116
+ * {@link reduce} methods. Likely to be refactored as {@link
117
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator#iterator_helpers | iterator
118
+ * helpers} are more widely deployed.
119
+ */
120
+ export class IterationHelper<T> implements IterableIterator<T> {
121
+ /**
122
+ * Iteration source.
123
+ */
124
+ private readonly _iterationSource: IterationSource<T>;
125
+
126
+ /**
127
+ * Iterable extracted from iteration source.
128
+ */
129
+ private _iterable?: Iterable<T>;
130
+
131
+ /**
132
+ * Iterator extracted from iteration source.
133
+ */
134
+ private _iterator?: Iterator<T>;
135
+
136
+ /**
137
+ * Constructor.
138
+ *
139
+ * @param iterationSource
140
+ * Iteration source.
141
+ */
142
+ private constructor(iterationSource: IterationSource<T>) {
143
+ this._iterationSource = iterationSource;
144
+ }
145
+
146
+ /**
147
+ * Get an iteration helper from an iteration source. If the iteration source is itself an iteration helper, it is
148
+ * returned verbatim, otherwise a new iteration helper is constructed.
149
+ *
150
+ * @param iterationSource
151
+ * Iteration source.
152
+ *
153
+ * @returns
154
+ * Iteration helper.
155
+ */
156
+ static from<T>(iterationSource: IterationSource<T>): IterationHelper<T> {
157
+ return iterationSource instanceof IterationHelper ? iterationSource as IterationHelper<T> : new IterationHelper(iterationSource);
158
+ }
159
+
160
+ /**
161
+ * Get the iteration source.
162
+ */
163
+ get iterationSource(): IterationSource<T> {
164
+ return this._iterationSource;
165
+ }
166
+
167
+ /**
168
+ * Get the iteration source as an iterable.
169
+ *
170
+ * @returns
171
+ * Iterable.
172
+ */
173
+ asIterable(): Iterable<T> {
174
+ if (this._iterable === undefined) {
175
+ const resolvedIterationSource = typeof this.iterationSource === "function" ? this.iterationSource() : this.iterationSource;
176
+
177
+ this._iterable = Symbol.iterator in resolvedIterationSource ?
178
+ resolvedIterationSource :
179
+ {
180
+ [Symbol.iterator](): Iterator<T> {
181
+ return resolvedIterationSource;
182
+ }
183
+ };
184
+ }
185
+
186
+ return this._iterable;
187
+ }
188
+
189
+ /**
190
+ * Get the iteration source as an array.
191
+ *
192
+ * @returns
193
+ * Array.
194
+ */
195
+ asArray(): readonly T[] {
196
+ const iterable = this.asIterable();
197
+
198
+ // Return iterable as array.
199
+ return Array.isArray(iterable) ? iterable as readonly T[] : Array.from(iterable);
200
+ }
201
+
202
+ /**
203
+ * Get the iteration source as an iterator.
204
+ *
205
+ * @returns
206
+ * Iterator.
207
+ */
208
+ asIterator(): Iterator<T> {
209
+ if (this._iterator === undefined) {
210
+ this._iterator = this.asIterable()[Symbol.iterator]();
211
+ }
212
+
213
+ return this._iterator;
214
+ }
215
+
216
+ /**
217
+ * Get the iteration source as a callback.
218
+ *
219
+ * @returns
220
+ * Callback.
221
+ */
222
+ asCallback(): () => IterationSource<T> {
223
+ return typeof this._iterationSource === "function" ? this._iterationSource : () => this._iterationSource;
224
+ }
225
+
226
+ /**
227
+ * {@link Iterable} interface implementation.
228
+ *
229
+ * @returns
230
+ * Iterable iterator.
231
+ */
232
+ [Symbol.iterator](): IterableIterator<T> {
233
+ return this;
234
+ }
235
+
236
+ /**
237
+ * {@link Iterator} interface implementation.
238
+ *
239
+ * @param args
240
+ * Arguments.
241
+ *
242
+ * @returns
243
+ * Next element.
244
+ */
245
+ next(...args: [] | [undefined]): IteratorResult<T, unknown> {
246
+ return this.asIterator().next(...args);
247
+ }
248
+
249
+ /**
250
+ * Perform an action for each element in the iteration helper.
251
+ *
252
+ * @param callback
253
+ * Callback that processes the element and its index in the iteration sequence.
254
+ */
255
+ forEach(callback: (element: T, index: number) => void): void {
256
+ let index = 0;
257
+
258
+ for (const element of this) {
259
+ callback(element, index++);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Map the iteration helper to another iteration helper by performing an action for each element in the iteration
265
+ * helper along the way.
266
+ *
267
+ * @param callback
268
+ * Callback that processes the element and its index in the iteration sequence.
269
+ *
270
+ * @returns
271
+ * Iterable iterator over callback results.
272
+ */
273
+ map<U>(callback: (element: T, index: number) => U): IterableIterator<U> {
274
+ return new IterationProxy(this, callback, false);
275
+ }
276
+
277
+ /**
278
+ * Filter the iteration helper based on the condition specified in a predicate. Each call to `.next()` will iterate
279
+ * as far as necessary until it reaches an element that satisfies the predicate. Care should be taken when working
280
+ * with large iterators and infrequently truthy predicates.
281
+ *
282
+ * @param predicate
283
+ * Predicate that processes the element and its index in the iteration sequence.
284
+ *
285
+ * @returns
286
+ * Iterator iterable over elements that satisfy the predicate.
287
+ */
288
+ filter(predicate: (element: T, index: number) => boolean): IterableIterator<T> {
289
+ return new IterationProxy(this, predicate, true);
290
+ }
291
+
292
+ /**
293
+ * Reduce the iterator to a single value by applying a callback.
294
+ *
295
+ * @param callback
296
+ * Callback that processes the previous return value of the callback, the current value of the iterator, and the
297
+ * current index. The initial value is considered to be the first element in the iteration helper.
298
+ *
299
+ * @returns
300
+ * Reduced value.
301
+ */
302
+ reduce(callback: (previousValue: T, currentValue: T, currentIndex: number) => T): T;
303
+
304
+ /**
305
+ * Reduce the iterator to a single value by applying a callback.
306
+ *
307
+ * @param callback
308
+ * Callback that processes the previous return value of the callback, the current value of the iterator, and the
309
+ * current index.
310
+ *
311
+ * @param initialValue
312
+ * Initial value, passed as the first previous return value of the callback.
313
+ *
314
+ * @returns
315
+ * Reduced value.
316
+ */
317
+ reduce(callback: (previousValue: T, currentValue: T, currentIndex: number) => T, initialValue: T): T;
318
+
319
+ reduce<U>(callback: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue?: U): U {
320
+ let index = 0;
321
+ let result = initialValue;
322
+
323
+ for (const value of this) {
324
+ if (index === 0 && initialValue === undefined) {
325
+ result = value as unknown as U;
326
+ } else {
327
+ // Iteration has occurred at least once so result is of the expected type.
328
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
329
+ result = callback(result!, value, index);
330
+ }
331
+
332
+ index++;
333
+ }
334
+
335
+ if (index === 0 && initialValue === undefined) {
336
+ throw new Error("reduce() of empty iterator with no initial value");
337
+ }
338
+
339
+ // Iteration has occurred at least once so result is of the expected type.
340
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
341
+ return result!;
342
+ }
343
+ }
@@ -0,0 +1,30 @@
1
+ export const localeStrings = {
2
+ Transformer: {
3
+ domainMustBeGreaterThanZero: "Domain {{domain}} must be greater than 0",
4
+ tweakMustBeGreaterThanOrEqualToZero: "Tweak {{tweak}} must be greater than or equal to 0",
5
+ valueMustBeGreaterThanOrEqualToZero: "Value {{startValue}} must be greater than or equal to 0",
6
+ startValueMustBeGreaterThanOrEqualToZero: "Start value {{startValue}} must be greater than or equal to 0",
7
+ valueMustBeLessThan: "Value {{endValue}} must be less than {{domain}}",
8
+ endValueMustBeLessThan: "End value (start value + count - 1) {{endValue}} must be less than {{domain}}"
9
+ },
10
+ RegExpValidator: {
11
+ stringDoesNotMatchPattern: "String {{s}} does not match pattern"
12
+ },
13
+ CharacterSetValidator: {
14
+ stringMustNotBeAllNumeric: "String must not be all numeric",
15
+ lengthMustBeGreaterThanOrEqualTo: "Length {{length}} must be greater than or equal to {{minimumLength}}",
16
+ lengthMustBeLessThanOrEqualTo: "Length {{length}} must be less than or equal to {{maximumLength}}",
17
+ lengthMustBeEqualTo: "Length {{length}} must be equal to {{exactLength}}",
18
+ lengthOfComponentMustBeGreaterThanOrEqualTo: "Length {{length}} of {{component}} must be greater than or equal to {{minimumLength}}",
19
+ lengthOfComponentMustBeLessThanOrEqualTo: "Length {{length}} of {{component}} must be less than or equal to {{maximumLength}}",
20
+ lengthOfComponentMustBeEqualTo: "Length {{length}} of {{component}} must be equal to {{exactLength}}",
21
+ invalidCharacterAtPosition: "Invalid character '{{c}}' at position {{position}}",
22
+ invalidCharacterAtPositionOfComponent: "Invalid character '{{c}}' at position {{position}} of {{component}}",
23
+ exclusionNotSupported: "Exclusion value of {{exclusion}} is not supported",
24
+ invalidTweakWithAllNumericExclusion: "Tweak must not be used with all-numeric exclusion",
25
+ endSequenceValueMustBeLessThanOrEqualTo: "End sequence value (start sequence value + count - 1) must be less than {{domain}}"
26
+ },
27
+ RecordValidator: {
28
+ typeNameKeyNotFound: "{{typeName}} \"{{key}}\" not found"
29
+ }
30
+ } as const;
@@ -0,0 +1,8 @@
1
+ import { i18nAddResourceBundle, i18next } from "@aidc-toolkit/core";
2
+ import { localeStrings as enLocaleStrings } from "./en/locale_strings.js";
3
+
4
+ export const utilityNS = "aidct_utility";
5
+
6
+ i18nAddResourceBundle("en", utilityNS, enLocaleStrings);
7
+
8
+ export default i18next;
@@ -0,0 +1,10 @@
1
+ import type { localeStrings } from "./en/locale_strings.js";
2
+
3
+ declare module "i18next" {
4
+ interface CustomTypeOptions {
5
+ resources: {
6
+ // Extract the type from the English locale strings object.
7
+ aidct_utility: typeof localeStrings;
8
+ };
9
+ }
10
+ }
package/src/record.ts ADDED
@@ -0,0 +1,64 @@
1
+ import i18next, { utilityNS } from "./locale/i18n.js";
2
+ import type { StringValidator } from "./string.js";
3
+
4
+ /**
5
+ * Record validator. Validation is performed against a record with a string key type and throws an exception if the key
6
+ * is not found.
7
+ */
8
+ export class RecordValidator<T> implements StringValidator {
9
+ /**
10
+ * Type name for error message.
11
+ */
12
+ private readonly _typeName: string;
13
+
14
+ /**
15
+ * Record in which to look up keys.
16
+ */
17
+ private readonly _record: Readonly<Record<string, T>>;
18
+
19
+ /**
20
+ * Constructor.
21
+ *
22
+ * @param typeName
23
+ * Type name for error message.
24
+ *
25
+ * @param record
26
+ * Record in which to look up keys.
27
+ */
28
+ constructor(typeName: string, record: Readonly<Record<string, T>>) {
29
+ this._typeName = typeName;
30
+ this._record = record;
31
+ }
32
+
33
+ /**
34
+ * Get the type name.
35
+ */
36
+ get typeName(): string {
37
+ return this._typeName;
38
+ }
39
+
40
+ /**
41
+ * Get the record.
42
+ */
43
+ get record(): Readonly<Record<string, T>> {
44
+ return this._record;
45
+ }
46
+
47
+ /**
48
+ * Validate a key by looking it up in the record.
49
+ *
50
+ * @param key
51
+ * Record key.
52
+ *
53
+ * @throws RangeError
54
+ */
55
+ validate(key: string): void {
56
+ if (this.record[key] === undefined) {
57
+ throw new RangeError(i18next.t("RecordValidator.typeNameKeyNotFound", {
58
+ ns: utilityNS,
59
+ typeName: this.typeName,
60
+ key
61
+ }));
62
+ }
63
+ }
64
+ }
package/src/reg_exp.ts ADDED
@@ -0,0 +1,58 @@
1
+ import i18next, { utilityNS } from "./locale/i18n.js";
2
+ import type { StringValidator } from "./string.js";
3
+
4
+ /**
5
+ * Regular expression validator. The regular expression applies to the full string only if constructed as such. For
6
+ * example, <code>&#x2F;\d&#x2A;&#x2F;</code> (0 or more digits) matches every string, <code>&#x2F;\d+&#x2F;</code>
7
+ * (1 or more digits) matches strings with at least one digit, <code>&#x2F;^\d&#x2A;$&#x2F;</code> matches strings that
8
+ * are all digits or empty, and <code>&#x2F;^\d+$&#x2F;</code> matches strings that are all digits and not empty.
9
+ *
10
+ * Clients of this class are recommended to override the {@link createErrorMessage} method create a more suitable error
11
+ * message for their use case.
12
+ */
13
+ export class RegExpValidator implements StringValidator {
14
+ /**
15
+ * Regular expression.
16
+ */
17
+ private readonly _regExp: RegExp;
18
+
19
+ /**
20
+ * Constructor.
21
+ *
22
+ * @param regExp
23
+ * Regular expression. See {@link RegExpValidator | class documentation} for notes.
24
+ */
25
+ constructor(regExp: RegExp) {
26
+ this._regExp = regExp;
27
+ }
28
+
29
+ /**
30
+ * Get the regular expression.
31
+ */
32
+ get regExp(): RegExp {
33
+ return this._regExp;
34
+ }
35
+
36
+ /**
37
+ * Create an error message for a string. The generic error message is sufficient for many use cases but a more
38
+ * domain-specific error message, possibly including the pattern itself, is often required.
39
+ *
40
+ * @param s
41
+ * String.
42
+ *
43
+ * @returns
44
+ * Error message.
45
+ */
46
+ protected createErrorMessage(s: string): string {
47
+ return i18next.t("RegExpValidator.stringDoesNotMatchPattern", {
48
+ ns: utilityNS,
49
+ s
50
+ });
51
+ }
52
+
53
+ validate(s: string): void {
54
+ if (!this._regExp.test(s)) {
55
+ throw new RangeError(this.createErrorMessage(s));
56
+ }
57
+ }
58
+ }
package/src/string.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * String validation interface. To ensure signature compatibility in implementing classes, string validation is
3
+ * controlled by validation interfaces specific to each validator type.
4
+ */
5
+ export interface StringValidation {
6
+ }
7
+
8
+ /**
9
+ * String validator interface.
10
+ */
11
+ export interface StringValidator {
12
+ /**
13
+ * Validate a string and throw an exception if validation fails.
14
+ *
15
+ * @param s
16
+ * String.
17
+ *
18
+ * @param validation
19
+ * String validation parameters.
20
+ */
21
+ validate: (s: string, validation?: StringValidation) => void;
22
+ }