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
**************************/