From 5f5ddb6e7e005bd7413060f842d45f782ee240bb Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Mon, 29 Nov 2021 15:47:51 +0000 Subject: [PATCH] Add `entries` and `from_entries`, inspired by JavaScript's `Object.entries` (and Python's `enumerate`) and `Object.from_entries`, respectively (#243) --- composer.json | 2 + docs/functional-php.md | 14 ++++ src/Functional/Entries.php | 36 ++++++++++ .../Exceptions/InvalidArgumentException.php | 7 ++ src/Functional/FromEntries.php | 38 ++++++++++ src/Functional/Functional.php | 10 +++ tests/Functional/EntriesFromEntriesTest.php | 71 +++++++++++++++++++ .../InvalidArgumentExceptionTest.php | 37 ++++++++++ 8 files changed, 215 insertions(+) create mode 100644 src/Functional/Entries.php create mode 100644 src/Functional/FromEntries.php create mode 100644 tests/Functional/EntriesFromEntriesTest.php diff --git a/composer.json b/composer.json index 69c5e16a..ebfe9738 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "src/Functional/DropFirst.php", "src/Functional/DropLast.php", "src/Functional/Each.php", + "src/Functional/Entries.php", "src/Functional/Equal.php", "src/Functional/ErrorToException.php", "src/Functional/Every.php", @@ -57,6 +58,7 @@ "src/Functional/FlatMap.php", "src/Functional/Flatten.php", "src/Functional/Flip.php", + "src/Functional/FromEntries.php", "src/Functional/GreaterThan.php", "src/Functional/GreaterThanOrEqual.php", "src/Functional/Group.php", diff --git a/docs/functional-php.md b/docs/functional-php.md index 9e0b8973..913c188f 100644 --- a/docs/functional-php.md +++ b/docs/functional-php.md @@ -951,6 +951,20 @@ use Functional\intersperse; intersperse(['a', 'b', 'c'], '-'); // ['a', '-', 'b', '-', 'c']; ``` +## entries() & from_entries() + +Inspired by JavaScript’s `Object.entries()` and `Object.from_entries()` and Python’s `enumerate()`, convert a key-value +map into an array of key-value pairs, respectively. + +```php +use function Functional\entries; +use function Functional\from_entries; + +$map = ['one' => 1, 'two' => 2, 'three' => 3]; +$pairs = entries($map); // [['one', 1], ['two', 2], ['three', 3]] +$map2 = from_entries($pairs); // $map === $map2 +``` + ## Other `array Functional\unique(array|Traversable $collection[, callback $indexer[, bool $strict = true]])` diff --git a/src/Functional/Entries.php b/src/Functional/Entries.php new file mode 100644 index 00000000..1fc22531 --- /dev/null +++ b/src/Functional/Entries.php @@ -0,0 +1,36 @@ + + * @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; + +/** + * Inspired by JavaScript’s `Object.entries`, and Python’s `enumerate`, + * convert a key-value map into an array of key-value pairs + * + * @see Functional\from_entries + * @param Traversable|array $collection + * @param int $start + * @return array + * @no-named-arguments + */ +function entries($collection, int $start = 0) +{ + InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); + + $aggregation = []; + foreach ($collection as $key => $value) { + $aggregation[$start++] = [$key, $value]; + } + + return $aggregation; +} diff --git a/src/Functional/Exceptions/InvalidArgumentException.php b/src/Functional/Exceptions/InvalidArgumentException.php index 50a6acb6..b4d61675 100644 --- a/src/Functional/Exceptions/InvalidArgumentException.php +++ b/src/Functional/Exceptions/InvalidArgumentException.php @@ -288,6 +288,13 @@ public static function assertNonZeroInteger($value, $callee) } } + public static function assertPair($pair, $callee, $position): void + { + if (!(\is_array($pair) || $pair instanceof ArrayAccess) || !isset($pair[0], $pair[1])) { + throw new static(\sprintf('%s() expects paramter %d to be a pair (array with two elements)', $callee, $position)); + } + } + private static function getType($value) { return \is_object($value) ? \get_class($value) : \gettype($value); diff --git a/src/Functional/FromEntries.php b/src/Functional/FromEntries.php new file mode 100644 index 00000000..65dd5ffc --- /dev/null +++ b/src/Functional/FromEntries.php @@ -0,0 +1,38 @@ + + * @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; + +/** + * Inspired by JavaScript’s `Object.fromEntries`, + * convert an array of key-value pairs into a key-value map + * + * @see Functional\entries + * @param Traversable|array $collection + * @return array + * @no-named-arguments + */ +function from_entries($collection) +{ + InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); + + $aggregation = []; + foreach ($collection as $entry) { + InvalidArgumentException::assertPair($entry, __FUNCTION__, 1); + [$key, $value] = $entry; + InvalidArgumentException::assertValidArrayKey($key, __FUNCTION__, 1); + $aggregation[$key] = $value; + } + + return $aggregation; +} diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index 2856d6a1..caaed895 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -98,6 +98,11 @@ final class Functional */ const each = '\Functional\each'; + /** + * @see \Functional\entries + */ + const entries = '\Functional\entries'; + /** * @see \Functional\equal */ @@ -153,6 +158,11 @@ final class Functional */ const flip = '\Functional\flip'; + /** + * @see \Functional\from_entries + */ + const from_entries = '\Functional\from_entries'; + /** * @see \Functional\greater_than */ diff --git a/tests/Functional/EntriesFromEntriesTest.php b/tests/Functional/EntriesFromEntriesTest.php new file mode 100644 index 00000000..8a238b68 --- /dev/null +++ b/tests/Functional/EntriesFromEntriesTest.php @@ -0,0 +1,71 @@ + + * @copyright 2021 Lars Strojny + * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/lstrojny/functional-php + */ + +namespace Functional\Tests; + +use ArrayIterator; +use PHPUnit\Framework\MockObject\MockObject; + +use function Functional\entries; +use function Functional\from_entries; + +class EntriesFromEntriesTest extends AbstractTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->list = ['value0', 'value1', 'value2', 'value3']; + $this->listIterator = new ArrayIterator($this->list); + $this->hash = ['k0' => 'value0', 'k1' => 'value1', 'k2' => 'value2', 'k3' => 'value3']; + $this->hashIterator = new ArrayIterator($this->hash); + } + + public function testArray(): void + { + $res = entries($this->list); + self::assertSame(\array_keys($res), \range(0, \count($this->list) - 1)); + self::assertSame(from_entries($res), $this->list); + } + + public function testIterator(): void + { + $res = entries($this->listIterator); + self::assertSame(\array_keys($res), \range(0, \count($this->listIterator) - 1)); + self::assertSame(from_entries($res), $this->listIterator->getArrayCopy()); + } + + public function testHash(): void + { + $res = entries($this->hash); + self::assertSame(\array_keys($res), \range(0, \count($this->hash) - 1)); + self::assertSame(from_entries($res), $this->hash); + } + + public function testHashIterator(): void + { + $res = entries($this->hashIterator); + self::assertSame(\array_keys($res), \range(0, \count($this->hashIterator) - 1)); + self::assertSame(from_entries($res), $this->hashIterator->getArrayCopy()); + } + + public function testHashWithStart(): void + { + $res = entries($this->hash, 42); + self::assertSame(\array_keys($res), \range(42, 42 + \count($this->hash) - 1)); + self::assertSame(from_entries($res), $this->hash); + } + + public function testPassNoCollection(): void + { + $this->expectArgumentError('Functional\entries() expects parameter 1 to be array or instance of Traversable'); + entries('invalidCollection'); + } +} diff --git a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php index 82b868a8..dfa3c77a 100644 --- a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php +++ b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php @@ -158,4 +158,41 @@ public function testAssertBooleanAccessWithObject(): void $this->expectExceptionMessage('func() expects parameter 4 to be boolean, stdClass given'); InvalidArgumentException::assertBoolean(new \stdClass(), "func", 4); } + + public function testAssertPairWithPair(): void + { + $this->expectNotToPerformAssertions(); + InvalidArgumentException::assertPair([1, 2], "func", 1); + InvalidArgumentException::assertPair(['1', 2], "func", 1); + InvalidArgumentException::assertPair([1, '2'], "func", 1); + InvalidArgumentException::assertPair([new \stdClass(), '2'], "func", 1); + } + + public function testAssertPairWithEmptyArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('func() expects paramter 1 to be a pair (array with two elements)'); + InvalidArgumentException::assertPair([], "func", 1); + } + + public function testAssertPairWithInvalidArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('func() expects paramter 1 to be a pair (array with two elements)'); + InvalidArgumentException::assertPair(['one'], "func", 1); + } + + public function testAssertPairWithTwoCharacterString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('func() expects paramter 1 to be a pair (array with two elements)'); + InvalidArgumentException::assertPair('ab', "func", 1); + } + + public function testAssertPairWithThreeCharacterString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('func() expects paramter 1 to be a pair (array with two elements)'); + InvalidArgumentException::assertPair('abc', "func", 1); + } }