Skip to content

Commit

Permalink
Improve query
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed May 21, 2024
1 parent 4b9de0d commit 19e27c1
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 57 deletions.
25 changes: 13 additions & 12 deletions src/Query/Constraint/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
use CallbackFilterIterator;
use Iterator;
use IteratorIterator;
use League\Csv\Query\Predicate;
use League\Csv\Query\Row;
use League\Csv\Query\QueryException;
use League\Csv\Query;
use ReflectionException;
use Traversable;

use function is_array;
use function array_filter;
use function iterator_to_array;

use const ARRAY_FILTER_USE_BOTH;

Expand All @@ -33,21 +33,22 @@
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record offset WILL NOT BE taken into account
*/
final class Column implements Predicate
final class Column implements Query\Predicate
{
/**
* @throws \League\Csv\Query\QueryException
* @throws Query\QueryException
*/
private function __construct(
public readonly string|int $column,
public readonly Comparison $operator,
public readonly mixed $value,
) {
if (!$this->operator->accept($this->value)) {
throw new QueryException('The value used for comparison with the `'.$this->operator->name.'` operator is not valid.');
}
$this->operator->accept($this->value);
}

/**
* @throws Query\QueryException
*/
public static function filterOn(
string|int $column,
Comparison|string $operator,
Expand All @@ -62,19 +63,19 @@ public static function filterOn(

/**
* @throws ReflectionException
* @throws \League\Csv\Query\QueryException
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
return $this->operator->compare(Row::from($value)->value($this->column), $this->value);
return $this->operator->compare(Query\Row::from($value)->value($this->column), $this->value);
}

public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
is_array($value) => new ArrayIterator($value),
$value instanceof Iterator => $value,
default => new IteratorIterator($value),
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}

Expand Down
79 changes: 53 additions & 26 deletions src/Query/Constraint/Comparison.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,53 +72,80 @@ public static function tryFromOperator(string $operator): ?self
*/
public static function fromOperator(string $operator): self
{
return self::tryFromOperator($operator) ?? throw new QueryException('Unknown or unsupported comparison operator `'.$operator.'`');
return self::tryFromOperator($operator) ?? throw QueryException::dueToUnknownOperator($operator);
}

/**
* @throws QueryException
*/
public function compare(mixed $needle, mixed $haystack): bool
public function compare(mixed $subject, mixed $reference): bool
{
$this->accept($reference);

return match ($this) {
self::Equals => self::isStrict($needle) ? $needle === $haystack : $needle == $haystack,
self::NotEquals => self::isStrict($needle) ? $needle !== $haystack : $needle != $haystack,
self::GreaterThan => $needle > $haystack,
self::GreaterThanOrEqual => $needle >= $haystack,
self::LesserThan => $needle < $haystack,
self::LesserThanOrEqual => $needle <= $haystack,
self::Between => (is_array($haystack) && array_is_list($haystack) && 2 === count($haystack)) ? $needle >= $haystack[0] && $needle <= $haystack[1] : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an list containing 2 values, the minimum and maximum values.'),
self::NotBetween => (is_array($haystack) && array_is_list($haystack) && 2 === count($haystack)) ? $needle < $haystack[0] || $needle > $haystack[1] : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an list containing 2 values, the minimum and maximum values.'),
self::Regexp => is_string($haystack) ? (is_string($needle) && 1 === preg_match($haystack, $needle)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::NotRegexp => is_string($haystack) ? (is_string($needle) && 1 !== preg_match($haystack, $needle)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::In => is_array($haystack) ? in_array($needle, $haystack, self::isStrict($needle)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an array.'), /* @phpstan-ignore-line */
self::NotIn => is_array($haystack) ? !in_array($needle, $haystack, self::isStrict($needle)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be an array.'), /* @phpstan-ignore-line */
self::Contains => is_string($haystack) ? (is_string($needle) && str_contains($needle, $haystack)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::NotContain => is_string($haystack) ? (is_string($needle) && !str_contains($needle, $haystack)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::StartsWith => is_string($haystack) ? (is_string($needle) && str_starts_with($needle, $haystack)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::EndsWith => is_string($haystack) ? (is_string($needle) && str_ends_with($needle, $haystack)) : throw new QueryException('The value used for comparison with the `'.$this->name.'` operator must be a string.'),
self::Equals => self::isSingleValue($subject) ? $subject === $reference : $subject == $reference,
self::NotEquals => self::isSingleValue($subject) ? $subject !== $reference : $subject != $reference,
self::GreaterThan => $subject > $reference,
self::GreaterThanOrEqual => $subject >= $reference,
self::LesserThan => $subject < $reference,
self::LesserThanOrEqual => $subject <= $reference,
self::Between => $subject >= $reference[0] && $subject <= $reference[1], /* @phpstan-ignore-line */
self::NotBetween => $subject < $reference[0] || $subject > $reference[1], /* @phpstan-ignore-line */
self::In => in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::NotIn => !in_array($subject, $reference, self::isSingleValue($subject)), /* @phpstan-ignore-line */
self::Regexp => is_string($subject) && 1 === preg_match($reference, $subject), /* @phpstan-ignore-line */
self::NotRegexp => is_string($subject) && 1 !== preg_match($reference, $subject), /* @phpstan-ignore-line */
self::Contains => str_contains($subject, $reference), /* @phpstan-ignore-line */
self::NotContain => is_string($subject) && !str_contains($subject, $reference), /* @phpstan-ignore-line */
self::StartsWith => is_string($subject) && str_starts_with($subject, $reference), /* @phpstan-ignore-line */
self::EndsWith => is_string($subject) && str_ends_with($subject, $reference), /* @phpstan-ignore-line */
};
}

private static function isStrict(mixed $value): bool
private static function isSingleValue(mixed $value): bool
{
return is_scalar($value) || null === $value;
}

public function accept(mixed $reference): bool
/**
* @throws QueryException
*/
public function accept(mixed $reference): void
{
return match ($this) {
match ($this) {
self::Between,
self::NotBetween => is_array($reference) && array_is_list($reference) && 2 === count($reference),
self::NotBetween => match (true) {
!is_array($reference),
!array_is_list($reference),
2 !== count($reference) => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be an list containing 2 values, the minimum and maximum values.'),
default => true,
},
self::In,
self::NotIn => is_array($reference),
self::NotIn => match (true) {
!is_array($reference) => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be an array.'),
default => true,
},
self::Regexp,
self::NotRegexp,
self::NotRegexp => match (true) {
!is_string($reference),
'' === $reference,
false === @preg_match($reference, '') => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be a valid regular expression pattern string.'),
default => true,
},
self::Contains,
self::NotContain,
self::StartsWith,
self::EndsWith => is_string($reference),
default => true,
self::EndsWith => match (true) {
!is_string($reference),
'' === $reference => throw new QueryException('The value used for comparison with the `' . $this->name . '` operator must be a non empty string.'),
default => true,
},
self::Equals,
self::NotEquals,
self::GreaterThanOrEqual,
self::GreaterThan,
self::LesserThanOrEqual,
self::LesserThan => true,
};
}
}
21 changes: 8 additions & 13 deletions src/Query/Constraint/TwoColumns.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use League\Csv\Query\Row;
use League\Csv\Query\QueryException;
use ReflectionException;
use Traversable;

use function array_filter;
use function is_array;
Expand Down Expand Up @@ -84,23 +85,17 @@ public function __invoke(mixed $value, int|string $key): bool

public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(
match (true) {
is_array($value) => new ArrayIterator($value),
$value instanceof Iterator => $value,
default => new IteratorIterator($value),
},
$this
);
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}

public function filterArray(iterable $values): array
public function filterArray(iterable $value): array
{
return array_filter(
match (is_array($values)) {
true => $values,
false => iterator_to_array($values),
},
!is_array($value) ? iterator_to_array($value) : $value,
$this,
ARRAY_FILTER_USE_BOTH
);
Expand Down
8 changes: 4 additions & 4 deletions src/Query/Limit.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ private function __construct(
public readonly int $length,
){
if (0 > $this->offset) {
throw new QueryException(__METHOD__.'() expects the offset to be greater or equal to 0, '.$this->offset.' given.');
throw new QueryException(self::class.' expects the offset to be greater or equal to 0, '.$this->offset.' given.');
}

if (-1 > $this->length) {
throw new QueryException(__METHOD__.'() expects the length to be greater or equal to -1, '.$this->length.' given.');
throw new QueryException(self::class.' expects the length to be greater or equal to -1, '.$this->length.' given.');
}
}

Expand All @@ -56,12 +56,12 @@ public function slice(iterable $value): LimitIterator
);
}

public function sliceArray(iterable $values, int $offset = 0, int $length = -1): array
public function sliceArray(iterable $values): array
{
return array_slice(
!is_array($values) ? iterator_to_array($values) : $values,
$this->offset,
$this->length === -1 ? null : $length,
$this->length === -1 ? null : $this->length,
true
);
}
Expand Down
9 changes: 7 additions & 2 deletions src/Query/QueryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
namespace League\Csv\Query;

use League\Csv\UnableToProcessCsv;
use RuntimeException;
use Exception;

final class QueryException extends RuntimeException implements UnableToProcessCsv
final class QueryException extends Exception implements UnableToProcessCsv
{
public static function dueToUnknownColumn(string|int $column, array|object $value): self
{
Expand All @@ -36,4 +36,9 @@ public static function dueToMissingColumn(): self
{
return new self('No valid column were found with the given data.');
}

public static function dueToUnknownOperator(string $operator): self
{
return new self('Unknown or unsupported comparison operator `'.$operator.'`');
}
}

0 comments on commit 19e27c1

Please sign in to comment.