@mantiq/helpers 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +52 -0
- package/src/Arr.ts +264 -0
- package/src/Collection.ts +804 -0
- package/src/Duration.ts +172 -0
- package/src/Http.ts +573 -0
- package/src/HttpFake.ts +361 -0
- package/src/Num.ts +187 -0
- package/src/Result.ts +196 -0
- package/src/Str.ts +457 -0
- package/src/async.ts +209 -0
- package/src/functions.ts +153 -0
- package/src/index.ts +65 -0
- package/src/is.ts +195 -0
- package/src/match.ts +78 -0
- package/src/objects.ts +180 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chainable collection wrapper — Laravel's Collection with extras.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* collect([1, 2, 3, 4, 5])
|
|
7
|
+
* .filter(n => n > 2)
|
|
8
|
+
* .map(n => n * 10)
|
|
9
|
+
* .toArray() // [30, 40, 50]
|
|
10
|
+
*
|
|
11
|
+
* collect(users)
|
|
12
|
+
* .sortBy('age')
|
|
13
|
+
* .groupBy('role')
|
|
14
|
+
* .toMap()
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type Iteratee<T, R> = ((item: T, index: number) => R) | keyof T
|
|
19
|
+
|
|
20
|
+
function resolveIteratee<T, R>(iteratee: Iteratee<T, R>): (item: T, index: number) => R {
|
|
21
|
+
if (typeof iteratee === 'function') return iteratee as (item: T, index: number) => R
|
|
22
|
+
return (item: T) => (item as any)[iteratee]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Collection<T> implements Iterable<T> {
|
|
26
|
+
protected items: T[]
|
|
27
|
+
|
|
28
|
+
constructor(items: Iterable<T> | T[] = []) {
|
|
29
|
+
this.items = Array.isArray(items) ? [...items] : [...items]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Iterable ──────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
[Symbol.iterator](): Iterator<T> {
|
|
35
|
+
return this.items[Symbol.iterator]()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Access ────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Get all items as a plain array */
|
|
41
|
+
toArray(): T[] { return [...this.items] }
|
|
42
|
+
|
|
43
|
+
/** Get all items as a plain array (alias) */
|
|
44
|
+
all(): T[] { return this.toArray() }
|
|
45
|
+
|
|
46
|
+
/** Number of items */
|
|
47
|
+
count(): number { return this.items.length }
|
|
48
|
+
|
|
49
|
+
/** Alias for count */
|
|
50
|
+
get length(): number { return this.items.length }
|
|
51
|
+
|
|
52
|
+
/** Check if the collection is empty */
|
|
53
|
+
isEmpty(): boolean { return this.items.length === 0 }
|
|
54
|
+
|
|
55
|
+
/** Check if the collection is not empty */
|
|
56
|
+
isNotEmpty(): boolean { return this.items.length > 0 }
|
|
57
|
+
|
|
58
|
+
/** Get item at index */
|
|
59
|
+
get(index: number): T | undefined { return this.items[index] }
|
|
60
|
+
|
|
61
|
+
/** Get the first item, optionally matching a predicate */
|
|
62
|
+
first(predicate?: (item: T, index: number) => boolean): T | undefined {
|
|
63
|
+
if (!predicate) return this.items[0]
|
|
64
|
+
return this.items.find(predicate)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get the first item or throw */
|
|
68
|
+
firstOrFail(predicate?: (item: T, index: number) => boolean): T {
|
|
69
|
+
const item = this.first(predicate)
|
|
70
|
+
if (item === undefined) throw new Error('Item not found')
|
|
71
|
+
return item
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Get the last item, optionally matching a predicate */
|
|
75
|
+
last(predicate?: (item: T, index: number) => boolean): T | undefined {
|
|
76
|
+
if (!predicate) return this.items[this.items.length - 1]
|
|
77
|
+
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
78
|
+
if (predicate(this.items[i]!, i)) return this.items[i]
|
|
79
|
+
}
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Transforms ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/** Map each item */
|
|
86
|
+
map<U>(fn: (item: T, index: number) => U): Collection<U> {
|
|
87
|
+
return new Collection(this.items.map(fn))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Flat-map each item */
|
|
91
|
+
flatMap<U>(fn: (item: T, index: number) => U[]): Collection<U> {
|
|
92
|
+
return new Collection(this.items.flatMap(fn))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Filter items */
|
|
96
|
+
filter(predicate: (item: T, index: number) => boolean): Collection<T> {
|
|
97
|
+
return new Collection(this.items.filter(predicate))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Reject items (inverse of filter) */
|
|
101
|
+
reject(predicate: (item: T, index: number) => boolean): Collection<T> {
|
|
102
|
+
return new Collection(this.items.filter((item, i) => !predicate(item, i)))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reduce to a single value */
|
|
106
|
+
reduce<U>(fn: (acc: U, item: T, index: number) => U, initial: U): U {
|
|
107
|
+
return this.items.reduce(fn, initial)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Take the first N items */
|
|
111
|
+
take(count: number): Collection<T> {
|
|
112
|
+
if (count < 0) return new Collection(this.items.slice(count))
|
|
113
|
+
return new Collection(this.items.slice(0, count))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Take items while predicate is true */
|
|
117
|
+
takeWhile(predicate: (item: T, index: number) => boolean): Collection<T> {
|
|
118
|
+
const result: T[] = []
|
|
119
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
120
|
+
if (!predicate(this.items[i]!, i)) break
|
|
121
|
+
result.push(this.items[i]!)
|
|
122
|
+
}
|
|
123
|
+
return new Collection(result)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Skip the first N items */
|
|
127
|
+
skip(count: number): Collection<T> {
|
|
128
|
+
return new Collection(this.items.slice(count))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Skip items while predicate is true */
|
|
132
|
+
skipWhile(predicate: (item: T, index: number) => boolean): Collection<T> {
|
|
133
|
+
let skipping = true
|
|
134
|
+
const result: T[] = []
|
|
135
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
136
|
+
if (skipping && predicate(this.items[i]!, i)) continue
|
|
137
|
+
skipping = false
|
|
138
|
+
result.push(this.items[i]!)
|
|
139
|
+
}
|
|
140
|
+
return new Collection(result)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Slice the collection */
|
|
144
|
+
slice(start: number, end?: number): Collection<T> {
|
|
145
|
+
return new Collection(this.items.slice(start, end))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Split into chunks of a given size */
|
|
149
|
+
chunk(size: number): Collection<Collection<T>> {
|
|
150
|
+
const chunks: Collection<T>[] = []
|
|
151
|
+
for (let i = 0; i < this.items.length; i += size) {
|
|
152
|
+
chunks.push(new Collection(this.items.slice(i, i + size)))
|
|
153
|
+
}
|
|
154
|
+
return new Collection(chunks)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Split into N groups (round-robin) */
|
|
158
|
+
split(groups: number): Collection<Collection<T>> {
|
|
159
|
+
const result: T[][] = Array.from({ length: Math.min(groups, this.items.length) }, () => [])
|
|
160
|
+
this.items.forEach((item, i) => {
|
|
161
|
+
result[i % result.length]!.push(item)
|
|
162
|
+
})
|
|
163
|
+
return new Collection(result.map((arr) => new Collection(arr)))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Flatten one level */
|
|
167
|
+
flatten<U = T>(): Collection<U> {
|
|
168
|
+
return new Collection(this.items.flat() as unknown as U[])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Get unique values by an optional key */
|
|
172
|
+
unique(key?: Iteratee<T, any>): Collection<T> {
|
|
173
|
+
if (!key) return new Collection([...new Set(this.items)])
|
|
174
|
+
const fn = resolveIteratee(key)
|
|
175
|
+
const seen = new Set()
|
|
176
|
+
const result: T[] = []
|
|
177
|
+
this.items.forEach((item, i) => {
|
|
178
|
+
const k = fn(item, i)
|
|
179
|
+
if (!seen.has(k)) {
|
|
180
|
+
seen.add(k)
|
|
181
|
+
result.push(item)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
return new Collection(result)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reverse the collection */
|
|
188
|
+
reverse(): Collection<T> {
|
|
189
|
+
return new Collection([...this.items].reverse())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Sort items */
|
|
193
|
+
sort(compareFn?: (a: T, b: T) => number): Collection<T> {
|
|
194
|
+
return new Collection([...this.items].sort(compareFn))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Sort by a key or callback */
|
|
198
|
+
sortBy(key: Iteratee<T, any>): Collection<T> {
|
|
199
|
+
const fn = resolveIteratee(key)
|
|
200
|
+
return new Collection([...this.items].sort((a, b) => {
|
|
201
|
+
const va = fn(a, 0)
|
|
202
|
+
const vb = fn(b, 0)
|
|
203
|
+
if (va < vb) return -1
|
|
204
|
+
if (va > vb) return 1
|
|
205
|
+
return 0
|
|
206
|
+
}))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Sort by a key in descending order */
|
|
210
|
+
sortByDesc(key: Iteratee<T, any>): Collection<T> {
|
|
211
|
+
return this.sortBy(key).reverse()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Shuffle items (Fisher-Yates) */
|
|
215
|
+
shuffle(): Collection<T> {
|
|
216
|
+
const result = [...this.items]
|
|
217
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
218
|
+
const j = Math.floor(Math.random() * (i + 1))
|
|
219
|
+
;[result[i], result[j]] = [result[j]!, result[i]!]
|
|
220
|
+
}
|
|
221
|
+
return new Collection(result)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Concatenate another iterable */
|
|
225
|
+
concat(other: Iterable<T>): Collection<T> {
|
|
226
|
+
return new Collection([...this.items, ...other])
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Merge another iterable (alias for concat) */
|
|
230
|
+
merge(other: Iterable<T>): Collection<T> {
|
|
231
|
+
return this.concat(other)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Zip with another array */
|
|
235
|
+
zip<U>(other: U[]): Collection<[T, U]> {
|
|
236
|
+
const len = Math.min(this.items.length, other.length)
|
|
237
|
+
const result: [T, U][] = []
|
|
238
|
+
for (let i = 0; i < len; i++) {
|
|
239
|
+
result.push([this.items[i]!, other[i]!])
|
|
240
|
+
}
|
|
241
|
+
return new Collection(result)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Grouping & Partitioning ───────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/** Group by a key or callback */
|
|
247
|
+
groupBy(key: Iteratee<T, string | number>): Map<string | number, Collection<T>> {
|
|
248
|
+
const fn = resolveIteratee(key)
|
|
249
|
+
const map = new Map<string | number, T[]>()
|
|
250
|
+
this.items.forEach((item, i) => {
|
|
251
|
+
const k = fn(item, i)
|
|
252
|
+
if (!map.has(k)) map.set(k, [])
|
|
253
|
+
map.get(k)!.push(item)
|
|
254
|
+
})
|
|
255
|
+
const result = new Map<string | number, Collection<T>>()
|
|
256
|
+
for (const [k, v] of map) {
|
|
257
|
+
result.set(k, new Collection(v))
|
|
258
|
+
}
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Key by a field or callback (last wins) */
|
|
263
|
+
keyBy(key: Iteratee<T, string | number>): Map<string | number, T> {
|
|
264
|
+
const fn = resolveIteratee(key)
|
|
265
|
+
const map = new Map<string | number, T>()
|
|
266
|
+
this.items.forEach((item, i) => {
|
|
267
|
+
map.set(fn(item, i), item)
|
|
268
|
+
})
|
|
269
|
+
return map
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Partition into [pass, fail] based on a predicate */
|
|
273
|
+
partition(predicate: (item: T, index: number) => boolean): [Collection<T>, Collection<T>] {
|
|
274
|
+
const pass: T[] = []
|
|
275
|
+
const fail: T[] = []
|
|
276
|
+
this.items.forEach((item, i) => {
|
|
277
|
+
if (predicate(item, i)) pass.push(item)
|
|
278
|
+
else fail.push(item)
|
|
279
|
+
})
|
|
280
|
+
return [new Collection(pass), new Collection(fail)]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Count by a key or callback */
|
|
284
|
+
countBy(key: Iteratee<T, string | number>): Map<string | number, number> {
|
|
285
|
+
const fn = resolveIteratee(key)
|
|
286
|
+
const map = new Map<string | number, number>()
|
|
287
|
+
this.items.forEach((item, i) => {
|
|
288
|
+
const k = fn(item, i)
|
|
289
|
+
map.set(k, (map.get(k) ?? 0) + 1)
|
|
290
|
+
})
|
|
291
|
+
return map
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Aggregation ───────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/** Sum of values (or extracted values) */
|
|
297
|
+
sum(key?: Iteratee<T, number>): number {
|
|
298
|
+
if (!key) return (this.items as unknown as number[]).reduce((a, b) => a + b, 0)
|
|
299
|
+
const fn = resolveIteratee(key)
|
|
300
|
+
return this.items.reduce((acc, item, i) => acc + fn(item, i), 0)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Average of values */
|
|
304
|
+
avg(key?: Iteratee<T, number>): number {
|
|
305
|
+
if (this.items.length === 0) return 0
|
|
306
|
+
return this.sum(key) / this.items.length
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Minimum value */
|
|
310
|
+
min(key?: Iteratee<T, number>): number {
|
|
311
|
+
if (!key) return Math.min(...(this.items as unknown as number[]))
|
|
312
|
+
const fn = resolveIteratee(key)
|
|
313
|
+
return Math.min(...this.items.map((item, i) => fn(item, i)))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Maximum value */
|
|
317
|
+
max(key?: Iteratee<T, number>): number {
|
|
318
|
+
if (!key) return Math.max(...(this.items as unknown as number[]))
|
|
319
|
+
const fn = resolveIteratee(key)
|
|
320
|
+
return Math.max(...this.items.map((item, i) => fn(item, i)))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Median value */
|
|
324
|
+
median(key?: Iteratee<T, number>): number {
|
|
325
|
+
if (this.items.length === 0) return 0
|
|
326
|
+
const fn = key ? resolveIteratee(key) : (item: T) => item as unknown as number
|
|
327
|
+
const sorted = this.items.map((item, i) => fn(item, i)).sort((a, b) => a - b)
|
|
328
|
+
const mid = Math.floor(sorted.length / 2)
|
|
329
|
+
return sorted.length % 2 !== 0
|
|
330
|
+
? sorted[mid]!
|
|
331
|
+
: (sorted[mid - 1]! + sorted[mid]!) / 2
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Pluck & Extract ───────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/** Extract a single property from each item */
|
|
337
|
+
pluck<K extends keyof T>(key: K): Collection<T[K]> {
|
|
338
|
+
return new Collection(this.items.map((item) => item[key]))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Pick specific keys from each object */
|
|
342
|
+
only<K extends keyof T>(...keys: K[]): Collection<Pick<T, K>> {
|
|
343
|
+
return new Collection(this.items.map((item) => {
|
|
344
|
+
const result: any = {}
|
|
345
|
+
for (const key of keys) {
|
|
346
|
+
if (key in (item as any)) result[key] = item[key]
|
|
347
|
+
}
|
|
348
|
+
return result
|
|
349
|
+
}))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Omit specific keys from each object */
|
|
353
|
+
except<K extends keyof T>(...keys: K[]): Collection<Omit<T, K>> {
|
|
354
|
+
const excluded = new Set(keys as unknown as string[])
|
|
355
|
+
return new Collection(this.items.map((item) => {
|
|
356
|
+
const result: any = {}
|
|
357
|
+
for (const key of Object.keys(item as any)) {
|
|
358
|
+
if (!excluded.has(key)) result[key] = (item as any)[key]
|
|
359
|
+
}
|
|
360
|
+
return result
|
|
361
|
+
}))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Predicates ────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/** Check if every item matches */
|
|
367
|
+
every(predicate: (item: T, index: number) => boolean): boolean {
|
|
368
|
+
return this.items.every(predicate)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Check if any item matches */
|
|
372
|
+
some(predicate: (item: T, index: number) => boolean): boolean {
|
|
373
|
+
return this.items.some(predicate)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Check if the collection contains a value */
|
|
377
|
+
contains(value: T): boolean
|
|
378
|
+
contains(predicate: (item: T) => boolean): boolean
|
|
379
|
+
contains(valueOrPredicate: T | ((item: T) => boolean)): boolean {
|
|
380
|
+
if (typeof valueOrPredicate === 'function') {
|
|
381
|
+
return this.items.some(valueOrPredicate as (item: T) => boolean)
|
|
382
|
+
}
|
|
383
|
+
return this.items.includes(valueOrPredicate)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Find the index of the first match */
|
|
387
|
+
search(value: T): number
|
|
388
|
+
search(predicate: (item: T) => boolean): number
|
|
389
|
+
search(valueOrPredicate: T | ((item: T) => boolean)): number {
|
|
390
|
+
if (typeof valueOrPredicate === 'function') {
|
|
391
|
+
return this.items.findIndex(valueOrPredicate as (item: T) => boolean)
|
|
392
|
+
}
|
|
393
|
+
return this.items.indexOf(valueOrPredicate)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Side effects ──────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/** Run a callback for each item */
|
|
399
|
+
each(fn: (item: T, index: number) => void): this {
|
|
400
|
+
this.items.forEach(fn)
|
|
401
|
+
return this
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Tap into the collection (for debugging) */
|
|
405
|
+
tap(fn: (collection: this) => void): this {
|
|
406
|
+
fn(this)
|
|
407
|
+
return this
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Pipe the collection through a function */
|
|
411
|
+
pipe<U>(fn: (collection: this) => U): U {
|
|
412
|
+
return fn(this)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Conditionals ──────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
/** Apply a callback only when condition is true */
|
|
418
|
+
when(condition: boolean, fn: (collection: Collection<T>) => Collection<T>): Collection<T> {
|
|
419
|
+
return condition ? fn(this) : this
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Apply a callback unless condition is true */
|
|
423
|
+
unless(condition: boolean, fn: (collection: Collection<T>) => Collection<T>): Collection<T> {
|
|
424
|
+
return condition ? this : fn(this)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Conversion ────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
/** Convert to a Map keyed by a field */
|
|
430
|
+
toMap<K extends keyof T>(key: K): Map<T[K], T> {
|
|
431
|
+
const map = new Map<T[K], T>()
|
|
432
|
+
for (const item of this.items) {
|
|
433
|
+
map.set(item[key], item)
|
|
434
|
+
}
|
|
435
|
+
return map
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Convert to a plain object keyed by a field */
|
|
439
|
+
toObject<K extends keyof T>(key: K): Record<string, T> {
|
|
440
|
+
const obj: Record<string, T> = {}
|
|
441
|
+
for (const item of this.items) {
|
|
442
|
+
obj[String(item[key])] = item
|
|
443
|
+
}
|
|
444
|
+
return obj
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Convert to a Set */
|
|
448
|
+
toSet(): Set<T> {
|
|
449
|
+
return new Set(this.items)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Convert to JSON string */
|
|
453
|
+
toJSON(): T[] {
|
|
454
|
+
return this.toArray()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Join items into a string */
|
|
458
|
+
join(separator = ', '): string {
|
|
459
|
+
return this.items.join(separator)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Get a random item */
|
|
463
|
+
random(): T | undefined {
|
|
464
|
+
if (this.items.length === 0) return undefined
|
|
465
|
+
return this.items[Math.floor(Math.random() * this.items.length)]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Get N random items */
|
|
469
|
+
sample(count: number): Collection<T> {
|
|
470
|
+
return this.shuffle().take(count)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Create a lazy version of this collection */
|
|
474
|
+
lazy(): LazyCollection<T> {
|
|
475
|
+
return new LazyCollection(this.items)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── LazyCollection (generator-based) ────────────────────────────────
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Generator-based lazy collection — operations are deferred until iteration.
|
|
483
|
+
* Ideal for large datasets or when chaining many operations where
|
|
484
|
+
* intermediate arrays would be wasteful.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* lazy(hugeArray)
|
|
489
|
+
* .filter(x => x > 100)
|
|
490
|
+
* .map(x => x * 2)
|
|
491
|
+
* .take(10)
|
|
492
|
+
* .toArray() // only processes items until 10 are found
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export class LazyCollection<T> implements Iterable<T> {
|
|
496
|
+
private source: Iterable<T>
|
|
497
|
+
|
|
498
|
+
constructor(source: Iterable<T>) {
|
|
499
|
+
this.source = source
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
[Symbol.iterator](): Iterator<T> {
|
|
503
|
+
return this.source[Symbol.iterator]()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Map each item lazily */
|
|
507
|
+
map<U>(fn: (item: T, index: number) => U): LazyCollection<U> {
|
|
508
|
+
const source = this.source
|
|
509
|
+
return new LazyCollection({
|
|
510
|
+
*[Symbol.iterator]() {
|
|
511
|
+
let i = 0
|
|
512
|
+
for (const item of source) {
|
|
513
|
+
yield fn(item, i++)
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Filter items lazily */
|
|
520
|
+
filter(predicate: (item: T, index: number) => boolean): LazyCollection<T> {
|
|
521
|
+
const source = this.source
|
|
522
|
+
return new LazyCollection({
|
|
523
|
+
*[Symbol.iterator]() {
|
|
524
|
+
let i = 0
|
|
525
|
+
for (const item of source) {
|
|
526
|
+
if (predicate(item, i++)) yield item
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Reject items lazily (inverse of filter) */
|
|
533
|
+
reject(predicate: (item: T, index: number) => boolean): LazyCollection<T> {
|
|
534
|
+
return this.filter((item, i) => !predicate(item, i))
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Flat-map each item lazily */
|
|
538
|
+
flatMap<U>(fn: (item: T, index: number) => Iterable<U>): LazyCollection<U> {
|
|
539
|
+
const source = this.source
|
|
540
|
+
return new LazyCollection({
|
|
541
|
+
*[Symbol.iterator]() {
|
|
542
|
+
let i = 0
|
|
543
|
+
for (const item of source) {
|
|
544
|
+
yield* fn(item, i++)
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Take the first N items */
|
|
551
|
+
take(count: number): LazyCollection<T> {
|
|
552
|
+
const source = this.source
|
|
553
|
+
return new LazyCollection({
|
|
554
|
+
*[Symbol.iterator]() {
|
|
555
|
+
let taken = 0
|
|
556
|
+
for (const item of source) {
|
|
557
|
+
if (taken >= count) return
|
|
558
|
+
yield item
|
|
559
|
+
taken++
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Take items while predicate is true */
|
|
566
|
+
takeWhile(predicate: (item: T, index: number) => boolean): LazyCollection<T> {
|
|
567
|
+
const source = this.source
|
|
568
|
+
return new LazyCollection({
|
|
569
|
+
*[Symbol.iterator]() {
|
|
570
|
+
let i = 0
|
|
571
|
+
for (const item of source) {
|
|
572
|
+
if (!predicate(item, i++)) return
|
|
573
|
+
yield item
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Skip the first N items */
|
|
580
|
+
skip(count: number): LazyCollection<T> {
|
|
581
|
+
const source = this.source
|
|
582
|
+
return new LazyCollection({
|
|
583
|
+
*[Symbol.iterator]() {
|
|
584
|
+
let skipped = 0
|
|
585
|
+
for (const item of source) {
|
|
586
|
+
if (skipped < count) {
|
|
587
|
+
skipped++
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
590
|
+
yield item
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Skip items while predicate is true */
|
|
597
|
+
skipWhile(predicate: (item: T, index: number) => boolean): LazyCollection<T> {
|
|
598
|
+
const source = this.source
|
|
599
|
+
return new LazyCollection({
|
|
600
|
+
*[Symbol.iterator]() {
|
|
601
|
+
let skipping = true
|
|
602
|
+
let i = 0
|
|
603
|
+
for (const item of source) {
|
|
604
|
+
if (skipping && predicate(item, i++)) continue
|
|
605
|
+
skipping = false
|
|
606
|
+
yield item
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Get unique values lazily */
|
|
613
|
+
unique(key?: (item: T) => any): LazyCollection<T> {
|
|
614
|
+
const source = this.source
|
|
615
|
+
return new LazyCollection({
|
|
616
|
+
*[Symbol.iterator]() {
|
|
617
|
+
const seen = new Set()
|
|
618
|
+
for (const item of source) {
|
|
619
|
+
const k = key ? key(item) : item
|
|
620
|
+
if (!seen.has(k)) {
|
|
621
|
+
seen.add(k)
|
|
622
|
+
yield item
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Chunk lazily — yields arrays of the given size */
|
|
630
|
+
chunk(size: number): LazyCollection<T[]> {
|
|
631
|
+
const source = this.source
|
|
632
|
+
return new LazyCollection({
|
|
633
|
+
*[Symbol.iterator]() {
|
|
634
|
+
let chunk: T[] = []
|
|
635
|
+
for (const item of source) {
|
|
636
|
+
chunk.push(item)
|
|
637
|
+
if (chunk.length >= size) {
|
|
638
|
+
yield chunk
|
|
639
|
+
chunk = []
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (chunk.length > 0) yield chunk
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Concatenate another iterable lazily */
|
|
648
|
+
concat(other: Iterable<T>): LazyCollection<T> {
|
|
649
|
+
const source = this.source
|
|
650
|
+
return new LazyCollection({
|
|
651
|
+
*[Symbol.iterator]() {
|
|
652
|
+
yield* source
|
|
653
|
+
yield* other
|
|
654
|
+
},
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Tap into each item for side effects */
|
|
659
|
+
tap(fn: (item: T) => void): LazyCollection<T> {
|
|
660
|
+
const source = this.source
|
|
661
|
+
return new LazyCollection({
|
|
662
|
+
*[Symbol.iterator]() {
|
|
663
|
+
for (const item of source) {
|
|
664
|
+
fn(item)
|
|
665
|
+
yield item
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── Eager terminal operations ─────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
/** Collect into a plain array */
|
|
674
|
+
toArray(): T[] {
|
|
675
|
+
return [...this.source]
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Collect into a Collection */
|
|
679
|
+
collect(): Collection<T> {
|
|
680
|
+
return new Collection(this.toArray())
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Count items */
|
|
684
|
+
count(): number {
|
|
685
|
+
let n = 0
|
|
686
|
+
for (const _ of this.source) n++
|
|
687
|
+
return n
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Get the first item */
|
|
691
|
+
first(): T | undefined {
|
|
692
|
+
for (const item of this.source) return item
|
|
693
|
+
return undefined
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Get the last item */
|
|
697
|
+
last(): T | undefined {
|
|
698
|
+
let last: T | undefined
|
|
699
|
+
for (const item of this.source) last = item
|
|
700
|
+
return last
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Reduce to a single value */
|
|
704
|
+
reduce<U>(fn: (acc: U, item: T) => U, initial: U): U {
|
|
705
|
+
let acc = initial
|
|
706
|
+
for (const item of this.source) {
|
|
707
|
+
acc = fn(acc, item)
|
|
708
|
+
}
|
|
709
|
+
return acc
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Check if every item matches */
|
|
713
|
+
every(predicate: (item: T) => boolean): boolean {
|
|
714
|
+
for (const item of this.source) {
|
|
715
|
+
if (!predicate(item)) return false
|
|
716
|
+
}
|
|
717
|
+
return true
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Check if any item matches */
|
|
721
|
+
some(predicate: (item: T) => boolean): boolean {
|
|
722
|
+
for (const item of this.source) {
|
|
723
|
+
if (predicate(item)) return true
|
|
724
|
+
}
|
|
725
|
+
return false
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Find the first matching item */
|
|
729
|
+
find(predicate: (item: T) => boolean): T | undefined {
|
|
730
|
+
for (const item of this.source) {
|
|
731
|
+
if (predicate(item)) return item
|
|
732
|
+
}
|
|
733
|
+
return undefined
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Run a callback for each item */
|
|
737
|
+
each(fn: (item: T) => void): void {
|
|
738
|
+
for (const item of this.source) fn(item)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** Join items into a string */
|
|
742
|
+
join(separator = ', '): string {
|
|
743
|
+
return this.toArray().join(separator)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Sum of values */
|
|
747
|
+
sum(key?: (item: T) => number): number {
|
|
748
|
+
let total = 0
|
|
749
|
+
for (const item of this.source) {
|
|
750
|
+
total += key ? key(item) : (item as unknown as number)
|
|
751
|
+
}
|
|
752
|
+
return total
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Min value */
|
|
756
|
+
min(key?: (item: T) => number): number {
|
|
757
|
+
let result = Infinity
|
|
758
|
+
for (const item of this.source) {
|
|
759
|
+
const v = key ? key(item) : (item as unknown as number)
|
|
760
|
+
if (v < result) result = v
|
|
761
|
+
}
|
|
762
|
+
return result
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Max value */
|
|
766
|
+
max(key?: (item: T) => number): number {
|
|
767
|
+
let result = -Infinity
|
|
768
|
+
for (const item of this.source) {
|
|
769
|
+
const v = key ? key(item) : (item as unknown as number)
|
|
770
|
+
if (v > result) result = v
|
|
771
|
+
}
|
|
772
|
+
return result
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── Factory functions ───────────────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
/** Create a new Collection from an iterable */
|
|
779
|
+
export function collect<T>(items: Iterable<T> | T[] = []): Collection<T> {
|
|
780
|
+
return new Collection(items)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Create a new LazyCollection from an iterable */
|
|
784
|
+
export function lazy<T>(items: Iterable<T>): LazyCollection<T> {
|
|
785
|
+
return new LazyCollection(items)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Create a LazyCollection from a generator function */
|
|
789
|
+
export function generate<T>(fn: () => Generator<T>): LazyCollection<T> {
|
|
790
|
+
return new LazyCollection({ [Symbol.iterator]: fn })
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Create a lazy range of numbers */
|
|
794
|
+
export function range(start: number, end: number, step = 1): LazyCollection<number> {
|
|
795
|
+
return new LazyCollection({
|
|
796
|
+
*[Symbol.iterator]() {
|
|
797
|
+
if (step > 0) {
|
|
798
|
+
for (let i = start; i <= end; i += step) yield i
|
|
799
|
+
} else {
|
|
800
|
+
for (let i = start; i >= end; i += step) yield i
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
})
|
|
804
|
+
}
|