1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus framework.
7: *
8: * (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Collection;
15:
16: use Nexus\Collection\Iterator\ClosureIteratorAggregate;
17: use Nexus\Collection\Iterator\RewindableIterator;
18:
19: /**
20: * @template TKey
21: * @template T
22: *
23: * @implements CollectionInterface<TKey, T>
24: *
25: * @immutable
26: */
27: final class Collection implements CollectionInterface
28: {
29: /**
30: * @var ClosureIteratorAggregate<TKey, T>
31: */
32: private ClosureIteratorAggregate $innerIterator;
33:
34: /**
35: * @template S
36: *
37: * @param (\Closure(S): \Iterator<TKey, T>) $callable
38: * @param iterable<int, S> $parameter
39: */
40: public function __construct(\Closure $callable, iterable $parameter = [])
41: {
42: $this->innerIterator = ClosureIteratorAggregate::from($callable, ...$parameter);
43: }
44:
45: /**
46: * @template WrapKey
47: * @template Wrap
48: *
49: * @return self<WrapKey, Wrap>
50: */
51: #[\Override]
52: public static function wrap(\Closure|iterable $items): self
53: {
54: if ($items instanceof \Closure) {
55: return new self(static fn(): iterable => yield from $items());
56: }
57:
58: return new self(static fn(): iterable => yield from $items);
59: }
60:
61: /**
62: * @return ($preserveKeys is false ? list<T> : array<array-key, T>)
63: */
64: #[\Override]
65: public function all(bool $preserveKeys = false): array
66: {
67: return iterator_to_array($this, $preserveKeys);
68: }
69:
70: #[\Override]
71: public function any(\Closure $predicate): bool
72: {
73: foreach ($this as $key => $item) {
74: if ($predicate($item, $key)) {
75: return true;
76: }
77: }
78:
79: return false;
80: }
81:
82: /**
83: * @template U
84: *
85: * @param U ...$items
86: *
87: * @return self<int|TKey, T|U>
88: */
89: #[\Override]
90: public function append(mixed ...$items): self
91: {
92: return new self(
93: static function (iterable $collection) use ($items): iterable {
94: $iterator = new \AppendIterator();
95:
96: foreach ([$collection, $items] as $iterable) {
97: $iterator->append(
98: new \NoRewindIterator(
99: (static fn(): \Generator => yield from $iterable)(),
100: ),
101: );
102: }
103:
104: yield from $iterator;
105: },
106: [$this],
107: );
108: }
109:
110: /**
111: * @template U
112: *
113: * @return self<T, U>
114: */
115: #[\Override]
116: public function associate(iterable $values): self
117: {
118: $valuesIterator = (static fn(): \Generator => yield from $values)();
119:
120: return new self(
121: static function (iterable $collection) use ($valuesIterator): iterable {
122: foreach ($collection->values() as $key) {
123: if (! $valuesIterator->valid()) {
124: throw new \InvalidArgumentException('The number of values is lesser than the keys.');
125: }
126:
127: yield $key => $valuesIterator->current();
128:
129: $valuesIterator->next();
130: }
131:
132: if ($valuesIterator->valid()) {
133: throw new \InvalidArgumentException('The number of values is greater than the keys.');
134: }
135: },
136: [$this],
137: );
138: }
139:
140: /**
141: * @return self<int, non-empty-array<TKey, T>>
142: */
143: #[\Override]
144: public function chunk(int $size): self
145: {
146: return new self(
147: static function (iterable $collection) use ($size): \Generator {
148: $chunk = [];
149: $count = 0;
150:
151: foreach ($collection as $key => $item) {
152: $chunk[$key] = $item;
153: ++$count;
154:
155: if ($count === $size) {
156: yield $chunk;
157:
158: $chunk = [];
159: $count = 0;
160: }
161: }
162:
163: if ([] !== $chunk) {
164: yield $chunk;
165: }
166: },
167: [$this],
168: );
169: }
170:
171: #[\Override]
172: public function count(): int
173: {
174: return iterator_count($this);
175: }
176:
177: /**
178: * @return self<TKey, T>
179: */
180: #[\Override]
181: public function cycle(): self
182: {
183: return new self(
184: static fn(iterable $collection): iterable => new \InfiniteIterator(
185: new RewindableIterator(
186: static fn(): \Generator => yield from $collection,
187: ),
188: ),
189: [$this],
190: );
191: }
192:
193: /**
194: * @return self<TKey, T>
195: */
196: #[\Override]
197: public function diff(iterable ...$others): self
198: {
199: return new self(
200: static function (iterable $collection) use ($others): iterable {
201: $hashTable = self::generateDiffHashTable($others);
202:
203: foreach ($collection as $key => $value) {
204: if (! \array_key_exists(self::toArrayKey($value), $hashTable)) {
205: yield $key => $value;
206: }
207: }
208: },
209: [$this],
210: );
211: }
212:
213: /**
214: * @return self<TKey, T>
215: */
216: #[\Override]
217: public function diffKey(iterable ...$others): self
218: {
219: return new self(
220: static function (iterable $collection) use ($others): iterable {
221: $hashTable = self::generateDiffHashTable($others);
222:
223: foreach ($collection as $key => $value) {
224: if (! \array_key_exists(self::toArrayKey($key), $hashTable)) {
225: yield $key => $value;
226: }
227: }
228: },
229: [$this],
230: );
231: }
232:
233: /**
234: * @return self<TKey, T>
235: */
236: #[\Override]
237: public function drop(int $length): self
238: {
239: return $this->slice($length);
240: }
241:
242: #[\Override]
243: public function every(\Closure $predicate): bool
244: {
245: foreach ($this as $key => $item) {
246: if (! $predicate($item, $key)) {
247: return false;
248: }
249: }
250:
251: return true;
252: }
253:
254: /**
255: * @return self<TKey, T>
256: */
257: #[\Override]
258: public function filter(?\Closure $predicate = null): self
259: {
260: $predicate ??= static fn(mixed $item): bool => (bool) $item;
261:
262: return new self(
263: static function (iterable $collection) use ($predicate): iterable {
264: foreach ($collection as $key => $item) {
265: if ($predicate($item)) {
266: yield $key => $item;
267: }
268: }
269: },
270: [$this],
271: );
272: }
273:
274: /**
275: * @return self<TKey, T>
276: */
277: #[\Override]
278: public function filterKeys(?\Closure $predicate = null): self
279: {
280: $predicate ??= static fn(mixed $key): bool => (bool) $key;
281:
282: return new self(
283: static function (iterable $collection) use ($predicate): iterable {
284: foreach ($collection as $key => $item) {
285: if ($predicate($key)) {
286: yield $key => $item;
287: }
288: }
289: },
290: [$this],
291: );
292: }
293:
294: /**
295: * @return self<TKey, T>
296: */
297: #[\Override]
298: public function filterWithKey(?\Closure $predicate = null): self
299: {
300: $predicate ??= static fn(mixed $item, mixed $key): bool => (bool) $item && (bool) $key;
301:
302: return new self(
303: static function (iterable $collection) use ($predicate): iterable {
304: foreach ($collection as $key => $item) {
305: if ($predicate($item, $key)) {
306: yield $key => $item;
307: }
308: }
309: },
310: [$this],
311: );
312: }
313:
314: #[\Override]
315: public function first(\Closure $predicate, mixed $default = null): mixed
316: {
317: return $this
318: ->filterWithKey($predicate)
319: ->append($default)
320: ->limit(1)
321: ->getIterator()
322: ->current()
323: ;
324: }
325:
326: /**
327: * @return self<T, TKey>
328: */
329: #[\Override]
330: public function flip(): self
331: {
332: return new self(
333: static function (iterable $collection): iterable {
334: foreach ($collection as $key => $item) {
335: yield $item => $key;
336: }
337: },
338: [$this],
339: );
340: }
341:
342: /**
343: * @return self<TKey, T>
344: */
345: #[\Override]
346: public function forget(mixed ...$keys): self
347: {
348: return $this->filterKeys(
349: static fn(mixed $key): bool => ! \in_array($key, $keys, true),
350: );
351: }
352:
353: #[\Override]
354: public function get(mixed $key, mixed $default = null): mixed
355: {
356: return $this
357: ->filterKeys(static fn(mixed $k): bool => $key === $k)
358: ->append($default)
359: ->limit(1)
360: ->getIterator()
361: ->current()
362: ;
363: }
364:
365: /**
366: * @return \Generator<TKey, T, mixed, void>
367: */
368: #[\Override]
369: public function getIterator(): \Traversable
370: {
371: yield from $this->innerIterator->getIterator();
372: }
373:
374: #[\Override]
375: public function has(mixed $key): bool
376: {
377: return $this
378: ->filterKeys(static fn(mixed $k): bool => $key === $k)
379: ->limit(1)
380: ->getIterator()
381: ->valid()
382: ;
383: }
384:
385: /**
386: * @return self<TKey, T>
387: */
388: #[\Override]
389: public function intersect(iterable ...$others): self
390: {
391: return new self(
392: static function (iterable $collection) use ($others): iterable {
393: $hashTable = self::generateIntersectHashTable($others);
394: $count = \count($others);
395:
396: foreach ($collection as $key => $value) {
397: $encodedValue = self::toArrayKey($value);
398:
399: if (
400: \array_key_exists($encodedValue, $hashTable)
401: && $hashTable[$encodedValue] === $count
402: ) {
403: yield $key => $value;
404: }
405: }
406: },
407: [$this],
408: );
409: }
410:
411: /**
412: * @return self<TKey, T>
413: */
414: #[\Override]
415: public function intersectKey(iterable ...$others): self
416: {
417: return new self(
418: static function (iterable $collection) use ($others): iterable {
419: $hashTable = self::generateIntersectHashTable($others);
420: $count = \count($others);
421:
422: foreach ($collection as $key => $value) {
423: $encodedKey = self::toArrayKey($key);
424:
425: if (
426: \array_key_exists($encodedKey, $hashTable)
427: && $hashTable[$encodedKey] === $count
428: ) {
429: yield $key => $value;
430: }
431: }
432: },
433: [$this],
434: );
435: }
436:
437: /**
438: * @return self<int, TKey>
439: */
440: #[\Override]
441: public function keys(): self
442: {
443: return new self(
444: static function (iterable $collection): iterable {
445: foreach ($collection as $key => $_) {
446: yield $key;
447: }
448: },
449: [$this],
450: );
451: }
452:
453: /**
454: * @return self<TKey, T>
455: */
456: #[\Override]
457: public function limit(int $limit = -1, int $offset = 0): self
458: {
459: return new self(
460: static fn(iterable $collection): iterable => yield from new \LimitIterator(
461: (static fn(): iterable => yield from $collection)(),
462: $offset,
463: $limit,
464: ),
465: [$this],
466: );
467: }
468:
469: /**
470: * @template U
471: *
472: * @return self<TKey, U>
473: */
474: #[\Override]
475: public function map(\Closure $predicate): self
476: {
477: return new self(
478: static function (iterable $collection) use ($predicate): iterable {
479: foreach ($collection as $key => $item) {
480: yield $key => $predicate($item);
481: }
482: },
483: [$this],
484: );
485: }
486:
487: /**
488: * @template UKey
489: *
490: * @return self<UKey, T>
491: */
492: #[\Override]
493: public function mapKeys(\Closure $predicate): self
494: {
495: return new self(
496: static function (iterable $collection) use ($predicate): iterable {
497: foreach ($collection as $key => $item) {
498: yield $predicate($key) => $item;
499: }
500: },
501: [$this],
502: );
503: }
504:
505: /**
506: * @template U
507: *
508: * @return self<TKey, U>
509: */
510: #[\Override]
511: public function mapWithKey(\Closure $predicate): self
512: {
513: return new self(
514: static function (iterable $collection) use ($predicate): iterable {
515: foreach ($collection as $key => $item) {
516: yield $key => $predicate($item, $key);
517: }
518: },
519: [$this],
520: );
521: }
522:
523: /**
524: * @return self<int, CollectionInterface<TKey, T>>
525: */
526: #[\Override]
527: public function partition(\Closure $predicate): self
528: {
529: return new self(
530: static function (iterable $collection) use ($predicate): iterable {
531: yield $collection->filterWithKey($predicate);
532:
533: yield $collection->reject($predicate);
534: },
535: [$this],
536: );
537: }
538:
539: #[\Override]
540: public function reduce(\Closure $predicate, mixed $initial = null): mixed
541: {
542: $accumulator = $initial;
543:
544: foreach ($this as $key => $item) {
545: $accumulator = $predicate($accumulator, $item, $key);
546: }
547:
548: return $accumulator;
549: }
550:
551: /**
552: * @template TAcc
553: *
554: * @return self<int, TAcc>
555: */
556: #[\Override]
557: public function reductions(\Closure $predicate, mixed $initial = null): self
558: {
559: return new self(
560: static function (iterable $collection) use ($predicate, $initial): iterable {
561: $accumulator = $initial;
562:
563: foreach ($collection as $key => $item) {
564: $accumulator = $predicate($accumulator, $item, $key);
565:
566: yield $accumulator;
567: }
568: },
569: [$this],
570: );
571: }
572:
573: /**
574: * @return self<TKey, T>
575: */
576: #[\Override]
577: public function reject(?\Closure $predicate = null): self
578: {
579: $predicate ??= static fn(mixed $item, mixed $key): bool => (bool) $item && (bool) $key;
580:
581: return new self(
582: static function (iterable $collection) use ($predicate): iterable {
583: foreach ($collection as $key => $item) {
584: if (! $predicate($item, $key)) {
585: yield $key => $item;
586: }
587: }
588: },
589: [$this],
590: );
591: }
592:
593: /**
594: * @return self<TKey, T>
595: */
596: #[\Override]
597: public function slice(int $start, ?int $length = null): self
598: {
599: return new self(
600: static function (iterable $collection) use ($start, $length): iterable {
601: if (0 === $length) {
602: yield from $collection;
603:
604: return;
605: }
606:
607: $i = 0;
608:
609: foreach ($collection as $key => $item) {
610: if ($i++ < $start) {
611: continue;
612: }
613:
614: yield $key => $item;
615:
616: if (null !== $length && $i >= $start + $length) {
617: break;
618: }
619: }
620: },
621: [$this],
622: );
623: }
624:
625: /**
626: * @return self<TKey, T>
627: */
628: #[\Override]
629: public function take(int $length): self
630: {
631: return $this->slice(0, $length);
632: }
633:
634: /**
635: * @return self<TKey, T>
636: */
637: #[\Override]
638: public function tap(\Closure ...$callbacks): self
639: {
640: return new self(
641: static function (iterable $collection) use ($callbacks): iterable {
642: foreach ($collection as $key => $item) {
643: foreach ($callbacks as $callback) {
644: $callback($item, $key);
645: }
646: }
647:
648: yield from $collection;
649: },
650: [$this],
651: );
652: }
653:
654: /**
655: * @return self<int, T>
656: */
657: #[\Override]
658: public function values(): self
659: {
660: return new self(
661: static function (iterable $collection): iterable {
662: foreach ($collection as $item) {
663: yield $item;
664: }
665: },
666: [$this],
667: );
668: }
669:
670: /**
671: * Generates a hash table for lookup by `diff` and `diffKey`.
672: *
673: * @param array<array-key, iterable<mixed, mixed>> $iterables
674: *
675: * @return array<string, true>
676: */
677: private static function generateDiffHashTable(array $iterables): array
678: {
679: $hashTable = [];
680:
681: foreach ($iterables as $iterable) {
682: foreach ($iterable as $value) {
683: $hashTable[self::toArrayKey($value)] = true;
684: }
685: }
686:
687: return $hashTable;
688: }
689:
690: /**
691: * Generates a hash table for lookup by `intersect` and `intersectKey`.
692: *
693: * @param array<array-key, iterable<mixed, mixed>> $iterables
694: *
695: * @return array<string, int<1, max>>
696: */
697: private static function generateIntersectHashTable(array $iterables): array
698: {
699: $hashTable = [];
700:
701: foreach ($iterables as $iterable) {
702: foreach ($iterable as $value) {
703: $encodedValue = self::toArrayKey($value);
704:
705: if (! \array_key_exists($encodedValue, $hashTable)) {
706: $hashTable[$encodedValue] = 1;
707: } else {
708: ++$hashTable[$encodedValue];
709: }
710: }
711: }
712:
713: return $hashTable;
714: }
715:
716: private static function toArrayKey(mixed $input): string
717: {
718: return (string) json_encode($input);
719: }
720: }
721: