From 4b1269a15922bffcf3b73eee04282f8336eb6c85 Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Fri, 10 Dec 2021 13:04:40 +0000 Subject: [PATCH] Add `cartesian_product` and `InvalidArgumentException::assertStringable` --- composer.json | 1 + src/Functional/CartesianProduct.php | 62 +++++++++++++++++++ .../Exceptions/InvalidArgumentException.php | 11 ++++ src/Functional/Functional.php | 5 ++ tests/Functional/CartesianProductTest.php | 24 +++++++ .../InvalidArgumentExceptionTest.php | 30 +++++++++ 6 files changed, 133 insertions(+) create mode 100644 src/Functional/CartesianProduct.php create mode 100644 tests/Functional/CartesianProductTest.php diff --git a/composer.json b/composer.json index ebfe9738..77e5f416 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "src/Functional/Average.php", "src/Functional/ButLast.php", "src/Functional/Capture.php", + "src/Functional/CartesianProduct.php", "src/Functional/ConstFunction.php", "src/Functional/CompareOn.php", "src/Functional/CompareObjectHashOn.php", diff --git a/src/Functional/CartesianProduct.php b/src/Functional/CartesianProduct.php new file mode 100644 index 00000000..e30ac53f --- /dev/null +++ b/src/Functional/CartesianProduct.php @@ -0,0 +1,62 @@ + + * @copyright 2021 Lars Strojny + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/lstrojny/functional-php + */ + +namespace Functional; + +use Functional\Exceptions\InvalidArgumentException; +use Traversable; + +/** + * @param string|array $separator + * @param array ...$collections + * @return array + * @no-named-arguments + */ +function cartesian_product($separator, ...$collections) +{ + InvalidArgumentException::assertIntegerGreaterThanOrEqual(\count($collections), 2, __FUNCTION__, 2); // TODO not great + + $aggregation = []; + $left = \array_shift($collections); + $index = 2; + InvalidArgumentException::assertCollection($left, __FUNCTION__, $index); + while (true) { + $right = \array_shift($collections); + InvalidArgumentException::assertCollection($right, __FUNCTION__, $index + 1); + $left_index = 0; + foreach ($left as $l) { + $right_index = 0; + foreach ($right as $r) { + InvalidArgumentException::assertStringable($l, __FUNCTION__, $index, $left_index); + InvalidArgumentException::assertStringable($r, __FUNCTION__, $index + 1, $right_index); + if (\is_string($separator)) { + $aggregation[] = "{$l}{$separator}{$r}"; + } else if (\is_array($separator)) { + foreach ($separator as $sep) { + $aggregation[] = "{$l}{$sep}{$r}"; + } + } else { + // TODO assert that $separator is string or array of strings + } + ++$right_index; + } + ++$left_index; + } + ++$index; + if (empty($collections)) { + break; + } else { + $left = $aggregation; + $aggregation = []; + } + } + + return $aggregation; +} diff --git a/src/Functional/Exceptions/InvalidArgumentException.php b/src/Functional/Exceptions/InvalidArgumentException.php index b4d61675..92a3421d 100644 --- a/src/Functional/Exceptions/InvalidArgumentException.php +++ b/src/Functional/Exceptions/InvalidArgumentException.php @@ -295,6 +295,17 @@ public static function assertPair($pair, $callee, $position): void } } + public static function assertStringable($value, $callee, $position, $positionInCollection = null): void + { + if (!(\is_string($value) || \is_numeric($value) || (\is_object($value) && \method_exists($value, '__toString')))) { + if (!\is_null($positionInCollection)) { + throw new static(\sprintf('%s() expects paramter %d to be an array with strings or objects with a __toString method (offending index: %d is of type %s)', $callee, $position, $positionInCollection, self::getType($value))); + } else { + throw new static(\sprintf('%s() expects paramter %d to be a string or an object with a __toString method (got type %s)', $callee, $position, self::getType($value))); + } + } + } + private static function getType($value) { return \is_object($value) ? \get_class($value) : \gettype($value); diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index caaed895..00a7ff73 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -33,6 +33,11 @@ final class Functional */ const capture = '\Functional\capture'; + /** + * @see \Functional\cartesian_product + */ + const cartesian_product = '\Functional\cartesian_product'; + /** * @see \Functional\compare_object_hash_on */ diff --git a/tests/Functional/CartesianProductTest.php b/tests/Functional/CartesianProductTest.php new file mode 100644 index 00000000..7d65269b --- /dev/null +++ b/tests/Functional/CartesianProductTest.php @@ -0,0 +1,24 @@ + + * @copyright 2021 Lars Strojny + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/lstrojny/functional-php + */ + +namespace Functional\Tests; + +use function Functional\cartesian_product; + +class CartesianProductTest extends AbstractTestCase +{ + public function testCartesianProduct(): void + { + self::assertSame(['1-one', '1-two', '2-one', '2-two'], cartesian_product('-', [1, 2], ['one', 'two'])); + self::assertSame(['1-one', '1_one', '1-two', '1_two', '2-one', '2_one', '2-two', '2_two'], cartesian_product(['-', '_'], [1, 2], ['one', 'two'])); + self::assertSame(['1one', '1two', '2one', '2two'], cartesian_product('', [1, 2], ['one', 'two'])); + self::assertSame(['1-one-H', '1-one-He', '1-two-H', '1-two-He', '2-one-H', '2-one-He', '2-two-H', '2-two-He'], cartesian_product('-', [1, 2], ['one', 'two'], ['H', 'He'])); + } +} diff --git a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php index dfa3c77a..64b5cbb0 100644 --- a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php +++ b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php @@ -195,4 +195,34 @@ public function testAssertPairWithThreeCharacterString(): void $this->expectExceptionMessage('func() expects paramter 1 to be a pair (array with two elements)'); InvalidArgumentException::assertPair('abc', "func", 1); } + + public function testAssertStringableValid(): void + { + $this->expectNotToPerformAssertions(); + InvalidArgumentException::assertStringable('abc', "func", 1); + InvalidArgumentException::assertStringable(1, "func", 1); + InvalidArgumentException::assertStringable(1.2, "func", 1); + InvalidArgumentException::assertStringable(1.2, "func", 1); + InvalidArgumentException::assertStringable( + new class + { + public function __toString() + { + } + }, + "func", + 1 + ); + } + + public function testAssertStringableInvalid(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('func() expects paramter 1 to be a string or an object with a __toString method (got type boolean)'); + InvalidArgumentException::assertStringable(false, "func", 1); + $this->expectExceptionMessage('func() expects paramter 1 to be a string or an object with a __toString method (got type array)'); + InvalidArgumentException::assertStringable([], "func", 1); + $this->expectExceptionMessage('func() expects paramter 1 to be a string or an object with a __toString method (got type stdClass)'); + InvalidArgumentException::assertStringable(new stdClass(), "func", 1); + } }