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