diff --git a/src/FragmentFinder.php b/src/FragmentFinder.php index 56f0a275..d9a5a74b 100644 --- a/src/FragmentFinder.php +++ b/src/FragmentFinder.php @@ -292,18 +292,17 @@ private function parseColumnSelection(string $selection, TabularData $tabularDat */ private function parseRowColumnSelection(string $selection): array { - if (1 !== preg_match(self::REGEXP_ROWS_COLUMNS_SELECTION, $selection, $found)) { + if (1 !== preg_match(self::REGEXP_ROWS_COLUMNS_SELECTION, $selection, $found, PREG_UNMATCHED_AS_NULL)) { return [-1, 0]; } - $start = $found['start']; - $end = $found['end'] ?? null; - $start = filter_var($start, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); + $start = filter_var($found['start'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); if (false === $start) { return [-1, 0]; } --$start; + $end = $found['end']; if (null === $end || '*' === $end) { return [$start, $end]; } diff --git a/src/TabularDataReaderTestCase.php b/src/TabularDataReaderTestCase.php index 9cf288e0..cd2d11bb 100644 --- a/src/TabularDataReaderTestCase.php +++ b/src/TabularDataReaderTestCase.php @@ -273,6 +273,37 @@ public static function provideValidExpressions(): iterable 'expression' => 'cell=48,12', 'expected' => null, ]; + + yield 'multiple single rows without range endpoint' => [ + 'expression' => 'row=1;3', + 'expected' => [ + 0 => ['date', 'temperature', 'place'], + 1 => ['2011-01-02', '-1', 'Galway'], + ], + ]; + + yield 'mixed single row and row range' => [ + 'expression' => 'row=1;3-5', + 'expected' => [ + 0 => ['date', 'temperature', 'place'], + 1 => ['2011-01-02', '-1', 'Galway'], + 2 => ['2011-01-03', '0', 'Galway'], + 3 => ['2011-01-01', '6', 'Berkeley'], + ], + ]; + + yield 'multiple single columns without range endpoint' => [ + 'expression' => 'col=1;3', + 'expected' => [ + 0 => [0 => 'date', 2 => 'place'], + 1 => [0 => '2011-01-01', 2 => 'Galway'], + 2 => [0 => '2011-01-02', 2 => 'Galway'], + 3 => [0 => '2011-01-03', 2 => 'Galway'], + 4 => [0 => '2011-01-01', 2 => 'Berkeley'], + 5 => [0 => '2011-01-02', 2 => 'Berkeley'], + 6 => [0 => '2011-01-03', 2 => 'Berkeley'], + ], + ]; } #[Test] @@ -323,6 +354,7 @@ public static function provideExpressionWithIgnoredSelections(): iterable 'expression selection is invalid for cell 7' => ['cell=1,0-2,3'], 'expression selection is invalid for row or column 3' => ['row=0-3'], 'expression selection is invalid for row or column 4' => ['row=3-0'], + 'all single row selections are out of bounds' => ['row=0;0'], ]; } @@ -352,6 +384,23 @@ public function it_fails_if_no_row_is_found(): void $this->tabularDataWithoutHeader()->matchingFirstOrFail('row=42'); } + #[Test] + public function it_throws_when_expression_contains_invalid_single_selection_alongside_valid(): void + { + $this->expectException(FragmentNotFound::class); + + $this->tabularDataWithoutHeader()->matchingFirstOrFail('row=0;3'); + } + + #[Test] + public function it_returns_valid_rows_when_mixed_selection_contains_invalid_single_row(): void + { + $result = $this->tabularDataWithoutHeader()->matchingFirst('row=0;3'); + + self::assertNotNull($result); + self::assertSame([0 => ['2011-01-02', '-1', 'Galway']], [...$result]); + } + /*************************** * TabularDataReader::map ****************************/