Refactor "Sorted" rule

The sorted rule accepts a callback on its constructor that may be used
to filter values from inside the input. However, with the "Call" rule
one can archive almost the same result. Besides that particular
characteristic, its constructor accepts a boolean value to determine
whether the sorting is ascending or descending.

This commit will remove the callback from the constructor and replace
the boolean by a string which can be "ASC" or "DESC."

Along with those changes, this change will make a few more improvements:

- Make the exception message specific about the sorting direction;

- Allow the rule to validate also strings;

- Update documentation.

Co-authored-by: Danilo Correa <danilosilva87@gmail.com>
Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2019-03-04 10:35:28 -03:00
parent 5444ab5b0a
commit 1f6c821fb6
No known key found for this signature in database
GPG key ID: 221E9281655813A6
6 changed files with 212 additions and 160 deletions

View file

@ -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)

View file

@ -13,21 +13,36 @@ declare(strict_types=1);
namespace Respect\Validation\Exceptions;
use Respect\Validation\Rules\Sorted;
/**
* @author Henrique Moody <henriquemoody@gmail.com>
* @author Mikhail Vyrtsev <reeywhaar@gmail.com>
*/
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;
}
}

View file

@ -13,30 +13,39 @@ declare(strict_types=1);
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 <henriquemoody@gmail.com>
* @author Mikhail Vyrtsev <reeywhaar@gmail.com>
*/
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 @@ final class Sorted extends AbstractRule
*/
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);
}
}

View file

@ -148,6 +148,7 @@ use function count;
* @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()

View file

@ -0,0 +1,71 @@
--CREDITS--
Danilo Correa <danilosilva87@gmail.com>
Henrique Moody <henriquemoody@gmail.com>
--FILE--
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Exceptions\SortedException;
use Respect\Validation\Validator as v;
try {
v::sorted('ASC')->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

View file

@ -13,133 +13,62 @@ declare(strict_types=1);
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 <carusogabriel34@gmail.com>
* @author Henrique Moody <henriquemoody@gmail.com>
* @author Mikhail Vyrtsev <reeywhaar@gmail.com>
*/
final class SortedTest extends TestCase
final class SortedTest extends RuleTestCase
{
/**
* @test
* {@inheritdoc}
*/
public function passes(): void
public function providerForValidInput(): array
{
$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
*/
public function notPasses(): void
{
$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);
}
/**
* @test
*/
public function passesByFunction(): void
{
$arr = [
[
'key' => 1,
],
[
'key' => 2,
],
[
'key' => 5,
],
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'],
];
}
/**
* {@inheritdoc}
*/
public function providerForInvalidInput(): array
{
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');
}
}