diff --git a/docs/rules/Sorted.md b/docs/rules/Sorted.md index 9a0772642..365b08680 100644 --- a/docs/rules/Sorted.md +++ b/docs/rules/Sorted.md @@ -1,48 +1,48 @@ -# Ordered +# Sorted -- `Sorted(callable $fn = null, bool $ascending = true)` +- `Sorted(string $direction)` -Validates if the input is Sorted +Validates whether the input is sorted in a certain order or not. ```php -v::sorted()->validate([1,2,3]); // true -v::sorted()->validate([1,6,3]); // false -v::sorted(null, false)->validate([3,2,1]); // true -v::sorted(function($x){ - return $x['key']; -})->validate([ - [ - 'key' => 1, - ], - [ - 'key' => 5, - ], - [ - 'key' => 9, - ], -]); // true -v::sorted(function($x){ - return $x['key']; -})->validate([ - [ - 'key' => 1, - ], - [ - 'key' => 7, - ], - [ - 'key' => 4, - ], -]); // false +v::sorted('ASC')->validate([1, 2, 3]); // true +v::sorted('ASC')->validate('ABC'); // true +v::sorted('DESC')->validate([3, 2, 1]); // true +v::sorted('ASC')->validate([]); // true +v::sorted('ASC')->validate([1]); // true +``` + +You can also combine [Call](Call.md) to create custom validations: + +```php +v::call( + static function (array $input): array { + return array_column($input, 'key'); + }, + v::sorted('ASC') + )->validate([ + ['key' => 1], + ['key' => 5], + ['key' => 9], + ]); // true + +v::call('strval', v::sorted('DESC'))->validate(4321); // true + +v::call('iterator_to_array', v::sorted())->validate(new ArrayIterator([1, 7, 4])); // false ``` ## Changelog Version | Description --------|------------- + 2.0.0 | Add support for strings + 2.0.0 | Do not use array keys to sort + 2.0.0 | Use sorting direction instead of boolean value + 2.0.0 | Do not accept callback in the constructor 1.1.1 | Created *** See also: +- [Call](Call.md) - [ArrayVal](ArrayVal.md) diff --git a/library/Exceptions/SortedException.php b/library/Exceptions/SortedException.php index 7b2e20cee..aa3b1f291 100644 --- a/library/Exceptions/SortedException.php +++ b/library/Exceptions/SortedException.php @@ -13,21 +13,36 @@ namespace Respect\Validation\Exceptions; +use Respect\Validation\Rules\Sorted; + /** * @author Henrique Moody * @author Mikhail Vyrtsev */ final class SortedException extends ValidationException { + public const ASCENDING = 'ascending'; + public const DESCENDING = 'descending'; + /** * {@inheritdoc} */ public static $defaultTemplates = [ self::MODE_DEFAULT => [ - self::STANDARD => '{{name}} must be ordered', + self::ASCENDING => '{{name}} must be sorted in ascending order', + self::DESCENDING => '{{name}} must be sorted in descending order', ], self::MODE_NEGATIVE => [ - self::STANDARD => '{{name}} must not be ordered', + self::ASCENDING => '{{name}} must not be sorted in ascending order', + self::DESCENDING => '{{name}} must not be sorted in descending order', ], ]; + + /** + * {@inheritdoc} + */ + protected function chooseTemplate(): string + { + return $this->getParam('direction') === Sorted::ASCENDING ? self::ASCENDING : self::DESCENDING; + } } diff --git a/library/Rules/Sorted.php b/library/Rules/Sorted.php index 882a4d8cd..1809a87f9 100644 --- a/library/Rules/Sorted.php +++ b/library/Rules/Sorted.php @@ -13,30 +13,39 @@ namespace Respect\Validation\Rules; +use Respect\Validation\Exceptions\ComponentException; +use function array_values; use function count; +use function is_array; +use function is_string; +use function sprintf; +use function str_split; /** + * Validates whether the input is sorted in a certain order or not. + * * @author Henrique Moody * @author Mikhail Vyrtsev */ final class Sorted extends AbstractRule { - /** - * @var callable - */ - private $fn = null; + public const ASCENDING = 'ASC'; + public const DESCENDING = 'DESC'; /** - * @var bool + * @var string */ - private $ascending = true; + private $direction; - public function __construct(?callable $fn = null, bool $ascending = true) + public function __construct(string $direction) { - $this->fn = $fn ?? static function ($x) { - return $x; - }; - $this->ascending = $ascending; + if ($direction !== self::ASCENDING && $direction !== self::DESCENDING) { + throw new ComponentException( + sprintf('Direction should be either "%s" or "%s"', self::ASCENDING, self::DESCENDING) + ); + } + + $this->direction = $direction; } /** @@ -44,18 +53,45 @@ public function __construct(?callable $fn = null, bool $ascending = true) */ public function validate($input): bool { - $count = count($input); - if ($count < 2) { - return true; + if (!is_array($input) && !is_string($input)) { + return false; } - for ($i = 1; $i < $count; ++$i) { - if (($this->ascending && ($this->fn)($input[$i]) < ($this->fn)($input[$i - 1])) - || (!$this->ascending && ($this->fn)($input[$i]) > ($this->fn)($input[$i - 1])) - ) { + + $values = $this->getValues($input); + $count = count($values); + for ($position = 1; $position < $count; ++$position) { + if (!$this->isSorted($values[$position], $values[$position - 1])) { return false; } } return true; } + + /** + * @param mixed $current + * @param mixed $last + */ + private function isSorted($current, $last): bool + { + if ($this->direction === self::ASCENDING) { + return $current > $last; + } + + return $current < $last; + } + + /** + * @param string|mixed[] $input + * + * @return mixed[] + */ + private function getValues($input): array + { + if (is_array($input)) { + return array_values($input); + } + + return str_split($input); + } } diff --git a/library/Validator.php b/library/Validator.php index 44c37ef37..cd2d8c881 100644 --- a/library/Validator.php +++ b/library/Validator.php @@ -148,6 +148,7 @@ * @method static Validator sf(Constraint $constraint, ValidatorInterface $validator = null) * @method static Validator size(string $minSize = null, string $maxSize = null) * @method static Validator slug() + * @method static Validator sorted(string $direction) * @method static Validator space(string ...$additionalChars) * @method static Validator startsWith($startValue, bool $identical = false) * @method static Validator stringType() diff --git a/tests/integration/rules/sorted.phpt b/tests/integration/rules/sorted.phpt new file mode 100644 index 000000000..6f9dfedcb --- /dev/null +++ b/tests/integration/rules/sorted.phpt @@ -0,0 +1,71 @@ +--CREDITS-- +Danilo Correa +Henrique Moody +--FILE-- +check([1, 3, 2]); +} catch (SortedException $exception) { + echo $exception->getMessage().PHP_EOL; +} + +try { + v::sorted('DESC')->check([1, 2, 3]); +} catch (SortedException $exception) { + echo $exception->getMessage().PHP_EOL; +} + +try { + v::not(v::sorted('ASC'))->check([1, 2, 3]); +} catch (SortedException $exception) { + echo $exception->getMessage().PHP_EOL; +} + +try { + v::not(v::sorted('DESC'))->check([3, 2, 1]); +} catch (SortedException $exception) { + echo $exception->getMessage().PHP_EOL; +} + +try { + v::sorted('ASC')->assert([3, 2, 1]); +} catch (NestedValidationException $exception) { + echo $exception->getFullMessage().PHP_EOL; +} + +try { + v::sorted('DESC')->assert([1, 2, 3]); +} catch (NestedValidationException $exception) { + echo $exception->getFullMessage().PHP_EOL; +} + +try { + v::not(v::sorted('ASC'))->assert([1, 2, 3]); +} catch (NestedValidationException $exception) { + echo $exception->getFullMessage().PHP_EOL; +} + +try { + v::not(v::sorted('DESC'))->assert([3, 2, 1]); +} catch (NestedValidationException $exception) { + echo $exception->getFullMessage().PHP_EOL; +} +?> +--EXPECT-- +`{ 1, 3, 2 }` must be sorted in ascending order +`{ 1, 2, 3 }` must be sorted in descending order +`{ 1, 2, 3 }` must not be sorted in ascending order +`{ 3, 2, 1 }` must not be sorted in descending order +- `{ 3, 2, 1 }` must be sorted in ascending order +- `{ 1, 2, 3 }` must be sorted in descending order +- `{ 1, 2, 3 }` must not be sorted in ascending order +- `{ 3, 2, 1 }` must not be sorted in descending order diff --git a/tests/unit/Rules/SortedTest.php b/tests/unit/Rules/SortedTest.php index fbc12e1f4..4fb6bdcbc 100644 --- a/tests/unit/Rules/SortedTest.php +++ b/tests/unit/Rules/SortedTest.php @@ -13,133 +13,62 @@ namespace Respect\Validation\Rules; -use Respect\Validation\Test\TestCase; +use Respect\Validation\Exceptions\ComponentException; +use Respect\Validation\Test\RuleTestCase; /** - * @group rule - * @covers \Respect\Validation\Exceptions\SortedException + * @group rules + * * @covers \Respect\Validation\Rules\Sorted * * @author Gabriel Caruso * @author Henrique Moody * @author Mikhail Vyrtsev */ -final class SortedTest extends TestCase +final class SortedTest extends RuleTestCase { /** - * @test - */ - public function passes(): void - { - $arr = [1, 2, 3]; - $rule = new Sorted(); - - self::assertTrue($rule->validate($arr)); - $rule->assert($arr); - $rule->check($arr); - } - - /** - * @test - */ - public function passesWithEqualValues(): void - { - $arr = [1, 2, 2, 3]; - $rule = new Sorted(); - - self::assertTrue($rule->validate($arr)); - $rule->assert($arr); - $rule->check($arr); - } - - /** - * @expectedException \Respect\Validation\Exceptions\SortedException - * - * @test + * {@inheritdoc} */ - public function notPasses(): void + public function providerForValidInput(): array { - $arr = [1, 2, 4, 3]; - $rule = new Sorted(); - - self::assertFalse($rule->validate($arr)); - $rule->check($arr); - } - - /** - * @test - */ - public function passesDescending(): void - { - $arr = [10, 9, 8]; - $rule = new Sorted(null, false); - - self::assertTrue($rule->validate($arr)); - $rule->assert($arr); - $rule->check($arr); - } - - /** - * @test - */ - public function passesDescendingWithEqualValues(): void - { - $arr = [10, 9, 9, 8]; - $rule = new Sorted(null, false); - - self::assertTrue($rule->validate($arr)); - $rule->assert($arr); - $rule->check($arr); + return [ + 'empty' => [new Sorted('ASC'), []], + 'one item' => [new Sorted('ASC'), [1]], + 'one character' => [new Sorted('ASC'), 'z'], + 'ASC array-sequence' => [new Sorted('ASC'), [1, 3, 5]], + 'ASC sequence in associative array' => [new Sorted('ASC'), ['foo' => 1, 'bar' => 3, 'baz' => 5]], + 'ASC string-sequence' => [new Sorted('ASC'), 'ABCD'], + 'DESC array-sequence ' => [new Sorted('DESC'), [10, 9, 8]], + 'DESC string-sequence ' => [new Sorted('DESC'), 'zyx'], + ]; } /** - * @test + * {@inheritdoc} */ - public function passesByFunction(): void + public function providerForInvalidInput(): array { - $arr = [ - [ - 'key' => 1, - ], - [ - 'key' => 2, - ], - [ - 'key' => 5, - ], + return [ + 'duplicate' => [new Sorted('ASC'), [1, 1, 1]], + 'wrong ASC array-sequence' => [new Sorted('ASC'), [1, 3, 2]], + 'wrong ASC string-sequence' => [new Sorted('ASC'), 'xzy'], + 'wrong DESC array-sequence' => [new Sorted('DESC'), [1, 3, 2]], + 'wrong DESC string-sequence' => [new Sorted('DESC'), 'jml'], + 'DESC array-sequence with ASC validation' => [new Sorted('ASC'), [3, 2, 1]], + 'DESC string-sequence with ASC validation' => [new Sorted('ASC'), '321'], + 'ASC array-sequence with DESC validation' => [new Sorted('DESC'), [1, 2, 3]], + 'ASC string-sequence with DESC validation' => [new Sorted('DESC'), 'abc'], ]; - $rule = new Sorted(static function ($x) { - return $x['key']; - }); - - self::assertTrue($rule->validate($arr)); - $rule->assert($arr); - $rule->check($arr); } /** - * @expectedException \Respect\Validation\Exceptions\SortedException - * * @test */ - public function notPassesByFunction(): void + public function itShouldNotAcceptWrongSortingDirection(): void { - $arr = [ - [ - 'key' => 1, - ], - [ - 'key' => 8, - ], - [ - 'key' => 5, - ], - ]; - $rule = new Sorted(static function ($x) { - return $x['key']; - }); + $this->expectExceptionObject(new ComponentException('Direction should be either "ASC" or "DESC"')); - self::assertFalse($rule->validate($arr)); - $rule->check($arr); + new Sorted('something'); } }