Skip to content

Commit 320b488

Browse files
authored
Merge pull request #66 from boesing/feature/map-key-exchange
feature: introduce `MapInterface::keyExchange`
2 parents 138185f + 145ce77 commit 320b488

File tree

4 files changed

+157
-8
lines changed

4 files changed

+157
-8
lines changed

src/Map.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,12 @@ public function group(callable $callback): MapInterface
359359
foreach ($this->data as $key => $value) {
360360
$groupIdentifier = $callback($value);
361361
try {
362-
/** @psalm-suppress ImpureMethodCall */
363362
$group = $groups->get($groupIdentifier);
364363
} catch (OutOfBoundsException $exception) {
365364
$group = clone $this;
366365
$group->data = [];
367366
}
368367

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

@@ -429,4 +427,31 @@ public function join(string $separator = ''): string
429427
throw new RuntimeException('Could not join map.', 0, $throwable);
430428
}
431429
}
430+
431+
/**
432+
* @template TNewKey of string
433+
* @param callable(TKey,TValue):TNewKey $keyGenerator
434+
*
435+
* @return MapInterface<TNewKey,TValue>
436+
* @throws RuntimeException if a new key is being generated more than once.
437+
*/
438+
public function keyExchange(callable $keyGenerator): MapInterface
439+
{
440+
/** @var MapInterface<TNewKey,TValue> $exchanged */
441+
$exchanged = new GenericMap();
442+
443+
foreach ($this->data as $key => $value) {
444+
$newKey = $keyGenerator($key, $value);
445+
if ($exchanged->has($newKey)) {
446+
throw new RuntimeException(sprintf(
447+
'Provided key generator generates the same key "%s" multiple times.',
448+
$newKey
449+
));
450+
}
451+
452+
$exchanged = $exchanged->put($newKey, $value);
453+
}
454+
455+
return $exchanged;
456+
}
432457
}

src/MapInterface.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Error;
88
use JsonSerializable;
99
use OutOfBoundsException;
10+
use RuntimeException;
1011

1112
/**
1213
* @template TKey of string
@@ -90,13 +91,15 @@ public function keys(): OrderedListInterface;
9091
* @psalm-param TKey $key
9192
* @psalm-param TValue $value
9293
* @psalm-return MapInterface<TKey,TValue>
94+
* @psalm-mutation-free
9395
*/
9496
public function put($key, $value): MapInterface;
9597

9698
/**
9799
* @psalm-param TKey $key
98100
* @psalm-return TValue
99101
* @throws OutOfBoundsException if key does not exist.
102+
* @psalm-pure
100103
*/
101104
public function get(string $key);
102105

@@ -128,6 +131,7 @@ public function intersectUserAssoc(
128131

129132
/**
130133
* @psalm-param TKey $key
134+
* @psalm-mutation-free
131135
*/
132136
public function has(string $key): bool;
133137

@@ -170,4 +174,13 @@ public function sortByKey(?callable $sorter = null): MapInterface;
170174
* @throws Error In case, the values are not `string` or {@see Stringable}.
171175
*/
172176
public function join(string $separator = ''): string;
177+
178+
/**
179+
* @template TNewKey of string
180+
* @param callable(TKey,TValue):TNewKey $keyGenerator
181+
*
182+
* @return MapInterface<TNewKey,TValue>
183+
* @throws RuntimeException if a new key is being generated more than once.
184+
*/
185+
public function keyExchange(callable $keyGenerator): MapInterface;
173186
}

src/OrderedList.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ public function unify(
220220
foreach ($instance->data as $value) {
221221
$identifier = $unificationIdentifierGenerator($value);
222222
try {
223-
/** @psalm-suppress ImpureMethodCall */
224223
$unique = $unified->get($identifier);
225224
} catch (OutOfBoundsException $exception) {
226225
$unique = $value;
@@ -230,7 +229,6 @@ public function unify(
230229
$unique = $callback($unique, $value);
231230
}
232231

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

@@ -332,17 +330,14 @@ public function group(callable $callback): MapInterface
332330
$groups = new GenericMap([]);
333331
foreach ($this as $value) {
334332
$groupName = $callback($value);
335-
/** @psalm-suppress ImpureMethodCall */
336333
if (! $groups->has($groupName)) {
337334
$groups = $groups->put($groupName, new GenericOrderedList([$value]));
338335
continue;
339336
}
340337

341-
/** @psalm-suppress ImpureMethodCall */
342338
$existingGroup = $groups->get($groupName);
343339
$existingGroup = $existingGroup->add($value);
344-
/** @psalm-suppress ImpureMethodCall */
345-
$groups = $groups->put($groupName, $existingGroup);
340+
$groups = $groups->put($groupName, $existingGroup);
346341
}
347342

348343
return $groups;

tests/GenericMapTest.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use function in_array;
2222
use function json_encode;
2323
use function ord;
24+
use function sprintf;
2425
use function strlen;
2526
use function strnatcmp;
2627
use function trim;
@@ -32,6 +33,95 @@ final class GenericMapTest extends TestCase
3233
/** @var int */
3334
private $iteration;
3435

36+
/**
37+
* @psalm-return Generator<non-empty-string,array{
38+
* 0: array<non-empty-string,mixed>,
39+
* 1: callable(non-empty-string):non-empty-string,
40+
* 2: array<non-empty-string,mixed>
41+
* }>
42+
*/
43+
public function exchangeKeys(): Generator
44+
{
45+
yield 'change all keys' => [
46+
[
47+
'foo' => 'bar',
48+
'bar' => 'baz',
49+
'qoo' => 'ooq',
50+
],
51+
static function (string $key): string {
52+
switch ($key) {
53+
case 'foo':
54+
return 'bar';
55+
56+
case 'bar':
57+
return 'baz';
58+
59+
case 'qoo':
60+
return 'ooq';
61+
}
62+
63+
self::fail(sprintf('Key "%s" is not handled here.', $key));
64+
},
65+
[
66+
'bar' => 'bar',
67+
'baz' => 'baz',
68+
'ooq' => 'ooq',
69+
],
70+
];
71+
72+
yield 'change some keys' => [
73+
[
74+
'foo' => 'bar',
75+
'bar' => 'baz',
76+
'qoo' => 'ooq',
77+
],
78+
static function (string $key): string {
79+
switch ($key) {
80+
case 'foo':
81+
return 'fooo';
82+
83+
case 'bar':
84+
return $key;
85+
86+
case 'qoo':
87+
return 'ooq';
88+
}
89+
90+
self::fail(sprintf('Key "%s" is not handled here.', $key));
91+
},
92+
[
93+
'fooo' => 'bar',
94+
'bar' => 'baz',
95+
'ooq' => 'ooq',
96+
],
97+
];
98+
99+
yield 'change one key' => [
100+
[
101+
'foo' => 'bar',
102+
'bar' => 'baz',
103+
'qoo' => 'ooq',
104+
],
105+
static function (string $key): string {
106+
switch ($key) {
107+
case 'bar':
108+
case 'foo':
109+
return $key;
110+
111+
case 'qoo':
112+
return 'ooq';
113+
}
114+
115+
self::fail(sprintf('Key "%s" is not handled here.', $key));
116+
},
117+
[
118+
'foo' => 'bar',
119+
'bar' => 'baz',
120+
'ooq' => 'ooq',
121+
],
122+
];
123+
}
124+
35125
protected function setUp(): void
36126
{
37127
parent::setUp();
@@ -493,6 +583,7 @@ public function testGetThrowsOutOfBoundsExceptionWhenKeyDoesNotExist(): void
493583
/** @var MapInterface<string,string> $map */
494584
$map = new GenericMap([]);
495585
$this->expectException(OutOfBoundsException::class);
586+
/** @psalm-suppress UnusedMethodCall */
496587
$map->get('foo');
497588
}
498589

@@ -1000,4 +1091,29 @@ public function testWillJoinValuesWithSeperator(): void
10001091

10011092
self::assertSame('foo:bar:baz', $map->join(':'));
10021093
}
1094+
1095+
/**
1096+
* @psalm-param array<non-empty-string,mixed> $initial
1097+
* @psalm-param callable(non-empty-string):non-empty-string $keyGenerator
1098+
* @psalm-param array<non-empty-string,mixed> $expected
1099+
*
1100+
* @dataProvider exchangeKeys
1101+
*/
1102+
public function testWillExchangeKeys(array $initial, callable $keyGenerator, array $expected): void
1103+
{
1104+
$map = new GenericMap($initial);
1105+
$exchanged = $map->keyExchange($keyGenerator);
1106+
self::assertEquals($expected, $exchanged->toNativeArray());
1107+
}
1108+
1109+
public function testWillDetectDuplicatedKeys(): void
1110+
{
1111+
$map = new GenericMap(['foo' => 'bar', 'bar' => 'baz']);
1112+
$this->expectException(RuntimeException::class);
1113+
$this->expectExceptionMessage('Provided key generator generates the same key "foo" multiple times.');
1114+
/** @psalm-suppress UnusedMethodCall */
1115+
$map->keyExchange(static function (): string {
1116+
return 'foo';
1117+
});
1118+
}
10031119
}

0 commit comments

Comments
 (0)