Skip to content

Commit

Permalink
Adding support for callback comparison on Column, Offset and TeoColum…
Browse files Browse the repository at this point in the history
…n constraints
  • Loading branch information
nyamsprod committed May 24, 2024
1 parent e201a39 commit ae9b441
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 30 deletions.
8 changes: 4 additions & 4 deletions docs/9.0/reader/statement.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ $filteredData = array_filter($data, $criteria, ARRAY_FILTER_USE_BOTH));
//Filtering an array using the XOR logical operator
```

As shown in the example the `Criteria` class also combines `Closure` conditions, which means that
you can use a callable whose signature matches the one use for the `where` method.
As shown in the example the `Criteria` class also combines `Closure` conditions, which means
that you can use a callable whose signature matches the one use for the `where` method.

### Ordering

Expand Down Expand Up @@ -302,8 +302,8 @@ $records = Statement::create()
// $records is a League\Csv\ResultSet instance with only 3 fields
```

While we explain each method separately it is understood that you could use them all together to query your
CSV document as you want like in the following example.
While we explain each method separately it is understood that you could use them all together
to query your CSV document as you want like in the following example.

```php
use League\Csv\Reader;
Expand Down
2 changes: 1 addition & 1 deletion src/FragmentFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ private function find(array $parsedExpression, TabularDataReader $tabularDataRea

return array_map(
fn (array $selection) => Statement::create()
->select(...$selection['columns'])
->offset($selection['start'])
->limit($selection['length'])
->select(...$selection['columns'])
->process($tabularDataReader),
$selections
);
Expand Down
24 changes: 18 additions & 6 deletions src/Query/Constraint/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query;
Expand All @@ -34,23 +35,29 @@ final class Column implements Query\Predicate
*/
private function __construct(
public readonly string|int $column,
public readonly Comparison $operator,
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
$this->operator->accept($this->value);
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}

/**
* @throws Query\QueryException
*/
public static function filterOn(
string|int $column,
Comparison|string $operator,
mixed $value,
Comparison|Closure|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($column, $operator, null);
}

return new self(
$column,
!$operator instanceof Comparison ? Comparison::fromOperator($operator) : $operator,
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}
Expand All @@ -61,7 +68,12 @@ public static function filterOn(
*/
public function __invoke(mixed $value, int|string $key): bool
{
return $this->operator->compare(Query\Row::from($value)->value($this->column), $this->value);
$subject = Query\Row::from($value)->value($this->column);
if ($this->operator instanceof Closure) {
return ($this->operator)($subject);
}

return $this->operator->compare($subject, $this->value);
}

public function filter(iterable $value): Iterator
Expand Down
9 changes: 9 additions & 0 deletions src/Query/Constraint/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,13 @@ public function it_will_throw_if_the_column_does_not_exist(): void

[...$this->stmt->where($predicate)->process($this->document)];
}

#[Test]
public function it_can_filter_the_tabular_data_based_on_the_column_value_and_a_callback(): void
{
$predicate = Column::filterOn('Country', fn (string $value): bool => 'UK' === $value);
$result = $this->stmt->where($predicate)->process($this->document);

self::assertCount(1, $result);
}
}
72 changes: 72 additions & 0 deletions src/Query/Constraint/Offset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace League\Csv\Query\Constraint;

use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query;
use Traversable;

/**
* Enable filtering a record based on its offset.
*
* When used with PHP's array_filter with the ARRAY_FILTER_USE_BOTH flag
* the record value WILL NOT BE taken into account
*/
final class Offset implements Query\Predicate
{
/**
* @throws Query\QueryException
*/
private function __construct(
public readonly Comparison|Closure $operator,
public readonly mixed $value,
) {
if (!$this->operator instanceof Closure) {
$this->operator->accept($this->value);
}
}

/**
* @throws Query\QueryException
*/
public static function filterOn(
Comparison|Closure|string $operator,
mixed $value = null,
): self {
if ($operator instanceof Closure) {
return new self($operator, null);
}

return new self(
is_string($operator) ? Comparison::fromOperator($operator) : $operator,
$value
);
}

/**
* @throws Query\QueryException
*/
public function __invoke(mixed $value, int|string $key): bool
{
if ($this->operator instanceof Closure) {
return ($this->operator)($key);
}

return $this->operator->compare($key, $this->value);
}

public function filter(iterable $value): Iterator
{
return new CallbackFilterIterator(match (true) {
$value instanceof Iterator => $value,
$value instanceof Traversable => new IteratorIterator($value),
default => new ArrayIterator($value),
}, $this);
}
}
17 changes: 13 additions & 4 deletions src/Query/Constraint/TwoColumns.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ArrayIterator;
use CallbackFilterIterator;
use Closure;
use Iterator;
use IteratorIterator;
use League\Csv\Query\Predicate;
Expand Down Expand Up @@ -43,13 +44,17 @@ final class TwoColumns implements Predicate
*/
private function __construct(
public readonly string|int $first,
public readonly Comparison $operator,
public readonly Comparison|Closure $operator,
public readonly array|string|int $second,
) {
if ($this->operator instanceof Closure && is_array($this->second)) {
throw new QueryException('The second column must be a string if the operator is a callback.');
}

if (is_array($this->second)) {
$res = array_filter($this->second, fn (mixed $value): bool => !is_string($value) && !is_int($value));
if ([] !== $res) {
throw new QueryException('The second column must be a string, an integer or a list of strings and/or integer.');
throw new QueryException('The second column must be a string, an integer or a list of strings and/or integer when the operator is not a callback.');
}
}
}
Expand All @@ -59,10 +64,10 @@ private function __construct(
*/
public static function filterOn(
string|int $firstColumn,
Comparison|string $operator,
Comparison|Closure|string $operator,
array|string|int $secondColumn
): self {
if (!$operator instanceof Comparison) {
if (is_string($operator)) {
$operator = Comparison::fromOperator($operator);
}

Expand All @@ -80,6 +85,10 @@ public function __invoke(mixed $value, int|string $key): bool
default => Row::from($value)->value($this->second),
};

if ($this->operator instanceof Closure) {
return ($this->operator)(Row::from($value)->value($this->first), $val);
}

return Column::filterOn($this->first, $this->operator, $val)($value, $key);
}

Expand Down
26 changes: 13 additions & 13 deletions src/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,50 +132,50 @@ final protected static function wrapSingleArgumentCallable(callable $where): cal
};
}

public function andWhere(string|int $column, Query\Constraint\Comparison|string $operator, mixed $value): self
public function andWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendCondition('and', Query\Constraint\Column::filterOn($column, $operator, $value));
return $this->appendWhere('and', Query\Constraint\Column::filterOn($column, $operator, $value));
}

public function orWhere(string|int $column, Query\Constraint\Comparison|string $operator, mixed $value): self
public function orWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendCondition('or', Query\Constraint\Column::filterOn($column, $operator, $value));
return $this->appendWhere('or', Query\Constraint\Column::filterOn($column, $operator, $value));
}

public function whereNot(string|int $column, Query\Constraint\Comparison|string $operator, mixed $value): self
public function whereNot(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendCondition('not', Query\Constraint\Column::filterOn($column, $operator, $value));
return $this->appendWhere('not', Query\Constraint\Column::filterOn($column, $operator, $value));
}

public function xorWhere(string|int $column, Query\Constraint\Comparison|string $operator, mixed $value): self
public function xorWhere(string|int $column, Query\Constraint\Comparison|Closure|string $operator, mixed $value = null): self
{
return $this->appendCondition('xor', Query\Constraint\Column::filterOn($column, $operator, $value));
return $this->appendWhere('xor', Query\Constraint\Column::filterOn($column, $operator, $value));
}

public function andWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendCondition('and', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
return $this->appendWhere('and', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}

public function orWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendCondition('or', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
return $this->appendWhere('or', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}

public function xorWhereColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendCondition('xor', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
return $this->appendWhere('xor', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}

public function whereNotColumn(string|int $first, Query\Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->appendCondition('not', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
return $this->appendWhere('not', Query\Constraint\TwoColumns::filterOn($first, $operator, $second));
}

/**
* @param 'and'|'not'|'or'|'xor' $joiner
*/
final protected function appendCondition(string $joiner, Query\Predicate $predicate): self
final protected function appendWhere(string $joiner, Query\Predicate $predicate): self
{
if ([] === $this->where) {
return $this->where(match ($joiner) {
Expand Down
4 changes: 2 additions & 2 deletions src/TabularDataReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public function getRecords(array $header = []): Iterator;
public function fetchPairs($offset_index = 0, $value_index = 1): Iterator;

/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.9.0
*
Expand All @@ -129,7 +129,7 @@ public function fetchPairs($offset_index = 0, $value_index = 1): Iterator;
public function fetchOne(int $nth_record = 0): array;

/**
* DEPRECATION WARNING! This class will be removed in the next major point release.
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.8.0
*
Expand Down

0 comments on commit ae9b441

Please sign in to comment.