Skip to content

Commit

Permalink
Bugfix issue #524
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed May 10, 2024
1 parent 9b1a03f commit 88b07a6
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 61 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ All Notable changes to `Csv` will be documented in this file
- `Statement::orderByAsc`
- `Statement::orderByDesc`
- `Statement::andWhere`
- `Statement::orWhere`
- `Statement::whereNot`
- `Statement::orWhere`
- `Statement::xorWhere`
- `Statement::andWhereColumn`
- `Statement::orWhereColumn`
- `Statement::whereColumnNot`
- `Statement::orWhereColumn`
- `Statement::xorWhereColumn`
- `Constraint` feature to allow easier filtering and ordering of tabular data

### Deprecated
Expand All @@ -31,6 +33,7 @@ All Notable changes to `Csv` will be documented in this file
- the `AbstractCsv` BOM related properties are moved to being `Bom` instances instead of nullable string.
- `setOutpuBOM` will only accept valid BOM sequences all other values except the empty string will throw a `ValueError` exception;
- The package no longer requires the `ext-mbstring` extension to work. But you should have it install in your system in order to use the `mbstring` related stream filters.
- Issue [#524](https://github.com/thephpleague/csv/issues/524) fix issue with `ResultSet::chunkBy` not working as documented.

### Removed

Expand Down
10 changes: 6 additions & 4 deletions docs/9.0/reader/statement.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ $records = Statement::create()

<p class="message-info">New since version <code>9.16.0</code></p>

To ease the `Statement::where` usage the following methods are introduced: `andWhere`, `orWhere` and `whereNot`;
To ease the `Statement::where` usage the following methods are introduced: `andWhere`, `whereNot`, `orWhere` and `xorWhere`;

These methods are used to filter the record based on their column value. Instead of using a callable,
the methods require three (3) arguments. The first argument is the column to filter on. It can be
Expand Down Expand Up @@ -147,10 +147,12 @@ Internally they use one of the following PHP's function `str_contains`, `str_sta
- If the specified column could not be found during process an `StatementError` exception is triggered;
- If the `value` is incorrect according to the operator constraints an `InvalidArgument` exception will be triggered.

To enable comparing columns with each other the following methods are also added: `andWhereColumn`, `orWhereColumn` and `whereColumnNot`
To enable comparing two columns with each other the following methods are also added:
`andWhereColumn`, `whereColumnNot`, `orWhereColumn` and `xorWhereColumn`

The only distinction with their value counterparts is in the third argument.Instead of specifying a value, it specifies
another column (via its string name or integer name) to compare columns with each other.
The only distinction with their value counterparts is in the third argument. Instead of specifying
a value, it specifies another column (via its string name or integer name) to compare columns
with each other.

```php
use League\Csv\Reader;
Expand Down
32 changes: 16 additions & 16 deletions src/Constraint/ColumnValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,6 @@ public static function filterOn(
return new self($column, $operator, $value);
}

private static function fieldValue(array $array, string|int $key): mixed
{
$offset = $key;
if (is_int($offset)) {
if (!array_is_list($array)) {
$array = array_values($array);
}

if ($offset < 0) {
$offset += count($array);
}
}

return array_key_exists($offset, $array) ? $array[$offset] : throw StatementError::dueToUnknownColumn($key);
}

/**
* @throws InvalidArgument
* @throws StatementError
Expand Down Expand Up @@ -148,4 +132,20 @@ public function __invoke(array $record, int|string $key): bool

return $filter($record);
}

private static function fieldValue(array $array, string|int $key): mixed
{
$offset = $key;
if (is_int($offset)) {
if (!array_is_list($array)) {
$array = array_values($array);
}

if ($offset < 0) {
$offset += count($array);
}
}

return array_key_exists($offset, $array) ? $array[$offset] : throw StatementError::dueToUnknownColumn($key);
}
}
16 changes: 6 additions & 10 deletions src/Constraint/Criteria.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Closure;

use function array_map;
use function array_reduce;

final class Criteria implements PredicateCombinator
{
Expand Down Expand Up @@ -114,16 +115,11 @@ public static function exclusiveOr(Predicate|Closure|callable ...$predicates): s
{
$predicates = array_map(self::callableToClosure(...), $predicates);

return new self(function (array $record, int|string $key) use ($predicates): bool {
$true = 0;
foreach ($predicates as $predicate) {
if (true === $predicate($record, $key)) {
$true++;
}
}

return 0 !== ($true % 2);
});
return new self(fn (array $record, int|string $key): bool => array_reduce(
$predicates,
fn (bool $bool, Predicate|Closure $predicate) => $predicate($record, $key) xor $bool,
false
));
}

public function __invoke(array $record, int|string $key): bool
Expand Down
14 changes: 4 additions & 10 deletions src/Constraint/MultiSort.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ final class MultiSort implements SortCombinator
*/
private function __construct(Sort|Closure|callable ...$sorts)
{
$this->sorts = array_map(self::callableToClosure(...), $sorts);
$this->sorts = array_map(
static fn (Sort|Closure|callable $sort): Sort|Closure => $sort instanceof Closure || $sort instanceof Sort ? $sort : $sort(...),
$sorts
);
}

/**
Expand All @@ -43,15 +46,6 @@ public static function new(Sort|Closure|callable ...$sorts): self
return new self(...$sorts);
}

private static function callableToClosure(Sort|Closure|callable $sort): Sort|Closure
{
if ($sort instanceof Closure || $sort instanceof Sort) {
return $sort;
}

return $sort(...);
}

/**
* @param Sort|Closure(array, array): int|callable(array, array): int ...$sorts
*/
Expand Down
26 changes: 13 additions & 13 deletions src/Constraint/TwoColumns.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ public static function filterOn(
return new self($firstColumn, $operator, $secondColumn);
}

/**
* @throws InvalidArgument
*/
public function __invoke(array $record, string|int $key): bool
{
$value = match (true) {
is_array($this->second) => array_map(fn (string|int $column) => self::fieldValue($record, $column), $this->second),
default => self::fieldValue($record, $this->second),
};

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

private static function fieldValue(array $array, string|int $key): mixed
{
$offset = $key;
Expand All @@ -81,17 +94,4 @@ private static function fieldValue(array $array, string|int $key): mixed

return array_key_exists($offset, $array) ? $array[$offset] : throw StatementError::dueToUnknownColumn($key);
}

/**
* @throws InvalidArgument
*/
public function __invoke(array $record, string|int $key): bool
{
$value = match (true) {
is_array($this->second) => array_map(fn (string|int $column) => self::fieldValue($record, $column), $this->second),
default => self::fieldValue($record, $this->second),
};

return ColumnValue::filterOn($this->first, $this->operator, $value)($record, $key);
}
}
6 changes: 3 additions & 3 deletions src/Doctrine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ $csv->setDelimiter(';');
$criteria = Criteria::create()
->andWhere(Criteria::expr()->eq('prenom', 'Adam'))
->orderBy( [ 'annee' => 'ASC', 'foo' => 'desc', ] )
->setFirstResult(0)
->setFirstResult(3)
->setMaxResults(10)
;

$resultset = CsvDoctrine\CriteriaConverter::convert($criteria)->process($csv);
```

can be written like follow since version `9.16.0`:
can be written using only the `Statement` class since version `9.16.0`:

```php
<?php
Expand All @@ -60,7 +60,7 @@ $criteria = Statement::create()
->andWhere('prenom', '=', 'Adam')
->orderByAsc('annee')
->orderByDesc('foo')
->offset(0) //added for completeness but not required in this example
->offset(3)
->limit(10);

$resultset = $criteria->process($csv);
Expand Down
2 changes: 2 additions & 0 deletions src/ResultSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ public function chunkBy(int $recordsCount): iterable
++$nbRecords;
if ($nbRecords === $recordsCount) {
yield new self($records, $header);
$records = [];
$nbRecords = 0;
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/ResultSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,24 @@ public function testHeaderMapperOnResultSetAlwaysIgnoreTheColumnName(): void
->process($reader)
->getRecords(['lastname' => 'nom de famille', 'firstname' => 'prenom', 'e-mail' => 'e-mail'])];
}

public function testChunkByIssue524(): void
{
$csv = <<<CSV
firstname,lastname,e-mail
john,doe,john.doe@example.com
jane,doe,jane.doe@example.com
jose,doe,jose.doe@example.com
jeny,doe,jeny.doe@example.com
jack,doe,jack.doe@example.com
CSV;
$reader = Reader::createFromString($csv)->setHeaderOffset(0);

$total = [];
foreach ($reader->chunkBy(2) as $row) {
$total[] = count($row);
}

self::assertSame([2, 2, 1], $total);
}
}
18 changes: 15 additions & 3 deletions src/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function select(string|int ...$columns): self
*/
public function where(callable $where): self
{
$where = $this->wrapSingleArgumentCallable($where);
$where = self::wrapSingleArgumentCallable($where);

$clone = clone $this;
$clone->where[] = $where;
Expand All @@ -98,7 +98,7 @@ public function where(callable $where): self
*
* @return callable(array, array-key): bool
*/
private function wrapSingleArgumentCallable(callable $where): callable
final protected static function wrapSingleArgumentCallable(callable $where): callable
{
if ($where instanceof Constraint\Predicate) {
return $where;
Expand Down Expand Up @@ -128,6 +128,11 @@ public function whereNot(string|int $column, Constraint\Comparison|string $opera
return $this->addCondition('not', Constraint\ColumnValue::filterOn($column, $operator, $value));
}

public function xorWhere(string|int $column, Constraint\Comparison|string $operator, mixed $value): self
{
return $this->addCondition('xor', Constraint\ColumnValue::filterOn($column, $operator, $value));
}

public function andWhereColumn(string|int $first, Constraint\Comparison|string $operator, array|int|string $second): self
{
return $this->addCondition('and', Constraint\TwoColumns::filterOn($first, $operator, $second));
Expand All @@ -138,13 +143,18 @@ public function orWhereColumn(string|int $first, Constraint\Comparison|string $o
return $this->addCondition('or', Constraint\TwoColumns::filterOn($first, $operator, $second));
}

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

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

/**
* @param 'and'|'or'|'not' $joiner
* @param 'and'|'not'|'or'|'xor' $joiner
*/
final protected function addCondition(string $joiner, Constraint\Predicate $predicate): self
{
Expand All @@ -153,6 +163,7 @@ final protected function addCondition(string $joiner, Constraint\Predicate $pred
'and' => $predicate,
'not' => Constraint\Criteria::none($predicate),
'or' => Constraint\Criteria::any($predicate),
'xor' => Constraint\Criteria::exclusiveOr($predicate),
});
}

Expand All @@ -163,6 +174,7 @@ final protected function addCondition(string $joiner, Constraint\Predicate $pred
'and' => $predicates->and($predicate),
'not' => $predicates->not($predicate),
'or' => $predicates->or($predicate),
'xor' => $predicates->xor($predicate),
}];

return $clone;
Expand Down

0 comments on commit 88b07a6

Please sign in to comment.