Skip to content

Commit 04b15c0

Browse files
committed
Xls/Xlsx/Xml Readers, Whole Rows and Columns
1 parent f943e2d commit 04b15c0

File tree

12 files changed

+288
-49
lines changed

12 files changed

+288
-49
lines changed

docs/topics/recipes.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,8 @@ directly in some cell range, say A1:A3, and instead use, say,
15601560
`$validation->setFormula1('\'Sheet title\'!$A$1:$A$3')`. Another benefit is that
15611561
the item values themselves can contain the comma `,` character itself.
15621562

1563+
### Setting Validation on Multiple Cells - Release 3 and Below
1564+
15631565
If you need data validation on multiple cells, one can clone the
15641566
ruleset:
15651567

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

1577+
### Setting Validation on Multiple Cells - Release 4 and Above
1578+
1579+
Starting with Release 4, Data Validation can be set simultaneously on several cells/cell ranges.
1580+
1581+
```php
1582+
$spreadsheet->getActiveSheet()->getDataValidation('A1:A4 D5 E6:E7')
1583+
->set...(...);
1584+
```
1585+
1586+
In theory, this means that more than one Data Validation can apply to a cell.
1587+
It appears that, when Excel reads a spreadsheet with more than one Data Validation applying to a cell,
1588+
whichever appears first in the Xml is what Xml uses.
1589+
PhpSpreadsheet will instead apply a DatValidation applying to a single cell first;
1590+
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).
1591+
This allows you, for example, to set Data Validation on all but a few cells in a column:
1592+
```php
1593+
$dv = new DataValidation();
1594+
$dv->setType(DataValidation::TYPE_NONE);
1595+
$sheet->setDataValidation('A5:A7', $dv);
1596+
$dv = new DataValidation();
1597+
$dv->set...(...);
1598+
$sheet->setDataValidation('A:A', $dv);
1599+
$dv = new DataValidation();
1600+
$dv->setType(DataValidation::TYPE_NONE);
1601+
$sheet->setDataValidation('A9', $dv);
1602+
```
1603+
15751604
## Setting a column's width
15761605

15771606
A column's width can be set using the following code:

src/PhpSpreadsheet/Cell/Coordinate.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Cell;
44

55
use PhpOffice\PhpSpreadsheet\Exception;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Validations;
67
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
78

89
/**
@@ -306,6 +307,7 @@ private static function validateReferenceAndGetData($reference): array
306307
*/
307308
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
308309
{
310+
$range = Validations::convertWholeRowColumn($range);
309311
$rangeData = self::validateReferenceAndGetData($range);
310312
if ($rangeData['type'] === 'invalid') {
311313
throw new Exception('First argument needs to be a range');

src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
44

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

180179
foreach ($cellRangeAddresses as $cellRange) {
181-
$stRange = $xls->phpSheet->shrinkRangeToFit($cellRange);
182-
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
183-
$objValidation = $xls->phpSheet->getCell($coordinate)->getDataValidation();
184-
$objValidation->setType($type);
185-
$objValidation->setErrorStyle($errorStyle);
186-
$objValidation->setAllowBlank((bool) $allowBlank);
187-
$objValidation->setShowInputMessage((bool) $showInputMessage);
188-
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
189-
$objValidation->setShowDropDown(!$suppressDropDown);
190-
$objValidation->setOperator($operator);
191-
$objValidation->setErrorTitle($errorTitle);
192-
$objValidation->setError($error);
193-
$objValidation->setPromptTitle($promptTitle);
194-
$objValidation->setPrompt($prompt);
195-
$objValidation->setFormula1($formula1);
196-
$objValidation->setFormula2($formula2);
197-
}
180+
$objValidation = new DataValidation();
181+
$objValidation->setType($type);
182+
$objValidation->setErrorStyle($errorStyle);
183+
$objValidation->setAllowBlank((bool) $allowBlank);
184+
$objValidation->setShowInputMessage((bool) $showInputMessage);
185+
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
186+
$objValidation->setShowDropDown(!$suppressDropDown);
187+
$objValidation->setOperator($operator);
188+
$objValidation->setErrorTitle($errorTitle);
189+
$objValidation->setError($error);
190+
$objValidation->setPromptTitle($promptTitle);
191+
$objValidation->setPrompt($prompt);
192+
$objValidation->setFormula1($formula1);
193+
$objValidation->setFormula2($formula2);
194+
$xls->phpSheet->setDataValidation($cellRange, $objValidation);
198195
}
199196
}
200197
}

src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ public function load(): void
5252
$docValidation->setPrompt((string) $dataValidation['prompt']);
5353
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
5454
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
55-
$docValidation->setSqref($range);
5655
$this->worksheet->setDataValidation($range, $docValidation);
5756
}
5857
}

src/PhpSpreadsheet/Reader/Xml/DataValidations.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
4343
/** @var callable $pregCallback */
4444
$pregCallback = [$this, 'replaceR1C1'];
4545
foreach ($xmlX->DataValidation as $dataValidation) {
46-
$cells = [];
46+
$combinedCells = '';
47+
$separator = '';
4748
$validation = new DataValidation();
4849

4950
// set defaults
@@ -72,13 +73,17 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
7273
$this->thisRow = (int) $selectionMatches[1];
7374
$this->thisColumn = (int) $selectionMatches[2];
7475
$sheet->getCell($firstCell);
76+
$combinedCells .= "$separator$cell";
77+
$separator = ' ';
7578
} elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
7679
// cell
7780
$cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
7881
. $selectionMatches[1];
7982
$sheet->getCell($cell);
8083
$this->thisRow = (int) $selectionMatches[1];
8184
$this->thisColumn = (int) $selectionMatches[2];
85+
$combinedCells .= "$separator$cell";
86+
$separator = ' ';
8287
} elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) {
8388
// column
8489
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
@@ -89,6 +94,8 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
8994
. ((string) AddressRange::MAX_ROW);
9095
$this->thisColumn = (int) $selectionMatches[1];
9196
$sheet->getCell($firstCell);
97+
$combinedCells .= "$separator$cell";
98+
$separator = ' ';
9299
} elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) {
93100
// row
94101
$firstCell = 'A'
@@ -99,11 +106,9 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
99106
. $selectionMatches[1];
100107
$this->thisRow = (int) $selectionMatches[1];
101108
$sheet->getCell($firstCell);
109+
$combinedCells .= "$separator$cell";
110+
$separator = ' ';
102111
}
103-
104-
$validation->setSqref($cell);
105-
$stRange = $sheet->shrinkRangeToFit($cell);
106-
$cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange));
107112
}
108113

109114
break;
@@ -169,9 +174,7 @@ public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $sp
169174
}
170175
}
171176

172-
foreach ($cells as $cell) {
173-
$sheet->getCell($cell)->setDataValidation(clone $validation);
174-
}
177+
$sheet->setDataValidation($combinedCells, $validation);
175178
}
176179
}
177180
}

src/PhpSpreadsheet/ReferenceHelper.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColu
290290
$separator = ' ';
291291
}
292292
if ($cellAddress !== $newReference) {
293-
$dataValidation->setSqref($newReference);
294293
$worksheet->setDataValidation($newReference, $dataValidation);
295294
$worksheet->setDataValidation($cellAddress, null);
296295
}

src/PhpSpreadsheet/Worksheet/Validations.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ public static function validateCellOrCellRange(AddressRange|CellAddress|int|stri
5656
private const SETMAXROW = '${1}1:${2}' . AddressRange::MAX_ROW;
5757
private const SETMAXCOL = 'A${1}:' . AddressRange::MAX_COLUMN . '${2}';
5858

59+
/**
60+
* Convert Column ranges like 'A:C' to 'A1:C1048576'
61+
* or Row ranges like '1:3' to 'A1:XFD3'.
62+
*/
63+
public static function convertWholeRowColumn(?string $addressRange): string
64+
{
65+
return (string) preg_replace(
66+
['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'],
67+
[self::SETMAXROW, self::SETMAXCOL],
68+
$addressRange ?? ''
69+
);
70+
}
71+
5972
/**
6073
* Validate a cell range.
6174
*
@@ -70,11 +83,7 @@ public static function validateCellRange(AddressRange|string|array $cellRange):
7083

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

7988
return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange);
8089
}

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3300,6 +3300,7 @@ public function setDataValidation(string $cellCoordinate, ?DataValidation $dataV
33003300
if ($dataValidation === null) {
33013301
unset($this->dataValidationCollection[$cellCoordinate]);
33023302
} else {
3303+
$dataValidation->setSqref($cellCoordinate);
33033304
$this->dataValidationCollection[$cellCoordinate] = $dataValidation;
33043305
}
33053306

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,13 +567,23 @@ private function writeBIFF8CellRangeAddressFixed(string $range): string
567567

568568
// extract first cell, e.g. 'A1'
569569
$firstCell = $explodes[0];
570+
if (ctype_alpha($firstCell)) {
571+
$firstCell .= '1';
572+
} elseif (ctype_digit($firstCell)) {
573+
$firstCell = "A$firstCell";
574+
}
570575

571576
// extract last cell, e.g. 'B6'
572577
if (count($explodes) == 1) {
573578
$lastCell = $firstCell;
574579
} else {
575580
$lastCell = $explodes[1];
576581
}
582+
if (ctype_alpha($lastCell)) {
583+
$lastCell .= (string) self::MAX_XLS_ROW;
584+
} elseif (ctype_digit($lastCell)) {
585+
$lastCell = self::MAX_XLS_COLUMN_STRING . $lastCell;
586+
}
577587

578588
$firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1]
579589
$lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6]

tests/PhpSpreadsheetTests/ReferenceHelperDVTest.php

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,33 +135,85 @@ public function testMultipleRanges(): void
135135
{
136136
$spreadsheet = new Spreadsheet();
137137
$sheet = $spreadsheet->getActiveSheet();
138-
$sheet->getCell('B1')->setValue(1);
139-
$sheet->getCell('B2')->setValue(2);
140-
$sheet->getCell('B3')->setValue(3);
141-
$dv = $sheet->getDataValidation('A1:A4 C5 D6:D7');
138+
$sheet->getCell('C1')->setValue(1);
139+
$sheet->getCell('C2')->setValue(2);
140+
$sheet->getCell('C3')->setValue(3);
141+
$dv = $sheet->getDataValidation('A1:A4 D5 E6:E7');
142142
$dv->setType(DataValidation::TYPE_LIST)
143143
->setShowDropDown(true)
144-
->setFormula1('$B$1:$B$3')
144+
->setFormula1('$C$1:$C$3')
145145
->setErrorStyle(DataValidation::STYLE_STOP)
146146
->setShowErrorMessage(true)
147147
->setErrorTitle('Input Error')
148148
->setError('Value is not a member of allowed list');
149-
$sheet->insertNewColumnBefore('A');
149+
$sheet->insertNewColumnBefore('B');
150150
$dvs = $sheet->getDataValidationCollection();
151151
self::assertCount(1, $dvs);
152-
$expected = 'B1:B4 D5 E6:E7';
152+
$expected = 'A1:A4 E5 F6:F7';
153153
self::assertSame([$expected], array_keys($dvs));
154154
$dv = $dvs[$expected];
155155
self::assertSame($expected, $dv->getSqref());
156-
self::assertSame('$C$1:$C$3', $dv->getFormula1());
157-
$sheet->getCell('B2')->setValue(3);
158-
self::assertTrue($sheet->getCell('B2')->hasValidValue());
159-
$sheet->getCell('D5')->setValue(7);
160-
self::assertFalse($sheet->getCell('D5')->hasValidValue());
161-
$sheet->getCell('E6')->setValue(7);
162-
self::assertFalse($sheet->getCell('E6')->hasValidValue());
163-
$sheet->getCell('E7')->setValue(1);
164-
self::assertTrue($sheet->getCell('E7')->hasValidValue());
156+
self::assertSame('$D$1:$D$3', $dv->getFormula1());
157+
$sheet->getCell('A3')->setValue(8);
158+
self::assertFalse($sheet->getCell('A3')->hasValidValue());
159+
$sheet->getCell('E5')->setValue(7);
160+
self::assertFalse($sheet->getCell('E5')->hasValidValue());
161+
$sheet->getCell('F6')->setValue(7);
162+
self::assertFalse($sheet->getCell('F6')->hasValidValue());
163+
$sheet->getCell('F7')->setValue(1);
164+
self::assertTrue($sheet->getCell('F7')->hasValidValue());
165+
$spreadsheet->disconnectWorksheets();
166+
}
167+
168+
public function testWholeColumn(): void
169+
{
170+
$spreadsheet = new Spreadsheet();
171+
$sheet = $spreadsheet->getActiveSheet();
172+
$dv = new DataValidation();
173+
$dv->setType(DataValidation::TYPE_NONE);
174+
$sheet->setDataValidation('A5:A7', $dv);
175+
$dv = new DataValidation();
176+
$dv->setType(DataValidation::TYPE_LIST)
177+
->setShowDropDown(true)
178+
->setFormula1('"Item A,Item B,Item C"')
179+
->setErrorStyle(DataValidation::STYLE_STOP)
180+
->setShowErrorMessage(true)
181+
->setErrorTitle('Input Error')
182+
->setError('Value is not a member of allowed list');
183+
$sheet->setDataValidation('A:A', $dv);
184+
$dv = new DataValidation();
185+
$dv->setType(DataValidation::TYPE_NONE);
186+
$sheet->setDataValidation('A9', $dv);
187+
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('A4')->getType());
188+
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('A10')->getType());
189+
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('A6')->getType());
190+
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('A9')->getType());
191+
$spreadsheet->disconnectWorksheets();
192+
}
193+
194+
public function testWholeRow(): void
195+
{
196+
$spreadsheet = new Spreadsheet();
197+
$sheet = $spreadsheet->getActiveSheet();
198+
$dv = new DataValidation();
199+
$dv->setType(DataValidation::TYPE_NONE);
200+
$sheet->setDataValidation('C1:F1', $dv);
201+
$dv = new DataValidation();
202+
$dv->setType(DataValidation::TYPE_LIST)
203+
->setShowDropDown(true)
204+
->setFormula1('"Item A,Item B,Item C"')
205+
->setErrorStyle(DataValidation::STYLE_STOP)
206+
->setShowErrorMessage(true)
207+
->setErrorTitle('Input Error')
208+
->setError('Value is not a member of allowed list');
209+
$sheet->setDataValidation('1:1', $dv);
210+
$dv = new DataValidation();
211+
$dv->setType(DataValidation::TYPE_NONE);
212+
$sheet->setDataValidation('H1', $dv);
213+
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('B1')->getType());
214+
self::assertSame(DataValidation::TYPE_LIST, $sheet->getDataValidation('J1')->getType());
215+
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('D1')->getType());
216+
self::assertSame(DataValidation::TYPE_NONE, $sheet->getDataValidation('H1')->getType());
165217
$spreadsheet->disconnectWorksheets();
166218
}
167219
}

0 commit comments

Comments
 (0)