Skip to content

Commit

Permalink
Xls/Xlsx/Xml Readers, Whole Rows and Columns
Browse files Browse the repository at this point in the history
  • Loading branch information
oleibman committed Dec 30, 2024
1 parent f943e2d commit 04b15c0
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 49 deletions.
29 changes: 29 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,8 @@ directly in some cell range, say A1:A3, and instead use, say,
`$validation->setFormula1('\'Sheet title\'!$A$1:$A$3')`. Another benefit is that
the item values themselves can contain the comma `,` character itself.

### Setting Validation on Multiple Cells - Release 3 and Below

If you need data validation on multiple cells, one can clone the
ruleset:

Expand All @@ -1572,6 +1574,33 @@ Alternatively, one can apply the validation to a range of cells:
$validation->setSqref('B5:B1048576');
```

### Setting Validation on Multiple Cells - Release 4 and Above

Starting with Release 4, Data Validation can be set simultaneously on several cells/cell ranges.

```php
$spreadsheet->getActiveSheet()->getDataValidation('A1:A4 D5 E6:E7')
->set...(...);
```

In theory, this means that more than one Data Validation can apply to a cell.
It appears that, when Excel reads a spreadsheet with more than one Data Validation applying to a cell,
whichever appears first in the Xml is what Xml uses.
PhpSpreadsheet will instead apply a DatValidation applying to a single cell first;
then, if it doesn't find such a match, it will use the first applicable definition which is read (or created after or in lieu of reading).
This allows you, for example, to set Data Validation on all but a few cells in a column:
```php
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A5:A7', $dv);
$dv = new DataValidation();
$dv->set...(...);
$sheet->setDataValidation('A:A', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A9', $dv);
```

## Setting a column's width

A column's width can be set using the following code:
Expand Down
2 changes: 2 additions & 0 deletions src/PhpSpreadsheet/Cell/Coordinate.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Cell;

use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Validations;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

/**
Expand Down Expand Up @@ -306,6 +307,7 @@ private static function validateReferenceAndGetData($reference): array
*/
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
{
$range = Validations::convertWholeRowColumn($range);
$rangeData = self::validateReferenceAndGetData($range);
if ($rangeData['type'] === 'invalid') {
throw new Exception('First argument needs to be a range');
Expand Down
33 changes: 15 additions & 18 deletions src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace PhpOffice\PhpSpreadsheet\Reader\Xls;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
Expand Down Expand Up @@ -178,23 +177,21 @@ protected function readDataValidation2(Xls $xls): void
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];

foreach ($cellRangeAddresses as $cellRange) {
$stRange = $xls->phpSheet->shrinkRangeToFit($cellRange);
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
$objValidation = $xls->phpSheet->getCell($coordinate)->getDataValidation();
$objValidation->setType($type);
$objValidation->setErrorStyle($errorStyle);
$objValidation->setAllowBlank((bool) $allowBlank);
$objValidation->setShowInputMessage((bool) $showInputMessage);
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
$objValidation->setShowDropDown(!$suppressDropDown);
$objValidation->setOperator($operator);
$objValidation->setErrorTitle($errorTitle);
$objValidation->setError($error);
$objValidation->setPromptTitle($promptTitle);
$objValidation->setPrompt($prompt);
$objValidation->setFormula1($formula1);
$objValidation->setFormula2($formula2);
}
$objValidation = new DataValidation();
$objValidation->setType($type);
$objValidation->setErrorStyle($errorStyle);
$objValidation->setAllowBlank((bool) $allowBlank);
$objValidation->setShowInputMessage((bool) $showInputMessage);
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
$objValidation->setShowDropDown(!$suppressDropDown);
$objValidation->setOperator($operator);
$objValidation->setErrorTitle($errorTitle);
$objValidation->setError($error);
$objValidation->setPromptTitle($promptTitle);
$objValidation->setPrompt($prompt);
$objValidation->setFormula1($formula1);
$objValidation->setFormula2($formula2);
$xls->phpSheet->setDataValidation($cellRange, $objValidation);
}
}
}
1 change: 0 additions & 1 deletion src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public function load(): void
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$docValidation->setSqref($range);
$this->worksheet->setDataValidation($range, $docValidation);
}
}
Expand Down
19 changes: 11 additions & 8 deletions src/PhpSpreadsheet/Reader/Xml/DataValidations.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
/** @var callable $pregCallback */
$pregCallback = [$this, 'replaceR1C1'];
foreach ($xmlX->DataValidation as $dataValidation) {
$cells = [];
$combinedCells = '';
$separator = '';
$validation = new DataValidation();

// set defaults
Expand Down Expand Up @@ -72,13 +73,17 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
$sheet->getCell($firstCell);
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// cell
$cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1];
$sheet->getCell($cell);
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// column
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
Expand All @@ -89,6 +94,8 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
. ((string) AddressRange::MAX_ROW);
$this->thisColumn = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) {
// row
$firstCell = 'A'
Expand All @@ -99,11 +106,9 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
. $selectionMatches[1];
$this->thisRow = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
$combinedCells .= "$separator$cell";
$separator = ' ';
}

$validation->setSqref($cell);
$stRange = $sheet->shrinkRangeToFit($cell);
$cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange));
}

break;
Expand Down Expand Up @@ -169,9 +174,7 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
}
}

foreach ($cells as $cell) {
$sheet->getCell($cell)->setDataValidation(clone $validation);
}
$sheet->setDataValidation($combinedCells, $validation);
}
}
}
1 change: 0 additions & 1 deletion src/PhpSpreadsheet/ReferenceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColu
$separator = ' ';
}
if ($cellAddress !== $newReference) {
$dataValidation->setSqref($newReference);
$worksheet->setDataValidation($newReference, $dataValidation);
$worksheet->setDataValidation($cellAddress, null);
}
Expand Down
19 changes: 14 additions & 5 deletions src/PhpSpreadsheet/Worksheet/Validations.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ public static function validateCellOrCellRange(AddressRange|CellAddress|int|stri
private const SETMAXROW = '${1}1:${2}' . AddressRange::MAX_ROW;
private const SETMAXCOL = 'A${1}:' . AddressRange::MAX_COLUMN . '${2}';

/**
* Convert Column ranges like 'A:C' to 'A1:C1048576'
* or Row ranges like '1:3' to 'A1:XFD3'.
*/
public static function convertWholeRowColumn(?string $addressRange): string
{
return (string) preg_replace(
['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'],
[self::SETMAXROW, self::SETMAXCOL],
$addressRange ?? ''
);
}

/**
* Validate a cell range.
*
Expand All @@ -70,11 +83,7 @@ public static function validateCellRange(AddressRange|string|array $cellRange):

// Convert Column ranges like 'A:C' to 'A1:C1048576'
// or Row ranges like '1:3' to 'A1:XFD3'
$addressRange = (string) preg_replace(
['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'],
[self::SETMAXROW, self::SETMAXCOL],
$addressRange ?? ''
);
$addressRange = self::convertWholeRowColumn($addressRange);

return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange);
}
Expand Down
1 change: 1 addition & 0 deletions src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -3300,6 +3300,7 @@ public function setDataValidation(string $cellCoordinate, ?DataValidation $dataV
if ($dataValidation === null) {
unset($this->dataValidationCollection[$cellCoordinate]);
} else {
$dataValidation->setSqref($cellCoordinate);
$this->dataValidationCollection[$cellCoordinate] = $dataValidation;
}

Expand Down
10 changes: 10 additions & 0 deletions src/PhpSpreadsheet/Writer/Xls/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -567,13 +567,23 @@ private function writeBIFF8CellRangeAddressFixed(string $range): string

// extract first cell, e.g. 'A1'
$firstCell = $explodes[0];
if (ctype_alpha($firstCell)) {
$firstCell .= '1';
} elseif (ctype_digit($firstCell)) {
$firstCell = "A$firstCell";
}

// extract last cell, e.g. 'B6'
if (count($explodes) == 1) {
$lastCell = $firstCell;
} else {
$lastCell = $explodes[1];
}
if (ctype_alpha($lastCell)) {
$lastCell .= (string) self::MAX_XLS_ROW;
} elseif (ctype_digit($lastCell)) {
$lastCell = self::MAX_XLS_COLUMN_STRING . $lastCell;
}

$firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1]
$lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6]
Expand Down
84 changes: 68 additions & 16 deletions tests/PhpSpreadsheetTests/ReferenceHelperDVTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,33 +135,85 @@ public function testMultipleRanges(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('B1')->setValue(1);
$sheet->getCell('B2')->setValue(2);
$sheet->getCell('B3')->setValue(3);
$dv = $sheet->getDataValidation('A1:A4 C5 D6:D7');
$sheet->getCell('C1')->setValue(1);
$sheet->getCell('C2')->setValue(2);
$sheet->getCell('C3')->setValue(3);
$dv = $sheet->getDataValidation('A1:A4 D5 E6:E7');
$dv->setType(DataValidation::TYPE_LIST)
->setShowDropDown(true)
->setFormula1('$B$1:$B$3')
->setFormula1('$C$1:$C$3')
->setErrorStyle(DataValidation::STYLE_STOP)
->setShowErrorMessage(true)
->setErrorTitle('Input Error')
->setError('Value is not a member of allowed list');
$sheet->insertNewColumnBefore('A');
$sheet->insertNewColumnBefore('B');
$dvs = $sheet->getDataValidationCollection();
self::assertCount(1, $dvs);
$expected = 'B1:B4 D5 E6:E7';
$expected = 'A1:A4 E5 F6:F7';
self::assertSame([$expected], array_keys($dvs));
$dv = $dvs[$expected];
self::assertSame($expected, $dv->getSqref());
self::assertSame('$C$1:$C$3', $dv->getFormula1());
$sheet->getCell('B2')->setValue(3);
self::assertTrue($sheet->getCell('B2')->hasValidValue());
$sheet->getCell('D5')->setValue(7);
self::assertFalse($sheet->getCell('D5')->hasValidValue());
$sheet->getCell('E6')->setValue(7);
self::assertFalse($sheet->getCell('E6')->hasValidValue());
$sheet->getCell('E7')->setValue(1);
self::assertTrue($sheet->getCell('E7')->hasValidValue());
self::assertSame('$D$1:$D$3', $dv->getFormula1());
$sheet->getCell('A3')->setValue(8);
self::assertFalse($sheet->getCell('A3')->hasValidValue());
$sheet->getCell('E5')->setValue(7);
self::assertFalse($sheet->getCell('E5')->hasValidValue());
$sheet->getCell('F6')->setValue(7);
self::assertFalse($sheet->getCell('F6')->hasValidValue());
$sheet->getCell('F7')->setValue(1);
self::assertTrue($sheet->getCell('F7')->hasValidValue());
$spreadsheet->disconnectWorksheets();
}

public function testWholeColumn(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A5:A7', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_LIST)
->setShowDropDown(true)
->setFormula1('"Item A,Item B,Item C"')
->setErrorStyle(DataValidation::STYLE_STOP)
->setShowErrorMessage(true)
->setErrorTitle('Input Error')
->setError('Value is not a member of allowed list');
$sheet->setDataValidation('A:A', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('A9', $dv);
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('A4')->getType());
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('A10')->getType());
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('A6')->getType());
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('A9')->getType());
$spreadsheet->disconnectWorksheets();
}

public function testWholeRow(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('C1:F1', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_LIST)
->setShowDropDown(true)
->setFormula1('"Item A,Item B,Item C"')
->setErrorStyle(DataValidation::STYLE_STOP)
->setShowErrorMessage(true)
->setErrorTitle('Input Error')
->setError('Value is not a member of allowed list');
$sheet->setDataValidation('1:1', $dv);
$dv = new DataValidation();
$dv->setType(DataValidation::TYPE_NONE);
$sheet->setDataValidation('H1', $dv);
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('B1')->getType());
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('J1')->getType());
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('D1')->getType());
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('H1')->getType());
$spreadsheet->disconnectWorksheets();
}
}
Loading

0 comments on commit 04b15c0

Please sign in to comment.