Inital commit after splitting from project
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\ArrayDiff;
|
||||
use Tez\Utils\Arr\ChangeType;
|
||||
use Tez\Utils\Arr\DiffResult;
|
||||
|
||||
final class ArrayDiffTest extends TestCase
|
||||
{
|
||||
public function testIdenticalArraysHaveNoChanges(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(['a' => 1], ['a' => 1]);
|
||||
|
||||
self::assertInstanceOf(DiffResult::class, $result);
|
||||
self::assertFalse($result->hasChanges());
|
||||
self::assertSame([], $result->changes());
|
||||
self::assertSame([], $result->paths());
|
||||
}
|
||||
|
||||
public function testDetectsChangedScalarValue(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['user' => ['name' => 'Anna', 'city' => 'Berlin']],
|
||||
['user' => ['name' => 'Anna', 'city' => 'Hamburg']],
|
||||
);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
self::assertCount(1, $result->changes());
|
||||
|
||||
$change = $result->changes()[0];
|
||||
self::assertSame('user.city', $change->path);
|
||||
self::assertSame(ChangeType::Changed, $change->type);
|
||||
self::assertSame('Berlin', $change->old);
|
||||
self::assertSame('Hamburg', $change->new);
|
||||
self::assertSame(['user.city'], $result->paths());
|
||||
}
|
||||
|
||||
public function testDetectsAddedKey(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['a' => 1],
|
||||
['a' => 1, 'b' => 2],
|
||||
);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
$change = $result->changes()[0];
|
||||
self::assertSame('b', $change->path);
|
||||
self::assertSame(ChangeType::Added, $change->type);
|
||||
self::assertNull($change->old);
|
||||
self::assertSame(2, $change->new);
|
||||
}
|
||||
|
||||
public function testDetectsRemovedKey(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['a' => 1, 'b' => 2],
|
||||
['a' => 1],
|
||||
);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
$change = $result->changes()[0];
|
||||
self::assertSame('b', $change->path);
|
||||
self::assertSame(ChangeType::Removed, $change->type);
|
||||
self::assertSame(2, $change->old);
|
||||
self::assertNull($change->new);
|
||||
}
|
||||
|
||||
public function testDeepNestedPath(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['a' => ['b' => ['c' => 'old']]],
|
||||
['a' => ['b' => ['c' => 'new']]],
|
||||
);
|
||||
|
||||
self::assertSame(['a.b.c'], $result->paths());
|
||||
self::assertSame(ChangeType::Changed, $result->changes()[0]->type);
|
||||
}
|
||||
|
||||
public function testStrictTypeCheckIntVsString(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(['x' => 1], ['x' => '1']);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
self::assertSame(ChangeType::Changed, $result->changes()[0]->type);
|
||||
}
|
||||
|
||||
public function testStrictTypeCheckIntVsTrue(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(['x' => 1], ['x' => true]);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
}
|
||||
|
||||
public function testMultipleDifferencesReported(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['a' => 1, 'b' => 2, 'c' => 3],
|
||||
['a' => 1, 'b' => 99, 'c' => 3, 'd' => 4],
|
||||
);
|
||||
|
||||
self::assertCount(2, $result->changes());
|
||||
self::assertContains('b', $result->paths());
|
||||
self::assertContains('d', $result->paths());
|
||||
}
|
||||
|
||||
public function testNestedAddedAndRemovedKeys(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['user' => ['name' => 'Anna', 'age' => 30]],
|
||||
['user' => ['name' => 'Anna', 'email' => 'anna@example.com']],
|
||||
);
|
||||
|
||||
$paths = $result->paths();
|
||||
self::assertContains('user.age', $paths);
|
||||
self::assertContains('user.email', $paths);
|
||||
|
||||
$types = array_map(
|
||||
static fn($c) => $c->type,
|
||||
$result->changes(),
|
||||
);
|
||||
self::assertContains(ChangeType::Removed, $types);
|
||||
self::assertContains(ChangeType::Added, $types);
|
||||
}
|
||||
|
||||
public function testEmptyArraysProduceNoDiff(): void
|
||||
{
|
||||
$result = ArrayDiff::compare([], []);
|
||||
|
||||
self::assertFalse($result->hasChanges());
|
||||
}
|
||||
|
||||
public function testScalarToArrayIsReportedAsChange(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['x' => 'scalar'],
|
||||
['x' => ['nested' => 'value']],
|
||||
);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
self::assertSame(ChangeType::Changed, $result->changes()[0]->type);
|
||||
}
|
||||
|
||||
public function testArrayToScalarIsReportedAsChange(): void
|
||||
{
|
||||
$result = ArrayDiff::compare(
|
||||
['x' => ['nested' => 'value']],
|
||||
['x' => 'scalar'],
|
||||
);
|
||||
|
||||
self::assertTrue($result->hasChanges());
|
||||
self::assertSame(ChangeType::Changed, $result->changes()[0]->type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\CartesianProduct;
|
||||
|
||||
final class CartesianProductTest extends TestCase
|
||||
{
|
||||
public function testEmptySetsYieldsOneEmptyTuple(): void
|
||||
{
|
||||
$result = iterator_to_array(CartesianProduct::create(), false);
|
||||
|
||||
self::assertSame([[]], $result);
|
||||
}
|
||||
|
||||
public function testSingleSet(): void
|
||||
{
|
||||
$result = iterator_to_array(CartesianProduct::create([1, 2, 3]), false);
|
||||
|
||||
self::assertSame([[1], [2], [3]], $result);
|
||||
}
|
||||
|
||||
public function testTwoSets(): void
|
||||
{
|
||||
$result = iterator_to_array(CartesianProduct::create([1, 2], ['a', 'b']), false);
|
||||
|
||||
self::assertSame(
|
||||
[[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']],
|
||||
$result,
|
||||
);
|
||||
}
|
||||
|
||||
public function testThreeSets(): void
|
||||
{
|
||||
$result = iterator_to_array(
|
||||
CartesianProduct::create(['x', 'y'], [1, 2], ['a', 'b']),
|
||||
false,
|
||||
);
|
||||
|
||||
self::assertCount(8, $result);
|
||||
self::assertSame(['x', 1, 'a'], $result[0]);
|
||||
self::assertSame(['x', 1, 'b'], $result[1]);
|
||||
self::assertSame(['x', 2, 'a'], $result[2]);
|
||||
self::assertSame(['x', 2, 'b'], $result[3]);
|
||||
self::assertSame(['y', 1, 'a'], $result[4]);
|
||||
self::assertSame(['y', 2, 'b'], $result[7]);
|
||||
}
|
||||
|
||||
public function testAssociativeArrayKeysAreStripped(): void
|
||||
{
|
||||
$result = iterator_to_array(
|
||||
CartesianProduct::create(['foo' => 'A', 'bar' => 'B'], ['x' => 1, 'y' => 2]),
|
||||
false,
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
[['A', 1], ['A', 2], ['B', 1], ['B', 2]],
|
||||
$result,
|
||||
);
|
||||
}
|
||||
|
||||
public function testTotalCountEqualsProductOfSetSizes(): void
|
||||
{
|
||||
$result = iterator_to_array(CartesianProduct::create([1, 2, 3], ['a', 'b'], [true, false]), false);
|
||||
|
||||
self::assertCount(3 * 2 * 2, $result);
|
||||
}
|
||||
|
||||
public function testSingleElementSets(): void
|
||||
{
|
||||
$result = iterator_to_array(CartesianProduct::create(['only'], [42]), false);
|
||||
|
||||
self::assertSame([['only', 42]], $result);
|
||||
}
|
||||
|
||||
public function testGeneratorAsInput(): void
|
||||
{
|
||||
$gen = (static function (): \Generator {
|
||||
yield 10;
|
||||
yield 20;
|
||||
})();
|
||||
|
||||
$result = iterator_to_array(CartesianProduct::create([1, 2], $gen), false);
|
||||
|
||||
self::assertSame([[1, 10], [1, 20], [2, 10], [2, 20]], $result);
|
||||
}
|
||||
|
||||
public function testReturnsGenerator(): void
|
||||
{
|
||||
$generator = CartesianProduct::create([1], [2]);
|
||||
|
||||
self::assertInstanceOf(\Generator::class, $generator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\DeepMerge;
|
||||
|
||||
final class DeepMergeTest extends TestCase
|
||||
{
|
||||
public function testMergesTopLevelScalars(): void
|
||||
{
|
||||
$result = DeepMerge::merge(
|
||||
['a' => 1, 'b' => 2],
|
||||
['b' => 99, 'c' => 3],
|
||||
);
|
||||
|
||||
self::assertSame(['a' => 1, 'b' => 99, 'c' => 3], $result);
|
||||
}
|
||||
|
||||
public function testMergesNestedArraysRecursively(): void
|
||||
{
|
||||
$result = DeepMerge::merge(
|
||||
['db' => ['host' => 'localhost', 'port' => 3306]],
|
||||
['db' => ['host' => 'db.prod']],
|
||||
);
|
||||
|
||||
self::assertSame(['db' => ['host' => 'db.prod', 'port' => 3306]], $result);
|
||||
}
|
||||
|
||||
public function testBaseOnlyKeysAreKept(): void
|
||||
{
|
||||
$result = DeepMerge::merge(
|
||||
['a' => 1, 'b' => 2],
|
||||
['b' => 99],
|
||||
);
|
||||
|
||||
self::assertSame(1, $result['a']);
|
||||
}
|
||||
|
||||
public function testOverrideOnlyKeysAreAdded(): void
|
||||
{
|
||||
$result = DeepMerge::merge(['a' => 1], ['b' => 2]);
|
||||
|
||||
self::assertSame(['a' => 1, 'b' => 2], $result);
|
||||
}
|
||||
|
||||
public function testNullOverwrites(): void
|
||||
{
|
||||
$result = DeepMerge::merge(['a' => 'value'], ['a' => null]);
|
||||
|
||||
self::assertNull($result['a']);
|
||||
}
|
||||
|
||||
public function testIntegerKeysAreAppended(): void
|
||||
{
|
||||
$result = DeepMerge::merge([1, 2, 3], [4, 5]);
|
||||
|
||||
self::assertSame([1, 2, 3, 4, 5], $result);
|
||||
}
|
||||
|
||||
public function testMultipleOverridesAppliedLeftToRight(): void
|
||||
{
|
||||
$result = DeepMerge::merge(
|
||||
['a' => 1],
|
||||
['a' => 2],
|
||||
['a' => 3],
|
||||
);
|
||||
|
||||
self::assertSame(3, $result['a']);
|
||||
}
|
||||
|
||||
public function testDeepNestedMerge(): void
|
||||
{
|
||||
$defaults = [
|
||||
'cache' => ['ttl' => 3600, 'driver' => 'file'],
|
||||
'debug' => false,
|
||||
];
|
||||
$env = [
|
||||
'cache' => ['driver' => 'redis'],
|
||||
'debug' => true,
|
||||
];
|
||||
|
||||
$result = DeepMerge::merge($defaults, $env);
|
||||
|
||||
/** @var array<string, mixed> $cache */
|
||||
$cache = $result['cache'];
|
||||
self::assertSame(3600, $cache['ttl']);
|
||||
self::assertSame('redis', $cache['driver']);
|
||||
self::assertTrue($result['debug']);
|
||||
}
|
||||
|
||||
public function testOverridingArrayWithScalarWins(): void
|
||||
{
|
||||
$result = DeepMerge::merge(
|
||||
['key' => ['nested' => 1]],
|
||||
['key' => 'scalar'],
|
||||
);
|
||||
|
||||
self::assertSame('scalar', $result['key']);
|
||||
}
|
||||
|
||||
public function testEmptyOverrideReturnsBase(): void
|
||||
{
|
||||
$base = ['a' => 1, 'b' => 2];
|
||||
self::assertSame($base, DeepMerge::merge($base));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Find;
|
||||
|
||||
final class FindTest extends TestCase
|
||||
{
|
||||
public function testFirstReturnsFirstMatch(): void
|
||||
{
|
||||
$items = [1, 2, 3, 4, 5];
|
||||
|
||||
$result = Find::first($items, fn($v) => $v > 2);
|
||||
|
||||
self::assertSame(3, $result);
|
||||
}
|
||||
|
||||
public function testFirstReturnsNullWhenNoMatch(): void
|
||||
{
|
||||
/** @var list<int> $items */
|
||||
$items = [1, 2, 3];
|
||||
$result = Find::first($items, fn($v) => $v > 10);
|
||||
|
||||
self::assertNull($result);
|
||||
}
|
||||
|
||||
public function testFirstReturnsNullOnEmptyArray(): void
|
||||
{
|
||||
$result = Find::first([], fn($v) => true);
|
||||
|
||||
self::assertNull($result);
|
||||
}
|
||||
|
||||
public function testFirstStopsAtFirstMatch(): void
|
||||
{
|
||||
$calls = 0;
|
||||
$items = [1, 2, 3, 4, 5];
|
||||
|
||||
Find::first($items, function ($v) use (&$calls): bool {
|
||||
$calls++;
|
||||
|
||||
return $v === 2;
|
||||
});
|
||||
|
||||
self::assertSame(2, $calls);
|
||||
}
|
||||
|
||||
public function testFirstWithAssociativeArray(): void
|
||||
{
|
||||
$items = ['a' => 1, 'b' => 2, 'c' => 3];
|
||||
|
||||
$result = Find::first($items, fn($v) => $v === 2);
|
||||
|
||||
self::assertSame(2, $result);
|
||||
}
|
||||
|
||||
public function testFirstWithObjects(): void
|
||||
{
|
||||
$a = new \stdClass();
|
||||
$a->id = 1;
|
||||
$b = new \stdClass();
|
||||
$b->id = 2;
|
||||
|
||||
$result = Find::first([$a, $b], fn($o) => $o->id === 2);
|
||||
|
||||
self::assertSame($b, $result);
|
||||
}
|
||||
|
||||
public function testLastReturnsLastMatch(): void
|
||||
{
|
||||
$items = [1, 2, 3, 4, 5];
|
||||
|
||||
$result = Find::last($items, fn($v) => $v < 4);
|
||||
|
||||
self::assertSame(3, $result);
|
||||
}
|
||||
|
||||
public function testLastReturnsNullWhenNoMatch(): void
|
||||
{
|
||||
/** @var list<int> $items */
|
||||
$items = [1, 2, 3];
|
||||
$result = Find::last($items, fn($v) => $v > 10);
|
||||
|
||||
self::assertNull($result);
|
||||
}
|
||||
|
||||
public function testLastReturnsNullOnEmptyArray(): void
|
||||
{
|
||||
$result = Find::last([], fn($v) => true);
|
||||
|
||||
self::assertNull($result);
|
||||
}
|
||||
|
||||
public function testLastWithObjects(): void
|
||||
{
|
||||
$a = new \stdClass();
|
||||
$a->active = true;
|
||||
$b = new \stdClass();
|
||||
$b->active = false;
|
||||
$c = new \stdClass();
|
||||
$c->active = true;
|
||||
|
||||
$result = Find::last([$a, $b, $c], fn($o) => $o->active);
|
||||
|
||||
self::assertSame($c, $result);
|
||||
}
|
||||
|
||||
public function testFirstAndLastDifferWhenMultipleMatches(): void
|
||||
{
|
||||
$items = [10, 20, 30, 40, 50];
|
||||
|
||||
$first = Find::first($items, fn($v) => $v % 20 === 0);
|
||||
$last = Find::last($items, fn($v) => $v % 20 === 0);
|
||||
|
||||
self::assertSame(20, $first);
|
||||
self::assertSame(40, $last);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Flatten;
|
||||
|
||||
final class FlattenTest extends TestCase
|
||||
{
|
||||
public function testFlattensFullyByDefault(): void
|
||||
{
|
||||
$result = Flatten::flatten([1, [2, 3], [4, [5, 6]]]);
|
||||
|
||||
self::assertSame([1, 2, 3, 4, 5, 6], $result);
|
||||
}
|
||||
|
||||
public function testDepthOne(): void
|
||||
{
|
||||
$result = Flatten::flatten([1, [2, 3], [4, [5, 6]]], depth: 1);
|
||||
|
||||
self::assertSame([1, 2, 3, 4, [5, 6]], $result);
|
||||
}
|
||||
|
||||
public function testDepthTwo(): void
|
||||
{
|
||||
$result = Flatten::flatten([1, [2, [3, [4]]]], depth: 2);
|
||||
|
||||
self::assertSame([1, 2, 3, [4]], $result);
|
||||
}
|
||||
|
||||
public function testAlreadyFlatArrayIsUnchanged(): void
|
||||
{
|
||||
$result = Flatten::flatten([1, 2, 3]);
|
||||
|
||||
self::assertSame([1, 2, 3], $result);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Flatten::flatten([]));
|
||||
}
|
||||
|
||||
public function testAssociativeKeysAreDiscarded(): void
|
||||
{
|
||||
$result = Flatten::flatten([['a' => 1, 'b' => 2], ['c' => 3]]);
|
||||
|
||||
self::assertSame([1, 2, 3], $result);
|
||||
}
|
||||
|
||||
public function testDepthZeroReturnsOriginalValues(): void
|
||||
{
|
||||
$result = Flatten::flatten([[1, 2], [3, 4]], depth: 0);
|
||||
|
||||
self::assertSame([[1, 2], [3, 4]], $result);
|
||||
}
|
||||
|
||||
public function testMixedNestingDepths(): void
|
||||
{
|
||||
$result = Flatten::flatten([1, [2, [3]], 4, [[5]]]);
|
||||
|
||||
self::assertSame([1, 2, 3, 4, 5], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\GroupBy;
|
||||
|
||||
final class GroupByTest extends TestCase
|
||||
{
|
||||
public function testGroupsByStringKey(): void
|
||||
{
|
||||
$users = [
|
||||
['name' => 'Alice', 'role' => 'admin'],
|
||||
['name' => 'Bob', 'role' => 'user'],
|
||||
['name' => 'Carol', 'role' => 'admin'],
|
||||
];
|
||||
|
||||
$result = GroupBy::group($users, fn($u) => $u['role']);
|
||||
|
||||
self::assertSame([
|
||||
'admin' => [
|
||||
['name' => 'Alice', 'role' => 'admin'],
|
||||
['name' => 'Carol', 'role' => 'admin'],
|
||||
],
|
||||
'user' => [
|
||||
['name' => 'Bob', 'role' => 'user'],
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testGroupsByIntKey(): void
|
||||
{
|
||||
$result = GroupBy::group(range(1, 6), fn($n) => $n % 2);
|
||||
|
||||
self::assertSame([
|
||||
1 => [1, 3, 5],
|
||||
0 => [2, 4, 6],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testGroupsPreserveDefinitionOrder(): void
|
||||
{
|
||||
$items = ['b', 'a', 'b', 'a', 'c'];
|
||||
$result = GroupBy::group($items, fn($v) => $v);
|
||||
|
||||
self::assertSame(['b', 'b'], $result['b']);
|
||||
self::assertSame(['a', 'a'], $result['a']);
|
||||
self::assertSame(['c'], $result['c']);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], GroupBy::group([], fn($v) => $v));
|
||||
}
|
||||
|
||||
public function testSingleGroupWhenAllItemsShareKey(): void
|
||||
{
|
||||
$result = GroupBy::group([1, 2, 3], fn() => 'same');
|
||||
|
||||
self::assertSame(['same' => [1, 2, 3]], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\KeyBy;
|
||||
|
||||
final class KeyByTest extends TestCase
|
||||
{
|
||||
public function testIndexByStringKey(): void
|
||||
{
|
||||
$users = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
];
|
||||
|
||||
$result = KeyBy::index($users, fn($u) => $u['name']);
|
||||
|
||||
self::assertSame([
|
||||
'Alice' => ['id' => 1, 'name' => 'Alice'],
|
||||
'Bob' => ['id' => 2, 'name' => 'Bob'],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testIndexByIntKey(): void
|
||||
{
|
||||
$users = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
];
|
||||
|
||||
$result = KeyBy::index($users, fn($u) => $u['id']);
|
||||
|
||||
self::assertSame(1, array_key_first($result));
|
||||
self::assertSame('Alice', $result[1]['name']);
|
||||
}
|
||||
|
||||
public function testLastItemWinsOnDuplicateKey(): void
|
||||
{
|
||||
$items = [
|
||||
['key' => 'x', 'val' => 1],
|
||||
['key' => 'x', 'val' => 2],
|
||||
];
|
||||
|
||||
$result = KeyBy::index($items, fn($i) => $i['key']);
|
||||
|
||||
self::assertSame(2, $result['x']['val']);
|
||||
}
|
||||
|
||||
public function testEmptyInputReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], KeyBy::index([], fn($v) => $v));
|
||||
}
|
||||
|
||||
public function testFullItemIsPreserved(): void
|
||||
{
|
||||
$items = [['id' => 1, 'name' => 'Alice', 'score' => 95]];
|
||||
|
||||
$result = KeyBy::index($items, fn($i) => $i['id']);
|
||||
|
||||
self::assertSame(['id' => 1, 'name' => 'Alice', 'score' => 95], $result[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\MapWithKeys;
|
||||
|
||||
final class MapWithKeysTest extends TestCase
|
||||
{
|
||||
public function testTransformsKeysAndValues(): void
|
||||
{
|
||||
/** @var list<array{id: int, name: string}> $users */
|
||||
$users = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
];
|
||||
|
||||
$result = MapWithKeys::map($users, function (mixed $u) {
|
||||
/** @var array{id: int, name: string} $u */
|
||||
return [$u['id'] => $u['name']];
|
||||
});
|
||||
|
||||
self::assertSame([1 => 'Alice', 2 => 'Bob'], $result);
|
||||
}
|
||||
|
||||
public function testSwapsKeyAndValue(): void
|
||||
{
|
||||
$result = MapWithKeys::map(['a' => 1, 'b' => 2], function (mixed $v, int|string $k) {
|
||||
/** @var int $v */
|
||||
return [$v => $k];
|
||||
});
|
||||
|
||||
self::assertSame([1 => 'a', 2 => 'b'], $result);
|
||||
}
|
||||
|
||||
public function testLastWinsOnDuplicateKey(): void
|
||||
{
|
||||
$result = MapWithKeys::map([1, 2, 3], fn($v) => ['key' => $v]);
|
||||
|
||||
self::assertSame(['key' => 3], $result);
|
||||
}
|
||||
|
||||
public function testEmptyInputReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], MapWithKeys::map([], fn($v) => ['x' => $v]));
|
||||
}
|
||||
|
||||
public function testReceivesOriginalKey(): void
|
||||
{
|
||||
$received = [];
|
||||
MapWithKeys::map(['x' => 10, 'y' => 20], function ($v, $k) use (&$received) {
|
||||
$received[] = $k;
|
||||
|
||||
return [$k => $v];
|
||||
});
|
||||
|
||||
self::assertSame(['x', 'y'], $received);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Page;
|
||||
use Tez\Utils\Arr\Paginator;
|
||||
|
||||
final class PaginatorTest extends TestCase
|
||||
{
|
||||
/** @return list<int> */
|
||||
private function range100(): array
|
||||
{
|
||||
return range(1, 100);
|
||||
}
|
||||
|
||||
public function testFirstPageReturnsCorrectSlice(): void
|
||||
{
|
||||
$page = Paginator::paginate($this->range100(), page: 1, perPage: 10);
|
||||
|
||||
self::assertInstanceOf(Page::class, $page);
|
||||
self::assertSame(range(1, 10), $page->items());
|
||||
self::assertSame(100, $page->total());
|
||||
self::assertSame(1, $page->currentPage());
|
||||
self::assertSame(10, $page->lastPage());
|
||||
}
|
||||
|
||||
public function testSecondPageReturnsCorrectSlice(): void
|
||||
{
|
||||
$page = Paginator::paginate($this->range100(), page: 2, perPage: 10);
|
||||
|
||||
self::assertSame(range(11, 20), $page->items());
|
||||
self::assertSame(2, $page->currentPage());
|
||||
}
|
||||
|
||||
public function testLastPageHasFewerItems(): void
|
||||
{
|
||||
$items = range(1, 25);
|
||||
$page = Paginator::paginate($items, page: 3, perPage: 10);
|
||||
|
||||
self::assertSame([21, 22, 23, 24, 25], $page->items());
|
||||
self::assertSame(3, $page->lastPage());
|
||||
self::assertFalse($page->hasNext());
|
||||
self::assertTrue($page->hasPrev());
|
||||
}
|
||||
|
||||
public function testHasNextAndHasPrev(): void
|
||||
{
|
||||
$items = range(1, 30);
|
||||
|
||||
$first = Paginator::paginate($items, page: 1, perPage: 10);
|
||||
self::assertTrue($first->hasNext());
|
||||
self::assertFalse($first->hasPrev());
|
||||
|
||||
$middle = Paginator::paginate($items, page: 2, perPage: 10);
|
||||
self::assertTrue($middle->hasNext());
|
||||
self::assertTrue($middle->hasPrev());
|
||||
|
||||
$last = Paginator::paginate($items, page: 3, perPage: 10);
|
||||
self::assertFalse($last->hasNext());
|
||||
self::assertTrue($last->hasPrev());
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmptyPage(): void
|
||||
{
|
||||
$page = Paginator::paginate([], page: 1, perPage: 10);
|
||||
|
||||
self::assertSame([], $page->items());
|
||||
self::assertSame(0, $page->total());
|
||||
self::assertSame(1, $page->lastPage());
|
||||
self::assertFalse($page->hasNext());
|
||||
self::assertFalse($page->hasPrev());
|
||||
}
|
||||
|
||||
public function testPageBeyondLastPageReturnsEmptyItems(): void
|
||||
{
|
||||
$page = Paginator::paginate(range(1, 5), page: 10, perPage: 10);
|
||||
|
||||
self::assertSame([], $page->items());
|
||||
}
|
||||
|
||||
public function testPageZeroThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
Paginator::paginate([], page: 0, perPage: 10);
|
||||
}
|
||||
|
||||
public function testNegativePageThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
Paginator::paginate([], page: -1, perPage: 10);
|
||||
}
|
||||
|
||||
public function testPerPageZeroThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
Paginator::paginate([], page: 1, perPage: 0);
|
||||
}
|
||||
|
||||
public function testPerPageNegativeThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
Paginator::paginate([], page: 1, perPage: -5);
|
||||
}
|
||||
|
||||
public function testItemsAreReIndexed(): void
|
||||
{
|
||||
$items = ['a' => 'x', 'b' => 'y', 'c' => 'z'];
|
||||
$page = Paginator::paginate(array_values($items), page: 1, perPage: 2);
|
||||
|
||||
self::assertSame(['x', 'y'], $page->items());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Partition;
|
||||
|
||||
final class PartitionTest extends TestCase
|
||||
{
|
||||
public function testSplitsIntoPassAndFail(): void
|
||||
{
|
||||
/** @var list<int> $numbers */
|
||||
$numbers = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
[$even, $odd] = Partition::by($numbers, fn($n) => $n % 2 === 0);
|
||||
|
||||
self::assertSame([2, 4, 6], $even);
|
||||
self::assertSame([1, 3, 5], $odd);
|
||||
}
|
||||
|
||||
public function testAllPassReturnsEmptyFail(): void
|
||||
{
|
||||
[$pass, $fail] = Partition::by([1, 2, 3], fn() => true);
|
||||
|
||||
self::assertSame([1, 2, 3], $pass);
|
||||
self::assertSame([], $fail);
|
||||
}
|
||||
|
||||
public function testAllFailReturnsEmptyPass(): void
|
||||
{
|
||||
[$pass, $fail] = Partition::by([1, 2, 3], fn() => false);
|
||||
|
||||
self::assertSame([], $pass);
|
||||
self::assertSame([1, 2, 3], $fail);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsTwoEmptyLists(): void
|
||||
{
|
||||
[$pass, $fail] = Partition::by([], fn() => true);
|
||||
|
||||
self::assertSame([], $pass);
|
||||
self::assertSame([], $fail);
|
||||
}
|
||||
|
||||
public function testWorksWithAssocArrayItems(): void
|
||||
{
|
||||
$users = [
|
||||
['name' => 'Alice', 'active' => true],
|
||||
['name' => 'Bob', 'active' => false],
|
||||
['name' => 'Carol', 'active' => true],
|
||||
];
|
||||
|
||||
[$active, $inactive] = Partition::by($users, fn($u) => $u['active']);
|
||||
|
||||
self::assertCount(2, $active);
|
||||
self::assertCount(1, $inactive);
|
||||
self::assertSame('Alice', $active[0]['name']);
|
||||
self::assertSame('Carol', $active[1]['name']);
|
||||
}
|
||||
|
||||
public function testResultIsReindexed(): void
|
||||
{
|
||||
[$pass, $fail] = Partition::by([10, 20, 30, 40], fn($n) => $n > 15);
|
||||
|
||||
self::assertSame(0, array_key_first($pass));
|
||||
self::assertSame(0, array_key_first($fail));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Pluck;
|
||||
|
||||
final class PluckTest extends TestCase
|
||||
{
|
||||
public function testPlucksTopLevelKey(): void
|
||||
{
|
||||
$items = [
|
||||
['name' => 'Alice', 'age' => 28],
|
||||
['name' => 'Bob', 'age' => 34],
|
||||
];
|
||||
|
||||
self::assertSame(['Alice', 'Bob'], Pluck::values($items, 'name'));
|
||||
}
|
||||
|
||||
public function testPlucksDotNotationKey(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'profile' => ['name' => 'Alice']],
|
||||
['id' => 2, 'profile' => ['name' => 'Bob']],
|
||||
];
|
||||
|
||||
self::assertSame(['Alice', 'Bob'], Pluck::values($items, 'profile.name'));
|
||||
}
|
||||
|
||||
public function testSkipsMissingKeys(): void
|
||||
{
|
||||
$items = [
|
||||
['name' => 'Alice'],
|
||||
['age' => 30],
|
||||
['name' => 'Carol'],
|
||||
];
|
||||
|
||||
self::assertSame(['Alice', 'Carol'], Pluck::values($items, 'name'));
|
||||
}
|
||||
|
||||
public function testKeyedByReturnsAssocArray(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
];
|
||||
|
||||
self::assertSame([1 => 'Alice', 2 => 'Bob'], Pluck::keyedBy($items, 'name', 'id'));
|
||||
}
|
||||
|
||||
public function testKeyedByWithDotNotation(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'profile' => ['name' => 'Alice']],
|
||||
['id' => 2, 'profile' => ['name' => 'Bob']],
|
||||
];
|
||||
|
||||
self::assertSame([1 => 'Alice', 2 => 'Bob'], Pluck::keyedBy($items, 'profile.name', 'id'));
|
||||
}
|
||||
|
||||
public function testEmptyInputReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Pluck::values([], 'name'));
|
||||
self::assertSame([], Pluck::keyedBy([], 'name', 'id'));
|
||||
}
|
||||
|
||||
public function testPlucksNullValues(): void
|
||||
{
|
||||
$items = [
|
||||
['name' => null],
|
||||
['name' => 'Bob'],
|
||||
];
|
||||
|
||||
self::assertSame([null, 'Bob'], Pluck::values($items, 'name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\SafeGet;
|
||||
|
||||
final class SafeGetTest extends TestCase
|
||||
{
|
||||
/** @var array<mixed> */
|
||||
private array $data;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->data = [
|
||||
'user' => [
|
||||
'name' => 'Alice',
|
||||
'age' => 30,
|
||||
'score' => 9.5,
|
||||
'active' => true,
|
||||
'tags' => ['admin', 'user'],
|
||||
],
|
||||
'chart' => [
|
||||
'result' => [
|
||||
[
|
||||
'meta' => ['currency' => 'EUR', 'price' => 123.45],
|
||||
'data' => [10, 20, 30],
|
||||
],
|
||||
],
|
||||
],
|
||||
'empty_string' => '',
|
||||
'zero' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// ── string ────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testStringShallowKey(): void
|
||||
{
|
||||
self::assertSame('Alice', SafeGet::string($this->data, 'user.name'));
|
||||
}
|
||||
|
||||
public function testStringDeeplyNested(): void
|
||||
{
|
||||
self::assertSame('EUR', SafeGet::string($this->data, 'chart.result.0.meta.currency'));
|
||||
}
|
||||
|
||||
public function testStringMissingPathReturnsDefault(): void
|
||||
{
|
||||
self::assertSame('fallback', SafeGet::string($this->data, 'user.missing', 'fallback'));
|
||||
}
|
||||
|
||||
public function testStringMissingPathReturnsNullByDefault(): void
|
||||
{
|
||||
self::assertNull(SafeGet::string($this->data, 'does.not.exist'));
|
||||
}
|
||||
|
||||
public function testStringWrongTypeReturnsDefault(): void
|
||||
{
|
||||
// age is int, not string
|
||||
self::assertNull(SafeGet::string($this->data, 'user.age'));
|
||||
}
|
||||
|
||||
// ── int ───────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testIntShallowKey(): void
|
||||
{
|
||||
self::assertSame(30, SafeGet::int($this->data, 'user.age'));
|
||||
}
|
||||
|
||||
public function testIntFromListIndex(): void
|
||||
{
|
||||
self::assertSame(20, SafeGet::int($this->data, 'chart.result.0.data.1'));
|
||||
}
|
||||
|
||||
public function testIntMissingReturnsDefault(): void
|
||||
{
|
||||
self::assertSame(42, SafeGet::int($this->data, 'user.missing', 42));
|
||||
}
|
||||
|
||||
public function testIntWrongTypeReturnsDefault(): void
|
||||
{
|
||||
// name is string, not int
|
||||
self::assertNull(SafeGet::int($this->data, 'user.name'));
|
||||
}
|
||||
|
||||
// ── float ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testFloatDirectValue(): void
|
||||
{
|
||||
self::assertSame(9.5, SafeGet::float($this->data, 'user.score'));
|
||||
}
|
||||
|
||||
public function testFloatWidensFromInt(): void
|
||||
{
|
||||
// age is int 30 → widened to 30.0
|
||||
self::assertSame(30.0, SafeGet::float($this->data, 'user.age'));
|
||||
}
|
||||
|
||||
public function testFloatNestedValue(): void
|
||||
{
|
||||
self::assertSame(123.45, SafeGet::float($this->data, 'chart.result.0.meta.price'));
|
||||
}
|
||||
|
||||
public function testFloatMissingReturnsDefault(): void
|
||||
{
|
||||
self::assertSame(1.5, SafeGet::float($this->data, 'missing', 1.5));
|
||||
}
|
||||
|
||||
// ── bool ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testBoolTrue(): void
|
||||
{
|
||||
self::assertTrue(SafeGet::bool($this->data, 'user.active'));
|
||||
}
|
||||
|
||||
public function testBoolMissingReturnsDefault(): void
|
||||
{
|
||||
self::assertFalse(SafeGet::bool($this->data, 'user.missing', false));
|
||||
}
|
||||
|
||||
public function testBoolWrongTypeReturnsDefault(): void
|
||||
{
|
||||
// integer 1 is not a bool
|
||||
self::assertNull(SafeGet::bool($this->data, 'user.age'));
|
||||
}
|
||||
|
||||
// ── array ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function testArrayShallowKey(): void
|
||||
{
|
||||
self::assertSame(['admin', 'user'], SafeGet::array($this->data, 'user.tags'));
|
||||
}
|
||||
|
||||
public function testArrayNestedObject(): void
|
||||
{
|
||||
$result = SafeGet::array($this->data, 'chart.result.0');
|
||||
self::assertIsArray($result);
|
||||
self::assertArrayHasKey('meta', $result);
|
||||
}
|
||||
|
||||
public function testArrayMissingReturnsDefault(): void
|
||||
{
|
||||
self::assertSame(['x'], SafeGet::array($this->data, 'missing', ['x']));
|
||||
}
|
||||
|
||||
public function testArrayMissingReturnsNullByDefault(): void
|
||||
{
|
||||
self::assertNull(SafeGet::array($this->data, 'user.name'));
|
||||
}
|
||||
|
||||
// ── edge cases ────────────────────────────────────────────────────────────
|
||||
|
||||
public function testMidPathNotArrayReturnsDefault(): void
|
||||
{
|
||||
// 'user.name' is a string; trying to go deeper must return null
|
||||
self::assertNull(SafeGet::string($this->data, 'user.name.nested'));
|
||||
}
|
||||
|
||||
public function testNumericStringKeyAddressesListIndex(): void
|
||||
{
|
||||
self::assertSame(10, SafeGet::int($this->data, 'chart.result.0.data.0'));
|
||||
self::assertSame(30, SafeGet::int($this->data, 'chart.result.0.data.2'));
|
||||
}
|
||||
|
||||
public function testEmptyPathSegmentsAreHandled(): void
|
||||
{
|
||||
$flat = ['key' => 'value'];
|
||||
self::assertSame('value', SafeGet::string($flat, 'key'));
|
||||
}
|
||||
|
||||
public function testRootLevelArrayAccess(): void
|
||||
{
|
||||
$data = ['name' => 'Bob'];
|
||||
self::assertSame('Bob', SafeGet::string($data, 'name'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Sliding;
|
||||
|
||||
final class SlidingTest extends TestCase
|
||||
{
|
||||
public function testDefaultStepOfOne(): void
|
||||
{
|
||||
$result = Sliding::window([1, 2, 3, 4, 5], size: 3);
|
||||
|
||||
self::assertSame([[1, 2, 3], [2, 3, 4], [3, 4, 5]], $result);
|
||||
}
|
||||
|
||||
public function testCustomStep(): void
|
||||
{
|
||||
$result = Sliding::window([1, 2, 3, 4, 5], size: 3, step: 2);
|
||||
|
||||
self::assertSame([[1, 2, 3], [3, 4, 5]], $result);
|
||||
}
|
||||
|
||||
public function testStepEqualsSizeProducesNonOverlappingChunks(): void
|
||||
{
|
||||
$result = Sliding::window([1, 2, 3, 4, 5, 6], size: 2, step: 2);
|
||||
|
||||
self::assertSame([[1, 2], [3, 4], [5, 6]], $result);
|
||||
}
|
||||
|
||||
public function testArraySmallerThanWindowReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Sliding::window([1, 2], size: 5));
|
||||
}
|
||||
|
||||
public function testExactFitProducesOneWindow(): void
|
||||
{
|
||||
$result = Sliding::window([1, 2, 3], size: 3);
|
||||
|
||||
self::assertSame([[1, 2, 3]], $result);
|
||||
}
|
||||
|
||||
public function testSizeOneReturnsEachElementWrapped(): void
|
||||
{
|
||||
$result = Sliding::window([1, 2, 3], size: 1);
|
||||
|
||||
self::assertSame([[1], [2], [3]], $result);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Sliding::window([], size: 3));
|
||||
}
|
||||
|
||||
public function testInvalidSizeThrows(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
Sliding::window([1, 2, 3], size: 0);
|
||||
}
|
||||
|
||||
public function testInvalidStepThrows(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
Sliding::window([1, 2, 3], size: 2, step: 0);
|
||||
}
|
||||
|
||||
public function testAssociativeKeysAreStripped(): void
|
||||
{
|
||||
$result = Sliding::window(['a' => 1, 'b' => 2, 'c' => 3], size: 2);
|
||||
|
||||
self::assertSame([[1, 2], [2, 3]], $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\SortBy;
|
||||
|
||||
final class SortByTest extends TestCase
|
||||
{
|
||||
public function testSortsByIntKeyAscending(): void
|
||||
{
|
||||
$users = [
|
||||
['name' => 'Carol', 'age' => 32],
|
||||
['name' => 'Alice', 'age' => 28],
|
||||
['name' => 'Bob', 'age' => 45],
|
||||
];
|
||||
|
||||
$result = SortBy::sort($users, fn($u) => $u['age']);
|
||||
|
||||
self::assertSame('Alice', $result[0]['name']);
|
||||
self::assertSame('Carol', $result[1]['name']);
|
||||
self::assertSame('Bob', $result[2]['name']);
|
||||
}
|
||||
|
||||
public function testSortsByIntKeyDescending(): void
|
||||
{
|
||||
$users = [
|
||||
['name' => 'Carol', 'age' => 32],
|
||||
['name' => 'Alice', 'age' => 28],
|
||||
['name' => 'Bob', 'age' => 45],
|
||||
];
|
||||
|
||||
$result = SortBy::sort($users, fn($u) => $u['age'], descending: true);
|
||||
|
||||
self::assertSame('Bob', $result[0]['name']);
|
||||
self::assertSame('Carol', $result[1]['name']);
|
||||
self::assertSame('Alice', $result[2]['name']);
|
||||
}
|
||||
|
||||
public function testSortsByStringKey(): void
|
||||
{
|
||||
$items = ['banana', 'apple', 'cherry'];
|
||||
|
||||
$result = SortBy::sort($items, fn($v) => $v);
|
||||
|
||||
self::assertSame(['apple', 'banana', 'cherry'], $result);
|
||||
}
|
||||
|
||||
public function testDoesNotMutateOriginal(): void
|
||||
{
|
||||
$original = [3, 1, 2];
|
||||
SortBy::sort($original, fn($v) => $v);
|
||||
|
||||
self::assertSame([3, 1, 2], $original);
|
||||
}
|
||||
|
||||
public function testResultIsReindexed(): void
|
||||
{
|
||||
$result = SortBy::sort([3, 1, 2], fn($v) => $v);
|
||||
|
||||
self::assertSame(0, array_key_first($result));
|
||||
self::assertSame(1, $result[0]);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], SortBy::sort([], fn($v) => $v));
|
||||
}
|
||||
|
||||
public function testStableSortPreservesEqualOrder(): void
|
||||
{
|
||||
$items = [
|
||||
['name' => 'A', 'age' => 30],
|
||||
['name' => 'B', 'age' => 30],
|
||||
];
|
||||
|
||||
$result = SortBy::sort($items, fn($u) => $u['age']);
|
||||
|
||||
// Equal keys: original order must be preserved (PHP's usort is stable since 8.0)
|
||||
self::assertSame('A', $result[0]['name']);
|
||||
self::assertSame('B', $result[1]['name']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Transpose;
|
||||
|
||||
final class TransposeTest extends TestCase
|
||||
{
|
||||
public function testTransposesMatrix(): void
|
||||
{
|
||||
$result = Transpose::matrix([
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
]);
|
||||
|
||||
self::assertSame([[1, 4], [2, 5], [3, 6]], $result);
|
||||
}
|
||||
|
||||
public function testTransposesSquareMatrix(): void
|
||||
{
|
||||
$result = Transpose::matrix([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
|
||||
self::assertSame([[1, 3], [2, 4]], $result);
|
||||
}
|
||||
|
||||
public function testTransposesSingleRow(): void
|
||||
{
|
||||
$result = Transpose::matrix([[1, 2, 3]]);
|
||||
|
||||
self::assertSame([[1], [2], [3]], $result);
|
||||
}
|
||||
|
||||
public function testTransposesSingleColumn(): void
|
||||
{
|
||||
$result = Transpose::matrix([[1], [2], [3]]);
|
||||
|
||||
self::assertSame([[1, 2, 3]], $result);
|
||||
}
|
||||
|
||||
public function testEmptyInputReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Transpose::matrix([]));
|
||||
}
|
||||
|
||||
public function testAssociativeKeysAreStripped(): void
|
||||
{
|
||||
$result = Transpose::matrix([
|
||||
['a' => 1, 'b' => 2],
|
||||
['a' => 3, 'b' => 4],
|
||||
]);
|
||||
|
||||
self::assertSame([[1, 3], [2, 4]], $result);
|
||||
}
|
||||
|
||||
public function testJaggedArrayThrows(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
Transpose::matrix([[1, 2, 3], [4, 5]]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\UniqueBy;
|
||||
|
||||
final class UniqueByTest extends TestCase
|
||||
{
|
||||
public function testFirstWinsByDefault(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
['id' => 1, 'name' => 'Alice2'],
|
||||
];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($item) => $item['id']);
|
||||
|
||||
self::assertCount(2, $result);
|
||||
self::assertSame('Alice', $result[0]['name']);
|
||||
}
|
||||
|
||||
public function testLastWins(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'name' => 'Alice'],
|
||||
['id' => 2, 'name' => 'Bob'],
|
||||
['id' => 1, 'name' => 'Alice2'],
|
||||
];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($item) => $item['id'], lastWins: true);
|
||||
|
||||
// Bob's only occurrence is at position 1, Alice2's last occurrence is at position 2
|
||||
// → order reflects last-occurrence position in the original array
|
||||
self::assertCount(2, $result);
|
||||
self::assertSame('Bob', $result[0]['name']);
|
||||
self::assertSame('Alice2', $result[1]['name']);
|
||||
}
|
||||
|
||||
public function testLastWinsPreservesOriginalOrder(): void
|
||||
{
|
||||
$items = [
|
||||
['id' => 1, 'name' => 'A'],
|
||||
['id' => 2, 'name' => 'B'],
|
||||
['id' => 1, 'name' => 'C'],
|
||||
['id' => 2, 'name' => 'D'],
|
||||
];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($item) => $item['id'], lastWins: true);
|
||||
|
||||
self::assertCount(2, $result);
|
||||
self::assertSame('C', $result[0]['name']);
|
||||
self::assertSame('D', $result[1]['name']);
|
||||
}
|
||||
|
||||
public function testReIndexedByDefault(): void
|
||||
{
|
||||
$items = [10 => 'a', 20 => 'b', 30 => 'a'];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => $v);
|
||||
|
||||
self::assertSame([0 => 'a', 1 => 'b'], $result);
|
||||
}
|
||||
|
||||
public function testPreserveKeys(): void
|
||||
{
|
||||
$items = [10 => 'a', 20 => 'b', 30 => 'a'];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => $v, preserveKeys: true);
|
||||
|
||||
self::assertArrayHasKey(10, $result);
|
||||
self::assertArrayHasKey(20, $result);
|
||||
self::assertArrayNotHasKey(30, $result);
|
||||
}
|
||||
|
||||
public function testPreserveKeysWithLastWins(): void
|
||||
{
|
||||
$items = [10 => 'a', 20 => 'b', 30 => 'a'];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => $v, lastWins: true, preserveKeys: true);
|
||||
|
||||
self::assertArrayNotHasKey(10, $result);
|
||||
self::assertArrayHasKey(20, $result);
|
||||
self::assertArrayHasKey(30, $result);
|
||||
}
|
||||
|
||||
public function testEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
$result = UniqueBy::filter([], fn($v) => $v);
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testAllUniqueReturnsAllItems(): void
|
||||
{
|
||||
$items = [1, 2, 3, 4];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => $v);
|
||||
|
||||
self::assertSame([1, 2, 3, 4], $result);
|
||||
}
|
||||
|
||||
public function testAllDuplicatesReturnsSingleItem(): void
|
||||
{
|
||||
$items = ['a', 'a', 'a'];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => $v);
|
||||
|
||||
self::assertSame(['a'], $result);
|
||||
}
|
||||
|
||||
public function testCallbackTransformation(): void
|
||||
{
|
||||
$items = ['Alice', 'alice', 'BOB', 'Bob'];
|
||||
|
||||
$result = UniqueBy::filter($items, fn($v) => strtolower($v));
|
||||
|
||||
self::assertSame(['Alice', 'BOB'], $result);
|
||||
}
|
||||
|
||||
public function testCallbackOnObjects(): void
|
||||
{
|
||||
$a = new \stdClass();
|
||||
$a->id = 1;
|
||||
$b = new \stdClass();
|
||||
$b->id = 2;
|
||||
$c = new \stdClass();
|
||||
$c->id = 1;
|
||||
|
||||
$result = UniqueBy::filter([$a, $b, $c], fn($o) => $o->id);
|
||||
|
||||
self::assertCount(2, $result);
|
||||
self::assertSame($a, $result[0]);
|
||||
self::assertSame($b, $result[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Wrap;
|
||||
|
||||
final class WrapTest extends TestCase
|
||||
{
|
||||
public function testWrapsScalarInArray(): void
|
||||
{
|
||||
self::assertSame(['hello'], Wrap::ensure('hello'));
|
||||
self::assertSame([42], Wrap::ensure(42));
|
||||
self::assertSame([3.14], Wrap::ensure(3.14));
|
||||
self::assertSame([true], Wrap::ensure(true));
|
||||
}
|
||||
|
||||
public function testReturnsArrayAsIs(): void
|
||||
{
|
||||
self::assertSame([1, 2, 3], Wrap::ensure([1, 2, 3]));
|
||||
self::assertSame(['a' => 1], Wrap::ensure(['a' => 1]));
|
||||
self::assertSame([], Wrap::ensure([]));
|
||||
}
|
||||
|
||||
public function testNullReturnsEmptyArray(): void
|
||||
{
|
||||
self::assertSame([], Wrap::ensure(null));
|
||||
}
|
||||
|
||||
public function testObjectIsWrapped(): void
|
||||
{
|
||||
$obj = new \stdClass();
|
||||
$result = Wrap::ensure($obj);
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame($obj, $result[0]);
|
||||
}
|
||||
|
||||
public function testAssociativeArrayIsReturnedAsIs(): void
|
||||
{
|
||||
$input = ['key' => 'value', 'other' => 42];
|
||||
$result = Wrap::ensure($input);
|
||||
|
||||
self::assertSame($input, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Tests\Arr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tez\Utils\Arr\Zip;
|
||||
|
||||
final class ZipTest extends TestCase
|
||||
{
|
||||
public function testZipEqualLengthArrays(): void
|
||||
{
|
||||
$result = Zip::zip([1, 2, 3], ['a', 'b', 'c']);
|
||||
|
||||
self::assertSame([[1, 'a'], [2, 'b'], [3, 'c']], $result);
|
||||
}
|
||||
|
||||
public function testZipPadsShorterArrayWithNull(): void
|
||||
{
|
||||
$result = Zip::zip([1, 2, 3], ['a', 'b']);
|
||||
|
||||
self::assertSame([[1, 'a'], [2, 'b'], [3, null]], $result);
|
||||
}
|
||||
|
||||
public function testZipThreeArrays(): void
|
||||
{
|
||||
$result = Zip::zip([1, 2], ['a', 'b'], [true, false]);
|
||||
|
||||
self::assertSame([[1, 'a', true], [2, 'b', false]], $result);
|
||||
}
|
||||
|
||||
public function testZipNoArraysReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Zip::zip());
|
||||
}
|
||||
|
||||
public function testZipEmptyArraysReturnEmpty(): void
|
||||
{
|
||||
self::assertSame([], Zip::zip([], []));
|
||||
}
|
||||
|
||||
public function testShortestStopsAtSmallest(): void
|
||||
{
|
||||
$result = Zip::shortest([1, 2, 3], ['a', 'b']);
|
||||
|
||||
self::assertSame([[1, 'a'], [2, 'b']], $result);
|
||||
}
|
||||
|
||||
public function testShortestWithEqualLengths(): void
|
||||
{
|
||||
$result = Zip::shortest([1, 2], ['a', 'b']);
|
||||
|
||||
self::assertSame([[1, 'a'], [2, 'b']], $result);
|
||||
}
|
||||
|
||||
public function testShortestWithEmptyArrayReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Zip::shortest([1, 2, 3], []));
|
||||
}
|
||||
|
||||
public function testShortestNoArraysReturnsEmpty(): void
|
||||
{
|
||||
self::assertSame([], Zip::shortest());
|
||||
}
|
||||
|
||||
public function testZipStripsAssociativeKeys(): void
|
||||
{
|
||||
$result = Zip::zip(['a' => 1, 'b' => 2], ['x' => 10, 'y' => 20]);
|
||||
|
||||
self::assertSame([[1, 10], [2, 20]], $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user