@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.
@@ -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
+ }