├── composer.json └── src ├── CartesianProduct.php └── function.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bentools/cartesian-product", 3 | "description": "A simple, low-memory footprint function to generate all combinations from a multi-dimensionnal array.", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "BenTools\\CartesianProduct\\" : "src" 9 | }, 10 | "files": ["src/function.php"] 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "BenTools\\CartesianProduct\\Tests\\" : "tests" 15 | }, 16 | "files": [ 17 | "vendor/symfony/var-dumper/Resources/functions/dump.php" 18 | ] 19 | }, 20 | "require": { 21 | "php": ">=7.4" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^8.0|^9.0", 25 | "squizlabs/php_codesniffer": "@stable", 26 | "php-coveralls/php-coveralls": "@stable", 27 | "symfony/var-dumper": "^3.2|^4.0", 28 | "dms/phpunit-arraysubset-asserts": "^0.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CartesianProduct.php: -------------------------------------------------------------------------------- 1 | set = $set; 35 | } 36 | 37 | /** 38 | * @return \Traversable 39 | */ 40 | public function getIterator(): Traversable 41 | { 42 | if ([] === $this->set) { 43 | if (true === $this->isRecursiveStep) { 44 | yield []; 45 | } 46 | 47 | return; 48 | } 49 | 50 | $set = $this->set; 51 | $keys = \array_keys($set); 52 | $last = \end($keys); 53 | $subset = \array_pop($set); 54 | $this->validate($subset, $last); 55 | foreach (self::subset($set) as $product) { 56 | foreach ($subset as $value) { 57 | yield $product + [$last => ($value instanceof \Closure ? $value($product) : $value)]; 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @param $subset 64 | * @param $key 65 | */ 66 | private function validate($subset, $key) 67 | { 68 | // Validate array subset 69 | if (\is_array($subset) && !empty($subset)) { 70 | return; 71 | } 72 | 73 | // Validate iterator subset 74 | if ($subset instanceof Traversable && $subset instanceof Countable && \count($subset) > 0) { 75 | return; 76 | } 77 | 78 | throw new InvalidArgumentException(\sprintf('Key "%s" should return a non-empty iterable', $key)); 79 | } 80 | 81 | /** 82 | * @param array $subset 83 | * @return CartesianProduct 84 | */ 85 | private static function subset(array $subset) 86 | { 87 | $product = new self($subset); 88 | $product->isRecursiveStep = true; 89 | return $product; 90 | } 91 | 92 | /** 93 | * @return array 94 | */ 95 | public function asArray() 96 | { 97 | return \iterator_to_array($this); 98 | } 99 | 100 | /** 101 | * @return int 102 | */ 103 | public function count(): int 104 | { 105 | if (null === $this->count) { 106 | $this->count = (int) \array_product( 107 | \array_map( 108 | function ($subset, $key) { 109 | $this->validate($subset, $key); 110 | return \count($subset); 111 | }, 112 | $this->set, 113 | \array_keys($this->set) 114 | ) 115 | ); 116 | } 117 | return $this->count; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/function.php: -------------------------------------------------------------------------------- 1 |