初始化仓库

This commit is contained in:
wangxiaowei
2025-04-22 14:09:52 +08:00
commit 8b100110bb
5155 changed files with 664201 additions and 0 deletions

View File

@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class ArrayArgumentHelper
{
/**
* @var int
*/
protected $indexStart = 0;
/**
* @var array
*/
protected $arguments;
/**
* @var int
*/
protected $argumentCount;
/**
* @var array
*/
protected $rows;
/**
* @var array
*/
protected $columns;
public function initialise(array $arguments): void
{
$keys = array_keys($arguments);
$this->indexStart = (int) array_shift($keys);
$this->rows = $this->rows($arguments);
$this->columns = $this->columns($arguments);
$this->argumentCount = count($arguments);
$this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns);
$this->rows = $this->rows($arguments);
$this->columns = $this->columns($arguments);
if ($this->arrayArguments() > 2) {
throw new Exception('Formulae with more than two array arguments are not supported');
}
}
public function arguments(): array
{
return $this->arguments;
}
public function hasArrayArgument(): bool
{
return $this->arrayArguments() > 0;
}
public function getFirstArrayArgumentNumber(): int
{
$rowArrays = $this->filterArray($this->rows);
$columnArrays = $this->filterArray($this->columns);
for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) {
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
return ++$index;
}
}
return 0;
}
public function getSingleRowVector(): ?int
{
$rowVectors = $this->getRowVectors();
return count($rowVectors) === 1 ? array_pop($rowVectors) : null;
}
private function getRowVectors(): array
{
$rowVectors = [];
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
if ($this->rows[$index] === 1 && $this->columns[$index] > 1) {
$rowVectors[] = $index;
}
}
return $rowVectors;
}
public function getSingleColumnVector(): ?int
{
$columnVectors = $this->getColumnVectors();
return count($columnVectors) === 1 ? array_pop($columnVectors) : null;
}
private function getColumnVectors(): array
{
$columnVectors = [];
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
if ($this->rows[$index] > 1 && $this->columns[$index] === 1) {
$columnVectors[] = $index;
}
}
return $columnVectors;
}
public function getMatrixPair(): array
{
for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) {
for ($j = $i + 1; $j < $this->argumentCount; ++$j) {
if (isset($this->rows[$i], $this->rows[$j])) {
return [$i, $j];
}
}
}
return [];
}
public function isVector(int $argument): bool
{
return $this->rows[$argument] === 1 || $this->columns[$argument] === 1;
}
public function isRowVector(int $argument): bool
{
return $this->rows[$argument] === 1;
}
public function isColumnVector(int $argument): bool
{
return $this->columns[$argument] === 1;
}
public function rowCount(int $argument): int
{
return $this->rows[$argument];
}
public function columnCount(int $argument): int
{
return $this->columns[$argument];
}
private function rows(array $arguments): array
{
return array_map(
function ($argument) {
return is_countable($argument) ? count($argument) : 1;
},
$arguments
);
}
private function columns(array $arguments): array
{
return array_map(
function ($argument) {
return is_array($argument) && is_array($argument[array_keys($argument)[0]])
? count($argument[array_keys($argument)[0]])
: 1;
},
$arguments
);
}
public function arrayArguments(): int
{
$count = 0;
foreach (array_keys($this->arguments) as $argument) {
if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) {
++$count;
}
}
return $count;
}
private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array
{
foreach ($arguments as $index => $argument) {
if ($rows[$index] === 1 && $columns[$index] === 1) {
while (is_array($argument)) {
$argument = array_pop($argument);
}
$arguments[$index] = $argument;
}
}
return $arguments;
}
private function filterArray(array $array): array
{
return array_filter(
$array,
function ($value) {
return $value > 1;
}
);
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class ArrayArgumentProcessor
{
/**
* @var ArrayArgumentHelper
*/
private static $arrayArgumentHelper;
/**
* @param mixed ...$arguments
*/
public static function processArguments(
ArrayArgumentHelper $arrayArgumentHelper,
callable $method,
...$arguments
): array {
self::$arrayArgumentHelper = $arrayArgumentHelper;
if (self::$arrayArgumentHelper->hasArrayArgument() === false) {
return [$method(...$arguments)];
}
if (self::$arrayArgumentHelper->arrayArguments() === 1) {
$nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber();
return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments);
}
$singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector();
$singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector();
if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) {
// Basic logic for a single row vector and a single column vector
return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments);
}
$matrixPair = self::$arrayArgumentHelper->getMatrixPair();
if ($matrixPair !== []) {
if (
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === true &&
self::$arrayArgumentHelper->isVector($matrixPair[1]) === false) ||
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === false &&
self::$arrayArgumentHelper->isVector($matrixPair[1]) === true)
) {
// Logic for a matrix and a vector (row or column)
return self::evaluateVectorMatrixPair($method, $matrixPair, ...$arguments);
}
// Logic for matrix/matrix, column vector/column vector or row vector/row vector
return self::evaluateMatrixPair($method, $matrixPair, ...$arguments);
}
// Still need to work out the logic for more than two array arguments,
// For the moment, we're throwing an Exception when we initialise the ArrayArgumentHelper
return ['#VALUE!'];
}
/**
* @param mixed ...$arguments
*/
private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array
{
$matrix2 = array_pop($matrixIndexes);
/** @var array $matrixValues2 */
$matrixValues2 = $arguments[$matrix2];
$matrix1 = array_pop($matrixIndexes);
/** @var array $matrixValues1 */
$matrixValues1 = $arguments[$matrix1];
$rows = min(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2]));
$columns = min(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2]));
if ($rows === 1) {
$rows = max(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2]));
}
if ($columns === 1) {
$columns = max(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2]));
}
$result = [];
for ($rowIndex = 0; $rowIndex < $rows; ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $columns; ++$columnIndex) {
$rowIndex1 = self::$arrayArgumentHelper->isRowVector($matrix1) ? 0 : $rowIndex;
$columnIndex1 = self::$arrayArgumentHelper->isColumnVector($matrix1) ? 0 : $columnIndex;
$value1 = $matrixValues1[$rowIndex1][$columnIndex1];
$rowIndex2 = self::$arrayArgumentHelper->isRowVector($matrix2) ? 0 : $rowIndex;
$columnIndex2 = self::$arrayArgumentHelper->isColumnVector($matrix2) ? 0 : $columnIndex;
$value2 = $matrixValues2[$rowIndex2][$columnIndex2];
$arguments[$matrix1] = $value1;
$arguments[$matrix2] = $value2;
$result[$rowIndex][$columnIndex] = $method(...$arguments);
}
}
return $result;
}
/**
* @param mixed ...$arguments
*/
private static function evaluateMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array
{
$matrix2 = array_pop($matrixIndexes);
/** @var array $matrixValues2 */
$matrixValues2 = $arguments[$matrix2];
$matrix1 = array_pop($matrixIndexes);
/** @var array $matrixValues1 */
$matrixValues1 = $arguments[$matrix1];
$result = [];
foreach ($matrixValues1 as $rowIndex => $row) {
foreach ($row as $columnIndex => $value1) {
if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) {
continue;
}
$value2 = $matrixValues2[$rowIndex][$columnIndex];
$arguments[$matrix1] = $value1;
$arguments[$matrix2] = $value2;
$result[$rowIndex][$columnIndex] = $method(...$arguments);
}
}
return $result;
}
/**
* @param mixed ...$arguments
*/
private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, ...$arguments): array
{
$rowVector = Functions::flattenArray($arguments[$rowIndex]);
$columnVector = Functions::flattenArray($arguments[$columnIndex]);
$result = [];
foreach ($columnVector as $column) {
$rowResults = [];
foreach ($rowVector as $row) {
$arguments[$rowIndex] = $row;
$arguments[$columnIndex] = $column;
$rowResults[] = $method(...$arguments);
}
$result[] = $rowResults;
}
return $result;
}
/**
* Note, offset is from 1 (for the first argument) rather than from 0.
*
* @param mixed ...$arguments
*/
private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, ...$arguments): array
{
$values = array_slice($arguments, $nthArgument - 1, 1);
/** @var array $values */
$values = array_pop($values);
$result = [];
foreach ($values as $value) {
$arguments[$nthArgument - 1] = $value;
$result[] = $method(...$arguments);
}
return $result;
}
}

View File

@ -0,0 +1,223 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class BranchPruner
{
/**
* @var bool
*/
protected $branchPruningEnabled = true;
/**
* Used to generate unique store keys.
*
* @var int
*/
private $branchStoreKeyCounter = 0;
/**
* currently pending storeKey (last item of the storeKeysStack.
*
* @var ?string
*/
protected $pendingStoreKey;
/**
* @var string[]
*/
protected $storeKeysStack = [];
/**
* @var bool[]
*/
protected $conditionMap = [];
/**
* @var bool[]
*/
protected $thenMap = [];
/**
* @var bool[]
*/
protected $elseMap = [];
/**
* @var int[]
*/
protected $braceDepthMap = [];
/**
* @var null|string
*/
protected $currentCondition;
/**
* @var null|string
*/
protected $currentOnlyIf;
/**
* @var null|string
*/
protected $currentOnlyIfNot;
/**
* @var null|string
*/
protected $previousStoreKey;
public function __construct(bool $branchPruningEnabled)
{
$this->branchPruningEnabled = $branchPruningEnabled;
}
public function clearBranchStore(): void
{
$this->branchStoreKeyCounter = 0;
}
public function initialiseForLoop(): void
{
$this->currentCondition = null;
$this->currentOnlyIf = null;
$this->currentOnlyIfNot = null;
$this->previousStoreKey = null;
$this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack);
if ($this->branchPruningEnabled) {
$this->initialiseCondition();
$this->initialiseThen();
$this->initialiseElse();
}
}
private function initialiseCondition(): void
{
if (isset($this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) {
$this->currentCondition = $this->pendingStoreKey;
$stackDepth = count($this->storeKeysStack);
if ($stackDepth > 1) {
// nested if
$this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2];
}
}
}
private function initialiseThen(): void
{
if (isset($this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) {
$this->currentOnlyIf = $this->pendingStoreKey;
} elseif (
isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey])
&& $this->thenMap[$this->previousStoreKey]
) {
$this->currentOnlyIf = $this->previousStoreKey;
}
}
private function initialiseElse(): void
{
if (isset($this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) {
$this->currentOnlyIfNot = $this->pendingStoreKey;
} elseif (
isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey])
&& $this->elseMap[$this->previousStoreKey]
) {
$this->currentOnlyIfNot = $this->previousStoreKey;
}
}
public function decrementDepth(): void
{
if (!empty($this->pendingStoreKey)) {
--$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function incrementDepth(): void
{
if (!empty($this->pendingStoreKey)) {
++$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function functionCall(string $functionName): void
{
if ($this->branchPruningEnabled && ($functionName === 'IF(')) {
// we handle a new if
$this->pendingStoreKey = $this->getUnusedBranchStoreKey();
$this->storeKeysStack[] = $this->pendingStoreKey;
$this->conditionMap[$this->pendingStoreKey] = true;
$this->braceDepthMap[$this->pendingStoreKey] = 0;
} elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) {
// this is not an if but we go deeper
++$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function argumentSeparator(): void
{
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) {
// We must go to the IF next argument
if ($this->conditionMap[$this->pendingStoreKey]) {
$this->conditionMap[$this->pendingStoreKey] = false;
$this->thenMap[$this->pendingStoreKey] = true;
} elseif ($this->thenMap[$this->pendingStoreKey]) {
$this->thenMap[$this->pendingStoreKey] = false;
$this->elseMap[$this->pendingStoreKey] = true;
} elseif ($this->elseMap[$this->pendingStoreKey]) {
throw new Exception('Reaching fourth argument of an IF');
}
}
}
/**
* @param mixed $value
*/
public function closingBrace($value): void
{
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) {
// we are closing an IF(
if ($value !== 'IF(') {
throw new Exception('Parser bug we should be in an "IF("');
}
if ($this->conditionMap[$this->pendingStoreKey]) {
throw new Exception('We should not be expecting a condition');
}
$this->thenMap[$this->pendingStoreKey] = false;
$this->elseMap[$this->pendingStoreKey] = false;
--$this->braceDepthMap[$this->pendingStoreKey];
array_pop($this->storeKeysStack);
$this->pendingStoreKey = null;
}
}
public function currentCondition(): ?string
{
return $this->currentCondition;
}
public function currentOnlyIf(): ?string
{
return $this->currentOnlyIf;
}
public function currentOnlyIfNot(): ?string
{
return $this->currentOnlyIfNot;
}
private function getUnusedBranchStoreKey(): string
{
$storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
++$this->branchStoreKeyCounter;
return $storeKeyValue;
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
class CyclicReferenceStack
{
/**
* The call stack for calculated cells.
*
* @var mixed[]
*/
private $stack = [];
/**
* Return the number of entries on the stack.
*
* @return int
*/
public function count()
{
return count($this->stack);
}
/**
* Push a new entry onto the stack.
*
* @param mixed $value
*/
public function push($value): void
{
$this->stack[$value] = $value;
}
/**
* Pop the last entry from the stack.
*
* @return mixed
*/
public function pop()
{
return array_pop($this->stack);
}
/**
* Test to see if a specified entry exists on the stack.
*
* @param mixed $value The value to test
*
* @return bool
*/
public function onStack($value)
{
return isset($this->stack[$value]);
}
/**
* Clear the stack.
*/
public function clear(): void
{
$this->stack = [];
}
/**
* Return an array of all entries on the stack.
*
* @return mixed[]
*/
public function showStack()
{
return $this->stack;
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class FormattedNumber
{
/** Constants */
/** Regular Expressions */
private const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
private const STRING_REGEXP_PERCENT = '~^(?:(?: *(?<PrefixedSign>[-+])? *\% *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i';
// preg_quoted string for major currency symbols, with a %s for locale currency
private const CURRENCY_CONVERSION_LIST = '\$€£¥%s';
private const STRING_CONVERSION_LIST = [
[self::class, 'convertToNumberIfNumeric'],
[self::class, 'convertToNumberIfFraction'],
[self::class, 'convertToNumberIfPercent'],
[self::class, 'convertToNumberIfCurrency'],
];
/**
* Identify whether a string contains a formatted numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfFormatted(string &$operand): bool
{
foreach (self::STRING_CONVERSION_LIST as $conversionMethod) {
if ($conversionMethod($operand) === true) {
return true;
}
}
return false;
}
/**
* Identify whether a string contains a numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfNumeric(string &$operand): bool
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
if (is_numeric($value)) {
$operand = (float) $value;
return true;
}
return false;
}
/**
* Identify whether a string contains a fractional numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfFraction(string &$operand): bool
{
if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) {
$sign = ($match[1] === '-') ? '-' : '+';
$wholePart = ($match[3] === '') ? '' : ($sign . $match[3]);
$fractionFormula = '=' . $wholePart . $sign . $match[4];
$operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula);
return true;
}
return false;
}
/**
* Identify whether a string contains a percentage, and if so,
* convert it to a numeric.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfPercent(string &$operand): bool
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
$match = [];
if ($value !== null && preg_match(self::STRING_REGEXP_PERCENT, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Calculate the percentage
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;
return true;
}
return false;
}
/**
* Identify whether a string contains a currency value, and if so,
* convert it to a numeric.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfCurrency(string &$operand): bool
{
$currencyRegexp = self::currencyMatcherRegexp();
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $operand);
$match = [];
if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Determine the sign
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
//Cast to a float
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue']));
return true;
}
return false;
}
public static function currencyMatcherRegexp(): string
{
$currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode()));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
return '~^(?:(?: *(?<PrefixedSign>[-+])? *(?<PrefixedCurrency>[' . $currencyCodes . ']) *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?<PostfixedCurrency>[' . $currencyCodes . ']) *))$~ui';
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
class Logger
{
/**
* Flag to determine whether a debug log should be generated by the calculation engine
* If true, then a debug log will be generated
* If false, then a debug log will not be generated.
*
* @var bool
*/
private $writeDebugLog = false;
/**
* Flag to determine whether a debug log should be echoed by the calculation engine
* If true, then a debug log will be echoed
* If false, then a debug log will not be echoed
* A debug log can only be echoed if it is generated.
*
* @var bool
*/
private $echoDebugLog = false;
/**
* The debug log generated by the calculation engine.
*
* @var string[]
*/
private $debugLog = [];
/**
* The calculation engine cell reference stack.
*
* @var CyclicReferenceStack
*/
private $cellStack;
/**
* Instantiate a Calculation engine logger.
*/
public function __construct(CyclicReferenceStack $stack)
{
$this->cellStack = $stack;
}
/**
* Enable/Disable Calculation engine logging.
*
* @param bool $writeDebugLog
*/
public function setWriteDebugLog($writeDebugLog): void
{
$this->writeDebugLog = $writeDebugLog;
}
/**
* Return whether calculation engine logging is enabled or disabled.
*
* @return bool
*/
public function getWriteDebugLog()
{
return $this->writeDebugLog;
}
/**
* Enable/Disable echoing of debug log information.
*
* @param bool $echoDebugLog
*/
public function setEchoDebugLog($echoDebugLog): void
{
$this->echoDebugLog = $echoDebugLog;
}
/**
* Return whether echoing of debug log information is enabled or disabled.
*
* @return bool
*/
public function getEchoDebugLog()
{
return $this->echoDebugLog;
}
/**
* Write an entry to the calculation engine debug log.
*
* @param mixed $args
*/
public function writeDebugLog(string $message, ...$args): void
{
// Only write the debug log if logging is enabled
if ($this->writeDebugLog) {
$message = sprintf($message, ...$args);
$cellReference = implode(' -> ', $this->cellStack->showStack());
if ($this->echoDebugLog) {
echo $cellReference,
($this->cellStack->count() > 0 ? ' => ' : ''),
$message,
PHP_EOL;
}
$this->debugLog[] = $cellReference .
($this->cellStack->count() > 0 ? ' => ' : '') .
$message;
}
}
/**
* Write a series of entries to the calculation engine debug log.
*
* @param string[] $args
*/
public function mergeDebugLog(array $args): void
{
if ($this->writeDebugLog) {
foreach ($args as $entry) {
$this->writeDebugLog($entry);
}
}
}
/**
* Clear the calculation engine debug log.
*/
public function clearLog(): void
{
$this->debugLog = [];
}
/**
* Return the calculation engine debug log.
*
* @return string[]
*/
public function getLog()
{
return $this->debugLog;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
interface Operand
{
public static function fromParser(string $formula, int $index, array $matches): self;
public function value(): string;
}

View File

@ -0,0 +1,344 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
final class StructuredReference implements Operand
{
public const NAME = 'Structured Reference';
private const OPEN_BRACE = '[';
private const CLOSE_BRACE = ']';
private const ITEM_SPECIFIER_ALL = '#All';
private const ITEM_SPECIFIER_HEADERS = '#Headers';
private const ITEM_SPECIFIER_DATA = '#Data';
private const ITEM_SPECIFIER_TOTALS = '#Totals';
private const ITEM_SPECIFIER_THIS_ROW = '#This Row';
private const ITEM_SPECIFIER_ROWS_SET = [
self::ITEM_SPECIFIER_ALL,
self::ITEM_SPECIFIER_HEADERS,
self::ITEM_SPECIFIER_DATA,
self::ITEM_SPECIFIER_TOTALS,
];
private const TABLE_REFERENCE = '/([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu';
private string $value;
private string $tableName;
private Table $table;
private string $reference;
private ?int $headersRow;
private int $firstDataRow;
private int $lastDataRow;
private ?int $totalsRow;
private array $columns;
public function __construct(string $structuredReference)
{
$this->value = $structuredReference;
}
public static function fromParser(string $formula, int $index, array $matches): self
{
$val = $matches[0];
$srCount = substr_count($val, self::OPEN_BRACE)
- substr_count($val, self::CLOSE_BRACE);
while ($srCount > 0) {
$srIndex = strlen($val);
$srStringRemainder = substr($formula, $index + $srIndex);
$closingPos = strpos($srStringRemainder, self::CLOSE_BRACE);
if ($closingPos === false) {
throw new Exception("Formula Error: No closing ']' to match opening '['");
}
$srStringRemainder = substr($srStringRemainder, 0, $closingPos + 1);
--$srCount;
if (strpos($srStringRemainder, self::OPEN_BRACE) !== false) {
++$srCount;
}
$val .= $srStringRemainder;
}
return new self($val);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
public function parse(Cell $cell): string
{
$this->getTableStructure($cell);
$cellRange = ($this->isRowReference()) ? $this->getRowReference($cell) : $this->getColumnReference();
return $cellRange;
}
private function isRowReference(): bool
{
return strpos($this->value, '[@') !== false
|| strpos($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']') !== false;
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableStructure(Cell $cell): void
{
preg_match(self::TABLE_REFERENCE, $this->value, $matches);
$this->tableName = $matches[1];
$this->table = ($this->tableName === '')
? $this->getTableForCell($cell)
: $this->getTableByName($cell);
$this->reference = $matches[2];
$tableRange = Coordinate::getRangeBoundaries($this->table->getRange());
$this->headersRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
$this->firstDataRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
$this->totalsRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
$this->lastDataRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
$this->columns = $this->getColumns($cell, $tableRange);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableForCell(Cell $cell): Table
{
$tables = $cell->getWorksheet()->getTableCollection();
foreach ($tables as $table) {
/** @var Table $table */
$range = $table->getRange();
if ($cell->isInRange($range) === true) {
$this->tableName = $table->getName();
return $table;
}
}
throw new Exception('Table for Structured Reference cannot be identified');
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableByName(Cell $cell): Table
{
$table = $cell->getWorksheet()->getTableByName($this->tableName);
if ($table === null) {
throw new Exception("Table {$this->tableName} for Structured Reference cannot be located");
}
return $table;
}
private function getColumns(Cell $cell, array $tableRange): array
{
$worksheet = $cell->getWorksheet();
$cellReference = $cell->getCoordinate();
$columns = [];
$lastColumn = ++$tableRange[1][0];
for ($column = $tableRange[0][0]; $column !== $lastColumn; ++$column) {
$columns[$column] = $worksheet
->getCell($column . ($this->headersRow ?? ($this->firstDataRow - 1)))
->getCalculatedValue();
}
$worksheet->getCell($cellReference);
return $columns;
}
private function getRowReference(Cell $cell): string
{
$reference = str_replace("\u{a0}", ' ', $this->reference);
/** @var string $reference */
$reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
foreach ($this->columns as $columnId => $columnName) {
$columnName = str_replace("\u{a0}", ' ', $columnName);
$reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
}
/** @var string $reference */
return $this->validateParsedReference(trim($reference, '[]@, '));
}
private function adjustRowReference(string $columnName, string $reference, Cell $cell, string $columnId): string
{
if ($columnName !== '') {
$cellReference = $columnId . $cell->getRow();
$pattern1 = '/\[' . preg_quote($columnName) . '\]/miu';
$pattern2 = '/@' . preg_quote($columnName) . '/miu';
if (preg_match($pattern1, $reference) === 1) {
$reference = preg_replace($pattern1, $cellReference, $reference);
} elseif (preg_match($pattern2, $reference) === 1) {
$reference = preg_replace($pattern2, $cellReference, $reference);
}
/** @var string $reference */
}
return $reference;
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getColumnReference(): string
{
$reference = str_replace("\u{a0}", ' ', $this->reference);
$startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow;
$endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow;
[$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
$reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
$reference = trim($reference, '[]@, ');
if (substr_count($reference, ':') > 1) {
$cells = explode(':', $reference);
$firstCell = array_shift($cells);
$lastCell = array_pop($cells);
$reference = "{$firstCell}:{$lastCell}";
}
return $this->validateParsedReference($reference);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function validateParsedReference(string $reference): string
{
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
throw new Exception(
"Invalid Structured Reference {$this->reference} {$reference}",
Exception::CALCULATION_ENGINE_PUSH_TO_STACK
);
}
}
return $reference;
}
private function fullData(int $startRow, int $endRow): string
{
$columns = array_keys($this->columns);
$firstColumn = array_shift($columns);
$lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns);
return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}";
}
private function getMinimumRow(string $reference): int
{
switch ($reference) {
case self::ITEM_SPECIFIER_ALL:
case self::ITEM_SPECIFIER_HEADERS:
return $this->headersRow ?? $this->firstDataRow;
case self::ITEM_SPECIFIER_DATA:
return $this->firstDataRow;
case self::ITEM_SPECIFIER_TOTALS:
return $this->totalsRow ?? $this->lastDataRow;
}
return $this->headersRow ?? $this->firstDataRow;
}
private function getMaximumRow(string $reference): int
{
switch ($reference) {
case self::ITEM_SPECIFIER_HEADERS:
return $this->headersRow ?? $this->firstDataRow;
case self::ITEM_SPECIFIER_DATA:
return $this->lastDataRow;
case self::ITEM_SPECIFIER_ALL:
case self::ITEM_SPECIFIER_TOTALS:
return $this->totalsRow ?? $this->lastDataRow;
}
return $this->totalsRow ?? $this->lastDataRow;
}
public function value(): string
{
return $this->value;
}
/**
* @return array<int, int>
*/
private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
{
$rowsSelected = false;
foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
$pattern = '/\[' . $rowReference . '\]/mui';
/** @var string $reference */
if (preg_match($pattern, $reference) === 1) {
if (($rowReference === self::ITEM_SPECIFIER_HEADERS) && ($this->table->getShowHeaderRow() === false)) {
throw new Exception(
'Table Headers are Hidden, and should not be Referenced',
Exception::CALCULATION_ENGINE_PUSH_TO_STACK
);
}
$rowsSelected = true;
$startRow = min($startRow, $this->getMinimumRow($rowReference));
$endRow = max($endRow, $this->getMaximumRow($rowReference));
$reference = preg_replace($pattern, '', $reference);
}
}
if ($rowsSelected === false) {
// If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
$startRow = $this->firstDataRow;
$endRow = $this->lastDataRow;
}
return [$startRow, $endRow];
}
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
{
$columnsSelected = false;
foreach ($this->columns as $columnId => $columnName) {
$columnName = str_replace("\u{a0}", ' ', $columnName);
$cellFrom = "{$columnId}{$startRow}";
$cellTo = "{$columnId}{$endRow}";
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
$pattern = '/\[' . preg_quote($columnName) . '\]/mui';
if (preg_match($pattern, $reference) === 1) {
$columnsSelected = true;
$reference = preg_replace($pattern, $cellReference, $reference);
}
/** @var string $reference */
}
if ($columnsSelected === false) {
return $this->fullData($startRow, $endRow);
}
return $reference;
}
}