This commit is contained in:
steven 2025-08-11 22:23:30 +02:00
commit 72a26edcff
22092 changed files with 2101903 additions and 0 deletions

View file

@ -0,0 +1,177 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Exception;
class AddressHelper
{
public const R1C1_COORDINATE_REGEX = '/(R((?:\[-?\d*\])|(?:\d*))?)(C((?:\[-?\d*\])|(?:\d*))?)/i';
/** @return string[] */
public static function getRowAndColumnChars(): array
{
$rowChar = 'R';
$colChar = 'C';
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL) {
$rowColChars = Calculation::localeFunc('*RC');
if (mb_strlen($rowColChars) === 2) {
$rowChar = mb_substr($rowColChars, 0, 1);
$colChar = mb_substr($rowColChars, 1, 1);
}
}
return [$rowChar, $colChar];
}
/**
* Converts an R1C1 format cell address to an A1 format cell address.
*/
public static function convertToA1(
string $address,
int $currentRowNumber = 1,
int $currentColumnNumber = 1,
bool $useLocale = true
): string {
[$rowChar, $colChar] = $useLocale ? self::getRowAndColumnChars() : ['R', 'C'];
$regex = '/^(' . $rowChar . '(\[?[-+]?\d*\]?))(' . $colChar . '(\[?[-+]?\d*\]?))$/i';
$validityCheck = preg_match($regex, $address, $cellReference);
if (empty($validityCheck)) {
throw new Exception('Invalid R1C1-format Cell Reference');
}
$rowReference = $cellReference[2];
// Empty R reference is the current row
if ($rowReference === '') {
$rowReference = (string) $currentRowNumber;
}
// Bracketed R references are relative to the current row
if ($rowReference[0] === '[') {
$rowReference = $currentRowNumber + (int) trim($rowReference, '[]');
}
$columnReference = $cellReference[4];
// Empty C reference is the current column
if ($columnReference === '') {
$columnReference = (string) $currentColumnNumber;
}
// Bracketed C references are relative to the current column
if (is_string($columnReference) && $columnReference[0] === '[') {
$columnReference = $currentColumnNumber + (int) trim($columnReference, '[]');
}
$columnReference = (int) $columnReference;
if ($columnReference <= 0 || $rowReference <= 0) {
throw new Exception('Invalid R1C1-format Cell Reference, Value out of range');
}
$A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference;
return $A1CellReference;
}
protected static function convertSpreadsheetMLFormula(string $formula): string
{
$formula = substr($formula, 3);
$temp = explode('"', $formula);
$key = false;
foreach ($temp as &$value) {
// Only replace in alternate array entries (i.e. non-quoted blocks)
$key = $key === false;
if ($key) {
$value = str_replace(['[.', ':.', ']'], ['', ':', ''], $value);
}
}
unset($value);
return implode('"', $temp);
}
/**
* Converts a formula that uses R1C1/SpreadsheetXML format cell address to an A1 format cell address.
*/
public static function convertFormulaToA1(
string $formula,
int $currentRowNumber = 1,
int $currentColumnNumber = 1
): string {
if (str_starts_with($formula, 'of:')) {
// We have an old-style SpreadsheetML Formula
return self::convertSpreadsheetMLFormula($formula);
}
// Convert R1C1 style references to A1 style references (but only when not quoted)
$temp = explode('"', $formula);
$key = false;
foreach ($temp as &$value) {
// Only replace in alternate array entries (i.e. non-quoted blocks)
$key = $key === false;
if ($key) {
preg_match_all(self::R1C1_COORDINATE_REGEX, $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
// Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
// through the formula from left to right. Reversing means that we work right to left.through
// the formula
$cellReferences = array_reverse($cellReferences);
// Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
// then modify the formula to use that new reference
foreach ($cellReferences as $cellReference) {
$A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber, false);
$value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
}
}
}
unset($value);
// Then rebuild the formula string
return implode('"', $temp);
}
/**
* Converts an A1 format cell address to an R1C1 format cell address.
* If $currentRowNumber or $currentColumnNumber are provided, then the R1C1 address will be formatted as a relative address.
*/
public static function convertToR1C1(
string $address,
?int $currentRowNumber = null,
?int $currentColumnNumber = null
): string {
$validityCheck = preg_match(Coordinate::A1_COORDINATE_REGEX, $address, $cellReference);
if ($validityCheck === 0) {
throw new Exception('Invalid A1-format Cell Reference');
}
if ($cellReference['col'][0] === '$') {
// Column must be absolute address
$currentColumnNumber = null;
}
$columnId = Coordinate::columnIndexFromString(ltrim($cellReference['col'], '$'));
if ($cellReference['row'][0] === '$') {
// Row must be absolute address
$currentRowNumber = null;
}
$rowId = (int) ltrim($cellReference['row'], '$');
if ($currentRowNumber !== null) {
if ($rowId === $currentRowNumber) {
$rowId = '';
} else {
$rowId = '[' . ($rowId - $currentRowNumber) . ']';
}
}
if ($currentColumnNumber !== null) {
if ($columnId === $currentColumnNumber) {
$columnId = '';
} else {
$columnId = '[' . ($columnId - $currentColumnNumber) . ']';
}
}
$R1C1Address = "R{$rowId}C{$columnId}";
return $R1C1Address;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
interface AddressRange
{
public const MAX_ROW = 1048576;
public const MAX_COLUMN = 'XFD';
public const MAX_COLUMN_INT = 16384;
public function from(): mixed;
public function to(): mixed;
public function __toString(): string;
}

View file

@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\FormattedNumber;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
{
/**
* Bind value to a cell.
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*/
public function bindValue(Cell $cell, $value = null): bool
{
if ($value === null) {
return parent::bindValue($cell, $value);
} elseif (is_string($value)) {
// sanitize UTF-8 strings
$value = StringHelper::sanitizeUTF8($value);
}
// Find out data type
$dataType = parent::dataTypeForValue($value);
// Style logic - strings
if ($dataType === DataType::TYPE_STRING && !$value instanceof RichText) {
// Test for booleans using locale-setting
if (StringHelper::strToUpper($value) === Calculation::getTRUE()) {
$cell->setValueExplicit(true, DataType::TYPE_BOOL);
return true;
} elseif (StringHelper::strToUpper($value) === Calculation::getFALSE()) {
$cell->setValueExplicit(false, DataType::TYPE_BOOL);
return true;
}
// Check for fractions
if (preg_match('~^([+-]?)\s*(\d+)\s*/\s*(\d+)$~', $value, $matches)) {
return $this->setProperFraction($matches, $cell);
} elseif (preg_match('~^([+-]?)(\d+)\s+(\d+)\s*/\s*(\d+)$~', $value, $matches)) {
return $this->setImproperFraction($matches, $cell);
}
$decimalSeparatorNoPreg = StringHelper::getDecimalSeparator();
$decimalSeparator = preg_quote($decimalSeparatorNoPreg, '/');
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
// Check for percentage
if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) {
return $this->setPercentage(preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $cell);
}
// Check for currency
if (preg_match(FormattedNumber::currencyMatcherRegexp(), preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) {
// Convert value to number
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null;
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'];
$value = (float) ($sign . trim(str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)))); // @phpstan-ignore-line
return $this->setCurrency($value, $cell, $currencyCode ?? '');
}
// Check for time without seconds e.g. '9:45', '09:45'
if (preg_match('/^(\d|[0-1]\d|2[0-3]):[0-5]\d$/', $value)) {
return $this->setTimeHoursMinutes($value, $cell);
}
// Check for time with seconds '9:45:59', '09:45:59'
if (preg_match('/^(\d|[0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$/', $value)) {
return $this->setTimeHoursMinutesSeconds($value, $cell);
}
// Check for datetime, e.g. '2008-12-31', '2008-12-31 15:59', '2008-12-31 15:59:10'
if (($d = Date::stringToExcel($value)) !== false) {
// Convert value to number
$cell->setValueExplicit($d, DataType::TYPE_NUMERIC);
// Determine style. Either there is a time part or not. Look for ':'
if (str_contains($value, ':')) {
$formatCode = 'yyyy-mm-dd h:mm';
} else {
$formatCode = 'yyyy-mm-dd';
}
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode($formatCode);
return true;
}
// Check for newline character "\n"
if (str_contains($value, "\n")) {
$cell->setValueExplicit($value, DataType::TYPE_STRING);
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getAlignment()->setWrapText(true);
return true;
}
}
// Not bound yet? Use parent...
return parent::bindValue($cell, $value);
}
protected function setImproperFraction(array $matches, Cell $cell): bool
{
// Convert value to number
$value = $matches[2] + ($matches[3] / $matches[4]);
if ($matches[1] === '-') {
$value = 0 - $value;
}
$cell->setValueExplicit((float) $value, DataType::TYPE_NUMERIC);
// Build the number format mask based on the size of the matched values
$dividend = str_repeat('?', strlen($matches[3]));
$divisor = str_repeat('?', strlen($matches[4]));
$fractionMask = "# {$dividend}/{$divisor}";
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode($fractionMask);
return true;
}
protected function setProperFraction(array $matches, Cell $cell): bool
{
// Convert value to number
$value = $matches[2] / $matches[3];
if ($matches[1] === '-') {
$value = 0 - $value;
}
$cell->setValueExplicit((float) $value, DataType::TYPE_NUMERIC);
// Build the number format mask based on the size of the matched values
$dividend = str_repeat('?', strlen($matches[2]));
$divisor = str_repeat('?', strlen($matches[3]));
$fractionMask = "{$dividend}/{$divisor}";
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode($fractionMask);
return true;
}
protected function setPercentage(string $value, Cell $cell): bool
{
// Convert value to number
$value = ((float) str_replace('%', '', $value)) / 100;
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_PERCENTAGE_00);
return true;
}
protected function setCurrency(float $value, Cell $cell, string $currencyCode): bool
{
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode(
str_replace('$', '[$' . $currencyCode . ']', NumberFormat::FORMAT_CURRENCY_USD)
);
return true;
}
protected function setTimeHoursMinutes(string $value, Cell $cell): bool
{
// Convert value to number
[$hours, $minutes] = explode(':', $value);
$hours = (int) $hours;
$minutes = (int) $minutes;
$days = ($hours / 24) + ($minutes / 1440);
$cell->setValueExplicit($days, DataType::TYPE_NUMERIC);
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_TIME3);
return true;
}
protected function setTimeHoursMinutesSeconds(string $value, Cell $cell): bool
{
// Convert value to number
[$hours, $minutes, $seconds] = explode(':', $value);
$hours = (int) $hours;
$minutes = (int) $minutes;
$seconds = (int) $seconds;
$days = ($hours / 24) + ($minutes / 1440) + ($seconds / 86400);
$cell->setValueExplicit($days, DataType::TYPE_NUMERIC);
// Set style
$cell->getWorksheet()->getStyle($cell->getCoordinate())
->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_TIME4);
return true;
}
}

View file

@ -0,0 +1,808 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Collection\Cells;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
class Cell implements Stringable
{
/**
* Value binder to use.
*/
private static ?IValueBinder $valueBinder = null;
/**
* Value of the cell.
*/
private mixed $value;
/**
* Calculated value of the cell (used for caching)
* This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
* create the original spreadsheet file.
* Note that this value is not guaranteed to reflect the actual calculated value because it is
* possible that auto-calculation was disabled in the original spreadsheet, and underlying data
* values used by the formula have changed since it was last calculated.
*
* @var mixed
*/
private $calculatedValue;
/**
* Type of the cell data.
*/
private string $dataType;
/**
* The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet).
*
* @var ?Cells
*/
private ?Cells $parent;
/**
* Index to the cellXf reference for the styling of this cell.
*/
private int $xfIndex = 0;
/**
* Attributes of the formula.
*/
private mixed $formulaAttributes = null;
private IgnoredErrors $ignoredErrors;
/**
* Update the cell into the cell collection.
*
* @throws SpreadsheetException
*/
public function updateInCollection(): self
{
$parent = $this->parent;
if ($parent === null) {
throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet');
}
$parent->update($this);
return $this;
}
public function detach(): void
{
$this->parent = null;
}
public function attach(Cells $parent): void
{
$this->parent = $parent;
}
/**
* Create a new Cell.
*
* @throws SpreadsheetException
*/
public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet)
{
// Initialise cell value
$this->value = $value;
// Set worksheet cache
$this->parent = $worksheet->getCellCollection();
// Set datatype?
if ($dataType !== null) {
if ($dataType == DataType::TYPE_STRING2) {
$dataType = DataType::TYPE_STRING;
}
$this->dataType = $dataType;
} elseif (self::getValueBinder()->bindValue($this, $value) === false) {
throw new SpreadsheetException('Value could not be bound to cell.');
}
$this->ignoredErrors = new IgnoredErrors();
}
/**
* Get cell coordinate column.
*
* @throws SpreadsheetException
*/
public function getColumn(): string
{
$parent = $this->parent;
if ($parent === null) {
throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet');
}
return $parent->getCurrentColumn();
}
/**
* Get cell coordinate row.
*
* @throws SpreadsheetException
*/
public function getRow(): int
{
$parent = $this->parent;
if ($parent === null) {
throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet');
}
return $parent->getCurrentRow();
}
/**
* Get cell coordinate.
*
* @throws SpreadsheetException
*/
public function getCoordinate(): string
{
$parent = $this->parent;
if ($parent !== null) {
$coordinate = $parent->getCurrentCoordinate();
} else {
$coordinate = null;
}
if ($coordinate === null) {
throw new SpreadsheetException('Coordinate no longer exists');
}
return $coordinate;
}
/**
* Get cell value.
*/
public function getValue(): mixed
{
return $this->value;
}
/**
* Get cell value with formatting.
*/
public function getFormattedValue(): string
{
return (string) NumberFormat::toFormattedString(
$this->getCalculatedValue(),
(string) $this->getStyle()->getNumberFormat()->getFormatCode()
);
}
protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
{
if (StringHelper::strToLower($oldValue ?? '') === StringHelper::strToLower($newValue ?? '') || $workSheet === null) {
return;
}
foreach ($workSheet->getTableCollection() as $table) {
/** @var Table $table */
if ($cell->isInRange($table->getRange())) {
$rangeRowsColumns = Coordinate::getRangeBoundaries($table->getRange());
if ($cell->getRow() === (int) $rangeRowsColumns[0][1]) {
Table\Column::updateStructuredReferences($workSheet, $oldValue, $newValue);
}
return;
}
}
}
/**
* Set cell value.
*
* Sets the value for a cell, automatically determining the datatype using the value binder
*
* @param mixed $value Value
* @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
*
* @throws SpreadsheetException
*/
public function setValue(mixed $value, ?IValueBinder $binder = null): self
{
$binder ??= self::getValueBinder();
if (!$binder->bindValue($this, $value)) {
throw new SpreadsheetException('Value could not be bound to cell.');
}
return $this;
}
/**
* Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
*
* @param mixed $value Value
* @param string $dataType Explicit data type, see DataType::TYPE_*
* Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
* method, then it is your responsibility as an end-user developer to validate that the value and
* the datatype match.
* If you do mismatch value and datatype, then the value you enter may be changed to match the datatype
* that you specify.
*
* @throws SpreadsheetException
*/
public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
{
$oldValue = $this->value;
// set the value according to data type
switch ($dataType) {
case DataType::TYPE_NULL:
$this->value = null;
break;
case DataType::TYPE_STRING2:
$dataType = DataType::TYPE_STRING;
// no break
case DataType::TYPE_STRING:
// Synonym for string
case DataType::TYPE_INLINE:
// Rich text
$this->value = DataType::checkString($value);
break;
case DataType::TYPE_NUMERIC:
if (is_string($value) && !is_numeric($value)) {
throw new SpreadsheetException('Invalid numeric value for datatype Numeric');
}
$this->value = 0 + $value;
break;
case DataType::TYPE_FORMULA:
$this->value = (string) $value;
break;
case DataType::TYPE_BOOL:
$this->value = (bool) $value;
break;
case DataType::TYPE_ISO_DATE:
$this->value = SharedDate::convertIsoDate($value);
$dataType = DataType::TYPE_NUMERIC;
break;
case DataType::TYPE_ERROR:
$this->value = DataType::checkErrorCode($value);
break;
default:
throw new SpreadsheetException('Invalid datatype: ' . $dataType);
}
// set the datatype
$this->dataType = $dataType;
$this->updateInCollection();
$cellCoordinate = $this->getCoordinate();
self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
return $this->getParent()?->get($cellCoordinate) ?? $this;
}
public const CALCULATE_DATE_TIME_ASIS = 0;
public const CALCULATE_DATE_TIME_FLOAT = 1;
public const CALCULATE_TIME_FLOAT = 2;
private static int $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS;
public static function getCalculateDateTimeType(): int
{
return self::$calculateDateTimeType;
}
/** @throws CalculationException */
public static function setCalculateDateTimeType(int $calculateDateTimeType): void
{
self::$calculateDateTimeType = match ($calculateDateTimeType) {
self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType,
default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"),
};
}
/**
* Convert date, time, or datetime from int to float if desired.
*/
private function convertDateTimeInt(mixed $result): mixed
{
if (is_int($result)) {
if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) {
if (SharedDate::isDateTime($this, $result, false)) {
$result = (float) $result;
}
} elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) {
if (SharedDate::isDateTime($this, $result, true)) {
$result = (float) $result;
}
}
}
return $result;
}
/**
* Get calculated cell value.
*
* @param bool $resetLog Whether the calculation engine logger should be reset or not
*
* @throws CalculationException
*/
public function getCalculatedValue(bool $resetLog = true): mixed
{
if ($this->dataType === DataType::TYPE_FORMULA) {
try {
$index = $this->getWorksheet()->getParentOrThrow()->getActiveSheetIndex();
$selected = $this->getWorksheet()->getSelectedCells();
$result = Calculation::getInstance(
$this->getWorksheet()->getParent()
)->calculateCellValue($this, $resetLog);
$result = $this->convertDateTimeInt($result);
$this->getWorksheet()->setSelectedCells($selected);
$this->getWorksheet()->getParentOrThrow()->setActiveSheetIndex($index);
// We don't yet handle array returns
if (is_array($result)) {
while (is_array($result)) {
$result = array_shift($result);
}
}
} catch (SpreadsheetException $ex) {
if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
return $this->calculatedValue; // Fallback for calculations referencing external files.
} elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
return ExcelError::NAME();
}
throw new CalculationException(
$this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
$ex->getCode(),
$ex
);
}
if ($result === '#Not Yet Implemented') {
return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
}
return $result;
} elseif ($this->value instanceof RichText) {
return $this->value->getPlainText();
}
return $this->convertDateTimeInt($this->value);
}
/**
* Set old calculated value (cached).
*
* @param mixed $originalValue Value
*/
public function setCalculatedValue(mixed $originalValue, bool $tryNumeric = true): self
{
if ($originalValue !== null) {
$this->calculatedValue = ($tryNumeric && is_numeric($originalValue)) ? (0 + $originalValue) : $originalValue;
}
return $this->updateInCollection();
}
/**
* Get old calculated value (cached)
* This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
* create the original spreadsheet file.
* Note that this value is not guaranteed to reflect the actual calculated value because it is
* possible that auto-calculation was disabled in the original spreadsheet, and underlying data
* values used by the formula have changed since it was last calculated.
*/
public function getOldCalculatedValue(): mixed
{
return $this->calculatedValue;
}
/**
* Get cell data type.
*/
public function getDataType(): string
{
return $this->dataType;
}
/**
* Set cell data type.
*
* @param string $dataType see DataType::TYPE_*
*/
public function setDataType(string $dataType): self
{
$this->setValueExplicit($this->value, $dataType);
return $this;
}
/**
* Identify if the cell contains a formula.
*/
public function isFormula(): bool
{
return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false;
}
/**
* Does this cell contain Data validation rules?
*
* @throws SpreadsheetException
*/
public function hasDataValidation(): bool
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet');
}
return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
}
/**
* Get Data validation rules.
*
* @throws SpreadsheetException
*/
public function getDataValidation(): DataValidation
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet');
}
return $this->getWorksheet()->getDataValidation($this->getCoordinate());
}
/**
* Set Data validation rules.
*
* @throws SpreadsheetException
*/
public function setDataValidation(?DataValidation $dataValidation = null): self
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet');
}
$this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation);
return $this->updateInCollection();
}
/**
* Does this cell contain valid value?
*/
public function hasValidValue(): bool
{
$validator = new DataValidator();
return $validator->isValid($this);
}
/**
* Does this cell contain a Hyperlink?
*
* @throws SpreadsheetException
*/
public function hasHyperlink(): bool
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet');
}
return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
}
/**
* Get Hyperlink.
*
* @throws SpreadsheetException
*/
public function getHyperlink(): Hyperlink
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
}
return $this->getWorksheet()->getHyperlink($this->getCoordinate());
}
/**
* Set Hyperlink.
*
* @throws SpreadsheetException
*/
public function setHyperlink(?Hyperlink $hyperlink = null): self
{
if (!isset($this->parent)) {
throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
}
$this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
return $this->updateInCollection();
}
/**
* Get cell collection.
*/
public function getParent(): ?Cells
{
return $this->parent;
}
/**
* Get parent worksheet.
*
* @throws SpreadsheetException
*/
public function getWorksheet(): Worksheet
{
$parent = $this->parent;
if ($parent !== null) {
$worksheet = $parent->getParent();
} else {
$worksheet = null;
}
if ($worksheet === null) {
throw new SpreadsheetException('Worksheet no longer exists');
}
return $worksheet;
}
public function getWorksheetOrNull(): ?Worksheet
{
$parent = $this->parent;
if ($parent !== null) {
$worksheet = $parent->getParent();
} else {
$worksheet = null;
}
return $worksheet;
}
/**
* Is this cell in a merge range.
*/
public function isInMergeRange(): bool
{
return (bool) $this->getMergeRange();
}
/**
* Is this cell the master (top left cell) in a merge range (that holds the actual data value).
*/
public function isMergeRangeValueCell(): bool
{
if ($mergeRange = $this->getMergeRange()) {
$mergeRange = Coordinate::splitRange($mergeRange);
[$startCell] = $mergeRange[0];
return $this->getCoordinate() === $startCell;
}
return false;
}
/**
* If this cell is in a merge range, then return the range.
*
* @return false|string
*/
public function getMergeRange()
{
foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
if ($this->isInRange($mergeRange)) {
return $mergeRange;
}
}
return false;
}
/**
* Get cell style.
*/
public function getStyle(): Style
{
return $this->getWorksheet()->getStyle($this->getCoordinate());
}
/**
* Get cell style.
*/
public function getAppliedStyle(): Style
{
if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) {
return $this->getStyle();
}
$range = $this->getWorksheet()->getConditionalRange($this->getCoordinate());
if ($range === null) {
return $this->getStyle();
}
$matcher = new CellStyleAssessor($this, $range);
return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate()));
}
/**
* Re-bind parent.
*/
public function rebindParent(Worksheet $parent): self
{
$this->parent = $parent->getCellCollection();
return $this->updateInCollection();
}
/**
* Is cell in a specific range?
*
* @param string $range Cell range (e.g. A1:A1)
*/
public function isInRange(string $range): bool
{
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
// Translate properties
$myColumn = Coordinate::columnIndexFromString($this->getColumn());
$myRow = $this->getRow();
// Verify if cell is in range
return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn)
&& ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
}
/**
* Compare 2 cells.
*
* @param Cell $a Cell a
* @param Cell $b Cell b
*
* @return int Result of comparison (always -1 or 1, never zero!)
*/
public static function compareCells(self $a, self $b): int
{
if ($a->getRow() < $b->getRow()) {
return -1;
} elseif ($a->getRow() > $b->getRow()) {
return 1;
} elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
return -1;
}
return 1;
}
/**
* Get value binder to use.
*/
public static function getValueBinder(): IValueBinder
{
if (self::$valueBinder === null) {
self::$valueBinder = new DefaultValueBinder();
}
return self::$valueBinder;
}
/**
* Set value binder to use.
*/
public static function setValueBinder(IValueBinder $binder): void
{
self::$valueBinder = $binder;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $propertyName => $propertyValue) {
if ((is_object($propertyValue)) && ($propertyName !== 'parent')) {
$this->$propertyName = clone $propertyValue;
} else {
$this->$propertyName = $propertyValue;
}
}
}
/**
* Get index to cellXf.
*/
public function getXfIndex(): int
{
return $this->xfIndex;
}
/**
* Set index to cellXf.
*/
public function setXfIndex(int $indexValue): self
{
$this->xfIndex = $indexValue;
return $this->updateInCollection();
}
/**
* Set the formula attributes.
*
* @return $this
*/
public function setFormulaAttributes(mixed $attributes): self
{
$this->formulaAttributes = $attributes;
return $this;
}
/**
* Get the formula attributes.
*/
public function getFormulaAttributes(): mixed
{
return $this->formulaAttributes;
}
/**
* Convert to string.
*/
public function __toString(): string
{
return (string) $this->getValue();
}
public function getIgnoredErrors(): IgnoredErrors
{
return $this->ignoredErrors;
}
public function isLocked(): bool
{
$protected = $this->parent?->getParent()?->getProtection()?->getSheet();
if ($protected !== true) {
return false;
}
$locked = $this->getStyle()->getProtection()->getLocked();
return $locked !== Protection::PROTECTION_UNPROTECTED;
}
public function isHiddenOnFormulaBar(): bool
{
if ($this->getDataType() !== DataType::TYPE_FORMULA) {
return false;
}
$protected = $this->parent?->getParent()?->getProtection()?->getSheet();
if ($protected !== true) {
return false;
}
$hidden = $this->getStyle()->getProtection()->getHidden();
return $hidden !== Protection::PROTECTION_UNPROTECTED;
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
class CellAddress implements Stringable
{
protected ?Worksheet $worksheet;
protected string $cellAddress;
protected string $columnName = '';
protected int $columnId;
protected int $rowId;
public function __construct(string $cellAddress, ?Worksheet $worksheet = null)
{
$this->cellAddress = str_replace('$', '', $cellAddress);
[$this->columnId, $this->rowId, $this->columnName] = Coordinate::indexesFromString($this->cellAddress);
$this->worksheet = $worksheet;
}
public function __destruct()
{
unset($this->worksheet);
}
/**
* @phpstan-assert int|numeric-string $columnId
* @phpstan-assert int|numeric-string $rowId
*/
private static function validateColumnAndRow(mixed $columnId, mixed $rowId): void
{
if (!is_numeric($columnId) || $columnId <= 0 || !is_numeric($rowId) || $rowId <= 0) {
throw new Exception('Row and Column Ids must be positive integer values');
}
}
public static function fromColumnAndRow(mixed $columnId, mixed $rowId, ?Worksheet $worksheet = null): self
{
self::validateColumnAndRow($columnId, $rowId);
return new self(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet);
}
public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self
{
[$columnId, $rowId] = $array;
return self::fromColumnAndRow($columnId, $rowId, $worksheet);
}
public static function fromCellAddress(mixed $cellAddress, ?Worksheet $worksheet = null): self
{
return new self($cellAddress, $worksheet);
}
/**
* The returned address string will contain the worksheet name as well, if available,
* (ie. if a Worksheet was provided to the constructor).
* e.g. "'Mark''s Worksheet'!C5".
*/
public function fullCellAddress(): string
{
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$this->cellAddress}";
}
return $this->cellAddress;
}
public function worksheet(): ?Worksheet
{
return $this->worksheet;
}
/**
* The returned address string will contain just the column/row address,
* (even if a Worksheet was provided to the constructor).
* e.g. "C5".
*/
public function cellAddress(): string
{
return $this->cellAddress;
}
public function rowId(): int
{
return $this->rowId;
}
public function columnId(): int
{
return $this->columnId;
}
public function columnName(): string
{
return $this->columnName;
}
public function nextRow(int $offset = 1): self
{
$newRowId = $this->rowId + $offset;
if ($newRowId < 1) {
$newRowId = 1;
}
return self::fromColumnAndRow($this->columnId, $newRowId);
}
public function previousRow(int $offset = 1): self
{
return $this->nextRow(0 - $offset);
}
public function nextColumn(int $offset = 1): self
{
$newColumnId = $this->columnId + $offset;
if ($newColumnId < 1) {
$newColumnId = 1;
}
return self::fromColumnAndRow($newColumnId, $this->rowId);
}
public function previousColumn(int $offset = 1): self
{
return $this->nextColumn(0 - $offset);
}
/**
* The returned address string will contain the worksheet name as well, if available,
* (ie. if a Worksheet was provided to the constructor).
* e.g. "'Mark''s Worksheet'!C5".
*/
public function __toString(): string
{
return $this->fullCellAddress();
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
class CellRange implements AddressRange, Stringable
{
protected CellAddress $from;
protected CellAddress $to;
public function __construct(CellAddress $from, CellAddress $to)
{
$this->validateFromTo($from, $to);
}
private function validateFromTo(CellAddress $from, CellAddress $to): void
{
// Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left)
$firstColumn = min($from->columnId(), $to->columnId());
$firstRow = min($from->rowId(), $to->rowId());
$lastColumn = max($from->columnId(), $to->columnId());
$lastRow = max($from->rowId(), $to->rowId());
$fromWorksheet = $from->worksheet();
$toWorksheet = $to->worksheet();
$this->validateWorksheets($fromWorksheet, $toWorksheet);
$this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet);
$this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet);
}
private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void
{
if ($fromWorksheet !== null && $toWorksheet !== null) {
// We could simply compare worksheets rather than worksheet titles; but at some point we may introduce
// support for 3d ranges; and at that point we drop this check and let the validation fall through
// to the check for same workbook; but unless we check on titles, this test will also detect if the
// worksheets are in different spreadsheets, and the next check will never execute or throw its
// own exception.
if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) {
throw new Exception('3d Cell Ranges are not supported');
} elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) {
throw new Exception('Worksheets must be in the same spreadsheet');
}
}
}
private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress
{
$cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row;
return new class ($cellAddress, $worksheet) extends CellAddress {
public function nextRow(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::nextRow($offset);
$this->rowId = $result->rowId;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function previousRow(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::previousRow($offset);
$this->rowId = $result->rowId;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function nextColumn(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::nextColumn($offset);
$this->columnId = $result->columnId;
$this->columnName = $result->columnName;
$this->cellAddress = $result->cellAddress;
return $this;
}
public function previousColumn(int $offset = 1): CellAddress
{
/** @var CellAddress $result */
$result = parent::previousColumn($offset);
$this->columnId = $result->columnId;
$this->columnName = $result->columnName;
$this->cellAddress = $result->cellAddress;
return $this;
}
};
}
public function from(): CellAddress
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
return $this->from;
}
public function to(): CellAddress
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
return $this->to;
}
public function __toString(): string
{
// Re-order from/to in case the cell addresses have been modified
$this->validateFromTo($this->from, $this->to);
if ($this->from->cellAddress() === $this->to->cellAddress()) {
return "{$this->from->fullCellAddress()}";
}
$fromAddress = $this->from->fullCellAddress();
$toAddress = $this->to->cellAddress();
return "{$fromAddress}:{$toAddress}";
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
class ColumnRange implements AddressRange, Stringable
{
protected ?Worksheet $worksheet;
protected int $from;
protected int $to;
public function __construct(string $from, ?string $to = null, ?Worksheet $worksheet = null)
{
$this->validateFromTo(
Coordinate::columnIndexFromString($from),
Coordinate::columnIndexFromString($to ?? $from)
);
$this->worksheet = $worksheet;
}
public function __destruct()
{
$this->worksheet = null;
}
public static function fromColumnIndexes(int $from, int $to, ?Worksheet $worksheet = null): self
{
return new self(Coordinate::stringFromColumnIndex($from), Coordinate::stringFromColumnIndex($to), $worksheet);
}
/**
* @param array<int|string> $array
*/
public static function fromArray(array $array, ?Worksheet $worksheet = null): self
{
array_walk(
$array,
function (&$column): void {
$column = is_numeric($column) ? Coordinate::stringFromColumnIndex((int) $column) : $column;
}
);
/** @var string $from */
/** @var string $to */
[$from, $to] = $array;
return new self($from, $to, $worksheet);
}
private function validateFromTo(int $from, int $to): void
{
// Identify actual top and bottom values (in case we've been given bottom and top)
$this->from = min($from, $to);
$this->to = max($from, $to);
}
public function columnCount(): int
{
return $this->to - $this->from + 1;
}
public function shiftDown(int $offset = 1): self
{
$newFrom = $this->from + $offset;
$newFrom = ($newFrom < 1) ? 1 : $newFrom;
$newTo = $this->to + $offset;
$newTo = ($newTo < 1) ? 1 : $newTo;
return self::fromColumnIndexes($newFrom, $newTo, $this->worksheet);
}
public function shiftUp(int $offset = 1): self
{
return $this->shiftDown(0 - $offset);
}
public function from(): string
{
return Coordinate::stringFromColumnIndex($this->from);
}
public function to(): string
{
return Coordinate::stringFromColumnIndex($this->to);
}
public function fromIndex(): int
{
return $this->from;
}
public function toIndex(): int
{
return $this->to;
}
public function toCellRange(): CellRange
{
return new CellRange(
CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet),
CellAddress::fromColumnAndRow($this->to, AddressRange::MAX_ROW)
);
}
public function __toString(): string
{
$from = $this->from();
$to = $this->to();
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$from}:{$to}";
}
return "{$from}:{$to}";
}
}

View file

@ -0,0 +1,678 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* Helper class to manipulate cell coordinates.
*
* Columns indexes and rows are always based on 1, **not** on 0. This match the behavior
* that Excel users are used to, and also match the Excel functions `COLUMN()` and `ROW()`.
*/
abstract class Coordinate
{
public const A1_COORDINATE_REGEX = '/^(?<col>\$?[A-Z]{1,3})(?<row>\$?\d{1,7})$/i';
public const FULL_REFERENCE_REGEX = '/^(?:(?<worksheet>[^!]*)!)?(?<localReference>(?<firstCoordinate>[$]?[A-Z]{1,3}[$]?\d{1,7})(?:\:(?<secondCoordinate>[$]?[A-Z]{1,3}[$]?\d{1,7}))?)$/i';
/**
* Default range variable constant.
*
* @var string
*/
const DEFAULT_RANGE = 'A1:A1';
/**
* Convert string coordinate to [0 => int column index, 1 => int row index].
*
* @param string $cellAddress eg: 'A1'
*
* @return array{0: string, 1: string} Array containing column and row (indexes 0 and 1)
*/
public static function coordinateFromString(string $cellAddress): array
{
if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) {
return [$matches['col'], $matches['row']];
} elseif (self::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells');
} elseif ($cellAddress == '') {
throw new Exception('Cell coordinate can not be zero-length string');
}
throw new Exception('Invalid cell coordinate ' . $cellAddress);
}
/**
* Convert string coordinate to [0 => int column index, 1 => int row index, 2 => string column string].
*
* @param string $coordinates eg: 'A1', '$B$12'
*
* @return array{0: int, 1: int, 2: string} Array containing column and row index, and column string
*/
public static function indexesFromString(string $coordinates): array
{
[$column, $row] = self::coordinateFromString($coordinates);
$column = ltrim($column, '$');
return [
self::columnIndexFromString($column),
(int) ltrim($row, '$'),
$column,
];
}
/**
* Checks if a Cell Address represents a range of cells.
*
* @param string $cellAddress eg: 'A1' or 'A1:A2' or 'A1:A2,C1:C2'
*
* @return bool Whether the coordinate represents a range of cells
*/
public static function coordinateIsRange(string $cellAddress): bool
{
return str_contains($cellAddress, ':') || str_contains($cellAddress, ',');
}
/**
* Make string row, column or cell coordinate absolute.
*
* @param int|string $cellAddress e.g. 'A' or '1' or 'A1'
* Note that this value can be a row or column reference as well as a cell reference
*
* @return string Absolute coordinate e.g. '$A' or '$1' or '$A$1'
*/
public static function absoluteReference(int|string $cellAddress): string
{
$cellAddress = (string) $cellAddress;
if (self::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells');
}
// Split out any worksheet name from the reference
[$worksheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if ($worksheet > '') {
$worksheet .= '!';
}
// Create absolute coordinate
$cellAddress = "$cellAddress";
if (ctype_digit($cellAddress)) {
return $worksheet . '$' . $cellAddress;
} elseif (ctype_alpha($cellAddress)) {
return $worksheet . '$' . strtoupper($cellAddress);
}
return $worksheet . self::absoluteCoordinate($cellAddress);
}
/**
* Make string coordinate absolute.
*
* @param string $cellAddress e.g. 'A1'
*
* @return string Absolute coordinate e.g. '$A$1'
*/
public static function absoluteCoordinate(string $cellAddress): string
{
if (self::coordinateIsRange($cellAddress)) {
throw new Exception('Cell coordinate string can not be a range of cells');
}
// Split out any worksheet name from the coordinate
[$worksheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if ($worksheet > '') {
$worksheet .= '!';
}
// Create absolute coordinate
[$column, $row] = self::coordinateFromString($cellAddress ?? 'A1');
$column = ltrim($column, '$');
$row = ltrim($row, '$');
return $worksheet . '$' . $column . '$' . $row;
}
/**
* Split range into coordinate strings.
*
* @param string $range e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
*
* @return array Array containing one or more arrays containing one or two coordinate strings
* e.g. ['B4','D9'] or [['B4','D9'], ['H2','O11']]
* or ['B4']
*/
public static function splitRange(string $range): array
{
// Ensure $pRange is a valid range
if (empty($range)) {
$range = self::DEFAULT_RANGE;
}
$exploded = explode(',', $range);
$outArray = [];
foreach ($exploded as $value) {
$outArray[] = explode(':', $value);
}
return $outArray;
}
/**
* Build range from coordinate strings.
*
* @param array $range Array containing one or more arrays containing one or two coordinate strings
*
* @return string String representation of $pRange
*/
public static function buildRange(array $range): string
{
// Verify range
if (empty($range) || !is_array($range[0])) {
throw new Exception('Range does not contain any information');
}
// Build range
$counter = count($range);
for ($i = 0; $i < $counter; ++$i) {
$range[$i] = implode(':', $range[$i]);
}
return implode(',', $range);
}
/**
* Calculate range boundaries.
*
* @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3)
*
* @return array Range coordinates [Start Cell, End Cell]
* where Start Cell and End Cell are arrays (Column Number, Row Number)
*/
public static function rangeBoundaries(string $range): array
{
// Ensure $pRange is a valid range
if (empty($range)) {
$range = self::DEFAULT_RANGE;
}
// Uppercase coordinate
$range = strtoupper($range);
// Extract range
if (!str_contains($range, ':')) {
$rangeA = $rangeB = $range;
} else {
[$rangeA, $rangeB] = explode(':', $range);
}
if (is_numeric($rangeA) && is_numeric($rangeB)) {
$rangeA = 'A' . $rangeA;
$rangeB = AddressRange::MAX_COLUMN . $rangeB;
}
if (ctype_alpha($rangeA) && ctype_alpha($rangeB)) {
$rangeA = $rangeA . '1';
$rangeB = $rangeB . AddressRange::MAX_ROW;
}
// Calculate range outer borders
$rangeStart = self::coordinateFromString($rangeA);
$rangeEnd = self::coordinateFromString($rangeB);
// Translate column into index
$rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
$rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
return [$rangeStart, $rangeEnd];
}
/**
* Calculate range dimension.
*
* @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3)
*
* @return array Range dimension (width, height)
*/
public static function rangeDimension(string $range): array
{
// Calculate range outer borders
[$rangeStart, $rangeEnd] = self::rangeBoundaries($range);
return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
}
/**
* Calculate range boundaries.
*
* @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3)
*
* @return array Range coordinates [Start Cell, End Cell]
* where Start Cell and End Cell are arrays [Column ID, Row Number]
*/
public static function getRangeBoundaries(string $range): array
{
[$rangeA, $rangeB] = self::rangeBoundaries($range);
return [
[self::stringFromColumnIndex($rangeA[0]), $rangeA[1]],
[self::stringFromColumnIndex($rangeB[0]), $rangeB[1]],
];
}
/**
* Check if cell or range reference is valid and return an array with type of reference (cell or range), worksheet (if it was given)
* and the coordinate or the first coordinate and second coordinate if it is a range.
*
* @param string $reference Coordinate or Range (e.g. A1:A1, B2, B:C, 2:3)
*
* @return array reference data
*/
private static function validateReferenceAndGetData($reference): array
{
$data = [];
preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches);
if (count($matches) === 0) {
return ['type' => 'invalid'];
}
if (isset($matches['secondCoordinate'])) {
$data['type'] = 'range';
$data['firstCoordinate'] = str_replace('$', '', $matches['firstCoordinate']);
$data['secondCoordinate'] = str_replace('$', '', $matches['secondCoordinate']);
} else {
$data['type'] = 'coordinate';
$data['coordinate'] = str_replace('$', '', $matches['firstCoordinate']);
}
$worksheet = $matches['worksheet'];
if ($worksheet !== '') {
if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") {
$worksheet = substr($worksheet, 1, -1);
}
$data['worksheet'] = strtolower($worksheet);
}
$data['localReference'] = str_replace('$', '', $matches['localReference']);
return $data;
}
/**
* Check if coordinate is inside a range.
*
* @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3)
* @param string $coordinate Cell coordinate (e.g. A1)
*
* @return bool true if coordinate is inside range
*/
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
{
$rangeData = self::validateReferenceAndGetData($range);
if ($rangeData['type'] === 'invalid') {
throw new Exception('First argument needs to be a range');
}
$coordinateData = self::validateReferenceAndGetData($coordinate);
if ($coordinateData['type'] === 'invalid') {
throw new Exception('Second argument needs to be a single coordinate');
}
if (isset($coordinateData['worksheet']) && !isset($rangeData['worksheet'])) {
return false;
}
if (!isset($coordinateData['worksheet']) && isset($rangeData['worksheet'])) {
return false;
}
if (isset($coordinateData['worksheet'], $rangeData['worksheet'])) {
if ($coordinateData['worksheet'] !== $rangeData['worksheet']) {
return false;
}
}
$boundaries = self::rangeBoundaries($rangeData['localReference']);
$coordinates = self::indexesFromString($coordinateData['localReference']);
$columnIsInside = $boundaries[0][0] <= $coordinates[0] && $coordinates[0] <= $boundaries[1][0];
if (!$columnIsInside) {
return false;
}
$rowIsInside = $boundaries[0][1] <= $coordinates[1] && $coordinates[1] <= $boundaries[1][1];
if (!$rowIsInside) {
return false;
}
return true;
}
/**
* Column index from string.
*
* @param ?string $columnAddress eg 'A'
*
* @return int Column index (A = 1)
*/
public static function columnIndexFromString(?string $columnAddress): int
{
// Using a lookup cache adds a slight memory overhead, but boosts speed
// caching using a static within the method is faster than a class static,
// though it's additional memory overhead
static $indexCache = [];
$columnAddress = $columnAddress ?? '';
if (isset($indexCache[$columnAddress])) {
return $indexCache[$columnAddress];
}
// It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array
// rather than use ord() and make it case insensitive to get rid of the strtoupper() as well.
// Because it's a static, there's no significant memory overhead either.
static $columnLookup = [
'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10,
'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19,
'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10,
'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19,
't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
];
// We also use the language construct isset() rather than the more costly strlen() function to match the
// length of $columnAddress for improved performance
if (isset($columnAddress[0])) {
if (!isset($columnAddress[1])) {
$indexCache[$columnAddress] = $columnLookup[$columnAddress];
return $indexCache[$columnAddress];
} elseif (!isset($columnAddress[2])) {
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26
+ $columnLookup[$columnAddress[1]];
return $indexCache[$columnAddress];
} elseif (!isset($columnAddress[3])) {
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676
+ $columnLookup[$columnAddress[1]] * 26
+ $columnLookup[$columnAddress[2]];
return $indexCache[$columnAddress];
}
}
throw new Exception(
'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')
);
}
/**
* String from column index.
*
* @param int|numeric-string $columnIndex Column index (A = 1)
*/
public static function stringFromColumnIndex(int|string $columnIndex): string
{
static $indexCache = [];
static $lookupCache = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (!isset($indexCache[$columnIndex])) {
$indexValue = $columnIndex;
$base26 = '';
do {
$characterValue = ($indexValue % 26) ?: 26;
$indexValue = ($indexValue - $characterValue) / 26;
$base26 = $lookupCache[$characterValue] . $base26;
} while ($indexValue > 0);
$indexCache[$columnIndex] = $base26;
}
return $indexCache[$columnIndex];
}
/**
* Extract all cell references in range, which may be comprised of multiple cell ranges.
*
* @param string $cellRange Range: e.g. 'A1' or 'A1:C10' or 'A1:E10,A20:E25' or 'A1:E5 C3:G7' or 'A1:C1,A3:C3 B1:C3'
*
* @return array Array containing single cell references
*/
public static function extractAllCellReferencesInRange(string $cellRange): array
{
if (substr_count($cellRange, '!') > 1) {
throw new Exception('3-D Range References are not supported');
}
[$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true);
$quoted = '';
if ($worksheet) {
$quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : '';
if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) {
$worksheet = substr($worksheet, 1, -1);
}
$worksheet = str_replace("'", "''", $worksheet);
}
[$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange ?? 'A1');
$cells = [];
foreach ($ranges as $range) {
$cells[] = self::getReferencesForCellBlock($range);
}
$cells = self::processRangeSetOperators($operators, $cells);
if (empty($cells)) {
return [];
}
$cellList = array_merge(...$cells);
return array_map(
fn ($cellAddress) => ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress,
self::sortCellReferenceArray($cellList)
);
}
private static function processRangeSetOperators(array $operators, array $cells): array
{
$operatorCount = count($operators);
for ($offset = 0; $offset < $operatorCount; ++$offset) {
$operator = $operators[$offset];
if ($operator !== ' ') {
continue;
}
$cells[$offset] = array_intersect($cells[$offset], $cells[$offset + 1]);
unset($operators[$offset], $cells[$offset + 1]);
$operators = array_values($operators);
$cells = array_values($cells);
--$offset;
--$operatorCount;
}
return $cells;
}
private static function sortCellReferenceArray(array $cellList): array
{
// Sort the result by column and row
$sortKeys = [];
foreach ($cellList as $coordinate) {
$column = '';
$row = 0;
sscanf($coordinate, '%[A-Z]%d', $column, $row);
$key = (--$row * 16384) + self::columnIndexFromString((string) $column);
$sortKeys[$key] = $coordinate;
}
ksort($sortKeys);
return array_values($sortKeys);
}
/**
* Get all cell references for an individual cell block.
*
* @param string $cellBlock A cell range e.g. A4:B5
*
* @return array All individual cells in that range
*/
private static function getReferencesForCellBlock(string $cellBlock): array
{
$returnValue = [];
// Single cell?
if (!self::coordinateIsRange($cellBlock)) {
return (array) $cellBlock;
}
// Range...
$ranges = self::splitRange($cellBlock);
foreach ($ranges as $range) {
// Single cell?
if (!isset($range[1])) {
$returnValue[] = $range[0];
continue;
}
// Range...
[$rangeStart, $rangeEnd] = $range;
[$startColumn, $startRow] = self::coordinateFromString($rangeStart);
[$endColumn, $endRow] = self::coordinateFromString($rangeEnd);
$startColumnIndex = self::columnIndexFromString($startColumn);
$endColumnIndex = self::columnIndexFromString($endColumn);
++$endColumnIndex;
// Current data
$currentColumnIndex = $startColumnIndex;
$currentRow = $startRow;
self::validateRange($cellBlock, $startColumnIndex, $endColumnIndex, (int) $currentRow, (int) $endRow);
// Loop cells
while ($currentColumnIndex < $endColumnIndex) {
while ($currentRow <= $endRow) {
$returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
++$currentRow;
}
++$currentColumnIndex;
$currentRow = $startRow;
}
}
return $returnValue;
}
/**
* Convert an associative array of single cell coordinates to values to an associative array
* of cell ranges to values. Only adjacent cell coordinates with the same
* value will be merged. If the value is an object, it must implement the method getHashCode().
*
* For example, this function converts:
*
* [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
*
* to:
*
* [ 'A1:A3' => 'x', 'A4' => 'y' ]
*
* @param array $coordinateCollection associative array mapping coordinates to values
*
* @return array associative array mapping coordinate ranges to valuea
*/
public static function mergeRangesInCollection(array $coordinateCollection): array
{
$hashedValues = [];
$mergedCoordCollection = [];
foreach ($coordinateCollection as $coord => $value) {
if (self::coordinateIsRange($coord)) {
$mergedCoordCollection[$coord] = $value;
continue;
}
[$column, $row] = self::coordinateFromString($coord);
$row = (int) (ltrim($row, '$'));
$hashCode = $column . '-' . ((is_object($value) && method_exists($value, 'getHashCode')) ? $value->getHashCode() : $value);
if (!isset($hashedValues[$hashCode])) {
$hashedValues[$hashCode] = (object) [
'value' => $value,
'col' => $column,
'rows' => [$row],
];
} else {
$hashedValues[$hashCode]->rows[] = $row;
}
}
ksort($hashedValues);
foreach ($hashedValues as $hashedValue) {
sort($hashedValue->rows);
$rowStart = null;
$rowEnd = null;
$ranges = [];
foreach ($hashedValue->rows as $row) {
if ($rowStart === null) {
$rowStart = $row;
$rowEnd = $row;
} elseif ($rowEnd === $row - 1) {
$rowEnd = $row;
} else {
if ($rowStart == $rowEnd) {
$ranges[] = $hashedValue->col . $rowStart;
} else {
$ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
}
$rowStart = $row;
$rowEnd = $row;
}
}
if ($rowStart !== null) {
if ($rowStart == $rowEnd) {
$ranges[] = $hashedValue->col . $rowStart;
} else {
$ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
}
}
foreach ($ranges as $range) {
$mergedCoordCollection[$range] = $hashedValue->value;
}
}
return $mergedCoordCollection;
}
/**
* Get the individual cell blocks from a range string, removing any $ characters.
* then splitting by operators and returning an array with ranges and operators.
*
* @return array[]
*/
private static function getCellBlocksFromRangeString(string $rangeString): array
{
$rangeString = str_replace('$', '', strtoupper($rangeString));
// split range sets on intersection (space) or union (,) operators
$tokens = preg_split('/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
$split = array_chunk($tokens, 2);
$ranges = array_column($split, 0);
$operators = array_column($split, 1);
return [$ranges, $operators];
}
/**
* Check that the given range is valid, i.e. that the start column and row are not greater than the end column and
* row.
*
* @param string $cellBlock The original range, for displaying a meaningful error message
*/
private static function validateRange(string $cellBlock, int $startColumnIndex, int $endColumnIndex, int $currentRow, int $endRow): void
{
if ($startColumnIndex >= $endColumnIndex || $currentRow > $endRow) {
throw new Exception('Invalid range: "' . $cellBlock . '"');
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class DataType
{
// Data types
const TYPE_STRING2 = 'str';
const TYPE_STRING = 's';
const TYPE_FORMULA = 'f';
const TYPE_NUMERIC = 'n';
const TYPE_BOOL = 'b';
const TYPE_NULL = 'null';
const TYPE_INLINE = 'inlineStr';
const TYPE_ERROR = 'e';
const TYPE_ISO_DATE = 'd';
/**
* List of error codes.
*
* @var array<string, int>
*/
private static array $errorCodes = [
'#NULL!' => 0,
'#DIV/0!' => 1,
'#VALUE!' => 2,
'#REF!' => 3,
'#NAME?' => 4,
'#NUM!' => 5,
'#N/A' => 6,
'#CALC!' => 7,
];
public const MAX_STRING_LENGTH = 32767;
/**
* Get list of error codes.
*
* @return array<string, int>
*/
public static function getErrorCodes(): array
{
return self::$errorCodes;
}
/**
* Check a string that it satisfies Excel requirements.
*
* @param null|RichText|string $textValue Value to sanitize to an Excel string
*
* @return RichText|string Sanitized value
*/
public static function checkString(null|RichText|string $textValue): RichText|string
{
if ($textValue instanceof RichText) {
// TODO: Sanitize Rich-Text string (max. character count is 32,767)
return $textValue;
}
// string must never be longer than 32,767 characters, truncate if necessary
$textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH);
// we require that newline is represented as "\n" in core, not as "\r\n" or "\r"
$textValue = str_replace(["\r\n", "\r"], "\n", $textValue);
return $textValue;
}
/**
* Check a value that it is a valid error code.
*
* @param mixed $value Value to sanitize to an Excel error code
*
* @return string Sanitized value
*/
public static function checkErrorCode(mixed $value): string
{
$value = (string) $value;
if (!isset(self::$errorCodes[$value])) {
$value = '#NULL!';
}
return $value;
}
}

View file

@ -0,0 +1,421 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
class DataValidation
{
// Data validation types
const TYPE_NONE = 'none';
const TYPE_CUSTOM = 'custom';
const TYPE_DATE = 'date';
const TYPE_DECIMAL = 'decimal';
const TYPE_LIST = 'list';
const TYPE_TEXTLENGTH = 'textLength';
const TYPE_TIME = 'time';
const TYPE_WHOLE = 'whole';
// Data validation error styles
const STYLE_STOP = 'stop';
const STYLE_WARNING = 'warning';
const STYLE_INFORMATION = 'information';
// Data validation operators
const OPERATOR_BETWEEN = 'between';
const OPERATOR_EQUAL = 'equal';
const OPERATOR_GREATERTHAN = 'greaterThan';
const OPERATOR_GREATERTHANOREQUAL = 'greaterThanOrEqual';
const OPERATOR_LESSTHAN = 'lessThan';
const OPERATOR_LESSTHANOREQUAL = 'lessThanOrEqual';
const OPERATOR_NOTBETWEEN = 'notBetween';
const OPERATOR_NOTEQUAL = 'notEqual';
private const DEFAULT_OPERATOR = self::OPERATOR_BETWEEN;
/**
* Formula 1.
*/
private string $formula1 = '';
/**
* Formula 2.
*/
private string $formula2 = '';
/**
* Type.
*/
private string $type = self::TYPE_NONE;
/**
* Error style.
*/
private string $errorStyle = self::STYLE_STOP;
/**
* Operator.
*/
private string $operator = self::DEFAULT_OPERATOR;
/**
* Allow Blank.
*/
private bool $allowBlank = false;
/**
* Show DropDown.
*/
private bool $showDropDown = false;
/**
* Show InputMessage.
*/
private bool $showInputMessage = false;
/**
* Show ErrorMessage.
*/
private bool $showErrorMessage = false;
/**
* Error title.
*/
private string $errorTitle = '';
/**
* Error.
*/
private string $error = '';
/**
* Prompt title.
*/
private string $promptTitle = '';
/**
* Prompt.
*/
private string $prompt = '';
/**
* Create a new DataValidation.
*/
public function __construct()
{
}
/**
* Get Formula 1.
*/
public function getFormula1(): string
{
return $this->formula1;
}
/**
* Set Formula 1.
*
* @return $this
*/
public function setFormula1(string $formula): static
{
$this->formula1 = $formula;
return $this;
}
/**
* Get Formula 2.
*/
public function getFormula2(): string
{
return $this->formula2;
}
/**
* Set Formula 2.
*
* @return $this
*/
public function setFormula2(string $formula): static
{
$this->formula2 = $formula;
return $this;
}
/**
* Get Type.
*/
public function getType(): string
{
return $this->type;
}
/**
* Set Type.
*
* @return $this
*/
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
/**
* Get Error style.
*/
public function getErrorStyle(): string
{
return $this->errorStyle;
}
/**
* Set Error style.
*
* @param string $errorStyle see self::STYLE_*
*
* @return $this
*/
public function setErrorStyle(string $errorStyle): static
{
$this->errorStyle = $errorStyle;
return $this;
}
/**
* Get Operator.
*/
public function getOperator(): string
{
return $this->operator;
}
/**
* Set Operator.
*
* @return $this
*/
public function setOperator(string $operator): static
{
$this->operator = ($operator === '') ? self::DEFAULT_OPERATOR : $operator;
return $this;
}
/**
* Get Allow Blank.
*/
public function getAllowBlank(): bool
{
return $this->allowBlank;
}
/**
* Set Allow Blank.
*
* @return $this
*/
public function setAllowBlank(bool $allowBlank): static
{
$this->allowBlank = $allowBlank;
return $this;
}
/**
* Get Show DropDown.
*/
public function getShowDropDown(): bool
{
return $this->showDropDown;
}
/**
* Set Show DropDown.
*
* @return $this
*/
public function setShowDropDown(bool $showDropDown): static
{
$this->showDropDown = $showDropDown;
return $this;
}
/**
* Get Show InputMessage.
*/
public function getShowInputMessage(): bool
{
return $this->showInputMessage;
}
/**
* Set Show InputMessage.
*
* @return $this
*/
public function setShowInputMessage(bool $showInputMessage): static
{
$this->showInputMessage = $showInputMessage;
return $this;
}
/**
* Get Show ErrorMessage.
*/
public function getShowErrorMessage(): bool
{
return $this->showErrorMessage;
}
/**
* Set Show ErrorMessage.
*
* @return $this
*/
public function setShowErrorMessage(bool $showErrorMessage): static
{
$this->showErrorMessage = $showErrorMessage;
return $this;
}
/**
* Get Error title.
*/
public function getErrorTitle(): string
{
return $this->errorTitle;
}
/**
* Set Error title.
*
* @return $this
*/
public function setErrorTitle(string $errorTitle): static
{
$this->errorTitle = $errorTitle;
return $this;
}
/**
* Get Error.
*/
public function getError(): string
{
return $this->error;
}
/**
* Set Error.
*
* @return $this
*/
public function setError(string $error): static
{
$this->error = $error;
return $this;
}
/**
* Get Prompt title.
*/
public function getPromptTitle(): string
{
return $this->promptTitle;
}
/**
* Set Prompt title.
*
* @return $this
*/
public function setPromptTitle(string $promptTitle): static
{
$this->promptTitle = $promptTitle;
return $this;
}
/**
* Get Prompt.
*/
public function getPrompt(): string
{
return $this->prompt;
}
/**
* Set Prompt.
*
* @return $this
*/
public function setPrompt(string $prompt): static
{
$this->prompt = $prompt;
return $this;
}
/**
* Get hash code.
*
* @return string Hash code
*/
public function getHashCode(): string
{
return md5(
$this->formula1
. $this->formula2
. $this->type
. $this->errorStyle
. $this->operator
. ($this->allowBlank ? 't' : 'f')
. ($this->showDropDown ? 't' : 'f')
. ($this->showInputMessage ? 't' : 'f')
. ($this->showErrorMessage ? 't' : 'f')
. $this->errorTitle
. $this->error
. $this->promptTitle
. $this->prompt
. $this->sqref
. __CLASS__
);
}
/**
* 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
{
return $this->sqref;
}
public function setSqref(?string $str): self
{
$this->sqref = $str;
return $this;
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Exception;
/**
* Validate a cell value according to its validation rules.
*/
class DataValidator
{
/**
* Does this cell contain valid value?
*
* @param Cell $cell Cell to check the value
*/
public function isValid(Cell $cell): bool
{
if (!$cell->hasDataValidation() || $cell->getDataValidation()->getType() === DataValidation::TYPE_NONE) {
return true;
}
$cellValue = $cell->getValue();
$dataValidation = $cell->getDataValidation();
if (!$dataValidation->getAllowBlank() && ($cellValue === null || $cellValue === '')) {
return false;
}
$returnValue = false;
$type = $dataValidation->getType();
if ($type === DataValidation::TYPE_LIST) {
$returnValue = $this->isValueInList($cell);
} elseif ($type === DataValidation::TYPE_WHOLE) {
if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue);
}
} 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);
}
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
$returnValue = $this->numericOperator($dataValidation, mb_strlen((string) $cellValue));
}
return $returnValue;
}
private function numericOperator(DataValidation $dataValidation, int|float $cellValue): 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;
}
return $returnValue;
}
/**
* Does this cell contain valid value, based on list?
*
* @param Cell $cell Cell to check the value
*/
private function isValueInList(Cell $cell): bool
{
$cellValue = $cell->getValue();
$dataValidation = $cell->getDataValidation();
$formula1 = $dataValidation->getFormula1();
if (!empty($formula1)) {
// inline values list
if ($formula1[0] === '"') {
return in_array(strtolower($cellValue), 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);
}
return $result !== ExcelError::NA();
} catch (Exception) {
return false;
}
}
}
return true;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use DateTimeInterface;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use Stringable;
class DefaultValueBinder implements IValueBinder
{
/**
* Bind value to a cell.
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*/
public function bindValue(Cell $cell, $value): bool
{
// sanitize UTF-8 strings
if (is_string($value)) {
$value = StringHelper::sanitizeUTF8($value);
} elseif ($value === null || is_scalar($value) || $value instanceof RichText) {
// No need to do anything
} elseif ($value instanceof DateTimeInterface) {
$value = $value->format('Y-m-d H:i:s');
} elseif ($value instanceof Stringable) {
$value = (string) $value;
} else {
throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value));
}
// Set value explicit
$cell->setValueExplicit($value, static::dataTypeForValue($value));
// Done!
return true;
}
/**
* DataType for value.
*/
public static function dataTypeForValue(mixed $value): string
{
// Match the value against a few data types
if ($value === null) {
return DataType::TYPE_NULL;
} elseif (is_float($value) || is_int($value)) {
return DataType::TYPE_NUMERIC;
} elseif (is_bool($value)) {
return DataType::TYPE_BOOL;
} elseif ($value === '') {
return DataType::TYPE_STRING;
} elseif ($value instanceof RichText) {
return DataType::TYPE_INLINE;
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
return DataType::TYPE_FORMULA;
} elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
$tValue = ltrim($value, '+-');
if (is_string($value) && strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') {
return DataType::TYPE_STRING;
} elseif ((!str_contains($value, '.')) && ($value > PHP_INT_MAX)) {
return DataType::TYPE_STRING;
} elseif (!is_numeric($value)) {
return DataType::TYPE_STRING;
}
return DataType::TYPE_NUMERIC;
} elseif (is_string($value)) {
$errorCodes = DataType::getErrorCodes();
if (isset($errorCodes[$value])) {
return DataType::TYPE_ERROR;
}
}
return DataType::TYPE_STRING;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
class Hyperlink
{
/**
* URL to link the cell to.
*/
private string $url;
/**
* Tooltip to display on the hyperlink.
*/
private string $tooltip;
/**
* Create a new Hyperlink.
*
* @param string $url Url to link the cell to
* @param string $tooltip Tooltip to display on the hyperlink
*/
public function __construct(string $url = '', string $tooltip = '')
{
// Initialise member variables
$this->url = $url;
$this->tooltip = $tooltip;
}
/**
* Get URL.
*/
public function getUrl(): string
{
return $this->url;
}
/**
* Set URL.
*
* @return $this
*/
public function setUrl(string $url): static
{
$this->url = $url;
return $this;
}
/**
* Get tooltip.
*/
public function getTooltip(): string
{
return $this->tooltip;
}
/**
* Set tooltip.
*
* @return $this
*/
public function setTooltip(string $tooltip): static
{
$this->tooltip = $tooltip;
return $this;
}
/**
* Is this hyperlink internal? (to another worksheet).
*/
public function isInternal(): bool
{
return str_contains($this->url, 'sheet://');
}
public function getTypeHyperlink(): string
{
return $this->isInternal() ? '' : 'External';
}
/**
* Get hash code.
*
* @return string Hash code
*/
public function getHashCode(): string
{
return md5(
$this->url
. $this->tooltip
. __CLASS__
);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
interface IValueBinder
{
/**
* Bind value to a cell.
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*/
public function bindValue(Cell $cell, mixed $value): bool;
}

View file

@ -0,0 +1,62 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
class IgnoredErrors
{
private bool $numberStoredAsText = false;
private bool $formula = false;
private bool $twoDigitTextYear = false;
private bool $evalError = false;
public function setNumberStoredAsText(bool $value): self
{
$this->numberStoredAsText = $value;
return $this;
}
public function getNumberStoredAsText(): bool
{
return $this->numberStoredAsText;
}
public function setFormula(bool $value): self
{
$this->formula = $value;
return $this;
}
public function getFormula(): bool
{
return $this->formula;
}
public function setTwoDigitTextYear(bool $value): self
{
$this->twoDigitTextYear = $value;
return $this;
}
public function getTwoDigitTextYear(): bool
{
return $this->twoDigitTextYear;
}
public function setEvalError(bool $value): self
{
$this->evalError = $value;
return $this;
}
public function getEvalError(): bool
{
return $this->evalError;
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Stringable;
class RowRange implements AddressRange, Stringable
{
protected ?Worksheet $worksheet;
protected int $from;
protected int $to;
public function __construct(int $from, ?int $to = null, ?Worksheet $worksheet = null)
{
$this->validateFromTo($from, $to ?? $from);
$this->worksheet = $worksheet;
}
public function __destruct()
{
$this->worksheet = null;
}
public static function fromArray(array $array, ?Worksheet $worksheet = null): self
{
[$from, $to] = $array;
return new self($from, $to, $worksheet);
}
private function validateFromTo(int $from, int $to): void
{
// Identify actual top and bottom values (in case we've been given bottom and top)
$this->from = min($from, $to);
$this->to = max($from, $to);
}
public function from(): int
{
return $this->from;
}
public function to(): int
{
return $this->to;
}
public function rowCount(): int
{
return $this->to - $this->from + 1;
}
public function shiftRight(int $offset = 1): self
{
$newFrom = $this->from + $offset;
$newFrom = ($newFrom < 1) ? 1 : $newFrom;
$newTo = $this->to + $offset;
$newTo = ($newTo < 1) ? 1 : $newTo;
return new self($newFrom, $newTo, $this->worksheet);
}
public function shiftLeft(int $offset = 1): self
{
return $this->shiftRight(0 - $offset);
}
public function toCellRange(): CellRange
{
return new CellRange(
CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet),
CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(AddressRange::MAX_COLUMN), $this->to)
);
}
public function __toString(): string
{
if ($this->worksheet !== null) {
$title = str_replace("'", "''", $this->worksheet->getTitle());
return "'{$title}'!{$this->from}:{$this->to}";
}
return "{$this->from}:{$this->to}";
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
use DateTimeInterface;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use Stringable;
class StringValueBinder implements IValueBinder
{
protected bool $convertNull = true;
protected bool $convertBoolean = true;
protected bool $convertNumeric = true;
protected bool $convertFormula = true;
public function setNullConversion(bool $suppressConversion = false): self
{
$this->convertNull = $suppressConversion;
return $this;
}
public function setBooleanConversion(bool $suppressConversion = false): self
{
$this->convertBoolean = $suppressConversion;
return $this;
}
public function getBooleanConversion(): bool
{
return $this->convertBoolean;
}
public function setNumericConversion(bool $suppressConversion = false): self
{
$this->convertNumeric = $suppressConversion;
return $this;
}
public function setFormulaConversion(bool $suppressConversion = false): self
{
$this->convertFormula = $suppressConversion;
return $this;
}
public function setConversionForAllValueTypes(bool $suppressConversion = false): self
{
$this->convertNull = $suppressConversion;
$this->convertBoolean = $suppressConversion;
$this->convertNumeric = $suppressConversion;
$this->convertFormula = $suppressConversion;
return $this;
}
/**
* Bind value to a cell.
*
* @param Cell $cell Cell to bind value to
* @param mixed $value Value to bind in cell
*/
public function bindValue(Cell $cell, $value): bool
{
if (is_object($value)) {
return $this->bindObjectValue($cell, $value);
}
if ($value !== null && !is_scalar($value)) {
throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value));
}
// sanitize UTF-8 strings
if (is_string($value)) {
$value = StringHelper::sanitizeUTF8($value);
}
if ($value === null && $this->convertNull === false) {
$cell->setValueExplicit($value, DataType::TYPE_NULL);
} elseif (is_bool($value) && $this->convertBoolean === false) {
$cell->setValueExplicit($value, DataType::TYPE_BOOL);
} elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) {
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC);
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) {
$cell->setValueExplicit($value, DataType::TYPE_FORMULA);
} else {
if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
$cell->getStyle()->setQuotePrefix(true);
}
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
}
return true;
}
protected function bindObjectValue(Cell $cell, object $value): bool
{
// Handle any objects that might be injected
if ($value instanceof DateTimeInterface) {
$value = $value->format('Y-m-d H:i:s');
$cell->setValueExplicit($value, DataType::TYPE_STRING);
} elseif ($value instanceof RichText) {
$cell->setValueExplicit($value, DataType::TYPE_INLINE);
} elseif ($value instanceof Stringable) {
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING);
} else {
throw new SpreadsheetException('Unable to bind unstringable object of type ' . get_class($value));
}
return true;
}
}