ts-data-forge 1.0.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 +201 -0
- package/README.md +534 -0
- package/package.json +101 -0
- package/src/array/array-utils-creation.test.mts +443 -0
- package/src/array/array-utils-modification.test.mts +197 -0
- package/src/array/array-utils-overload-type-error.test.mts +149 -0
- package/src/array/array-utils-reducing-value.test.mts +425 -0
- package/src/array/array-utils-search.test.mts +169 -0
- package/src/array/array-utils-set-op.test.mts +335 -0
- package/src/array/array-utils-slice-clamped.test.mts +113 -0
- package/src/array/array-utils-slicing.test.mts +316 -0
- package/src/array/array-utils-transformation.test.mts +790 -0
- package/src/array/array-utils-validation.test.mts +492 -0
- package/src/array/array-utils.mts +4000 -0
- package/src/array/array.test.mts +146 -0
- package/src/array/index.mts +2 -0
- package/src/array/tuple-utils.mts +519 -0
- package/src/array/tuple-utils.test.mts +518 -0
- package/src/collections/imap-mapped.mts +801 -0
- package/src/collections/imap-mapped.test.mts +860 -0
- package/src/collections/imap.mts +651 -0
- package/src/collections/imap.test.mts +932 -0
- package/src/collections/index.mts +6 -0
- package/src/collections/iset-mapped.mts +889 -0
- package/src/collections/iset-mapped.test.mts +1187 -0
- package/src/collections/iset.mts +682 -0
- package/src/collections/iset.test.mts +1084 -0
- package/src/collections/queue.mts +390 -0
- package/src/collections/queue.test.mts +282 -0
- package/src/collections/stack.mts +423 -0
- package/src/collections/stack.test.mts +225 -0
- package/src/expect-type.mts +206 -0
- package/src/functional/index.mts +4 -0
- package/src/functional/match.mts +300 -0
- package/src/functional/match.test.mts +177 -0
- package/src/functional/optional.mts +733 -0
- package/src/functional/optional.test.mts +619 -0
- package/src/functional/pipe.mts +212 -0
- package/src/functional/pipe.test.mts +85 -0
- package/src/functional/result.mts +1134 -0
- package/src/functional/result.test.mts +777 -0
- package/src/globals.d.mts +38 -0
- package/src/guard/has-key.mts +119 -0
- package/src/guard/has-key.test.mts +219 -0
- package/src/guard/index.mts +7 -0
- package/src/guard/is-non-empty-string.mts +108 -0
- package/src/guard/is-non-empty-string.test.mts +91 -0
- package/src/guard/is-non-null-object.mts +106 -0
- package/src/guard/is-non-null-object.test.mts +90 -0
- package/src/guard/is-primitive.mts +165 -0
- package/src/guard/is-primitive.test.mts +102 -0
- package/src/guard/is-record.mts +153 -0
- package/src/guard/is-record.test.mts +112 -0
- package/src/guard/is-type.mts +450 -0
- package/src/guard/is-type.test.mts +496 -0
- package/src/guard/key-is-in.mts +163 -0
- package/src/guard/key-is-in.test.mts +19 -0
- package/src/index.mts +10 -0
- package/src/iterator/index.mts +1 -0
- package/src/iterator/range.mts +120 -0
- package/src/iterator/range.test.mts +33 -0
- package/src/json/index.mts +1 -0
- package/src/json/json.mts +711 -0
- package/src/json/json.test.mts +628 -0
- package/src/number/branded-types/finite-number.mts +354 -0
- package/src/number/branded-types/finite-number.test.mts +135 -0
- package/src/number/branded-types/index.mts +26 -0
- package/src/number/branded-types/int.mts +278 -0
- package/src/number/branded-types/int.test.mts +140 -0
- package/src/number/branded-types/int16.mts +192 -0
- package/src/number/branded-types/int16.test.mts +170 -0
- package/src/number/branded-types/int32.mts +193 -0
- package/src/number/branded-types/int32.test.mts +170 -0
- package/src/number/branded-types/non-negative-finite-number.mts +223 -0
- package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
- package/src/number/branded-types/non-negative-int16.mts +187 -0
- package/src/number/branded-types/non-negative-int16.test.mts +201 -0
- package/src/number/branded-types/non-negative-int32.mts +187 -0
- package/src/number/branded-types/non-negative-int32.test.mts +204 -0
- package/src/number/branded-types/non-zero-finite-number.mts +229 -0
- package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
- package/src/number/branded-types/non-zero-int.mts +167 -0
- package/src/number/branded-types/non-zero-int.test.mts +177 -0
- package/src/number/branded-types/non-zero-int16.mts +196 -0
- package/src/number/branded-types/non-zero-int16.test.mts +195 -0
- package/src/number/branded-types/non-zero-int32.mts +196 -0
- package/src/number/branded-types/non-zero-int32.test.mts +197 -0
- package/src/number/branded-types/non-zero-safe-int.mts +196 -0
- package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
- package/src/number/branded-types/non-zero-uint16.mts +189 -0
- package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
- package/src/number/branded-types/non-zero-uint32.mts +189 -0
- package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
- package/src/number/branded-types/positive-finite-number.mts +241 -0
- package/src/number/branded-types/positive-finite-number.test.mts +204 -0
- package/src/number/branded-types/positive-int.mts +304 -0
- package/src/number/branded-types/positive-int.test.mts +176 -0
- package/src/number/branded-types/positive-int16.mts +188 -0
- package/src/number/branded-types/positive-int16.test.mts +197 -0
- package/src/number/branded-types/positive-int32.mts +188 -0
- package/src/number/branded-types/positive-int32.test.mts +197 -0
- package/src/number/branded-types/positive-safe-int.mts +187 -0
- package/src/number/branded-types/positive-safe-int.test.mts +210 -0
- package/src/number/branded-types/positive-uint16.mts +188 -0
- package/src/number/branded-types/positive-uint16.test.mts +203 -0
- package/src/number/branded-types/positive-uint32.mts +188 -0
- package/src/number/branded-types/positive-uint32.test.mts +203 -0
- package/src/number/branded-types/safe-int.mts +291 -0
- package/src/number/branded-types/safe-int.test.mts +170 -0
- package/src/number/branded-types/safe-uint.mts +187 -0
- package/src/number/branded-types/safe-uint.test.mts +176 -0
- package/src/number/branded-types/uint.mts +179 -0
- package/src/number/branded-types/uint.test.mts +158 -0
- package/src/number/branded-types/uint16.mts +186 -0
- package/src/number/branded-types/uint16.test.mts +170 -0
- package/src/number/branded-types/uint32.mts +218 -0
- package/src/number/branded-types/uint32.test.mts +170 -0
- package/src/number/enum/index.mts +2 -0
- package/src/number/enum/int8.mts +344 -0
- package/src/number/enum/int8.test.mts +180 -0
- package/src/number/enum/uint8.mts +293 -0
- package/src/number/enum/uint8.test.mts +164 -0
- package/src/number/index.mts +4 -0
- package/src/number/num.mts +604 -0
- package/src/number/num.test.mts +242 -0
- package/src/number/refined-number-utils.mts +566 -0
- package/src/object/index.mts +1 -0
- package/src/object/object.mts +447 -0
- package/src/object/object.test.mts +124 -0
- package/src/others/cast-mutable.mts +113 -0
- package/src/others/cast-readonly.mts +192 -0
- package/src/others/cast-readonly.test.mts +89 -0
- package/src/others/if-then.mts +98 -0
- package/src/others/if-then.test.mts +75 -0
- package/src/others/index.mts +7 -0
- package/src/others/map-nullable.mts +172 -0
- package/src/others/map-nullable.test.mts +297 -0
- package/src/others/memoize-function.mts +196 -0
- package/src/others/memoize-function.test.mts +168 -0
- package/src/others/tuple.mts +160 -0
- package/src/others/tuple.test.mts +11 -0
- package/src/others/unknown-to-string.mts +215 -0
- package/src/others/unknown-to-string.test.mts +114 -0
|
@@ -0,0 +1,4000 @@
|
|
|
1
|
+
import { IMap } from '../collections/index.mjs';
|
|
2
|
+
import { expectType } from '../expect-type.mjs';
|
|
3
|
+
import { Optional, pipe, Result } from '../functional/index.mjs';
|
|
4
|
+
import { isString, isUndefined } from '../guard/index.mjs';
|
|
5
|
+
import { range as rangeIterator } from '../iterator/index.mjs';
|
|
6
|
+
import { asPositiveUint32, asUint32, Num, Uint32 } from '../number/index.mjs';
|
|
7
|
+
import { castMutable, tp, unknownToString } from '../others/index.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A comprehensive, immutable utility library for array manipulations in TypeScript.
|
|
11
|
+
* Provides a wide range of functions for array creation, validation, transformation,
|
|
12
|
+
* reduction, slicing, set operations, and more, with a focus on type safety and
|
|
13
|
+
* leveraging TypeScript's type inference capabilities.
|
|
14
|
+
* All functions operate on `readonly` arrays and return new `readonly` arrays,
|
|
15
|
+
* ensuring immutability.
|
|
16
|
+
*/
|
|
17
|
+
export namespace Arr {
|
|
18
|
+
/**
|
|
19
|
+
* Returns the size (length) of an array as a type-safe branded integer.
|
|
20
|
+
*
|
|
21
|
+
* This function provides the array length with enhanced type safety through branded types:
|
|
22
|
+
* - For arrays known to be non-empty at compile time: returns `PositiveNumber & SizeType.Arr`
|
|
23
|
+
* - For general arrays that may be empty: returns `SizeType.Arr` (branded Uint32)
|
|
24
|
+
*
|
|
25
|
+
* The returned value is always a non-negative integer that can be safely used for array indexing
|
|
26
|
+
* and size comparisons. The branded type prevents common integer overflow issues and provides
|
|
27
|
+
* better type checking than plain numbers.
|
|
28
|
+
*
|
|
29
|
+
* @template Ar The exact type of the input array, used for precise return type inference.
|
|
30
|
+
* @param array The array to measure. Can be any readonly array type.
|
|
31
|
+
* @returns The length of the array as a branded type:
|
|
32
|
+
* - `IntersectBrand<PositiveNumber, SizeType.Arr>` for known non-empty arrays
|
|
33
|
+
* - `SizeType.Arr` for general arrays (branded Uint32, may be 0)
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Known non-empty arrays get positive branded type
|
|
38
|
+
* const tuple = [1, 2, 3] as const;
|
|
39
|
+
* const tupleSize = Arr.size(tuple);
|
|
40
|
+
* // Type: IntersectBrand<PositiveNumber, SizeType.Arr>
|
|
41
|
+
* // Value: 3 (branded, guaranteed positive)
|
|
42
|
+
*
|
|
43
|
+
* const nonEmpty: NonEmptyArray<string> = ['a', 'b'] as NonEmptyArray<string>;
|
|
44
|
+
* const nonEmptySize = Arr.size(nonEmpty);
|
|
45
|
+
* // Type: IntersectBrand<PositiveNumber, SizeType.Arr>
|
|
46
|
+
* // Guaranteed to be > 0
|
|
47
|
+
*
|
|
48
|
+
* // General arrays may be empty, get regular branded type
|
|
49
|
+
* const generalArray: number[] = [1, 2, 3];
|
|
50
|
+
* const generalSize = Arr.size(generalArray);
|
|
51
|
+
* // Type: SizeType.Arr (branded Uint32)
|
|
52
|
+
* // May be 0 or positive
|
|
53
|
+
*
|
|
54
|
+
* // Empty arrays
|
|
55
|
+
* const emptyArray = [] as const;
|
|
56
|
+
* const emptySize = Arr.size(emptyArray);
|
|
57
|
+
* // Type: SizeType.Arr
|
|
58
|
+
* // Value: 0 (branded)
|
|
59
|
+
*
|
|
60
|
+
* // Runtime arrays with unknown content
|
|
61
|
+
* const dynamicArray = Array.from({ length: Math.random() * 10 }, (_, i) => i);
|
|
62
|
+
* const dynamicSize = Arr.size(dynamicArray);
|
|
63
|
+
* // Type: SizeType.Arr (may be 0)
|
|
64
|
+
*
|
|
65
|
+
* // Using size for safe operations
|
|
66
|
+
* const data = [10, 20, 30];
|
|
67
|
+
* const dataSize = Arr.size(data);
|
|
68
|
+
*
|
|
69
|
+
* // Safe for array creation
|
|
70
|
+
* const indices = Arr.seq(dataSize); // Creates [0, 1, 2]
|
|
71
|
+
* const zeros = Arr.zeros(dataSize); // Creates [0, 0, 0]
|
|
72
|
+
*
|
|
73
|
+
* // Safe for bounds checking
|
|
74
|
+
* const isValidIndex = (index: number) => index >= 0 && index < dataSize;
|
|
75
|
+
*
|
|
76
|
+
* // Comparison with other sizes
|
|
77
|
+
* const otherArray = ['a', 'b'];
|
|
78
|
+
* const sizeDiff = Uint32.sub(Arr.size(data), Arr.size(otherArray)); // 1
|
|
79
|
+
*
|
|
80
|
+
* // Functional composition
|
|
81
|
+
* const arrays = [
|
|
82
|
+
* [1, 2],
|
|
83
|
+
* [3, 4, 5],
|
|
84
|
+
* [],
|
|
85
|
+
* [6]
|
|
86
|
+
* ];
|
|
87
|
+
* const sizes = arrays.map(Arr.size); // [2, 3, 0, 1] (all branded)
|
|
88
|
+
* const totalElements = sizes.reduce(Uint32.add, 0); // 6
|
|
89
|
+
*
|
|
90
|
+
* // Type guards work with size
|
|
91
|
+
* if (Arr.size(data) > 0) {
|
|
92
|
+
* // TypeScript knows data is non-empty here
|
|
93
|
+
* const firstElement = data[0]; // Safe access
|
|
94
|
+
* }
|
|
95
|
+
*
|
|
96
|
+
* // Type inference examples
|
|
97
|
+
* expectType<typeof tupleSize, IntersectBrand<PositiveNumber, SizeType.Arr>>('=');
|
|
98
|
+
* expectType<typeof generalSize, SizeType.Arr>('=');
|
|
99
|
+
* expectType<typeof emptySize, SizeType.Arr>('=');
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @see {@link length} - Alias for this function
|
|
103
|
+
* @see {@link isEmpty} for checking if size is 0
|
|
104
|
+
* @see {@link isNonEmpty} for checking if size > 0
|
|
105
|
+
*/
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
107
|
+
export const size: SizeFnOverload = (<Ar extends readonly unknown[]>(
|
|
108
|
+
array: Ar,
|
|
109
|
+
): SizeType.Arr => asUint32(array.length)) as SizeFnOverload;
|
|
110
|
+
|
|
111
|
+
type SizeFnOverload = {
|
|
112
|
+
<Ar extends NonEmptyArray<unknown>>(
|
|
113
|
+
array: Ar,
|
|
114
|
+
): IntersectBrand<PositiveNumber, SizeType.Arr>;
|
|
115
|
+
|
|
116
|
+
<Ar extends readonly unknown[]>(array: Ar): SizeType.Arr;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const length = size;
|
|
120
|
+
|
|
121
|
+
// type guard
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Type guard that checks if a value is an array, excluding types that cannot be arrays.
|
|
125
|
+
* This function refines the type by filtering out non-array types from unions.
|
|
126
|
+
* @template E The input type that may or may not be an array.
|
|
127
|
+
* @param value The value to check.
|
|
128
|
+
* @returns `true` if the value is an array, `false` otherwise.
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* function processValue(value: string | number[] | null) {
|
|
132
|
+
* if (Arr.isArray(value)) {
|
|
133
|
+
* // value is now typed as number[]
|
|
134
|
+
* console.log(value.length);
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
*
|
|
138
|
+
* Arr.isArray([1, 2, 3]); // true
|
|
139
|
+
* Arr.isArray("hello"); // false
|
|
140
|
+
* Arr.isArray(null); // false
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export const isArray = <E,>(value: E): value is FilterArray<E> =>
|
|
144
|
+
Array.isArray(value);
|
|
145
|
+
|
|
146
|
+
type FilterArray<T> = T extends T
|
|
147
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
BoolOr<TypeEq<T, unknown>, TypeEq<T, any>> extends true
|
|
149
|
+
? Cast<readonly unknown[], T>
|
|
150
|
+
: T extends readonly unknown[]
|
|
151
|
+
? T
|
|
152
|
+
: never // Exclude non-array types
|
|
153
|
+
: never;
|
|
154
|
+
|
|
155
|
+
type Cast<A, B> = A extends B ? A : never;
|
|
156
|
+
|
|
157
|
+
// validation
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Type guard that checks if an array is empty (has no elements).
|
|
161
|
+
*
|
|
162
|
+
* This function serves as both a runtime check and a TypeScript type guard,
|
|
163
|
+
* narrowing the array type to `readonly []` when the check passes. It's useful
|
|
164
|
+
* for conditional logic and type-safe handling of potentially empty arrays.
|
|
165
|
+
*
|
|
166
|
+
* @template E The type of elements in the array.
|
|
167
|
+
* @param array The array to check for emptiness.
|
|
168
|
+
* @returns `true` if the array has length 0, `false` otherwise.
|
|
169
|
+
* When `true`, TypeScript narrows the type to `readonly []`.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // Basic emptiness checking
|
|
174
|
+
* const emptyArray: number[] = [];
|
|
175
|
+
* const nonEmptyArray = [1, 2, 3];
|
|
176
|
+
*
|
|
177
|
+
* console.log(Arr.isEmpty(emptyArray)); // true
|
|
178
|
+
* console.log(Arr.isEmpty(nonEmptyArray)); // false
|
|
179
|
+
*
|
|
180
|
+
* // Type guard behavior
|
|
181
|
+
* function processArray(arr: readonly number[]) {
|
|
182
|
+
* if (Arr.isEmpty(arr)) {
|
|
183
|
+
* // arr is now typed as readonly []
|
|
184
|
+
* console.log('Array is empty');
|
|
185
|
+
* return 0;
|
|
186
|
+
* } else {
|
|
187
|
+
* // arr is now typed as NonEmptyArray<number>
|
|
188
|
+
* return arr[0]; // Safe access - TypeScript knows it's non-empty
|
|
189
|
+
* }
|
|
190
|
+
* }
|
|
191
|
+
*
|
|
192
|
+
* // Conditional processing
|
|
193
|
+
* const data = [10, 20, 30];
|
|
194
|
+
* if (!Arr.isEmpty(data)) {
|
|
195
|
+
* // Safe to access elements
|
|
196
|
+
* const firstElement = data[0]; // No undefined risk
|
|
197
|
+
* const lastElement = data[data.length - 1];
|
|
198
|
+
* }
|
|
199
|
+
*
|
|
200
|
+
* // Filtering empty arrays
|
|
201
|
+
* const arrayList: readonly number[][] = [[1, 2], [], [3], []];
|
|
202
|
+
* const nonEmptyArrays = arrayList.filter(arr => !Arr.isEmpty(arr));
|
|
203
|
+
* // nonEmptyArrays: [[1, 2], [3]]
|
|
204
|
+
*
|
|
205
|
+
* // Early returns
|
|
206
|
+
* function sumArray(numbers: readonly number[]): number {
|
|
207
|
+
* if (Arr.isEmpty(numbers)) {
|
|
208
|
+
* return 0; // Handle empty case early
|
|
209
|
+
* }
|
|
210
|
+
* return numbers.reduce((sum, n) => sum + n, 0);
|
|
211
|
+
* }
|
|
212
|
+
*
|
|
213
|
+
* // Type inference examples
|
|
214
|
+
* const testEmpty = [] as const;
|
|
215
|
+
* const testNonEmpty = [1, 2] as const;
|
|
216
|
+
*
|
|
217
|
+
* expectType<Parameters<typeof Arr.isEmpty>[0], readonly unknown[]>('=');
|
|
218
|
+
* expectType<ReturnType<typeof Arr.isEmpty>, boolean>('=');
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @see {@link isNonEmpty} for the opposite check (non-empty arrays)
|
|
222
|
+
* @see {@link size} for getting the exact length
|
|
223
|
+
* @see {@link isArrayOfLength} for checking specific lengths
|
|
224
|
+
*/
|
|
225
|
+
export const isEmpty = <E,>(array: readonly E[]): array is readonly [] =>
|
|
226
|
+
array.length === 0;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Type guard that checks if an array is non-empty (has at least one element).
|
|
230
|
+
*
|
|
231
|
+
* This function serves as both a runtime check and a TypeScript type guard,
|
|
232
|
+
* narrowing the array type to `NonEmptyArray<E>` when the check passes. This enables
|
|
233
|
+
* safe access to array elements without undefined checks, as TypeScript knows the array
|
|
234
|
+
* has at least one element.
|
|
235
|
+
*
|
|
236
|
+
* @template E The type of elements in the array.
|
|
237
|
+
* @param array The array to check for non-emptiness.
|
|
238
|
+
* @returns `true` if the array has length > 0, `false` otherwise.
|
|
239
|
+
* When `true`, TypeScript narrows the type to `NonEmptyArray<E>`.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* // Basic non-emptiness checking
|
|
244
|
+
* const emptyArray: number[] = [];
|
|
245
|
+
* const nonEmptyArray = [1, 2, 3];
|
|
246
|
+
*
|
|
247
|
+
* console.log(Arr.isNonEmpty(emptyArray)); // false
|
|
248
|
+
* console.log(Arr.isNonEmpty(nonEmptyArray)); // true
|
|
249
|
+
*
|
|
250
|
+
* // Type guard behavior enables safe element access
|
|
251
|
+
* function getFirstElement(arr: readonly number[]): number | undefined {
|
|
252
|
+
* if (Arr.isNonEmpty(arr)) {
|
|
253
|
+
* // arr is now typed as NonEmptyArray<number>
|
|
254
|
+
* return arr[0]; // Safe - no undefined, TypeScript knows this exists
|
|
255
|
+
* }
|
|
256
|
+
* return undefined;
|
|
257
|
+
* }
|
|
258
|
+
*
|
|
259
|
+
* // Safe operations on non-empty arrays
|
|
260
|
+
* function processData(data: readonly string[]) {
|
|
261
|
+
* if (Arr.isNonEmpty(data)) {
|
|
262
|
+
* // All of these are now safe without undefined checks
|
|
263
|
+
* const first = data[0];
|
|
264
|
+
* const last = data[data.length - 1];
|
|
265
|
+
* const middle = data[Math.floor(data.length / 2)];
|
|
266
|
+
*
|
|
267
|
+
* // Can safely use non-empty array methods
|
|
268
|
+
* const joined = data.join(', ');
|
|
269
|
+
* const reduced = data.reduce((acc, item) => acc + item.length, 0);
|
|
270
|
+
* }
|
|
271
|
+
* }
|
|
272
|
+
*
|
|
273
|
+
* // Filtering and working with arrays
|
|
274
|
+
* const possiblyEmptyArrays: readonly number[][] = [
|
|
275
|
+
* [1, 2, 3],
|
|
276
|
+
* [],
|
|
277
|
+
* [4, 5],
|
|
278
|
+
* []
|
|
279
|
+
* ];
|
|
280
|
+
*
|
|
281
|
+
* // Get only non-empty arrays with proper typing
|
|
282
|
+
* const definitelyNonEmpty = possiblyEmptyArrays.filter(Arr.isNonEmpty);
|
|
283
|
+
* // Type: NonEmptyArray<number>[]
|
|
284
|
+
*
|
|
285
|
+
* // Now safe to access elements
|
|
286
|
+
* const firstElements = definitelyNonEmpty.map(arr => arr[0]); // [1, 4]
|
|
287
|
+
*
|
|
288
|
+
* // Early validation
|
|
289
|
+
* function calculateAverage(numbers: readonly number[]): number {
|
|
290
|
+
* if (!Arr.isNonEmpty(numbers)) {
|
|
291
|
+
* throw new Error('Cannot calculate average of empty array');
|
|
292
|
+
* }
|
|
293
|
+
*
|
|
294
|
+
* // numbers is now NonEmptyArray<number>
|
|
295
|
+
* return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
|
|
296
|
+
* }
|
|
297
|
+
*
|
|
298
|
+
* // Functional composition
|
|
299
|
+
* const arrayGroups = [
|
|
300
|
+
* [1, 2],
|
|
301
|
+
* [],
|
|
302
|
+
* [3, 4, 5],
|
|
303
|
+
* []
|
|
304
|
+
* ];
|
|
305
|
+
*
|
|
306
|
+
* const nonEmptyGroups = arrayGroups
|
|
307
|
+
* .filter(Arr.isNonEmpty) // Filter to NonEmptyArray<number>[]
|
|
308
|
+
* .map(group => group[0]); // Safe access to first element: [1, 3]
|
|
309
|
+
*
|
|
310
|
+
* // Combined with other array operations
|
|
311
|
+
* function processArraySafely<T>(
|
|
312
|
+
* arr: readonly T[],
|
|
313
|
+
* processor: (item: T) => string
|
|
314
|
+
* ): string {
|
|
315
|
+
* if (Arr.isNonEmpty(arr)) {
|
|
316
|
+
* return arr.map(processor).join(' -> ');
|
|
317
|
+
* }
|
|
318
|
+
* return 'No items to process';
|
|
319
|
+
* }
|
|
320
|
+
*
|
|
321
|
+
* // Type inference examples
|
|
322
|
+
* const testArray = [1, 2, 3];
|
|
323
|
+
* const isNonEmptyResult = Arr.isNonEmpty(testArray);
|
|
324
|
+
*
|
|
325
|
+
* expectType<typeof isNonEmptyResult, boolean>('=');
|
|
326
|
+
* expectType<Parameters<typeof Arr.isNonEmpty>[0], readonly unknown[]>('=');
|
|
327
|
+
*
|
|
328
|
+
* // Type narrowing in conditional
|
|
329
|
+
* if (Arr.isNonEmpty(testArray)) {
|
|
330
|
+
* expectType<typeof testArray, NonEmptyArray<number>>('=');
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @see {@link isEmpty} for the opposite check (empty arrays)
|
|
335
|
+
* @see {@link size} for getting the exact length
|
|
336
|
+
* @see {@link head} for safely getting the first element
|
|
337
|
+
* @see {@link last} for safely getting the last element
|
|
338
|
+
*/
|
|
339
|
+
export const isNonEmpty = <E,>(
|
|
340
|
+
array: readonly E[],
|
|
341
|
+
): array is NonEmptyArray<E> => array.length > 0;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Checks if an array has a specific length.
|
|
345
|
+
* @template E The type of elements in the array.
|
|
346
|
+
* @template N The expected length of the array (must be a number type).
|
|
347
|
+
* @param array The array to check.
|
|
348
|
+
* @param len The expected length.
|
|
349
|
+
* @returns `true` if the array has the specified length, `false` otherwise.
|
|
350
|
+
* @example
|
|
351
|
+
* ```ts
|
|
352
|
+
* const arr: readonly number[] = [1, 2, 3];
|
|
353
|
+
* if (Arr.isArrayOfLength(arr, 3)) {
|
|
354
|
+
* // arr is now typed as readonly [number, number, number]
|
|
355
|
+
* }
|
|
356
|
+
* Arr.isArrayOfLength([1, 2], 3); // false
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
export const isArrayOfLength = <E, N extends SizeType.ArgArrNonNegative>(
|
|
360
|
+
array: readonly E[],
|
|
361
|
+
len: N,
|
|
362
|
+
): array is ArrayOfLength<N, E> => array.length === len;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Checks if an array has at least a specific length.
|
|
366
|
+
* @template E The type of elements in the array.
|
|
367
|
+
* @template N The minimum expected length of the array (must be a number type).
|
|
368
|
+
* @param array The array to check.
|
|
369
|
+
* @param len The minimum expected length.
|
|
370
|
+
* @returns `true` if the array has at least the specified length, `false` otherwise.
|
|
371
|
+
* @example
|
|
372
|
+
* ```ts
|
|
373
|
+
* const arr: readonly number[] = [1, 2, 3];
|
|
374
|
+
* if (Arr.isArrayAtLeastLength(arr, 2)) {
|
|
375
|
+
* // arr is now typed as readonly [number, number, ...number[]]
|
|
376
|
+
* }
|
|
377
|
+
* Arr.isArrayAtLeastLength([1], 2); // false
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
export const isArrayAtLeastLength = <E, N extends SizeType.ArgArrNonNegative>(
|
|
381
|
+
array: readonly E[],
|
|
382
|
+
len: N,
|
|
383
|
+
): array is ArrayAtLeastLen<N, E> => array.length >= len;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Checks if an index is within the valid range of an array (i.e., `0 <= index < array.length`).
|
|
387
|
+
* @template E The type of elements in the array.
|
|
388
|
+
* @param array The input array.
|
|
389
|
+
* @param index The index to check.
|
|
390
|
+
* @returns `true` if the index is within the array bounds, `false` otherwise.
|
|
391
|
+
* @example
|
|
392
|
+
* ```ts
|
|
393
|
+
* Arr.indexIsInRange([10, 20], 0); // true
|
|
394
|
+
* Arr.indexIsInRange([10, 20], 1); // true
|
|
395
|
+
* Arr.indexIsInRange([10, 20], 2); // false
|
|
396
|
+
* Arr.indexIsInRange([10, 20], -1); // false
|
|
397
|
+
* Arr.indexIsInRange([], 0); // false
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
export const indexIsInRange = <E,>(
|
|
401
|
+
array: readonly E[],
|
|
402
|
+
index: SizeType.ArgArrNonNegative,
|
|
403
|
+
): boolean => Num.isInRange(0, array.length)(index);
|
|
404
|
+
|
|
405
|
+
// array creation
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates an array of zeros with the specified length.
|
|
409
|
+
*
|
|
410
|
+
* This function provides compile-time type safety with precise return types:
|
|
411
|
+
* - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple with exact length
|
|
412
|
+
* - When `len` is a positive runtime value, returns a `NonEmptyArray<0>`
|
|
413
|
+
* - Otherwise, returns a `readonly 0[]` that may be empty
|
|
414
|
+
*
|
|
415
|
+
* @template N The type of the length parameter. When a `SmallUint` literal is provided,
|
|
416
|
+
* the return type will be a tuple of exactly that length filled with zeros.
|
|
417
|
+
* @param len The length of the array to create. Must be a non-negative integer.
|
|
418
|
+
* @returns An immutable array of zeros. The exact return type depends on the input:
|
|
419
|
+
* - `ArrayOfLength<N, 0>` when `N` is a `SmallUint` literal
|
|
420
|
+
* - `NonEmptyArray<0>` when `len` is a positive runtime value
|
|
421
|
+
* - `readonly 0[]` for general non-negative values
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```typescript
|
|
425
|
+
* // Compile-time known lengths produce precise tuple types
|
|
426
|
+
* const exactLength = Arr.zeros(3); // readonly [0, 0, 0]
|
|
427
|
+
* const empty = Arr.zeros(0); // readonly []
|
|
428
|
+
*
|
|
429
|
+
* // Runtime positive values produce non-empty arrays
|
|
430
|
+
* const count = Math.floor(Math.random() * 5) + 1;
|
|
431
|
+
* const nonEmpty = Arr.zeros(count); // NonEmptyArray<0>
|
|
432
|
+
*
|
|
433
|
+
* // General runtime values may be empty
|
|
434
|
+
* const maybeEmpty = Arr.zeros(Math.floor(Math.random() * 5)); // readonly 0[]
|
|
435
|
+
*
|
|
436
|
+
* // Type inference examples
|
|
437
|
+
* expectType<typeof exactLength, readonly [0, 0, 0]>('=');
|
|
438
|
+
* expectType<typeof empty, readonly []>('=');
|
|
439
|
+
* expectType<typeof nonEmpty, NonEmptyArray<0>>('=');
|
|
440
|
+
* expectType<typeof maybeEmpty, readonly 0[]>('=');
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
444
|
+
export const zeros: ZerosFnOverload = ((
|
|
445
|
+
len: SizeType.ArgArrNonNegative,
|
|
446
|
+
): readonly 0[] => Array.from<0>({ length: len }).fill(0)) as ZerosFnOverload;
|
|
447
|
+
|
|
448
|
+
type ZerosFnOverload = {
|
|
449
|
+
/**
|
|
450
|
+
* Create array of zeros with compile-time length.
|
|
451
|
+
*/
|
|
452
|
+
<N extends SmallUint>(len: N): ArrayOfLength<N, 0>;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Create non-empty array of zeros.
|
|
456
|
+
*/
|
|
457
|
+
(len: SizeType.ArgArrPositive): NonEmptyArray<0>;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create array of zeros.
|
|
461
|
+
*/
|
|
462
|
+
(len: SizeType.ArgArrNonNegative): readonly 0[];
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Creates a sequence of consecutive integers from 0 to `len-1`.
|
|
467
|
+
*
|
|
468
|
+
* This function generates index sequences with precise compile-time typing:
|
|
469
|
+
* - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple of consecutive integers
|
|
470
|
+
* - When `len` is a positive runtime value, returns a `NonEmptyArray<SizeType.Arr>`
|
|
471
|
+
* - Otherwise, returns a `readonly SizeType.Arr[]` that may be empty
|
|
472
|
+
*
|
|
473
|
+
* @template N The type of the length parameter. When a `SmallUint` literal is provided,
|
|
474
|
+
* the return type will be a tuple containing the sequence [0, 1, 2, ..., N-1].
|
|
475
|
+
* @param len The length of the sequence to create. Must be a non-negative integer.
|
|
476
|
+
* @returns An immutable array containing the sequence [0, 1, 2, ..., len-1].
|
|
477
|
+
* The exact return type depends on the input:
|
|
478
|
+
* - `Seq<N>` (precise tuple) when `N` is a `SmallUint` literal
|
|
479
|
+
* - `NonEmptyArray<SizeType.Arr>` when `len` is a positive runtime value
|
|
480
|
+
* - `readonly SizeType.Arr[]` for general non-negative values
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```typescript
|
|
484
|
+
* // Compile-time known lengths produce precise tuple types
|
|
485
|
+
* const indices = Arr.seq(4); // readonly [0, 1, 2, 3]
|
|
486
|
+
* const empty = Arr.seq(0); // readonly []
|
|
487
|
+
* const single = Arr.seq(1); // readonly [0]
|
|
488
|
+
*
|
|
489
|
+
* // Runtime positive values produce non-empty arrays
|
|
490
|
+
* const count = Math.floor(Math.random() * 5) + 1;
|
|
491
|
+
* const nonEmpty = Arr.seq(count); // NonEmptyArray<SizeType.Arr>
|
|
492
|
+
*
|
|
493
|
+
* // General runtime values may be empty
|
|
494
|
+
* const maybeEmpty = Arr.seq(Math.floor(Math.random() * 5)); // readonly SizeType.Arr[]
|
|
495
|
+
*
|
|
496
|
+
* // Useful for generating array indices
|
|
497
|
+
* const data = ['a', 'b', 'c', 'd'];
|
|
498
|
+
* const indexSequence = Arr.seq(data.length); // [0, 1, 2, 3]
|
|
499
|
+
*
|
|
500
|
+
* // Type inference examples
|
|
501
|
+
* expectType<typeof indices, readonly [0, 1, 2, 3]>('=');
|
|
502
|
+
* expectType<typeof empty, readonly []>('=');
|
|
503
|
+
* expectType<typeof single, readonly [0]>('=');
|
|
504
|
+
* ```
|
|
505
|
+
*/
|
|
506
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
507
|
+
export const seq: SeqFnOverload = ((
|
|
508
|
+
len: SizeType.ArgArrNonNegative,
|
|
509
|
+
): readonly SizeType.Arr[] =>
|
|
510
|
+
Array.from({ length: len }, (_, i) => asUint32(i))) as SeqFnOverload;
|
|
511
|
+
|
|
512
|
+
type SeqFnOverload = {
|
|
513
|
+
<N extends SmallUint>(len: N): Seq<N>;
|
|
514
|
+
|
|
515
|
+
(len: SizeType.ArgArrPositive): NonEmptyArray<SizeType.Arr>;
|
|
516
|
+
|
|
517
|
+
(len: SizeType.ArgArrNonNegative): readonly SizeType.Arr[];
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Creates a new array of the specified length, with each position filled with the provided initial value.
|
|
522
|
+
*
|
|
523
|
+
* This function provides compile-time type safety with precise return types and performs shallow copying
|
|
524
|
+
* of the initial value (the same reference is used for all positions):
|
|
525
|
+
* - When `len` is a compile-time known `SmallUint` (0-100), returns a tuple of exactly that length
|
|
526
|
+
* - When `len` is a positive runtime value, returns a `NonEmptyArray<V>`
|
|
527
|
+
* - Otherwise, returns a `readonly V[]` that may be empty
|
|
528
|
+
*
|
|
529
|
+
* @template V The type of the initial value. The `const` constraint preserves literal types.
|
|
530
|
+
* @template N The type of the length parameter when it's a `SmallUint` literal.
|
|
531
|
+
* @param len The length of the array to create. Must be a non-negative integer.
|
|
532
|
+
* @param init The value to fill each position with. The same reference is used for all positions.
|
|
533
|
+
* @returns An immutable array filled with the initial value. The exact return type depends on the length:
|
|
534
|
+
* - `ArrayOfLength<N, V>` when `len` is a `SmallUint` literal
|
|
535
|
+
* - `NonEmptyArray<V>` when `len` is a positive runtime value
|
|
536
|
+
* - `readonly V[]` for general non-negative values
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* // Compile-time known lengths produce precise tuple types
|
|
541
|
+
* const strings = Arr.create(3, 'hello'); // readonly ['hello', 'hello', 'hello']
|
|
542
|
+
* const numbers = Arr.create(2, 42); // readonly [42, 42]
|
|
543
|
+
* const empty = Arr.create(0, 'unused'); // readonly []
|
|
544
|
+
*
|
|
545
|
+
* // Object references are shared (shallow copy behavior)
|
|
546
|
+
* const obj = { id: 1, name: 'test' };
|
|
547
|
+
* const objects = Arr.create(3, obj); // readonly [obj, obj, obj]
|
|
548
|
+
* objects[0] === objects[1]; // true - same reference
|
|
549
|
+
* objects[0].id = 999; // Mutates the shared object
|
|
550
|
+
* console.log(objects[1].id); // 999 - all positions affected
|
|
551
|
+
*
|
|
552
|
+
* // Runtime positive values produce non-empty arrays
|
|
553
|
+
* const count = Math.floor(Math.random() * 5) + 1;
|
|
554
|
+
* const nonEmpty = Arr.create(count, 'item'); // NonEmptyArray<string>
|
|
555
|
+
*
|
|
556
|
+
* // Literal type preservation with const assertion
|
|
557
|
+
* const literals = Arr.create(2, 'success' as const); // readonly ['success', 'success']
|
|
558
|
+
*
|
|
559
|
+
* // Type inference examples
|
|
560
|
+
* expectType<typeof strings, readonly ['hello', 'hello', 'hello']>('=');
|
|
561
|
+
* expectType<typeof numbers, readonly [42, 42]>('=');
|
|
562
|
+
* expectType<typeof empty, readonly []>('=');
|
|
563
|
+
* ```
|
|
564
|
+
*
|
|
565
|
+
* @see {@link zeros} for creating arrays filled with zeros
|
|
566
|
+
* @see {@link seq} for creating sequences of consecutive integers
|
|
567
|
+
*/
|
|
568
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
569
|
+
export const create: CreateFnOverload = (<const V,>(
|
|
570
|
+
len: SizeType.ArgArrNonNegative,
|
|
571
|
+
init: V,
|
|
572
|
+
): readonly V[] =>
|
|
573
|
+
Array.from({ length: Math.max(0, len) }, () => init)) as CreateFnOverload;
|
|
574
|
+
|
|
575
|
+
type CreateFnOverload = {
|
|
576
|
+
<const V, N extends SmallUint>(len: N, init: V): ArrayOfLength<N, V>;
|
|
577
|
+
|
|
578
|
+
<const V>(len: SizeType.ArgArrPositive, init: V): NonEmptyArray<V>;
|
|
579
|
+
|
|
580
|
+
<const V>(len: SizeType.ArgArrNonNegative, init: V): readonly V[];
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
export const newArray = create;
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Creates a shallow copy of an array, preserving the exact type signature.
|
|
587
|
+
*
|
|
588
|
+
* This function creates a new array with the same elements as the input, but with a new array reference.
|
|
589
|
+
* Object references within the array are preserved (shallow copy), and the readonly/mutable status
|
|
590
|
+
* of the array type is maintained.
|
|
591
|
+
*
|
|
592
|
+
* @template Ar The exact type of the input array, preserving tuple types, readonly status, and element types.
|
|
593
|
+
* @param array The array to copy. Can be any array type: mutable, readonly, tuple, or general array.
|
|
594
|
+
* @returns A new array that is a shallow copy of the input. The return type exactly matches the input type,
|
|
595
|
+
* preserving readonly status, tuple structure, and element types.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* // Mutable arrays remain mutable
|
|
600
|
+
* const mutableOriginal = [1, 2, 3];
|
|
601
|
+
* const mutableCopy = Arr.copy(mutableOriginal); // number[]
|
|
602
|
+
* mutableCopy[0] = 999; // OK - still mutable
|
|
603
|
+
* mutableOriginal[0]; // 1 - original unchanged
|
|
604
|
+
*
|
|
605
|
+
* // Readonly arrays remain readonly
|
|
606
|
+
* const readonlyOriginal = [1, 2, 3] as const;
|
|
607
|
+
* const readonlyCopy = Arr.copy(readonlyOriginal); // readonly [1, 2, 3]
|
|
608
|
+
* // readonlyCopy[0] = 999; // Error - readonly array
|
|
609
|
+
*
|
|
610
|
+
* // Tuple types are preserved
|
|
611
|
+
* const tupleOriginal: [string, number, boolean] = ['hello', 42, true];
|
|
612
|
+
* const tupleCopy = Arr.copy(tupleOriginal); // [string, number, boolean]
|
|
613
|
+
*
|
|
614
|
+
* // Shallow copy behavior with objects
|
|
615
|
+
* const objectArray = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
|
|
616
|
+
* const objectCopy = Arr.copy(objectArray);
|
|
617
|
+
* objectCopy[0].name = 'Charlie'; // Mutates the shared object reference
|
|
618
|
+
* console.log(objectArray[0].name); // 'Charlie' - original affected
|
|
619
|
+
* objectCopy.push({ id: 3, name: 'Dave' }); // Array structure changes don't affect original
|
|
620
|
+
* console.log(objectArray.length); // 2 - original array length unchanged
|
|
621
|
+
*
|
|
622
|
+
* // Empty arrays
|
|
623
|
+
* const emptyArray: number[] = [];
|
|
624
|
+
* const emptyCopy = Arr.copy(emptyArray); // number[]
|
|
625
|
+
* const emptyTuple = [] as const;
|
|
626
|
+
* const emptyTupleCopy = Arr.copy(emptyTuple); // readonly []
|
|
627
|
+
*
|
|
628
|
+
* // Type inference examples
|
|
629
|
+
* expectType<typeof mutableCopy, number[]>('=');
|
|
630
|
+
* expectType<typeof readonlyCopy, readonly [1, 2, 3]>('=');
|
|
631
|
+
* expectType<typeof tupleCopy, [string, number, boolean]>('=');
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
634
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice | Array.prototype.slice}
|
|
635
|
+
* The underlying implementation uses `slice()` for efficient shallow copying
|
|
636
|
+
*/
|
|
637
|
+
export const copy = <Ar extends readonly unknown[]>(array: Ar): Ar =>
|
|
638
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
639
|
+
array.slice() as unknown as Ar;
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @internal
|
|
643
|
+
* Helper type for `range` function to represent a sequence of numbers up to N-1.
|
|
644
|
+
* `LEQ[N]` would be `0 | 1 | ... | N-1`.
|
|
645
|
+
*/
|
|
646
|
+
type LT = Readonly<{
|
|
647
|
+
[N in SmallUint]: Index<N>;
|
|
648
|
+
}>;
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* @internal
|
|
652
|
+
* This type is used to avoid incorrect type calculation results for unions with `Seq`.
|
|
653
|
+
* It computes the type of an array generated by `Arr.range(S, E)`.
|
|
654
|
+
* If `S` or `E` is a union type, it falls back to a more general `readonly number[]` type
|
|
655
|
+
* to prevent overly complex or incorrect tuple/union types.
|
|
656
|
+
* Otherwise, it computes a precise tuple type like `readonly [S, S+1, ..., E-1]`.
|
|
657
|
+
* @template S The start of the range (inclusive), constrained to `SmallUint`.
|
|
658
|
+
* @template E The end of the range (exclusive), constrained to `SmallUint`.
|
|
659
|
+
*/
|
|
660
|
+
type RangeList<S extends SmallUint, E extends SmallUint> =
|
|
661
|
+
BoolOr<IsUnion<S>, IsUnion<E>> extends true
|
|
662
|
+
? readonly RelaxedExclude<LT[E], LT[Min<S>]>[] // Avoid incorrect type calculation for unions with Seq
|
|
663
|
+
: List.Skip<S, Seq<E>>;
|
|
664
|
+
|
|
665
|
+
expectType<RangeList<1, 5>, readonly [1, 2, 3, 4]>('=');
|
|
666
|
+
expectType<RangeList<1, 2>, readonly [1]>('=');
|
|
667
|
+
expectType<RangeList<1, 1>, readonly []>('=');
|
|
668
|
+
expectType<RangeList<1, 1 | 3>, readonly (1 | 2)[]>('=');
|
|
669
|
+
expectType<RangeList<1 | 3, 3 | 5>, readonly (1 | 2 | 3 | 4)[]>('=');
|
|
670
|
+
expectType<
|
|
671
|
+
RangeList<1 | 2 | 3, 5 | 6 | 7>,
|
|
672
|
+
readonly (1 | 2 | 3 | 4 | 5 | 6)[]
|
|
673
|
+
>('=');
|
|
674
|
+
expectType<RangeList<5, 1>, readonly []>('=');
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Creates an array of numbers within a specified range with optional step increment.
|
|
678
|
+
*
|
|
679
|
+
* This function generates arithmetic sequences with advanced compile-time type inference:
|
|
680
|
+
* - When `start` and `end` are {@link SmallUint} literals and `step` is 1 (or omitted), returns a precise tuple type
|
|
681
|
+
* - When parameters are runtime values, returns appropriate array types based on sign constraints
|
|
682
|
+
* - Empty arrays are returned for invalid ranges (e.g., start ≥ end with positive step)
|
|
683
|
+
* - Never throws exceptions - invalid parameters result in empty arrays
|
|
684
|
+
*
|
|
685
|
+
* **SmallUint Constraint:** The {@link SmallUint} constraint (0-255) enables precise tuple type inference
|
|
686
|
+
* for compile-time known ranges. This allows TypeScript to compute exact tuple types like `readonly [1, 2, 3, 4]`
|
|
687
|
+
* instead of generic `readonly number[]`.
|
|
688
|
+
*
|
|
689
|
+
* **Type Inference Behavior:**
|
|
690
|
+
* - Literal {@link SmallUint} values with step=1 → precise tuple type (`RangeList<S, E>`)
|
|
691
|
+
* - Non-negative parameters → `readonly SafeUint[]`
|
|
692
|
+
* - Mixed signs or negative parameters → `readonly SafeInt[]`
|
|
693
|
+
* - Runtime values → lose precise typing but maintain safety
|
|
694
|
+
*
|
|
695
|
+
* @template S The type of the start value. When a {@link SmallUint} literal (0-255), enables precise tuple typing.
|
|
696
|
+
* @template E The type of the end value. When a {@link SmallUint} literal (0-255), enables precise tuple typing.
|
|
697
|
+
* @param start The start of the range (inclusive). Must be a safe integer. Supports:
|
|
698
|
+
* - **Literal {@link SmallUint}:** Enables precise tuple types (0-255)
|
|
699
|
+
* - **Runtime {@link SafeInt}:** Fallback to general array types
|
|
700
|
+
* - **Negative values:** Supported for countdown sequences
|
|
701
|
+
* @param end The end of the range (exclusive). Must be a safe integer. Supports:
|
|
702
|
+
* - **Literal {@link SmallUint}:** Enables precise tuple types (0-255)
|
|
703
|
+
* - **Runtime {@link SafeInt}:** Fallback to general array types
|
|
704
|
+
* - **Equal to start:** Results in empty array
|
|
705
|
+
* @param step The step increment (default: 1). Must be a non-zero safe integer.
|
|
706
|
+
* - **Positive step:** generates increasing sequence from start to end
|
|
707
|
+
* - **Negative step:** generates decreasing sequence from start to end
|
|
708
|
+
* - **Zero step:** Not allowed (branded type prevents this)
|
|
709
|
+
* @returns An immutable array containing the arithmetic sequence. Return type depends on parameters:
|
|
710
|
+
* - `RangeList<S, E>` (precise tuple like `readonly [1, 2, 3, 4]`) when `S` and `E` are {@link SmallUint} literals and step is 1
|
|
711
|
+
* - `readonly SafeUint[]` when all parameters are non-negative
|
|
712
|
+
* - `readonly SafeInt[]` for general integer ranges including negative values
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```typescript
|
|
716
|
+
* // Compile-time known ranges with step=1 produce precise tuple types
|
|
717
|
+
* const range1to4 = Arr.range(1, 5); // readonly [1, 2, 3, 4]
|
|
718
|
+
* const range0to2 = Arr.range(0, 3); // readonly [0, 1, 2]
|
|
719
|
+
* const emptyRange = Arr.range(5, 5); // readonly []
|
|
720
|
+
* const reverseEmpty = Arr.range(5, 1); // readonly [] (invalid with positive step)
|
|
721
|
+
*
|
|
722
|
+
* // SmallUint constraint examples (0-255 for precise typing)
|
|
723
|
+
* const small = Arr.range(0, 10); // readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
724
|
+
* const maxSmall = Arr.range(250, 255); // readonly [250, 251, 252, 253, 254]
|
|
725
|
+
* const beyondSmall = Arr.range(0, 300); // readonly SafeUint[] (loses precision)
|
|
726
|
+
*
|
|
727
|
+
* // Custom step increments
|
|
728
|
+
* const evens = Arr.range(0, 10, 2); // readonly SafeUint[] -> [0, 2, 4, 6, 8]
|
|
729
|
+
* const odds = Arr.range(1, 10, 2); // readonly SafeUint[] -> [1, 3, 5, 7, 9]
|
|
730
|
+
* const countdown = Arr.range(5, 0, -1); // readonly SafeInt[] -> [5, 4, 3, 2, 1]
|
|
731
|
+
* const bigStep = Arr.range(0, 20, 5); // readonly SafeUint[] -> [0, 5, 10, 15]
|
|
732
|
+
*
|
|
733
|
+
* // Edge cases that return empty arrays
|
|
734
|
+
* const singleElement = Arr.range(3, 4); // readonly [3]
|
|
735
|
+
* const invalidRange = Arr.range(10, 5, 2); // readonly [] (start > end with positive step)
|
|
736
|
+
* const invalidReverse = Arr.range(1, 10, -1); // readonly [] (start < end with negative step)
|
|
737
|
+
* const zeroRange = Arr.range(42, 42); // readonly [] (start equals end)
|
|
738
|
+
*
|
|
739
|
+
* // Runtime ranges lose precise typing but maintain safety
|
|
740
|
+
* const dynamicStart = Math.floor(Math.random() * 10) as SafeInt;
|
|
741
|
+
* const dynamicEnd = (dynamicStart + 5) as SafeInt;
|
|
742
|
+
* const dynamicRange = Arr.range(dynamicStart, dynamicEnd); // readonly SafeInt[]
|
|
743
|
+
*
|
|
744
|
+
* // Negative numbers and mixed signs
|
|
745
|
+
* const negativeRange = Arr.range(-5, 5); // readonly SafeInt[] -> [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
|
|
746
|
+
* const negativeCountdown = Arr.range(0, -5, -1); // readonly SafeInt[] -> [0, -1, -2, -3, -4]
|
|
747
|
+
*
|
|
748
|
+
* // Useful for generating index ranges and iteration
|
|
749
|
+
* const indices = Arr.range(0, 10); // readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
750
|
+
* const reversedIndices = Arr.range(9, -1, -1); // readonly SafeInt[] -> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
|
751
|
+
*
|
|
752
|
+
* // Functional programming patterns
|
|
753
|
+
* const squares = Arr.range(1, 6).map(x => x * x); // [1, 4, 9, 16, 25]
|
|
754
|
+
* const fibonacci = Arr.range(0, 10).reduce((acc, _, i) => {\n * if (i <= 1) return [...acc, i];\n * return [...acc, acc[i-1] + acc[i-2]];\n * }, [] as number[]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
|
755
|
+
*
|
|
756
|
+
* // Type inference examples showing precise vs general types
|
|
757
|
+
* expectType<typeof range1to4, readonly [1, 2, 3, 4]>('='); // Precise tuple
|
|
758
|
+
* expectType<typeof emptyRange, readonly []>('='); // Precise empty tuple
|
|
759
|
+
* expectType<typeof evens, readonly SafeUint[]>('='); // General positive array
|
|
760
|
+
* expectType<typeof countdown, readonly SafeInt[]>('='); // General integer array
|
|
761
|
+
* expectType<typeof negativeRange, readonly SafeInt[]>('='); // General integer array
|
|
762
|
+
* expectType<typeof small, readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>('='); // Precise tuple
|
|
763
|
+
* expectType<typeof beyondSmall, readonly SafeUint[]>('='); // General array (beyond SmallUint)
|
|
764
|
+
* ```
|
|
765
|
+
*
|
|
766
|
+
* @throws Never throws - invalid ranges simply return empty arrays
|
|
767
|
+
* @see {@link seq} for creating sequences starting from 0
|
|
768
|
+
* @see {@link SmallUint} for understanding the constraint that enables precise typing
|
|
769
|
+
* @see {@link SafeInt} and {@link SafeUint} for the safe integer types used
|
|
770
|
+
*/
|
|
771
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
772
|
+
export const range: RangeFnOverload = ((
|
|
773
|
+
start: SafeIntWithSmallInt,
|
|
774
|
+
end: SafeIntWithSmallInt,
|
|
775
|
+
step: NonZeroSafeIntWithSmallInt = 1,
|
|
776
|
+
): readonly SafeInt[] =>
|
|
777
|
+
Array.from(rangeIterator(start, end, step))) as RangeFnOverload;
|
|
778
|
+
|
|
779
|
+
type RangeFnOverload = {
|
|
780
|
+
<S extends SmallUint, E extends SmallUint>(
|
|
781
|
+
start: S,
|
|
782
|
+
end: E,
|
|
783
|
+
step?: 1,
|
|
784
|
+
): RangeList<S, E>;
|
|
785
|
+
|
|
786
|
+
(
|
|
787
|
+
start: SafeUintWithSmallInt,
|
|
788
|
+
end: SafeUintWithSmallInt,
|
|
789
|
+
step?: PositiveSafeIntWithSmallInt,
|
|
790
|
+
): readonly SafeUint[];
|
|
791
|
+
|
|
792
|
+
(
|
|
793
|
+
start: SafeIntWithSmallInt,
|
|
794
|
+
end: SafeIntWithSmallInt,
|
|
795
|
+
step?: NonZeroSafeIntWithSmallInt,
|
|
796
|
+
): readonly SafeInt[];
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// element access
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Safely retrieves an element at a given index from an array, returning an {@link Optional}.
|
|
803
|
+
*
|
|
804
|
+
* This function provides type-safe array access with support for negative indexing
|
|
805
|
+
* (e.g., -1 for the last element). Unlike direct array access which can return
|
|
806
|
+
* `undefined` for out-of-bounds indices, this function always returns a well-typed
|
|
807
|
+
* {@link Optional} that explicitly represents the possibility of absence.
|
|
808
|
+
*
|
|
809
|
+
* **Negative Indexing:** Negative indices count from the end of the array:
|
|
810
|
+
* - `-1` refers to the last element
|
|
811
|
+
* - `-2` refers to the second-to-last element, etc.
|
|
812
|
+
*
|
|
813
|
+
* **Curried Usage:** This function supports currying - when called with only an index, it returns
|
|
814
|
+
* a function that can be applied to arrays, making it ideal for use in pipe operations.
|
|
815
|
+
*
|
|
816
|
+
* **Optional Return Type:** The return type is always {@link Optional}<E> which provides:
|
|
817
|
+
* - Type-safe access without `undefined` in your business logic
|
|
818
|
+
* - Explicit handling of \"not found\" cases
|
|
819
|
+
* - Composable error handling with {@link Optional} utilities
|
|
820
|
+
*
|
|
821
|
+
* @template E The type of elements in the array.
|
|
822
|
+
* @param array The array to access (when using direct call syntax).
|
|
823
|
+
* @param index The index to access. Must be a branded `SizeType.ArgArr` (safe integer). Can be:
|
|
824
|
+
* - **Positive integer:** 0-based index from the start (0, 1, 2, ...)
|
|
825
|
+
* - **Negative integer:** index from the end (-1 is last element, -2 is second-to-last, etc.)
|
|
826
|
+
* - **Out of bounds:** any index beyond array bounds returns {@link Optional.None}
|
|
827
|
+
* @returns An {@link Optional}<E> containing:
|
|
828
|
+
* - {@link Optional.Some}<E> with the element if the index is valid
|
|
829
|
+
* - {@link Optional.None} if the index is out of bounds (including empty arrays)
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* ```typescript
|
|
833
|
+
* // Direct usage with positive indices
|
|
834
|
+
* const fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry'];
|
|
835
|
+
* const first = Arr.at(fruits, 0); // Optional.Some('apple')
|
|
836
|
+
* const third = Arr.at(fruits, 2); // Optional.Some('cherry')
|
|
837
|
+
* const outOfBounds = Arr.at(fruits, 10); // Optional.None
|
|
838
|
+
*
|
|
839
|
+
* // Negative indexing (accessing from the end)
|
|
840
|
+
* const last = Arr.at(fruits, -1); // Optional.Some('elderberry')
|
|
841
|
+
* const secondLast = Arr.at(fruits, -2); // Optional.Some('date')
|
|
842
|
+
* const negativeOutOfBounds = Arr.at(fruits, -10); // Optional.None
|
|
843
|
+
*
|
|
844
|
+
* // Edge cases
|
|
845
|
+
* const emptyResult = Arr.at([], 0); // Optional.None
|
|
846
|
+
* const negativeOnEmpty = Arr.at([], -1); // Optional.None
|
|
847
|
+
* const singleElement = Arr.at(['only'], 0); // Optional.Some('only')
|
|
848
|
+
* const singleNegative = Arr.at(['only'], -1); // Optional.Some('only')
|
|
849
|
+
*
|
|
850
|
+
* // Safe access pattern with type-safe unwrapping
|
|
851
|
+
* const maybeElement = Arr.at(fruits, 2);
|
|
852
|
+
* if (Optional.isSome(maybeElement)) {
|
|
853
|
+
* console.log(`Found: ${maybeElement.value}`); // Type-safe access, no undefined
|
|
854
|
+
* } else {
|
|
855
|
+
* console.log('Index out of bounds');
|
|
856
|
+
* }
|
|
857
|
+
*
|
|
858
|
+
* // Alternative unwrapping with default
|
|
859
|
+
* const elementOrDefault = Optional.unwrapOr(Arr.at(fruits, 100), 'not found');
|
|
860
|
+
* console.log(elementOrDefault); // 'not found'
|
|
861
|
+
*
|
|
862
|
+
* // Curried usage for functional composition
|
|
863
|
+
* const getSecondElement = Arr.at(1);
|
|
864
|
+
* const getLastElement = Arr.at(-1);
|
|
865
|
+
* const getMiddleElement = Arr.at(2);
|
|
866
|
+
*
|
|
867
|
+
* const nestedArrays = [
|
|
868
|
+
* [10, 20, 30, 40],
|
|
869
|
+
* [50, 60],
|
|
870
|
+
* [70]
|
|
871
|
+
* ];
|
|
872
|
+
* const secondElements = nestedArrays.map(getSecondElement);
|
|
873
|
+
* // [Optional.Some(20), Optional.None, Optional.None]
|
|
874
|
+
*
|
|
875
|
+
* const lastElements = nestedArrays.map(getLastElement);
|
|
876
|
+
* // [Optional.Some(40), Optional.Some(60), Optional.Some(70)]
|
|
877
|
+
*
|
|
878
|
+
* // Pipe composition for data processing
|
|
879
|
+
* const processArray = (arr: readonly string[]) => pipe(arr)
|
|
880
|
+
* .map(getSecondElement)
|
|
881
|
+
* .map(opt => Optional.map(opt, s => s.toUpperCase()))
|
|
882
|
+
* .map(opt => Optional.unwrapOr(opt, 'MISSING'))
|
|
883
|
+
* .value;
|
|
884
|
+
*
|
|
885
|
+
* console.log(processArray(['a', 'b', 'c'])); // 'B'
|
|
886
|
+
* console.log(processArray(['x'])); // 'MISSING'
|
|
887
|
+
*
|
|
888
|
+
* // Advanced curried usage with transformation pipelines
|
|
889
|
+
* const extractAndProcess = pipe([
|
|
890
|
+
* ['hello', 'world', 'typescript'],
|
|
891
|
+
* ['functional', 'programming'],
|
|
892
|
+
* ['type', 'safety', 'matters', 'most']
|
|
893
|
+
* ])
|
|
894
|
+
* .map(arr => arr.map(getSecondElement))
|
|
895
|
+
* .map(opts => opts.map(opt => Optional.unwrapOr(opt, '[missing]')))
|
|
896
|
+
* .value;
|
|
897
|
+
* // [['world'], ['[missing]'], ['safety']]
|
|
898
|
+
*
|
|
899
|
+
* // Type inference examples
|
|
900
|
+
* expectType<typeof first, Optional<string>>('=');
|
|
901
|
+
* expectType<typeof getSecondElement, <T>(array: readonly T[]) => Optional<T>>('=');
|
|
902
|
+
* expectType<typeof negativeOutOfBounds, Optional<string>>('=');
|
|
903
|
+
* ```
|
|
904
|
+
*
|
|
905
|
+
* @see {@link head} for getting the first element specifically
|
|
906
|
+
* @see {@link last} for getting the last element specifically
|
|
907
|
+
* @see {@link Optional} for working with the returned Optional values
|
|
908
|
+
* @see {@link Optional.unwrapOr} for safe unwrapping with defaults
|
|
909
|
+
* @see {@link Optional.map} for transforming Optional values
|
|
910
|
+
*/
|
|
911
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
912
|
+
export const at: AtFnOverload = (<E,>(
|
|
913
|
+
...args:
|
|
914
|
+
| readonly [array: readonly E[], index: SizeType.ArgArr]
|
|
915
|
+
| readonly [index: SizeType.ArgArr]
|
|
916
|
+
): Optional<E> | (<E2>(array: readonly E2[]) => Optional<E2>) => {
|
|
917
|
+
switch (args.length) {
|
|
918
|
+
case 2: {
|
|
919
|
+
const [array, index] = args;
|
|
920
|
+
return pipe(index < 0 ? array.length + index : index).map(
|
|
921
|
+
(normalizedIndex) =>
|
|
922
|
+
normalizedIndex < 0 || normalizedIndex >= array.length
|
|
923
|
+
? Optional.none
|
|
924
|
+
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
925
|
+
Optional.some(array[normalizedIndex]!),
|
|
926
|
+
).value;
|
|
927
|
+
}
|
|
928
|
+
case 1: {
|
|
929
|
+
const [index] = args;
|
|
930
|
+
return <E2,>(array: readonly E2[]) => at(array, index);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}) as AtFnOverload;
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Function type for safely accessing an array element at a given index.
|
|
937
|
+
*/
|
|
938
|
+
type AtFnOverload = {
|
|
939
|
+
<E>(array: readonly E[], index: SizeType.ArgArr): Optional<E>;
|
|
940
|
+
|
|
941
|
+
// Curried version
|
|
942
|
+
(index: SizeType.ArgArr): <E>(array: readonly E[]) => Optional<E>;
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Returns the first element of an array wrapped in an Optional.
|
|
947
|
+
*
|
|
948
|
+
* This function provides type-safe access to the first element with precise return types:
|
|
949
|
+
* - For empty arrays: returns `Optional.None`
|
|
950
|
+
* - For tuples with known first element: returns `Optional.Some<FirstElementType>`
|
|
951
|
+
* - For non-empty arrays: returns `Optional.Some<ElementType>`
|
|
952
|
+
* - For general arrays: returns `Optional<ElementType>`
|
|
953
|
+
*
|
|
954
|
+
* The function leverages TypeScript's type system to provide the most precise return type
|
|
955
|
+
* based on the input array type, making it safer than direct indexing.
|
|
956
|
+
*
|
|
957
|
+
* @template E The type of elements in the array.
|
|
958
|
+
* @param array The array to get the first element from.
|
|
959
|
+
* @returns An Optional containing the first element:
|
|
960
|
+
* - `Optional.None` if the array is empty
|
|
961
|
+
* - `Optional.Some<E>` containing the first element if the array is non-empty
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* ```typescript
|
|
965
|
+
* // Empty array - precise None type
|
|
966
|
+
* const emptyResult = Arr.head([]); // Optional.None
|
|
967
|
+
* console.log(Optional.isNone(emptyResult)); // true
|
|
968
|
+
*
|
|
969
|
+
* // Tuple with known structure - precise Some type
|
|
970
|
+
* const tupleResult = Arr.head(['first', 'second', 'third'] as const);
|
|
971
|
+
* // Type: Optional.Some<'first'>
|
|
972
|
+
* if (Optional.isSome(tupleResult)) {
|
|
973
|
+
* console.log(tupleResult.value); // 'first' - TypeScript knows exact type
|
|
974
|
+
* }
|
|
975
|
+
*
|
|
976
|
+
* // Non-empty array - guaranteed Some type
|
|
977
|
+
* const nonEmpty: NonEmptyArray<number> = [10, 20, 30] as NonEmptyArray<number>;
|
|
978
|
+
* const guaranteedResult = Arr.head(nonEmpty); // Optional.Some<number>
|
|
979
|
+
* // No need to check - always Some for NonEmptyArray
|
|
980
|
+
*
|
|
981
|
+
* // General array - may be Some or None
|
|
982
|
+
* const generalArray: number[] = [1, 2, 3];
|
|
983
|
+
* const maybeResult = Arr.head(generalArray); // Optional<number>
|
|
984
|
+
* if (Optional.isSome(maybeResult)) {
|
|
985
|
+
* console.log(`First element: ${maybeResult.value}`);
|
|
986
|
+
* } else {
|
|
987
|
+
* console.log('Array is empty');
|
|
988
|
+
* }
|
|
989
|
+
*
|
|
990
|
+
* // Working with different types
|
|
991
|
+
* const strings = ['hello', 'world'];
|
|
992
|
+
* const firstString = Arr.head(strings); // Optional<string>
|
|
993
|
+
*
|
|
994
|
+
* const objects = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
|
|
995
|
+
* const firstObject = Arr.head(objects); // Optional<{id: number, name: string}>
|
|
996
|
+
*
|
|
997
|
+
* // Functional composition
|
|
998
|
+
* const getFirstElements = (arrays: readonly number[][]) =>
|
|
999
|
+
* arrays.map(Arr.head).filter(Optional.isSome);
|
|
1000
|
+
*
|
|
1001
|
+
* const nestedArrays = [[1, 2], [3, 4], [], [5]];
|
|
1002
|
+
* const firstElements = getFirstElements(nestedArrays);
|
|
1003
|
+
* // [Optional.Some(1), Optional.Some(3), Optional.Some(5)]
|
|
1004
|
+
*
|
|
1005
|
+
* // Type inference examples
|
|
1006
|
+
* expectType<typeof emptyResult, Optional.None>('=');
|
|
1007
|
+
* expectType<typeof tupleResult, Optional.Some<'first'>>('=');
|
|
1008
|
+
* expectType<typeof guaranteedResult, Optional.Some<number>>('=');
|
|
1009
|
+
* expectType<typeof maybeResult, Optional<number>>('=');
|
|
1010
|
+
* ```
|
|
1011
|
+
*
|
|
1012
|
+
* @see {@link last} for getting the last element
|
|
1013
|
+
* @see {@link at} for accessing elements at specific indices
|
|
1014
|
+
* @see {@link tail} for getting all elements except the first
|
|
1015
|
+
*/
|
|
1016
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1017
|
+
export const head: HeadFnOverload = (<E,>(array: readonly E[]) => {
|
|
1018
|
+
const element = array.at(0);
|
|
1019
|
+
return element === undefined ? Optional.none : Optional.some(element);
|
|
1020
|
+
}) as HeadFnOverload;
|
|
1021
|
+
|
|
1022
|
+
type HeadFnOverload = {
|
|
1023
|
+
/**
|
|
1024
|
+
* Get head of empty array.
|
|
1025
|
+
*/
|
|
1026
|
+
(array: readonly []): Optional.None;
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Get head of tuple.
|
|
1030
|
+
*/
|
|
1031
|
+
<E, L extends readonly unknown[]>(
|
|
1032
|
+
array: readonly [E, ...L],
|
|
1033
|
+
): Optional.Some<E>;
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Get head of non-empty array.
|
|
1037
|
+
*/
|
|
1038
|
+
<E>(array: NonEmptyArray<E>): Optional.Some<E>;
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Get head of any array.
|
|
1042
|
+
*/
|
|
1043
|
+
<E>(array: readonly E[]): Optional<E>;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Returns the last element of an array wrapped in an Optional.
|
|
1048
|
+
*
|
|
1049
|
+
* This function provides type-safe access to the last element with precise return types:
|
|
1050
|
+
* - For empty arrays: returns `Optional.None`
|
|
1051
|
+
* - For tuples with known last element: returns `Optional.Some<LastElementType>`
|
|
1052
|
+
* - For non-empty arrays: returns `Optional.Some<ElementType>`
|
|
1053
|
+
* - For general arrays: returns `Optional<ElementType>`
|
|
1054
|
+
*
|
|
1055
|
+
* The function leverages TypeScript's type system to provide the most precise return type
|
|
1056
|
+
* based on the input array type, making it safer than direct indexing.
|
|
1057
|
+
*
|
|
1058
|
+
* @template E The type of elements in the array.
|
|
1059
|
+
* @param array The array to get the last element from.
|
|
1060
|
+
* @returns An Optional containing the last element:
|
|
1061
|
+
* - `Optional.None` if the array is empty
|
|
1062
|
+
* - `Optional.Some<E>` containing the last element if the array is non-empty
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* ```typescript
|
|
1066
|
+
* // Empty array - precise None type
|
|
1067
|
+
* const emptyResult = Arr.last([]); // Optional.None
|
|
1068
|
+
* console.log(Optional.isNone(emptyResult)); // true
|
|
1069
|
+
*
|
|
1070
|
+
* // Tuple with known structure - precise Some type
|
|
1071
|
+
* const tupleResult = Arr.last(['first', 'middle', 'last'] as const);
|
|
1072
|
+
* // Type: Optional.Some<'last'>
|
|
1073
|
+
* if (Optional.isSome(tupleResult)) {
|
|
1074
|
+
* console.log(tupleResult.value); // 'last' - TypeScript knows exact type
|
|
1075
|
+
* }
|
|
1076
|
+
*
|
|
1077
|
+
* // Non-empty array - guaranteed Some type
|
|
1078
|
+
* const nonEmpty: NonEmptyArray<number> = [10, 20, 30] as NonEmptyArray<number>;
|
|
1079
|
+
* const guaranteedResult = Arr.last(nonEmpty); // Optional.Some<number>
|
|
1080
|
+
* // No need to check - always Some for NonEmptyArray
|
|
1081
|
+
*
|
|
1082
|
+
* // General array - may be Some or None
|
|
1083
|
+
* const generalArray: number[] = [1, 2, 3];
|
|
1084
|
+
* const maybeResult = Arr.last(generalArray); // Optional<number>
|
|
1085
|
+
* if (Optional.isSome(maybeResult)) {
|
|
1086
|
+
* console.log(`Last element: ${maybeResult.value}`);
|
|
1087
|
+
* } else {
|
|
1088
|
+
* console.log('Array is empty');
|
|
1089
|
+
* }
|
|
1090
|
+
*
|
|
1091
|
+
* // Working with different types
|
|
1092
|
+
* const strings = ['hello', 'world', 'example'];
|
|
1093
|
+
* const lastString = Arr.last(strings); // Optional<string>
|
|
1094
|
+
*
|
|
1095
|
+
* const coordinates = [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}];
|
|
1096
|
+
* const lastCoordinate = Arr.last(coordinates); // Optional<{x: number, y: number}>
|
|
1097
|
+
*
|
|
1098
|
+
* // Single element arrays
|
|
1099
|
+
* const single = [42];
|
|
1100
|
+
* const singleResult = Arr.last(single); // Optional<number> containing 42
|
|
1101
|
+
*
|
|
1102
|
+
* // Functional composition with arrays of arrays
|
|
1103
|
+
* const getLastElements = (arrays: readonly string[][]) =>
|
|
1104
|
+
* arrays.map(Arr.last).filter(Optional.isSome);
|
|
1105
|
+
*
|
|
1106
|
+
* const nestedArrays = [['a', 'b'], ['c'], [], ['d', 'e', 'f']];
|
|
1107
|
+
* const lastElements = getLastElements(nestedArrays);
|
|
1108
|
+
* // [Optional.Some('b'), Optional.Some('c'), Optional.Some('f')]
|
|
1109
|
+
*
|
|
1110
|
+
* // Common pattern: get last element or default
|
|
1111
|
+
* const data = [10, 20, 30];
|
|
1112
|
+
* const lastOrDefault = Optional.unwrapOr(Arr.last(data), 0); // 30
|
|
1113
|
+
* const emptyLastOrDefault = Optional.unwrapOr(Arr.last([]), 0); // 0
|
|
1114
|
+
*
|
|
1115
|
+
* // Type inference examples
|
|
1116
|
+
* expectType<typeof emptyResult, Optional.None>('=');
|
|
1117
|
+
* expectType<typeof tupleResult, Optional.Some<'last'>>('=');
|
|
1118
|
+
* expectType<typeof guaranteedResult, Optional.Some<number>>('=');
|
|
1119
|
+
* expectType<typeof maybeResult, Optional<number>>('=');
|
|
1120
|
+
* ```
|
|
1121
|
+
*
|
|
1122
|
+
* @see {@link head} for getting the first element
|
|
1123
|
+
* @see {@link at} for accessing elements at specific indices with negative indexing support
|
|
1124
|
+
* @see {@link butLast} for getting all elements except the last
|
|
1125
|
+
*/
|
|
1126
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1127
|
+
export const last: LastFnOverload = (<E,>(array: readonly E[]) => {
|
|
1128
|
+
const element = array.at(-1);
|
|
1129
|
+
return element === undefined ? Optional.none : Optional.some(element);
|
|
1130
|
+
}) as LastFnOverload;
|
|
1131
|
+
|
|
1132
|
+
type LastFnOverload = {
|
|
1133
|
+
/**
|
|
1134
|
+
* Get last of empty array.
|
|
1135
|
+
*/
|
|
1136
|
+
(array: readonly []): Optional.None;
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Get last of tuple.
|
|
1140
|
+
*/
|
|
1141
|
+
<Ar extends readonly unknown[], L>(
|
|
1142
|
+
array: readonly [...Ar, L],
|
|
1143
|
+
): Optional.Some<L>;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Get last of non-empty array.
|
|
1147
|
+
*/
|
|
1148
|
+
<E>(array: NonEmptyArray<E>): Optional.Some<E>;
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Get last of any array.
|
|
1152
|
+
*/
|
|
1153
|
+
<E>(array: readonly E[]): Optional<E>;
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// slicing
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Slices an array with automatically clamped start and end indices for safe bounds handling.
|
|
1160
|
+
*
|
|
1161
|
+
* This function provides a safer alternative to `Array.slice()` by automatically clamping
|
|
1162
|
+
* the start and end indices to valid bounds, preventing out-of-bounds access and ensuring
|
|
1163
|
+
* consistent behavior regardless of input values.
|
|
1164
|
+
*
|
|
1165
|
+
* **Clamping Behavior:**
|
|
1166
|
+
* - `start` is clamped to `[0, array.length]`
|
|
1167
|
+
* - `end` is clamped to `[clampedStart, array.length]` (ensuring end ≥ start)
|
|
1168
|
+
* - Invalid ranges (start > end after clamping) return empty arrays
|
|
1169
|
+
* - Negative indices are clamped to 0, large indices are clamped to array.length
|
|
1170
|
+
*
|
|
1171
|
+
* **Curried Usage:** This function supports currying - when called with only start and end
|
|
1172
|
+
* indices, it returns a function that can be applied to arrays.
|
|
1173
|
+
*
|
|
1174
|
+
* @template E The type of elements in the array.
|
|
1175
|
+
* @param array The array to slice (when using direct call syntax).
|
|
1176
|
+
* @param start The start index for the slice (inclusive). Will be clamped to valid bounds.
|
|
1177
|
+
* @param end The end index for the slice (exclusive). Will be clamped to valid bounds.
|
|
1178
|
+
* @returns A new immutable array containing the sliced elements. Always returns a valid array,
|
|
1179
|
+
* never throws for out-of-bounds indices.
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* ```typescript
|
|
1183
|
+
* const data = [10, 20, 30, 40, 50];
|
|
1184
|
+
*
|
|
1185
|
+
* // Normal slicing
|
|
1186
|
+
* const middle = Arr.sliceClamped(data, 1, 4); // [20, 30, 40]
|
|
1187
|
+
* const beginning = Arr.sliceClamped(data, 0, 2); // [10, 20]
|
|
1188
|
+
* const end = Arr.sliceClamped(data, 3, 5); // [40, 50]
|
|
1189
|
+
*
|
|
1190
|
+
* // Automatic clamping for out-of-bounds indices
|
|
1191
|
+
* const clampedStart = Arr.sliceClamped(data, -10, 3); // [10, 20, 30] (start clamped to 0)
|
|
1192
|
+
* const clampedEnd = Arr.sliceClamped(data, 2, 100); // [30, 40, 50] (end clamped to length)
|
|
1193
|
+
* const bothClamped = Arr.sliceClamped(data, -5, 100); // [10, 20, 30, 40, 50] (entire array)
|
|
1194
|
+
*
|
|
1195
|
+
* // Invalid ranges become empty arrays
|
|
1196
|
+
* const emptyReversed = Arr.sliceClamped(data, 4, 1); // [] (start > end after clamping)
|
|
1197
|
+
* const emptyAtEnd = Arr.sliceClamped(data, 5, 10); // [] (start at end of array)
|
|
1198
|
+
*
|
|
1199
|
+
* // Edge cases
|
|
1200
|
+
* const emptyArray = Arr.sliceClamped([], 0, 5); // [] (empty input)
|
|
1201
|
+
* const singleElement = Arr.sliceClamped([42], 0, 1); // [42]
|
|
1202
|
+
* const fullCopy = Arr.sliceClamped(data, 0, data.length); // [10, 20, 30, 40, 50]
|
|
1203
|
+
*
|
|
1204
|
+
* // Curried usage for functional composition
|
|
1205
|
+
* const takeFirst3 = Arr.sliceClamped(0, 3);
|
|
1206
|
+
* const getMiddle2 = Arr.sliceClamped(1, 3);
|
|
1207
|
+
*
|
|
1208
|
+
* const arrays = [
|
|
1209
|
+
* [1, 2, 3, 4, 5],
|
|
1210
|
+
* [10, 20],
|
|
1211
|
+
* [100, 200, 300, 400, 500, 600]
|
|
1212
|
+
* ];
|
|
1213
|
+
*
|
|
1214
|
+
* const first3Elements = arrays.map(takeFirst3);
|
|
1215
|
+
* // [[1, 2, 3], [10, 20], [100, 200, 300]]
|
|
1216
|
+
*
|
|
1217
|
+
* const middle2Elements = arrays.map(getMiddle2);
|
|
1218
|
+
* // [[2, 3], [20], [200, 300]]
|
|
1219
|
+
*
|
|
1220
|
+
* // Pipe composition
|
|
1221
|
+
* const result = pipe([1, 2, 3, 4, 5, 6])
|
|
1222
|
+
* .map(takeFirst3)
|
|
1223
|
+
* .map(Arr.sum)
|
|
1224
|
+
* .value; // 6 (sum of [1, 2, 3])
|
|
1225
|
+
*
|
|
1226
|
+
* // Comparison with regular Array.slice (which can throw or behave unexpectedly)
|
|
1227
|
+
* try {
|
|
1228
|
+
* // Regular slice with out-of-bounds - works but may be unintuitive
|
|
1229
|
+
* const regularSlice = data.slice(-10, 100); // [10, 20, 30, 40, 50]
|
|
1230
|
+
* // sliceClamped provides same safe behavior explicitly
|
|
1231
|
+
* const clampedSlice = Arr.sliceClamped(data, -10, 100); // [10, 20, 30, 40, 50]
|
|
1232
|
+
* } catch (error) {
|
|
1233
|
+
* // sliceClamped never throws
|
|
1234
|
+
* }
|
|
1235
|
+
* ```
|
|
1236
|
+
*
|
|
1237
|
+
* @see {@link take} for taking the first N elements
|
|
1238
|
+
* @see {@link skip} for skipping the first N elements
|
|
1239
|
+
* @see {@link takeLast} for taking the last N elements
|
|
1240
|
+
*/
|
|
1241
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1242
|
+
export const sliceClamped: SliceClampedFnOverload = (<E,>(
|
|
1243
|
+
...args:
|
|
1244
|
+
| readonly [readonly E[], SizeType.ArgArr, SizeType.ArgArr]
|
|
1245
|
+
| readonly [SizeType.ArgArr, SizeType.ArgArr]
|
|
1246
|
+
) => {
|
|
1247
|
+
switch (args.length) {
|
|
1248
|
+
case 3: {
|
|
1249
|
+
const [array, start, end] = args;
|
|
1250
|
+
const startClamped = Num.clamp(0, array.length)(start);
|
|
1251
|
+
// Ensure endClamped is not less than startClamped.
|
|
1252
|
+
const endClamped = Num.clamp(startClamped, array.length)(end);
|
|
1253
|
+
return array.slice(startClamped, endClamped);
|
|
1254
|
+
}
|
|
1255
|
+
case 2: {
|
|
1256
|
+
const [start, end] = args;
|
|
1257
|
+
return <E2,>(array: readonly E2[]) => sliceClamped(array, start, end);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}) as SliceClampedFnOverload;
|
|
1261
|
+
|
|
1262
|
+
type SliceClampedFnOverload = {
|
|
1263
|
+
<E>(
|
|
1264
|
+
array: readonly E[],
|
|
1265
|
+
start: SizeType.ArgArr,
|
|
1266
|
+
end: SizeType.ArgArr,
|
|
1267
|
+
): readonly E[];
|
|
1268
|
+
|
|
1269
|
+
// Curried version
|
|
1270
|
+
(
|
|
1271
|
+
start: SizeType.ArgArr,
|
|
1272
|
+
end: SizeType.ArgArr,
|
|
1273
|
+
): <E>(array: readonly E[]) => readonly E[];
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Returns all elements of an array except the first one.
|
|
1278
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1279
|
+
* @param array The input array.
|
|
1280
|
+
* @returns A new array containing all elements except the first. The type is inferred as `List.Tail<T>`.
|
|
1281
|
+
* @example
|
|
1282
|
+
* ```ts
|
|
1283
|
+
* Arr.tail([1, 2, 3] as const); // [2, 3]
|
|
1284
|
+
* Arr.tail([1] as const); // []
|
|
1285
|
+
* Arr.tail([]); // []
|
|
1286
|
+
* ```
|
|
1287
|
+
*/
|
|
1288
|
+
export const tail = <Ar extends readonly unknown[]>(
|
|
1289
|
+
array: Ar,
|
|
1290
|
+
): List.Tail<Ar> =>
|
|
1291
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1292
|
+
array.slice(1) as unknown as List.Tail<Ar>;
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Returns all elements of an array except the last one.
|
|
1296
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1297
|
+
* @param array The input array.
|
|
1298
|
+
* @returns A new array containing all elements except the last. The type is inferred as `List.ButLast<T>`.
|
|
1299
|
+
* @example
|
|
1300
|
+
* ```ts
|
|
1301
|
+
* Arr.butLast([1, 2, 3] as const); // [1, 2]
|
|
1302
|
+
* Arr.butLast([1] as const); // []
|
|
1303
|
+
* Arr.butLast([]); // []
|
|
1304
|
+
* ```
|
|
1305
|
+
*/
|
|
1306
|
+
export const butLast = <Ar extends readonly unknown[]>(
|
|
1307
|
+
array: Ar,
|
|
1308
|
+
): List.ButLast<Ar> =>
|
|
1309
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1310
|
+
(isEmpty(array) ? [] : array.slice(0, -1)) as unknown as List.ButLast<Ar>;
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Takes the first N elements from an array.
|
|
1314
|
+
*
|
|
1315
|
+
* - If the array is a tuple, the return type is inferred as a tuple of the first N elements.
|
|
1316
|
+
* - If the array is a NonEmptyArray and N is a SizeType.ArgArrPositive, returns a NonEmptyArray.
|
|
1317
|
+
* - Otherwise, returns a readonly array of up to N elements.
|
|
1318
|
+
*
|
|
1319
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1320
|
+
* @template N The number of elements to take, constrained to `SmallUint`.
|
|
1321
|
+
* @param array The input array.
|
|
1322
|
+
* @param num The number of elements to take.
|
|
1323
|
+
* @returns A new array containing the first N elements.
|
|
1324
|
+
* @example
|
|
1325
|
+
* ```ts
|
|
1326
|
+
* // Regular usage
|
|
1327
|
+
* Arr.take([1, 2, 3, 4] as const, 2); // [1, 2]
|
|
1328
|
+
*
|
|
1329
|
+
* // Curried usage for pipe composition
|
|
1330
|
+
* const takeFirst3 = Arr.take(3);
|
|
1331
|
+
* const result = pipe([1, 2, 3, 4, 5]).map(takeFirst3).value;
|
|
1332
|
+
* console.log(result); // [1, 2, 3]
|
|
1333
|
+
* ```
|
|
1334
|
+
*/
|
|
1335
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1336
|
+
export const take: TakeFnOverload = (<Ar extends readonly unknown[]>(
|
|
1337
|
+
...args:
|
|
1338
|
+
| readonly [array: Ar, num: SizeType.ArgArrNonNegative]
|
|
1339
|
+
| readonly [num: SizeType.ArgArrNonNegative]
|
|
1340
|
+
) => {
|
|
1341
|
+
switch (args.length) {
|
|
1342
|
+
case 2: {
|
|
1343
|
+
const [array, num] = args;
|
|
1344
|
+
return sliceClamped(array, 0, num);
|
|
1345
|
+
}
|
|
1346
|
+
case 1: {
|
|
1347
|
+
const [num] = args;
|
|
1348
|
+
return <E,>(array: readonly E[]) => take(array, num);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}) as unknown as TakeFnOverload;
|
|
1352
|
+
|
|
1353
|
+
type TakeFnOverload = {
|
|
1354
|
+
<Ar extends readonly unknown[], N extends SmallUint>(
|
|
1355
|
+
array: Ar,
|
|
1356
|
+
num: N,
|
|
1357
|
+
): List.Take<N, Ar>;
|
|
1358
|
+
|
|
1359
|
+
// curried version
|
|
1360
|
+
<N extends SmallUint>(
|
|
1361
|
+
num: N,
|
|
1362
|
+
): <Ar extends readonly unknown[]>(array: Ar) => List.Take<N, Ar>;
|
|
1363
|
+
|
|
1364
|
+
<E>(
|
|
1365
|
+
array: NonEmptyArray<E>,
|
|
1366
|
+
num: SizeType.ArgArrPositive,
|
|
1367
|
+
): NonEmptyArray<E>;
|
|
1368
|
+
|
|
1369
|
+
// curried version
|
|
1370
|
+
(
|
|
1371
|
+
num: SizeType.ArgArrPositive,
|
|
1372
|
+
): <E>(array: NonEmptyArray<E>) => NonEmptyArray<E>;
|
|
1373
|
+
|
|
1374
|
+
<E>(array: readonly E[], num: SizeType.ArgArrNonNegative): readonly E[];
|
|
1375
|
+
|
|
1376
|
+
// curried version
|
|
1377
|
+
(num: SizeType.ArgArrNonNegative): <E>(array: readonly E[]) => readonly E[];
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Takes the last N elements from an array.
|
|
1382
|
+
*
|
|
1383
|
+
* - If the array is a tuple, the return type is inferred as a tuple of the last N elements.
|
|
1384
|
+
* - If the array is a non-empty array and N is a positive integer, returns a non-empty array.
|
|
1385
|
+
* - Otherwise, returns a readonly array of up to N elements.
|
|
1386
|
+
*
|
|
1387
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1388
|
+
* @template N The number of elements to take, constrained to `SmallUint`.
|
|
1389
|
+
* @param array The input array.
|
|
1390
|
+
* @param num The number of elements to take.
|
|
1391
|
+
* @returns A new array containing the last N elements.
|
|
1392
|
+
* @example
|
|
1393
|
+
* ```ts
|
|
1394
|
+
* // Regular usage
|
|
1395
|
+
* Arr.takeLast([1, 2, 3, 4] as const, 2); // [3, 4]
|
|
1396
|
+
*
|
|
1397
|
+
* // Curried usage for pipe composition
|
|
1398
|
+
* const takeLast2 = Arr.takeLast(2);
|
|
1399
|
+
* const result = pipe([1, 2, 3, 4, 5]).map(takeLast2).value;
|
|
1400
|
+
* console.log(result); // [4, 5]
|
|
1401
|
+
* ```
|
|
1402
|
+
*/
|
|
1403
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1404
|
+
export const takeLast: TakeLastFnOverload = (<Ar extends readonly unknown[]>(
|
|
1405
|
+
...args:
|
|
1406
|
+
| readonly [array: Ar, num: SizeType.ArgArrNonNegative]
|
|
1407
|
+
| readonly [num: SizeType.ArgArrNonNegative]
|
|
1408
|
+
) => {
|
|
1409
|
+
switch (args.length) {
|
|
1410
|
+
case 2: {
|
|
1411
|
+
const [array, num] = args;
|
|
1412
|
+
return sliceClamped(array, Uint32.sub(size(array), num), size(array));
|
|
1413
|
+
}
|
|
1414
|
+
case 1: {
|
|
1415
|
+
const [num] = args;
|
|
1416
|
+
return <E,>(array: readonly E[]) => takeLast(array, num);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}) as unknown as TakeLastFnOverload;
|
|
1420
|
+
|
|
1421
|
+
type TakeLastFnOverload = {
|
|
1422
|
+
<Ar extends readonly unknown[], N extends SmallUint>(
|
|
1423
|
+
array: Ar,
|
|
1424
|
+
num: N,
|
|
1425
|
+
): List.TakeLast<N, Ar>;
|
|
1426
|
+
|
|
1427
|
+
// curried version
|
|
1428
|
+
<N extends SmallUint>(
|
|
1429
|
+
num: N,
|
|
1430
|
+
): <Ar extends readonly unknown[]>(array: Ar) => List.TakeLast<N, Ar>;
|
|
1431
|
+
|
|
1432
|
+
<E>(
|
|
1433
|
+
array: NonEmptyArray<E>,
|
|
1434
|
+
num: SizeType.ArgArrPositive,
|
|
1435
|
+
): NonEmptyArray<E>;
|
|
1436
|
+
|
|
1437
|
+
// curried version
|
|
1438
|
+
(
|
|
1439
|
+
num: SizeType.ArgArrPositive,
|
|
1440
|
+
): <E>(array: NonEmptyArray<E>) => NonEmptyArray<E>;
|
|
1441
|
+
|
|
1442
|
+
<E>(array: readonly E[], num: SizeType.ArgArrNonNegative): readonly E[];
|
|
1443
|
+
|
|
1444
|
+
// curried version
|
|
1445
|
+
(num: SizeType.ArgArrNonNegative): <E>(array: readonly E[]) => readonly E[];
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Skips the first N elements of an array.
|
|
1450
|
+
*
|
|
1451
|
+
* - If the array is a tuple, the return type is inferred as a tuple with the first N elements removed.
|
|
1452
|
+
* - If the array is a non-empty array and N is a positive integer, returns a readonly array (may be empty).
|
|
1453
|
+
* - Otherwise, returns a readonly array with the first N elements skipped.
|
|
1454
|
+
*
|
|
1455
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1456
|
+
* @template N The number of elements to skip, constrained to `SmallUint`.
|
|
1457
|
+
* @param array The input array.
|
|
1458
|
+
* @param num The number of elements to skip.
|
|
1459
|
+
* @returns A new array containing the elements after skipping the first N.
|
|
1460
|
+
* @example
|
|
1461
|
+
* ```ts
|
|
1462
|
+
* // Regular usage
|
|
1463
|
+
* Arr.skip([1, 2, 3, 4] as const, 2); // [3, 4]
|
|
1464
|
+
*
|
|
1465
|
+
* // Curried usage for pipe composition
|
|
1466
|
+
* const skipFirst2 = Arr.skip(2);
|
|
1467
|
+
* const result = pipe([1, 2, 3, 4, 5]).map(skipFirst2).value;
|
|
1468
|
+
* console.log(result); // [3, 4, 5]
|
|
1469
|
+
* ```
|
|
1470
|
+
*/
|
|
1471
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1472
|
+
export const skip: SkipFnOverload = (<E,>(
|
|
1473
|
+
...args:
|
|
1474
|
+
| readonly [readonly E[], SizeType.ArgArrNonNegative]
|
|
1475
|
+
| readonly [SizeType.ArgArrNonNegative]
|
|
1476
|
+
) => {
|
|
1477
|
+
switch (args.length) {
|
|
1478
|
+
case 2: {
|
|
1479
|
+
const [array, num] = args;
|
|
1480
|
+
return sliceClamped(array, num, size(array));
|
|
1481
|
+
}
|
|
1482
|
+
case 1: {
|
|
1483
|
+
const [num] = args;
|
|
1484
|
+
return <E2,>(array: readonly E2[]) => skip(array, num);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}) as SkipFnOverload;
|
|
1488
|
+
|
|
1489
|
+
type SkipFnOverload = {
|
|
1490
|
+
<Ar extends readonly unknown[], N extends SmallUint>(
|
|
1491
|
+
array: Ar,
|
|
1492
|
+
num: N,
|
|
1493
|
+
): List.Skip<N, Ar>;
|
|
1494
|
+
|
|
1495
|
+
<E>(array: NonEmptyArray<E>, num: SizeType.ArgArrPositive): readonly E[];
|
|
1496
|
+
|
|
1497
|
+
<E>(array: readonly E[], num: SizeType.ArgArrNonNegative): readonly E[];
|
|
1498
|
+
|
|
1499
|
+
// curried version
|
|
1500
|
+
<N extends SmallUint>(
|
|
1501
|
+
num: N,
|
|
1502
|
+
): <Ar extends readonly unknown[]>(array: Ar) => List.Skip<N, Ar>;
|
|
1503
|
+
|
|
1504
|
+
(
|
|
1505
|
+
num: SizeType.ArgArrPositive,
|
|
1506
|
+
): <E>(array: NonEmptyArray<E>) => readonly E[];
|
|
1507
|
+
|
|
1508
|
+
(num: SizeType.ArgArrNonNegative): <E>(array: readonly E[]) => readonly E[];
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Skips the last N elements of an array.
|
|
1513
|
+
*
|
|
1514
|
+
* - If the array is a tuple, the return type is inferred as a tuple with the last N elements removed.
|
|
1515
|
+
* - If the array is a non-empty array and N is a positive integer, returns a readonly array (may be empty).
|
|
1516
|
+
* - Otherwise, returns a readonly array with the last N elements skipped.
|
|
1517
|
+
*
|
|
1518
|
+
* @template E The type of the array (can be a tuple for more precise typing).
|
|
1519
|
+
* @template N The number of elements to skip, constrained to `SmallUint`.
|
|
1520
|
+
* @param array The input array.
|
|
1521
|
+
* @param num The number of elements to skip from the end.
|
|
1522
|
+
* @returns A new array containing the elements after skipping the last N.
|
|
1523
|
+
* @example
|
|
1524
|
+
* ```ts
|
|
1525
|
+
* // Regular usage
|
|
1526
|
+
* Arr.skipLast([1, 2, 3, 4] as const, 2); // [1, 2]
|
|
1527
|
+
*
|
|
1528
|
+
* // Curried usage for pipe composition
|
|
1529
|
+
* const skipLast2 = Arr.skipLast(2);
|
|
1530
|
+
* const result = pipe([1, 2, 3, 4, 5]).map(skipLast2).value;
|
|
1531
|
+
* console.log(result); // [1, 2, 3]
|
|
1532
|
+
* ```
|
|
1533
|
+
*/
|
|
1534
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1535
|
+
export const skipLast: SkipLastFnOverload = (<E,>(
|
|
1536
|
+
...args:
|
|
1537
|
+
| readonly [array: readonly E[], num: SizeType.ArgArrNonNegative]
|
|
1538
|
+
| readonly [num: SizeType.ArgArrNonNegative]
|
|
1539
|
+
): readonly E[] | ((array: readonly E[]) => readonly E[]) => {
|
|
1540
|
+
switch (args.length) {
|
|
1541
|
+
case 2: {
|
|
1542
|
+
const [array, num] = args;
|
|
1543
|
+
return sliceClamped(array, 0, Uint32.sub(size(array), num));
|
|
1544
|
+
}
|
|
1545
|
+
case 1: {
|
|
1546
|
+
const [num] = args;
|
|
1547
|
+
return <E2,>(array: readonly E2[]) => skipLast(array, num);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}) as SkipLastFnOverload;
|
|
1551
|
+
|
|
1552
|
+
type SkipLastFnOverload = {
|
|
1553
|
+
<Ar extends readonly unknown[], N extends SmallUint>(
|
|
1554
|
+
array: Ar,
|
|
1555
|
+
num: N,
|
|
1556
|
+
): List.SkipLast<N, Ar>;
|
|
1557
|
+
|
|
1558
|
+
<E>(array: readonly E[], num: SizeType.ArgArrNonNegative): readonly E[];
|
|
1559
|
+
|
|
1560
|
+
// curried version
|
|
1561
|
+
<N extends SmallUint>(
|
|
1562
|
+
num: N,
|
|
1563
|
+
): <Ar extends readonly unknown[]>(array: Ar) => List.SkipLast<N, Ar>;
|
|
1564
|
+
|
|
1565
|
+
(num: SizeType.ArgArrNonNegative): <E>(array: readonly E[]) => readonly E[];
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// modification (returns new array)
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Returns a new array with the element at the specified index updated by a function.
|
|
1572
|
+
*
|
|
1573
|
+
* This function provides immutable array updates with type-safe bounds checking. It applies an updater
|
|
1574
|
+
* function to the element at the given index and returns a new array with the transformed value.
|
|
1575
|
+
* The original array is never modified, ensuring immutability.
|
|
1576
|
+
*
|
|
1577
|
+
* **Type Union Behavior:** When the updater function returns a different type `U` than the original
|
|
1578
|
+
* element type `E`, the result type becomes `readonly (E | U)[]` to accommodate both original and
|
|
1579
|
+
* updated element types. This ensures type safety when elements have different types after updating.
|
|
1580
|
+
*
|
|
1581
|
+
* **Bounds Checking:** Unlike native array access which can cause runtime errors, this function
|
|
1582
|
+
* performs safe bounds checking:
|
|
1583
|
+
* - **Valid index:** Creates new array with updated element
|
|
1584
|
+
* - **Invalid index:** Returns the original array unchanged (no errors thrown)
|
|
1585
|
+
* - **Negative index:** Treated as invalid (returns original array)
|
|
1586
|
+
*
|
|
1587
|
+
* **Curried Usage:** Supports currying for functional composition - when called with only index and
|
|
1588
|
+
* updater, returns a reusable function that can be applied to arrays.
|
|
1589
|
+
*
|
|
1590
|
+
* @template E The type of elements in the original array.
|
|
1591
|
+
* @template U The type of the value returned by the updater function.
|
|
1592
|
+
* @param array The input array to update. Can be any readonly array.
|
|
1593
|
+
* @param index The index of the element to update. Must be a non-negative {@link SizeType.ArgArrNonNegative}.
|
|
1594
|
+
* - **Valid range:** `0 <= index < array.length`
|
|
1595
|
+
* - **Out of bounds:** Returns original array unchanged
|
|
1596
|
+
* - **Negative values:** Not allowed by type system (non-negative constraint)
|
|
1597
|
+
* @param updater A function `(prev: E) => U` that transforms the existing element:
|
|
1598
|
+
* - **prev:** The current element at the specified index
|
|
1599
|
+
* - **returns:** The new value to place at that index (can be different type)
|
|
1600
|
+
* @returns A new `readonly (E | U)[]` array where:
|
|
1601
|
+
* - All elements except the target index remain unchanged (type `E`)
|
|
1602
|
+
* - The element at the target index is replaced with the updater result (type `U`)
|
|
1603
|
+
* - Type union `E | U` accommodates both original and updated element types
|
|
1604
|
+
* - If index is out of bounds, returns the original array unchanged
|
|
1605
|
+
*
|
|
1606
|
+
* @example
|
|
1607
|
+
* ```typescript
|
|
1608
|
+
* // Basic usage with same type transformation
|
|
1609
|
+
* const numbers = [1, 2, 3, 4, 5];
|
|
1610
|
+
* const doubled = Arr.toUpdated(numbers, 2, x => x * 2);
|
|
1611
|
+
* // readonly number[] -> [1, 2, 6, 4, 5]
|
|
1612
|
+
*
|
|
1613
|
+
* // Type union when updater returns different type
|
|
1614
|
+
* const mixed = Arr.toUpdated(numbers, 1, x => `value: ${x}`);
|
|
1615
|
+
* // readonly (number | string)[] -> [1, 'value: 2', 3, 4, 5]
|
|
1616
|
+
*
|
|
1617
|
+
* // Complex object updates
|
|
1618
|
+
* const users = [
|
|
1619
|
+
* { id: 1, name: 'Alice', active: true },
|
|
1620
|
+
* { id: 2, name: 'Bob', active: false },
|
|
1621
|
+
* { id: 3, name: 'Charlie', active: true }
|
|
1622
|
+
* ];
|
|
1623
|
+
*
|
|
1624
|
+
* const activatedUser = Arr.toUpdated(users, 1, user => ({
|
|
1625
|
+
* ...user,
|
|
1626
|
+
* active: true,
|
|
1627
|
+
* lastUpdated: new Date()
|
|
1628
|
+
* }));
|
|
1629
|
+
* // Bob is now active with lastUpdated field
|
|
1630
|
+
*
|
|
1631
|
+
* // Bounds checking behavior
|
|
1632
|
+
* const safe1 = Arr.toUpdated([1, 2, 3], 10, x => x * 2); // [1, 2, 3] (index out of bounds)
|
|
1633
|
+
* const safe2 = Arr.toUpdated([1, 2, 3], 0, x => x * 2); // [2, 2, 3] (valid index)
|
|
1634
|
+
* const safe3 = Arr.toUpdated([], 0, x => x); // [] (empty array, index out of bounds)
|
|
1635
|
+
*
|
|
1636
|
+
* // Functional transformations
|
|
1637
|
+
* const products = [
|
|
1638
|
+
* { name: 'laptop', price: 1000 },
|
|
1639
|
+
* { name: 'mouse', price: 25 },
|
|
1640
|
+
* { name: 'keyboard', price: 75 }
|
|
1641
|
+
* ];
|
|
1642
|
+
*
|
|
1643
|
+
* const discounted = Arr.toUpdated(products, 0, product => ({
|
|
1644
|
+
* ...product,
|
|
1645
|
+
* price: Math.round(product.price * 0.8), // 20% discount
|
|
1646
|
+
* onSale: true
|
|
1647
|
+
* }));
|
|
1648
|
+
* // First product now has discounted price and onSale flag
|
|
1649
|
+
*
|
|
1650
|
+
* // Curried usage for reusable updates
|
|
1651
|
+
* const doubleAtIndex2 = Arr.toUpdated(2, (x: number) => x * 2);
|
|
1652
|
+
* const capitalizeAtIndex0 = Arr.toUpdated(0, (s: string) => s.toUpperCase());
|
|
1653
|
+
* const markCompleteAtIndex = (index: number) =>
|
|
1654
|
+
* Arr.toUpdated(index, (task: {done: boolean}) => ({...task, done: true}));
|
|
1655
|
+
*
|
|
1656
|
+
* const numberArrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
|
|
1657
|
+
* const allDoubled = numberArrays.map(doubleAtIndex2);
|
|
1658
|
+
* // [[1, 2, 6], [4, 5, 12], [7, 8, 18]]
|
|
1659
|
+
*
|
|
1660
|
+
* const words = [['hello', 'world'], ['foo', 'bar'], ['type', 'script']];
|
|
1661
|
+
* const capitalized = words.map(capitalizeAtIndex0);
|
|
1662
|
+
* // [['HELLO', 'world'], ['FOO', 'bar'], ['TYPE', 'script']]
|
|
1663
|
+
*
|
|
1664
|
+
* // Pipe composition for data processing
|
|
1665
|
+
* const processArray = (arr: readonly number[]) => pipe(arr)
|
|
1666
|
+
* .map(Arr.toUpdated(0, x => x * 10)) // Scale first element
|
|
1667
|
+
* .map(Arr.toUpdated(1, x => typeof x === 'number' ? x + 100 : x)) // Add to second if number
|
|
1668
|
+
* .value;
|
|
1669
|
+
*
|
|
1670
|
+
* console.log(processArray([1, 2, 3])); // [10, 102, 3]
|
|
1671
|
+
*
|
|
1672
|
+
* // Multiple sequential updates
|
|
1673
|
+
* const pipeline = (data: readonly number[]) => pipe(data)
|
|
1674
|
+
* .map(Arr.toUpdated(0, x => x * 2))
|
|
1675
|
+
* .map(Arr.toUpdated(1, x => typeof x === 'number' ? x + 10 : x))
|
|
1676
|
+
* .map(Arr.toUpdated(2, x => typeof x === 'number' ? x.toString() : x))
|
|
1677
|
+
* .value;
|
|
1678
|
+
*
|
|
1679
|
+
* console.log(pipeline([1, 2, 3])); // [2, 12, '3'] - readonly (number | string)[]
|
|
1680
|
+
*
|
|
1681
|
+
* // Error-safe updates in data processing
|
|
1682
|
+
* const safeUpdate = <T, U>(
|
|
1683
|
+
* array: readonly T[],
|
|
1684
|
+
* index: number,
|
|
1685
|
+
* updater: (value: T) => U
|
|
1686
|
+
* ) => {
|
|
1687
|
+
* if (index < 0 || index >= array.length) {
|
|
1688
|
+
* console.warn(`Index ${index} out of bounds for array of length ${array.length}`);
|
|
1689
|
+
* return array;
|
|
1690
|
+
* }
|
|
1691
|
+
* return Arr.toUpdated(array, index as SizeType.ArgArrNonNegative, updater);
|
|
1692
|
+
* };
|
|
1693
|
+
*
|
|
1694
|
+
* // Advanced: State management pattern
|
|
1695
|
+
* type AppState = {
|
|
1696
|
+
* users: Array<{id: number, name: string}>;
|
|
1697
|
+
* currentUserId: number;
|
|
1698
|
+
* };
|
|
1699
|
+
*
|
|
1700
|
+
* const updateUserName = (state: AppState, userId: number, newName: string): AppState => {
|
|
1701
|
+
* const userIndex = state.users.findIndex(u => u.id === userId);
|
|
1702
|
+
* if (userIndex === -1) return state;
|
|
1703
|
+
*
|
|
1704
|
+
* return {
|
|
1705
|
+
* ...state,
|
|
1706
|
+
* users: Arr.toUpdated(state.users, userIndex as SizeType.ArgArrNonNegative, user => ({
|
|
1707
|
+
* ...user,
|
|
1708
|
+
* name: newName
|
|
1709
|
+
* }))
|
|
1710
|
+
* };
|
|
1711
|
+
* };
|
|
1712
|
+
*
|
|
1713
|
+
* // Type inference examples showing union types
|
|
1714
|
+
* expectType<typeof doubled, readonly number[]>('='); // Same type
|
|
1715
|
+
* expectType<typeof mixed, readonly (number | string)[]>('='); // Union type
|
|
1716
|
+
* expectType<typeof doubleAtIndex2, <T extends readonly number[]>(array: T) => readonly (number | number)[]>('=');
|
|
1717
|
+
* expectType<typeof safe1, readonly number[]>('='); // Bounds check preserves type
|
|
1718
|
+
* ```
|
|
1719
|
+
*
|
|
1720
|
+
* @see {@link Array.prototype.with} for the native method with different error handling
|
|
1721
|
+
* @see {@link SizeType.ArgArrNonNegative} for the index type constraint
|
|
1722
|
+
* @see Immutable update patterns for functional programming approaches
|
|
1723
|
+
*/
|
|
1724
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1725
|
+
export const toUpdated: ToUpdatedFnOverload = (<E, U>(
|
|
1726
|
+
...args:
|
|
1727
|
+
| readonly [
|
|
1728
|
+
array: readonly E[],
|
|
1729
|
+
index: SizeType.ArgArrNonNegative,
|
|
1730
|
+
updater: (prev: E) => U,
|
|
1731
|
+
]
|
|
1732
|
+
| readonly [index: SizeType.ArgArrNonNegative, updater: (prev: E) => U]
|
|
1733
|
+
) => {
|
|
1734
|
+
switch (args.length) {
|
|
1735
|
+
case 3: {
|
|
1736
|
+
const [array, index, updater] = args;
|
|
1737
|
+
return index < 0 || index >= array.length
|
|
1738
|
+
? array // Return a copy if index is out of bounds
|
|
1739
|
+
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-type-assertion
|
|
1740
|
+
(array as (E | U)[]).with(index, updater(array[index]!));
|
|
1741
|
+
}
|
|
1742
|
+
case 2: {
|
|
1743
|
+
const [index, updater] = args;
|
|
1744
|
+
return (array: readonly E[]) => toUpdated(array, index, updater);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}) as ToUpdatedFnOverload;
|
|
1748
|
+
|
|
1749
|
+
type ToUpdatedFnOverload = {
|
|
1750
|
+
<E, U>(
|
|
1751
|
+
array: readonly E[],
|
|
1752
|
+
index: SizeType.ArgArrNonNegative,
|
|
1753
|
+
updater: (prev: E) => U,
|
|
1754
|
+
): readonly (E | U)[];
|
|
1755
|
+
|
|
1756
|
+
// curried version
|
|
1757
|
+
<E, U>(
|
|
1758
|
+
index: SizeType.ArgArrNonNegative,
|
|
1759
|
+
updater: (prev: E) => U,
|
|
1760
|
+
): (array: readonly E[]) => readonly (E | U)[];
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Returns a new array with a new value inserted at the specified index.
|
|
1765
|
+
* Index can be out of bounds (e.g., negative or greater than length), `toSpliced` handles this.
|
|
1766
|
+
* @template E The type of elements in the array.
|
|
1767
|
+
* @param array The input array.
|
|
1768
|
+
* @param index The index at which to insert the new value.
|
|
1769
|
+
* @param newValue The value to insert.
|
|
1770
|
+
* @returns A new array with the value inserted.
|
|
1771
|
+
* @example
|
|
1772
|
+
* ```ts
|
|
1773
|
+
* // Regular usage
|
|
1774
|
+
* Arr.toInserted([1, 2, 3], 1, 10); // [1, 10, 2, 3]
|
|
1775
|
+
*
|
|
1776
|
+
* // Curried usage for pipe composition
|
|
1777
|
+
* const insertAtStart = Arr.toInserted(0, 99);
|
|
1778
|
+
* const result = pipe([1, 2, 3]).map(insertAtStart).value;
|
|
1779
|
+
* console.log(result); // [99, 1, 2, 3]
|
|
1780
|
+
* ```
|
|
1781
|
+
*/
|
|
1782
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1783
|
+
export const toInserted: ToInsertedFnOverload = (<E, V = E>(
|
|
1784
|
+
...args:
|
|
1785
|
+
| readonly [
|
|
1786
|
+
array: readonly E[],
|
|
1787
|
+
index: SizeType.ArgArrNonNegative,
|
|
1788
|
+
newValue: V,
|
|
1789
|
+
]
|
|
1790
|
+
| readonly [index: SizeType.ArgArrNonNegative, newValue: V]
|
|
1791
|
+
) => {
|
|
1792
|
+
switch (args.length) {
|
|
1793
|
+
case 3: {
|
|
1794
|
+
const [array, index, newValue] = args;
|
|
1795
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1796
|
+
return (array as (E | V)[]).toSpliced(
|
|
1797
|
+
index,
|
|
1798
|
+
0,
|
|
1799
|
+
newValue,
|
|
1800
|
+
) as MutableNonEmptyArray<E | V>;
|
|
1801
|
+
}
|
|
1802
|
+
case 2: {
|
|
1803
|
+
const [index, newValue] = args;
|
|
1804
|
+
return <E2,>(array: readonly (E2 | V)[]) =>
|
|
1805
|
+
toInserted(array, index, newValue);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}) as ToInsertedFnOverload;
|
|
1809
|
+
|
|
1810
|
+
type ToInsertedFnOverload = {
|
|
1811
|
+
<E, const V = E>(
|
|
1812
|
+
array: readonly E[],
|
|
1813
|
+
index: SizeType.ArgArrNonNegative,
|
|
1814
|
+
newValue: V,
|
|
1815
|
+
): NonEmptyArray<E | V>;
|
|
1816
|
+
|
|
1817
|
+
// curried version
|
|
1818
|
+
<E, const V = E>(
|
|
1819
|
+
index: SizeType.ArgArrNonNegative,
|
|
1820
|
+
newValue: V,
|
|
1821
|
+
): (array: readonly E[]) => NonEmptyArray<E | V>;
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Returns a new array with the element at the specified index removed.
|
|
1826
|
+
* If index is out of bounds, `toSpliced` handles this (usually by returning a copy).
|
|
1827
|
+
* @template E The type of elements in the array.
|
|
1828
|
+
* @param array The input array.
|
|
1829
|
+
* @param index The index of the element to remove.
|
|
1830
|
+
* @returns A new array with the element removed.
|
|
1831
|
+
* @example
|
|
1832
|
+
* ```ts
|
|
1833
|
+
* // Regular usage
|
|
1834
|
+
* Arr.toRemoved([1, 2, 3], 1); // [1, 3]
|
|
1835
|
+
*
|
|
1836
|
+
* // Curried usage for pipe composition
|
|
1837
|
+
* const removeFirst = Arr.toRemoved(0);
|
|
1838
|
+
* const result = pipe([10, 20, 30]).map(removeFirst).value;
|
|
1839
|
+
* console.log(result); // [20, 30]
|
|
1840
|
+
* ```
|
|
1841
|
+
*/
|
|
1842
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1843
|
+
export const toRemoved: ToRemovedFnOverload = (<E,>(
|
|
1844
|
+
...args:
|
|
1845
|
+
| readonly [array: readonly E[], index: SizeType.ArgArrNonNegative]
|
|
1846
|
+
| readonly [index: SizeType.ArgArrNonNegative]
|
|
1847
|
+
) => {
|
|
1848
|
+
switch (args.length) {
|
|
1849
|
+
case 2: {
|
|
1850
|
+
const [array, index] = args;
|
|
1851
|
+
return array.toSpliced(index, 1);
|
|
1852
|
+
}
|
|
1853
|
+
case 1: {
|
|
1854
|
+
const [index] = args;
|
|
1855
|
+
return <E2,>(array: readonly E2[]) => toRemoved(array, index);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}) as ToRemovedFnOverload;
|
|
1859
|
+
|
|
1860
|
+
type ToRemovedFnOverload = {
|
|
1861
|
+
<E>(array: readonly E[], index: SizeType.ArgArrNonNegative): readonly E[];
|
|
1862
|
+
|
|
1863
|
+
// curried version
|
|
1864
|
+
(
|
|
1865
|
+
index: SizeType.ArgArrNonNegative,
|
|
1866
|
+
): <E>(array: readonly E[]) => readonly E[];
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Returns a new array with a value added to the end.
|
|
1871
|
+
* @template E The type of the input array (can be a tuple).
|
|
1872
|
+
* @template V The type of the value to add.
|
|
1873
|
+
* @param array The input array.
|
|
1874
|
+
* @param newValue The value to add.
|
|
1875
|
+
* @returns A new array with the value added to the end. Type is `readonly [...E, V]`.
|
|
1876
|
+
* @example
|
|
1877
|
+
* ```ts
|
|
1878
|
+
* // Regular usage
|
|
1879
|
+
* Arr.toPushed([1, 2] as const, 3); // [1, 2, 3]
|
|
1880
|
+
*
|
|
1881
|
+
* // Curried usage for pipe composition
|
|
1882
|
+
* const addZero = Arr.toPushed(0);
|
|
1883
|
+
* const result = pipe([1, 2, 3]).map(addZero).value;
|
|
1884
|
+
* console.log(result); // [1, 2, 3, 0]
|
|
1885
|
+
* ```
|
|
1886
|
+
*/
|
|
1887
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1888
|
+
export const toPushed: ToPushedFnOverload = (<
|
|
1889
|
+
Ar extends readonly unknown[],
|
|
1890
|
+
const V,
|
|
1891
|
+
>(
|
|
1892
|
+
...args: readonly [array: Ar, newValue: V] | readonly [newValue: V]
|
|
1893
|
+
) => {
|
|
1894
|
+
switch (args.length) {
|
|
1895
|
+
case 2: {
|
|
1896
|
+
const [array, newValue] = args;
|
|
1897
|
+
return array.toSpliced(array.length, 0, newValue);
|
|
1898
|
+
}
|
|
1899
|
+
case 1: {
|
|
1900
|
+
const [newValue] = args;
|
|
1901
|
+
return <Ar2 extends readonly unknown[]>(array: Ar2) =>
|
|
1902
|
+
toPushed(array, newValue);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}) as unknown as ToPushedFnOverload;
|
|
1906
|
+
|
|
1907
|
+
type ToPushedFnOverload = {
|
|
1908
|
+
<Ar extends readonly unknown[], const V>(
|
|
1909
|
+
array: Ar,
|
|
1910
|
+
newValue: V,
|
|
1911
|
+
): readonly [...Ar, V];
|
|
1912
|
+
|
|
1913
|
+
// curried version
|
|
1914
|
+
<const V>(
|
|
1915
|
+
newValue: V,
|
|
1916
|
+
): <Ar extends readonly unknown[]>(array: Ar) => readonly [...Ar, V];
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Returns a new array with a value added to the beginning.
|
|
1921
|
+
* @template E The type of the input array (can be a tuple).
|
|
1922
|
+
* @template V The type of the value to add.
|
|
1923
|
+
* @param array The input array.
|
|
1924
|
+
* @param newValue The value to add.
|
|
1925
|
+
* @returns A new array with the value added to the beginning. Type is `readonly [V, ...E]`.
|
|
1926
|
+
* @example
|
|
1927
|
+
* ```ts
|
|
1928
|
+
* // Regular usage
|
|
1929
|
+
* Arr.toUnshifted([1, 2] as const, 0); // [0, 1, 2]
|
|
1930
|
+
*
|
|
1931
|
+
* // Curried usage for pipe composition
|
|
1932
|
+
* const prependZero = Arr.toUnshifted(0);
|
|
1933
|
+
* const result = pipe([1, 2, 3]).map(prependZero).value;
|
|
1934
|
+
* console.log(result); // [0, 1, 2, 3]
|
|
1935
|
+
* ```
|
|
1936
|
+
*/
|
|
1937
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1938
|
+
export const toUnshifted: ToUnshiftedFnOverload = (<
|
|
1939
|
+
Ar extends readonly unknown[],
|
|
1940
|
+
const V,
|
|
1941
|
+
>(
|
|
1942
|
+
...args: readonly [array: Ar, newValue: V] | readonly [newValue: V]
|
|
1943
|
+
) => {
|
|
1944
|
+
switch (args.length) {
|
|
1945
|
+
case 2: {
|
|
1946
|
+
const [array, newValue] = args;
|
|
1947
|
+
return array.toSpliced(0, 0, newValue);
|
|
1948
|
+
}
|
|
1949
|
+
case 1: {
|
|
1950
|
+
const [newValue] = args;
|
|
1951
|
+
return <Ar2 extends readonly unknown[]>(array: Ar2) =>
|
|
1952
|
+
toUnshifted(array, newValue);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}) as unknown as ToUnshiftedFnOverload;
|
|
1956
|
+
|
|
1957
|
+
type ToUnshiftedFnOverload = {
|
|
1958
|
+
<Ar extends readonly unknown[], const V>(
|
|
1959
|
+
array: Ar,
|
|
1960
|
+
newValue: V,
|
|
1961
|
+
): readonly [V, ...Ar];
|
|
1962
|
+
|
|
1963
|
+
// curried version
|
|
1964
|
+
<const V>(
|
|
1965
|
+
newValue: V,
|
|
1966
|
+
): <Ar extends readonly unknown[]>(array: Ar) => readonly [V, ...Ar];
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Fills an array with a value (creates a new filled array).
|
|
1971
|
+
* @param array The array.
|
|
1972
|
+
* @param value The value to fill with.
|
|
1973
|
+
* @param start The start index.
|
|
1974
|
+
* @param end The end index.
|
|
1975
|
+
* @returns A new filled array.
|
|
1976
|
+
* @example
|
|
1977
|
+
* ```typescript
|
|
1978
|
+
* // Regular usage
|
|
1979
|
+
* const arr = [1, 2, 3, 4, 5];
|
|
1980
|
+
* const result = Arr.toFilled(arr, 0, 1, 4);
|
|
1981
|
+
* console.log(result); // [1, 0, 0, 0, 5]
|
|
1982
|
+
*
|
|
1983
|
+
* // Curried usage for pipe composition
|
|
1984
|
+
* const fillWithZeros = Arr.toFilled(0, 1, 3);
|
|
1985
|
+
* const result2 = pipe([1, 2, 3, 4]).map(fillWithZeros).value;
|
|
1986
|
+
* console.log(result2); // [1, 0, 0, 4]
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1990
|
+
export const toFilled: ToFilledFnOverload = (<E,>(
|
|
1991
|
+
...args: readonly [array: readonly E[], value: E] | readonly [value: E]
|
|
1992
|
+
) => {
|
|
1993
|
+
switch (args.length) {
|
|
1994
|
+
case 2: {
|
|
1995
|
+
const [array, value] = args;
|
|
1996
|
+
const cp = castMutable(copy(array));
|
|
1997
|
+
cp.fill(value);
|
|
1998
|
+
return cp;
|
|
1999
|
+
}
|
|
2000
|
+
case 1: {
|
|
2001
|
+
const [value] = args;
|
|
2002
|
+
return (array: readonly E[]) => toFilled(array, value);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}) as ToFilledFnOverload;
|
|
2006
|
+
|
|
2007
|
+
type ToFilledFnOverload = {
|
|
2008
|
+
<E>(array: readonly E[], value: E): readonly E[];
|
|
2009
|
+
|
|
2010
|
+
// curried version
|
|
2011
|
+
<E>(value: E): (array: readonly E[]) => readonly E[];
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2015
|
+
export const toRangeFilled: ToRangeFilledFnOverload = (<E,>(
|
|
2016
|
+
...args:
|
|
2017
|
+
| readonly [
|
|
2018
|
+
array: readonly E[],
|
|
2019
|
+
value: E,
|
|
2020
|
+
fillRange: readonly [start: SizeType.ArgArr, end: SizeType.ArgArr],
|
|
2021
|
+
]
|
|
2022
|
+
| readonly [
|
|
2023
|
+
value: E,
|
|
2024
|
+
fillRange: readonly [start: SizeType.ArgArr, end: SizeType.ArgArr],
|
|
2025
|
+
]
|
|
2026
|
+
) => {
|
|
2027
|
+
switch (args.length) {
|
|
2028
|
+
case 3: {
|
|
2029
|
+
const [array, value, [start, end]] = args;
|
|
2030
|
+
const cp = castMutable(copy(array));
|
|
2031
|
+
cp.fill(value, start, end);
|
|
2032
|
+
return cp;
|
|
2033
|
+
}
|
|
2034
|
+
case 2: {
|
|
2035
|
+
const [value, fillRange] = args;
|
|
2036
|
+
return (array: readonly unknown[]) =>
|
|
2037
|
+
toRangeFilled(array, value, fillRange);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}) as ToRangeFilledFnOverload;
|
|
2041
|
+
|
|
2042
|
+
type ToRangeFilledFnOverload = {
|
|
2043
|
+
<E>(
|
|
2044
|
+
array: readonly E[],
|
|
2045
|
+
value: E,
|
|
2046
|
+
fillRange: readonly [start: SizeType.ArgArr, end: SizeType.ArgArr],
|
|
2047
|
+
): readonly E[];
|
|
2048
|
+
|
|
2049
|
+
// curried version
|
|
2050
|
+
<E>(
|
|
2051
|
+
value: E,
|
|
2052
|
+
fillRange: readonly [start: SizeType.ArgArr, end: SizeType.ArgArr],
|
|
2053
|
+
): (array: readonly E[]) => readonly E[];
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
// searching
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Safely finds the first element in an array that satisfies a predicate function.
|
|
2060
|
+
*
|
|
2061
|
+
* This function provides type-safe searching with no risk of runtime errors. It returns
|
|
2062
|
+
* the first element that matches the predicate wrapped in an Optional, or Optional.None
|
|
2063
|
+
* if no element is found. The predicate receives the element, its index, and the entire array.
|
|
2064
|
+
*
|
|
2065
|
+
* **Curried Usage:** This function supports currying - when called with only a predicate,
|
|
2066
|
+
* it returns a function that can be applied to arrays, making it ideal for functional composition.
|
|
2067
|
+
*
|
|
2068
|
+
* @template E The type of elements in the array.
|
|
2069
|
+
* @param array The array to search through (when using direct call syntax).
|
|
2070
|
+
* @param predicate A function that tests each element. Called with:
|
|
2071
|
+
* - `value`: The current element being tested
|
|
2072
|
+
* - `index`: The index of the current element (branded as `SizeType.Arr`)
|
|
2073
|
+
* - `arr`: The entire array being searched
|
|
2074
|
+
* @returns An `Optional<E>` containing:
|
|
2075
|
+
* - `Optional.Some<E>` with the first matching element if found
|
|
2076
|
+
* - `Optional.None` if no element satisfies the predicate
|
|
2077
|
+
*
|
|
2078
|
+
* @example
|
|
2079
|
+
* ```typescript
|
|
2080
|
+
* // Basic element finding
|
|
2081
|
+
* const numbers = [1, 2, 3, 4, 5];
|
|
2082
|
+
* const firstEven = Arr.find(numbers, x => x % 2 === 0);
|
|
2083
|
+
* if (Optional.isSome(firstEven)) {
|
|
2084
|
+
* console.log(firstEven.value); // 2 - first even number
|
|
2085
|
+
* }
|
|
2086
|
+
*
|
|
2087
|
+
* // Finding with index information
|
|
2088
|
+
* const findLargeAtEnd = Arr.find(numbers, (value, index) => value > 3 && index > 2);
|
|
2089
|
+
* // Optional.Some(4) - first number > 3 after index 2
|
|
2090
|
+
*
|
|
2091
|
+
* // Finding objects by property
|
|
2092
|
+
* const users = [
|
|
2093
|
+
* { id: 1, name: 'Alice', age: 25 },
|
|
2094
|
+
* { id: 2, name: 'Bob', age: 30 },
|
|
2095
|
+
* { id: 3, name: 'Charlie', age: 35 }
|
|
2096
|
+
* ];
|
|
2097
|
+
*
|
|
2098
|
+
* const adult = Arr.find(users, user => user.age >= 30);
|
|
2099
|
+
* // Optional.Some({ id: 2, name: 'Bob', age: 30 })
|
|
2100
|
+
*
|
|
2101
|
+
* const nonExistent = Arr.find(users, user => user.age > 100);
|
|
2102
|
+
* // Optional.None
|
|
2103
|
+
*
|
|
2104
|
+
* // Empty array handling
|
|
2105
|
+
* const emptyResult = Arr.find([], x => x > 0); // Optional.None
|
|
2106
|
+
*
|
|
2107
|
+
* // Curried usage for functional composition
|
|
2108
|
+
* const findNegative = Arr.find((x: number) => x < 0);
|
|
2109
|
+
* const findLongString = Arr.find((s: string) => s.length > 5);
|
|
2110
|
+
*
|
|
2111
|
+
* const datasets = [
|
|
2112
|
+
* [1, 2, -3, 4],
|
|
2113
|
+
* [5, 6, 7, 8],
|
|
2114
|
+
* [-1, 0, 1]
|
|
2115
|
+
* ];
|
|
2116
|
+
*
|
|
2117
|
+
* const negativeNumbers = datasets.map(findNegative);
|
|
2118
|
+
* // [Optional.Some(-3), Optional.None, Optional.Some(-1)]
|
|
2119
|
+
*
|
|
2120
|
+
* // Pipe composition
|
|
2121
|
+
* const result = pipe(['short', 'medium', 'very long string'])
|
|
2122
|
+
* .map(findLongString)
|
|
2123
|
+
* .map(opt => Optional.unwrapOr(opt, 'default'))
|
|
2124
|
+
* .value; // 'very long string'
|
|
2125
|
+
*
|
|
2126
|
+
* // Complex predicate with all parameters
|
|
2127
|
+
* const findSpecial = (arr: readonly number[]) =>
|
|
2128
|
+
* Arr.find(arr, (value, index, array) => {
|
|
2129
|
+
* return value > 10 && index > 0 && index < array.length - 1;
|
|
2130
|
+
* });
|
|
2131
|
+
*
|
|
2132
|
+
* const hasMiddleSpecial = findSpecial([5, 15, 20, 3]);
|
|
2133
|
+
* // Optional.Some(15) - first value > 10 that's not at start or end
|
|
2134
|
+
*
|
|
2135
|
+
* // Safe unwrapping patterns
|
|
2136
|
+
* const maybeFound = Arr.find(numbers, x => x > 10);
|
|
2137
|
+
* const foundOrDefault = Optional.unwrapOr(maybeFound, 0); // 0 (not found)
|
|
2138
|
+
* const foundOrThrow = Optional.isSome(maybeFound)
|
|
2139
|
+
* ? maybeFound.value
|
|
2140
|
+
* : (() => { throw new Error('Not found'); })();
|
|
2141
|
+
*
|
|
2142
|
+
* // Type inference examples
|
|
2143
|
+
* expectType<typeof firstEven, Optional<number>>('=');
|
|
2144
|
+
* expectType<typeof adult, Optional<{id: number, name: string, age: number}>>('=');
|
|
2145
|
+
* expectType<typeof findNegative, (array: readonly number[]) => Optional<number>>('=');
|
|
2146
|
+
* ```
|
|
2147
|
+
*
|
|
2148
|
+
* @see {@link findIndex} for finding the index instead of the element
|
|
2149
|
+
* @see {@link indexOf} for finding elements by equality
|
|
2150
|
+
* @see {@link Optional} for working with the returned Optional values
|
|
2151
|
+
*/
|
|
2152
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2153
|
+
export const find: FindFnOverload = (<E,>(
|
|
2154
|
+
...args:
|
|
2155
|
+
| readonly [
|
|
2156
|
+
array: readonly E[],
|
|
2157
|
+
predicate: (
|
|
2158
|
+
value: E,
|
|
2159
|
+
index: SizeType.Arr,
|
|
2160
|
+
arr: readonly E[],
|
|
2161
|
+
) => boolean,
|
|
2162
|
+
]
|
|
2163
|
+
| readonly [
|
|
2164
|
+
predicate: (
|
|
2165
|
+
value: E,
|
|
2166
|
+
index: SizeType.Arr,
|
|
2167
|
+
arr: readonly E[],
|
|
2168
|
+
) => boolean,
|
|
2169
|
+
]
|
|
2170
|
+
) => {
|
|
2171
|
+
switch (args.length) {
|
|
2172
|
+
case 2: {
|
|
2173
|
+
const [array, predicate] = args;
|
|
2174
|
+
const foundIndex = array.findIndex(
|
|
2175
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2176
|
+
predicate as (value: E, index: number, arr: readonly E[]) => boolean,
|
|
2177
|
+
);
|
|
2178
|
+
|
|
2179
|
+
return foundIndex === -1
|
|
2180
|
+
? Optional.none
|
|
2181
|
+
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2182
|
+
Optional.some(array[foundIndex]!);
|
|
2183
|
+
}
|
|
2184
|
+
case 1: {
|
|
2185
|
+
const [predicate] = args;
|
|
2186
|
+
return (array: readonly E[]) => find(array, predicate);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}) as FindFnOverload;
|
|
2190
|
+
|
|
2191
|
+
type FindFnOverload = {
|
|
2192
|
+
<E>(
|
|
2193
|
+
array: readonly E[],
|
|
2194
|
+
predicate: (value: E, index: SizeType.Arr, arr: readonly E[]) => boolean,
|
|
2195
|
+
): Optional<E>;
|
|
2196
|
+
|
|
2197
|
+
// curried version
|
|
2198
|
+
<E>(
|
|
2199
|
+
predicate: (value: E, index: SizeType.Arr, arr: readonly E[]) => boolean,
|
|
2200
|
+
): (array: readonly E[]) => Optional<E>;
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* Safely finds the index of the first element in an array that satisfies a predicate function.
|
|
2205
|
+
*
|
|
2206
|
+
* This function provides type-safe index searching with no risk of runtime errors. It returns
|
|
2207
|
+
* the index of the first element that matches the predicate wrapped in an Optional, or Optional.None
|
|
2208
|
+
* if no element is found. The returned index is branded as `SizeType.Arr` for type safety.
|
|
2209
|
+
*
|
|
2210
|
+
* **Curried Usage:** This function supports currying - when called with only a predicate,
|
|
2211
|
+
* it returns a function that can be applied to arrays, making it ideal for functional composition.
|
|
2212
|
+
*
|
|
2213
|
+
* @template E The type of elements in the array.
|
|
2214
|
+
* @param array The array to search through (when using direct call syntax).
|
|
2215
|
+
* @param predicate A function that tests each element. Called with:
|
|
2216
|
+
* - `value`: The current element being tested
|
|
2217
|
+
* - `index`: The index of the current element (branded as `SizeType.Arr`)
|
|
2218
|
+
* @returns An `Optional<SizeType.Arr>` containing:
|
|
2219
|
+
* - `Optional.Some<SizeType.Arr>` with the index of the first matching element if found
|
|
2220
|
+
* - `Optional.None` if no element satisfies the predicate
|
|
2221
|
+
*
|
|
2222
|
+
* @example
|
|
2223
|
+
* ```typescript
|
|
2224
|
+
* // Basic index finding
|
|
2225
|
+
* const fruits = ['apple', 'banana', 'cherry', 'banana'];
|
|
2226
|
+
* const bananaIndex = Arr.findIndex(fruits, fruit => fruit === 'banana');
|
|
2227
|
+
* if (Optional.isSome(bananaIndex)) {
|
|
2228
|
+
* console.log(bananaIndex.value); // 1 - index of first 'banana'
|
|
2229
|
+
* }
|
|
2230
|
+
*
|
|
2231
|
+
* // Finding with complex conditions
|
|
2232
|
+
* const numbers = [1, 5, 10, 15, 20];
|
|
2233
|
+
* const firstLargeIndex = Arr.findIndex(numbers, (value, index) =>
|
|
2234
|
+
* value > 10 && index > 1
|
|
2235
|
+
* );
|
|
2236
|
+
* // Optional.Some(3) - index of 15 (first value > 10 after index 1)
|
|
2237
|
+
*
|
|
2238
|
+
* // Finding objects by property
|
|
2239
|
+
* const users = [
|
|
2240
|
+
* { id: 1, active: false },
|
|
2241
|
+
* { id: 2, active: true },
|
|
2242
|
+
* { id: 3, active: true }
|
|
2243
|
+
* ];
|
|
2244
|
+
*
|
|
2245
|
+
* const firstActiveIndex = Arr.findIndex(users, user => user.active);
|
|
2246
|
+
* // Optional.Some(1) - index of first active user
|
|
2247
|
+
*
|
|
2248
|
+
* const inactiveAdminIndex = Arr.findIndex(users, user => !user.active && user.id > 5);
|
|
2249
|
+
* // Optional.None - no inactive user with id > 5
|
|
2250
|
+
*
|
|
2251
|
+
* // Empty array handling
|
|
2252
|
+
* const emptyResult = Arr.findIndex([], x => x > 0); // Optional.None
|
|
2253
|
+
*
|
|
2254
|
+
* // Curried usage for functional composition
|
|
2255
|
+
* const findNegativeIndex = Arr.findIndex((x: number) => x < 0);
|
|
2256
|
+
* const findLongStringIndex = Arr.findIndex((s: string) => s.length > 5);
|
|
2257
|
+
*
|
|
2258
|
+
* const datasets = [
|
|
2259
|
+
* [1, 2, -3, 4], // index 2 has negative
|
|
2260
|
+
* [5, 6, 7, 8], // no negative
|
|
2261
|
+
* [-1, 0, 1] // index 0 has negative
|
|
2262
|
+
* ];
|
|
2263
|
+
*
|
|
2264
|
+
* const negativeIndices = datasets.map(findNegativeIndex);
|
|
2265
|
+
* // [Optional.Some(2), Optional.None, Optional.Some(0)]
|
|
2266
|
+
*
|
|
2267
|
+
* // Using found indices for further operations
|
|
2268
|
+
* const data = ['short', 'medium', 'very long string', 'tiny'];
|
|
2269
|
+
* const longStringIndex = Arr.findIndex(data, s => s.length > 8);
|
|
2270
|
+
*
|
|
2271
|
+
* if (Optional.isSome(longStringIndex)) {
|
|
2272
|
+
* const index = longStringIndex.value;
|
|
2273
|
+
* console.log(`Found at position ${index}: ${data[index]}`);
|
|
2274
|
+
* // "Found at position 2: very long string"
|
|
2275
|
+
* }
|
|
2276
|
+
*
|
|
2277
|
+
* // Pipe composition
|
|
2278
|
+
* const result = pipe(['a', 'bb', 'ccc'])
|
|
2279
|
+
* .map(findLongStringIndex)
|
|
2280
|
+
* .map(opt => Optional.unwrapOr(opt, -1))
|
|
2281
|
+
* .value; // 2 (index of 'ccc')
|
|
2282
|
+
*
|
|
2283
|
+
* // Comparing with native findIndex (which returns -1)
|
|
2284
|
+
* const nativeResult = fruits.findIndex(fruit => fruit === 'grape'); // -1
|
|
2285
|
+
* const safeResult = Arr.findIndex(fruits, fruit => fruit === 'grape'); // Optional.None
|
|
2286
|
+
*
|
|
2287
|
+
* // Safe index usage patterns
|
|
2288
|
+
* const maybeIndex = Arr.findIndex(numbers, x => x > 100);
|
|
2289
|
+
* const indexOrDefault = Optional.unwrapOr(maybeIndex, 0); // 0 (not found)
|
|
2290
|
+
*
|
|
2291
|
+
* // Using index for array access
|
|
2292
|
+
* const foundIndex = Arr.findIndex(fruits, f => f.startsWith('c'));
|
|
2293
|
+
* const foundElement = Optional.isSome(foundIndex)
|
|
2294
|
+
* ? fruits[foundIndex.value]
|
|
2295
|
+
* : 'not found';
|
|
2296
|
+
* // 'cherry'
|
|
2297
|
+
*
|
|
2298
|
+
* // Type inference examples
|
|
2299
|
+
* expectType<typeof bananaIndex, Optional<SizeType.Arr>>('=');
|
|
2300
|
+
* expectType<typeof findNegativeIndex, (array: readonly number[]) => Optional<SizeType.Arr>>('=');
|
|
2301
|
+
* ```
|
|
2302
|
+
*
|
|
2303
|
+
* @see {@link find} for finding the element instead of its index
|
|
2304
|
+
* @see {@link indexOf} for finding elements by equality (not predicate)
|
|
2305
|
+
* @see {@link lastIndexOf} for finding the last occurrence
|
|
2306
|
+
* @see {@link Optional} for working with the returned Optional values
|
|
2307
|
+
*/
|
|
2308
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2309
|
+
export const findIndex: FindIndexFnOverload = (<E,>(
|
|
2310
|
+
...args:
|
|
2311
|
+
| readonly [
|
|
2312
|
+
array: readonly E[],
|
|
2313
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2314
|
+
]
|
|
2315
|
+
| readonly [predicate: (value: E, index: SizeType.Arr) => boolean]
|
|
2316
|
+
) => {
|
|
2317
|
+
switch (args.length) {
|
|
2318
|
+
case 2: {
|
|
2319
|
+
const [array, predicate] = args;
|
|
2320
|
+
return pipe(
|
|
2321
|
+
array.findIndex(
|
|
2322
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2323
|
+
predicate as (value: E, index: number) => boolean,
|
|
2324
|
+
),
|
|
2325
|
+
).map((idx) => (idx >= 0 ? asUint32(idx) : -1)).value;
|
|
2326
|
+
}
|
|
2327
|
+
case 1: {
|
|
2328
|
+
const [predicate] = args;
|
|
2329
|
+
return (array: readonly E[]) => findIndex(array, predicate);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}) as FindIndexFnOverload;
|
|
2333
|
+
|
|
2334
|
+
type FindIndexFnOverload = {
|
|
2335
|
+
<E>(
|
|
2336
|
+
array: readonly E[],
|
|
2337
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2338
|
+
): SizeType.Arr | -1;
|
|
2339
|
+
|
|
2340
|
+
// curried version
|
|
2341
|
+
<E>(
|
|
2342
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2343
|
+
): (array: readonly E[]) => SizeType.Arr | -1;
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* Gets the index of a value in an array.
|
|
2348
|
+
* @param array The array to search.
|
|
2349
|
+
* @param searchElement The element to search for.
|
|
2350
|
+
* @param fromIndex The index to start searching from.
|
|
2351
|
+
* @returns The index if found, -1 otherwise.
|
|
2352
|
+
* @example
|
|
2353
|
+
* ```typescript
|
|
2354
|
+
* // Regular usage
|
|
2355
|
+
* const arr = ['a', 'b', 'c', 'b'];
|
|
2356
|
+
* const result = Arr.indexOf(arr, 'b');
|
|
2357
|
+
* if (Optional.isSome(result)) {
|
|
2358
|
+
* console.log(result.value); // 1 (branded as SizeType.Arr)
|
|
2359
|
+
* }
|
|
2360
|
+
*
|
|
2361
|
+
* // Curried usage for pipe composition
|
|
2362
|
+
* const findB = Arr.indexOf('b');
|
|
2363
|
+
* const result2 = pipe(['a', 'b', 'c']).map(findB).value;
|
|
2364
|
+
* console.log(Optional.unwrapOr(result2, -1)); // 1
|
|
2365
|
+
* ```
|
|
2366
|
+
*/
|
|
2367
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2368
|
+
export const indexOf: IndexOfFnOverload = (<E,>(
|
|
2369
|
+
...args:
|
|
2370
|
+
| readonly [array: readonly E[], searchElement: E]
|
|
2371
|
+
| readonly [searchElement: E]
|
|
2372
|
+
) => {
|
|
2373
|
+
switch (args.length) {
|
|
2374
|
+
case 2: {
|
|
2375
|
+
const [array, searchElement] = args;
|
|
2376
|
+
|
|
2377
|
+
const index = array.indexOf(searchElement);
|
|
2378
|
+
return index >= 0 ? asUint32(index) : -1;
|
|
2379
|
+
}
|
|
2380
|
+
case 1: {
|
|
2381
|
+
const [searchElement] = args;
|
|
2382
|
+
return (array: readonly E[]) => indexOf(array, searchElement);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}) as IndexOfFnOverload;
|
|
2386
|
+
|
|
2387
|
+
type IndexOfFnOverload = {
|
|
2388
|
+
<E>(array: readonly E[], searchElement: E): SizeType.Arr | -1;
|
|
2389
|
+
|
|
2390
|
+
// curried version
|
|
2391
|
+
<E>(searchElement: E): (array: readonly E[]) => SizeType.Arr | -1;
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2395
|
+
export const indexOfFrom: IndexOfFromFnOverload = (<E,>(
|
|
2396
|
+
...args:
|
|
2397
|
+
| readonly [
|
|
2398
|
+
array: readonly E[],
|
|
2399
|
+
searchElement: E,
|
|
2400
|
+
fromIndex: SizeType.ArgArr,
|
|
2401
|
+
]
|
|
2402
|
+
| readonly [searchElement: E, fromIndex: SizeType.ArgArr]
|
|
2403
|
+
) => {
|
|
2404
|
+
switch (args.length) {
|
|
2405
|
+
case 3: {
|
|
2406
|
+
const [array, searchElement, fromIndex] = args;
|
|
2407
|
+
const index = array.indexOf(searchElement, fromIndex);
|
|
2408
|
+
return index >= 0 ? asUint32(index) : -1;
|
|
2409
|
+
}
|
|
2410
|
+
case 2: {
|
|
2411
|
+
const [searchElement, fromIndex] = args;
|
|
2412
|
+
return (array: readonly E[]) =>
|
|
2413
|
+
indexOfFrom(array, searchElement, fromIndex);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}) as IndexOfFromFnOverload;
|
|
2417
|
+
|
|
2418
|
+
type IndexOfFromFnOverload = {
|
|
2419
|
+
<E>(
|
|
2420
|
+
array: readonly E[],
|
|
2421
|
+
searchElement: E,
|
|
2422
|
+
fromIndex: SizeType.ArgArr,
|
|
2423
|
+
): SizeType.Arr | -1;
|
|
2424
|
+
|
|
2425
|
+
// curried version
|
|
2426
|
+
<E>(
|
|
2427
|
+
searchElement: E,
|
|
2428
|
+
fromIndex: SizeType.ArgArr,
|
|
2429
|
+
): (array: readonly E[]) => SizeType.Arr | -1;
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* Gets the last index of a value in an array.
|
|
2434
|
+
* @param array The array to search.
|
|
2435
|
+
* @param searchElement The element to search for.
|
|
2436
|
+
* @param fromIndex The index to start searching from (searches backwards).
|
|
2437
|
+
* @returns Optional.Some with the index if found, Optional.None otherwise.
|
|
2438
|
+
* @example
|
|
2439
|
+
* ```typescript
|
|
2440
|
+
* // Regular usage
|
|
2441
|
+
* const arr = ['a', 'b', 'c', 'b'];
|
|
2442
|
+
* const result = Arr.lastIndexOf(arr, 'b');
|
|
2443
|
+
* if (Optional.isSome(result)) {
|
|
2444
|
+
* console.log(result.value); // 3 (branded as SizeType.Arr)
|
|
2445
|
+
* }
|
|
2446
|
+
*
|
|
2447
|
+
* // Curried usage for pipe composition
|
|
2448
|
+
* const findLastB = Arr.lastIndexOf('b');
|
|
2449
|
+
* const result2 = pipe(['a', 'b', 'c', 'b']).map(findLastB).value;
|
|
2450
|
+
* console.log(Optional.unwrapOr(result2, -1)); // 3
|
|
2451
|
+
* ```
|
|
2452
|
+
*/
|
|
2453
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2454
|
+
export const lastIndexOf: LastIndexOfFnOverload = (<E,>(
|
|
2455
|
+
...args:
|
|
2456
|
+
| readonly [array: readonly E[], searchElement: E]
|
|
2457
|
+
| readonly [searchElement: E]
|
|
2458
|
+
) => {
|
|
2459
|
+
switch (args.length) {
|
|
2460
|
+
case 2: {
|
|
2461
|
+
const [array, searchElement] = args;
|
|
2462
|
+
const index = array.lastIndexOf(searchElement);
|
|
2463
|
+
return index >= 0 ? asUint32(index) : -1;
|
|
2464
|
+
}
|
|
2465
|
+
case 1: {
|
|
2466
|
+
const [searchElement] = args;
|
|
2467
|
+
return (array: readonly E[]) => lastIndexOf(array, searchElement);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}) as LastIndexOfFnOverload;
|
|
2471
|
+
|
|
2472
|
+
type LastIndexOfFnOverload = {
|
|
2473
|
+
<E>(array: readonly E[], searchElement: E): SizeType.Arr | -1;
|
|
2474
|
+
|
|
2475
|
+
// curried version
|
|
2476
|
+
<E>(searchElement: E): (array: readonly E[]) => SizeType.Arr | -1;
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2480
|
+
export const lastIndexOfFrom: LastIndexOfFromFnOverload = (<E,>(
|
|
2481
|
+
...args:
|
|
2482
|
+
| readonly [
|
|
2483
|
+
array: readonly E[],
|
|
2484
|
+
searchElement: E,
|
|
2485
|
+
fromIndex: SizeType.ArgArr,
|
|
2486
|
+
]
|
|
2487
|
+
| readonly [searchElement: E, fromIndex: SizeType.ArgArr]
|
|
2488
|
+
) => {
|
|
2489
|
+
switch (args.length) {
|
|
2490
|
+
case 3: {
|
|
2491
|
+
const [array, searchElement, fromIndex] = args;
|
|
2492
|
+
|
|
2493
|
+
const index = array.lastIndexOf(searchElement, fromIndex);
|
|
2494
|
+
|
|
2495
|
+
return index >= 0 ? asUint32(index) : -1;
|
|
2496
|
+
}
|
|
2497
|
+
case 2: {
|
|
2498
|
+
const [searchElement, fromIndex] = args;
|
|
2499
|
+
return (array: readonly E[]) =>
|
|
2500
|
+
lastIndexOfFrom(array, searchElement, fromIndex);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}) as LastIndexOfFromFnOverload;
|
|
2504
|
+
|
|
2505
|
+
type LastIndexOfFromFnOverload = {
|
|
2506
|
+
<E>(
|
|
2507
|
+
array: readonly E[],
|
|
2508
|
+
searchElement: E,
|
|
2509
|
+
fromIndex: SizeType.ArgArr,
|
|
2510
|
+
): SizeType.Arr | -1;
|
|
2511
|
+
|
|
2512
|
+
// curried version
|
|
2513
|
+
<E>(
|
|
2514
|
+
searchElement: E,
|
|
2515
|
+
fromIndex: SizeType.ArgArr,
|
|
2516
|
+
): (array: readonly E[]) => SizeType.Arr | -1;
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// reducing value
|
|
2520
|
+
|
|
2521
|
+
/**
|
|
2522
|
+
* Applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.
|
|
2523
|
+
* This is an alias for `Array.prototype.reduce`.
|
|
2524
|
+
* @template E The type of elements in the array.
|
|
2525
|
+
* @template S The type of the accumulated value.
|
|
2526
|
+
* @param array The input array.
|
|
2527
|
+
* @param callbackfn A function to execute on each element in the array: `(previousValue: S, currentValue: A, currentIndex: SizeType.Arr) => S`.
|
|
2528
|
+
* @param initialValue The initial value of the accumulator.
|
|
2529
|
+
* @returns The single value that results from the reduction.
|
|
2530
|
+
* @example
|
|
2531
|
+
* ```ts
|
|
2532
|
+
* // Regular usage
|
|
2533
|
+
* Arr.foldl([1, 2, 3], (sum, n) => sum + n, 0); // 6
|
|
2534
|
+
*
|
|
2535
|
+
* // Curried usage for pipe composition
|
|
2536
|
+
* const sumWithZero = Arr.foldl((sum: number, n: number) => sum + n, 0);
|
|
2537
|
+
* const result = pipe([1, 2, 3, 4]).map(sumWithZero).value;
|
|
2538
|
+
* console.log(result); // 10
|
|
2539
|
+
* ```
|
|
2540
|
+
*/
|
|
2541
|
+
export const foldl: FoldlFnOverload = <E, P>(
|
|
2542
|
+
...args:
|
|
2543
|
+
| readonly [
|
|
2544
|
+
array: readonly E[],
|
|
2545
|
+
callbackfn: (
|
|
2546
|
+
previousValue: P,
|
|
2547
|
+
currentValue: E,
|
|
2548
|
+
currentIndex: SizeType.Arr,
|
|
2549
|
+
) => P,
|
|
2550
|
+
initialValue: P,
|
|
2551
|
+
]
|
|
2552
|
+
| readonly [
|
|
2553
|
+
callbackfn: (
|
|
2554
|
+
previousValue: P,
|
|
2555
|
+
currentValue: E,
|
|
2556
|
+
currentIndex: SizeType.Arr,
|
|
2557
|
+
) => P,
|
|
2558
|
+
initialValue: P,
|
|
2559
|
+
]
|
|
2560
|
+
) => {
|
|
2561
|
+
switch (args.length) {
|
|
2562
|
+
case 3: {
|
|
2563
|
+
const [array, callbackfn, initialValue] = args;
|
|
2564
|
+
return array.reduce(
|
|
2565
|
+
(prev, curr, index) => callbackfn(prev, curr, asUint32(index)),
|
|
2566
|
+
initialValue,
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
case 2: {
|
|
2570
|
+
const [callbackfn, initialValue] = args;
|
|
2571
|
+
return (array: readonly E[]) => foldl(array, callbackfn, initialValue);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
|
|
2576
|
+
type FoldlFnOverload = {
|
|
2577
|
+
<E, P>(
|
|
2578
|
+
array: readonly E[],
|
|
2579
|
+
callbackfn: (
|
|
2580
|
+
previousValue: P,
|
|
2581
|
+
currentValue: E,
|
|
2582
|
+
currentIndex: SizeType.Arr,
|
|
2583
|
+
) => P,
|
|
2584
|
+
initialValue: P,
|
|
2585
|
+
): P;
|
|
2586
|
+
|
|
2587
|
+
// curried version
|
|
2588
|
+
<E, P>(
|
|
2589
|
+
callbackfn: (
|
|
2590
|
+
previousValue: P,
|
|
2591
|
+
currentValue: E,
|
|
2592
|
+
currentIndex: SizeType.Arr,
|
|
2593
|
+
) => P,
|
|
2594
|
+
initialValue: P,
|
|
2595
|
+
): (array: readonly E[]) => P;
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2598
|
+
/**
|
|
2599
|
+
* Applies a function against an accumulator and each element in the array (from right to left) to reduce it to a single value.
|
|
2600
|
+
* This is an alias for `Array.prototype.reduceRight`.
|
|
2601
|
+
* @template E The type of elements in the array.
|
|
2602
|
+
* @template S The type of the accumulated value.
|
|
2603
|
+
* @param array The input array.
|
|
2604
|
+
* @param callbackfn A function to execute on each element in the array: `(previousValue: S, currentValue: A, currentIndex: SizeType.Arr) => S`.
|
|
2605
|
+
* @param initialValue The initial value of the accumulator.
|
|
2606
|
+
* @returns The single value that results from the reduction.
|
|
2607
|
+
* @example
|
|
2608
|
+
* ```ts
|
|
2609
|
+
* // Regular usage
|
|
2610
|
+
* Arr.foldr([1, 2, 3], (sum, n) => sum + n, 0); // 6
|
|
2611
|
+
*
|
|
2612
|
+
* // Curried usage for pipe composition
|
|
2613
|
+
* const concatRight = Arr.foldr((acc: string, curr: string) => curr + acc, '');
|
|
2614
|
+
* const result = pipe(['a', 'b', 'c']).map(concatRight).value;
|
|
2615
|
+
* console.log(result); // "abc"
|
|
2616
|
+
* ```
|
|
2617
|
+
*/
|
|
2618
|
+
export const foldr: FoldrFnOverload = <E, P>(
|
|
2619
|
+
...args:
|
|
2620
|
+
| readonly [
|
|
2621
|
+
array: readonly E[],
|
|
2622
|
+
callbackfn: (
|
|
2623
|
+
previousValue: P,
|
|
2624
|
+
currentValue: E,
|
|
2625
|
+
currentIndex: SizeType.Arr,
|
|
2626
|
+
) => P,
|
|
2627
|
+
initialValue: P,
|
|
2628
|
+
]
|
|
2629
|
+
| readonly [
|
|
2630
|
+
callbackfn: (
|
|
2631
|
+
previousValue: P,
|
|
2632
|
+
currentValue: E,
|
|
2633
|
+
currentIndex: SizeType.Arr,
|
|
2634
|
+
) => P,
|
|
2635
|
+
initialValue: P,
|
|
2636
|
+
]
|
|
2637
|
+
) => {
|
|
2638
|
+
switch (args.length) {
|
|
2639
|
+
case 3: {
|
|
2640
|
+
const [array, callbackfn, initialValue] = args;
|
|
2641
|
+
return array.reduceRight(
|
|
2642
|
+
(prev, curr, index) => callbackfn(prev, curr, asUint32(index)),
|
|
2643
|
+
initialValue,
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
case 2: {
|
|
2647
|
+
const [callbackfn, initialValue] = args;
|
|
2648
|
+
return (array: readonly E[]) => foldr(array, callbackfn, initialValue);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
type FoldrFnOverload = {
|
|
2654
|
+
<E, P>(
|
|
2655
|
+
array: readonly E[],
|
|
2656
|
+
callbackfn: (
|
|
2657
|
+
previousValue: P,
|
|
2658
|
+
currentValue: E,
|
|
2659
|
+
currentIndex: SizeType.Arr,
|
|
2660
|
+
) => P,
|
|
2661
|
+
initialValue: P,
|
|
2662
|
+
): P;
|
|
2663
|
+
|
|
2664
|
+
// curried version
|
|
2665
|
+
<E, P>(
|
|
2666
|
+
callbackfn: (
|
|
2667
|
+
previousValue: P,
|
|
2668
|
+
currentValue: E,
|
|
2669
|
+
currentIndex: SizeType.Arr,
|
|
2670
|
+
) => P,
|
|
2671
|
+
initialValue: P,
|
|
2672
|
+
): (array: readonly E[]) => P;
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
/**
|
|
2676
|
+
* Finds the minimum value in an array.
|
|
2677
|
+
* @template E The type of numbers in the array (must extend `number`).
|
|
2678
|
+
* @param array The input array.
|
|
2679
|
+
* @param comparator An optional custom comparator function `(x: N, y: N) => number`. Should return < 0 if x is smaller, 0 if equal, > 0 if x is larger. Defaults to `x - y`.
|
|
2680
|
+
* @returns The minimum value in the array wrapped in Optional.
|
|
2681
|
+
* @example
|
|
2682
|
+
* ```ts
|
|
2683
|
+
* Arr.min([3, 1, 4, 1, 5] as const); // Optional.some(1)
|
|
2684
|
+
* Arr.min([{v:3}, {v:1}], (a,b) => a.v - b.v) // Optional.some({v:1})
|
|
2685
|
+
* Arr.min([]); // Optional.none
|
|
2686
|
+
* ```
|
|
2687
|
+
*/
|
|
2688
|
+
|
|
2689
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2690
|
+
export const min: MinFnOverload = (<E extends number>(
|
|
2691
|
+
array: readonly E[],
|
|
2692
|
+
comparator?: (x: E, y: E) => number,
|
|
2693
|
+
): Optional<E> => {
|
|
2694
|
+
if (!isNonEmpty(array)) {
|
|
2695
|
+
return Optional.none;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
const cmp = comparator ?? ((x, y) => Num.from(x) - Num.from(y));
|
|
2699
|
+
|
|
2700
|
+
return Optional.some(
|
|
2701
|
+
array.reduce(
|
|
2702
|
+
(currentMin, curr) => (cmp(curr, currentMin) < 0 ? curr : currentMin),
|
|
2703
|
+
array[0],
|
|
2704
|
+
),
|
|
2705
|
+
);
|
|
2706
|
+
}) as MinFnOverload;
|
|
2707
|
+
|
|
2708
|
+
type MinFnOverload = {
|
|
2709
|
+
<E extends number>(
|
|
2710
|
+
array: NonEmptyArray<E>,
|
|
2711
|
+
comparator?: (x: E, y: E) => number,
|
|
2712
|
+
): Optional.Some<E>;
|
|
2713
|
+
|
|
2714
|
+
<E extends number>(
|
|
2715
|
+
array: readonly E[],
|
|
2716
|
+
comparator?: (x: E, y: E) => number,
|
|
2717
|
+
): Optional<E>;
|
|
2718
|
+
|
|
2719
|
+
<E>(
|
|
2720
|
+
array: NonEmptyArray<E>,
|
|
2721
|
+
comparator: (x: E, y: E) => number,
|
|
2722
|
+
): Optional.Some<E>;
|
|
2723
|
+
|
|
2724
|
+
<E>(array: readonly E[], comparator: (x: E, y: E) => number): Optional<E>;
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2727
|
+
/**
|
|
2728
|
+
* Finds the maximum value in an array.
|
|
2729
|
+
*
|
|
2730
|
+
* - If the array is non-empty, returns the maximum value.
|
|
2731
|
+
* - If the array is empty, returns `Optional.none`.
|
|
2732
|
+
* - You can provide a custom comparator for arbitrary types.
|
|
2733
|
+
*
|
|
2734
|
+
* @template E The type of elements in the array.
|
|
2735
|
+
* @param array The input array.
|
|
2736
|
+
* @param comparator An optional custom comparator function `(x: A, y: A) => number`. Should return < 0 if x is smaller, 0 if equal, > 0 if x is larger. Defaults to numeric comparison.
|
|
2737
|
+
* @returns The maximum value in the array wrapped in Optional.
|
|
2738
|
+
* @example
|
|
2739
|
+
* ```ts
|
|
2740
|
+
* Arr.max([3, 1, 4, 1, 5] as const); // Optional.some(5)
|
|
2741
|
+
* Arr.max([{v:3}, {v:1}], (a,b) => a.v - b.v) // Optional.some({v:3})
|
|
2742
|
+
* Arr.max([]); // Optional.none
|
|
2743
|
+
* ```
|
|
2744
|
+
*/
|
|
2745
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2746
|
+
export const max: MaxFnOverload = (<E extends number>(
|
|
2747
|
+
array: readonly E[],
|
|
2748
|
+
comparator?: (x: E, y: E) => number,
|
|
2749
|
+
): Optional<E> => {
|
|
2750
|
+
const cmp = comparator ?? ((x, y) => Num.from(x) - Num.from(y));
|
|
2751
|
+
// Find max by finding min with an inverted comparator
|
|
2752
|
+
return min(array, (x, y) => -cmp(x, y));
|
|
2753
|
+
}) as MaxFnOverload;
|
|
2754
|
+
|
|
2755
|
+
type MaxFnOverload = {
|
|
2756
|
+
<E extends number>(
|
|
2757
|
+
array: NonEmptyArray<E>,
|
|
2758
|
+
comparator?: (x: E, y: E) => number,
|
|
2759
|
+
): Optional.Some<E>;
|
|
2760
|
+
|
|
2761
|
+
<E extends number>(
|
|
2762
|
+
array: readonly E[],
|
|
2763
|
+
comparator?: (x: E, y: E) => number,
|
|
2764
|
+
): Optional<E>;
|
|
2765
|
+
|
|
2766
|
+
<E>(
|
|
2767
|
+
array: NonEmptyArray<E>,
|
|
2768
|
+
comparator: (x: E, y: E) => number,
|
|
2769
|
+
): Optional.Some<E>;
|
|
2770
|
+
|
|
2771
|
+
<E>(array: readonly E[], comparator: (x: E, y: E) => number): Optional<E>;
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2774
|
+
/**
|
|
2775
|
+
* Finds the element with the minimum value according to a mapped numeric value.
|
|
2776
|
+
*
|
|
2777
|
+
* - If the array is non-empty, returns the element with the minimum mapped value.
|
|
2778
|
+
* - If the array is empty, returns `Optional.none`.
|
|
2779
|
+
* - You can provide a custom comparator for the mapped values.
|
|
2780
|
+
*
|
|
2781
|
+
* @template E The type of elements in the array.
|
|
2782
|
+
* @template V The type of the value to compare by.
|
|
2783
|
+
* @param array The input array.
|
|
2784
|
+
* @param comparatorValueMapper A function that maps an element to a value for comparison.
|
|
2785
|
+
* @param comparator An optional custom comparator function for the mapped values.
|
|
2786
|
+
* @returns The element with the minimum mapped value wrapped in Optional.
|
|
2787
|
+
* @example
|
|
2788
|
+
* ```ts
|
|
2789
|
+
* const people = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 20 }] as const;
|
|
2790
|
+
* Arr.minBy(people, p => p.age); // Optional.some({ name: 'Bob', age: 20 })
|
|
2791
|
+
* Arr.minBy([], p => p.age); // Optional.none
|
|
2792
|
+
* ```
|
|
2793
|
+
*/
|
|
2794
|
+
|
|
2795
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2796
|
+
export const minBy: MinByFnOverload = (<E, V>(
|
|
2797
|
+
array: readonly E[],
|
|
2798
|
+
comparatorValueMapper: (value: E) => V,
|
|
2799
|
+
comparator?: (x: V, y: V) => number,
|
|
2800
|
+
): Optional<E> =>
|
|
2801
|
+
min(array, (x, y) =>
|
|
2802
|
+
comparator === undefined
|
|
2803
|
+
? Num.from(comparatorValueMapper(x)) -
|
|
2804
|
+
Num.from(comparatorValueMapper(y))
|
|
2805
|
+
: comparator(comparatorValueMapper(x), comparatorValueMapper(y)),
|
|
2806
|
+
)) as MinByFnOverload;
|
|
2807
|
+
|
|
2808
|
+
type MinByFnOverload = {
|
|
2809
|
+
<E>(
|
|
2810
|
+
array: NonEmptyArray<E>,
|
|
2811
|
+
comparatorValueMapper: (value: E) => number,
|
|
2812
|
+
): Optional.Some<E>;
|
|
2813
|
+
|
|
2814
|
+
<E>(
|
|
2815
|
+
array: readonly E[],
|
|
2816
|
+
comparatorValueMapper: (value: E) => number,
|
|
2817
|
+
): Optional<E>;
|
|
2818
|
+
|
|
2819
|
+
<E, V>(
|
|
2820
|
+
array: NonEmptyArray<E>,
|
|
2821
|
+
comparatorValueMapper: (value: E) => V,
|
|
2822
|
+
comparator: (x: V, y: V) => number,
|
|
2823
|
+
): Optional.Some<E>;
|
|
2824
|
+
|
|
2825
|
+
<E, V>(
|
|
2826
|
+
array: readonly E[],
|
|
2827
|
+
comparatorValueMapper: (value: E) => V,
|
|
2828
|
+
comparator: (x: V, y: V) => number,
|
|
2829
|
+
): Optional<E>;
|
|
2830
|
+
};
|
|
2831
|
+
|
|
2832
|
+
/**
|
|
2833
|
+
* Finds the element with the maximum value according to a mapped numeric value.
|
|
2834
|
+
*
|
|
2835
|
+
* - If the array is non-empty, returns the element with the maximum mapped value.
|
|
2836
|
+
* - If the array is empty, returns `Optional.none`.
|
|
2837
|
+
* - You can provide a custom comparator for the mapped values.
|
|
2838
|
+
*
|
|
2839
|
+
* @template E The type of elements in the array.
|
|
2840
|
+
* @template V The type of the value to compare by.
|
|
2841
|
+
* @param array The input array.
|
|
2842
|
+
* @param comparatorValueMapper A function that maps an element to a value for comparison.
|
|
2843
|
+
* @param comparator An optional custom comparator function for the mapped values.
|
|
2844
|
+
* @returns The element with the maximum mapped value wrapped in Optional.
|
|
2845
|
+
* @example
|
|
2846
|
+
* ```ts
|
|
2847
|
+
* const people = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 20 }] as const;
|
|
2848
|
+
* Arr.maxBy(people, p => p.age); // Optional.some({ name: 'Alice', age: 30 })
|
|
2849
|
+
* Arr.maxBy([], p => p.age); // Optional.none
|
|
2850
|
+
* ```
|
|
2851
|
+
*/
|
|
2852
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2853
|
+
export const maxBy: MaxByFnOverload = (<E, V>(
|
|
2854
|
+
array: readonly E[],
|
|
2855
|
+
comparatorValueMapper: (value: E) => V,
|
|
2856
|
+
comparator?: (x: V, y: V) => number,
|
|
2857
|
+
): Optional<E> =>
|
|
2858
|
+
max(array, (x, y) =>
|
|
2859
|
+
comparator === undefined
|
|
2860
|
+
? Num.from(comparatorValueMapper(x)) -
|
|
2861
|
+
Num.from(comparatorValueMapper(y))
|
|
2862
|
+
: comparator(comparatorValueMapper(x), comparatorValueMapper(y)),
|
|
2863
|
+
)) as MaxByFnOverload;
|
|
2864
|
+
|
|
2865
|
+
type MaxByFnOverload = {
|
|
2866
|
+
<E>(
|
|
2867
|
+
array: NonEmptyArray<E>,
|
|
2868
|
+
comparatorValueMapper: (value: E) => number,
|
|
2869
|
+
): Optional.Some<E>;
|
|
2870
|
+
|
|
2871
|
+
<E>(
|
|
2872
|
+
array: readonly E[],
|
|
2873
|
+
comparatorValueMapper: (value: E) => number,
|
|
2874
|
+
): Optional<E>;
|
|
2875
|
+
|
|
2876
|
+
<E, V>(
|
|
2877
|
+
array: NonEmptyArray<E>,
|
|
2878
|
+
comparatorValueMapper: (value: E) => V,
|
|
2879
|
+
comparator: (x: V, y: V) => number,
|
|
2880
|
+
): Optional.Some<E>;
|
|
2881
|
+
|
|
2882
|
+
<E, V>(
|
|
2883
|
+
array: readonly E[],
|
|
2884
|
+
comparatorValueMapper: (value: E) => V,
|
|
2885
|
+
comparator: (x: V, y: V) => number,
|
|
2886
|
+
): Optional<E>;
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
/**
|
|
2890
|
+
* Counts the number of elements in an array that satisfy a predicate.
|
|
2891
|
+
* @template E The type of elements in the array.
|
|
2892
|
+
* @param array The input array.
|
|
2893
|
+
* @param predicate A function `(value: A, index: number) => boolean` to test each element for a condition.
|
|
2894
|
+
* @returns The number of elements that satisfy the predicate.
|
|
2895
|
+
* @example
|
|
2896
|
+
* ```ts
|
|
2897
|
+
* // Regular usage
|
|
2898
|
+
* Arr.count([1, 2, 3, 4], (x) => x > 2); // 2
|
|
2899
|
+
*
|
|
2900
|
+
* // Curried usage for pipe composition
|
|
2901
|
+
* const countEvens = Arr.count((x: number) => x % 2 === 0);
|
|
2902
|
+
* const result = pipe([1, 2, 3, 4, 5, 6]).map(countEvens).value;
|
|
2903
|
+
* console.log(result); // 3
|
|
2904
|
+
* ```
|
|
2905
|
+
*/
|
|
2906
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2907
|
+
export const count: CountFnOverload = (<E,>(
|
|
2908
|
+
...args:
|
|
2909
|
+
| readonly [
|
|
2910
|
+
array: readonly E[],
|
|
2911
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2912
|
+
]
|
|
2913
|
+
| readonly [predicate: (value: E, index: SizeType.Arr) => boolean]
|
|
2914
|
+
) => {
|
|
2915
|
+
switch (args.length) {
|
|
2916
|
+
case 2: {
|
|
2917
|
+
const [array, predicate] = args;
|
|
2918
|
+
return array.reduce<Uint32>(
|
|
2919
|
+
(acc, curr, index) =>
|
|
2920
|
+
predicate(curr, asUint32(index)) ? Uint32.add(acc, 1) : acc,
|
|
2921
|
+
asUint32(0),
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
case 1: {
|
|
2925
|
+
const [predicate] = args;
|
|
2926
|
+
return (array: readonly E[]) => count(array, predicate);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}) as CountFnOverload;
|
|
2930
|
+
|
|
2931
|
+
type CountFnOverload = {
|
|
2932
|
+
<E>(
|
|
2933
|
+
array: readonly E[],
|
|
2934
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2935
|
+
): SizeType.Arr;
|
|
2936
|
+
|
|
2937
|
+
// curried version
|
|
2938
|
+
<E>(
|
|
2939
|
+
predicate: (value: E, index: SizeType.Arr) => boolean,
|
|
2940
|
+
): (array: readonly E[]) => SizeType.Arr;
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
/**
|
|
2944
|
+
* Groups elements of an array by a key derived from each element and counts the elements in each group.
|
|
2945
|
+
* @template E The type of elements in the array.
|
|
2946
|
+
* @template G The type of the group key (must be a primitive type: `string`, `number`, `boolean`, `symbol`, `bigint`, `null`, or `undefined`).
|
|
2947
|
+
* @param array The input array.
|
|
2948
|
+
* @param grouper A function `(value: A, index: number) => G` that maps an element and its index to a group key.
|
|
2949
|
+
* @returns An `IMap` where keys are group keys and values are the counts of elements in each group.
|
|
2950
|
+
* @example
|
|
2951
|
+
* ```ts
|
|
2952
|
+
* // Regular usage
|
|
2953
|
+
* Arr.countBy([1, 2, 2, 3, 1, 1], (x) => x);
|
|
2954
|
+
* // IMap { 1 => 3, 2 => 2, 3 => 1 }
|
|
2955
|
+
*
|
|
2956
|
+
* // Curried usage for pipe composition
|
|
2957
|
+
* const countByType = Arr.countBy((x: {type: string}) => x.type);
|
|
2958
|
+
* const data = [{type: 'a'}, {type: 'b'}, {type: 'a'}];
|
|
2959
|
+
* const result = pipe(data).map(countByType).value;
|
|
2960
|
+
* // IMap { 'a' => 2, 'b' => 1 }
|
|
2961
|
+
* ```
|
|
2962
|
+
*/
|
|
2963
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2964
|
+
export const countBy: CountByFnOverload = (<E, G extends MapSetKeyType>(
|
|
2965
|
+
...args:
|
|
2966
|
+
| readonly [
|
|
2967
|
+
array: readonly E[],
|
|
2968
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
2969
|
+
]
|
|
2970
|
+
| readonly [grouper: (value: E, index: SizeType.Arr) => G]
|
|
2971
|
+
) => {
|
|
2972
|
+
switch (args.length) {
|
|
2973
|
+
case 2: {
|
|
2974
|
+
const [array, grouper] = args;
|
|
2975
|
+
const mut_groups = new Map<MapSetKeyType, SizeType.Arr>();
|
|
2976
|
+
|
|
2977
|
+
for (const [index, e] of array.entries()) {
|
|
2978
|
+
const key = grouper(e, asUint32(index));
|
|
2979
|
+
const curr = mut_groups.get(key) ?? 0;
|
|
2980
|
+
|
|
2981
|
+
mut_groups.set(key, asUint32(curr + 1));
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
return IMap.create(mut_groups);
|
|
2985
|
+
}
|
|
2986
|
+
case 1: {
|
|
2987
|
+
const [grouper] = args;
|
|
2988
|
+
return (array: readonly E[]) => countBy(array, grouper);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}) as CountByFnOverload;
|
|
2992
|
+
|
|
2993
|
+
type CountByFnOverload = {
|
|
2994
|
+
<E, G extends MapSetKeyType>(
|
|
2995
|
+
array: readonly E[],
|
|
2996
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
2997
|
+
): IMap<G, SizeType.Arr>;
|
|
2998
|
+
|
|
2999
|
+
// curried version
|
|
3000
|
+
<E, G extends MapSetKeyType>(
|
|
3001
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
3002
|
+
): (array: readonly E[]) => IMap<G, SizeType.Arr>;
|
|
3003
|
+
};
|
|
3004
|
+
|
|
3005
|
+
/**
|
|
3006
|
+
* Calculates the sum of numbers in an array.
|
|
3007
|
+
* @param array The input array of numbers.
|
|
3008
|
+
* @returns The sum of the numbers. Returns 0 for an empty array.
|
|
3009
|
+
* @example
|
|
3010
|
+
* ```ts
|
|
3011
|
+
* Arr.sum([1, 2, 3]); // 6
|
|
3012
|
+
* Arr.sum([]); // 0
|
|
3013
|
+
* Arr.sum([-1, 0, 1]); // 0
|
|
3014
|
+
* ```
|
|
3015
|
+
*/
|
|
3016
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3017
|
+
export const sum: SumFnOverload = ((array: readonly number[]): number =>
|
|
3018
|
+
array.reduce((prev, curr) => prev + curr, 0)) as SumFnOverload;
|
|
3019
|
+
|
|
3020
|
+
type SumFnOverload = {
|
|
3021
|
+
(array: readonly []): 0;
|
|
3022
|
+
<N extends number>(array: readonly [N]): N;
|
|
3023
|
+
(array: readonly Uint[]): Uint;
|
|
3024
|
+
(array: readonly Int[]): Int;
|
|
3025
|
+
(array: readonly number[]): number;
|
|
3026
|
+
};
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Joins array elements into a string.
|
|
3030
|
+
* @param array The array to join.
|
|
3031
|
+
* @param separator The separator string.
|
|
3032
|
+
* @returns Result.Ok with the joined string, Result.Err if the operation throws.
|
|
3033
|
+
* @example
|
|
3034
|
+
* ```typescript
|
|
3035
|
+
* // Regular usage
|
|
3036
|
+
* const arr = ['Hello', 'World'];
|
|
3037
|
+
* const result = Arr.join(arr, ' ');
|
|
3038
|
+
* if (Result.isOk(result)) {
|
|
3039
|
+
* console.log(result.value); // "Hello World"
|
|
3040
|
+
* }
|
|
3041
|
+
*
|
|
3042
|
+
* // Curried usage for pipe composition
|
|
3043
|
+
* const joinWithComma = Arr.join(',');
|
|
3044
|
+
* const result2 = pipe(['a', 'b', 'c']).map(joinWithComma).value;
|
|
3045
|
+
* console.log(Result.unwrapOr(result2, '')); // "a,b,c"
|
|
3046
|
+
* ```
|
|
3047
|
+
*/
|
|
3048
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3049
|
+
export const join: JoinFnOverload = (<E,>(
|
|
3050
|
+
...args:
|
|
3051
|
+
| readonly [array: readonly E[], separator?: string]
|
|
3052
|
+
| readonly [separator?: string]
|
|
3053
|
+
) => {
|
|
3054
|
+
switch (args.length) {
|
|
3055
|
+
case 0:
|
|
3056
|
+
return <E2,>(array: readonly E2[]) => joinImpl(array, undefined);
|
|
3057
|
+
|
|
3058
|
+
case 1: {
|
|
3059
|
+
const [arg] = args;
|
|
3060
|
+
if (isString(arg) || isUndefined(arg)) {
|
|
3061
|
+
return <E2,>(array: readonly E2[]) => joinImpl(array, arg);
|
|
3062
|
+
}
|
|
3063
|
+
return joinImpl(arg, undefined);
|
|
3064
|
+
}
|
|
3065
|
+
case 2: {
|
|
3066
|
+
const [array, separator] = args;
|
|
3067
|
+
return joinImpl(array, separator);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}) as JoinFnOverload;
|
|
3071
|
+
|
|
3072
|
+
const joinImpl = <E,>(
|
|
3073
|
+
array: readonly E[],
|
|
3074
|
+
separator: string | undefined,
|
|
3075
|
+
): Result<string, Error> => {
|
|
3076
|
+
try {
|
|
3077
|
+
const result = array.join(separator);
|
|
3078
|
+
return Result.ok(result);
|
|
3079
|
+
} catch (error) {
|
|
3080
|
+
return Result.err(
|
|
3081
|
+
error instanceof Error
|
|
3082
|
+
? error
|
|
3083
|
+
: pipe(unknownToString(error))
|
|
3084
|
+
.map(Result.unwrapOkOr('Failed to join array'))
|
|
3085
|
+
.map((e) => new Error(e)).value,
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
|
|
3090
|
+
type JoinFnOverload = {
|
|
3091
|
+
<E>(array: readonly E[], separator?: string): Result<string, Error>;
|
|
3092
|
+
|
|
3093
|
+
// curried version
|
|
3094
|
+
(separator?: string): <E>(array: readonly E[]) => Result<string, Error>;
|
|
3095
|
+
};
|
|
3096
|
+
|
|
3097
|
+
// transformation
|
|
3098
|
+
|
|
3099
|
+
/**
|
|
3100
|
+
* Creates an array of tuples by pairing up corresponding elements from two arrays.
|
|
3101
|
+
* The resulting array has a length equal to the minimum of the two input array lengths.
|
|
3102
|
+
* @template E1 The type of the first array.
|
|
3103
|
+
* @template E2 The type of the second array.
|
|
3104
|
+
* @param array1 The first array.
|
|
3105
|
+
* @param array2 The second array.
|
|
3106
|
+
* @returns An array of tuples where each tuple contains corresponding elements from both arrays.
|
|
3107
|
+
* @example
|
|
3108
|
+
* ```ts
|
|
3109
|
+
* Arr.zip([1, 2, 3] as const, ['a', 'b', 'c'] as const); // [[1, 'a'], [2, 'b'], [3, 'c']]
|
|
3110
|
+
* Arr.zip([1, 2], ['a', 'b', 'c']); // [[1, 'a'], [2, 'b']]
|
|
3111
|
+
* Arr.zip([1, 2, 3], ['a']); // [[1, 'a']]
|
|
3112
|
+
* Arr.zip([], ['a']); // []
|
|
3113
|
+
* ```
|
|
3114
|
+
*/
|
|
3115
|
+
export const zip = <
|
|
3116
|
+
Ar1 extends readonly unknown[],
|
|
3117
|
+
Ar2 extends readonly unknown[],
|
|
3118
|
+
>(
|
|
3119
|
+
array1: Ar1,
|
|
3120
|
+
array2: Ar2,
|
|
3121
|
+
): List.Zip<Ar1, Ar2> =>
|
|
3122
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3123
|
+
seq(Uint32.min(size(array1), size(array2))).map((i) =>
|
|
3124
|
+
// Non-null assertion is safe here because `i` is always within bounds of both arrays up to the length of the shorter one.
|
|
3125
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
3126
|
+
tp(array1[i]!, array2[i]!),
|
|
3127
|
+
) as unknown as List.Zip<Ar1, Ar2>;
|
|
3128
|
+
|
|
3129
|
+
/**
|
|
3130
|
+
* Filters an array by excluding elements for which the predicate returns true.
|
|
3131
|
+
* This is the opposite of `Array.prototype.filter`.
|
|
3132
|
+
* @template E The type of elements in the array.
|
|
3133
|
+
* @param array The input array.
|
|
3134
|
+
* @param predicate A function `(a: A, index: number) => boolean` that returns `true` for elements to be excluded.
|
|
3135
|
+
* @returns A new array with elements for which the predicate returned `false`.
|
|
3136
|
+
* @example
|
|
3137
|
+
* ```ts
|
|
3138
|
+
* // Regular usage
|
|
3139
|
+
* Arr.filterNot([1, 2, 3, 4], (x) => x % 2 === 0); // [1, 3] (excludes even numbers)
|
|
3140
|
+
*
|
|
3141
|
+
* // Curried usage for pipe composition
|
|
3142
|
+
* const excludeEvens = Arr.filterNot((x: number) => x % 2 === 0);
|
|
3143
|
+
* const result = pipe([1, 2, 3, 4, 5, 6]).map(excludeEvens).value;
|
|
3144
|
+
* console.log(result); // [1, 3, 5]
|
|
3145
|
+
* ```
|
|
3146
|
+
*/
|
|
3147
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3148
|
+
export const filterNot: FilterNotFnOverload = (<E,>(
|
|
3149
|
+
...args:
|
|
3150
|
+
| readonly [
|
|
3151
|
+
array: readonly E[],
|
|
3152
|
+
predicate: (a: E, index: SizeType.Arr) => boolean,
|
|
3153
|
+
]
|
|
3154
|
+
| readonly [predicate: (a: E, index: SizeType.Arr) => boolean]
|
|
3155
|
+
) => {
|
|
3156
|
+
switch (args.length) {
|
|
3157
|
+
case 2: {
|
|
3158
|
+
const [array, predicate] = args;
|
|
3159
|
+
return array.filter((a, i) => !predicate(a, asUint32(i)));
|
|
3160
|
+
}
|
|
3161
|
+
case 1: {
|
|
3162
|
+
const [predicate] = args;
|
|
3163
|
+
return (array: readonly E[]) => filterNot(array, predicate);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}) as FilterNotFnOverload;
|
|
3167
|
+
|
|
3168
|
+
type FilterNotFnOverload = {
|
|
3169
|
+
<E>(
|
|
3170
|
+
array: readonly E[],
|
|
3171
|
+
predicate: (a: E, index: SizeType.Arr) => boolean,
|
|
3172
|
+
): readonly E[];
|
|
3173
|
+
|
|
3174
|
+
// curried version
|
|
3175
|
+
<E>(
|
|
3176
|
+
predicate: (a: E, index: SizeType.Arr) => boolean,
|
|
3177
|
+
): (array: readonly E[]) => readonly E[];
|
|
3178
|
+
};
|
|
3179
|
+
|
|
3180
|
+
/**
|
|
3181
|
+
* Concatenates two arrays.
|
|
3182
|
+
* @template E1 The type of the first array (can be a tuple).
|
|
3183
|
+
* @template E2 The type of the second array (can be a tuple).
|
|
3184
|
+
* @param array1 The first array.
|
|
3185
|
+
* @param array2 The second array.
|
|
3186
|
+
* @returns A new array that is the concatenation of the two input arrays. Type is `readonly [...E1, ...E2]`.
|
|
3187
|
+
* @example
|
|
3188
|
+
* ```ts
|
|
3189
|
+
* Arr.concat([1, 2] as const, [3, 4] as const); // [1, 2, 3, 4]
|
|
3190
|
+
* Arr.concat([], [1, 2]); // [1, 2]
|
|
3191
|
+
* Arr.concat([1, 2], []); // [1, 2]
|
|
3192
|
+
* ```
|
|
3193
|
+
*/
|
|
3194
|
+
export const concat = <
|
|
3195
|
+
Ar1 extends readonly unknown[],
|
|
3196
|
+
Ar2 extends readonly unknown[],
|
|
3197
|
+
>(
|
|
3198
|
+
array1: Ar1,
|
|
3199
|
+
array2: Ar2,
|
|
3200
|
+
): readonly [...Ar1, ...Ar2] => [...array1, ...array2];
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Partitions an array into sub-arrays of a specified size.
|
|
3204
|
+
* The last partition may be smaller if the array length is not a multiple of `chunkSize`.
|
|
3205
|
+
* Returns an empty array if chunkSize < 2.
|
|
3206
|
+
* @template N The size of each partition (must be a number type, typically a literal for precise typing).
|
|
3207
|
+
* @template E The type of elements in the array.
|
|
3208
|
+
* @param array The input array.
|
|
3209
|
+
* @param chunkSize The size of each partition.
|
|
3210
|
+
* @returns An array of arrays, where each inner array has up to `chunkSize` elements.
|
|
3211
|
+
* @example
|
|
3212
|
+
* ```ts
|
|
3213
|
+
* // Regular usage
|
|
3214
|
+
* Arr.partition([1, 2, 3, 4, 5, 6], 2); // [[1, 2], [3, 4], [5, 6]]
|
|
3215
|
+
*
|
|
3216
|
+
* // Curried usage for pipe composition
|
|
3217
|
+
* const chunkBy3 = Arr.partition(3);
|
|
3218
|
+
* const result = pipe([1, 2, 3, 4, 5, 6, 7]).map(chunkBy3).value;
|
|
3219
|
+
* console.log(result); // [[1, 2, 3], [4, 5, 6], [7]]
|
|
3220
|
+
* ```
|
|
3221
|
+
*/
|
|
3222
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3223
|
+
export const partition: PartitionFnOverload = (<
|
|
3224
|
+
N extends WithSmallInt<PositiveInt & SizeType.Arr>,
|
|
3225
|
+
E,
|
|
3226
|
+
>(
|
|
3227
|
+
...args:
|
|
3228
|
+
| readonly [array: readonly E[], chunkSize: N]
|
|
3229
|
+
| readonly [chunkSize: N]
|
|
3230
|
+
) => {
|
|
3231
|
+
switch (args.length) {
|
|
3232
|
+
case 2: {
|
|
3233
|
+
const [array, chunkSize] = args;
|
|
3234
|
+
return chunkSize < 2
|
|
3235
|
+
? []
|
|
3236
|
+
: seq(asUint32(Math.ceil(array.length / chunkSize))).map((i) =>
|
|
3237
|
+
array.slice(chunkSize * i, chunkSize * (i + 1)),
|
|
3238
|
+
);
|
|
3239
|
+
}
|
|
3240
|
+
case 1: {
|
|
3241
|
+
const [chunkSize] = args;
|
|
3242
|
+
return (array: readonly E[]) => partition(array, chunkSize);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}) as PartitionFnOverload;
|
|
3246
|
+
|
|
3247
|
+
type PartitionFnOverload = {
|
|
3248
|
+
<N extends WithSmallInt<PositiveInt & SizeType.Arr>, E>(
|
|
3249
|
+
array: readonly E[],
|
|
3250
|
+
chunkSize: N,
|
|
3251
|
+
): readonly (readonly E[])[];
|
|
3252
|
+
|
|
3253
|
+
// curried version
|
|
3254
|
+
<N extends WithSmallInt<PositiveInt & SizeType.Arr>>(
|
|
3255
|
+
chunkSize: N,
|
|
3256
|
+
): <E>(array: readonly E[]) => readonly (readonly E[])[];
|
|
3257
|
+
};
|
|
3258
|
+
|
|
3259
|
+
/**
|
|
3260
|
+
* Sorts an array by a value derived from its elements, using a numeric mapping.
|
|
3261
|
+
* @template E The type of elements in the array.
|
|
3262
|
+
* @param array The input array.
|
|
3263
|
+
* @param comparatorValueMapper A function `(value: A) => number` that maps an element to a number for comparison.
|
|
3264
|
+
* @param comparator An optional custom comparator function `(x: number, y: number) => number` for the mapped numbers. Defaults to ascending sort (x - y).
|
|
3265
|
+
* @returns A new array sorted by the mapped values.
|
|
3266
|
+
* @example
|
|
3267
|
+
* ```ts
|
|
3268
|
+
* const items = [{ name: 'Eve', score: 70 }, { name: 'Adam', score: 90 }, { name: 'Bob', score: 80 }];
|
|
3269
|
+
* Arr.toSortedBy(items, item => item.score);
|
|
3270
|
+
* // [{ name: 'Eve', score: 70 }, { name: 'Bob', score: 80 }, { name: 'Adam', score: 90 }]
|
|
3271
|
+
* Arr.toSortedBy(items, item => item.score, (a, b) => b - a); // Sort descending
|
|
3272
|
+
* // [{ name: 'Adam', score: 90 }, { name: 'Bob', score: 80 }, { name: 'Eve', score: 70 }]
|
|
3273
|
+
* ```
|
|
3274
|
+
*/
|
|
3275
|
+
export const toSortedBy: ToSortedByFnOverload = <E, const V>(
|
|
3276
|
+
array: readonly E[],
|
|
3277
|
+
comparatorValueMapper: (value: E) => V,
|
|
3278
|
+
comparator?: (x: V, y: V) => number,
|
|
3279
|
+
): readonly E[] =>
|
|
3280
|
+
array.toSorted((x, y) =>
|
|
3281
|
+
comparator === undefined
|
|
3282
|
+
? // This branch assumes B is number if comparator is undefined.
|
|
3283
|
+
// The overloads should handle this, but explicit cast might be needed if B is not number.
|
|
3284
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3285
|
+
(comparatorValueMapper(x) as number) -
|
|
3286
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3287
|
+
(comparatorValueMapper(y) as number)
|
|
3288
|
+
: comparator(comparatorValueMapper(x), comparatorValueMapper(y)),
|
|
3289
|
+
);
|
|
3290
|
+
|
|
3291
|
+
type ToSortedByFnOverload = {
|
|
3292
|
+
<E>(
|
|
3293
|
+
array: readonly E[],
|
|
3294
|
+
comparatorValueMapper: (value: E) => number,
|
|
3295
|
+
comparator?: (x: number, y: number) => number,
|
|
3296
|
+
): readonly E[];
|
|
3297
|
+
|
|
3298
|
+
<E, const V>(
|
|
3299
|
+
array: readonly E[],
|
|
3300
|
+
comparatorValueMapper: (value: E) => V,
|
|
3301
|
+
comparator: (x: V, y: V) => number,
|
|
3302
|
+
): readonly E[];
|
|
3303
|
+
};
|
|
3304
|
+
|
|
3305
|
+
/**
|
|
3306
|
+
* Returns an array of successively reduced values from an array, starting with an initial value.
|
|
3307
|
+
*
|
|
3308
|
+
* This function creates a \"running tally\" by applying a reducer function to each element and
|
|
3309
|
+
* accumulating the results. Unlike {@link reduce} which returns a single final value, `scan`
|
|
3310
|
+
* returns all intermediate accumulated values, providing visibility into the reduction process.
|
|
3311
|
+
*
|
|
3312
|
+
* **Key Differences from Reduce:**
|
|
3313
|
+
* - {@link reduce}: `[1, 2, 3] -> 6` (final sum only)
|
|
3314
|
+
* - `scan`: `[1, 2, 3] -> [0, 1, 3, 6]` (all intermediate sums including initial value)
|
|
3315
|
+
*
|
|
3316
|
+
* **Guaranteed Non-Empty Return:** The result is always a {@link NonEmptyArray}<S> because it includes
|
|
3317
|
+
* the initial value as the first element, even for empty input arrays. This provides type safety
|
|
3318
|
+
* and eliminates the need for empty array checks.
|
|
3319
|
+
*
|
|
3320
|
+
* **Array Length Relationship:** `result.length === array.length + 1` (includes initial value)
|
|
3321
|
+
*
|
|
3322
|
+
* **Curried Usage:** Supports currying for functional composition - when called with only the reducer
|
|
3323
|
+
* and initial value, returns a reusable function that can be applied to arrays.
|
|
3324
|
+
*
|
|
3325
|
+
* @template E The type of elements in the input array.
|
|
3326
|
+
* @template S The type of the accumulated values and the initial value.
|
|
3327
|
+
* @param array The input array to scan over. Can be empty (result will still contain the initial value).
|
|
3328
|
+
* @param reducer A function `(accumulator: S, currentValue: E, currentIndex: SizeType.Arr) => S` that:
|
|
3329
|
+
* - **accumulator:** The current accumulated value (starts with `init`, then previous results)
|
|
3330
|
+
* - **currentValue:** The current array element being processed
|
|
3331
|
+
* - **currentIndex:** The 0-based index of the current element (typed as {@link SizeType.Arr})
|
|
3332
|
+
* - **returns:** The new accumulated value to include in the result array
|
|
3333
|
+
* @param init The initial accumulated value. Becomes the first element of the result array.
|
|
3334
|
+
* @returns A {@link NonEmptyArray}<S> of accumulated values with length `array.length + 1`:
|
|
3335
|
+
* - `result[0]` is always the `init` value
|
|
3336
|
+
* - `result[i+1]` is the result of applying the reducer to `result[i]` and `array[i]`
|
|
3337
|
+
* - Guaranteed to be non-empty regardless of input array length
|
|
3338
|
+
*
|
|
3339
|
+
* @example
|
|
3340
|
+
* ```typescript
|
|
3341
|
+
* // Basic running sum example
|
|
3342
|
+
* const numbers = [1, 2, 3, 4];
|
|
3343
|
+
* const runningSum = Arr.scan(numbers, (acc, curr) => acc + curr, 0);
|
|
3344
|
+
* // NonEmptyArray<number> -> [0, 1, 3, 6, 10]
|
|
3345
|
+
* // ^ ^ ^ ^ ^
|
|
3346
|
+
* // | | | | └─ 0+1+2+3+4 = 10
|
|
3347
|
+
* // | | | └─ 0+1+2+3 = 6
|
|
3348
|
+
* // | | └─ 0+1+2 = 3
|
|
3349
|
+
* // | └─ 0+1 = 1
|
|
3350
|
+
* // └─ init = 0
|
|
3351
|
+
*
|
|
3352
|
+
* // Difference from reduce
|
|
3353
|
+
* const reduced = numbers.reduce((acc, curr) => acc + curr, 0); // 10 (final only)
|
|
3354
|
+
* const scanned = Arr.scan(numbers, (acc, curr) => acc + curr, 0); // [0, 1, 3, 6, 10] (all steps)
|
|
3355
|
+
*
|
|
3356
|
+
* // Running product
|
|
3357
|
+
* const factorial = Arr.scan([1, 2, 3, 4, 5], (acc, curr) => acc * curr, 1);
|
|
3358
|
+
* // [1, 1, 2, 6, 24, 120] - factorial sequence
|
|
3359
|
+
*
|
|
3360
|
+
* // Running maximum
|
|
3361
|
+
* const temperatures = [20, 25, 18, 30, 22];
|
|
3362
|
+
* const runningMax = Arr.scan(temperatures, (max, temp) => Math.max(max, temp), -Infinity);
|
|
3363
|
+
* // [-Infinity, 20, 25, 25, 30, 30]
|
|
3364
|
+
*
|
|
3365
|
+
* // Building strings incrementally
|
|
3366
|
+
* const words = ['Hello', 'beautiful', 'world'];
|
|
3367
|
+
* const sentences = Arr.scan(words, (sentence, word) => sentence + ' ' + word, '');
|
|
3368
|
+
* // ['', ' Hello', ' Hello beautiful', ' Hello beautiful world']
|
|
3369
|
+
*
|
|
3370
|
+
* // Array accumulation (collecting elements)
|
|
3371
|
+
* const items = ['a', 'b', 'c'];
|
|
3372
|
+
* const growing = Arr.scan(items, (acc, item) => [...acc, item], [] as string[]);
|
|
3373
|
+
* // [[], ['a'], ['a', 'b'], ['a', 'b', 'c']]
|
|
3374
|
+
*
|
|
3375
|
+
* // Financial running balance
|
|
3376
|
+
* const transactions = [100, -20, 50, -30];
|
|
3377
|
+
* const balances = Arr.scan(transactions, (balance, transaction) => balance + transaction, 1000);
|
|
3378
|
+
* // [1000, 1100, 1080, 1130, 1100] - account balance after each transaction
|
|
3379
|
+
*
|
|
3380
|
+
* // Using index information
|
|
3381
|
+
* const letters = ['a', 'b', 'c'];
|
|
3382
|
+
* const indexed = Arr.scan(letters, (acc, letter, index) => acc + `${index}:${letter} `, '');
|
|
3383
|
+
* // ['', '0:a ', '0:a 1:b ', '0:a 1:b 2:c ']
|
|
3384
|
+
*
|
|
3385
|
+
* // Edge cases
|
|
3386
|
+
* const emptyArray: number[] = [];
|
|
3387
|
+
* const emptyResult = Arr.scan(emptyArray, (acc, curr) => acc + curr, 42);
|
|
3388
|
+
* // [42] - NonEmptyArray even for empty input
|
|
3389
|
+
*
|
|
3390
|
+
* const singleElement = Arr.scan([5], (acc, curr) => acc * curr, 2);
|
|
3391
|
+
* // [2, 10] - init value plus one result
|
|
3392
|
+
*
|
|
3393
|
+
* // Complex object accumulation
|
|
3394
|
+
* const sales = [
|
|
3395
|
+
* { product: 'A', amount: 100 },
|
|
3396
|
+
* { product: 'B', amount: 200 },
|
|
3397
|
+
* { product: 'A', amount: 150 }
|
|
3398
|
+
* ];
|
|
3399
|
+
*
|
|
3400
|
+
* const runningSales = Arr.scan(sales, (totals, sale) => ({
|
|
3401
|
+
* ...totals,
|
|
3402
|
+
* [sale.product]: (totals[sale.product] || 0) + sale.amount
|
|
3403
|
+
* }), {} as Record<string, number>);
|
|
3404
|
+
* // [
|
|
3405
|
+
* // {},
|
|
3406
|
+
* // { A: 100 },
|
|
3407
|
+
* // { A: 100, B: 200 },
|
|
3408
|
+
* // { A: 250, B: 200 }
|
|
3409
|
+
* // ]
|
|
3410
|
+
*
|
|
3411
|
+
* // Curried usage for functional composition
|
|
3412
|
+
* const runningSumFn = Arr.scan((acc: number, curr: number) => acc + curr, 0);
|
|
3413
|
+
* const runningProductFn = Arr.scan((acc: number, curr: number) => acc * curr, 1);
|
|
3414
|
+
* const collectingFn = Arr.scan((acc: string[], curr: string) => [...acc, curr], [] as string[]);
|
|
3415
|
+
*
|
|
3416
|
+
* const datasets = [[1, 2, 3], [4, 5], [6, 7, 8, 9]];
|
|
3417
|
+
* const allSums = datasets.map(runningSumFn);
|
|
3418
|
+
* // [
|
|
3419
|
+
* // [0, 1, 3, 6],
|
|
3420
|
+
* // [0, 4, 9],
|
|
3421
|
+
* // [0, 6, 13, 21, 30]
|
|
3422
|
+
* // ]
|
|
3423
|
+
*
|
|
3424
|
+
* // Pipe composition for data analysis
|
|
3425
|
+
* const analysisResult = pipe([10, 20, 30, 40])
|
|
3426
|
+
* .map(runningSumFn)
|
|
3427
|
+
* .map(sums => sums.slice(1)) // Remove initial value to get pure running sums
|
|
3428
|
+
* .map(sums => sums.map((sum, i) => ({ step: i + 1, total: sum })))
|
|
3429
|
+
* .value;
|
|
3430
|
+
* // [{ step: 1, total: 10 }, { step: 2, total: 30 }, { step: 3, total: 60 }, { step: 4, total: 100 }]
|
|
3431
|
+
*
|
|
3432
|
+
* // Advanced: State machine simulation
|
|
3433
|
+
* type State = 'idle' | 'loading' | 'success' | 'error';
|
|
3434
|
+
* type Event = 'start' | 'complete' | 'fail' | 'reset';
|
|
3435
|
+
*
|
|
3436
|
+
* const events: Event[] = ['start', 'complete', 'reset', 'start', 'fail'];
|
|
3437
|
+
* const stateTransition = (state: State, event: Event): State => {
|
|
3438
|
+
* switch (state) {
|
|
3439
|
+
* case 'idle': return event === 'start' ? 'loading' : state;
|
|
3440
|
+
* case 'loading': return event === 'complete' ? 'success' : event === 'fail' ? 'error' : state;
|
|
3441
|
+
* case 'success': return event === 'reset' ? 'idle' : state;
|
|
3442
|
+
* case 'error': return event === 'reset' ? 'idle' : state;
|
|
3443
|
+
* }
|
|
3444
|
+
* };
|
|
3445
|
+
*
|
|
3446
|
+
* const stateHistory = Arr.scan(events, stateTransition, 'idle' as State);
|
|
3447
|
+
* // ['idle', 'loading', 'success', 'idle', 'loading', 'error']
|
|
3448
|
+
*
|
|
3449
|
+
* // Type inference examples
|
|
3450
|
+
* expectType<typeof runningSum, NonEmptyArray<number>>('=');
|
|
3451
|
+
* expectType<typeof emptyResult, NonEmptyArray<number>>('=');
|
|
3452
|
+
* expectType<typeof runningSumFn, <T extends readonly number[]>(array: T) => NonEmptyArray<number>>('=');
|
|
3453
|
+
* expectType<typeof stateHistory, NonEmptyArray<State>>('=');
|
|
3454
|
+
* ```
|
|
3455
|
+
*
|
|
3456
|
+
* @see {@link reduce} for getting only the final accumulated value
|
|
3457
|
+
* @see {@link NonEmptyArray} for understanding the guaranteed non-empty return type
|
|
3458
|
+
* @see {@link SizeType.Arr} for the index parameter type
|
|
3459
|
+
* @see Array.prototype.reduce for the standard reduce function
|
|
3460
|
+
*/
|
|
3461
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3462
|
+
export const scan: ScanFnOverload = (<E, S>(
|
|
3463
|
+
...args:
|
|
3464
|
+
| readonly [
|
|
3465
|
+
array: readonly E[],
|
|
3466
|
+
reducer: (
|
|
3467
|
+
accumulator: S,
|
|
3468
|
+
currentValue: E,
|
|
3469
|
+
currentIndex: SizeType.Arr,
|
|
3470
|
+
) => S,
|
|
3471
|
+
init: S,
|
|
3472
|
+
]
|
|
3473
|
+
| readonly [
|
|
3474
|
+
reducer: (
|
|
3475
|
+
accumulator: S,
|
|
3476
|
+
currentValue: E,
|
|
3477
|
+
currentIndex: SizeType.Arr,
|
|
3478
|
+
) => S,
|
|
3479
|
+
init: S,
|
|
3480
|
+
]
|
|
3481
|
+
) => {
|
|
3482
|
+
switch (args.length) {
|
|
3483
|
+
case 3: {
|
|
3484
|
+
const [array, reducer, init] = args;
|
|
3485
|
+
const mut_result: MutableNonEmptyArray<S> = castMutable(
|
|
3486
|
+
newArray<S>(asPositiveUint32(array.length + 1), init),
|
|
3487
|
+
);
|
|
3488
|
+
|
|
3489
|
+
let mut_acc = init;
|
|
3490
|
+
|
|
3491
|
+
for (const [index, value] of array.entries()) {
|
|
3492
|
+
mut_acc = reducer(mut_acc, value, asUint32(index));
|
|
3493
|
+
mut_result[index + 1] = mut_acc;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
return mut_result;
|
|
3497
|
+
}
|
|
3498
|
+
case 2: {
|
|
3499
|
+
const [reducer, init] = args;
|
|
3500
|
+
return (array: readonly E[]) => scan(array, reducer, init);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
}) as ScanFnOverload;
|
|
3504
|
+
|
|
3505
|
+
type ScanFnOverload = {
|
|
3506
|
+
<E, S>(
|
|
3507
|
+
array: readonly E[],
|
|
3508
|
+
reducer: (
|
|
3509
|
+
accumulator: S,
|
|
3510
|
+
currentValue: E,
|
|
3511
|
+
currentIndex: SizeType.Arr,
|
|
3512
|
+
) => S,
|
|
3513
|
+
init: S,
|
|
3514
|
+
): NonEmptyArray<S>;
|
|
3515
|
+
|
|
3516
|
+
// curried version
|
|
3517
|
+
<E, S>(
|
|
3518
|
+
reducer: (
|
|
3519
|
+
accumulator: S,
|
|
3520
|
+
currentValue: E,
|
|
3521
|
+
currentIndex: SizeType.Arr,
|
|
3522
|
+
) => S,
|
|
3523
|
+
init: S,
|
|
3524
|
+
): (array: readonly E[]) => NonEmptyArray<S>;
|
|
3525
|
+
};
|
|
3526
|
+
|
|
3527
|
+
/**
|
|
3528
|
+
* Groups elements of an array by a key derived from each element, returning an immutable {@link IMap}.
|
|
3529
|
+
*
|
|
3530
|
+
* This function categorizes array elements into groups based on a computed key, using the efficient
|
|
3531
|
+
* {@link IMap} data structure for the result. The grouper function receives both the element and its
|
|
3532
|
+
* index, enabling flexible grouping strategies.
|
|
3533
|
+
*
|
|
3534
|
+
* **MapSetKeyType Constraint:** The group key type `G` must extend {@link MapSetKeyType}, which includes
|
|
3535
|
+
* primitive types that can be used as Map keys (string, number, boolean, symbol, null, undefined).
|
|
3536
|
+
* This constraint ensures type safety and efficient key-based operations.
|
|
3537
|
+
*
|
|
3538
|
+
* **IMap Return Type:** Returns an {@link IMap}<G, readonly E[]> where:
|
|
3539
|
+
* - Keys are the computed group identifiers of type `G`
|
|
3540
|
+
* - Values are immutable arrays containing all elements that belong to each group
|
|
3541
|
+
* - Preserves insertion order of first occurrence of each group
|
|
3542
|
+
* - Maintains type safety with precise generic types
|
|
3543
|
+
*
|
|
3544
|
+
* **Curried Usage:** Supports currying for functional composition - when called with only the grouper
|
|
3545
|
+
* function, returns a reusable function that can be applied to arrays.
|
|
3546
|
+
*
|
|
3547
|
+
* @template E The type of elements in the input array.
|
|
3548
|
+
* @template G The type of the group key, constrained to {@link MapSetKeyType} (primitives usable as Map keys).
|
|
3549
|
+
* Must be one of: `string | number | boolean | symbol | null | undefined`
|
|
3550
|
+
* @param array The input array to group. Can be empty (returns empty {@link IMap}).
|
|
3551
|
+
* @param grouper A function `(value: E, index: SizeType.Arr) => G` that computes the group key for each element.
|
|
3552
|
+
* - **value:** The current array element
|
|
3553
|
+
* - **index:** The 0-based index of the element (typed as {@link SizeType.Arr})
|
|
3554
|
+
* - **returns:** The group key (must be {@link MapSetKeyType})
|
|
3555
|
+
* @returns An {@link IMap}<G, readonly E[]> where:
|
|
3556
|
+
* - Keys are unique group identifiers computed by the grouper function
|
|
3557
|
+
* - Values are immutable arrays of elements belonging to each group
|
|
3558
|
+
* - Empty groups are not included (only groups with at least one element)
|
|
3559
|
+
* - Insertion order is preserved based on first occurrence of each group key
|
|
3560
|
+
*
|
|
3561
|
+
* @example
|
|
3562
|
+
* ```typescript
|
|
3563
|
+
* // Basic grouping by object property
|
|
3564
|
+
* const products = [
|
|
3565
|
+
* { type: 'fruit', name: 'apple', price: 1.2 },
|
|
3566
|
+
* { type: 'vegetable', name: 'carrot', price: 0.8 },
|
|
3567
|
+
* { type: 'fruit', name: 'banana', price: 0.9 },
|
|
3568
|
+
* { type: 'vegetable', name: 'broccoli', price: 2.1 },
|
|
3569
|
+
* { type: 'fruit', name: 'orange', price: 1.5 }
|
|
3570
|
+
* ];
|
|
3571
|
+
*
|
|
3572
|
+
* const byType = Arr.groupBy(products, item => item.type);
|
|
3573
|
+
* // IMap<string, readonly Product[]> {
|
|
3574
|
+
* // 'fruit' => [
|
|
3575
|
+
* // { type: 'fruit', name: 'apple', price: 1.2 },
|
|
3576
|
+
* // { type: 'fruit', name: 'banana', price: 0.9 },
|
|
3577
|
+
* // { type: 'fruit', name: 'orange', price: 1.5 }
|
|
3578
|
+
* // ],
|
|
3579
|
+
* // 'vegetable' => [
|
|
3580
|
+
* // { type: 'vegetable', name: 'carrot', price: 0.8 },
|
|
3581
|
+
* // { type: 'vegetable', name: 'broccoli', price: 2.1 }
|
|
3582
|
+
* // ]
|
|
3583
|
+
* // }
|
|
3584
|
+
*
|
|
3585
|
+
* // Access grouped results with IMap methods
|
|
3586
|
+
* const fruits = IMap.get(byType, 'fruit'); // Optional<readonly Product[]>
|
|
3587
|
+
* const fruitCount = Optional.map(fruits, arr => arr.length); // Optional<number>
|
|
3588
|
+
* const fruitNames = Optional.map(fruits, arr => arr.map(p => p.name)); // Optional<string[]>
|
|
3589
|
+
*
|
|
3590
|
+
* // Grouping by computed values
|
|
3591
|
+
* const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
3592
|
+
* const byParity = Arr.groupBy(numbers, n => n % 2 === 0 ? 'even' : 'odd');
|
|
3593
|
+
* // IMap<string, readonly number[]> {
|
|
3594
|
+
* // 'odd' => [1, 3, 5, 7, 9],
|
|
3595
|
+
* // 'even' => [2, 4, 6, 8, 10]
|
|
3596
|
+
* // }
|
|
3597
|
+
*
|
|
3598
|
+
* // Grouping by price ranges using index information
|
|
3599
|
+
* const byPriceRange = Arr.groupBy(products, (product, index) => {
|
|
3600
|
+
* const category = product.price < 1.0 ? 'cheap' :
|
|
3601
|
+
* product.price < 2.0 ? 'moderate' : 'expensive';
|
|
3602
|
+
* return `${category}_${index < 2 ? 'early' : 'late'}`;
|
|
3603
|
+
* });
|
|
3604
|
+
*
|
|
3605
|
+
* // MapSetKeyType constraint examples (valid key types)
|
|
3606
|
+
* const byStringKey = Arr.groupBy([1, 2, 3], n => `group_${n}`); // string keys
|
|
3607
|
+
* const byNumberKey = Arr.groupBy(['a', 'b', 'c'], (_, i) => i); // number keys
|
|
3608
|
+
* const byBooleanKey = Arr.groupBy([1, 2, 3, 4], n => n > 2); // boolean keys
|
|
3609
|
+
* const bySymbolKey = Arr.groupBy([1, 2], n => Symbol(n.toString())); // symbol keys
|
|
3610
|
+
*
|
|
3611
|
+
* // Edge cases
|
|
3612
|
+
* const emptyGroup = Arr.groupBy([], x => x); // IMap<never, readonly never[]> (empty)
|
|
3613
|
+
* const singleGroup = Arr.groupBy([1, 2, 3], () => 'all'); // All elements in one group
|
|
3614
|
+
* const uniqueGroups = Arr.groupBy([1, 2, 3], x => x); // Each element in its own group
|
|
3615
|
+
*
|
|
3616
|
+
* // Curried usage for functional composition
|
|
3617
|
+
* const groupByType = Arr.groupBy((item: { type: string }) => item.type);
|
|
3618
|
+
* const groupByLength = Arr.groupBy((str: string) => str.length);
|
|
3619
|
+
* const groupByFirstChar = Arr.groupBy((str: string) => str.charAt(0).toLowerCase());
|
|
3620
|
+
*
|
|
3621
|
+
* const datasets = [
|
|
3622
|
+
* [{ type: 'A' }, { type: 'B' }, { type: 'A' }],
|
|
3623
|
+
* [{ type: 'C' }, { type: 'A' }],
|
|
3624
|
+
* [{ type: 'B' }, { type: 'B' }, { type: 'C' }]
|
|
3625
|
+
* ];
|
|
3626
|
+
* const allGrouped = datasets.map(groupByType);
|
|
3627
|
+
* // Array of IMap instances, each grouped by type
|
|
3628
|
+
*
|
|
3629
|
+
* // Pipe composition for complex data processing
|
|
3630
|
+
* const words = ['apple', 'banana', 'apricot', 'blueberry', 'avocado', 'blackberry'];
|
|
3631
|
+
* const processedGroups = pipe(words)
|
|
3632
|
+
* .map(groupByFirstChar)
|
|
3633
|
+
* .map(groupMap => IMap.map(groupMap, (wordsInGroup, firstLetter) => ({
|
|
3634
|
+
* letter: firstLetter,
|
|
3635
|
+
* count: wordsInGroup.length,
|
|
3636
|
+
* longest: wordsInGroup.reduce((longest, word) =>
|
|
3637
|
+
* word.length > longest.length ? word : longest
|
|
3638
|
+
* )
|
|
3639
|
+
* })))
|
|
3640
|
+
* .value;
|
|
3641
|
+
* // IMap<string, {letter: string, count: number, longest: string}>
|
|
3642
|
+
*
|
|
3643
|
+
* // Advanced: Grouping with complex transformations
|
|
3644
|
+
* const students = [
|
|
3645
|
+
* { name: 'Alice', grade: 85, subject: 'Math' },
|
|
3646
|
+
* { name: 'Bob', grade: 92, subject: 'Science' },
|
|
3647
|
+
* { name: 'Charlie', grade: 78, subject: 'Math' },
|
|
3648
|
+
* { name: 'Diana', grade: 96, subject: 'Science' }
|
|
3649
|
+
* ];
|
|
3650
|
+
*
|
|
3651
|
+
* const byGradeLevel = Arr.groupBy(students, student => {
|
|
3652
|
+
* if (student.grade >= 90) return 'A';
|
|
3653
|
+
* if (student.grade >= 80) return 'B';
|
|
3654
|
+
* return 'C';
|
|
3655
|
+
* });
|
|
3656
|
+
*
|
|
3657
|
+
* // Working with the grouped results
|
|
3658
|
+
* const aStudents = Optional.unwrapOr(IMap.get(byGradeLevel, 'A'), []);
|
|
3659
|
+
* const averageAGrade = aStudents.length > 0
|
|
3660
|
+
* ? aStudents.reduce((sum, s) => sum + s.grade, 0) / aStudents.length
|
|
3661
|
+
* : 0;
|
|
3662
|
+
*
|
|
3663
|
+
* // Type inference examples
|
|
3664
|
+
* expectType<typeof byType, IMap<string, readonly typeof products[number][]>>('=');
|
|
3665
|
+
* expectType<typeof byParity, IMap<string, readonly number[]>>('=');
|
|
3666
|
+
* expectType<typeof groupByType, <T extends {type: string}>(array: readonly T[]) => IMap<string, readonly T[]>>('=');
|
|
3667
|
+
* expectType<typeof emptyGroup, IMap<never, readonly never[]>>('=');
|
|
3668
|
+
* ```
|
|
3669
|
+
*
|
|
3670
|
+
* @see {@link IMap} for working with the returned immutable map
|
|
3671
|
+
* @see {@link MapSetKeyType} for understanding valid key types
|
|
3672
|
+
* @see {@link IMap.get} for safely accessing grouped results
|
|
3673
|
+
* @see {@link IMap.map} for transforming grouped data
|
|
3674
|
+
* @see {@link Optional} for handling potentially missing groups
|
|
3675
|
+
*/
|
|
3676
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3677
|
+
export const groupBy: GroupByFnOverload = (<E, G extends MapSetKeyType>(
|
|
3678
|
+
...args:
|
|
3679
|
+
| readonly [
|
|
3680
|
+
array: readonly E[],
|
|
3681
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
3682
|
+
]
|
|
3683
|
+
| readonly [grouper: (value: E, index: SizeType.Arr) => G]
|
|
3684
|
+
) => {
|
|
3685
|
+
switch (args.length) {
|
|
3686
|
+
case 2: {
|
|
3687
|
+
const [array, grouper] = args;
|
|
3688
|
+
const mut_groups = new Map<G, E[]>(); // Store mutable arrays internally
|
|
3689
|
+
|
|
3690
|
+
for (const [index, e] of array.entries()) {
|
|
3691
|
+
const key = grouper(e, asUint32(index)); // Ensure index is treated as SizeType.Arr
|
|
3692
|
+
const mut_group = mut_groups.get(key);
|
|
3693
|
+
if (mut_group !== undefined) {
|
|
3694
|
+
mut_group.push(e);
|
|
3695
|
+
} else {
|
|
3696
|
+
mut_groups.set(key, [e]);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
// Cast to IMap<G, readonly A[]> for the public interface
|
|
3700
|
+
return IMap.create<G, readonly E[]>(mut_groups);
|
|
3701
|
+
}
|
|
3702
|
+
case 1: {
|
|
3703
|
+
const [grouper] = args;
|
|
3704
|
+
return (array: readonly E[]) => groupBy(array, grouper);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}) as GroupByFnOverload;
|
|
3708
|
+
|
|
3709
|
+
type GroupByFnOverload = {
|
|
3710
|
+
<E, G extends MapSetKeyType>(
|
|
3711
|
+
array: readonly E[],
|
|
3712
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
3713
|
+
): IMap<G, readonly E[]>;
|
|
3714
|
+
|
|
3715
|
+
// curried version
|
|
3716
|
+
<E, G extends MapSetKeyType>(
|
|
3717
|
+
grouper: (value: E, index: SizeType.Arr) => G,
|
|
3718
|
+
): (array: readonly E[]) => IMap<G, readonly E[]>;
|
|
3719
|
+
};
|
|
3720
|
+
|
|
3721
|
+
/**
|
|
3722
|
+
* Creates a new array with unique elements from the input array. Order is preserved from the first occurrence.
|
|
3723
|
+
* Uses `Set` internally for efficient uniqueness checking.
|
|
3724
|
+
* @template P The type of elements in the array.
|
|
3725
|
+
* @param array The input array.
|
|
3726
|
+
* @returns A new array with unique elements from the input array. Returns `[]` for an empty input.
|
|
3727
|
+
* @example
|
|
3728
|
+
* ```ts
|
|
3729
|
+
* Arr.uniq([1, 2, 2, 3, 1, 4]); // [1, 2, 3, 4]
|
|
3730
|
+
* Arr.uniq(['a', 'b', 'a']); // ['a', 'b']
|
|
3731
|
+
* ```
|
|
3732
|
+
*/
|
|
3733
|
+
export const uniq = <P extends Primitive>(
|
|
3734
|
+
array: readonly P[],
|
|
3735
|
+
): readonly P[] => Array.from(new Set(array));
|
|
3736
|
+
|
|
3737
|
+
/**
|
|
3738
|
+
* Creates a new array with unique elements from the input array, based on the values returned by `mapFn`.
|
|
3739
|
+
*
|
|
3740
|
+
* - If the input is a non-empty array, returns a non-empty array.
|
|
3741
|
+
* - Otherwise, returns a readonly array.
|
|
3742
|
+
*
|
|
3743
|
+
* @template E The type of elements in the array.
|
|
3744
|
+
* @template P The type of the mapped value (used for uniqueness comparison).
|
|
3745
|
+
* @param array The input array.
|
|
3746
|
+
* @param mapFn A function `(value: A) => P` to map elements to values for uniqueness comparison.
|
|
3747
|
+
* @returns A new array with unique elements based on the mapped values.
|
|
3748
|
+
* @example
|
|
3749
|
+
* ```ts
|
|
3750
|
+
* const users = [
|
|
3751
|
+
* { id: 1, name: 'Alice' },
|
|
3752
|
+
* { id: 2, name: 'Bob' },
|
|
3753
|
+
* { id: 1, name: 'Alicia' }, // Duplicate id
|
|
3754
|
+
* ];
|
|
3755
|
+
* Arr.uniqBy(users, user => user.id); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
|
|
3756
|
+
* ```
|
|
3757
|
+
*/
|
|
3758
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3759
|
+
export const uniqBy: UniqByFnOverload = (<E, P extends Primitive>(
|
|
3760
|
+
array: readonly E[],
|
|
3761
|
+
mapFn: (value: E) => P,
|
|
3762
|
+
): readonly E[] => {
|
|
3763
|
+
const mut_mappedValues = new Set<P>();
|
|
3764
|
+
|
|
3765
|
+
return array.filter((val) => {
|
|
3766
|
+
const mappedValue = mapFn(val);
|
|
3767
|
+
|
|
3768
|
+
if (mut_mappedValues.has(mappedValue)) return false;
|
|
3769
|
+
mut_mappedValues.add(mappedValue);
|
|
3770
|
+
|
|
3771
|
+
return true;
|
|
3772
|
+
});
|
|
3773
|
+
}) as UniqByFnOverload;
|
|
3774
|
+
|
|
3775
|
+
type UniqByFnOverload = {
|
|
3776
|
+
<E, P extends Primitive>(
|
|
3777
|
+
array: NonEmptyArray<E>,
|
|
3778
|
+
mapFn: (value: E) => P,
|
|
3779
|
+
): NonEmptyArray<E>;
|
|
3780
|
+
|
|
3781
|
+
<E, P extends Primitive>(
|
|
3782
|
+
array: readonly E[],
|
|
3783
|
+
mapFn: (value: E) => P,
|
|
3784
|
+
): readonly E[];
|
|
3785
|
+
};
|
|
3786
|
+
|
|
3787
|
+
// set operations & equality
|
|
3788
|
+
|
|
3789
|
+
/**
|
|
3790
|
+
* Checks if two arrays are equal by performing a shallow comparison of their elements.
|
|
3791
|
+
* @template E The type of elements in the arrays.
|
|
3792
|
+
* @param array1 The first array.
|
|
3793
|
+
* @param array2 The second array.
|
|
3794
|
+
* @param equality An optional function `(a: T, b: T) => boolean` to compare elements. Defaults to `Object.is`.
|
|
3795
|
+
* @returns `true` if the arrays have the same length and all corresponding elements are equal according to the `equality` function, `false` otherwise.
|
|
3796
|
+
* @example
|
|
3797
|
+
* ```ts
|
|
3798
|
+
* Arr.eq([1, 2, 3], [1, 2, 3]); // true
|
|
3799
|
+
* Arr.eq([1, 2, 3], [1, 2, 4]); // false
|
|
3800
|
+
* Arr.eq([1, 2], [1, 2, 3]); // false
|
|
3801
|
+
* Arr.eq([{a:1}], [{a:1}]); // false (different object references)
|
|
3802
|
+
* Arr.eq([{a:1}], [{a:1}], (o1, o2) => o1.a === o2.a); // true
|
|
3803
|
+
* ```
|
|
3804
|
+
*/
|
|
3805
|
+
export const eq = <E,>(
|
|
3806
|
+
array1: readonly E[],
|
|
3807
|
+
array2: readonly E[],
|
|
3808
|
+
equality: (a: E, b: E) => boolean = Object.is,
|
|
3809
|
+
): boolean =>
|
|
3810
|
+
array1.length === array2.length &&
|
|
3811
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
3812
|
+
array1.every((v, i) => equality(v, array2[i]!));
|
|
3813
|
+
|
|
3814
|
+
/**
|
|
3815
|
+
* Alias for `eq`.
|
|
3816
|
+
*/
|
|
3817
|
+
export const equal = eq;
|
|
3818
|
+
|
|
3819
|
+
/**
|
|
3820
|
+
* Checks if the first array (`array1`) is a subset of the second array (`array2`).
|
|
3821
|
+
* An array `A` is a subset of `B` if all elements of `A` are also present in `B`.
|
|
3822
|
+
* Elements must be primitive types for `includes` to work reliably for comparison.
|
|
3823
|
+
* @template E1 The type of elements in the first array (subset candidate), must be a primitive type.
|
|
3824
|
+
* @template E2 The type of elements in the second array (superset candidate), must be a primitive type.
|
|
3825
|
+
* @param array1 The first array.
|
|
3826
|
+
* @param array2 The second array.
|
|
3827
|
+
* @returns `true` if `array1` is a subset of `array2`, `false` otherwise.
|
|
3828
|
+
* @remarks `array1` ⊂ `array2`
|
|
3829
|
+
* @example
|
|
3830
|
+
* ```ts
|
|
3831
|
+
* Arr.isSubset([1, 2], [1, 2, 3]); // true
|
|
3832
|
+
* Arr.isSubset([1, 2, 3], [1, 2]); // false
|
|
3833
|
+
* Arr.isSubset([], [1, 2, 3]); // true
|
|
3834
|
+
* Arr.isSubset([1, 5], [1, 2, 3]); // false
|
|
3835
|
+
* ```
|
|
3836
|
+
*/
|
|
3837
|
+
export const isSubset = <E1 extends Primitive, E2 extends Primitive = E1>(
|
|
3838
|
+
array1: readonly E1[],
|
|
3839
|
+
array2: readonly E2[],
|
|
3840
|
+
): boolean =>
|
|
3841
|
+
array1.every((a) =>
|
|
3842
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3843
|
+
array2.includes(a as E1 & E2),
|
|
3844
|
+
);
|
|
3845
|
+
|
|
3846
|
+
/**
|
|
3847
|
+
* Checks if the first array (`array1`) is a superset of the second array (`array2`).
|
|
3848
|
+
* An array `A` is a superset of `B` if all elements of `B` are also present in `A`.
|
|
3849
|
+
* Elements must be primitive types.
|
|
3850
|
+
* @template E1 The type of elements in the first array (superset candidate), must be a primitive type.
|
|
3851
|
+
* @template E2 The type of elements in the second array (subset candidate), must be a primitive type.
|
|
3852
|
+
* @param array1 The first array.
|
|
3853
|
+
* @param array2 The second array.
|
|
3854
|
+
* @returns `true` if `array1` is a superset of `array2`, `false` otherwise.
|
|
3855
|
+
* @remarks `array1` ⊃ `array2`
|
|
3856
|
+
* @example
|
|
3857
|
+
* ```ts
|
|
3858
|
+
* Arr.isSuperset([1, 2, 3], [1, 2]); // true
|
|
3859
|
+
* Arr.isSuperset([1, 2], [1, 2, 3]); // false
|
|
3860
|
+
* Arr.isSuperset([1, 2, 3], []); // true
|
|
3861
|
+
* ```
|
|
3862
|
+
*/
|
|
3863
|
+
export const isSuperset = <E1 extends Primitive, E2 extends Primitive = E1>(
|
|
3864
|
+
array1: readonly E1[],
|
|
3865
|
+
array2: readonly E2[],
|
|
3866
|
+
): boolean => isSubset(array2, array1);
|
|
3867
|
+
|
|
3868
|
+
/**
|
|
3869
|
+
* Returns the intersection of two arrays of primitive types.
|
|
3870
|
+
* The intersection contains elements that are present in both arrays. Order is based on `array1`.
|
|
3871
|
+
* @template E1 The type of elements in the first array (must be a primitive type).
|
|
3872
|
+
* @template E2 The type of elements in the second array (must be a primitive type).
|
|
3873
|
+
* @param array1 The first array.
|
|
3874
|
+
* @param array2 The second array.
|
|
3875
|
+
* @returns A new array containing elements that are in both `array1` and `array2`.
|
|
3876
|
+
* @example
|
|
3877
|
+
* ```ts
|
|
3878
|
+
* Arr.setIntersection([1, 2, 3], [2, 3, 4]); // [2, 3]
|
|
3879
|
+
* Arr.setIntersection(['a', 'b'], ['b', 'c']); // ['b']
|
|
3880
|
+
* Arr.setIntersection([1, 2], [3, 4]); // []
|
|
3881
|
+
* ```
|
|
3882
|
+
*/
|
|
3883
|
+
export const setIntersection = <
|
|
3884
|
+
E1 extends Primitive,
|
|
3885
|
+
E2 extends Primitive = E1,
|
|
3886
|
+
>(
|
|
3887
|
+
array1: readonly E1[],
|
|
3888
|
+
array2: readonly E2[],
|
|
3889
|
+
): readonly (E1 & E2)[] =>
|
|
3890
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
3891
|
+
array1.filter((e) => array2.includes(e as E1 & E2)) as (E1 & E2)[];
|
|
3892
|
+
|
|
3893
|
+
/**
|
|
3894
|
+
* Returns the set difference of two arrays (`array1` - `array2`).
|
|
3895
|
+
* The difference contains elements that are in `array1` but not in `array2`. Order is based on `array1`.
|
|
3896
|
+
* Elements must be primitive types.
|
|
3897
|
+
* @template E The type of elements in the arrays (must be a primitive type).
|
|
3898
|
+
* @param array1 The first array.
|
|
3899
|
+
* @param array2 The second array.
|
|
3900
|
+
* @returns A new array containing elements from `array1` that are not in `array2`.
|
|
3901
|
+
* @example
|
|
3902
|
+
* ```ts
|
|
3903
|
+
* Arr.setDifference([1, 2, 3], [2, 3, 4]); // [1]
|
|
3904
|
+
* Arr.setDifference([1, 2, 3], [1, 2, 3]); // []
|
|
3905
|
+
* Arr.setDifference([1, 2], [3, 4]); // [1, 2]
|
|
3906
|
+
* ```
|
|
3907
|
+
*/
|
|
3908
|
+
export const setDifference = <E extends Primitive>(
|
|
3909
|
+
array1: readonly E[],
|
|
3910
|
+
array2: readonly E[],
|
|
3911
|
+
): readonly E[] => array1.filter((e) => !array2.includes(e));
|
|
3912
|
+
|
|
3913
|
+
/**
|
|
3914
|
+
* Returns the set difference of two sorted arrays of numbers (`sortedList1` - `sortedList2`).
|
|
3915
|
+
* This operation is more efficient for sorted arrays than the generic `setDifference`.
|
|
3916
|
+
* The resulting array is also sorted.
|
|
3917
|
+
* @template E The type of numbers in the arrays (must extend `number`).
|
|
3918
|
+
* @param sortedList1 The first sorted array of numbers.
|
|
3919
|
+
* @param sortedList2 The second sorted array of numbers.
|
|
3920
|
+
* @returns A new sorted array containing numbers from `sortedList1` that are not in `sortedList2`.
|
|
3921
|
+
* @example
|
|
3922
|
+
* ```ts
|
|
3923
|
+
* Arr.sortedNumSetDifference([1, 2, 3, 5], [2, 4, 5]); // [1, 3]
|
|
3924
|
+
* Arr.sortedNumSetDifference([1, 2, 3], [1, 2, 3]); // []
|
|
3925
|
+
* Arr.sortedNumSetDifference([1, 2], [3, 4]); // [1, 2]
|
|
3926
|
+
* ```
|
|
3927
|
+
*/
|
|
3928
|
+
export const sortedNumSetDifference = <E extends number>(
|
|
3929
|
+
sortedList1: readonly E[],
|
|
3930
|
+
sortedList2: readonly E[],
|
|
3931
|
+
): readonly E[] => {
|
|
3932
|
+
const mut_result: E[] = [];
|
|
3933
|
+
let mut_it1 = 0; // iterator for sortedList1
|
|
3934
|
+
let mut_it2 = 0; // iterator for sortedList2
|
|
3935
|
+
|
|
3936
|
+
while (mut_it1 < sortedList1.length && mut_it2 < sortedList2.length) {
|
|
3937
|
+
// Non-null assertions are safe due to loop condition
|
|
3938
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
3939
|
+
const val1 = sortedList1[mut_it1]!;
|
|
3940
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
3941
|
+
const val2 = sortedList2[mut_it2]!;
|
|
3942
|
+
|
|
3943
|
+
if (val1 === val2) {
|
|
3944
|
+
mut_it1 += 1;
|
|
3945
|
+
mut_it2 += 1;
|
|
3946
|
+
} else if (val1 < val2) {
|
|
3947
|
+
mut_result.push(val1);
|
|
3948
|
+
mut_it1 += 1;
|
|
3949
|
+
} else {
|
|
3950
|
+
// val1 > val2
|
|
3951
|
+
mut_it2 += 1;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
// Add remaining elements from sortedList1
|
|
3955
|
+
for (; mut_it1 < sortedList1.length; mut_it1 += 1) {
|
|
3956
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
3957
|
+
mut_result.push(sortedList1[mut_it1]!);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
return mut_result;
|
|
3961
|
+
};
|
|
3962
|
+
|
|
3963
|
+
// aliases
|
|
3964
|
+
|
|
3965
|
+
/**
|
|
3966
|
+
* Alias for `head`. Returns the first element of an array.
|
|
3967
|
+
* @see {@link head}
|
|
3968
|
+
*/
|
|
3969
|
+
export const first = head;
|
|
3970
|
+
|
|
3971
|
+
/**
|
|
3972
|
+
* Alias for `tail`. Returns all elements of an array except the first one.
|
|
3973
|
+
* @see {@link tail}
|
|
3974
|
+
*/
|
|
3975
|
+
export const rest = tail;
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* Alias for `skip`. Skips the first N elements of an array.
|
|
3979
|
+
* @see {@link skip}
|
|
3980
|
+
*/
|
|
3981
|
+
export const drop = skip;
|
|
3982
|
+
|
|
3983
|
+
/**
|
|
3984
|
+
* Alias for `foldl`. Applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.
|
|
3985
|
+
* @see {@link foldl}
|
|
3986
|
+
*/
|
|
3987
|
+
export const reduce = foldl;
|
|
3988
|
+
|
|
3989
|
+
/**
|
|
3990
|
+
* Alias for `foldr`. Applies a function against an accumulator and each element in the array (from right to left) to reduce it to a single value.
|
|
3991
|
+
* @see {@link foldr}
|
|
3992
|
+
*/
|
|
3993
|
+
export const reduceRight = foldr;
|
|
3994
|
+
|
|
3995
|
+
/**
|
|
3996
|
+
* Alias for `partition`. Splits an array into chunks of a specified size.
|
|
3997
|
+
* @see {@link partition}
|
|
3998
|
+
*/
|
|
3999
|
+
export const chunk = partition;
|
|
4000
|
+
}
|