Skip to content

Commit

Permalink
Merge pull request #4240 from oleibman/issue797
Browse files Browse the repository at this point in the history
Breaking Change for DataValidation
  • Loading branch information
oleibman authored Feb 4, 2025
2 parents e721975 + d93a707 commit e1ae687
Show file tree
Hide file tree
Showing 22 changed files with 890 additions and 253 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org).

## TBD - 4.0.0

### BREAKING CHANGES

- Data Validations will be stored by worksheet, not cell. Index can be one or more cells or cell ranges. [Issue #797](https://github.com/PHPOffice/PhpSpreadsheet/issues/797) [Issue #4091](https://github.com/PHPOffice/PhpSpreadsheet/issues/4091) [Issue #4206](https://github.com/PHPOffice/PhpSpreadsheet/issues/4206) [PR #4240](https://github.com/PHPOffice/PhpSpreadsheet/pull/4240)
- Conditional Formatting adds Priority property and handles overlapping ranges better. [Issue #4312](https://github.com/PHPOffice/PhpSpreadsheet/issues/4312) [Issue #4318](https://github.com/PHPOffice/PhpSpreadsheet/issues/4318) [PR #4314](https://github.com/PHPOffice/PhpSpreadsheet/pull/4314)
- Deletion of items deprecated in Release 3. See "removed" below.

### Added

- Pdf Charts and Drawings. [Discussion #4129](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4129) [Discussion #4168](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4168) [PR #4327](https://github.com/PHPOffice/PhpSpreadsheet/pull/4327)

### Removed

- Nothing yet.

### Changed

- Nothing yet.
Expand Down
29 changes: 29 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,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 @@ -1575,6 +1577,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
22 changes: 0 additions & 22 deletions src/PhpSpreadsheet/Cell/DataValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,6 @@ class DataValidation
*/
private string $prompt = '';

/**
* Create a new DataValidation.
*/
public function __construct()
{
}

/**
* Get Formula 1.
*/
Expand Down Expand Up @@ -390,21 +383,6 @@ public function getHashCode(): string
);
}

/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
$this->$key = clone $value;
} else {
$this->$key = $value;
}
}
}

private ?string $sqref = null;

public function getSqref(): ?string
Expand Down
100 changes: 62 additions & 38 deletions src/PhpSpreadsheet/Cell/DataValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Cell;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Exception;

/**
Expand Down Expand Up @@ -37,46 +37,70 @@ public function isValid(Cell $cell): bool
if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue);
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue, $cell);
}
} elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) {
if (!is_numeric($cellValue)) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue);
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue, $cell);
}
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()));
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()), $cell);
}

return $returnValue;
}

private function numericOperator(DataValidation $dataValidation, int|float $cellValue): bool
private const TWO_FORMULAS = [DataValidation::OPERATOR_BETWEEN, DataValidation::OPERATOR_NOTBETWEEN];

private static function evaluateNumericFormula(mixed $formula, Cell $cell): mixed
{
if (!is_numeric($formula)) {
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

try {
$result = $calculation
->calculateFormula("=$formula", $cell->getCoordinate(), $cell);
while (is_array($result)) {
$result = array_pop($result);
}
$formula = $result;
} catch (Exception) {
// do nothing
}
}

return $formula;
}

private function numericOperator(DataValidation $dataValidation, int|float $cellValue, Cell $cell): bool
{
$operator = $dataValidation->getOperator();
$formula1 = $dataValidation->getFormula1();
$formula2 = $dataValidation->getFormula2();
$returnValue = false;
if ($operator === DataValidation::OPERATOR_BETWEEN) {
$returnValue = $cellValue >= $formula1 && $cellValue <= $formula2;
} elseif ($operator === DataValidation::OPERATOR_NOTBETWEEN) {
$returnValue = $cellValue < $formula1 || $cellValue > $formula2;
} elseif ($operator === DataValidation::OPERATOR_EQUAL) {
$returnValue = $cellValue == $formula1;
} elseif ($operator === DataValidation::OPERATOR_NOTEQUAL) {
$returnValue = $cellValue != $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHAN) {
$returnValue = $cellValue < $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHANOREQUAL) {
$returnValue = $cellValue <= $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHAN) {
$returnValue = $cellValue > $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHANOREQUAL) {
$returnValue = $cellValue >= $formula1;
$formula1 = self::evaluateNumericFormula(
$dataValidation->getFormula1(),
$cell
);

$formula2 = 0;
if (in_array($operator, self::TWO_FORMULAS, true)) {
$formula2 = self::evaluateNumericFormula(
$dataValidation->getFormula2(),
$cell
);
}

return $returnValue;
return match ($operator) {
DataValidation::OPERATOR_BETWEEN => $cellValue >= $formula1 && $cellValue <= $formula2,
DataValidation::OPERATOR_NOTBETWEEN => $cellValue < $formula1 || $cellValue > $formula2,
DataValidation::OPERATOR_EQUAL => $cellValue == $formula1,
DataValidation::OPERATOR_NOTEQUAL => $cellValue != $formula1,
DataValidation::OPERATOR_LESSTHAN => $cellValue < $formula1,
DataValidation::OPERATOR_LESSTHANOREQUAL => $cellValue <= $formula1,
DataValidation::OPERATOR_GREATERTHAN => $cellValue > $formula1,
DataValidation::OPERATOR_GREATERTHANOREQUAL => $cellValue >= $formula1,
default => false,
};
}

/**
Expand All @@ -94,22 +118,22 @@ private function isValueInList(Cell $cell): bool
// inline values list
if ($formula1[0] === '"') {
return in_array(strtolower($cellValueString), explode(',', strtolower(trim($formula1, '"'))), true);
} elseif (strpos($formula1, ':') > 0) {
// values list cells
$matchFormula = '=MATCH(' . $cell->getCoordinate() . ', ' . $formula1 . ', 0)';
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

try {
$result = $calculation->calculateFormula($matchFormula, $cell->getCoordinate(), $cell);
while (is_array($result)) {
$result = array_pop($result);
}
}
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());

return $result !== ExcelError::NA();
} catch (Exception) {
return false;
try {
$result = $calculation->calculateFormula("=$formula1", $cell->getCoordinate(), $cell);
$result = is_array($result) ? Functions::flattenArray($result) : [$result];
foreach ($result as $oneResult) {
if (is_scalar($oneResult) && strcasecmp((string) $oneResult, $cellValueString) === 0) {
return true;
}
}
} catch (Exception) {
// do nothing
}

return false;
}

return true;
Expand Down
54 changes: 36 additions & 18 deletions src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace PhpOffice\PhpSpreadsheet\Reader\Xls;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet as XlsWorksheet;

class DataValidationHelper extends Xls
{
Expand Down Expand Up @@ -176,25 +177,42 @@ protected function readDataValidation2(Xls $xls): void
// offset: var; size: var; cell range address list with
$cellRangeAddressList = Biff8::readBIFF8CellRangeAddressList(substr($recordData, $offset));
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
$maxRow = (string) AddressRange::MAX_ROW;
$maxCol = AddressRange::MAX_COLUMN;
$maxXlsRow = (string) XlsWorksheet::MAX_XLS_ROW;
$maxXlsColumnString = (string) XlsWorksheet::MAX_XLS_COLUMN_STRING;

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);
}
$cellRange = preg_replace(
[
"/([a-z]+)1:([a-z]+)$maxXlsRow/i",
"/([a-z]+\\d+):([a-z]+)$maxXlsRow/i",
"/A(\\d+):$maxXlsColumnString(\\d+)/i",
"/([a-z]+\\d+):$maxXlsColumnString(\\d+)/i",
],
[
'$1:$2',
'$1:${2}' . $maxRow,
'$1:$2',
'$1:' . $maxCol . '$2',
],
$cellRange
) ?? $cellRange;
$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);
}
}
}
42 changes: 17 additions & 25 deletions src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
Expand Down Expand Up @@ -36,31 +37,22 @@ public function load(): void
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);
$rangeSet = explode(' ', $range);
foreach ($rangeSet as $range) {
$stRange = $this->worksheet->shrinkRangeToFit($range);

// Extract all cell references in $range
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $reference) {
// Create validation
$docValidation = $this->worksheet->getCell($reference)->getDataValidation();
$docValidation->setType((string) $dataValidation['type']);
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
$docValidation->setOperator((string) $dataValidation['operator']);
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
// showDropDown is inverted (works as hideDropDown if true)
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
$docValidation->setError((string) $dataValidation['error']);
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$docValidation->setSqref($range);
}
}
$docValidation = new DataValidation();
$docValidation->setType((string) $dataValidation['type']);
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
$docValidation->setOperator((string) $dataValidation['operator']);
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
// showDropDown is inverted (works as hideDropDown if true)
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
$docValidation->setError((string) $dataValidation['error']);
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$this->worksheet->setDataValidation($range, $docValidation);
}
}
}
Loading

0 comments on commit e1ae687

Please sign in to comment.