diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd2112d..e746b141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All Notable changes to `Csv` will be documented in this file ### Added - `Writer::necessaryEnclosure` +- `TabularDataReader::selectExcept` +- `Statement::selectExcept` ### Deprecated @@ -16,7 +18,7 @@ All Notable changes to `Csv` will be documented in this file - `Comparison::CONTAINS` must check the value is a string before calling `str_compare` [#548](https://github.com/thephpleague/csv/pull/548) by [cage-is](https://github.com/cage-is) - Fix testing to improve Debian integration [#549](https://github.com/thephpleague/csv/pull/549) by [David Prévot and tenzap](https://github.com/tenzap) -- `Bom::tryFromSequence` and `Bom::fromSequence` supports the `Reader` and `Writer` class. +- `Bom::tryFromSequence` and `Bom::fromSequence` supports the `Reader` and `Writer` classes. ### Removed diff --git a/docs/9.0/reader/statement.md b/docs/9.0/reader/statement.md index 753fc02b..6e587979 100644 --- a/docs/9.0/reader/statement.md +++ b/docs/9.0/reader/statement.md @@ -406,6 +406,24 @@ $records = $constraints->process($csv); Since a `Statement` instance is independent of the CSV document you can re-use it on different CSV documents or `TabularDataReader` instances if needed. +In the event where you have a lot of fields to select but a few to remove you may use the `selectExcept` method +to select all the fields except the one you will specify as argument to the method: + +```php +$constraints = Statement::create() + ->selectExcept('Integer') + ->andWhere('Float', '<', 1.3) + ->orderByDesc('Integer') + ->offset(2) + ->limit(5); + +//will select all the fields except the `Integer` one. +``` + +

Added in version 9.22.0 for Reader and ResultSet.

+

You can not use select and selectExcept within the same +statement, both methods are mutually exclusive.

+ ### Adding constraint on condition

new in version 9.19.0.

diff --git a/docs/9.0/reader/tabular-data-reader.md b/docs/9.0/reader/tabular-data-reader.md index 0457cc24..bf26d2a0 100644 --- a/docs/9.0/reader/tabular-data-reader.md +++ b/docs/9.0/reader/tabular-data-reader.md @@ -455,7 +455,7 @@ $records = $resultSet->slice(10, 25);

Wraps the functionality of Statement::offset and Statement::limit.

Added in version 9.11.0 for Reader and ResultSet.

-### select +### select and selectExcept You may not always want to select all columns from the tabular data. Using the `select` method, you can specify which columns to use. The column can be specified by their name, if the instance @@ -474,6 +474,22 @@ $reader = Reader::createFromPath('/path/to/my/file.csv')

Added in version 9.12.0 for Reader and ResultSet.

Wraps the functionality of Statement::select.

+In the event where you have a lot of fields to select but a few to remove you may use the `selectExcept` method +to select all the fields except the one you will specify as argument to the method: + +```php +use League\Csv\Reader; + +$reader = Reader::createFromPath('/path/to/my/file.csv') + ->selectExcept(3); + +//$reader is a new TabularDataReader with all the fields except the 4th one. +// fields are re-indexed. +``` + +

Added in version 9.22.0 for Reader and ResultSet.

+

Wraps the functionality of Statement::selectExcept.

+ ### mapHeader

Added in version 9.15.0 for Reader and ResultSet.

diff --git a/src/Reader.php b/src/Reader.php index 4f7383ed..0bb6b3fa 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -475,6 +475,11 @@ public function select(string|int ...$columns): TabularDataReader return ResultSet::createFromTabularDataReader($this)->select(...$columns); } + public function selectExcept(string|int ...$columns): TabularDataReader + { + return ResultSet::createFromTabularDataReader($this)->selectExcept(...$columns); + } + /** * @param array $header * diff --git a/src/ResultSet.php b/src/ResultSet.php index 05fbaffd..cd61ffbc 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -31,6 +31,7 @@ use function array_reduce; use function array_search; use function array_values; +use function is_int; use function is_string; use function iterator_count; @@ -274,6 +275,74 @@ public function select(string|int ...$columns): TabularDataReader return new self(new MapIterator($this, $callback), $hasHeader ? $header : []); } + public function selectExcept(string|int ...$columns): TabularDataReader + { + if ([] === $columns) { + return $this; + } + + $recordsHeader = $this->getHeader(); + $hasHeader = [] !== $recordsHeader; + $selectColumnsToSkip = function (array $res, string|int $column) use ($recordsHeader, $hasHeader): array { + if ($hasHeader) { + if (is_string($column)) { + $index = array_search($column, $recordsHeader, true); + if (false === $index) { + throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__); + } + + $res[$index] = 1; + + return $res; + } + + if (!array_key_exists($column, $recordsHeader)) { + throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__); + } + + $res[$column] = 1; + + return $res; + } + + if (!is_int($column)) { + throw InvalidArgument::dueToInvalidColumnIndex($column, 'offset', __METHOD__); + } + + $res[$column] = 1; + + return $res; + }; + + /** @var array $columnsToSkip */ + $columnsToSkip = array_reduce($columns, $selectColumnsToSkip, []); + $callback = function (array $record) use ($columnsToSkip): array { + $element = []; + $index = 0; + foreach ($record as $name => $value) { + if (!array_key_exists($index, $columnsToSkip)) { + $element[$name] = $value; + } + ++$index; + } + + return $element; + }; + + $newHeader = []; + if ($hasHeader) { + $newHeader = array_values( + array_filter( + $recordsHeader, + fn (string|int $key) => !array_key_exists($key, $columnsToSkip), + ARRAY_FILTER_USE_KEY + ) + ); + } + + return new self(new MapIterator($this, $callback), $newHeader); + } + /** * EXPERIMENTAL WARNING! This method implementation will change in the next major point release. * diff --git a/src/Statement.php b/src/Statement.php index d9dd5806..34807ec7 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -36,6 +36,10 @@ */ class Statement { + private const INCLUDE_SELECT = 1; + private const EXCLUDE_SELECT = 2; + private const NO_SELECT = 0; + /** @var array Callables to filter the iterator. */ protected array $where = []; /** @var array Callables to sort the iterator. */ @@ -46,6 +50,7 @@ class Statement protected int $limit = -1; /** @var array */ protected array $select = []; + protected int $select_mode = self::NO_SELECT; /** * @param ?callable(array, array-key): bool $where, Deprecated argument use Statement::where instead @@ -79,12 +84,29 @@ public static function create(?callable $where = null, int $offset = 0, int $lim */ public function select(string|int ...$columns): self { - if ($columns === $this->select) { + if ($columns === $this->select && self::INCLUDE_SELECT === $this->select_mode) { + return $this; + } + + $clone = clone $this; + $clone->select = $columns; + $clone->select_mode = self::INCLUDE_SELECT; + + return $clone; + } + + /** + * Sets the Iterator element columns. + */ + public function selectExcept(string|int ...$columns): self + { + if ($columns === $this->select && self::EXCLUDE_SELECT === $this->select_mode) { return $this; } $clone = clone $this; $clone->select = $columns; + $clone->select_mode = self::EXCLUDE_SELECT; return $clone; } @@ -339,7 +361,13 @@ public function process(TabularDataReader $tabular_data, array $header = []): Ta $iterator = Query\Limit::new($this->offset, $this->limit)->slice($iterator); } - return (new ResultSet($iterator, $header))->select(...$this->select); + $iterator = new ResultSet($iterator, $header); + + return match ($this->select_mode) { + self::EXCLUDE_SELECT => $iterator->selectExcept(...$this->select), + self::INCLUDE_SELECT => $iterator->select(...$this->select), + default => $iterator, + }; } /** diff --git a/src/StatementTest.php b/src/StatementTest.php index 75540cc5..f01479ca 100644 --- a/src/StatementTest.php +++ b/src/StatementTest.php @@ -259,4 +259,23 @@ public function testItCanCompareNullValue(): void self::assertCount(1, $statement->process($csv)); } + + public function testSelectExcept(): void + { + $document = <<setHeaderOffset(0); + + $statement = Statement::create() + ->limit(1) + ->selectExcept('Number'); + + self::assertSame(['Title' => 'Commander', 'Name' => 'Fred'], $statement->process($csv)->first()); + } } diff --git a/src/TabularDataReader.php b/src/TabularDataReader.php index 47933714..c097cfe7 100644 --- a/src/TabularDataReader.php +++ b/src/TabularDataReader.php @@ -39,6 +39,7 @@ * @method TabularDataReader slice(int $offset, ?int $length = null) extracts a slice of $length elements starting at position $offset from the Collection. * @method TabularDataReader sorted(Query\Sort|Closure $orderBy) sorts the Collection according to the closure provided see Statement::orderBy method * @method TabularDataReader select(string|int ...$columnOffsetOrName) extract a selection of the tabular data records columns. + * @method TabularDataReader selectExcept(string|int ...$columnOffsetOrName) specifies the names or index of one or more columns to exclude from the selection of the tabular data records columns. * @method TabularDataReader matchingFirstOrFail(string $expression) extract the first found fragment identifier of the tabular data or fail * @method TabularDataReader|null matchingFirst(string $expression) extract the first found fragment identifier of the tabular data or return null if none is found * @method iterable matching(string $expression) extract all found fragment identifiers for the tabular data diff --git a/src/TabularDataReaderTestCase.php b/src/TabularDataReaderTestCase.php index 1f58c273..cb0ffbe7 100644 --- a/src/TabularDataReaderTestCase.php +++ b/src/TabularDataReaderTestCase.php @@ -81,6 +81,50 @@ public function testTabularReaderSelectFailsWithInvalidColumnOffset(): void ->select(0, 18); } + /*************************** + * TabularDataReader::selectExcept + ****************************/ + + + #[Test] + public function testTabularSelectExceptWithoutHeader(): void + { + self::assertSame([1 => 'temperature', 2 => 'place'], $this->tabularData()->selectExcept(0)->first()); + } + + #[Test] + public function testTabularSelectExceptWithHeader(): void + { + self::assertSame(['temperature' => '1', 'place' => 'Galway'], $this->tabularDataWithHeader()->selectExcept('date')->first()); + self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectExcept('temperature', 'date')->first()); + self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectExcept(1, 'date')->first()); + self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectExcept('temperature', 0)->first()); + } + + public function testTabularReaderSelectExceptFailsWithInvalidColumn(): void + { + $this->expectException(InvalidArgument::class); + + $this->tabularData() + ->selectExcept('temperature', 'place'); + } + + public function testTabularReaderSelectExceptFailsWithInvalidColumnName(): void + { + $this->expectException(InvalidArgument::class); + + $this->tabularDataWithHeader() + ->selectExcept('temperature', 'foobar'); + } + + public function testTabularReaderSelectExceptFailsWithInvalidColumnOffset(): void + { + $this->expectException(InvalidArgument::class); + + $this->tabularDataWithHeader() + ->selectExcept(0, 18); + } + /*************************** * TabularDataReader::matching, matchingFirst, matchingFirstOrFail **************************/