From 75f373e54ac184f8b76874783c88323e07a47815 Mon Sep 17 00:00:00 2001 From: Smoren Date: Tue, 12 Mar 2024 14:34:35 +0300 Subject: [PATCH 1/3] Errors tests added. --- tests/unit/ArrayView/ErrorsTest.php | 150 ++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/unit/ArrayView/ErrorsTest.php b/tests/unit/ArrayView/ErrorsTest.php index 0180e30..a5f8f5e 100644 --- a/tests/unit/ArrayView/ErrorsTest.php +++ b/tests/unit/ArrayView/ErrorsTest.php @@ -7,6 +7,7 @@ use Smoren\ArrayView\Exceptions\SizeError; use Smoren\ArrayView\Exceptions\ValueError; use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Views\ArrayView; class ErrorsTest extends \Codeception\Test\Unit @@ -127,6 +128,64 @@ public function testWriteByMaskSizeError(array $source, array $boolMask) $view[new MaskSelector($boolMask)] = $boolMask; } + /** + * @dataProvider dataProviderForInvalidSlice + */ + public function testInvalidSliceRead(array $source, string $slice) + { + $view = ArrayView::toView($source); + + $this->expectException(IndexError::class); + $this->expectExceptionMessage("Step cannot be 0."); + + $_ = $view[new SliceSelector($slice)]; + } + + /** + * @dataProvider dataProviderForInvalidSlice + */ + public function testInvalidSliceWrite(array $source, string $slice) + { + $view = ArrayView::toView($source); + + $this->expectException(IndexError::class); + $this->expectExceptionMessage("Step cannot be 0."); + + $view[new SliceSelector($slice)] = [1, 2, 3]; + } + + /** + * @dataProvider dataProviderForApplyWithSizeError + */ + public function testApplyWithSizeError(array $source, callable $viewGetter, callable $mapper, array $toApplyWith) + { + $view = ArrayView::toView($source); + + $sourceSize = \count($source); + $argSize = \count($toApplyWith); + + $this->expectException(SizeError::class); + $this->expectExceptionMessage("Length of values array not equal to view length ({$argSize} != {$sourceSize})."); + + $view->applyWith($toApplyWith, $mapper); + } + + /** + * @dataProvider dataProviderForWriteSizeError + */ + public function testWriteSizeError(array $source, callable $viewGetter, array $toWrite) + { + $view = ArrayView::toView($source); + + $sourceSize = \count($source); + $argSize = \count($toWrite); + + $this->expectException(SizeError::class); + $this->expectExceptionMessage("Length of values array not equal to view length ({$argSize} != {$sourceSize})."); + + $view[':'] = $toWrite; + } + /** * @dataProvider dataProviderForNonSequentialError */ @@ -177,6 +236,97 @@ public function dataProviderForBadSizeMask(): array ]; } + public function dataProviderForInvalidSlice(): array + { + return [ + [[], '::0'], + [[], '0:0:0'], + [[], '0:1:0'], + [[], '0::0'], + [[], ':-1:0'], + [[], '1:-1:0'], + [[1], '::0'], + [[1], '0:0:0'], + [[1], '0:1:0'], + [[1], '0::0'], + [[1], ':-1:0'], + [[1], '1:-1:0'], + [[1, 2, 3], '::0'], + [[1, 2, 3], '0:0:0'], + [[1, 2, 3], '0:1:0'], + [[1, 2, 3], '0::0'], + [[1, 2, 3], ':-1:0'], + [[1, 2, 3], '1:-1:0'], + ]; + } + + public function dataProviderForApplyWithSizeError(): array + { + return [ + [ + [], + fn (array &$source) => ArrayView::toView($source), + fn (int $item) => $item, + [1], + ], + [ + [1], + fn (array &$source) => ArrayView::toView($source), + fn (int $item) => $item, + [], + ], + [ + [1], + fn (array &$source) => ArrayView::toView($source), + fn (int $item) => $item, + [1, 2], + ], + [ + [1, 2, 3], + fn (array &$source) => ArrayView::toView($source), + fn (int $item) => $item, + [1, 2], + ], + [ + [1, 2, 3], + fn (array &$source) => ArrayView::toView($source), + fn (int $item) => $item, + [1, 2, 3, 4, 5], + ], + ]; + } + + public function dataProviderForWriteSizeError(): array + { + return [ + [ + [], + fn (array &$source) => ArrayView::toView($source), + [1], + ], + [ + [1], + fn (array &$source) => ArrayView::toView($source), + [], + ], + [ + [1], + fn (array &$source) => ArrayView::toView($source), + [1, 2], + ], + [ + [1, 2, 3], + fn (array &$source) => ArrayView::toView($source), + [1, 2], + ], + [ + [1, 2, 3], + fn (array &$source) => ArrayView::toView($source), + [1, 2, 3, 4, 5], + ], + ]; + } + public function dataProviderForNonSequentialError(): array { return [ From 3335dba0d2befd240c9a5126f6c15d85d9e1428b Mon Sep 17 00:00:00 2001 From: Smoren Date: Tue, 12 Mar 2024 15:15:08 +0300 Subject: [PATCH 2/3] New tests. --- src/Structs/Slice.php | 53 ++++-- tests/unit/Structs/SliceTest.php | 275 +++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 tests/unit/Structs/SliceTest.php diff --git a/src/Structs/Slice.php b/src/Structs/Slice.php index 3744abc..eff1cca 100644 --- a/src/Structs/Slice.php +++ b/src/Structs/Slice.php @@ -38,7 +38,8 @@ public static function toSlice($s): Slice } if (!self::isSliceString($s)) { - throw new ValueError("Invalid slice: \"{$s}\"."); + $str = \is_scalar($s) ? "{$s}" : gettype($s); + throw new ValueError("Invalid slice: \"{$str}\"."); } $slice = self::parseSliceString($s); @@ -80,6 +81,33 @@ public static function isSliceString($s): bool return !(\count($slice) < 1 || \count($slice) > 3); } + /** + * @param mixed $s + * + * @return bool + */ + public static function isSliceArray($s): bool + { + if (!\is_array($s)) { + return false; + } + + if (!(\count($s) >= 0 && \count($s) <= 3)) { + return false; + } + + foreach ($s as $key => $item) { + if (\is_string($key)) { + return false; + } + if ($item !== null && (!\is_numeric($item) || \is_float($item + 0))) { + return false; + } + } + + return true; + } + /** * @param int|null $start * @param int|null $end @@ -93,11 +121,11 @@ public function __construct(?int $start = null, ?int $end = null, ?int $step = n } /** - * @param int $containerLength + * @param int $containerSize * * @return NormalizedSlice */ - public function normalize(int $containerLength): NormalizedSlice + public function normalize(int $containerSize): NormalizedSlice { // TODO: Need refactor $step = $this->step ?? 1; @@ -108,25 +136,25 @@ public function normalize(int $containerLength): NormalizedSlice $defaultEnd = ($step < 0 && $this->end === null) ? -1 : null; - $start = $this->start ?? ($step > 0 ? 0 : $containerLength - 1); - $end = $this->end ?? ($step > 0 ? $containerLength : -1); + $start = $this->start ?? ($step > 0 ? 0 : $containerSize - 1); + $end = $this->end ?? ($step > 0 ? $containerSize : -1); $start = intval(round($start)); $end = intval(round($end)); $step = intval(round($step)); - $start = Util::normalizeIndex($start, $containerLength, false); - $end = Util::normalizeIndex($end, $containerLength, false); + $start = Util::normalizeIndex($start, $containerSize, false); + $end = Util::normalizeIndex($end, $containerSize, false); - if ($step > 0 && $start >= $containerLength) { - $start = $end = $containerLength - 1; + if ($step > 0 && $start >= $containerSize) { + $start = $end = $containerSize - 1; } elseif ($step < 0 && $start < 0) { $start = $end = 0; $defaultEnd = 0; } - $start = $this->squeezeInBounds($start, 0, $containerLength - 1); - $end = $this->squeezeInBounds($end, $step > 0 ? 0 : -1, $containerLength); + $start = $this->squeezeInBounds($start, 0, $containerSize - 1); + $end = $this->squeezeInBounds($end, $step > 0 ? 0 : -1, $containerSize); if (($step > 0 && $end < $start) || ($step < 0 && $end > $start)) { $end = $start; @@ -150,6 +178,9 @@ public function toString(): string */ private static function parseSliceString(string $s): array { + if ($s === '') { + return []; + } return array_map(fn($x) => trim($x) === '' ? null : \intval(trim($x)), \explode(':', $s)); } diff --git a/tests/unit/Structs/SliceTest.php b/tests/unit/Structs/SliceTest.php new file mode 100644 index 0000000..1bdd297 --- /dev/null +++ b/tests/unit/Structs/SliceTest.php @@ -0,0 +1,275 @@ +assertTrue(Slice::isSlice($input)); + $this->assertTrue(Slice::isSliceString($input)); + } + + /** + * @dataProvider dataProviderForFalse + */ + public function testIsSliceFalse($input) + { + $this->assertFalse(Slice::isSlice($input)); + $this->assertFalse(Slice::isSliceString($input)); + } + + /** + * @dataProvider dataProviderForFalse + */ + public function testSliceError($input) + { + $this->expectException(ValueError::class); + $strInput = \is_scalar($input) ? "{$input}" : gettype($input); + $this->expectExceptionMessage("Invalid slice: \"{$strInput}\""); + + Slice::toSlice($input); + } + + /** + * @dataProvider dataProviderForToSlice + */ + public function testToSlice($input, array $expected) + { + $actual = Slice::toSlice($input); + $expectedSlice = new Slice(...$expected); + + $this->assertSame($expectedSlice->start, $actual->start); + $this->assertSame($expectedSlice->end, $actual->end); + $this->assertSame($expectedSlice->step, $actual->step); + } + + /** + * @dataProvider dataProviderForSliceToString + */ + public function testSliceToString(string $input, string $expected) + { + $slice = Slice::toSlice($input); + $this->assertSame($expected, $slice->toString()); + } + + /** + * @dataProvider dataProviderForSliceNormalize + */ + public function testSliceNormalize(string $input, int $size, string $expected, array $expectedIndexes) + { + $slice = Slice::toSlice($input); + $normalizedSlice = $slice->normalize($size); + + $this->assertInstanceOf(NormalizedSlice::class, $normalizedSlice); + $this->assertSame($expected, $normalizedSlice->toString()); + $this->assertSame($expectedIndexes, [...$normalizedSlice]); + } + + /** + * @dataProvider dataProviderForIsSliceArrayTrue + */ + public function testIsSliceArrayTrue(array $input) + { + $this->assertTrue(Slice::isSliceArray($input)); + } + + /** + * @dataProvider dataProviderForIsSliceArrayFalse + */ + public function testIsSliceArrayFalse($input) + { + $this->assertFalse(Slice::isSliceArray($input)); + } + + public function dataProviderForTrue(): array + { + return [ + [':'], + ['::'], + ['0:'], + ['1:'], + ['-1:'], + ['0::'], + ['1::'], + ['-1::'], + [':0'], + [':1'], + [':-1'], + [':0:'], + [':1:'], + [':-1:'], + ['0:0:'], + ['1:1:'], + ['-1:-1:'], + ['1:1:-1'], + ['-1:-1:-1'], + ['1:2:3'], + ]; + } + + public function dataProviderForFalse(): array + { + return [ + [''], + ['0'], + ['1'], + ['1:::'], + [':1::'], + ['::1:'], + [':::1'], + ['test'], + ['[::]'], + ['a:b:c'], + [0], + [1], + [1.1], + [true], + [false], + [null], + [[]], + [[1, 2, 3]], + [[null]], + [new \ArrayObject([])], + [['a' => 1]], + ]; + } + + public function dataProviderForToSlice(): array + { + return [ + [':', [null, null, null]], + ['::', [null, null, null]], + ['0:', [0, null, null]], + ['1:', [1, null, null]], + ['-1:', [-1, null, null]], + ['0::', [0, null, null]], + ['1::', [1, null, null]], + ['-1::', [-1, null, null]], + [':0', [null, 0, null]], + [':1', [null, 1, null]], + [':-1', [null, -1, null]], + [':0:', [null, 0, null]], + [':1:', [null, 1, null]], + [':-1:', [null, -1, null]], + ['0:0:', [0, 0, null]], + ['1:1:', [1, 1, null]], + ['-1:-1:', [-1, -1, null]], + ['1:1:-1', [1, 1, -1]], + ['-1:-1:-1', [-1, -1, -1]], + ['1:2:3', [1, 2, 3]], + ]; + } + + public function dataProviderForSliceToString(): array + { + return [ + [':', '::'], + ['::', '::'], + ['0:', '0::'], + ['1:', '1::'], + ['-1:', '-1::'], + ['0::', '0::'], + ['1::', '1::'], + ['-1::', '-1::'], + [':0', ':0:'], + [':1', ':1:'], + [':-1', ':-1:'], + [':0:', ':0:'], + [':1:', ':1:'], + [':-1:', ':-1:'], + ['0:0:', '0:0:'], + ['1:1:', '1:1:'], + ['-1:-1:', '-1:-1:'], + ['1:1:-1', '1:1:-1'], + ['-1:-1:-1', '-1:-1:-1'], + ['1:2:3', '1:2:3'], + ]; + } + + public function dataProviderForSliceNormalize(): array + { + return [ + [':', 0, '0:0:1', []], + ['::', 1, '0:1:1', [0]], + ['0:', 2, '0:2:1', [0, 1]], + ['1:', 5, '1:5:1', [1, 2, 3, 4]], + ['-1:', 3, '2:3:1', [2]], + ['0::', 10, '0:10:1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + ['1::', 0, '0:0:1', []], + ['-1::', 0, '0:0:1', []], + [':0', 1, '0:0:1', []], + [':1', 2, '0:1:1', [0]], + [':-1', 5, '0:4:1', [0, 1, 2, 3]], + [':0:', 3, '0:0:1', []], + [':1:', 1, '0:1:1', [0]], + [':-1:', 3, '0:2:1', [0, 1]], + ['0:0:', 3, '0:0:1', []], + ['1:1:', 3, '1:1:1', []], + ['-1:-1:', 10, '9:9:1', []], + ['1:1:-1', 10, '1:1:-1', []], + ['-1:-1:-1', 10, '9:9:-1', []], + ['1:2:3', 10, '1:2:3', [1]], + ['1:2:3', 1, '0:0:3', []], + ['::-1', 1, '0:-1:-1', [0]], + ['1::-1', 1, '0:-1:-1', [0]], + ['2::-1', 1, '0:-1:-1', [0]], + ['2:-3:-1', 1, '0:-1:-1', [0]], + ['2::-1', 10, '2:-1:-1', [2, 1, 0]], + [':3:-1', 10, '9:3:-1', [9, 8, 7, 6, 5, 4]], + ]; + } + + public function dataProviderForIsSliceArrayTrue(): array + { + return [ + [[]], + [[null, null]], + [[null, null, null]], + [[0]], + [[0, null]], + [[0, null, null]], + [[1, null, null]], + [[1, 0, null]], + [[1, 1, null]], + [[-1, 1, null]], + [[1, null, 1]], + [[1, null, 2]], + [[null, null, 1]], + [[null, null, -1]], + [[1, 10, -1]], + ]; + } + + public function dataProviderForIsSliceArrayFalse(): array + { + return [ + [['']], + [['a']], + [[0, 1, 'a']], + [[0, 1, 2, 3]], + [[1.1, 1, 2]], + [[1, 1, 2.2]], + [null], + [0], + [1], + [0.0], + [1.0], + [true], + [false], + [new \ArrayObject([])], + [['a' => 1]], + [[[]]], + [[['a' => 1]]], + ]; + } +} From 0d6b0ade8d1095e3c2d6b350605a24ab214053a3 Mon Sep 17 00:00:00 2001 From: Smoren Date: Tue, 12 Mar 2024 15:20:18 +0300 Subject: [PATCH 3/3] Stan fixes. --- src/Structs/Slice.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Structs/Slice.php b/src/Structs/Slice.php index eff1cca..38d79ce 100644 --- a/src/Structs/Slice.php +++ b/src/Structs/Slice.php @@ -33,6 +33,7 @@ class Slice */ public static function toSlice($s): Slice { + /** @var mixed $s */ if ($s instanceof Slice) { return $s; } @@ -42,6 +43,7 @@ public static function toSlice($s): Slice throw new ValueError("Invalid slice: \"{$str}\"."); } + /** @var string $s */ $slice = self::parseSliceString($s); return new Slice(...$slice); @@ -92,7 +94,7 @@ public static function isSliceArray($s): bool return false; } - if (!(\count($s) >= 0 && \count($s) <= 3)) { + if (\count($s) > 3) { return false; }