1: | <?php |
2: | |
3: | declare(strict_types=1); |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | namespace Nexus\Collection; |
15: | |
16: | use Nexus\Collection\Iterator\ClosureIteratorAggregate; |
17: | use Nexus\Collection\Iterator\RewindableIterator; |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | final class Collection implements CollectionInterface |
28: | { |
29: | |
30: | |
31: | |
32: | private ClosureIteratorAggregate $innerIterator; |
33: | |
34: | |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | public function __construct(\Closure $callable, iterable $parameter = []) |
41: | { |
42: | $this->innerIterator = ClosureIteratorAggregate::from($callable, ...$parameter); |
43: | } |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | |
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: | |
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: | |
84: | |
85: | |
86: | |
87: | |
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: | |
112: | |
113: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
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: | |
471: | |
472: | |
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: | |
489: | |
490: | |
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: | |
507: | |
508: | |
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: | |
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: | |
553: | |
554: | |
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: | |
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: | |
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: | |
627: | |
628: | #[\Override] |
629: | public function take(int $length): self |
630: | { |
631: | return $this->slice(0, $length); |
632: | } |
633: | |
634: | |
635: | |
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: | |
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: | |
672: | |
673: | |
674: | |
675: | |
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: | |
692: | |
693: | |
694: | |
695: | |
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: | |