diff --git a/src/Tuple/Tuple.php b/src/Tuple/Tuple.php index 9b49a74..fe180b6 100644 --- a/src/Tuple/Tuple.php +++ b/src/Tuple/Tuple.php @@ -113,6 +113,18 @@ public static function hasIncompleteVersionstamp(array $elements): bool return false; } + /** + * @param list> $tuple1 + * @param list> $tuple2 + */ + public static function compare(array $tuple1, array $tuple2): int + { + $packed1 = self::pack($tuple1); + $packed2 = self::pack($tuple2); + + return $packed1 <=> $packed2; + } + /** * @param list> $elements * @return array{string, string} diff --git a/tests/Unit/Tuple/TupleCompareTest.php b/tests/Unit/Tuple/TupleCompareTest.php new file mode 100644 index 0000000..15af09b --- /dev/null +++ b/tests/Unit/Tuple/TupleCompareTest.php @@ -0,0 +1,330 @@ +> $lesser + * @param list> $greater + */ + #[Test] + #[DataProvider('typeOrderingProvider')] + public function compareTypeCodeOrdering(array $lesser, array $greater, string $description): void + { + self::assertSame(-1, Tuple::compare($lesser, $greater), "$description: expected lesser < greater"); + self::assertSame(1, Tuple::compare($greater, $lesser), "$description: expected greater > lesser"); + } + + /** + * @return iterable>, + * list>, + * string, + * }> + */ + public static function typeOrderingProvider(): iterable + { + yield 'null < bytes' => [[null], [new Bytes('a')], 'null < bytes']; + yield 'bytes < string' => [[new Bytes('a')], ['a'], 'bytes < string']; + yield 'string < nested' => [['z'], [[]], 'string < nested']; + yield 'nested < int zero' => [[[]], [0], 'nested < int zero']; + yield 'int < single float' => [[100], [new SingleFloat(0.0)], 'int < single float']; + yield 'single float < double' => [[new SingleFloat(999.0)], [0.0], 'single float < double']; + yield 'double < false' => [[999.0], [false], 'double < false']; + yield 'false < true' => [[false], [true], 'false < true']; + yield 'true < uuid' => [[true], [new Uuid(str_repeat("\x00", 16))], 'true < uuid']; + yield 'uuid < versionstamp' => [ + [new Uuid(str_repeat("\xFF", 16))], + [new Versionstamp(str_repeat("\x00", 10), 0)], + 'uuid < versionstamp', + ]; + } + + #[Test] + public function compareMultiElementDiffersAtSecondPosition(): void + { + self::assertSame(-1, Tuple::compare([1, 'a'], [1, 'b'])); + self::assertSame(1, Tuple::compare([1, 'b'], [1, 'a'])); + } + + #[Test] + public function compareMultiElementDiffersAtFirstPosition(): void + { + self::assertSame(-1, Tuple::compare([1, 'z'], [2, 'a'])); + } + + #[Test] + public function compareIsAntiSymmetric(): void + { + $t1 = [1, 'hello']; + $t2 = [1, 'world']; + $forward = Tuple::compare($t1, $t2); + $backward = Tuple::compare($t2, $t1); + self::assertSame(-$forward, $backward); + } + + #[Test] + public function compareIsTransitive(): void + { + $t1 = [1]; + $t2 = [2]; + $t3 = [3]; + self::assertSame(-1, Tuple::compare($t1, $t2)); + self::assertSame(-1, Tuple::compare($t2, $t3)); + self::assertSame(-1, Tuple::compare($t1, $t3)); + } + + #[Test] + public function compareLargeIntegers(): void + { + self::assertSame(-1, Tuple::compare([PHP_INT_MAX - 1], [PHP_INT_MAX])); + self::assertSame(-1, Tuple::compare([PHP_INT_MIN], [PHP_INT_MIN + 1])); + } + + #[Test] + public function compareGmpIntegers(): void + { + if (!extension_loaded('gmp')) { + self::markTestSkipped('GMP extension required'); + } + + $big1 = gmp_init('999999999999999999999999999999'); + $big2 = gmp_init('9999999999999999999999999999990'); + self::assertSame(-1, Tuple::compare([$big1], [$big2])); + self::assertSame(1, Tuple::compare([$big2], [$big1])); + self::assertSame(0, Tuple::compare([$big1], [$big1])); + } + + #[Test] + public function compareMatchesPackedByteOrdering(): void + { + $pairs = [ + [[1], [2]], + [['a'], ['b']], + [[null], [0]], + [[false], [true]], + [[-1], [0]], + [[new Bytes('a')], ['a']], + ]; + + foreach ($pairs as [$t1, $t2]) { + $packed1 = Tuple::pack($t1); + $packed2 = Tuple::pack($t2); + $byteComparison = $packed1 <=> $packed2; + $tupleComparison = Tuple::compare($t1, $t2); + self::assertSame( + $byteComparison <=> 0, + $tupleComparison, + sprintf( + 'Tuple::compare() should match packed byte ordering for %s vs %s', + json_encode($t1), + json_encode($t2), + ), + ); + } + } +}