diff --git a/src/Collection/Iterator/MapIterator.php b/src/Collection/Iterator/MapIterator.php index 44e2154..a7c0e3c 100644 --- a/src/Collection/Iterator/MapIterator.php +++ b/src/Collection/Iterator/MapIterator.php @@ -6,15 +6,22 @@ use Munus\Collection\Iterator; use Munus\Exception\NoSuchElementException; -use Munus\Tuple; +use Munus\Tuple\Tuple2; +/** + * @template K + * @template V + */ final class MapIterator extends Iterator { /** - * @var mixed[] + * @var array> */ private array $map; + /** + * @param array> $map + */ public function __construct(array $map) { $this->map = $map; @@ -23,12 +30,12 @@ public function __construct(array $map) public function key(): mixed { - return key($this->map); + return current($this->map)[0]; } public function current(): mixed { - return current($this->map); + return current($this->map)[1]; } public function rewind(): void @@ -36,12 +43,17 @@ public function rewind(): void reset($this->map); } - public function next(): Tuple + /** + * @throws NoSuchElementException + * + * @return Tuple2 + */ + public function next(): Tuple2 { if (!$this->valid()) { throw new NoSuchElementException(); } - $next = Tuple::of(key($this->map), current($this->map)); + $next = current($this->map); next($this->map); return $next; @@ -57,6 +69,9 @@ public function hasNext(): bool return $this->valid(); } + /** + * @return array> + */ public function toArray(): array { return $this->map; diff --git a/src/Collection/Map.php b/src/Collection/Map.php index 62f7a23..b528187 100644 --- a/src/Collection/Map.php +++ b/src/Collection/Map.php @@ -9,7 +9,6 @@ use Munus\Exception\NoSuchElementException; use Munus\Exception\UnsupportedOperationException; use Munus\Tuple; -use Munus\Tuple\Tuple1; use Munus\Tuple\Tuple2; use Munus\Value; use Munus\Value\Comparator; @@ -21,13 +20,15 @@ * @template V * * @extends Traversable + * + * @implements \ArrayAccess */ final class Map extends Traversable implements \ArrayAccess { /** - * @var array + * @var array> */ - private $map = []; + private array $map = []; private function __construct() { @@ -38,6 +39,22 @@ public static function empty(): self return new self(); } + /** + * @template U + * @template T + * + * @param array> $map + * + * @return self + */ + public static function from(array $map): self + { + $newMap = new self(); + $newMap->map = $map; + + return $newMap; + } + /** * Creates Map from given array, all keys will be cast to string. * @@ -51,7 +68,7 @@ public static function fromArray(array $array): self { $map = []; foreach ($array as $key => $value) { - $map[(string) $key] = $value; + $map[] = new Tuple2((string) $key, $value); } return self::fromPointer($map); @@ -59,10 +76,11 @@ public static function fromArray(array $array): self /** * @template U + * @template T * - * @param array $map + * @param array> $map * - * @return Map + * @return Map */ private static function fromPointer(array &$map): self { @@ -82,33 +100,44 @@ public function get(): Option } $key = func_get_arg(0); + $position = $this->findPosition($key); /** @var Option $option */ - $option = isset($this->map[$key]) ? Option::some($this->map[$key]) : Option::none(); + $option = $position !== false ? Option::some($this->map[$position][1]) : Option::none(); return $option; } /** + * @param K $key * @param V $value * - * @return Map + * @return Map */ - public function put(string $key, $value): self + public function put(mixed $key, mixed $value): self { $map = $this->map; - $map[$key] = $value; + $position = $this->findPosition($key); + if ($position === false) { + $map[] = new Tuple2($key, $value); + } else { + $map[$position] = new Tuple2($key, $value); + } return self::fromPointer($map); } - public function remove(string $key): self + /** + * @param K $key + */ + public function remove(mixed $key): self { - if (!isset($this->map[$key])) { + $position = $this->findPosition($key); + if ($position === false) { return $this; } $map = $this->map; - unset($map[$key]); + array_splice($map, $position, 1); return self::fromPointer($map); } @@ -119,6 +148,8 @@ public function length(): int } /** + * @throws NoSuchElementException + * * @return Tuple2 */ public function head(): Tuple2 @@ -127,35 +158,39 @@ public function head(): Tuple2 throw new NoSuchElementException('head of empty Map'); } - $key = array_key_first($this->map); - - return Tuple::of($key, $this->map[$key]); + return $this->map[0]; } + /** + * @throws NoSuchElementException + * + * @return Map + */ public function tail() { if ($this->isEmpty()) { throw new NoSuchElementException('tail of empty Map'); } - $key = array_key_last($this->map); + $map = $this->map; + array_splice($map, 0, 1); - return Tuple::of($key, $this->map[$key]); + return self::fromPointer($map); } /** * @template U + * @template T * - * @phpstan-param callable(Tuple1): Tuple1 $mapper + * @phpstan-param callable(Tuple2): Tuple2 $mapper * - * @return Map + * @return Map */ public function map(callable $mapper) { $map = []; - foreach ($this->map as $key => $value) { - $mapped = $mapper(Tuple::of($key, $value)); - $map[$mapped[0]] = $mapped[1]; + foreach ($this->map as $tuple) { + $map[] = $mapper($tuple); } return self::fromPointer($map); @@ -164,9 +199,9 @@ public function map(callable $mapper) public function flatMap(callable $mapper) { $map = []; - foreach ($this->map as $key => $value) { - foreach ($mapper(Tuple::of($key, $value)) as $mapped) { - $map[$mapped[0]] = $mapped[1]; + foreach ($this->map as $tuple) { + foreach ($mapper($tuple) as $mapped) { + $map[] = $mapped; } } @@ -174,15 +209,17 @@ public function flatMap(callable $mapper) } /** - * @param callable(string):string $keyMapper + * @template U * - * @return Map + * @param callable(K):U $keyMapper + * + * @return Map */ public function mapKeys(callable $keyMapper): self { $map = []; - foreach ($this->map as $key => $value) { - $map[$keyMapper($key)] = $value; + foreach ($this->map as $tuple) { + $map[] = new Tuple2($keyMapper($tuple[0]), $tuple[1]); } return self::fromPointer($map); @@ -193,29 +230,29 @@ public function mapKeys(callable $keyMapper): self * * @param callable(V):U $valueMapper * - * @return Map + * @return Map */ public function mapValues(callable $valueMapper): self { $map = []; - foreach ($this->map as $key => $value) { - $map[$key] = $valueMapper($value); + foreach ($this->map as $tuple) { + $map[] = new Tuple2($tuple[0], $valueMapper($tuple[1])); } return self::fromPointer($map); } /** - * @param callable(Tuple):bool $predicate + * @param callable(Tuple2):bool $predicate * - * @return Map + * @return Map */ public function filter(callable $predicate) { $map = []; - foreach ($this->map as $key => $value) { - if ($predicate(Tuple::of($key, $value)) === true) { - $map[$key] = $value; + foreach ($this->map as $tuple) { + if ($predicate($tuple) === true) { + $map[] = $tuple; } } @@ -223,25 +260,25 @@ public function filter(callable $predicate) } /** - * @return Map + * @return Map */ public function sorted() { $map = $this->map; - asort($map); + usort($map, fn (Tuple2 $a, Tuple2 $b) => $a[1] <=> $b[1]); return self::fromPointer($map); } /** - * @param callable(Tuple):bool $predicate + * @param callable(Tuple2):bool $predicate * - * @return Map + * @return Map */ public function dropWhile(callable $predicate) { $map = $this->map; - while ($map !== [] && $predicate(Tuple::of(key($map), current($map))) === true) { + while ($map !== [] && $predicate(current($map)) === true) { unset($map[key($map)]); } @@ -251,7 +288,7 @@ public function dropWhile(callable $predicate) /** * Take n next entries of map. * - * @return Map + * @return Map */ public function take(int $n) { @@ -267,7 +304,7 @@ public function take(int $n) /** * Drop n next entries of map. * - * @return Map + * @return Map */ public function drop(int $n) { @@ -299,32 +336,45 @@ public function iterator(): Iterator */ public function values(): Stream { - return Stream::ofAll(array_values($this->map)); + return Stream::ofAll(array_map(fn (Tuple2 $tuple) => $tuple[1], $this->map)); } /** - * @return Set + * @return Set */ public function keys(): Set { - return Set::ofAll(array_keys($this->map)); + return Set::ofAll(array_map(fn (Tuple2 $tuple) => $tuple[0], $this->map)); } /** * Default contains() method will search for Tuple of key and value. * - * @param Tuple $element + * @param Tuple2 $element */ public function contains($element): bool { - return $this->get($element[0])->map(function ($value) use ($element) { - return Comparator::equals($value, $element[1]); - })->getOrElse(false); + foreach ($this->map as $tuple) { + if ($tuple->equals($element)) { + return true; + } + } + + return false; } - public function containsKey(string $key): bool + /** + * @param K $key + */ + public function containsKey($key): bool { - return isset($this->map[$key]); + foreach ($this->map as $tuple) { + if (Comparator::equals($tuple[0], $key)) { + return true; + } + } + + return false; } /** @@ -332,8 +382,8 @@ public function containsKey(string $key): bool */ public function containsValue($value): bool { - foreach ($this->map as $v) { - if (Comparator::equals($v, $value)) { + foreach ($this->map as $tuple) { + if (Comparator::equals($tuple[1], $value)) { return true; } } @@ -354,14 +404,17 @@ public function merge(self $map): self return $this; } - return $map->fold($this, function (Map $result, Tuple $entry) { + return $map->fold($this, function (Map $result, Tuple2 $entry) { return !$result->containsKey($entry[0]) ? $result->put($entry[0], $entry[1]) : $result; }); } + /** + * @param K $offset + */ public function offsetExists(mixed $offset): bool { - return isset($this->map[$offset]); + return $this->findPosition($offset) !== false; } /** @@ -371,7 +424,12 @@ public function offsetExists(mixed $offset): bool */ public function offsetGet(mixed $offset): mixed { - return $this->map[$offset] ?? throw new NoSuchElementException(); + $position = $this->findPosition($offset); + if ($position === false) { + throw new NoSuchElementException(); + } + + return $this->map[$position][1]; } /** @@ -386,4 +444,33 @@ public function offsetUnset(mixed $offset): void { throw new UnsupportedOperationException(); } + + /** + * Will try to cast all keys to string, may result in data loss if the keys cannot be converted to a unique string. + * + * @return array + */ + public function toNativeArray(): array + { + $map = []; + foreach ($this->map as $tuple) { + $map[(string) $tuple[0]] = $tuple[1]; + } + + return $map; + } + + /** + * @param K $key + */ + private function findPosition(mixed $key): int|false + { + foreach ($this->map as $index => $tuple) { + if (Comparator::equals($tuple[0], $key)) { + return $index; + } + } + + return false; + } } diff --git a/tests/Collection/MapTest.php b/tests/Collection/MapTest.php index beab423..3deef27 100644 --- a/tests/Collection/MapTest.php +++ b/tests/Collection/MapTest.php @@ -12,6 +12,7 @@ use Munus\Control\Option; use Munus\Exception\NoSuchElementException; use Munus\Exception\UnsupportedOperationException; +use Munus\Tests\Stub\Event; use Munus\Tuple; use PHPUnit\Framework\TestCase; @@ -68,8 +69,8 @@ public function testMapHead(): void public function testMapTail(): void { - self::assertTrue(Map::fromArray(['a' => 'b', 'c' => 'd', 'e' => 'f'])->tail()->equals(Tuple::of('e', 'f'))); - self::assertTrue(Map::fromArray(['e' => 'f', 'a' => 'b'])->tail()->equals(Tuple::of('a', 'b'))); + self::assertTrue(Map::fromArray(['a' => 'b', 'c' => 'd', 'e' => 'f'])->tail()->equals(Map::fromArray(['c' => 'd', 'e' => 'f']))); + self::assertTrue(Map::fromArray(['e' => 'f', 'a' => 'b'])->tail()->equals(Map::fromArray(['a' => 'b']))); $this->expectException(NoSuchElementException::class); Map::empty()->tail(); @@ -277,7 +278,7 @@ public function testMapToStream(): void public function testMapToArray(): void { - self::assertEquals(['a' => 'b', 'c' => 'd'], Map::fromArray(['a' => 'b', 'c' => 'd'])->toArray()); + self::assertEquals([Tuple::of('a', 'b'), Tuple::of('c', 'd')], Map::fromArray(['a' => 'b', 'c' => 'd'])->toArray()); } public function testMapSorted(): void @@ -330,4 +331,36 @@ public function testMapArrayAccessOffsetUnset(): void unset($map['a']); } + + public function testMapWithObjects(): void + { + $event1 = new Event('1', 'same'); + $event2 = new Event('2', 'same'); + $event3 = new Event('2', 'different'); + + $map = Map::empty(); + $map = $map->put($event1, 'magic'); + + self::assertSame('magic', $map->get($event1)->getOrNull()); + self::assertSame('magic', $map[$event1]); + self::assertSame('magic', $map->get($event2)->getOrNull()); + self::assertSame('magic', $map[$event2]); + self::assertNull($map->get($event3)->getOrNull()); + + self::assertTrue(Set::of($event1)->equals($map->keys())); + self::assertTrue(Stream::of('magic')->equals($map->values())); + } + + public function testMapToNativeArray(): void + { + self::assertSame(['a' => 'b', 'c' => 'd'], Map::fromArray(['a' => 'b', 'c' => 'd'])->toNativeArray()); + + $map = Map::from([Tuple::of(new Event('a', 'same'), 'one'), Tuple::of(new Event('b', 'same'), 'two')]); + + self::assertSame(['a' => 'one', 'b' => 'two'], $map->toNativeArray()); + + $map = Map::from([Tuple::of(new Event('a', 'same'), 'one'), Tuple::of(new Event('a', 'same'), 'two')]); + + self::assertSame(['a' => 'two'], $map->toNativeArray()); + } } diff --git a/tests/Stub/Event.php b/tests/Stub/Event.php index be86914..5d334a1 100644 --- a/tests/Stub/Event.php +++ b/tests/Stub/Event.php @@ -6,7 +6,7 @@ use Munus\Value\Comparable; -final class Event implements Comparable +final class Event implements Comparable, \Stringable { public function __construct(public string $id, public string $name) { @@ -16,4 +16,9 @@ public function equals(mixed $other): bool { return self::class === $other::class && $this->name === $other->name; } + + public function __toString(): string + { + return $this->id; + } }