Skip to content

Commit

Permalink
AAdding TabularDataReader::selectAllExcept method
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 15, 2025
1 parent 0ed5fa3 commit cd16489
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 5 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All Notable changes to `Csv` will be documented in this file
### Added

- `Writer::necessaryEnclosure`
- `TabularDataReader::selectAllExcept`
- `Statement::selectAllExcept`

### Deprecated

Expand All @@ -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

Expand Down
22 changes: 22 additions & 0 deletions docs/9.0/reader/statement.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,28 @@ $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 `selectAllExcept` method
to select all the fields except the one you will specify as argument to the method:

```php
$constraints = Statement::create()
->selectAllExcept('Integer')
->andWhere('Float', '<', 1.3)
->orderByDesc('Integer')
->offset(2)
->limit(5);

//will select all the fields except the `Integer` one.
```

Last but not least, to clear all selected columns and start a new, you need to call either
the `select` or the `selectAllExcept` methods with no parameter. It will return a new
`Statement` instance cleared of any previously made selection.

<p class="message-notice"><code>selectAllExcept</code> is available since version <code>9.22.0</code>.</p>
<p class="message-warning">You can not use <code>select</code> and <code>selectAllExcept</code> within the same
statement, both methods are mutually exclusive.</p>

### Adding constraint on condition

<p class="message-info">new in version <code>9.19.0</code>.</p>
Expand Down
18 changes: 17 additions & 1 deletion docs/9.0/reader/tabular-data-reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ $records = $resultSet->slice(10, 25);
<p class="message-info"> Wraps the functionality of <code>Statement::offset</code> and <code>Statement::limit</code>.</p>
<p class="message-notice">Added in version <code>9.11.0</code> for <code>Reader</code> and <code>ResultSet</code>.</p>

### select
### select and selectAllExcept

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
Expand All @@ -474,6 +474,22 @@ $reader = Reader::createFromPath('/path/to/my/file.csv')
<p class="message-notice">Added in version <code>9.12.0</code> for <code>Reader</code> and <code>ResultSet</code>.</p>
<p class="message-info"> Wraps the functionality of <code>Statement::select</code>.</p>

In the event where you have a lot of fields to select but a few to remove you may use the `selectAllExcept` 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')
->selectAllExcept(3);

//$reader is a new TabularDataReader with all the fields except the 4th one.
// fields are re-indexed.
```

<p class="message-notice">Added in version <code>9.22.0</code> for <code>Reader</code> and <code>ResultSet</code>.</p>
<p class="message-info"> Wraps the functionality of <code>Statement::selectAllExcept</code>.</p>

### mapHeader

<p class="message-notice">Added in version <code>9.15.0</code> for <code>Reader</code> and <code>ResultSet</code>.</p>
Expand Down
5 changes: 5 additions & 0 deletions src/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@ public function select(string|int ...$columns): TabularDataReader
return ResultSet::createFromTabularDataReader($this)->select(...$columns);
}

public function selectAllExcept(string|int ...$columns): TabularDataReader
{
return ResultSet::createFromTabularDataReader($this)->selectAllExcept(...$columns);
}

/**
* @param array<string> $header
*
Expand Down
69 changes: 69 additions & 0 deletions src/ResultSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -274,6 +275,74 @@ public function select(string|int ...$columns): TabularDataReader
return new self(new MapIterator($this, $callback), $hasHeader ? $header : []);
}

public function selectAllExcept(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<int> $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.
*
Expand Down
35 changes: 32 additions & 3 deletions src/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
*/
class Statement
{
final protected const COLUMN_INCLUDE = 1;
final protected const COLUMN_EXCLUDE = 2;
final protected const COLUMN_ALL = 0;

/** @var array<ConditionExtended> Callables to filter the iterator. */
protected array $where = [];
/** @var array<OrderingExtended> Callables to sort the iterator. */
Expand All @@ -46,6 +50,8 @@ class Statement
protected int $limit = -1;
/** @var array<string|int> */
protected array $select = [];
/** @var self::COLUMN_* */
protected int $select_mode = self::COLUMN_ALL;

/**
* @param ?callable(array, array-key): bool $where, Deprecated argument use Statement::where instead
Expand Down Expand Up @@ -75,16 +81,33 @@ public static function create(?callable $where = null, int $offset = 0, int $lim
}

/**
* Sets the Iterator element columns.
* Select all the columns from the tabular data that MUST BE present in the ResultSet.
*/
public function select(string|int ...$columns): self
{
if ($columns === $this->select) {
if ($columns === $this->select && self::COLUMN_INCLUDE === $this->select_mode) {
return $this;
}

$clone = clone $this;
$clone->select = $columns;
$clone->select_mode = [] === $columns ? self::COLUMN_ALL : self::COLUMN_INCLUDE;

return $clone;
}

/**
* Select all the columns from the tabular data that MUST NOT BE present in the ResultSet.
*/
public function selectAllExcept(string|int ...$columns): self
{
if ($columns === $this->select && self::COLUMN_EXCLUDE === $this->select_mode) {
return $this;
}

$clone = clone $this;
$clone->select = $columns;
$clone->select_mode = [] === $columns ? self::COLUMN_ALL : self::COLUMN_EXCLUDE;

return $clone;
}
Expand Down Expand Up @@ -339,7 +362,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::COLUMN_EXCLUDE => $iterator->selectAllExcept(...$this->select),
self::COLUMN_INCLUDE => $iterator->select(...$this->select),
default => $iterator,
};
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/StatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,23 @@ public function testItCanCompareNullValue(): void

self::assertCount(1, $statement->process($csv));
}

public function testselectAllExcept(): void
{
$document = <<<CSV
Title,Name,Number
Commander,Fred,104
Officer,John,117
Major,Avery
CSV;

$csv = Reader::createFromString($document);
$csv->setHeaderOffset(0);

$statement = Statement::create()
->limit(1)
->selectAllExcept('Number');

self::assertSame(['Title' => 'Commander', 'Name' => 'Fred'], $statement->process($csv)->first());
}
}
1 change: 1 addition & 0 deletions src/TabularDataReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 selectAllExcept(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<int, TabularDataReader> matching(string $expression) extract all found fragment identifiers for the tabular data
Expand Down
44 changes: 44 additions & 0 deletions src/TabularDataReaderTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,50 @@ public function testTabularReaderSelectFailsWithInvalidColumnOffset(): void
->select(0, 18);
}

/***************************
* TabularDataReader::selectAllExcept
****************************/


#[Test]
public function testTabularselectAllExceptWithoutHeader(): void
{
self::assertSame([1 => 'temperature', 2 => 'place'], $this->tabularData()->selectAllExcept(0)->first());
}

#[Test]
public function testTabularselectAllExceptWithHeader(): void
{
self::assertSame(['temperature' => '1', 'place' => 'Galway'], $this->tabularDataWithHeader()->selectAllExcept('date')->first());
self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectAllExcept('temperature', 'date')->first());
self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectAllExcept(1, 'date')->first());
self::assertSame(['place' => 'Galway'], $this->tabularDataWithHeader()->selectAllExcept('temperature', 0)->first());
}

public function testTabularReaderselectAllExceptFailsWithInvalidColumn(): void
{
$this->expectException(InvalidArgument::class);

$this->tabularData()
->selectAllExcept('temperature', 'place');
}

public function testTabularReaderselectAllExceptFailsWithInvalidColumnName(): void
{
$this->expectException(InvalidArgument::class);

$this->tabularDataWithHeader()
->selectAllExcept('temperature', 'foobar');
}

public function testTabularReaderselectAllExceptFailsWithInvalidColumnOffset(): void
{
$this->expectException(InvalidArgument::class);

$this->tabularDataWithHeader()
->selectAllExcept(0, 18);
}

/***************************
* TabularDataReader::matching, matchingFirst, matchingFirstOrFail
**************************/
Expand Down

0 comments on commit cd16489

Please sign in to comment.