diff --git a/composer.json b/composer.json index d0a8aa52..627c5e33 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "require-dev": { "phpunit/phpunit": "~6", "squizlabs/php_codesniffer": "~3.0", - "friendsofphp/php-cs-fixer": "^2.14" + "friendsofphp/php-cs-fixer": "^2.14", + "apantle/hashmapper": "^1.3" }, "autoload": { "psr-4": {"Functional\\": "src/Functional"}, @@ -35,6 +36,7 @@ "src/Functional/Concat.php", "src/Functional/Contains.php", "src/Functional/Converge.php", + "src/Functional/CreateAssoc.php", "src/Functional/Curry.php", "src/Functional/CurryN.php", "src/Functional/Difference.php", diff --git a/src/Functional/CreateAssoc.php b/src/Functional/CreateAssoc.php new file mode 100644 index 00000000..4e486f48 --- /dev/null +++ b/src/Functional/CreateAssoc.php @@ -0,0 +1,62 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +namespace Functional; + +/** + * Return a function that applies a set of functions to build an + * associative array from a single input (scalar, object or sequence) + * + * @param array $specs associative array of keys and functions that map to the target + * @param object|null $tameme + * @return callable + */ +function create_assoc(array $specs, $tameme = null): callable +{ + return function ($input, $optional = null) use ($specs, $tameme) { + $mecapal = []; + + foreach ($specs as $target => $mapper) { + $mecapal[$target] = is_unary($mapper) + ? \call_user_func($mapper, $input) + : \call_user_func($mapper, $input, $optional, $tameme) + ; + } + + return $mecapal; + }; +} + +/** + * determine if the callable passed expect only one argument + * @param callable $callable + * @return bool + * @throws \ReflectionException + */ +function is_unary($callable) +{ + if (!\is_callable($callable)) { + return false; + } + $reflector = new \ReflectionFunction($callable); + return \boolval($reflector->getNumberOfParameters() === 1); +} diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index 17ae6211..8fdd31f0 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -54,6 +54,11 @@ final class Functional */ const converge = '\Functional\converge'; + /** + * @see \Functional\create_assoc + */ + const create_assoc = '\Functional\create_assoc'; + /** * @see \Functional\curry */ @@ -169,6 +174,11 @@ final class Functional */ const identical = '\Functional\identical'; + /** + * @see \Functional\is_unary + */ + const is_unary = '\Functional\is_unary'; + /** * @see \Functional\if_else */ diff --git a/tests/Functional/CreateAssocTest.php b/tests/Functional/CreateAssocTest.php new file mode 100644 index 00000000..73fa36d2 --- /dev/null +++ b/tests/Functional/CreateAssocTest.php @@ -0,0 +1,129 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +namespace Functional\Tests; + +use Functional\Functional; + +use function Functional\create_assoc; +use function Apantle\HashMapper\hashMapper; + +class CreateAssocTest extends AbstractTestCase +{ + public function testBasicCreateAssoc() + { + $input = new \DateTimeImmutable(); + + $expected = [ + 'targetA' => $input, + 'targetB' => $input + ]; + + $actual = create_assoc([ + 'targetA' => Functional::id, + 'targetB' => Functional::id, + ])($input); + + $this->assertEquals($expected, $actual); + $this->assertEquals($input->format('U'), $actual['targetA']->format('U')); + $this->assertEquals($input->format('U'), $actual['targetB']->format('U')); + } + + public function testExpectsTameme() + { + $input = [ 1, 2, 3 ]; + + $expected = [ + 'targetA' => [ 4 => 1, 5 => 2, 6 => 3 ], + 'targetB' => [ 5 => 2 ] + ]; + // phpcs:disable + $tameme = new class extends \ArrayObject {}; + // phpcs:enable + $_irrelevant = null; + $actual = create_assoc([ + 'targetA' => function ($member, $_irrelevant, $tameme) { + $build = \array_reduce($member, function ($accum, $num) { + $accum[$num + 3] = $num; + return $accum; + }, []); + + $tameme['prev'] = $build; + return $build; + }, + 'targetB' => function ($member, $input, $tameme) { + $prev = $tameme['prev']; + $build = []; + foreach ($prev as $key => $val) { + if ($key % 2 !== 0) { + $build[$key] = $val; + } + } + return $build; + } + ], $tameme)($input); + + $this->assertEquals($expected, $actual); + } + + public function testReceivesOptionalArgument() + { + $input = [ + 'vendor' => 'tzkmx', + 'utility' => 'unfold' + ]; + + $expected = [ + 'vendorName' => 'tzkmx', + 'vendorLen' => 5, + 'serialized' => 'a:2:{s:6:"vendor";s:5:"tzkmx";s:7:"utility";s:6:"unfold";}', + 'utility' => 'unfold', + 'utilLen' => 6, + 'package' => [ 'tzkmx/unfold' => $input ] + ]; + // phpcs:disable + $tameme = new class extends \ArrayObject {}; + // phpcs:enable + $actual = hashMapper([ + 'vendor' => [ '...', create_assoc([ + 'vendorName' => 'strval', + 'vendorLen' => 'strlen', + 'serialized' => function ($member, $hash, $tameme) { + $tameme['name'] = $member; + return \serialize($hash); + } + ], $tameme) + ], + 'utility' => [ '...', create_assoc([ + 'utility' => 'strval', + 'utilLen' => 'strlen', + 'package' => function ($member, $hash, $tameme) { + $name = $tameme['name']; + return [ "$name/$member" => $hash ]; + } + ], $tameme) + ] + ])($input); + + $this->assertEquals($expected, $actual); + } +}