init
This commit is contained in:
commit
72a26edcff
22092 changed files with 2101903 additions and 0 deletions
177
lib/PhpSpreadsheet/Cell/AddressHelper.php
Normal file
177
lib/PhpSpreadsheet/Cell/AddressHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
18
lib/PhpSpreadsheet/Cell/AddressRange.php
Normal file
18
lib/PhpSpreadsheet/Cell/AddressRange.php
Normal 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;
|
||||
}
|
||||
209
lib/PhpSpreadsheet/Cell/AdvancedValueBinder.php
Normal file
209
lib/PhpSpreadsheet/Cell/AdvancedValueBinder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
808
lib/PhpSpreadsheet/Cell/Cell.php
Normal file
808
lib/PhpSpreadsheet/Cell/Cell.php
Normal 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;
|
||||
}
|
||||
}
|
||||
148
lib/PhpSpreadsheet/Cell/CellAddress.php
Normal file
148
lib/PhpSpreadsheet/Cell/CellAddress.php
Normal 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();
|
||||
}
|
||||
}
|
||||
131
lib/PhpSpreadsheet/Cell/CellRange.php
Normal file
131
lib/PhpSpreadsheet/Cell/CellRange.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
122
lib/PhpSpreadsheet/Cell/ColumnRange.php
Normal file
122
lib/PhpSpreadsheet/Cell/ColumnRange.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
678
lib/PhpSpreadsheet/Cell/Coordinate.php
Normal file
678
lib/PhpSpreadsheet/Cell/Coordinate.php
Normal 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 . '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
89
lib/PhpSpreadsheet/Cell/DataType.php
Normal file
89
lib/PhpSpreadsheet/Cell/DataType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
421
lib/PhpSpreadsheet/Cell/DataValidation.php
Normal file
421
lib/PhpSpreadsheet/Cell/DataValidation.php
Normal 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;
|
||||
}
|
||||
}
|
||||
117
lib/PhpSpreadsheet/Cell/DataValidator.php
Normal file
117
lib/PhpSpreadsheet/Cell/DataValidator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
79
lib/PhpSpreadsheet/Cell/DefaultValueBinder.php
Normal file
79
lib/PhpSpreadsheet/Cell/DefaultValueBinder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
96
lib/PhpSpreadsheet/Cell/Hyperlink.php
Normal file
96
lib/PhpSpreadsheet/Cell/Hyperlink.php
Normal 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__
|
||||
);
|
||||
}
|
||||
}
|
||||
14
lib/PhpSpreadsheet/Cell/IValueBinder.php
Normal file
14
lib/PhpSpreadsheet/Cell/IValueBinder.php
Normal 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;
|
||||
}
|
||||
62
lib/PhpSpreadsheet/Cell/IgnoredErrors.php
Normal file
62
lib/PhpSpreadsheet/Cell/IgnoredErrors.php
Normal 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;
|
||||
}
|
||||
}
|
||||
90
lib/PhpSpreadsheet/Cell/RowRange.php
Normal file
90
lib/PhpSpreadsheet/Cell/RowRange.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
118
lib/PhpSpreadsheet/Cell/StringValueBinder.php
Normal file
118
lib/PhpSpreadsheet/Cell/StringValueBinder.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue