Skip to content

Commit

Permalink
Merge pull request #66 from boesing/feature/map-key-exchange
Browse files Browse the repository at this point in the history
feature: introduce `MapInterface::keyExchange`
  • Loading branch information
boesing committed May 26, 2021
2 parents 138185f + 145ce77 commit 320b488
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 8 deletions.
29 changes: 27 additions & 2 deletions src/Map.php
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,12 @@ public function group(callable $callback): MapInterface
foreach ($this->data as $key => $value) {
$groupIdentifier = $callback($value);
try {
/** @psalm-suppress ImpureMethodCall */
$group = $groups->get($groupIdentifier);
} catch (OutOfBoundsException $exception) {
$group = clone $this;
$group->data = [];
}

/** @psalm-suppress ImpureMethodCall */
$groups = $groups->put($groupIdentifier, $group->put($key, $value));
}

Expand Down Expand Up @@ -429,4 +427,31 @@ public function join(string $separator = ''): string
throw new RuntimeException('Could not join map.', 0, $throwable);
}
}

/**
* @template TNewKey of string
* @param callable(TKey,TValue):TNewKey $keyGenerator
*
* @return MapInterface<TNewKey,TValue>
* @throws RuntimeException if a new key is being generated more than once.
*/
public function keyExchange(callable $keyGenerator): MapInterface
{
/** @var MapInterface<TNewKey,TValue> $exchanged */
$exchanged = new GenericMap();

foreach ($this->data as $key => $value) {
$newKey = $keyGenerator($key, $value);
if ($exchanged->has($newKey)) {
throw new RuntimeException(sprintf(
'Provided key generator generates the same key "%s" multiple times.',
$newKey
));
}

$exchanged = $exchanged->put($newKey, $value);
}

return $exchanged;
}
}
13 changes: 13 additions & 0 deletions src/MapInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Error;
use JsonSerializable;
use OutOfBoundsException;
use RuntimeException;

/**
* @template TKey of string
Expand Down Expand Up @@ -90,13 +91,15 @@ public function keys(): OrderedListInterface;
* @psalm-param TKey $key
* @psalm-param TValue $value
* @psalm-return MapInterface<TKey,TValue>
* @psalm-mutation-free
*/
public function put($key, $value): MapInterface;

/**
* @psalm-param TKey $key
* @psalm-return TValue
* @throws OutOfBoundsException if key does not exist.
* @psalm-pure
*/
public function get(string $key);

Expand Down Expand Up @@ -128,6 +131,7 @@ public function intersectUserAssoc(

/**
* @psalm-param TKey $key
* @psalm-mutation-free
*/
public function has(string $key): bool;

Expand Down Expand Up @@ -170,4 +174,13 @@ public function sortByKey(?callable $sorter = null): MapInterface;
* @throws Error In case, the values are not `string` or {@see Stringable}.
*/
public function join(string $separator = ''): string;

/**
* @template TNewKey of string
* @param callable(TKey,TValue):TNewKey $keyGenerator
*
* @return MapInterface<TNewKey,TValue>
* @throws RuntimeException if a new key is being generated more than once.
*/
public function keyExchange(callable $keyGenerator): MapInterface;
}
7 changes: 1 addition & 6 deletions src/OrderedList.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ public function unify(
foreach ($instance->data as $value) {
$identifier = $unificationIdentifierGenerator($value);
try {
/** @psalm-suppress ImpureMethodCall */
$unique = $unified->get($identifier);
} catch (OutOfBoundsException $exception) {
$unique = $value;
Expand All @@ -230,7 +229,6 @@ public function unify(
$unique = $callback($unique, $value);
}

/** @psalm-suppress ImpureMethodCall */
$unified = $unified->put($identifier, $unique);
}

Expand Down Expand Up @@ -332,17 +330,14 @@ public function group(callable $callback): MapInterface
$groups = new GenericMap([]);
foreach ($this as $value) {
$groupName = $callback($value);
/** @psalm-suppress ImpureMethodCall */
if (! $groups->has($groupName)) {
$groups = $groups->put($groupName, new GenericOrderedList([$value]));
continue;
}

/** @psalm-suppress ImpureMethodCall */
$existingGroup = $groups->get($groupName);
$existingGroup = $existingGroup->add($value);
/** @psalm-suppress ImpureMethodCall */
$groups = $groups->put($groupName, $existingGroup);
$groups = $groups->put($groupName, $existingGroup);
}

return $groups;
Expand Down
116 changes: 116 additions & 0 deletions tests/GenericMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use function in_array;
use function json_encode;
use function ord;
use function sprintf;
use function strlen;
use function strnatcmp;
use function trim;
Expand All @@ -32,6 +33,95 @@ final class GenericMapTest extends TestCase
/** @var int */
private $iteration;

/**
* @psalm-return Generator<non-empty-string,array{
* 0: array<non-empty-string,mixed>,
* 1: callable(non-empty-string):non-empty-string,
* 2: array<non-empty-string,mixed>
* }>
*/
public function exchangeKeys(): Generator
{
yield 'change all keys' => [
[
'foo' => 'bar',
'bar' => 'baz',
'qoo' => 'ooq',
],
static function (string $key): string {
switch ($key) {
case 'foo':
return 'bar';

case 'bar':
return 'baz';

case 'qoo':
return 'ooq';
}

self::fail(sprintf('Key "%s" is not handled here.', $key));
},
[
'bar' => 'bar',
'baz' => 'baz',
'ooq' => 'ooq',
],
];

yield 'change some keys' => [
[
'foo' => 'bar',
'bar' => 'baz',
'qoo' => 'ooq',
],
static function (string $key): string {
switch ($key) {
case 'foo':
return 'fooo';

case 'bar':
return $key;

case 'qoo':
return 'ooq';
}

self::fail(sprintf('Key "%s" is not handled here.', $key));
},
[
'fooo' => 'bar',
'bar' => 'baz',
'ooq' => 'ooq',
],
];

yield 'change one key' => [
[
'foo' => 'bar',
'bar' => 'baz',
'qoo' => 'ooq',
],
static function (string $key): string {
switch ($key) {
case 'bar':
case 'foo':
return $key;

case 'qoo':
return 'ooq';
}

self::fail(sprintf('Key "%s" is not handled here.', $key));
},
[
'foo' => 'bar',
'bar' => 'baz',
'ooq' => 'ooq',
],
];
}

protected function setUp(): void
{
parent::setUp();
Expand Down Expand Up @@ -493,6 +583,7 @@ public function testGetThrowsOutOfBoundsExceptionWhenKeyDoesNotExist(): void
/** @var MapInterface<string,string> $map */
$map = new GenericMap([]);
$this->expectException(OutOfBoundsException::class);
/** @psalm-suppress UnusedMethodCall */
$map->get('foo');
}

Expand Down Expand Up @@ -1000,4 +1091,29 @@ public function testWillJoinValuesWithSeperator(): void

self::assertSame('foo:bar:baz', $map->join(':'));
}

/**
* @psalm-param array<non-empty-string,mixed> $initial
* @psalm-param callable(non-empty-string):non-empty-string $keyGenerator
* @psalm-param array<non-empty-string,mixed> $expected
*
* @dataProvider exchangeKeys
*/
public function testWillExchangeKeys(array $initial, callable $keyGenerator, array $expected): void
{
$map = new GenericMap($initial);
$exchanged = $map->keyExchange($keyGenerator);
self::assertEquals($expected, $exchanged->toNativeArray());
}

public function testWillDetectDuplicatedKeys(): void
{
$map = new GenericMap(['foo' => 'bar', 'bar' => 'baz']);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Provided key generator generates the same key "foo" multiple times.');
/** @psalm-suppress UnusedMethodCall */
$map->keyExchange(static function (): string {
return 'foo';
});
}
}

0 comments on commit 320b488

Please sign in to comment.