generated from jric11/baseProject
Codex's updates...
This commit is contained in:
+11
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class BinaryComparison
|
||||
@@ -41,17 +42,25 @@ class BinaryComparison
|
||||
return strcmp($str1, $str2);
|
||||
}
|
||||
|
||||
public static function compare(mixed $operand1, mixed $operand2, string $operator): bool
|
||||
public static function compare(mixed $operand1, mixed $operand2, string $operator): bool|string
|
||||
{
|
||||
// Simple validate the two operands if they are string values
|
||||
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) {
|
||||
$operand1 = Calculation::unwrapResult($operand1);
|
||||
}
|
||||
if (ErrorValue::isError($operand1, true)) {
|
||||
/** @var string $operand1 */
|
||||
return $operand1;
|
||||
}
|
||||
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) {
|
||||
$operand2 = Calculation::unwrapResult($operand2);
|
||||
}
|
||||
if (ErrorValue::isError($operand2, true)) {
|
||||
/** @var string $operand2 */
|
||||
return $operand2;
|
||||
}
|
||||
|
||||
// Use case insensitive comparaison if not OpenOffice mode
|
||||
// Use case-insensitive comparison if not OpenOffice mode
|
||||
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
if (is_string($operand1)) {
|
||||
$operand1 = StringHelper::strToUpper($operand1);
|
||||
|
||||
+193
-48
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use Composer\Pcre\Preg; // many pregs in this program use u modifier, which has side-effects which make it unsuitable for this
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
|
||||
@@ -13,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\DefinedName;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\NamedRange;
|
||||
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
@@ -36,7 +38,7 @@ class Calculation extends CalculationLocale
|
||||
const CALCULATION_REGEXP_OPENBRACE = '\(';
|
||||
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
|
||||
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?((?:__xludf\.)?[\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
|
||||
// Cell reference (cell or range of cells, with or without a sheet reference)
|
||||
// Cell reference, with or without a sheet reference)
|
||||
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
|
||||
// Used only to detect spill operator #
|
||||
const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i';
|
||||
@@ -88,9 +90,22 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
private bool $calculationCacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Maximum number of entries in the formula token cache.
|
||||
* Default 0 (disabled). Set via setFormulaTokenCacheMaxSize() to enable.
|
||||
*/
|
||||
private int $formulaTokenCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Cache of parsed formula tokens, keyed by the raw formula string.
|
||||
*
|
||||
* @var array<string, array<mixed>|bool>
|
||||
*/
|
||||
private array $formulaTokenCache = [];
|
||||
|
||||
private BranchPruner $branchPruner;
|
||||
|
||||
private bool $branchPruningEnabled = true;
|
||||
protected bool $branchPruningEnabled = true;
|
||||
|
||||
/**
|
||||
* List of operators that can be used within formulae
|
||||
@@ -239,6 +254,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
$this->clearCalculationCache();
|
||||
$this->branchPruner->clearBranchStore();
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,6 +380,44 @@ class Calculation extends CalculationLocale
|
||||
$this->calculationCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the formula token cache.
|
||||
*/
|
||||
public function clearFormulaTokenCache(): void
|
||||
{
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of entries in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheSize(): int
|
||||
{
|
||||
return count($this->formulaTokenCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of entries in the formula token cache.
|
||||
* Set to 0 to disable caching (default), or a positive integer to enable.
|
||||
*/
|
||||
public function setFormulaTokenCacheMaxSize(int $size): self
|
||||
{
|
||||
$this->formulaTokenCacheMaxSize = max(0, $size);
|
||||
if ($this->formulaTokenCacheMaxSize === 0) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of entries allowed in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheMaxSize(): int
|
||||
{
|
||||
return $this->formulaTokenCacheMaxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear calculation cache for a specified worksheet.
|
||||
*/
|
||||
@@ -415,7 +469,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
if (is_string($value)) {
|
||||
// Error values cannot be "wrapped"
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
if (Preg::isMatch('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
// Return Excel errors "as is"
|
||||
return $value;
|
||||
}
|
||||
@@ -494,13 +548,13 @@ class Calculation extends CalculationLocale
|
||||
try {
|
||||
$value = $cell->getValue();
|
||||
if (is_string($value) && $cell->getDataType() === DataType::TYPE_FORMULA) {
|
||||
$value = preg_replace_callback(
|
||||
$value = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$value
|
||||
);
|
||||
}
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore argument.type ($value can be mixed not string)
|
||||
if ($this->spreadsheet === null) {
|
||||
throw new Exception('null spreadsheet in calculateCellValue');
|
||||
}
|
||||
@@ -520,7 +574,7 @@ class Calculation extends CalculationLocale
|
||||
$cellAddress = array_pop($this->cellStack);
|
||||
}
|
||||
if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
|
||||
$sheetName = $cellAddress['sheet'] ?? null;
|
||||
$sheetName = $cellAddress['sheet'];
|
||||
$testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null;
|
||||
if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
|
||||
/** @var array{cell: string} $cellAddress */
|
||||
@@ -557,11 +611,17 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
public function parseFormula(string $formula): array|bool
|
||||
{
|
||||
$formula = preg_replace_callback(
|
||||
// Check the formula token cache first (only when caching is enabled)
|
||||
if ($this->formulaTokenCacheMaxSize > 0 && isset($this->formulaTokenCache[$formula])) {
|
||||
return $this->formulaTokenCache[$formula];
|
||||
}
|
||||
|
||||
$originalFormula = $formula;
|
||||
$formula = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$formula
|
||||
) ?? $formula;
|
||||
);
|
||||
// Basic validation that this is indeed a formula
|
||||
// We return an empty array if not
|
||||
$formula = trim($formula);
|
||||
@@ -574,7 +634,21 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
|
||||
// Parse the formula and return the token stack
|
||||
return $this->internalParseFormula($formula);
|
||||
$result = $this->internalParseFormula($formula);
|
||||
|
||||
// Cache the result when caching is enabled (clear cache if it exceeds the maximum size)
|
||||
if ($this->formulaTokenCacheMaxSize > 0) {
|
||||
// Phpstan says if condition is always false,
|
||||
// but coverage report says next statement is covered.
|
||||
if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
// Cache key is the original formula string (before ANCHORARRAY transformation)
|
||||
// to ensure consistent lookup regardless of internal transformations.
|
||||
$this->formulaTokenCache[$originalFormula] = $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,8 +731,9 @@ class Calculation extends CalculationLocale
|
||||
return self::wrapResult((string) $formula);
|
||||
}
|
||||
|
||||
// https://www.reddit.com/r/excel/comments/chr41y/cmd_formula_stopped_working_since_last_update/
|
||||
if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
|
||||
return self::wrapResult($formula);
|
||||
return ExcelError::REF(); // returns #BLOCKED in newer versions
|
||||
}
|
||||
|
||||
// Basic validation that this is indeed a formula
|
||||
@@ -914,13 +989,14 @@ class Calculation extends CalculationLocale
|
||||
$pad = $rpad = ', ';
|
||||
foreach ($value as $row) {
|
||||
if (is_array($row)) {
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore argument.type (array_map can theoretically return array<mixed> not array<string>)
|
||||
$rpad = '; ';
|
||||
} else {
|
||||
$returnMatrix[] = $this->showValue($row);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string[] $returnMatrix */
|
||||
return '{ ' . implode($rpad, $returnMatrix) . ' }';
|
||||
} elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
|
||||
return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
|
||||
@@ -1052,6 +1128,50 @@ class Calculation extends CalculationLocale
|
||||
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
|
||||
];
|
||||
|
||||
/** @param array<?string> $matches */
|
||||
private function unionForComma(array $matches): string
|
||||
{
|
||||
$matches5 = (string) $matches[5];
|
||||
// Weirdly, the regexp which get us here for issue 4832
|
||||
// only gets us here for Php8.4+. 8.4 introduced
|
||||
// major changes for PCRE, but I cannot identify
|
||||
// the exact change which caused this discrepancy.
|
||||
// I do plan to update coverage to 8.4 at some point,
|
||||
// and I can remove the coverage annotations after that.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (str_contains($matches5, '(') && !str_contains($matches5, ')')) {
|
||||
if ($this->spreadsheet !== null) {
|
||||
if ($this->spreadsheet->getSheetByName($matches5) === null) {
|
||||
$matches0 = (string) $matches[0];
|
||||
$this->debugLog->writeDebugLog('Not Reformulating %s', $matches0);
|
||||
|
||||
return $matches0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$matches1 = (string) $matches[1];
|
||||
$matches2 = (string) $matches[2];
|
||||
|
||||
return $matches1 . str_replace(',', '∪', $matches2);
|
||||
}
|
||||
|
||||
private const CELL_OR_CELLRANGE_OR_DEFINED_NAME
|
||||
= '(?:'
|
||||
. self::CALCULATION_REGEXP_CELLREF // cell address
|
||||
. '(?::' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address, non-capturing
|
||||
. '|' . self::CALCULATION_REGEXP_DEFINEDNAME
|
||||
. ')'
|
||||
;
|
||||
|
||||
public const UNIONABLE_COMMAS = '/((?:[,(]|^)\s*)' // comma or open paren or start of string, followed by optional whitespace
|
||||
. '([(]' // open paren
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. '(?:\s*,\s*' // optioonal whitespace, comma, optional whitespace, non-capturing
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. ')+' // one or more occurrences
|
||||
. '\s*[)])/i'; // optional whitespace, end paren
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>|false
|
||||
*/
|
||||
@@ -1060,6 +1180,12 @@ class Calculation extends CalculationLocale
|
||||
if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldFormula = $formula;
|
||||
$formula = Preg::replaceCallback(self::UNIONABLE_COMMAS, $this->unionForComma(...), $formula);
|
||||
if ($oldFormula !== $formula) {
|
||||
$this->debugLog->writeDebugLog('Reformulated as %s', $formula);
|
||||
}
|
||||
$phpSpreadsheetFunctions = &self::getFunctionsAddress();
|
||||
|
||||
// If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
|
||||
@@ -1179,7 +1305,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
}
|
||||
} elseif (is_string($expectedArgumentCount) && $expectedArgumentCount !== '*') {
|
||||
if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
if (!Preg::isMatch('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
$argMatch = ['', '', '', ''];
|
||||
}
|
||||
switch ($argMatch[2]) {
|
||||
@@ -1236,16 +1362,14 @@ class Calculation extends CalculationLocale
|
||||
// because at least the braces are paired up (at this stage in the formula)
|
||||
// MS Excel allows this if the content is cell references; but doesn't allow actual values,
|
||||
// but at this point, we can't differentiate (so allow both)
|
||||
return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
/* The following code may be a better choice, but, with
|
||||
the other changes for this PR, I can no longer come up
|
||||
with a test case that gets here
|
||||
//return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
|
||||
$stack->push('Binary Operator', '∪');
|
||||
|
||||
++$index;
|
||||
$expectingOperator = false;
|
||||
|
||||
continue;*/
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string, int> $d */
|
||||
@@ -1267,11 +1391,12 @@ class Calculation extends CalculationLocale
|
||||
// do we now have a function/variable/number?
|
||||
$expectingOperator = true;
|
||||
$expectingOperand = false;
|
||||
$val = $match[1] ?? ''; //* @phpstan-ignore-line
|
||||
$val = $match[1] ?? '';
|
||||
$length = strlen($val);
|
||||
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
|
||||
$val = (string) preg_replace('/\s/u', '', $val);
|
||||
// $val is known to be valid unicode from statement above, so Preg::replace is okay even with u modifier
|
||||
$val = Preg::replace('/\s/u', '', $val);
|
||||
if (isset($phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||
$valToUpper = strtoupper($val);
|
||||
} else {
|
||||
@@ -1410,7 +1535,7 @@ class Calculation extends CalculationLocale
|
||||
$refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
|
||||
}
|
||||
|
||||
if (ctype_digit($val) && $val <= 1048576) {
|
||||
if (ctype_digit($val) && $val <= AddressRange::MAX_ROW) {
|
||||
// Row range
|
||||
$stackItemType = 'Row Reference';
|
||||
$valx = $val;
|
||||
@@ -1419,7 +1544,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
|
||||
// Column range
|
||||
$stackItemType = 'Column Reference';
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow() : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$val = "{$rangeWS2}{$val}{$endRowColRef}";
|
||||
}
|
||||
$stackItemReference = $val;
|
||||
@@ -1445,7 +1570,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$column = 'A';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevop must be non-null), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$column = $pCellParent->getHighestDataColumn($val);
|
||||
}
|
||||
$val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
|
||||
@@ -1459,7 +1584,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$row = '1';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevOp must be non-null?), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$row = $pCellParent->getHighestDataRow($val);
|
||||
}
|
||||
$val = "{$val}{$row}";
|
||||
@@ -1610,7 +1735,7 @@ class Calculation extends CalculationLocale
|
||||
foreach ($tokens as $tokenIdx => $tokenData) {
|
||||
/** @var mixed[] $tokenData */
|
||||
$this->processingAnchorArray = false;
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore offsetAccess.nonOffsetAccessible ($tokens might be mixed not array)
|
||||
$this->processingAnchorArray = true;
|
||||
}
|
||||
$token = $tokenData['value'];
|
||||
@@ -1712,7 +1837,7 @@ class Calculation extends CalculationLocale
|
||||
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore offsetAccess.invalidOffset ($token is mixed)
|
||||
// if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack
|
||||
// We must have two operands, error if we don't
|
||||
$operand2Data = $stack->pop();
|
||||
@@ -1830,7 +1955,7 @@ class Calculation extends CalculationLocale
|
||||
if ($breakNeeded) {
|
||||
break;
|
||||
}
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
|
||||
} else {
|
||||
@@ -1901,7 +2026,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (Information\ErrorValue::isError($operand2)) {
|
||||
$result = $operand2;
|
||||
} else {
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore binaryOp.invalid (unwrapresult can return mixed rather than string)
|
||||
$result = StringHelper::substring(
|
||||
$result,
|
||||
0,
|
||||
@@ -1934,8 +2059,8 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Error', ExcelError::null(), null);
|
||||
} else {
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Value', $cellIntersect, $cellRef);
|
||||
}
|
||||
@@ -1950,13 +2075,13 @@ class Calculation extends CalculationLocale
|
||||
|
||||
break;
|
||||
}
|
||||
} elseif (($token === '~') || ($token === '%')) {
|
||||
} elseif (($token === '~') || ($token === '%')) { // @phpstan-ignore booleanOr.alwaysFalse (phpstan says token can't be plain string), identical.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
|
||||
if (($arg = $stack->pop()) === null) {
|
||||
return $this->raiseFormulaError('Internal error - Operand value missing from stack');
|
||||
}
|
||||
$arg = $arg['value'];
|
||||
if ($token === '~') {
|
||||
if ($token === '~') { // @phpstan-ignore identical.alwaysFalse (phpstan says token can't be plain string)
|
||||
$this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
|
||||
$multiplier = -1;
|
||||
} else {
|
||||
@@ -1988,20 +2113,27 @@ class Calculation extends CalculationLocale
|
||||
} else {
|
||||
$this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
|
||||
}
|
||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
} elseif (Preg::isMatch('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
$cellRef = null;
|
||||
|
||||
/* Phpstan says matches[8/9/10] is never set,
|
||||
and code coverage report seems to confirm.
|
||||
Appease PhpStan for now;
|
||||
probably delete this block later.
|
||||
regex101.com confirms - only 7 capturing groups.
|
||||
My theory is that this code expected regexp to
|
||||
match cell *or* cellRange, but it does not
|
||||
match the latter. Retain the code for now in case
|
||||
we do want to add the range match later.
|
||||
Probably delete this block later.
|
||||
Until delete happens, turn code coverage off.
|
||||
*/
|
||||
if (isset($matches[self::$matchIndex8])) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($cell === null) {
|
||||
// We can't access the range, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2026,12 +2158,14 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
if ($cell === null) {
|
||||
// We can't access the cell, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2042,7 +2176,11 @@ class Calculation extends CalculationLocale
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
|
||||
if ($cellSheet && !$cellSheet->cellExists($cellRef)) {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
try {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
} catch (SpreadsheetException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
if ($cellSheet && $cellSheet->cellExists($cellRef)) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
|
||||
@@ -2073,7 +2211,7 @@ class Calculation extends CalculationLocale
|
||||
$cellValue = array_shift($cellValue);
|
||||
}
|
||||
if (is_string($cellValue)) {
|
||||
$cellValue = preg_replace('/"/', '""', $cellValue);
|
||||
$cellValue = Preg::replace('/"/', '""', $cellValue);
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
@@ -2117,7 +2255,7 @@ class Calculation extends CalculationLocale
|
||||
$a = $argCount - $i - 1;
|
||||
if (
|
||||
($passByReference)
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore offsetAccess.nonOffsetAccessible (possibly need to pass 2 arguments to isset)
|
||||
&& ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
|
||||
) {
|
||||
/** @var mixed[] $arg */
|
||||
@@ -2184,7 +2322,7 @@ class Calculation extends CalculationLocale
|
||||
if ($functionName !== 'MKMATRIX') {
|
||||
if ($this->debugLog->getWriteDebugLog()) {
|
||||
krsort($argArrayVals);
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore argument.type (flattenArray returns array<mixed> rather than array<string>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2221,7 +2359,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
} else {
|
||||
// if the token is a number, boolean, string or an Excel error, push it onto the stack
|
||||
/** @var ?string $token */
|
||||
/** @var null|numeric-string $token */
|
||||
if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) {
|
||||
$excelConstant = strtoupper("$token");
|
||||
$stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]);
|
||||
@@ -2229,7 +2367,7 @@ class Calculation extends CalculationLocale
|
||||
$branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant];
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant]));
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore function.alreadyNarrowedType (phpstan says is_bool has argument of *NEVER*?), equal.alwaysFalse (ditto)
|
||||
/** @var array{type: string, reference: ?string} $tokenData */
|
||||
$stack->push($tokenData['type'], $token, $tokenData['reference']);
|
||||
if (isset($storeKey)) {
|
||||
@@ -2355,7 +2493,7 @@ class Calculation extends CalculationLocale
|
||||
$r = $stack->pop();
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} elseif (is_array($operand2) && is_array($operand1)) {
|
||||
} elseif (is_array($operand2) /*&& is_array($operand1)*/) {
|
||||
// Operand 1 and Operand 2 are both arrays
|
||||
if (!$recursingArrays) {
|
||||
self::checkMatrixOperands($operand1, $operand2, 2);
|
||||
@@ -2368,7 +2506,7 @@ class Calculation extends CalculationLocale
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} else {
|
||||
throw new Exception('Neither operand is an arra');
|
||||
throw new Exception('Neither operand is an array');
|
||||
}
|
||||
// Log the result details
|
||||
$this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
|
||||
@@ -2378,8 +2516,8 @@ class Calculation extends CalculationLocale
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return bool|mixed[] */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool
|
||||
/** @return array<mixed>|bool|string */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool|string
|
||||
{
|
||||
// If we're dealing with matrix operations, we want a matrix result
|
||||
if ((is_array($operand1)) || (is_array($operand2))) {
|
||||
@@ -2581,6 +2719,8 @@ class Calculation extends CalculationLocale
|
||||
if (!isset($aReferences[1])) {
|
||||
// Single cell in range
|
||||
sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($aReferences[0])) {
|
||||
$worksheet->setCellValue($aReferences[0], null);
|
||||
}
|
||||
@@ -2600,6 +2740,8 @@ class Calculation extends CalculationLocale
|
||||
foreach ($aReferences as $reference) {
|
||||
// Extract range
|
||||
sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($reference)) {
|
||||
$worksheet->setCellValue($reference, null);
|
||||
}
|
||||
@@ -2760,6 +2902,7 @@ class Calculation extends CalculationLocale
|
||||
// read constant value
|
||||
if (str_contains($constantName, '::')) {
|
||||
[$className, $constantName] = explode('::', $constantName);
|
||||
/** @var class-string $className */
|
||||
$constantReflector = new ReflectionClassConstant($className, $constantName);
|
||||
|
||||
return $constantReflector->getValue();
|
||||
@@ -2814,10 +2957,12 @@ class Calculation extends CalculationLocale
|
||||
$definedNameValue = $namedRange->getValue();
|
||||
$definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
|
||||
if ($definedNameType === 'Range') {
|
||||
if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) {
|
||||
$matches2 = trim($matches[2]);
|
||||
$matches2 = preg_replace('/ +/', ' ∩ ', $matches2) ?? $matches2;
|
||||
$matches2 = preg_replace('/,/', ' ∪ ', $matches2) ?? $matches2;
|
||||
if (Preg::isMatch('/^(.*!)?(.*)$/', $definedNameValue, $matches)) {
|
||||
$matches2 = Preg::replace(
|
||||
['/ +/', '/,/'],
|
||||
[' ∩ ', ' ∪ '],
|
||||
trim($matches[2])
|
||||
);
|
||||
$definedNameValue = $matches[1] . $matches2;
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
/**
|
||||
* A dedicated Calculation singleton for formula parsing only.
|
||||
*
|
||||
* This class provides an isolated instance specifically for parsing formulas
|
||||
* without branch pruning, avoiding state conflicts with the main Calculation
|
||||
* singleton used for cell value calculations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CalculationParserOnly extends Calculation
|
||||
{
|
||||
/**
|
||||
* Instance of this class.
|
||||
*/
|
||||
private static ?CalculationParserOnly $parserInstance = null;
|
||||
|
||||
/**
|
||||
* Branch pruning is disabled by default for parsing-only operations.
|
||||
*/
|
||||
protected bool $branchPruningEnabled = false;
|
||||
|
||||
/**
|
||||
* Get the singleton instance of this parser-only calculator.
|
||||
*/
|
||||
public static function getParserInstance(): self
|
||||
{
|
||||
if (!self::$parserInstance) {
|
||||
self::$parserInstance = new self();
|
||||
}
|
||||
|
||||
return self::$parserInstance;
|
||||
}
|
||||
|
||||
/** @param mixed $enabled Unused, property will always be false in this class */
|
||||
public function setBranchPruningEnabled(mixed $enabled): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Vendored
+2
-2
@@ -53,7 +53,7 @@ abstract class DatabaseAbstract
|
||||
|
||||
/** @var callable */
|
||||
$callable = 'strtoupper';
|
||||
$fieldNames = array_map($callable, array_shift($database)); //* @phpstan-ignore-line
|
||||
$fieldNames = array_map($callable, array_shift($database)); //* @phpstan-ignore argument.type (array_shift can return mixed not array?)
|
||||
if (is_numeric($field)) {
|
||||
$field = (int) $field - 1;
|
||||
if ($field < 0 || $field >= count($fieldNames)) {
|
||||
@@ -143,7 +143,7 @@ abstract class DatabaseAbstract
|
||||
}
|
||||
|
||||
$rowQuery = array_map(
|
||||
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore-line
|
||||
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore nullCoalesce.offset ($rowValue[0] always exists?)
|
||||
$baseQuery
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class Date
|
||||
* Excel Function:
|
||||
* DATE(year,month,day)
|
||||
*
|
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
|
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non-numeric values to this function.
|
||||
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
|
||||
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
|
||||
*
|
||||
|
||||
+14
-5
@@ -46,13 +46,22 @@ class DateValue
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
|
||||
}
|
||||
|
||||
return self::fromString2($dateValue, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* depending on the value of the ReturnDateType flag
|
||||
*/
|
||||
public static function fromString2(null|string|int|bool|float $dateValue, ?int $calendar = null): array|string|float|int|DateTime
|
||||
{
|
||||
// try to parse as date iff there is at least one digit
|
||||
if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
$dti = new DateTimeImmutable();
|
||||
$baseYear = SharedDateHelper::getExcelCalendar();
|
||||
$baseYear = $calendar ?? SharedDateHelper::getExcelCalendar();
|
||||
$dateValue = trim((string) $dateValue, '"');
|
||||
// Strip any ordinals because they're allowed in Excel (English only)
|
||||
$dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue);
|
||||
@@ -86,7 +95,7 @@ class DateValue
|
||||
return self::finalResults($PHPDateArray, $dti, $baseYear);
|
||||
}
|
||||
|
||||
/** @param mixed[] $t1 */
|
||||
/** @param list<float|int|string> $t1 */
|
||||
private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string
|
||||
{
|
||||
if (count($t1) == 2) {
|
||||
@@ -118,7 +127,7 @@ class DateValue
|
||||
if (!Helpers::dateParseSucceeded($PHPDateArray)) {
|
||||
// If original count was 1, we've already returned.
|
||||
// If it was 2, we added another.
|
||||
// Therefore, neither of the first 2 stroks below can fail.
|
||||
// Therefore, neither of the first 2 strtoks below can fail.
|
||||
$testVal1 = strtok($dateValue, '- ');
|
||||
$testVal2 = strtok('- ');
|
||||
$testVal3 = strtok('- ') ?: $dti->format('Y');
|
||||
@@ -160,9 +169,9 @@ class DateValue
|
||||
$day = self::getInt($PHPDateArray, 'day');
|
||||
$year = self::getInt($PHPDateArray, 'year');
|
||||
if (!checkdate($month, $day, $year)) {
|
||||
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE();
|
||||
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0, calendar: $baseYear) : ExcelError::VALUE();
|
||||
}
|
||||
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true);
|
||||
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true, calendar: $baseYear);
|
||||
}
|
||||
|
||||
return $retValue;
|
||||
|
||||
+3
-5
@@ -50,11 +50,9 @@ class Days
|
||||
|
||||
$days = ExcelError::VALUE();
|
||||
$diff = $PHPStartDateObject->diff($PHPEndDateObject);
|
||||
if (!is_bool($diff->days)) {
|
||||
$days = $diff->days;
|
||||
if ($diff->invert) {
|
||||
$days = -$days;
|
||||
}
|
||||
$days = $diff->days;
|
||||
if ($diff->invert) {
|
||||
$days = -$days;
|
||||
}
|
||||
|
||||
return $days;
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ class Days360
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of days between two dates based on a 360 day calendar.
|
||||
* Return the number of days between two dates based on a 360-day calendar.
|
||||
*/
|
||||
private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int
|
||||
{
|
||||
|
||||
+28
-19
@@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
|
||||
use Throwable;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
@@ -29,10 +30,10 @@ class Helpers
|
||||
*
|
||||
* @return float Excel date/time serial value
|
||||
*/
|
||||
public static function getDateValue(mixed $dateValue, bool $allowBool = true): float
|
||||
public static function getDateValue(mixed $dateValue, bool $allowBool = true, ?int $calendar = null): float
|
||||
{
|
||||
if (is_object($dateValue)) {
|
||||
$retval = SharedDateHelper::PHPToExcel($dateValue);
|
||||
$retval = SharedDateHelper::PHPToExcel($dateValue, calendar: $calendar);
|
||||
if (is_bool($retval)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
@@ -40,7 +41,7 @@ class Helpers
|
||||
return $retval;
|
||||
}
|
||||
|
||||
self::nullFalseTrueToNumber($dateValue, $allowBool);
|
||||
self::nullFalseTrueToNumber($dateValue, $allowBool, $calendar);
|
||||
if (!is_numeric($dateValue)) {
|
||||
$saveReturnDateType = Functions::getReturnDateType();
|
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
|
||||
@@ -56,6 +57,12 @@ class Helpers
|
||||
throw new Exception(ExcelError::NAN());
|
||||
}
|
||||
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject((float) $dateValue, calendar: $calendar);
|
||||
} catch (Throwable) {
|
||||
throw new Exception(ExcelError::NAN());
|
||||
}
|
||||
|
||||
return (float) $dateValue;
|
||||
}
|
||||
|
||||
@@ -80,10 +87,10 @@ class Helpers
|
||||
*
|
||||
* @param float|int $dateValue date to be adjusted
|
||||
*/
|
||||
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime
|
||||
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0, ?int $calendar = null): DateTime
|
||||
{
|
||||
// Execute function
|
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
|
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue, calendar: $calendar);
|
||||
$oMonth = (int) $PHPDateObject->format('m');
|
||||
$oYear = (int) $PHPDateObject->format('Y');
|
||||
|
||||
@@ -134,7 +141,7 @@ class Helpers
|
||||
*
|
||||
* @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray
|
||||
*/
|
||||
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int
|
||||
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false, ?int $calendar = null): DateTime|float|int
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
|
||||
@@ -154,58 +161,60 @@ class Helpers
|
||||
$dateArray['day'],
|
||||
$dateArray['hour'],
|
||||
$dateArray['minute'],
|
||||
$dateArray['second']
|
||||
$dateArray['second'],
|
||||
calendar: $calendar
|
||||
);
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return $noFrac ? floor($excelDateValue) : $excelDateValue;
|
||||
}
|
||||
// RETURNDATE_UNIX_TIMESTAMP)
|
||||
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue);
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result in one of three formats.
|
||||
*/
|
||||
public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime
|
||||
public static function returnIn3FormatsFloat(float $excelDateValue, ?int $calendar = null): float|int|DateTime
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return $excelDateValue;
|
||||
}
|
||||
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue);
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
// RETURNDATE_PHP_DATETIME_OBJECT
|
||||
|
||||
return SharedDateHelper::excelToDateTimeObject($excelDateValue);
|
||||
return SharedDateHelper::excelToDateTimeObject($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result in one of three formats.
|
||||
*/
|
||||
public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int
|
||||
public static function returnIn3FormatsObject(DateTime $PHPDateObject, ?int $calendar = null): DateTime|float|int
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
|
||||
return $PHPDateObject;
|
||||
}
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return (float) SharedDateHelper::PHPToExcel($PHPDateObject);
|
||||
return (float) SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
|
||||
}
|
||||
// RETURNDATE_UNIX_TIMESTAMP
|
||||
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject);
|
||||
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
|
||||
$stamp = is_bool($stamp) ? ((int) $stamp) : $stamp;
|
||||
|
||||
return SharedDateHelper::excelToTimestamp($stamp);
|
||||
return SharedDateHelper::excelToTimestamp($stamp, calendar: $calendar);
|
||||
}
|
||||
|
||||
private static function baseDate(): int
|
||||
private static function baseDate(?int $calendar): int
|
||||
{
|
||||
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
return 0;
|
||||
}
|
||||
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) {
|
||||
$calendar ??= SharedDateHelper::getExcelCalendar();
|
||||
if ($calendar === SharedDateHelper::CALENDAR_MAC_1904) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -215,10 +224,10 @@ class Helpers
|
||||
/**
|
||||
* Many functions accept null/false/true argument treated as 0/0/1.
|
||||
*/
|
||||
public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void
|
||||
public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true, ?int $calendar = null): void
|
||||
{
|
||||
$number = Functions::flattenSingleValue($number);
|
||||
$nullVal = self::baseDate();
|
||||
$nullVal = self::baseDate($calendar);
|
||||
if ($number === null) {
|
||||
$number = $nullVal;
|
||||
} elseif ($allowBool && is_bool($number)) {
|
||||
|
||||
+17
@@ -4,7 +4,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
|
||||
use Throwable;
|
||||
|
||||
class TimeParts
|
||||
{
|
||||
@@ -44,6 +46,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
@@ -85,6 +92,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
@@ -126,6 +138,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use Datetime;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
@@ -45,12 +45,12 @@ class TimeValue
|
||||
* Date information in time_text is ignored.
|
||||
* Or can be an array of date/time values
|
||||
*
|
||||
* @return array<mixed>|Datetime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* @return array<mixed>|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* depending on the value of the ReturnDateType flag
|
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function fromString(null|array|string|int|bool|float $timeValue): array|string|Datetime|int|float
|
||||
public static function fromString(null|array|string|int|bool|float $timeValue): array|string|DateTime|int|float
|
||||
{
|
||||
if (is_array($timeValue)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
|
||||
@@ -88,7 +88,7 @@ class TimeValue
|
||||
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
|
||||
$retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600;
|
||||
} else {
|
||||
$retValue = new Datetime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
|
||||
$retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -78,8 +78,8 @@ class Logger
|
||||
{
|
||||
// Only write the debug log if logging is enabled
|
||||
if ($this->writeDebugLog) {
|
||||
$message = sprintf($message, ...$args); //* @phpstan-ignore-line
|
||||
$cellReference = implode(' -> ', $this->cellStack->showStack());
|
||||
$message = sprintf($message, ...$args); //* @phpstan-ignore argument.type (...$args could contain items that sprintf can't handle)
|
||||
$cellReference = implode(' -> ', $this->cellStack->showStack()); //* @phpstan-ignore argument.type (showStack can return array<mixed> not array<string>)
|
||||
if ($this->echoDebugLog) {
|
||||
echo $cellReference,
|
||||
($this->cellStack->count() > 0 ? ' => ' : ''),
|
||||
|
||||
+6
-4
@@ -204,8 +204,9 @@ final class StructuredReference implements Operand, Stringable
|
||||
/** @var string $reference */
|
||||
$reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
|
||||
|
||||
foreach ($this->columns as $columnId => $columnName) {
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnName); //* @phpstan-ignore-line
|
||||
foreach ($this->columns as $columnId => $columnNamex) {
|
||||
/** @var string $columnNamex */
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnNamex);
|
||||
$reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
|
||||
}
|
||||
|
||||
@@ -338,8 +339,9 @@ final class StructuredReference implements Operand, Stringable
|
||||
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
|
||||
{
|
||||
$columnsSelected = false;
|
||||
foreach ($this->columns as $columnId => $columnName) {
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnName ?? ''); //* @phpstan-ignore-line
|
||||
foreach ($this->columns as $columnId => $columnNamex) {
|
||||
/** @var ?string $columnNamex */
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnNamex ?? '');
|
||||
$cellFrom = "{$columnId}{$startRow}";
|
||||
$cellTo = "{$columnId}{$endRow}";
|
||||
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
|
||||
|
||||
Vendored
+1
-1
@@ -150,7 +150,7 @@ class ConvertDecimal extends ConvertBase
|
||||
/**
|
||||
* toOctal.
|
||||
*
|
||||
* Return an decimal value as octal.
|
||||
* Return a decimal value as octal.
|
||||
*
|
||||
* Excel Function:
|
||||
* DEC2OCT(x[,places])
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class ErfC
|
||||
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument,
|
||||
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
|
||||
* improved, so that it can now calculate the function for both positive and negative x values.
|
||||
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments.
|
||||
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments.
|
||||
*
|
||||
* Excel Function:
|
||||
* ERFC(x)
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ class Interest
|
||||
* =ISPMT(interest_rate, period, number_payments, pv)
|
||||
*
|
||||
* @param mixed $interestRate is the interest rate for the investment
|
||||
* @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments.
|
||||
* @param mixed $period is the period to calculate the interest rate. It must be between 1 and number_payments.
|
||||
* @param mixed $numberOfPeriods is the number of payments for the annuity
|
||||
* @param mixed $principleRemaining is the loan amount or present value of the payments
|
||||
*/
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
|
||||
|
||||
class InterestAndPrincipal
|
||||
{
|
||||
protected float $interest;
|
||||
|
||||
protected float $principal;
|
||||
|
||||
public function __construct(
|
||||
float $rate = 0.0,
|
||||
int $period = 0,
|
||||
int $numberOfPeriods = 0,
|
||||
float $presentValue = 0,
|
||||
float $futureValue = 0,
|
||||
int $type = FinancialConstants::PAYMENT_END_OF_PERIOD
|
||||
) {
|
||||
$payment = Payments::annuity($rate, $numberOfPeriods, $presentValue, $futureValue, $type);
|
||||
$capital = $presentValue;
|
||||
$interest = 0.0;
|
||||
$principal = 0.0;
|
||||
for ($i = 1; $i <= $period; ++$i) {
|
||||
$interest = ($type === FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD && $i == 1) ? 0 : -$capital * $rate;
|
||||
$principal = (float) $payment - $interest;
|
||||
$capital += $principal;
|
||||
}
|
||||
|
||||
$this->interest = $interest;
|
||||
$this->principal = $principal;
|
||||
}
|
||||
|
||||
public function interest(): float
|
||||
{
|
||||
return $this->interest;
|
||||
}
|
||||
|
||||
public function principal(): float
|
||||
{
|
||||
return $this->principal;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -166,7 +166,7 @@ class NonPeriodic
|
||||
$foundneg = false;
|
||||
for ($i = 0; $i < $valCount; ++$i) {
|
||||
$fld = $values[$i];
|
||||
if (!is_numeric($fld)) { //* @phpstan-ignore-line
|
||||
if (!is_numeric($fld)) {
|
||||
return ExcelError::VALUE();
|
||||
} elseif ($fld > 0) {
|
||||
$foundpos = true;
|
||||
|
||||
+6
-15
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
|
||||
* software or the use or other dealings in the software.
|
||||
*
|
||||
* The following links are no longer valid.
|
||||
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
|
||||
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
|
||||
*
|
||||
* @deprecated 5.5.0 No replacement.
|
||||
*/
|
||||
class FormulaParser
|
||||
{
|
||||
@@ -463,16 +466,8 @@ class FormulaParser
|
||||
$tokenCount = count($tokens1);
|
||||
for ($i = 0; $i < $tokenCount; ++$i) {
|
||||
$token = $tokens1[$i];
|
||||
if (isset($tokens1[$i - 1])) {
|
||||
$previousToken = $tokens1[$i - 1];
|
||||
} else {
|
||||
$previousToken = null;
|
||||
}
|
||||
if (isset($tokens1[$i + 1])) {
|
||||
$nextToken = $tokens1[$i + 1];
|
||||
} else {
|
||||
$nextToken = null;
|
||||
}
|
||||
$previousToken = $tokens1[$i - 1] ?? null;
|
||||
$nextToken = $tokens1[$i + 1] ?? null;
|
||||
|
||||
if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
|
||||
$tokens2[] = $token;
|
||||
@@ -518,11 +513,7 @@ class FormulaParser
|
||||
$tokenCount = count($tokens2);
|
||||
for ($i = 0; $i < $tokenCount; ++$i) {
|
||||
$token = $tokens2[$i];
|
||||
if (isset($tokens2[$i - 1])) {
|
||||
$previousToken = $tokens2[$i - 1];
|
||||
} else {
|
||||
$previousToken = null;
|
||||
}
|
||||
$previousToken = $tokens2[$i - 1] ?? null;
|
||||
|
||||
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
|
||||
if ($i == 0) {
|
||||
|
||||
+3
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
|
||||
* software or the use or other dealings in the software.
|
||||
*
|
||||
* The following links are no longer valid.
|
||||
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
|
||||
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
|
||||
*
|
||||
* @deprecated 5.5.0 No replacement.
|
||||
*/
|
||||
class FormulaToken
|
||||
{
|
||||
|
||||
+15
-12
@@ -9,7 +9,7 @@ class FunctionArray extends CalculationBase
|
||||
*
|
||||
* @var array<string, array{category: string, functionCall: string|string[], argumentCount: string, passCellReference?: bool, passByReference?: bool[], custom?: bool}>
|
||||
*/
|
||||
protected static array $phpSpreadsheetFunctions = [
|
||||
protected static array $phpSpreadsheetFunctions = [ // @phpstan-ignore property.defaultValue (Phpstan is wrong)
|
||||
'ABS' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
'functionCall' => [MathTrig\Absolute::class, 'evaluate'],
|
||||
@@ -149,7 +149,7 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'BAHTTEXT' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [TextData\Thai::class, 'getBahtText'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'BASE' => [
|
||||
@@ -405,6 +405,7 @@ class FunctionArray extends CalculationBase
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
|
||||
'argumentCount' => '1+',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'CONFIDENCE' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
@@ -919,7 +920,7 @@ class FunctionArray extends CalculationBase
|
||||
'FLOOR' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
'functionCall' => [MathTrig\Floor::class, 'floor'],
|
||||
'argumentCount' => '1-2', // Excel requries 2, Ods/Gnumeric 1-2
|
||||
'argumentCount' => '1-2', // Excel requires 2, Ods/Gnumeric 1-2
|
||||
],
|
||||
'FLOOR.MATH' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
@@ -1284,8 +1285,9 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'INFO' => [
|
||||
'category' => Category::CATEGORY_INFORMATION,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Information\Info::class, 'getInfo'],
|
||||
'argumentCount' => '1',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'INT' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
@@ -2335,17 +2337,17 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'T.DIST' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDist'],
|
||||
'argumentCount' => '3',
|
||||
],
|
||||
'T.DIST.2T' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDot2T'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'T.DIST.RT' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDotRT'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'TEXT' => [
|
||||
@@ -2425,12 +2427,12 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'T.INV' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotInv'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'T.INV.2T' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'TODAY' => [
|
||||
@@ -2495,12 +2497,12 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'UNICHAR' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'character'],
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'characterUnicode'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'UNICODE' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'code'],
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'codeUnicode'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'UNIQUE' => [
|
||||
@@ -2577,6 +2579,7 @@ class FunctionArray extends CalculationBase
|
||||
'category' => Category::CATEGORY_WEB,
|
||||
'functionCall' => [Web\Service::class, 'webService'],
|
||||
'argumentCount' => '1',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'WEEKDAY' => [
|
||||
'category' => Category::CATEGORY_DATE_AND_TIME,
|
||||
@@ -2625,7 +2628,7 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'XLOOKUP' => [
|
||||
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [LookupRef\XLookup::class, 'lookup'],
|
||||
'argumentCount' => '3-6',
|
||||
],
|
||||
'XNPV' => [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
@@ -360,4 +361,47 @@ class Functions
|
||||
|
||||
return $coordinate;
|
||||
}
|
||||
|
||||
/** @param mixed[] $array */
|
||||
public static function convertArrayToCellRange(array $array): string
|
||||
{
|
||||
$retVal = '';
|
||||
$lastRow = $lastColumn = $firstRow = $firstColumn = 0;
|
||||
foreach ($array as $rowkey => $row) {
|
||||
if (!is_array($row) || !is_int($rowkey) || $rowkey < 1) {
|
||||
$firstRow = 0;
|
||||
|
||||
break;
|
||||
}
|
||||
if ($firstRow > $rowkey || $firstRow === 0) {
|
||||
$firstRow = $rowkey;
|
||||
}
|
||||
if ($lastRow < $rowkey) {
|
||||
$lastRow = $rowkey;
|
||||
}
|
||||
foreach ($row as $colkey => $cellValue) {
|
||||
if (!preg_match('/^[A-Z]{1,3}$/', $colkey)) {
|
||||
$firstRow = 0;
|
||||
|
||||
break 2;
|
||||
}
|
||||
$column = Coordinate::columnIndexFromString($colkey);
|
||||
if ($firstColumn > $column || $firstColumn === 0) {
|
||||
$firstColumn = $column;
|
||||
}
|
||||
if ($lastColumn < $column) {
|
||||
$lastColumn = $column;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($firstRow > 0 && $firstColumn > 0 && ($firstRow !== $lastRow || $firstColumn !== $lastColumn)) {
|
||||
$retVal = Coordinate::stringFromColumnIndex($firstColumn)
|
||||
. $firstRow
|
||||
. ':'
|
||||
. Coordinate::stringFromColumnIndex($lastColumn)
|
||||
. $lastRow;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
|
||||
class Info
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static bool $infoSupported = true;
|
||||
|
||||
/**
|
||||
* INFO.
|
||||
*
|
||||
* Excel Function:
|
||||
* =INFO(type_text)
|
||||
*
|
||||
* @param mixed $typeText String specifying the type of information to be returned
|
||||
* @param ?Cell $cell Cell from which spreadsheet information is retrieved
|
||||
*
|
||||
* @return int|string The requested information about the current operating environment
|
||||
*/
|
||||
public static function getInfo(mixed $typeText = '', ?Cell $cell = null): int|string
|
||||
{
|
||||
if (!self::$infoSupported) {
|
||||
return Functions::DUMMY();
|
||||
}
|
||||
|
||||
return match (is_string($typeText) ? strtolower($typeText) : $typeText) {
|
||||
'directory' => '/',
|
||||
'numfile' => $cell?->getWorksheetOrNull()?->getParent()?->getSheetCount() ?? 1,
|
||||
'origin' => '$A:$A$1',
|
||||
'osversion' => 'PHP ' . PHP_VERSION,
|
||||
'recalc' => 'Automatic',
|
||||
'release' => PHP_VERSION,
|
||||
'system' => 'PHP',
|
||||
'memavail', 'memused', 'totmem' => ExcelError::NA(),
|
||||
default => ExcelError::VALUE(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -52,8 +52,10 @@ class Value
|
||||
if (!empty($worksheet) && $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheet) === null) {
|
||||
return false;
|
||||
}
|
||||
[$column, $row] = Coordinate::indexesFromString($cellValue ?? '');
|
||||
if ($column > 16384 || $row > 1048576) {
|
||||
|
||||
try {
|
||||
[$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); //* @phpstan-ignore nullCoalesce.variable (Not sure whether Phpstan is correct)
|
||||
} catch (SpreadsheetException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ class Value
|
||||
if (is_bool($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (is_string($value) && substr($value, 0, 1) === '#') {
|
||||
if (is_string($value) && str_starts_with($value, '#')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class Conditional
|
||||
{
|
||||
$condition = ($condition === null) ? true : Functions::flattenSingleValue($condition);
|
||||
|
||||
if (ErrorValue::isError($condition)) {
|
||||
if (ErrorValue::isError($condition, true)) {
|
||||
return $condition;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class ChooseRowsEtc
|
||||
*/
|
||||
public static function transpose(array $array): array
|
||||
{
|
||||
return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore-line
|
||||
return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore argument.type (phstan is wrong), argument.type (phpstan is wrong)
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ class ExcelMatch
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
// MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type.
|
||||
// MATCH() is not case-sensitive, so we convert lookup value to be lower cased if it's a string type.
|
||||
if (is_string($lookupValue)) {
|
||||
$lookupValue = StringHelper::strToLower($lookupValue);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class ExcelMatch
|
||||
};
|
||||
|
||||
if ($valueKey !== null) {
|
||||
return ++$valueKey; //* @phpstan-ignore-line
|
||||
return ++$valueKey; //* @phpstan-ignore preInc.type ($valueKey can be mixed not numeric)
|
||||
}
|
||||
|
||||
// Unsuccessful in finding a match, return #N/A error value
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Filter
|
||||
*/
|
||||
private static function filterByRow(array $lookupArray, array $matchArray): array
|
||||
{
|
||||
$matchArray = array_values(array_column($matchArray, 0)); // @phpstan-ignore-line
|
||||
$matchArray = array_values(array_column($matchArray, 0));
|
||||
|
||||
return array_filter(
|
||||
array_values($lookupArray),
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Hstack
|
||||
}
|
||||
}
|
||||
|
||||
$transpose = array_map(null, ...$inputData); //* @phpstan-ignore-line
|
||||
$transpose = array_map(null, ...$inputData); //* @phpstan-ignore argument.type (inputData is mixed not array)
|
||||
$returnMatrix = [];
|
||||
foreach ($transpose as $array) {
|
||||
$returnMatrix[] = Functions::flattenArray($array);
|
||||
|
||||
+13
-7
@@ -23,6 +23,13 @@ class Hyperlink
|
||||
*/
|
||||
public static function set(mixed $linkURL = '', mixed $displayName = null, ?Cell $cell = null): string
|
||||
{
|
||||
$worksheet = null;
|
||||
$coordinate = '';
|
||||
if ($cell !== null) {
|
||||
$coordinate = $cell->getCoordinate();
|
||||
$worksheet = $cell->getWorksheetOrNull();
|
||||
}
|
||||
|
||||
$linkURL = ($linkURL === null) ? '' : StringHelper::convertToString(Functions::flattenSingleValue($linkURL));
|
||||
$displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
|
||||
|
||||
@@ -30,17 +37,16 @@ class Hyperlink
|
||||
return ExcelError::REF();
|
||||
}
|
||||
|
||||
if (is_object($displayName)) {
|
||||
$displayName = $linkURL;
|
||||
}
|
||||
$displayName = StringHelper::convertToString($displayName);
|
||||
$displayName = StringHelper::convertToString($displayName, false);
|
||||
if (trim($displayName) === '') {
|
||||
$displayName = $linkURL;
|
||||
}
|
||||
|
||||
$cell->getHyperlink()
|
||||
->setUrl($linkURL);
|
||||
$cell->getHyperlink()->setTooltip($displayName);
|
||||
$worksheet?->getCell($coordinate)
|
||||
->getHyperlink()
|
||||
->setUrl($linkURL)
|
||||
->setTooltip($displayName)
|
||||
->setDisplay('');
|
||||
|
||||
return $displayName;
|
||||
}
|
||||
|
||||
+2
-2
@@ -106,14 +106,14 @@ class Indirect
|
||||
*/
|
||||
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
|
||||
{
|
||||
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
|
||||
return Calculation::getInstance($worksheet?->getParent())
|
||||
->extractCellRange($cellAddress, $worksheet, false, createCell: true);
|
||||
}
|
||||
|
||||
private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string
|
||||
{
|
||||
// Being lazy, we're only checking a single row/column to get the max
|
||||
if (ctype_digit($start) && $start <= 1048576) {
|
||||
if (ctype_digit($start) && $start <= AddressRange::MAX_ROW) {
|
||||
// Max 16,384 columns for Excel2007
|
||||
$endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : AddressRange::MAX_COLUMN;
|
||||
|
||||
|
||||
+6
-5
@@ -40,15 +40,15 @@ class Lookup
|
||||
$lookupColumns = self::columnCount($lookupVector);
|
||||
}
|
||||
|
||||
$resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore-line
|
||||
$resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore argument.type (resultVector can be mixed not array<array<mixed>>)
|
||||
|
||||
if ($lookupRows === 2 && !$hasResultVector) {
|
||||
$resultVector = array_pop($lookupVector);
|
||||
$lookupVector = array_shift($lookupVector);
|
||||
}
|
||||
|
||||
/** @var mixed[] $lookupVector */
|
||||
/** @var mixed[] $resultVector */
|
||||
/** @var array<int, mixed> $lookupVector */
|
||||
/** @var array<int, mixed> $resultVector */
|
||||
if ($lookupColumns !== 2) {
|
||||
$lookupVector = self::verifyLookupValues($lookupVector, $resultVector);
|
||||
}
|
||||
@@ -57,8 +57,8 @@ class Lookup
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $lookupVector
|
||||
* @param mixed[] $resultVector
|
||||
* @param array<int, mixed> $lookupVector
|
||||
* @param array<int, mixed> $resultVector
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
@@ -80,6 +80,7 @@ class Lookup
|
||||
if (is_array($dataValue2)) {
|
||||
$dataValue2 = array_shift($dataValue2);
|
||||
}
|
||||
/** @var int $key2 */
|
||||
$value = [$key1 => $dataValue1, $key2 => $dataValue2];
|
||||
}
|
||||
unset($value);
|
||||
|
||||
Vendored
+1
-1
@@ -11,7 +11,7 @@ class LookupRefValidations
|
||||
public static function validateInt(mixed $value): int
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
if (is_string($value) && ErrorValue::isError($value)) {
|
||||
if (is_string($value) && ErrorValue::isError($value, true)) {
|
||||
throw new Exception($value);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -125,7 +125,7 @@ class Matrix
|
||||
}
|
||||
|
||||
$rowKeys = array_keys($matrix);
|
||||
$columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore-line
|
||||
$columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore argument.type (argument 1 of array_keys may be mixed not array)
|
||||
|
||||
if ($columnNum > count($columnKeys)) {
|
||||
return ExcelError::REF();
|
||||
@@ -135,14 +135,14 @@ class Matrix
|
||||
return self::extractRowValue($matrix, $rowKeys, $rowNum);
|
||||
}
|
||||
|
||||
$columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore-line
|
||||
$columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$columnNum does not exist)
|
||||
if ($rowNum === 0) {
|
||||
return array_map(
|
||||
fn ($value): array => [$value],
|
||||
array_column($matrix, $columnNum)
|
||||
);
|
||||
}
|
||||
$rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore-line
|
||||
$rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$rowNum does not exist)
|
||||
/** @var mixed[][] $matrix */
|
||||
|
||||
return $matrix[$rowNum][$columnNum];
|
||||
@@ -150,7 +150,7 @@ class Matrix
|
||||
|
||||
/**
|
||||
* @param mixed[] $matrix
|
||||
* @param mixed[] $rowKeys
|
||||
* @param array<int, int> $rowKeys
|
||||
*/
|
||||
private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum): mixed
|
||||
{
|
||||
@@ -159,7 +159,7 @@ class Matrix
|
||||
}
|
||||
|
||||
$rowNum = $rowKeys[--$rowNum];
|
||||
$row = $matrix[$rowNum]; //* @phpstan-ignore-line
|
||||
$row = $matrix[$rowNum];
|
||||
if (is_array($row)) {
|
||||
return [$rowNum => $row];
|
||||
}
|
||||
|
||||
+3
-3
@@ -84,7 +84,7 @@ class Offset
|
||||
$endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns);
|
||||
$startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
|
||||
|
||||
$endCellRow = self::adustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
|
||||
$endCellRow = self::adjustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
|
||||
|
||||
if (($endCellRow <= 0) || ($endCellColumn < 0)) {
|
||||
return ExcelError::REF();
|
||||
@@ -102,7 +102,7 @@ class Offset
|
||||
/** @return mixed[] */
|
||||
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
|
||||
{
|
||||
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
|
||||
return Calculation::getInstance($worksheet?->getParent())
|
||||
->extractCellRange($cellAddress, $worksheet, false);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class Offset
|
||||
* @param null|object|scalar $height
|
||||
* @param scalar $rows
|
||||
*/
|
||||
private static function adustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
|
||||
private static function adjustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
|
||||
{
|
||||
if (($height !== null) && (!is_object($height))) {
|
||||
$endCellRow = $startCellRow + (int) $height - 1;
|
||||
|
||||
Vendored
+4
-3
@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
@@ -106,7 +107,7 @@ class RowColumnInformation
|
||||
if (self::cellAddressNullOrWhitespace($cellAddress)) {
|
||||
return 1;
|
||||
}
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
|
||||
return $cellAddress;
|
||||
}
|
||||
if (!is_array($cellAddress)) {
|
||||
@@ -131,7 +132,7 @@ class RowColumnInformation
|
||||
|
||||
private static function convert0ToName(int|string $result): int|string
|
||||
{
|
||||
if (is_int($result) && ($result <= 0 || $result > 1048576)) {
|
||||
if (is_int($result) && ($result <= 0 || $result > AddressRange::MAX_ROW)) {
|
||||
return ExcelError::NAME();
|
||||
}
|
||||
|
||||
@@ -211,7 +212,7 @@ class RowColumnInformation
|
||||
if (self::cellAddressNullOrWhitespace($cellAddress)) {
|
||||
return 1;
|
||||
}
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
|
||||
return $cellAddress;
|
||||
}
|
||||
if (!is_array($cellAddress)) {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Selection
|
||||
} else {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
$chosenEntry = floor($chosenEntry);
|
||||
$chosenEntry = (int) floor($chosenEntry);
|
||||
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
+57
-21
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
@@ -20,6 +19,12 @@ class Sort extends LookupRefValidations
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
*
|
||||
* NOTE: If $sortArray contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @param mixed $sortIndex The column or row number within the sortArray to sort on
|
||||
* @param mixed $sortOrder Flag indicating whether to sort ascending or descending
|
||||
@@ -32,8 +37,7 @@ class Sort extends LookupRefValidations
|
||||
public static function sort(mixed $sortArray, mixed $sortIndex = 1, mixed $sortOrder = self::ORDER_ASCENDING, mixed $byColumn = false): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
$sortArray = [[$sortArray]];
|
||||
}
|
||||
|
||||
/** @var mixed[][] */
|
||||
@@ -55,7 +59,7 @@ class Sort extends LookupRefValidations
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
// We want a simple, enumrated array of arrays where we can reference column by its index number.
|
||||
// We want a simple, enumerated array of arrays where we can reference column by its index number.
|
||||
/** @var callable(mixed): mixed */
|
||||
$temp = 'array_values';
|
||||
/** @var array<int> $sortOrder */
|
||||
@@ -72,6 +76,18 @@ class Sort extends LookupRefValidations
|
||||
* The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array.
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
* Microsoft doesn't even bother documenting that a column sort
|
||||
* is possible. However, it is. According to:
|
||||
* https://exceljet.net/functions/sortby-function
|
||||
* When by_array is a horizontal range, SORTBY sorts horizontally by columns.
|
||||
* My interpretation of this is that by_array must be an
|
||||
* array which contains exactly one row.
|
||||
*
|
||||
* NOTE: If the "byArray" contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @param mixed $args
|
||||
@@ -87,8 +103,16 @@ class Sort extends LookupRefValidations
|
||||
public static function sortBy(mixed $sortArray, mixed ...$args): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
$sortArray = [[$sortArray]];
|
||||
}
|
||||
$transpose = false;
|
||||
$args0 = $args[0] ?? null;
|
||||
if (is_array($args0) && count($args0) === 1) {
|
||||
$args0 = reset($args0);
|
||||
if (is_array($args0) && count($args0) > 1) {
|
||||
$transpose = true;
|
||||
$sortArray = Matrix::transpose($sortArray);
|
||||
}
|
||||
}
|
||||
|
||||
$sortArray = self::enumerateArrayKeys($sortArray);
|
||||
@@ -99,14 +123,23 @@ class Sort extends LookupRefValidations
|
||||
try {
|
||||
$sortBy = $sortOrder = [];
|
||||
for ($i = 0; $i < $argumentCount; $i += 2) {
|
||||
$sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
|
||||
$argsI = $args[$i];
|
||||
if (!is_array($argsI)) {
|
||||
$argsI = [[$argsI]];
|
||||
}
|
||||
$sortBy[] = self::validateSortVector($argsI, $lookupArraySize);
|
||||
$sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
return self::processSortBy($sortArray, $sortBy, $sortOrder);
|
||||
$temp = self::processSortBy($sortArray, $sortBy, $sortOrder);
|
||||
if ($transpose) {
|
||||
$temp = Matrix::transpose($temp);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,10 +163,6 @@ class Sort extends LookupRefValidations
|
||||
|
||||
private static function validateScalarArgumentsForSort(mixed &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
|
||||
{
|
||||
if (is_array($sortIndex) || is_array($sortOrder)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
$sortIndex = self::validatePositiveInt($sortIndex, false);
|
||||
|
||||
if ($sortIndex > $sortArraySize) {
|
||||
@@ -143,13 +172,13 @@ class Sort extends LookupRefValidations
|
||||
$sortOrder = self::validateSortOrder($sortOrder);
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
private static function validateSortVector(mixed $sortVector, int $sortArraySize): array
|
||||
/**
|
||||
* @param mixed[] $sortVector
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private static function validateSortVector(array $sortVector, int $sortArraySize): array
|
||||
{
|
||||
if (!is_array($sortVector)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
// It doesn't matter if it's a row or a column vectors, it works either way
|
||||
$sortVector = Functions::flattenArray($sortVector);
|
||||
if (count($sortVector) !== $sortArraySize) {
|
||||
@@ -203,12 +232,19 @@ class Sort extends LookupRefValidations
|
||||
*/
|
||||
private static function prepareSortVectorValues(array $sortVector): array
|
||||
{
|
||||
// Strings should be sorted case-insensitive; with booleans converted to locale-strings
|
||||
// Strings should be sorted case-insensitive.
|
||||
// Booleans are a complete mess. Excel always seems to sort
|
||||
// booleans in a mixed vector at either the top or the bottom,
|
||||
// so converting them to string or int doesn't really work.
|
||||
// Best advice is to use them in a boolean-only vector.
|
||||
// Code below chooses int conversion, which is sensible,
|
||||
// and, as a bonus, compatible with LibreOffice.
|
||||
return array_map(
|
||||
function ($value) {
|
||||
if (is_bool($value)) {
|
||||
return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
|
||||
} elseif (is_string($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return StringHelper::strToLower($value);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class TorowTocol
|
||||
continue;
|
||||
}
|
||||
$cell = 0;
|
||||
} elseif (ErrorValue::isError($cell)) {
|
||||
} elseif (ErrorValue::isError($cell, true)) {
|
||||
if ($ignore === 2 || $ignore === 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ class Unique
|
||||
// so implode each row into a single string value
|
||||
array_walk(
|
||||
$lookupVector,
|
||||
//* @phpstan-ignore-next-line
|
||||
// @phpstan-ignore argument.type (not sure what is needed)
|
||||
function (array &$value): void {
|
||||
$valuex = '';
|
||||
$separator = '';
|
||||
@@ -79,7 +79,7 @@ class Unique
|
||||
if (str_ends_with($stringValue, "\x01")) {
|
||||
// x01 should only end a string which is otherwise a float or int,
|
||||
// so phpstan is technically correct but what it fears should not happen.
|
||||
$stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
|
||||
$stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore binaryOp.invalid (stringValue might not be numeric?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class Vstack
|
||||
if (!is_array($matrix)) {
|
||||
$count = 1;
|
||||
} else {
|
||||
$count = count(reset($matrix)); //* @phpstan-ignore-line
|
||||
$count = count(reset($matrix)); //* @phpstan-ignore argument.type (reset can theoretically return mixed not array)
|
||||
}
|
||||
$columns = max($columns, $count);
|
||||
}
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use UnhandledMatchError;
|
||||
|
||||
class XLookup extends LookupBase
|
||||
{
|
||||
//use ArrayEnabled; // not yet supported
|
||||
|
||||
/**
|
||||
* XLOOKUP — PHP emulation of Excel's XLOOKUP function.
|
||||
*
|
||||
* @param mixed $lookupValue Value to search for
|
||||
* @param mixed $lookupArray Expect array, Array to search in
|
||||
* @param mixed $returnArray Expect array, Array to return from (must match lookupArray size)
|
||||
* @param mixed $ifNotFound Value to return if no match found (default: #N/A!)
|
||||
* @param mixed $matchMode expect int 0 = exact match (default)
|
||||
* -1 = exact or next smaller
|
||||
* 1 = exact or next larger
|
||||
* 2 = wildcard match (* ? ~)
|
||||
* @param mixed $searchMode expect int 1 = first to last (default)
|
||||
* -1 = last to first
|
||||
* 2 = binary search ascending
|
||||
* -2 = binary search descending
|
||||
*/
|
||||
public static function lookup(
|
||||
mixed $lookupValue,
|
||||
mixed $lookupArray,
|
||||
mixed $returnArray,
|
||||
mixed $ifNotFound = '#N/A!',
|
||||
mixed $matchMode = 0,
|
||||
mixed $searchMode = 1
|
||||
): mixed {
|
||||
if (is_array($lookupValue)) {
|
||||
$lookupValue = Functions::flattenArray($lookupValue);
|
||||
if (count($lookupValue) === 1) {
|
||||
$lookupValue = reset($lookupValue);
|
||||
}
|
||||
}
|
||||
if (is_array($lookupValue)) {
|
||||
$result = [];
|
||||
foreach ($lookupValue as $value) {
|
||||
$result[] = self::lookup($value, $lookupArray, $returnArray, $ifNotFound, $matchMode, $searchMode);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
if (!is_array($lookupArray)) {
|
||||
$lookupArray = [$lookupArray];
|
||||
}
|
||||
if (!is_array($returnArray)) {
|
||||
$returnArray = [$returnArray];
|
||||
}
|
||||
$lookupArray = Functions::flattenArray($lookupArray);
|
||||
if (count($returnArray) === 1) {
|
||||
$returnArray = Functions::flattenArray($returnArray);
|
||||
} else {
|
||||
$oldArray = $returnArray;
|
||||
$returnArray = [];
|
||||
foreach ($oldArray as $row) {
|
||||
$newRow = Functions::flattenArray($row);
|
||||
if (count($newRow) === 1) {
|
||||
$newRow = reset($newRow);
|
||||
}
|
||||
$returnArray[] = $newRow;
|
||||
}
|
||||
}
|
||||
/*if (is_array($lookupValue)) { // not yet supported by ArrayEnabled
|
||||
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $returnArray, $ifNotFound, $matchMode, $searchMode);
|
||||
}*/
|
||||
|
||||
try {
|
||||
self::validateLookupArray($lookupArray);
|
||||
self::validateLookupArray($returnArray);
|
||||
$matchMode = LookupRefValidations::validateInt($matchMode);
|
||||
$searchMode = LookupRefValidations::validateInt($searchMode);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
if (!in_array($matchMode, [0, -1, 1, 2], true)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
if (count($lookupArray) !== count($returnArray)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
try {
|
||||
$index = match ($searchMode) {
|
||||
1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, false),
|
||||
-1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, true),
|
||||
2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, true),
|
||||
-2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, false),
|
||||
};
|
||||
} catch (UnhandledMatchError) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
return ($index === null) ? $ifNotFound : $returnArray[$index];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search strategies
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Linear search (searchMode 1 and -1).
|
||||
*
|
||||
* @param mixed[] $lookupArray
|
||||
*/
|
||||
private static function searchLinear(
|
||||
mixed $lookupValue,
|
||||
array $lookupArray,
|
||||
int $matchMode,
|
||||
bool $reverse
|
||||
): ?int {
|
||||
|
||||
$keys = array_keys($lookupArray);
|
||||
if ($reverse) {
|
||||
$keys = array_reverse($keys);
|
||||
}
|
||||
|
||||
$bestIdx = null;
|
||||
$bestVal = null;
|
||||
|
||||
foreach ($keys as $i) {
|
||||
/** @var scalar */
|
||||
$candidate = $lookupArray[$i];
|
||||
|
||||
if ($matchMode === 2) {
|
||||
// Wildcard: convert Excel wildcards to PHP regex
|
||||
/** @var scalar $lookupValue */
|
||||
if (self::wildcardMatch((string) $lookupValue, (string) $candidate)) {
|
||||
return $i;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$cmp = self::compareValues($candidate, $lookupValue);
|
||||
|
||||
if ($cmp === 0) {
|
||||
return $i; // Exact match — return immediately
|
||||
}
|
||||
|
||||
if ($matchMode === -1 && $cmp < 0) {
|
||||
// Next smaller: track largest value still below lookupValue
|
||||
if ($bestVal === null || self::compareValues($candidate, $bestVal) > 0) {
|
||||
$bestVal = $candidate;
|
||||
$bestIdx = $i;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchMode === 1 && $cmp > 0) {
|
||||
// Next larger: track smallest value still above lookupValue
|
||||
if ($bestVal === null || self::compareValues($candidate, $bestVal) < 0) {
|
||||
$bestVal = $candidate;
|
||||
$bestIdx = $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @var ?int $bestIdx */
|
||||
|
||||
return $bestIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary search (searchMode 2 and -2)
|
||||
* Assumes array is sorted ascending (searchMode 2) or descending (searchMode -2).
|
||||
*
|
||||
* @param mixed[] $lookupArray
|
||||
*/
|
||||
private static function searchBinary(
|
||||
mixed $lookupValue,
|
||||
array $lookupArray,
|
||||
int $matchMode,
|
||||
bool $ascending
|
||||
): ?int {
|
||||
|
||||
$values = array_values($lookupArray);
|
||||
$keys = array_keys($lookupArray);
|
||||
$lo = 0;
|
||||
$hi = count($values) - 1;
|
||||
$bestIdx = null;
|
||||
|
||||
while ($lo <= $hi) {
|
||||
$mid = intdiv($lo + $hi, 2);
|
||||
$cmp = self::compareValues($values[$mid], $lookupValue);
|
||||
|
||||
if (!$ascending) {
|
||||
$cmp = -$cmp; // Flip for descending
|
||||
}
|
||||
|
||||
if ($cmp === 0) {
|
||||
return $keys[$mid]; // Exact match
|
||||
}
|
||||
|
||||
if ($cmp < 0) {
|
||||
if ($matchMode === -1) {
|
||||
$bestIdx = $keys[$mid]; // Candidate for next smaller
|
||||
}
|
||||
$lo = $mid + 1;
|
||||
} else {
|
||||
if ($matchMode === 1) {
|
||||
$bestIdx = $keys[$mid]; // Candidate for next larger
|
||||
}
|
||||
$hi = $mid - 1;
|
||||
}
|
||||
}
|
||||
/** @var int $bestIdx */
|
||||
|
||||
return ($matchMode !== 0) ? $bestIdx : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values with type coercion matching Excel's behaviour:
|
||||
* numbers < strings < booleans
|
||||
*/
|
||||
private static function compareValues(mixed $a, mixed $b): int
|
||||
{
|
||||
// Numeric comparison
|
||||
if (is_numeric($a) && is_numeric($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
// String comparison (case-insensitive, like Excel)
|
||||
if (is_string($a) && is_string($b)) {
|
||||
return strcasecmp($a, $b);
|
||||
}
|
||||
// Bool comparison
|
||||
if (is_bool($a) && is_bool($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
// Cross-type: number < string < bool
|
||||
$typeOrder = fn ($v) => match (true) {
|
||||
is_numeric($v) => 0,
|
||||
is_string($v) => 1,
|
||||
is_bool($v) => 2,
|
||||
default => 3,
|
||||
};
|
||||
|
||||
return $typeOrder($a) <=> $typeOrder($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard match (matchMode 2)
|
||||
* Supports Excel wildcards: * (any sequence), ? (any single char), ~ (escape).
|
||||
*/
|
||||
private static function wildcardMatch(string $pattern, string $subject): bool
|
||||
{
|
||||
// Handle ~* and ~? escapes first
|
||||
$regex = '';
|
||||
$len = strlen($pattern);
|
||||
for ($i = 0; $i < $len; ++$i) {
|
||||
$ch = $pattern[$i];
|
||||
if ($ch === '~' && $i + 1 < $len) {
|
||||
$next = $pattern[++$i];
|
||||
$regex .= preg_quote($next, '/');
|
||||
} elseif ($ch === '*') {
|
||||
$regex .= '.*';
|
||||
} elseif ($ch === '?') {
|
||||
$regex .= '.';
|
||||
} else {
|
||||
$regex .= preg_quote($ch, '/');
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) preg_match('/^' . $regex . '$/i', $subject);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ class Arabic
|
||||
*
|
||||
* @param string|string[] $roman Should be a string, or can be an array of strings
|
||||
*
|
||||
* @return array<mixed>|int|string the arabic numberal contrived from the roman numeral
|
||||
* @return array<mixed>|int|string the arabic numeral contrived from the roman numeral
|
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array
|
||||
* with the same dimensions
|
||||
*/
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ class Round
|
||||
return round(
|
||||
(float) (string) $number,
|
||||
$digits,
|
||||
RoundingMode::AwayFromZero //* @phpstan-ignore-line
|
||||
RoundingMode::AwayFromZero //* @phpstan-ignore argument.type (phpstan error till we no longer support Php < 8.4)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Round
|
||||
return round(
|
||||
(float) (string) $number,
|
||||
$digits,
|
||||
RoundingMode::TowardsZero //* @phpstan-ignore-line
|
||||
RoundingMode::TowardsZero
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+25
-1
@@ -32,6 +32,25 @@ class Subtotal
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected static function filterFilteredArgs(Cell $cellReference, array $args): array
|
||||
{
|
||||
return array_filter(
|
||||
$args,
|
||||
function ($index) use ($cellReference) {
|
||||
$explodeArray = explode('.', $index);
|
||||
$row = $explodeArray[1] ?? '';
|
||||
|
||||
return is_numeric($row) ? ($cellReference->getWorksheet()->getRowDimension((int) $row)->getVisibleAfterFilter()) : true;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
@@ -125,13 +144,18 @@ class Subtotal
|
||||
if ($subtotal > 100) {
|
||||
$aArgs = self::filterHiddenArgs($cellReference, $aArgs);
|
||||
$subtotal -= 100;
|
||||
} else {
|
||||
$aArgs = self::filterFilteredArgs($cellReference, $aArgs);
|
||||
}
|
||||
|
||||
$aArgs = self::filterFormulaArgs($cellReference, $aArgs);
|
||||
if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) {
|
||||
$call = self::CALL_FUNCTIONS[$subtotal];
|
||||
|
||||
return call_user_func_array($call, $aArgs); //* @phpstan-ignore-line
|
||||
/** @var float|int|string */
|
||||
$temp = call_user_func_array($call, $aArgs);
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
return ExcelError::VALUE();
|
||||
|
||||
+2
-1
@@ -59,7 +59,7 @@ class Sum
|
||||
$returnValue += $arg;
|
||||
} elseif (is_bool($arg)) {
|
||||
$returnValue += (int) $arg;
|
||||
} elseif (ErrorValue::isError($arg)) {
|
||||
} elseif (ErrorValue::isError($arg, true)) {
|
||||
/** @var string $arg */
|
||||
return $arg;
|
||||
} elseif ($arg !== null && !Functions::isCellValue($k)) {
|
||||
@@ -110,6 +110,7 @@ class Sum
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<float|int> $wrkArray */
|
||||
return array_sum($wrkArray);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -163,7 +163,8 @@ class Averages extends AggregateBase
|
||||
sort($aArgs, SORT_NUMERIC);
|
||||
$valueCount = $valueCount / 2;
|
||||
if ($valueCount == floor($valueCount)) {
|
||||
$returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2; //* @phpstan-ignore-line
|
||||
$valueCount = (int) $valueCount;
|
||||
$returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2;
|
||||
} else {
|
||||
$valueCount = (int) floor($valueCount);
|
||||
$returnValue = $aArgs[$valueCount];
|
||||
@@ -243,7 +244,7 @@ class Averages extends AggregateBase
|
||||
$maxfreqkey = $key;
|
||||
$maxfreqdatum = $datum;
|
||||
} elseif ($freq == $maxfreq) {
|
||||
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore-line
|
||||
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore offsetAccess.notFound (not sure what phpstan wants)
|
||||
$maxfreqkey = $key;
|
||||
$maxfreqdatum = $datum;
|
||||
}
|
||||
|
||||
+7
-3
@@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class Conditional
|
||||
{
|
||||
@@ -26,11 +27,14 @@ class Conditional
|
||||
* AVERAGEIF(range,condition[, average_range])
|
||||
*
|
||||
* @param mixed $range Data values, expect array
|
||||
* @param null|mixed[]|string $condition the criteria that defines which cells will be checked
|
||||
* @param mixed $condition the criteria that defines which cells will be checked, expect null|mixed[]|string
|
||||
* @param mixed $averageRange Data values
|
||||
*/
|
||||
public static function AVERAGEIF(mixed $range, null|array|string $condition, mixed $averageRange = []): null|int|float|string
|
||||
public static function AVERAGEIF(mixed $range, mixed $condition, mixed $averageRange = []): null|int|float|string
|
||||
{
|
||||
if ($condition !== null && !is_array($condition)) {
|
||||
$condition = StringHelper::convertToString($condition);
|
||||
}
|
||||
if (!is_array($range) || !is_array($averageRange) || array_key_exists(0, $range) || array_key_exists(0, $averageRange)) {
|
||||
$refError = ExcelError::REF();
|
||||
if (in_array($refError, [$range, $averageRange], true)) {
|
||||
@@ -62,7 +66,7 @@ class Conditional
|
||||
return 0.0;
|
||||
}
|
||||
if (count($args) === 3) {
|
||||
return self::AVERAGEIF($args[1], $args[2], $args[0]); //* @phpstan-ignore-line
|
||||
return self::AVERAGEIF($args[1], $args[2], $args[0]);
|
||||
}
|
||||
foreach ($args as $arg) {
|
||||
if (is_array($arg) && array_key_exists(0, $arg)) {
|
||||
|
||||
Vendored
+8
-3
@@ -28,9 +28,9 @@ class Beta
|
||||
* Or can be an array of values
|
||||
* @param mixed $beta Parameter to the distribution as a float
|
||||
* Or can be an array of values
|
||||
* @param mixed $rMin as an float
|
||||
* @param mixed $rMin as a float
|
||||
* Or can be an array of values
|
||||
* @param mixed $rMax as an float
|
||||
* @param mixed $rMax as a float
|
||||
* Or can be an array of values
|
||||
*
|
||||
* @return array<mixed>|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
|
||||
@@ -116,6 +116,11 @@ class Beta
|
||||
if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
if (($alpha + $beta) > self::LOG_GAMMA_X_MAX_VALUE) {
|
||||
// incompleteBeta declines to evaluate here and returns 0 for every x,
|
||||
// so there is no quantile to search for.
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax);
|
||||
}
|
||||
@@ -130,7 +135,7 @@ class Beta
|
||||
while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
|
||||
$guess = ($a + $b) / 2;
|
||||
$result = self::distribution($guess, $alpha, $beta);
|
||||
if (($result === $probability) || ($result === 0.0)) {
|
||||
if ($result === $probability) {
|
||||
$b = $a;
|
||||
} elseif ($result > $probability) {
|
||||
$b = $guess;
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class Binomial
|
||||
/**
|
||||
* BINOM.DIST.RANGE.
|
||||
*
|
||||
* Returns returns the Binomial Distribution probability for the number of successes from a specified number
|
||||
* Returns the Binomial Distribution probability for the number of successes from a specified number
|
||||
* of trials falling into a specified range.
|
||||
*
|
||||
* @param mixed $trials Integer number of trials
|
||||
|
||||
+13
-75
@@ -50,7 +50,7 @@ class ChiSquared
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2));
|
||||
return Gamma::regularizedGammaQ($degrees / 2, $value / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,8 +99,16 @@ class ChiSquared
|
||||
return 1 - (is_numeric($temp) ? $temp : 0);
|
||||
}
|
||||
|
||||
return ($value ** (($degrees / 2) - 1) * exp(-$value / 2))
|
||||
/ ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2));
|
||||
if ($value == 0.0) {
|
||||
if ($degrees === 2) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
return ($degrees === 1) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large degrees of freedom cannot overflow Gamma(d/2).
|
||||
return exp((($degrees / 2) - 1) * log($value) - $value / 2 - ($degrees / 2) * M_LN2 - Gamma::logGamma($degrees / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,8 +141,7 @@ class ChiSquared
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
$callback = fn (float $value): float => 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2)
|
||||
/ Gamma::gammaValue($degrees / 2));
|
||||
$callback = fn (float $value): float => Gamma::regularizedGammaQ($degrees / 2, $value / 2);
|
||||
|
||||
$newtonRaphson = new NewtonRaphson($callback);
|
||||
|
||||
@@ -258,75 +265,6 @@ class ChiSquared
|
||||
|
||||
private static function pchisq(float $chi2, int $degrees): float
|
||||
{
|
||||
return self::gammp($degrees, 0.5 * $chi2);
|
||||
}
|
||||
|
||||
private static function gammp(int $n, float $x): float
|
||||
{
|
||||
if ($x < 0.5 * $n + 1) {
|
||||
return self::gser($n, $x);
|
||||
}
|
||||
|
||||
return 1 - self::gcf($n, $x);
|
||||
}
|
||||
|
||||
// Return the incomplete gamma function P(n/2,x) evaluated by
|
||||
// series representation. Algorithm from numerical recipe.
|
||||
// Assume that n is a positive integer and x>0, won't check arguments.
|
||||
// Relative error controlled by the eps parameter
|
||||
private static function gser(int $n, float $x): float
|
||||
{
|
||||
/** @var float $gln */
|
||||
$gln = Gamma::ln($n / 2);
|
||||
$a = 0.5 * $n;
|
||||
$ap = $a;
|
||||
$sum = 1.0 / $a;
|
||||
$del = $sum;
|
||||
for ($i = 1; $i < 101; ++$i) {
|
||||
++$ap;
|
||||
$del = $del * $x / $ap;
|
||||
$sum += $del;
|
||||
if ($del < $sum * self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $sum * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
// Return the incomplete gamma function Q(n/2,x) evaluated by
|
||||
// its continued fraction representation. Algorithm from numerical recipe.
|
||||
// Assume that n is a postive integer and x>0, won't check arguments.
|
||||
// Relative error controlled by the eps parameter
|
||||
private static function gcf(int $n, float $x): float
|
||||
{
|
||||
/** @var float $gln */
|
||||
$gln = Gamma::ln($n / 2);
|
||||
$a = 0.5 * $n;
|
||||
$b = $x + 1 - $a;
|
||||
$fpmin = 1.e-300;
|
||||
$c = 1 / $fpmin;
|
||||
$d = 1 / $b;
|
||||
$h = $d;
|
||||
for ($i = 1; $i < 101; ++$i) {
|
||||
$an = -$i * ($i - $a);
|
||||
$b += 2;
|
||||
$d = $an * $d + $b;
|
||||
if (abs($d) < $fpmin) {
|
||||
$d = $fpmin;
|
||||
}
|
||||
$c = $b + $an / $c;
|
||||
if (abs($c) < $fpmin) {
|
||||
$c = $fpmin;
|
||||
}
|
||||
$d = 1 / $d;
|
||||
$del = $d * $c;
|
||||
$h = $h * $del;
|
||||
if (abs($del - 1) < self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $h * exp(-$x + $a * log($x) - $gln);
|
||||
return Gamma::regularizedGammaP($degrees / 2, 0.5 * $chi2);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+15
-4
@@ -55,9 +55,20 @@ class F
|
||||
return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2);
|
||||
}
|
||||
|
||||
return (Gamma::gammaValue(($v + $u) / 2)
|
||||
/ (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2)))
|
||||
* (($u / $v) ** ($u / 2))
|
||||
* (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2)));
|
||||
if ($value == 0.0) {
|
||||
if ($u === 2) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return ($u === 1) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large degrees of freedom cannot overflow the Gamma ratio.
|
||||
return exp(
|
||||
Gamma::logGamma(($v + $u) / 2) - Gamma::logGamma($u / 2) - Gamma::logGamma($v / 2)
|
||||
+ ($u / 2) * log($u / $v)
|
||||
+ (($u - 2) / 2) * log($value)
|
||||
- (($u + $v) / 2) * log(1 + ($u / $v) * $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+8
-1
@@ -85,6 +85,10 @@ class Gamma extends GammaBase
|
||||
*
|
||||
* Returns the inverse of the Gamma distribution.
|
||||
*
|
||||
* For a probability so far into the upper tail that the forward-CDF series
|
||||
* approximation plateaus below it, the root cannot be bracketed; the result
|
||||
* is capped at the alpha*beta*5 search ceiling rather than diverging.
|
||||
*
|
||||
* @param mixed $probability Float probability at which you want to evaluate the distribution
|
||||
* Or can be an array of values
|
||||
* @param mixed $alpha Parameter to the distribution as a float
|
||||
@@ -143,6 +147,9 @@ class Gamma extends GammaBase
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return log(self::gammaValue($value));
|
||||
$result = self::logGamma($value);
|
||||
|
||||
// logGamma returns its MAX_VALUE sentinel when the result would overflow.
|
||||
return ($result >= self::MAX_VALUE) ? ExcelError::NAN() : $result;
|
||||
}
|
||||
}
|
||||
|
||||
+120
-12
@@ -11,7 +11,7 @@ abstract class GammaBase
|
||||
|
||||
private const EPS = 2.22e-16;
|
||||
|
||||
private const MAX_VALUE = 1.2e308;
|
||||
protected const MAX_VALUE = 1.2e308;
|
||||
|
||||
private const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
|
||||
|
||||
@@ -20,10 +20,19 @@ abstract class GammaBase
|
||||
protected static function calculateDistribution(float $value, float $a, float $b, bool $cumulative): float
|
||||
{
|
||||
if ($cumulative) {
|
||||
return self::incompleteGamma($a, $value / $b) / self::gammaValue($a);
|
||||
return self::regularizedGammaP($a, $value / $b);
|
||||
}
|
||||
|
||||
return (1 / ($b ** $a * self::gammaValue($a))) * $value ** ($a - 1) * exp(0 - ($value / $b));
|
||||
if ($value == 0.0) {
|
||||
if ($a == 1.0) {
|
||||
return 1.0 / $b;
|
||||
}
|
||||
|
||||
return ($a < 1.0) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large shape parameters cannot overflow Gamma(a).
|
||||
return exp(($a - 1.0) * log($value) - $value / $b - $a * log($b) - self::logGamma($a));
|
||||
}
|
||||
|
||||
/** @return float|string */
|
||||
@@ -32,6 +41,24 @@ abstract class GammaBase
|
||||
$xLo = 0;
|
||||
$xHi = $alpha * $beta * 5;
|
||||
|
||||
// Extend the upper bound while it does not yet bracket the root, so a
|
||||
// tail quantile beyond alpha*beta*5 is no longer clamped to it. Stop if
|
||||
// the CDF stops increasing (series approximation past its usable range)
|
||||
// and keep the original bound rather than expanding into that region.
|
||||
$xHiBase = $xHi;
|
||||
$cdfHi = self::calculateDistribution($xHi, $alpha, $beta, true);
|
||||
while ($cdfHi < $probability) {
|
||||
$xHiNext = $xHi * 2;
|
||||
$cdfNext = self::calculateDistribution($xHiNext, $alpha, $beta, true);
|
||||
if ($cdfNext <= $cdfHi) {
|
||||
$xHi = $xHiBase;
|
||||
|
||||
break;
|
||||
}
|
||||
$xHi = $xHiNext;
|
||||
$cdfHi = $cdfNext;
|
||||
}
|
||||
|
||||
$dx = 1024;
|
||||
$x = $xNew = 1;
|
||||
$i = 0;
|
||||
@@ -78,17 +105,98 @@ abstract class GammaBase
|
||||
//
|
||||
public static function incompleteGamma(float $a, float $x): float
|
||||
{
|
||||
static $max = 32;
|
||||
$summer = 0;
|
||||
for ($n = 0; $n <= $max; ++$n) {
|
||||
$divisor = $a;
|
||||
for ($i = 1; $i <= $n; ++$i) {
|
||||
$divisor *= ($a + $i);
|
||||
}
|
||||
$summer += ($x ** $n / $divisor);
|
||||
// Unregularized lower incomplete gamma; kept for backward compatibility.
|
||||
return self::regularizedGammaP($a, $x) * self::gammaValue($a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regularized lower incomplete gamma P(a,x) = gamma(a,x) / Gamma(a).
|
||||
* Series for x < a+1, else the complement of the continued fraction.
|
||||
*/
|
||||
public static function regularizedGammaP(float $a, float $x): float
|
||||
{
|
||||
if ($x <= 0.0 || $a <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if ($x < $a + 1.0) {
|
||||
return self::gammaSeries($a, $x);
|
||||
}
|
||||
|
||||
return $x ** $a * exp(0 - $x) * $summer;
|
||||
return 1.0 - self::gammaContinuedFraction($a, $x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regularized upper incomplete gamma Q(a,x) = 1 - P(a,x).
|
||||
* Continued fraction for x >= a+1 keeps the right tail free of cancellation.
|
||||
*/
|
||||
public static function regularizedGammaQ(float $a, float $x): float
|
||||
{
|
||||
if ($x <= 0.0 || $a <= 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
if ($x < $a + 1.0) {
|
||||
return 1.0 - self::gammaSeries($a, $x);
|
||||
}
|
||||
|
||||
return self::gammaContinuedFraction($a, $x);
|
||||
}
|
||||
|
||||
// Near x ~ a both expansions need O(sqrt(a)) terms to reach EPS.
|
||||
private static function incompleteGammaIterations(float $a): int
|
||||
{
|
||||
return max(self::MAX_ITERATIONS, (int) ceil(10.0 * sqrt($a)));
|
||||
}
|
||||
|
||||
// P(a,x) by its series representation (Numerical Recipes gser).
|
||||
private static function gammaSeries(float $a, float $x): float
|
||||
{
|
||||
$maxIterations = self::incompleteGammaIterations($a);
|
||||
$gln = self::logGamma($a);
|
||||
$ap = $a;
|
||||
$sum = 1.0 / $a;
|
||||
$del = $sum;
|
||||
for ($i = 1; $i <= $maxIterations; ++$i) {
|
||||
++$ap;
|
||||
$del *= $x / $ap;
|
||||
$sum += $del;
|
||||
if (abs($del) < abs($sum) * self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $sum * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
// Q(a,x) by its continued fraction representation (Numerical Recipes gcf).
|
||||
private static function gammaContinuedFraction(float $a, float $x): float
|
||||
{
|
||||
$maxIterations = self::incompleteGammaIterations($a);
|
||||
$fpMin = 1.0e-300;
|
||||
$gln = self::logGamma($a);
|
||||
$b = $x + 1.0 - $a;
|
||||
$c = 1.0 / $fpMin;
|
||||
$d = 1.0 / $b;
|
||||
$h = $d;
|
||||
for ($i = 1; $i <= $maxIterations; ++$i) {
|
||||
$an = -$i * ($i - $a);
|
||||
$b += 2.0;
|
||||
$d = $an * $d + $b;
|
||||
if (abs($d) < $fpMin) {
|
||||
$d = $fpMin;
|
||||
}
|
||||
$c = $b + $an / $c;
|
||||
if (abs($c) < $fpMin) {
|
||||
$c = $fpMin;
|
||||
}
|
||||
$d = 1.0 / $d;
|
||||
$del = $d * $c;
|
||||
$h *= $del;
|
||||
if (abs($del - 1.0) < self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $h * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
private const GAMMA_VALUE_P0 = 1.000000000190015;
|
||||
|
||||
Vendored
+1
-1
@@ -116,7 +116,7 @@ class Normal
|
||||
// whatever purpose you want, but please show common courtesy and give credit
|
||||
// where credit is due.
|
||||
|
||||
// Input paramater is $p - probability - where 0 < p < 1.
|
||||
// Input parameter is $p - probability - where 0 < p < 1.
|
||||
|
||||
// Coefficients in rational approximations
|
||||
/** @var array<int, float> */
|
||||
|
||||
+225
-41
@@ -4,9 +4,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
/**
|
||||
* Some of this code is drived from Perl CPAN Statistical::Distributions.
|
||||
* Its copyright statement is:
|
||||
* Copyright 2003 Michael Kospach. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
|
||||
*/
|
||||
class StudentT
|
||||
{
|
||||
use ArrayEnabled;
|
||||
@@ -28,9 +34,39 @@ class StudentT
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function distribution(mixed $value, mixed $degrees, mixed $tails)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, $tails, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.2T.
|
||||
* Returns the two-tailed Student's t distribution.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDistDot2T(mixed $value, mixed $degrees)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, 2, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.RT.
|
||||
* Returns the right-tailed Student's t distribution.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDistDotRT(mixed $value, mixed $degrees)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, 1, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
private static function calcDistribution(mixed $value, mixed $degrees, mixed $tails, callable $callback)
|
||||
{
|
||||
if (is_array($value) || is_array($degrees) || is_array($tails)) {
|
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails);
|
||||
return self::evaluateArrayArguments($callback, $value, $degrees, $tails);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -45,13 +81,105 @@ class StudentT
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return self::calculateDistribution($value, $degrees, $tails);
|
||||
return self::subTProb($value, $degrees, $tails);
|
||||
}
|
||||
|
||||
/**
|
||||
* TINV.
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*/
|
||||
private static function subTProb(float $x, int $n, int $tails): float
|
||||
{
|
||||
$w = atan2($x / sqrt($n), 1);
|
||||
$z = cos($w) ** 2;
|
||||
$y = 1;
|
||||
|
||||
for ($i = $n - 2; $i >= 2; $i -= 2) {
|
||||
$y = 1 + ($i - 1) / $i * $z * $y;
|
||||
}
|
||||
|
||||
if ($n % 2 == 0) {
|
||||
$a = sin($w) / 2;
|
||||
$b = 0.5;
|
||||
} else {
|
||||
$a = ($n == 1) ? 0 : (sin($w) * cos($w) / M_PI);
|
||||
$b = 0.5 + $w / M_PI;
|
||||
}
|
||||
|
||||
return $tails * max(0, 1 - $b - $a * $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.
|
||||
* Returns the Student's left-tailed t distribution,
|
||||
* either as a cumulative distribution function (cdf) (TRUE)
|
||||
* or as a probability density function (pdf) (FALSE),
|
||||
* where TRUE/FALSE are the value of $cumulative parameter.
|
||||
*
|
||||
* Returns the one-tailed probability of the chi-squared distribution.
|
||||
* "True" algoritm adapted from java.
|
||||
* org.apache.commons.math3.distribution.TDistribution.
|
||||
* "False" algorithm comes from:
|
||||
* https://statproofbook.github.io/P/t-pdf.html
|
||||
*
|
||||
* @param mixed $cumulative Expecting bool. See above for explanation.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDist(mixed $value, mixed $degrees, mixed $cumulative)
|
||||
{
|
||||
if (is_array($value) || is_array($degrees) || is_array($cumulative)) {
|
||||
return self::evaluateArrayArguments(self::tDotDist(...), $value, $degrees, $cumulative);
|
||||
}
|
||||
|
||||
try {
|
||||
$value = DistributionValidations::validateFloat($value);
|
||||
$degrees = DistributionValidations::validateInt($degrees);
|
||||
$cumulative = DistributionValidations::validateBool($cumulative);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var int $degrees */
|
||||
if (($degrees < 1)) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
/** @var float $value */
|
||||
if (!$cumulative) {
|
||||
return self::tDotDistFalse($value, $degrees);
|
||||
}
|
||||
|
||||
$f16 = $degrees / ($degrees + $value * $value);
|
||||
$g16 = 0.5 * $degrees;
|
||||
$h16 = 0.5;
|
||||
$result = Beta::distribution($f16, $g16, $h16);
|
||||
if (is_numeric($result)) {
|
||||
$result = ($value < 0) ? (0.5 * $result) : (1 - 0.5 * $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function tDotDistFalse(float $value, int $degrees): float|string
|
||||
{
|
||||
$result = $k15 = Gamma::gamma(($degrees + 1) / 2);
|
||||
if (is_numeric($k15)) {
|
||||
$result = $k16 = Gamma::gamma($degrees / 2);
|
||||
if (is_numeric($k16)) {
|
||||
$k17 = sqrt(M_PI * $degrees);
|
||||
$k18 = $k15 / ($k16 * $k17);
|
||||
$k19 = $value * $value / $degrees + 1;
|
||||
$k20 = -($degrees + 1) / 2;
|
||||
$k21 = $k19 ** $k20;
|
||||
$result = $k18 * $k21;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var float|string $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* TINV and T.INV.2T.
|
||||
* Returns the two-tailed inverse of the Student t distribution.
|
||||
*
|
||||
* @param mixed $probability Float probability for the function
|
||||
* Or can be an array of values
|
||||
@@ -63,9 +191,17 @@ class StudentT
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function inverse(mixed $probability, mixed $degrees)
|
||||
{
|
||||
return self::calcInverse($probability, $degrees, 2, self::inverse(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
private static function calcInverse(mixed $probability, mixed $degrees, int $tails, callable $callback2)
|
||||
{
|
||||
if (is_array($probability) || is_array($degrees)) {
|
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
|
||||
return self::evaluateArrayArguments($callback2, $probability, $degrees, $tails);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -79,54 +215,102 @@ class StudentT
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
$callback = fn ($value) => self::distribution($value, $degrees, 2);
|
||||
$callback = fn ($value) => self::distribution($value, $degrees, $tails);
|
||||
|
||||
$newtonRaphson = new NewtonRaphson($callback);
|
||||
|
||||
return $newtonRaphson->execute($probability);
|
||||
$result = $newtonRaphson->execute($probability);
|
||||
if (is_numeric($result) && $tails === 1) {
|
||||
$result = -$result; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function calculateDistribution(float $value, int $degrees, int $tails): float
|
||||
/**
|
||||
* T.INV.
|
||||
* Returns the left-tailed inverse of the Student's t distribution.
|
||||
*
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotInv(mixed $probability, mixed $degrees)
|
||||
{
|
||||
// tdist, which finds the probability that corresponds to a given value
|
||||
// of t with k degrees of freedom. This algorithm is translated from a
|
||||
// pascal function on p81 of "Statistical Computing in Pascal" by D
|
||||
// Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd:
|
||||
// London). The above Pascal algorithm is itself a translation of the
|
||||
// fortran algoritm "AS 3" by B E Cooper of the Atlas Computer
|
||||
// Laboratory as reported in (among other places) "Applied Statistics
|
||||
// Algorithms", editied by P Griffiths and I D Hill (1985; Ellis
|
||||
// Horwood Ltd.; W. Sussex, England).
|
||||
$tterm = $degrees;
|
||||
$ttheta = atan2($value, sqrt($tterm));
|
||||
$tc = cos($ttheta);
|
||||
$ts = sin($ttheta);
|
||||
|
||||
if (($degrees % 2) === 1) {
|
||||
$ti = 3;
|
||||
$tterm = $tc;
|
||||
} else {
|
||||
$ti = 2;
|
||||
$tterm = 1;
|
||||
if (is_array($probability) || is_array($degrees)) {
|
||||
return self::evaluateArrayArguments(self::tDotInv(...), $probability, $degrees);
|
||||
}
|
||||
|
||||
$tsum = $tterm;
|
||||
while ($ti < $degrees) {
|
||||
$tterm *= $tc * $tc * ($ti - 1) / $ti;
|
||||
$tsum += $tterm;
|
||||
$ti += 2;
|
||||
try {
|
||||
$probability = DistributionValidations::validateProbability($probability);
|
||||
$degrees = DistributionValidations::validateInt($degrees);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
$tsum *= $ts;
|
||||
if (($degrees % 2) == 1) {
|
||||
$tsum = Functions::M_2DIVPI * ($tsum + $ttheta);
|
||||
if ($degrees < 1) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
if ($probability == 0.5) {
|
||||
return 0.0;
|
||||
}
|
||||
if ($probability < 0.5) {
|
||||
$result = self::tDotInv(1.0 - $probability, $degrees);
|
||||
|
||||
return is_numeric($result) ? -$result : $result;
|
||||
}
|
||||
$p = $probability;
|
||||
$n = $degrees;
|
||||
$u = self::subU($p);
|
||||
$u2 = $u ** 2;
|
||||
|
||||
$a = ($u2 + 1) / 4;
|
||||
$b = ((5 * $u2 + 16) * $u2 + 3) / 96;
|
||||
$c = (((3 * $u2 + 19) * $u2 + 17) * $u2 - 15) / 384;
|
||||
$d = ((((79 * $u2 + 776) * $u2 + 1482) * $u2 - 1920) * $u2 - 945) / 92160;
|
||||
$e = (((((27 * $u2 + 339) * $u2 + 930) * $u2 - 1782) * $u2 - 765) * $u2 + 17955) / 368640;
|
||||
|
||||
$x = $u * (1 + ($a + ($b + ($c + ($d + $e / $n) / $n) / $n) / $n) / $n);
|
||||
|
||||
if ($n <= log10($p) ** 2 + 3) {
|
||||
do {
|
||||
$p1 = self::subTProb($x, $n, 1);
|
||||
$n1 = $n + 1;
|
||||
$delta = ($p1 - $p)
|
||||
/ exp(($n1 * log($n1 / ($n + $x * $x))
|
||||
+ log($n / $n1 / 2 / M_PI) - 1
|
||||
+ (1 / $n1 - 1 / $n) / 6) / 2);
|
||||
$x += $delta;
|
||||
$round = sprintf('%.' . abs((int) (log10(abs($x)) - 4)) . 'F', $delta);
|
||||
} while (($x) && ($round != 0));
|
||||
}
|
||||
|
||||
$tValue = 0.5 * (1 + $tsum);
|
||||
if ($tails == 1) {
|
||||
return 1 - abs($tValue);
|
||||
return -$x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*/
|
||||
private static function subU(float $p): float
|
||||
{
|
||||
$y = -log(4 * $p * (1 - $p));
|
||||
$x = sqrt(
|
||||
$y * (1.570796288
|
||||
+ $y * (.03706987906
|
||||
+ $y * (-.8364353589E-3
|
||||
+ $y * (-.2250947176E-3
|
||||
+ $y * (.6841218299E-5
|
||||
+ $y * (0.5824238515E-5
|
||||
+ $y * (-.104527497E-5
|
||||
+ $y * (.8360937017E-7
|
||||
+ $y * (-.3231081277E-8
|
||||
+ $y * (.3657763036E-10
|
||||
+ $y * .6936233982E-12))))))))))
|
||||
);
|
||||
if ($p > 0.5) {
|
||||
$x = -$x;
|
||||
}
|
||||
|
||||
return 1 - abs((1 - $tValue) - $tValue);
|
||||
return $x;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class Maximum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
@@ -63,7 +63,7 @@ class Maximum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class Minimum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
@@ -63,7 +63,7 @@ class Minimum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
|
||||
+2
@@ -89,6 +89,7 @@ class Percentiles
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var array<float|int|numeric-string> */
|
||||
$valueSet = self::rankFilterValues($valueSet);
|
||||
$valueCount = count($valueSet);
|
||||
if ($valueCount == 0) {
|
||||
@@ -172,6 +173,7 @@ class Percentiles
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var array<float|int|numeric-string> */
|
||||
$valueSet = self::rankFilterValues($valueSet);
|
||||
if ($order === self::RANK_SORT_DESCENDING) {
|
||||
rsort($valueSet, SORT_NUMERIC);
|
||||
|
||||
Vendored
+73
-18
@@ -6,11 +6,14 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class CharacterConvert
|
||||
{
|
||||
use ArrayEnabled;
|
||||
|
||||
private static string $oneByteCharacterSet = 'Windows-1252';
|
||||
|
||||
/**
|
||||
* CHAR.
|
||||
*
|
||||
@@ -27,19 +30,45 @@ class CharacterConvert
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
|
||||
}
|
||||
|
||||
return self::characterBoth($character, true);
|
||||
}
|
||||
|
||||
/** @return array<mixed>|string */
|
||||
public static function characterUnicode(mixed $character): array|string
|
||||
{
|
||||
if (is_array($character)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
|
||||
}
|
||||
|
||||
return self::characterBoth($character, false);
|
||||
}
|
||||
|
||||
private static function characterBoth(mixed $character, bool $ansi = true): string
|
||||
{
|
||||
try {
|
||||
$character = Helpers::validateInt($character, true);
|
||||
} catch (CalcExp $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
if ($ansi && $character === 219 && self::$oneByteCharacterSet[0] === 'M') {
|
||||
return '€';
|
||||
}
|
||||
|
||||
$min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
|
||||
if ($character < $min || $character > 255) {
|
||||
if ($character < $min || ($ansi && $character > 255) || $character > 0x10FFFF) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
$result = iconv('UCS-4LE', 'UTF-8', pack('V', $character));
|
||||
if ($character > 0x10FFFD) { // last assigned
|
||||
return ExcelError::NA();
|
||||
}
|
||||
if ($ansi) {
|
||||
$result = chr($character);
|
||||
|
||||
return ($result === false) ? '' : $result;
|
||||
return (string) iconv(self::$oneByteCharacterSet, 'UTF-8//IGNORE', $result);
|
||||
}
|
||||
|
||||
return mb_chr($character, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +86,28 @@ class CharacterConvert
|
||||
if (is_array($characters)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
|
||||
}
|
||||
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
$characters = $characters ? '1' : '0';
|
||||
}
|
||||
|
||||
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), true);
|
||||
}
|
||||
|
||||
/** @return array<mixed>|int|string */
|
||||
public static function codeUnicode(mixed $characters): array|string|int
|
||||
{
|
||||
if (is_array($characters)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
|
||||
}
|
||||
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
$characters = $characters ? '1' : '0';
|
||||
}
|
||||
|
||||
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), false);
|
||||
}
|
||||
|
||||
private static function codeBoth(string $characters, bool $ansi = true): int|string
|
||||
{
|
||||
try {
|
||||
$characters = Helpers::extractString($characters, true);
|
||||
} catch (CalcExp $e) {
|
||||
@@ -72,22 +122,27 @@ class CharacterConvert
|
||||
if (mb_strlen($characters, 'UTF-8') > 1) {
|
||||
$character = mb_substr($characters, 0, 1, 'UTF-8');
|
||||
}
|
||||
|
||||
return self::unicodeToOrd($character);
|
||||
}
|
||||
|
||||
private static function unicodeToOrd(string $character): int
|
||||
{
|
||||
$retVal = 0;
|
||||
$iconv = iconv('UTF-8', 'UCS-4LE', $character);
|
||||
if ($iconv !== false) {
|
||||
/** @var false|int[] */
|
||||
$result = unpack('V', $iconv);
|
||||
if (is_array($result) && isset($result[1])) {
|
||||
$retVal = $result[1];
|
||||
}
|
||||
if ($ansi && $character === '€' && self::$oneByteCharacterSet[0] === 'M') {
|
||||
return 219;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
$result = mb_ord($character, 'UTF-8');
|
||||
if ($ansi) {
|
||||
$result = iconv('UTF-8', self::$oneByteCharacterSet . '//IGNORE', $character);
|
||||
|
||||
return ($result !== '') ? ord("$result") : 63; // question mark
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function setWindowsCharacterSet(): void
|
||||
{
|
||||
self::$oneByteCharacterSet = 'Windows-1252';
|
||||
}
|
||||
|
||||
public static function setMacCharacterSet(): void
|
||||
{
|
||||
self::$oneByteCharacterSet = 'MAC';
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -7,6 +7,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Internal\ExcelArrayPseudoFunctions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
@@ -17,7 +19,7 @@ class Concatenate
|
||||
/**
|
||||
* This implements the CONCAT function, *not* CONCATENATE.
|
||||
*
|
||||
* @param mixed[] $args
|
||||
* @param mixed $args data to be concatenated
|
||||
*/
|
||||
public static function CONCATENATE(...$args): string
|
||||
{
|
||||
@@ -47,17 +49,33 @@ class Concatenate
|
||||
/**
|
||||
* This implements the CONCATENATE function.
|
||||
*
|
||||
* @param mixed[] $args data to be concatenated
|
||||
* @param mixed $args data to be concatenated
|
||||
*
|
||||
* @return array<string>|string
|
||||
*/
|
||||
public static function actualCONCATENATE(...$args): array|string
|
||||
{
|
||||
$useSingle = false;
|
||||
$cell = null;
|
||||
$count = count($args);
|
||||
if ($args[$count - 1] instanceof Cell) {
|
||||
/** @var Cell */
|
||||
$cell = array_pop($args);
|
||||
$type = $cell->getWorksheet()->getParent()?->getCalculationEngine()->getInstanceArrayReturnType() ?? Calculation::getArrayReturnType();
|
||||
$useSingle = $type === Calculation::RETURN_ARRAY_AS_VALUE;
|
||||
}
|
||||
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) {
|
||||
return self::CONCATENATE(...$args);
|
||||
}
|
||||
$result = '';
|
||||
foreach ($args as $operand2) {
|
||||
if ($useSingle && $cell instanceof Cell && is_array($operand2)) {
|
||||
$temp = Functions::convertArrayToCellRange($operand2);
|
||||
if ($temp !== '') {
|
||||
$operand2 = ExcelArrayPseudoFunctions::single($temp, $cell);
|
||||
}
|
||||
}
|
||||
/** @var null|array<mixed>|bool|float|int|string $operand2 */
|
||||
$result = self::concatenate2Args($result, $operand2);
|
||||
if (ErrorValue::isError($result, true) === true) {
|
||||
break;
|
||||
@@ -142,6 +160,7 @@ class Concatenate
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
$returnValue = self::evaluateTextJoinArray($ignoreEmpty, $aArgs);
|
||||
|
||||
/** @var string[] $aArgs */
|
||||
$returnValue ??= implode($delimiter, $aArgs);
|
||||
if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) {
|
||||
$returnValue = ExcelError::CALC();
|
||||
|
||||
+10
-11
@@ -96,16 +96,13 @@ class Format
|
||||
if ($decimals < 0) {
|
||||
$decimals = 0;
|
||||
}
|
||||
if ($noCommas === false) {
|
||||
$valueResult = number_format(
|
||||
$valueResult,
|
||||
$decimals,
|
||||
StringHelper::getDecimalSeparator(),
|
||||
StringHelper::getThousandsSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
return (string) $valueResult;
|
||||
return number_format(
|
||||
$valueResult,
|
||||
$decimals,
|
||||
StringHelper::getDecimalSeparator(),
|
||||
$noCommas ? '' : StringHelper::getThousandsSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,18 +206,20 @@ class Format
|
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
|
||||
|
||||
if (str_contains($value, ':')) {
|
||||
/** @var DateTimeInterface|float|int|string */
|
||||
$timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value));
|
||||
if ($timeValue !== ExcelError::VALUE()) {
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
return $timeValue; //* @phpstan-ignore-line
|
||||
return $timeValue;
|
||||
}
|
||||
}
|
||||
/** @var DateTimeInterface|float|int|string */
|
||||
$dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value));
|
||||
if ($dateValue !== ExcelError::VALUE()) {
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
return $dateValue; //* @phpstan-ignore-line
|
||||
return $dateValue;
|
||||
}
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ class Search
|
||||
use ArrayEnabled;
|
||||
|
||||
/**
|
||||
* FIND (case sensitive search).
|
||||
* FIND (case-sensitive search).
|
||||
*
|
||||
* @param mixed $needle The string to look for
|
||||
* Or can be an array of values
|
||||
@@ -54,7 +54,7 @@ class Search
|
||||
}
|
||||
|
||||
/**
|
||||
* SEARCH (case insensitive search).
|
||||
* SEARCH (case-insensitive search).
|
||||
*
|
||||
* @param mixed $needle The string to look for
|
||||
* Or can be an array of values
|
||||
|
||||
+2
-2
@@ -171,12 +171,12 @@ class Text
|
||||
{
|
||||
$columnCount = array_reduce(
|
||||
$rows,
|
||||
fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore-line
|
||||
fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore argument.type (I don't know)
|
||||
0
|
||||
);
|
||||
|
||||
return array_map(
|
||||
fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore-line
|
||||
fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore argument.type (I don't know)
|
||||
? array_merge($row, array_fill(0, $columnCount - count($row), $padding))
|
||||
: $row,
|
||||
$rows
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
class Thai
|
||||
{
|
||||
use ArrayEnabled;
|
||||
|
||||
private const THAI_DIGITS = [
|
||||
0 => 'ศูนย์',
|
||||
1 => 'หนึ่ง',
|
||||
2 => 'สอง',
|
||||
3 => 'สาม',
|
||||
4 => 'สี่',
|
||||
5 => 'ห้า',
|
||||
6 => 'หก',
|
||||
7 => 'เจ็ด',
|
||||
8 => 'แปด',
|
||||
9 => 'เก้า',
|
||||
];
|
||||
|
||||
private const THAI_UNITS = [
|
||||
1 => 'สิบ',
|
||||
2 => 'ร้อย',
|
||||
3 => 'พัน',
|
||||
4 => 'หมื่น',
|
||||
5 => 'แสน',
|
||||
6 => 'ล้าน',
|
||||
];
|
||||
|
||||
private const THAI_COMPOUND_ONE = 'เอ็ด';
|
||||
private const THAI_COMPOUND_TWO = 'ยี่';
|
||||
private const THAI_INTEGER = 'ถ้วน';
|
||||
private const THAI_MINUS = 'ลบ';
|
||||
private const THAI_BAHT = 'บาท';
|
||||
private const THAI_SATANG = 'สตางค์';
|
||||
|
||||
/**
|
||||
* BAHTTEXT.
|
||||
*
|
||||
* @param mixed $number The number or array of numbers to convert
|
||||
*
|
||||
* @return array<mixed>|string If an array of values is passed as the argument, then the returned result will also be an array with the same dimensions
|
||||
*/
|
||||
public static function getBahtText(mixed $number): array|string
|
||||
{
|
||||
if (is_array($number)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number);
|
||||
}
|
||||
|
||||
if (is_string($number) && preg_match('/^-?\d+$/', $number)) {
|
||||
$isNegative = str_starts_with($number, '-');
|
||||
$baht = ltrim($number, '-0') ?: '0';
|
||||
$satang = '00';
|
||||
} elseif (is_bool($number) || is_numeric($number)) {
|
||||
$number += 0;
|
||||
$isNegative = $number < 0;
|
||||
[$baht, $satang] = explode('.', number_format(abs($number), 2, '.', ''));
|
||||
} else {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
$hasWhole = $baht !== '0';
|
||||
$hasFraction = $satang !== '00';
|
||||
|
||||
if (!$hasWhole && !$hasFraction) {
|
||||
return self::THAI_DIGITS[0] . self::THAI_BAHT . self::THAI_INTEGER;
|
||||
}
|
||||
|
||||
$text = $isNegative
|
||||
? self::THAI_MINUS
|
||||
: '';
|
||||
|
||||
if ($hasWhole) {
|
||||
$text .= self::convertLarge($baht) . self::THAI_BAHT;
|
||||
}
|
||||
|
||||
$text .= $hasFraction
|
||||
? self::convertBlock($satang) . self::THAI_SATANG
|
||||
: self::THAI_INTEGER;
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function convertLarge(string $digits): string
|
||||
{
|
||||
$length = strlen($digits) % 6 ?: 6;
|
||||
|
||||
$chunks = [
|
||||
substr($digits, 0, $length),
|
||||
...str_split(substr($digits, $length), 6),
|
||||
];
|
||||
|
||||
$chunks = array_filter($chunks, fn (string $chunk): bool => $chunk !== '');
|
||||
|
||||
return implode(
|
||||
self::THAI_UNITS[6],
|
||||
array_map(self::convertBlock(...), $chunks)
|
||||
);
|
||||
}
|
||||
|
||||
private static function convertBlock(string $block): string
|
||||
{
|
||||
$out = '';
|
||||
$length = strlen($block);
|
||||
$i = 0;
|
||||
|
||||
// Hundreds and higher powers
|
||||
for ($power = $length - 1; $power >= 2; --$power) {
|
||||
$digit = $block[$i++];
|
||||
if ($digit !== '0') {
|
||||
$out .= self::THAI_DIGITS[$digit] . self::THAI_UNITS[$power];
|
||||
}
|
||||
}
|
||||
|
||||
// Tens
|
||||
$ten = $length > 1 ? $block[$i++] : '0';
|
||||
if ($ten !== '0') {
|
||||
$out .= match ($ten) {
|
||||
'1' => '',
|
||||
'2' => self::THAI_COMPOUND_TWO,
|
||||
default => self::THAI_DIGITS[$ten],
|
||||
} . self::THAI_UNITS[1];
|
||||
}
|
||||
|
||||
// Ones
|
||||
$one = $block[$i] ?? '0';
|
||||
if ($one !== '0') {
|
||||
$out .= $ten !== '0' && $one === '1'
|
||||
? self::THAI_COMPOUND_ONE
|
||||
: self::THAI_DIGITS[$one];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
+29
-23
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Web;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Settings;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class Service
|
||||
{
|
||||
@@ -18,38 +19,43 @@ class Service
|
||||
*
|
||||
* @return string the output resulting from a call to the webservice
|
||||
*/
|
||||
public static function webService(string $url): string
|
||||
public static function webService(mixed $url, ?Cell $cell = null): ?string
|
||||
{
|
||||
$url = trim($url);
|
||||
if (strlen($url) > 2048) {
|
||||
if (is_array($url)) {
|
||||
$url = Functions::flattenSingleValue($url);
|
||||
}
|
||||
$url = trim(StringHelper::convertToString($url, false));
|
||||
if (mb_strlen($url) > 2048) {
|
||||
return ExcelError::VALUE(); // Invalid URL length
|
||||
}
|
||||
|
||||
if (!preg_match('/^http[s]?:\/\//', $url)) {
|
||||
$parsed = parse_url($url);
|
||||
$scheme = $parsed['scheme'] ?? '';
|
||||
if ($scheme !== 'http' && $scheme !== 'https') {
|
||||
return ExcelError::VALUE(); // Invalid protocol
|
||||
}
|
||||
$domainWhiteList = $cell?->getWorksheet()->getParent()?->getDomainWhiteList() ?? [];
|
||||
$host = $parsed['host'] ?? '';
|
||||
if (!in_array($host, $domainWhiteList, true)) {
|
||||
return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED; // will be converted to oldCalculatedValue or null
|
||||
}
|
||||
|
||||
// Get results from the webservice
|
||||
$client = Settings::getHttpClient();
|
||||
$requestFactory = Settings::getRequestFactory();
|
||||
$request = $requestFactory->createRequest('GET', $url);
|
||||
|
||||
try {
|
||||
$response = $client->sendRequest($request);
|
||||
} catch (ClientExceptionInterface) {
|
||||
return ExcelError::VALUE(); // cURL error
|
||||
$ctxArray = [
|
||||
'http' => [
|
||||
'follow_location' => 0,
|
||||
'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
],
|
||||
];
|
||||
if ($scheme === 'https') {
|
||||
$ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT];
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() != 200) {
|
||||
return ExcelError::VALUE(); // cURL error
|
||||
}
|
||||
|
||||
$output = $response->getBody()->getContents();
|
||||
if (strlen($output) > 32767) {
|
||||
$ctx = stream_context_create($ctxArray);
|
||||
$output = @file_get_contents($url, false, $ctx);
|
||||
if ($output === false || mb_strlen($output) > 32767) {
|
||||
return ExcelError::VALUE(); // Output not a string or too long
|
||||
}
|
||||
|
||||
return $output;
|
||||
return ($output === '') ? Functions::NOT_YET_IMPLEMENTED : $output;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -38,6 +38,7 @@ DVARP = DBVARP
|
||||
## Datum- en tijdfuncties (Date & Time Functions)
|
||||
##
|
||||
DATE = DATUM
|
||||
DATEDIF = DATUMVERSCHIL
|
||||
DATESTRING = DATUMNOTATIE
|
||||
DATEVALUE = DATUMWAARDE
|
||||
DAY = DAG
|
||||
|
||||
@@ -58,7 +58,7 @@ class AddressHelper
|
||||
$columnReference = (string) $currentColumnNumber;
|
||||
}
|
||||
// Bracketed C references are relative to the current column
|
||||
if (is_string($columnReference) && $columnReference[0] === '[') { // @phpstan-ignore-line
|
||||
if ($columnReference[0] === '[') {
|
||||
$columnReference = $currentColumnNumber + (int) trim($columnReference, '[]');
|
||||
}
|
||||
$columnReference = (int) $columnReference;
|
||||
|
||||
@@ -7,11 +7,14 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
|
||||
*/
|
||||
interface AddressRange
|
||||
{
|
||||
public const MAX_ROW = 1048576;
|
||||
|
||||
public const MAX_ROW = 1_048_576;
|
||||
public const MAX_COLUMN = 'XFD';
|
||||
public const MAX_COLUMN_INT = 16_384;
|
||||
|
||||
public const MAX_COLUMN_INT = 16384;
|
||||
public const MAX_ROW_XLS_OLD = 16_384;
|
||||
public const MAX_ROW_XLS = 65_536;
|
||||
public const MAX_COLUMN_XLS = 'IV';
|
||||
public const MAX_COLUMN_INT_XLS = 256;
|
||||
|
||||
/**
|
||||
* @return T
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
|
||||
// Check for currency
|
||||
if (preg_match(FormattedNumber::currencyMatcherRegexp(), (string) 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;
|
||||
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null; //* @phpstan-ignore nullCoalesce.unnecessary (Not sure whether Phpstan is correct)
|
||||
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'] ?? '';
|
||||
/** @var string */
|
||||
$temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value));
|
||||
|
||||
+170
-18
@@ -15,6 +15,7 @@ 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\BaseDrawing;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Stringable;
|
||||
@@ -190,12 +191,22 @@ class Cell implements Stringable
|
||||
public function getFormattedValue(): string
|
||||
{
|
||||
$currentCalendar = SharedDate::getExcelCalendar();
|
||||
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
|
||||
$formattedValue = (string) NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
|
||||
SharedDate::setExcelCalendar(
|
||||
$this->getWorksheet()
|
||||
->getParent()
|
||||
?->getExcelCalendar()
|
||||
);
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
|
||||
try {
|
||||
$formattedValue = NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()
|
||||
->getNumberFormat()
|
||||
->getFormatCode(true)
|
||||
);
|
||||
} finally {
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
}
|
||||
|
||||
return $formattedValue;
|
||||
}
|
||||
@@ -233,6 +244,9 @@ class Cell implements Stringable
|
||||
*/
|
||||
public function setValue(mixed $value, ?IValueBinder $binder = null): self
|
||||
{
|
||||
if ($this->hadHyperlink) {
|
||||
$this->clearHyperlink();
|
||||
}
|
||||
// Cells?->Worksheet?->Spreadsheet
|
||||
$binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
|
||||
if (!$binder->bindValue($this, $value)) {
|
||||
@@ -242,11 +256,31 @@ class Cell implements Stringable
|
||||
return $this;
|
||||
}
|
||||
|
||||
private bool $hadHyperlink = false;
|
||||
|
||||
/** @internal */
|
||||
public function setHadHyperlink(bool $hadHyperlink): void
|
||||
{
|
||||
$this->hadHyperlink = $hadHyperlink;
|
||||
}
|
||||
|
||||
private function clearHyperlink(): void
|
||||
{
|
||||
$worksheet = $this->getWorksheetOrNull();
|
||||
if ($worksheet !== null) {
|
||||
$coordinate = $this->getCoordinate();
|
||||
$worksheet->setHyperlink($coordinate, null);
|
||||
}
|
||||
$this->hadHyperlink = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_*
|
||||
* This parameter is currently optional (default = string).
|
||||
* Omitting it is ***DEPRECATED***, and the default will be removed in a future release.
|
||||
* 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.
|
||||
@@ -257,6 +291,9 @@ class Cell implements Stringable
|
||||
*/
|
||||
public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
|
||||
{
|
||||
if ($this->hadHyperlink) {
|
||||
$this->clearHyperlink();
|
||||
}
|
||||
$oldValue = $this->value;
|
||||
$quotePrefix = false;
|
||||
|
||||
@@ -307,6 +344,14 @@ class Cell implements Stringable
|
||||
$this->value = SharedDate::convertIsoDate($value);
|
||||
$dataType = DataType::TYPE_NUMERIC;
|
||||
|
||||
break;
|
||||
case DataType::TYPE_DRAWING_IN_CELL:
|
||||
if ($value instanceof BaseDrawing) {
|
||||
$this->value = $value;
|
||||
} else {
|
||||
throw new SpreadsheetException('Item is not a drawing');
|
||||
}
|
||||
|
||||
break;
|
||||
case DataType::TYPE_ERROR:
|
||||
$this->value = DataType::checkErrorCode($value);
|
||||
@@ -325,16 +370,16 @@ class Cell implements Stringable
|
||||
$worksheet = $this->getWorksheet();
|
||||
$spreadsheet = $worksheet->getParent();
|
||||
if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
|
||||
$originalSelected = $worksheet->getSelectedCells();
|
||||
$activeSheetIndex = $spreadsheet->getActiveSheetIndex();
|
||||
$style = $this->getStyle();
|
||||
$oldQuotePrefix = $style->getQuotePrefix();
|
||||
// Avoid Worksheet::getStyle() (selection + validation) unless quotePrefix must change.
|
||||
$oldQuotePrefix = $spreadsheet->getCellXfByIndex($this->getXfIndex())->getQuotePrefix();
|
||||
if ($oldQuotePrefix !== $quotePrefix) {
|
||||
$style->setQuotePrefix($quotePrefix);
|
||||
}
|
||||
$worksheet->setSelectedCells($originalSelected);
|
||||
if ($activeSheetIndex >= 0) {
|
||||
$spreadsheet->setActiveSheetIndex($activeSheetIndex);
|
||||
$originalSelected = $worksheet->getSelectedCells();
|
||||
$activeSheetIndex = $spreadsheet->getActiveSheetIndex();
|
||||
$this->getStyle()->setQuotePrefix($quotePrefix);
|
||||
$worksheet->setSelectedCells($originalSelected);
|
||||
if ($activeSheetIndex >= 0) {
|
||||
$spreadsheet->setActiveSheetIndex($activeSheetIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +436,7 @@ class Cell implements Stringable
|
||||
$value = array_shift($value);
|
||||
}
|
||||
|
||||
return StringHelper::convertToString($value, false);
|
||||
return StringHelper::convertToString($value, false, convertBool: true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +548,7 @@ class Cell implements Stringable
|
||||
$coordinate = $this->getCoordinate();
|
||||
$ref = $oldAttributesRef;
|
||||
if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
|
||||
if (isset($matches[3])) {
|
||||
if (isset($matches[5])) {
|
||||
$minCol = $matches[1];
|
||||
$minRow = (int) $matches[2];
|
||||
$maxCol = $matches[4];
|
||||
@@ -746,7 +791,8 @@ class Cell implements Stringable
|
||||
throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
|
||||
}
|
||||
|
||||
return $this->getWorksheet()->getHyperlink($this->getCoordinate());
|
||||
return $this->getWorksheet()
|
||||
->getHyperlink($this->getCoordinate());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,7 +806,8 @@ class Cell implements Stringable
|
||||
throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
|
||||
}
|
||||
|
||||
$this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
|
||||
$this->getWorksheet()
|
||||
->setHyperlink($this->getCoordinate(), $hyperlink);
|
||||
|
||||
return $this->updateInCollection();
|
||||
}
|
||||
@@ -973,6 +1020,19 @@ class Cell implements Stringable
|
||||
return $this->updateInCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XF index without triggering updateInCollection().
|
||||
*
|
||||
* This is intended for use by readers that will immediately follow with
|
||||
* setValueExplicit(), avoiding a redundant cache write.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setXfIndexNoUpdate(int $indexValue): void
|
||||
{
|
||||
$this->xfIndex = $indexValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the formula attributes.
|
||||
*
|
||||
@@ -1034,4 +1094,96 @@ class Cell implements Stringable
|
||||
|
||||
return $hidden !== Protection::PROTECTION_UNPROTECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $right positions to the right of this one.
|
||||
*/
|
||||
public function cursorRight(int $right = 1): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$col = $this->getColumn();
|
||||
$colIndex = Coordinate::columnIndexFromString($col);
|
||||
$newCol = max(
|
||||
1,
|
||||
min($colIndex + $right, AddressRange::MAX_COLUMN_INT)
|
||||
);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $left positions to the left of this one.
|
||||
*/
|
||||
public function cursorLeft(int $left = 1): self
|
||||
{
|
||||
return $this->cursorRight(-$left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $down positions below this one.
|
||||
*/
|
||||
public function cursorDown(int $down = 1): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$col = $this->getColumn();
|
||||
$newRow = max(
|
||||
1,
|
||||
min($row + $down, AddressRange::MAX_ROW)
|
||||
);
|
||||
|
||||
return $this->getWorksheet()->getCell("$col$newRow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $up positions above this one.
|
||||
*/
|
||||
public function cursorUp(int $up = 1): self
|
||||
{
|
||||
return $this->cursorDown(-$up);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell at row $row in current column.
|
||||
*/
|
||||
public function cursorRow(int $row = 1): self
|
||||
{
|
||||
$col = $this->getColumn();
|
||||
$newRow = max(
|
||||
1,
|
||||
min($row, AddressRange::MAX_ROW)
|
||||
);
|
||||
|
||||
return $this->getWorksheet()->getCell("$col$newRow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell at column $column in current row.
|
||||
*/
|
||||
public function cursorColumn(string $column = 'A'): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$colIndex = Coordinate::columnIndexFromString($column);
|
||||
$newCol = max(
|
||||
1,
|
||||
min($colIndex, AddressRange::MAX_COLUMN_INT)
|
||||
);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell adjusted for Xls limits if applicable.
|
||||
*/
|
||||
public function cursorXlsLimits(): self
|
||||
{
|
||||
$row = min($this->getRow(), AddressRange::MAX_ROW_XLS);
|
||||
$column = $this->getColumn();
|
||||
$colIndex = Coordinate::columnIndexFromString($column);
|
||||
$newCol = min($colIndex, AddressRange::MAX_COLUMN_INT_XLS);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ abstract class Coordinate
|
||||
public static function coordinateFromString(string $cellAddress): array
|
||||
{
|
||||
if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) {
|
||||
return [$matches['col'], $matches['row']];
|
||||
$row = (int) ltrim($matches['row'], '$');
|
||||
// reluctantly allow row 0 due to regression problems
|
||||
if (/*$row > 0 &&*/ $row <= AddressRange::MAX_ROW) {
|
||||
return [$matches['col'], $matches['row']];
|
||||
}
|
||||
} elseif (self::coordinateIsRange($cellAddress)) {
|
||||
throw new Exception('Cell coordinate string can not be a range of cells');
|
||||
} elseif ($cellAddress == '') {
|
||||
@@ -186,7 +190,7 @@ abstract class Coordinate
|
||||
/**
|
||||
* Build range from coordinate strings.
|
||||
*
|
||||
* @param mixed[] $range Array containing one or more arrays containing one or two coordinate strings
|
||||
* @param array<array<string>> $range Array containing one or more arrays containing one or two coordinate strings
|
||||
*
|
||||
* @return string String representation of $pRange
|
||||
*/
|
||||
@@ -206,6 +210,7 @@ abstract class Coordinate
|
||||
$range[$i] = implode(':', $range[$i]);
|
||||
}
|
||||
|
||||
/** @var array<string> $range */
|
||||
return implode(',', $range);
|
||||
}
|
||||
|
||||
@@ -316,7 +321,7 @@ abstract class Coordinate
|
||||
|
||||
$worksheet = $matches['worksheet'];
|
||||
if ($worksheet !== '') {
|
||||
if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") {
|
||||
if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) {
|
||||
$worksheet = substr($worksheet, 1, -1);
|
||||
}
|
||||
$data['worksheet'] = strtolower($worksheet);
|
||||
@@ -401,7 +406,7 @@ abstract class Coordinate
|
||||
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.
|
||||
// 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.
|
||||
/** @var array<string, int> */
|
||||
static $columnLookup = [
|
||||
@@ -420,22 +425,28 @@ abstract class Coordinate
|
||||
$indexCache[$columnAddress] = $columnLookup[$columnAddress];
|
||||
|
||||
return $indexCache[$columnAddress];
|
||||
} elseif (!isset($columnAddress[2])) {
|
||||
}
|
||||
if (!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
|
||||
}
|
||||
if (!isset($columnAddress[3])) {
|
||||
$temp = $columnLookup[$columnAddress[0]] * 676
|
||||
+ $columnLookup[$columnAddress[1]] * 26
|
||||
+ $columnLookup[$columnAddress[2]];
|
||||
|
||||
return $indexCache[$columnAddress];
|
||||
if ($temp <= AddressRange::MAX_COLUMN_INT) {
|
||||
$indexCache[$columnAddress] = $temp;
|
||||
|
||||
return $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')
|
||||
'Column string index can not be ' . ((isset($columnAddress[0])) ? ('beyond ' . AddressRange::MAX_COLUMN) : 'empty')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,11 +457,19 @@ abstract class Coordinate
|
||||
*
|
||||
* @param int|numeric-string $columnIndex Column index (A = 1)
|
||||
*/
|
||||
public static function stringFromColumnIndex(int|string $columnIndex): string
|
||||
public static function stringFromColumnIndex(int|string $columnIndex, bool $tolerateZero = false): string
|
||||
{
|
||||
/** @var string[] */
|
||||
static $indexCache = [];
|
||||
$columnIndex2 = (int) $columnIndex;
|
||||
if ($columnIndex2 === 0 && $tolerateZero) {
|
||||
return '';
|
||||
}
|
||||
if ($columnIndex2 < 1 || $columnIndex2 > AddressRange::MAX_COLUMN_INT) {
|
||||
throw new Exception("Invalid column index $columnIndex");
|
||||
}
|
||||
|
||||
$columnIndex = $columnIndex2;
|
||||
if (!isset($indexCache[$columnIndex])) {
|
||||
$indexValue = $columnIndex;
|
||||
$base26 = '';
|
||||
@@ -503,8 +522,7 @@ abstract class Coordinate
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
$cellList = array_merge(...$cells); //* @phpstan-ignore-line
|
||||
// Unsure how to satisfy phpstan in line above
|
||||
$cellList = array_merge(...$cells); //* @phpstan-ignore argument.type (Unsure how to satisfy phpstan)
|
||||
|
||||
$retVal = array_map(
|
||||
fn (string $cellAddress) => ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress,
|
||||
@@ -516,7 +534,7 @@ abstract class Coordinate
|
||||
|
||||
/**
|
||||
* @param mixed[] $operators
|
||||
* @param mixed[][] $cells
|
||||
* @param string[][] $cells
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
@@ -554,7 +572,7 @@ abstract class Coordinate
|
||||
$row = 0;
|
||||
sscanf($coordinate, '%[A-Z]%d', $column, $row);
|
||||
/** @var int $row */
|
||||
$key = (--$row * 16384) + self::columnIndexFromString((string) $column);
|
||||
$key = (--$row * AddressRange::MAX_COLUMN_INT) + self::columnIndexFromString((string) $column);
|
||||
$sortKeys[$key] = $coordinate;
|
||||
}
|
||||
ksort($sortKeys);
|
||||
@@ -628,10 +646,8 @@ abstract class Coordinate
|
||||
|
||||
// Range...
|
||||
[$rangeStart, $rangeEnd] = $range;
|
||||
[$startColumn, $startRow] = self::coordinateFromString($rangeStart);
|
||||
[$endColumn, $endRow] = self::coordinateFromString($rangeEnd);
|
||||
$startColumnIndex = self::columnIndexFromString($startColumn);
|
||||
$endColumnIndex = self::columnIndexFromString($endColumn);
|
||||
[$startColumnIndex, $startRow, $startColumn] = self::indexesFromString($rangeStart);
|
||||
[$endColumnIndex, $endRow, $endColumn] = self::indexesFromString($rangeEnd);
|
||||
++$endColumnIndex;
|
||||
|
||||
// Current data
|
||||
@@ -642,8 +658,6 @@ abstract class Coordinate
|
||||
|
||||
// Loop cells
|
||||
while ($currentColumnIndex < $endColumnIndex) {
|
||||
/** @var int $currentRow */
|
||||
/** @var int $endRow */
|
||||
while ($currentRow <= $endRow) {
|
||||
$returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
|
||||
++$currentRow;
|
||||
@@ -671,7 +685,7 @@ abstract class Coordinate
|
||||
*
|
||||
* @param array<string, mixed> $coordinateCollection associative array mapping coordinates to values
|
||||
*
|
||||
* @return array<string, mixed> associative array mapping coordinate ranges to valuea
|
||||
* @return array<string, mixed> associative array mapping coordinate ranges to values
|
||||
*/
|
||||
public static function mergeRangesInCollection(array $coordinateCollection): array
|
||||
{
|
||||
@@ -726,7 +740,7 @@ abstract class Coordinate
|
||||
}
|
||||
}
|
||||
|
||||
if ($rowStart !== null) { // @phpstan-ignore-line
|
||||
if ($rowStart !== null) {
|
||||
if ($rowStart == $rowEnd) {
|
||||
$ranges[] = $hashedValue->col . $rowStart;
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,7 @@ class DataType
|
||||
const TYPE_INLINE = 'inlineStr';
|
||||
const TYPE_ERROR = 'e';
|
||||
const TYPE_ISO_DATE = 'd';
|
||||
const TYPE_DRAWING_IN_CELL = 'drawingCell';
|
||||
|
||||
/**
|
||||
* List of error codes.
|
||||
|
||||
+12
-4
@@ -4,11 +4,12 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use DateTimeInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\CalculationParserOnly;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
|
||||
use Stringable;
|
||||
|
||||
class DefaultValueBinder implements IValueBinder
|
||||
@@ -33,6 +34,11 @@ class DefaultValueBinder implements IValueBinder
|
||||
$value = $value->format('Y-m-d H:i:s');
|
||||
} elseif ($value instanceof Stringable) {
|
||||
$value = (string) $value;
|
||||
} elseif ($value instanceof BaseDrawing) {
|
||||
$value->setCoordinates($cell->getCoordinate());
|
||||
$value->setResizeProportional(false);
|
||||
$value->setInCell(true);
|
||||
$value->setWorksheet($cell->getWorksheet(), true);
|
||||
} else {
|
||||
throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value));
|
||||
}
|
||||
@@ -68,6 +74,9 @@ class DefaultValueBinder implements IValueBinder
|
||||
if ($value instanceof RichText) {
|
||||
return DataType::TYPE_INLINE;
|
||||
}
|
||||
if ($value instanceof BaseDrawing) {
|
||||
return DataType::TYPE_DRAWING_IN_CELL;
|
||||
}
|
||||
if ($value instanceof Stringable) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
@@ -77,8 +86,7 @@ class DefaultValueBinder implements IValueBinder
|
||||
throw new SpreadsheetException("unusable type $gettype");
|
||||
}
|
||||
if (strlen($value) > 1 && $value[0] === '=') {
|
||||
$calculation = new Calculation();
|
||||
$calculation->disableBranchPruning();
|
||||
$calculation = CalculationParserOnly::getParserInstance();
|
||||
|
||||
try {
|
||||
if (empty($calculation->parseFormula($value))) {
|
||||
@@ -107,7 +115,7 @@ class DefaultValueBinder implements IValueBinder
|
||||
return DataType::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
if (!is_numeric($value)) {
|
||||
if (!is_numeric($value) || !is_finite((float) $value)) {
|
||||
return DataType::TYPE_STRING;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class Hyperlink
|
||||
*/
|
||||
private string $tooltip;
|
||||
|
||||
private string $display = '';
|
||||
|
||||
/**
|
||||
* Create a new Hyperlink.
|
||||
*
|
||||
@@ -80,6 +82,23 @@ class Hyperlink
|
||||
return $this->isInternal() ? '' : 'External';
|
||||
}
|
||||
|
||||
public function getDisplay(): string
|
||||
{
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be displayed in cell rather than actual cell contents.
|
||||
* It seems to be ignored by Excel.
|
||||
* It may be used by Google Sheets.
|
||||
*/
|
||||
public function setDisplay(string $display): self
|
||||
{
|
||||
$this->display = $display;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash code.
|
||||
*
|
||||
@@ -89,7 +108,11 @@ class Hyperlink
|
||||
{
|
||||
return md5(
|
||||
$this->url
|
||||
. ','
|
||||
. $this->tooltip
|
||||
. ','
|
||||
. $this->display
|
||||
. ','
|
||||
. __CLASS__
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class IgnoredErrors
|
||||
|
||||
private bool $evalError = false;
|
||||
|
||||
private bool $misleadingFormat = false;
|
||||
|
||||
public function setNumberStoredAsText(bool $value): self
|
||||
{
|
||||
$this->numberStoredAsText = $value;
|
||||
@@ -73,4 +75,16 @@ class IgnoredErrors
|
||||
{
|
||||
return $this->evalError;
|
||||
}
|
||||
|
||||
public function setMisleadingFormat(bool $value): self
|
||||
{
|
||||
$this->misleadingFormat = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMisleadingFormat(): bool
|
||||
{
|
||||
return $this->misleadingFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ class Axis extends Properties
|
||||
{
|
||||
parent::__clone();
|
||||
$this->majorGridlines = ($this->majorGridlines === null) ? null : clone $this->majorGridlines;
|
||||
$this->majorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
|
||||
$this->minorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
|
||||
$this->axisText = ($this->axisText === null) ? null : clone $this->axisText;
|
||||
$this->dispUnitsTitle = ($this->dispUnitsTitle === null) ? null : clone $this->dispUnitsTitle;
|
||||
$this->fillColor = clone $this->fillColor;
|
||||
|
||||
@@ -110,6 +110,37 @@ class Chart
|
||||
|
||||
private bool $roundedCorners = false;
|
||||
|
||||
private bool $date1904 = false;
|
||||
|
||||
private string $lang = 'en-GB';
|
||||
|
||||
/** @var array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
private array $pageMargins = [
|
||||
'b' => '0.75',
|
||||
'l' => '0.7',
|
||||
'r' => '0.7',
|
||||
't' => '0.75',
|
||||
'header' => '0.3',
|
||||
'footer' => '0.3',
|
||||
];
|
||||
|
||||
/** @var array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
private array $pageSetup = [
|
||||
'orientation' => 'portrait',
|
||||
];
|
||||
|
||||
private GridLines $borderLines;
|
||||
|
||||
private ChartColor $fillColor;
|
||||
@@ -680,9 +711,11 @@ class Chart
|
||||
return $this->autoTitleDeleted;
|
||||
}
|
||||
|
||||
public function setAutoTitleDeleted(bool $autoTitleDeleted): self
|
||||
public function setAutoTitleDeleted(?bool $autoTitleDeleted): self
|
||||
{
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
if (is_bool($autoTitleDeleted)) {
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -725,6 +758,34 @@ class Chart
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate1904(): bool
|
||||
{
|
||||
return $this->date1904;
|
||||
}
|
||||
|
||||
public function setDate1904(?bool $date1904): self
|
||||
{
|
||||
if ($date1904 !== null) {
|
||||
$this->date1904 = $date1904;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLang(): string
|
||||
{
|
||||
return $this->lang;
|
||||
}
|
||||
|
||||
public function setLang(?string $lang): self
|
||||
{
|
||||
if ($lang !== null && $lang !== '') {
|
||||
$this->lang = $lang;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBorderLines(): GridLines
|
||||
{
|
||||
return $this->borderLines;
|
||||
@@ -782,4 +843,58 @@ class Chart
|
||||
$this->borderLines = clone $this->borderLines;
|
||||
$this->fillColor = clone $this->fillColor;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
public function getPageMargins(): array
|
||||
{
|
||||
return $this->pageMargins;
|
||||
}
|
||||
|
||||
/** @param mixed $pageMargins expecting array matching $this->pageMargins */
|
||||
public function setPageMargins(mixed $pageMargins): self
|
||||
{
|
||||
if (is_array($pageMargins)) {
|
||||
foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
|
||||
$value = $pageMargins[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->pageMargins[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
public function getPageSetup(): array
|
||||
{
|
||||
return $this->pageSetup;
|
||||
}
|
||||
|
||||
/** @param mixed $pageSetup expecting array matching $this->pageSetup */
|
||||
public function setPageSetup(mixed $pageSetup): self
|
||||
{
|
||||
if (is_array($pageSetup)) {
|
||||
foreach (['paperSize', 'orientation'] as $key) {
|
||||
$value = $pageSetup[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->pageSetup[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class ChartColor
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param array{value: ?string, alpha: null|int|string, brightness?: null|int|string, type: ?string} $color */
|
||||
/** @param array{value: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: null|string} $color */
|
||||
public function setColorPropertiesArray(array $color): self
|
||||
{
|
||||
return $this->setColorProperties(
|
||||
|
||||
@@ -387,7 +387,7 @@ class DataSeries
|
||||
$plotLabels = $this->plotLabel;
|
||||
$this->plotLabel = [];
|
||||
foreach ($plotLabels as $plotLabel) {
|
||||
$this->plotLabel[] = $plotLabel;
|
||||
$this->plotLabel[] = clone $plotLabel;
|
||||
}
|
||||
$plotCategories = $this->plotCategory;
|
||||
$this->plotCategory = [];
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Chart;
|
||||
|
||||
class DataTable
|
||||
{
|
||||
private bool $showHorizontalBorder = true;
|
||||
|
||||
private bool $showVerticalBorder = true;
|
||||
|
||||
private bool $showOutline = true;
|
||||
|
||||
private bool $showKeys = true;
|
||||
|
||||
public function getShowHorizontalBorder(): bool
|
||||
{
|
||||
return $this->showHorizontalBorder;
|
||||
}
|
||||
|
||||
public function getShowVerticalBorder(): bool
|
||||
{
|
||||
return $this->showVerticalBorder;
|
||||
}
|
||||
|
||||
public function getShowOutline(): bool
|
||||
{
|
||||
return $this->showOutline;
|
||||
}
|
||||
|
||||
public function getShowKeys(): bool
|
||||
{
|
||||
return $this->showKeys;
|
||||
}
|
||||
|
||||
public function setShowHorizontalBorder(bool $showHorizontalBorder): self
|
||||
{
|
||||
$this->showHorizontalBorder = $showHorizontalBorder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowVerticalBorder(bool $showVerticalBorder): self
|
||||
{
|
||||
$this->showVerticalBorder = $showVerticalBorder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowOutline(bool $showOutline): self
|
||||
{
|
||||
$this->showOutline = $showOutline;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowKeys(bool $showKeys): self
|
||||
{
|
||||
$this->showKeys = $showKeys;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,26 @@ class Layout
|
||||
|
||||
private ?Properties $labelEffects = null;
|
||||
|
||||
/** @var array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
private array $bodyPr = [
|
||||
'wrap' => 'square',
|
||||
'lIns' => '38100',
|
||||
'tIns' => '19050',
|
||||
'rIns' => '38100',
|
||||
'bIns' => '19050',
|
||||
'anchor' => 'ctr',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new Layout.
|
||||
*
|
||||
@@ -106,7 +126,22 @@ class Layout
|
||||
*/
|
||||
public function __construct(array $layout = [])
|
||||
{
|
||||
/** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */
|
||||
/** @var array{
|
||||
* layoutTarget?: string,
|
||||
* xMode?: string,
|
||||
* yMode?: string,
|
||||
* x?: float,
|
||||
* y?: float,
|
||||
* w?:float,
|
||||
* h?:float,
|
||||
* dLblPos?: string,
|
||||
* labelFont?: ?mixed,
|
||||
* labelFontColor?: ?mixed,
|
||||
* labelEffects?: ?mixed,
|
||||
* numFmtCode?: string,
|
||||
* bodyPr?: mixed,
|
||||
* } $layout
|
||||
*/
|
||||
if (isset($layout['layoutTarget'])) {
|
||||
$this->layoutTarget = $layout['layoutTarget'];
|
||||
}
|
||||
@@ -156,6 +191,10 @@ class Layout
|
||||
if ($labelEffects instanceof Properties) {
|
||||
$this->labelEffects = $labelEffects;
|
||||
}
|
||||
$bodyPr = $layout['bodyPr'] ?? null;
|
||||
if (is_array($bodyPr)) {
|
||||
$this->setBodyPr($bodyPr);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed[] $layout */
|
||||
@@ -469,11 +508,7 @@ class Layout
|
||||
|
||||
public function getLabelFontColor(): ?ChartColor
|
||||
{
|
||||
if ($this->labelFont === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->labelFont->getChartColor();
|
||||
return $this->labelFont?->getChartColor();
|
||||
}
|
||||
|
||||
public function setLabelFontColor(?ChartColor $chartColor): self
|
||||
@@ -523,6 +558,45 @@ class Layout
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
public function getBodyPr(): array
|
||||
{
|
||||
return $this->bodyPr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $bodyPr expect array matching $this->bodyPr
|
||||
*/
|
||||
public function setBodyPr(mixed $bodyPr): self
|
||||
{
|
||||
if (is_array($bodyPr)) {
|
||||
foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||
*/
|
||||
|
||||
@@ -66,7 +66,7 @@ class Legend
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legend position as an excel string value.
|
||||
* Get legend position as an Excel string value.
|
||||
*/
|
||||
public function getPosition(): string
|
||||
{
|
||||
@@ -74,7 +74,7 @@ class Legend
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legend position using an excel string value.
|
||||
* Get legend position using an Excel string value.
|
||||
*
|
||||
* @param string $position see self::POSITION_*
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,8 @@ class PlotArea
|
||||
*/
|
||||
private array $plotSeries;
|
||||
|
||||
private ?DataTable $dataTable = null;
|
||||
|
||||
/**
|
||||
* Create a new PlotArea.
|
||||
*
|
||||
@@ -195,16 +197,34 @@ class PlotArea
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDataTable(DataTable $dataTable): self
|
||||
{
|
||||
$this->dataTable = $dataTable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDataTable(): ?DataTable
|
||||
{
|
||||
return $this->dataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->layout = ($this->layout === null) ? null : clone $this->layout;
|
||||
$this->dataTable = ($this->dataTable === null) ? null : clone $this->dataTable;
|
||||
$plotSeries = $this->plotSeries;
|
||||
$this->plotSeries = [];
|
||||
foreach ($plotSeries as $series) {
|
||||
$this->plotSeries[] = clone $series;
|
||||
}
|
||||
$gradientFillStops = $this->gradientFillStops;
|
||||
$this->gradientFillStops = [];
|
||||
foreach ($gradientFillStops as $gradientFillStop) {
|
||||
$this->gradientFillStops[] = [$gradientFillStop[0], clone $gradientFillStop[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +99,11 @@ abstract class Properties
|
||||
const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22;
|
||||
const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23;
|
||||
|
||||
const POINTS_WIDTH_MULTIPLIER = 12700;
|
||||
const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky
|
||||
const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy
|
||||
const POINTS_WIDTH_MULTIPLIER = 12_700;
|
||||
const ANGLE_MULTIPLIER = 60_000; // direction and size-kx size-ky
|
||||
const PERCENTAGE_MULTIPLIER = 100_000; // size sx and sy, and gradient pos
|
||||
const MAX_SKEW_ANGLE_XML = 90 * self::ANGLE_MULTIPLIER - 1;
|
||||
const MAX_SKEW_ANGLE_DEGREES = self::MAX_SKEW_ANGLE_XML / self::ANGLE_MULTIPLIER; // max for size-kx size-ky
|
||||
|
||||
protected bool $objectState = false; // used only for minor gridlines
|
||||
|
||||
@@ -417,7 +419,7 @@ abstract class Properties
|
||||
}
|
||||
|
||||
foreach ($elements as $keys) {
|
||||
$reference = &$reference[$keys]; //* @phpstan-ignore-line
|
||||
$reference = &$reference[$keys]; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I'm not sure what this is doing)
|
||||
}
|
||||
|
||||
return $reference;
|
||||
@@ -519,7 +521,7 @@ abstract class Properties
|
||||
return $this->softEdges['size'];
|
||||
}
|
||||
|
||||
/** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string}|float|string $value */
|
||||
/** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string, sx?: ?float, sy?: ?float, ky?: ?float, kx?: ?float}|float|string $value */
|
||||
public function setShadowProperty(string $propertyName, mixed $value): self
|
||||
{
|
||||
$this->activateObject();
|
||||
@@ -853,7 +855,7 @@ abstract class Properties
|
||||
public function setLineStyleProperty(string $propertyName, mixed $value): self
|
||||
{
|
||||
$this->activateObject();
|
||||
$this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore-line
|
||||
$this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore assign.propertyType (Too complicated)
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
|
||||
|
||||
/**
|
||||
* Jpgraph is not oficially maintained in Composer, so the version there
|
||||
* Jpgraph is not officially maintained in Composer, so the version there
|
||||
* could be out of date. For that reason, all unit test requiring Jpgraph
|
||||
* are skipped. So, do not measure code coverage for this class till that
|
||||
* is fixed.
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ abstract class JpGraphRendererBase implements IRenderer
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be overriden in descendants to do real JpGraph library initialization.
|
||||
* This method should be overridden in descendants to do real JpGraph library initialization.
|
||||
*/
|
||||
abstract protected static function init(): void;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Collection;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
@@ -11,7 +12,8 @@ use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
class Cells
|
||||
{
|
||||
protected const MAX_COLUMN_ID = 16384;
|
||||
/** @deprecated 5.6.0 use AddressRange::MAX_COLUMN_INT */
|
||||
protected const MAX_COLUMN_ID = AddressRange::MAX_COLUMN_INT;
|
||||
|
||||
private CacheInterface $cache;
|
||||
|
||||
@@ -242,9 +244,9 @@ class Cells
|
||||
// Lookup highest column and highest row
|
||||
$maxRow = $maxColumn = 1;
|
||||
foreach ($this->index as $coordinate) {
|
||||
$row = (int) floor(($coordinate - 1) / self::MAX_COLUMN_ID) + 1;
|
||||
$row = (int) floor(($coordinate - 1) / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$maxRow = ($maxRow > $row) ? $maxRow : $row;
|
||||
$column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
|
||||
$column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
|
||||
$maxColumn = ($maxColumn > $column) ? $maxColumn : $column;
|
||||
}
|
||||
|
||||
@@ -274,14 +276,14 @@ class Cells
|
||||
}
|
||||
|
||||
$maxColumn = 1;
|
||||
$toRow = $row * self::MAX_COLUMN_ID;
|
||||
$fromRow = --$row * self::MAX_COLUMN_ID;
|
||||
$toRow = $row * AddressRange::MAX_COLUMN_INT;
|
||||
$fromRow = --$row * AddressRange::MAX_COLUMN_INT;
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate < $fromRow || $coordinate >= $toRow) {
|
||||
continue;
|
||||
}
|
||||
$column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
|
||||
$maxColumn = $maxColumn > $column ? $maxColumn : $column;
|
||||
$column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
|
||||
$maxColumn = max($column, $maxColumn);
|
||||
}
|
||||
|
||||
return Coordinate::stringFromColumnIndex($maxColumn);
|
||||
@@ -304,10 +306,10 @@ class Cells
|
||||
$maxRow = 1;
|
||||
$columnIndex = Coordinate::columnIndexFromString($column);
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) {
|
||||
if ($coordinate % AddressRange::MAX_COLUMN_INT !== $columnIndex) {
|
||||
continue;
|
||||
}
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$maxRow = ($maxRow > $row) ? $maxRow : $row;
|
||||
}
|
||||
|
||||
@@ -371,12 +373,12 @@ class Cells
|
||||
throw new PhpSpreadsheetException('Row number must be a positive integer');
|
||||
}
|
||||
|
||||
$toRow = $row * self::MAX_COLUMN_ID;
|
||||
$fromRow = --$row * self::MAX_COLUMN_ID;
|
||||
$toRow = $row * AddressRange::MAX_COLUMN_INT;
|
||||
$fromRow = --$row * AddressRange::MAX_COLUMN_INT;
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate >= $fromRow && $coordinate < $toRow) {
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
|
||||
$this->delete("{$column}{$row}");
|
||||
}
|
||||
}
|
||||
@@ -393,9 +395,9 @@ class Cells
|
||||
|
||||
$columnIndex = Coordinate::columnIndexFromString($column);
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate % self::MAX_COLUMN_ID === $columnIndex) {
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
|
||||
if ($coordinate % AddressRange::MAX_COLUMN_INT === $columnIndex) {
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
|
||||
$this->delete("{$column}{$row}");
|
||||
}
|
||||
}
|
||||
@@ -443,7 +445,7 @@ class Cells
|
||||
$row = '';
|
||||
sscanf($cellCoordinate, '%[A-Z]%d', $column, $row);
|
||||
/** @var int $row */
|
||||
$this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column);
|
||||
$this->index[$cellCoordinate] = (--$row * AddressRange::MAX_COLUMN_INT) + Coordinate::columnIndexFromString((string) $column);
|
||||
|
||||
// Clear index sorted flag and index caches
|
||||
$this->indexSorted = false;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
|
||||
/**
|
||||
@@ -29,19 +31,16 @@ class Downloader
|
||||
|
||||
public function __construct(string $folder, string $filename, ?string $filetype = null)
|
||||
{
|
||||
if ((is_dir($folder) === false) || (is_readable($folder) === false)) {
|
||||
throw new Exception('Folder is not accessible');
|
||||
}
|
||||
$filepath = "{$folder}/{$filename}";
|
||||
$this->filepath = (string) realpath($filepath);
|
||||
$this->filename = basename($filepath);
|
||||
clearstatcache();
|
||||
if ((is_file($this->filepath) === false) || (is_readable($this->filepath) === false)) {
|
||||
throw new Exception('File not found, or not a regular file, or cannot be read');
|
||||
$filepath = realpath("{$folder}/{$filename}");
|
||||
if ($filepath === false || !is_file($filepath) || !is_readable($filepath)) {
|
||||
throw new Exception('File not found, or cannot be read');
|
||||
}
|
||||
$this->filepath = $filepath;
|
||||
$this->filename = basename($this->filepath);
|
||||
|
||||
$filetype ??= pathinfo($filename, PATHINFO_EXTENSION);
|
||||
if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) {
|
||||
$filetype ??= pathinfo($this->filename, PATHINFO_EXTENSION);
|
||||
if (!array_key_exists(strtolower($filetype), self::CONTENT_TYPES)) {
|
||||
throw new Exception('Invalid filetype: file cannot be downloaded');
|
||||
}
|
||||
$this->filetype = strtolower($filetype);
|
||||
@@ -90,7 +89,8 @@ class Downloader
|
||||
|
||||
// If you're serving to IE over SSL, then the following may be needed
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
|
||||
$dt = new DateTimeImmutable(timezone: new DateTimeZone('UTC'));
|
||||
header('Last-Modified: ' . $dt->format('D, d M Y H:i:s') . ' GMT'); // always modified
|
||||
header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
|
||||
header('Pragma: public'); // HTTP/1.0
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
class Handler
|
||||
/** @internal */
|
||||
final class Handler
|
||||
{
|
||||
private static string $invalidHex = 'Y';
|
||||
|
||||
// A bunch of methods to show that we continue
|
||||
// to capture messages even using PhpUnit 10.
|
||||
public static function suppressed(): bool
|
||||
public static function suppressed(): true
|
||||
{
|
||||
return @trigger_error('hello');
|
||||
}
|
||||
@@ -28,17 +29,17 @@ class Handler
|
||||
return file_get_contents(__FILE__ . 'noexist') !== false;
|
||||
}
|
||||
|
||||
public static function userDeprecated(): bool
|
||||
public static function userDeprecated(): true
|
||||
{
|
||||
return trigger_error('hello', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
public static function userNotice(): bool
|
||||
public static function userNotice(): true
|
||||
{
|
||||
return trigger_error('userNotice', E_USER_NOTICE);
|
||||
}
|
||||
|
||||
public static function userWarning(): bool
|
||||
public static function userWarning(): true
|
||||
{
|
||||
return trigger_error('userWarning', E_USER_WARNING);
|
||||
}
|
||||
|
||||
@@ -131,16 +131,21 @@ class Sample
|
||||
$callStartTime = microtime(true);
|
||||
$writer->save($path);
|
||||
$this->logWrite($writer, $path, $callStartTime);
|
||||
if ($this->isCli() === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
echo '<a href="/download.php?type=' . pathinfo($path, PATHINFO_EXTENSION) . '&name=' . basename($path) . '">Download ' . basename($path) . '</a><br />';
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
$this->addDownloadLink($path);
|
||||
}
|
||||
|
||||
$this->logEndingNotes();
|
||||
}
|
||||
|
||||
public function addDownloadLink(string $path): void
|
||||
{
|
||||
if ($this->isCli() === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
echo '<a href="/download.php?type=' . pathinfo($path, PATHINFO_EXTENSION) . '&name=' . basename($path) . '">Download ' . basename($path) . '</a><br />';
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
protected function isDirOrMkdir(string $folder): bool
|
||||
{
|
||||
return \is_dir($folder) || \mkdir($folder);
|
||||
@@ -242,9 +247,15 @@ class Sample
|
||||
}
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function displayGrid(array $matrix): void
|
||||
public function displayGrid(array $matrix, null|bool|TextGridRightAlign $numbersRight = null): void
|
||||
{
|
||||
$renderer = new TextGrid($matrix, $this->isCli());
|
||||
if (is_bool($numbersRight)) {
|
||||
$numbersRight = $numbersRight ? TextGridRightAlign::numeric : TextGridRightAlign::none;
|
||||
}
|
||||
if ($numbersRight !== null) {
|
||||
$renderer->setNumbersRight($numbersRight);
|
||||
}
|
||||
echo $renderer->render();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class TextGrid
|
||||
{
|
||||
private bool $isCli;
|
||||
protected bool $isCli;
|
||||
|
||||
/** @var mixed[][] */
|
||||
protected array $matrix;
|
||||
@@ -17,16 +17,18 @@ class TextGrid
|
||||
/** @var string[] */
|
||||
protected array $columns;
|
||||
|
||||
private string $gridDisplay;
|
||||
protected string $gridDisplay;
|
||||
|
||||
private bool $rowDividers = false;
|
||||
protected bool $rowDividers = false;
|
||||
|
||||
private bool $rowHeaders = true;
|
||||
protected bool $rowHeaders = true;
|
||||
|
||||
private bool $columnHeaders = true;
|
||||
protected bool $columnHeaders = true;
|
||||
|
||||
protected TextGridRightAlign $numbersRight = TextGridRightAlign::none;
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true)
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true, TextGridRightAlign $numbersRight = TextGridRightAlign::none)
|
||||
{
|
||||
$this->rows = array_keys($matrix);
|
||||
$this->columns = array_keys($matrix[$this->rows[0]]);
|
||||
@@ -44,6 +46,12 @@ class TextGrid
|
||||
$this->rowDividers = $rowDividers;
|
||||
$this->rowHeaders = $rowHeaders;
|
||||
$this->columnHeaders = $columnHeaders;
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
|
||||
public function setNumbersRight(TextGridRightAlign $numbersRight): void
|
||||
{
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
@@ -52,7 +60,7 @@ class TextGrid
|
||||
|
||||
if (!empty($this->rows)) {
|
||||
$maxRow = max($this->rows);
|
||||
$maxRowLength = strlen((string) $maxRow) + 1;
|
||||
$maxRowLength = $this->strlen((string) $maxRow) + 1;
|
||||
$columnWidths = $this->getColumnWidths();
|
||||
|
||||
$this->renderColumnHeader($maxRowLength, $columnWidths);
|
||||
@@ -68,7 +76,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderRows(int $maxRowLength, array $columnWidths): void
|
||||
protected function renderRows(int $maxRowLength, array $columnWidths): void
|
||||
{
|
||||
foreach ($this->matrix as $row => $rowData) {
|
||||
if ($this->rowHeaders) {
|
||||
@@ -86,18 +94,27 @@ class TextGrid
|
||||
* @param mixed[] $rowData
|
||||
* @param int[] $columnWidths
|
||||
*/
|
||||
private function renderCells(array $rowData, array $columnWidths): void
|
||||
protected function renderCells(array $rowData, array $columnWidths): void
|
||||
{
|
||||
foreach ($rowData as $column => $cell) {
|
||||
$valueForLength = $this->getString($cell);
|
||||
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
|
||||
$this->gridDisplay .= '| ';
|
||||
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
|
||||
if ($this->rightAlign($displayCell, $cell)) {
|
||||
$this->gridDisplay .= str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength)) . $displayCell . ' ';
|
||||
} else {
|
||||
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function rightAlign(string $displayCell, mixed $cell = null): bool
|
||||
{
|
||||
return ($this->numbersRight === TextGridRightAlign::numeric && is_numeric($displayCell)) || ($this->numbersRight === TextGridRightAlign::floatOrInt && (is_int($cell) || is_float($cell)));
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
|
||||
protected function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
|
||||
{
|
||||
if (!$this->columnHeaders) {
|
||||
$this->renderFooter($maxRowLength, $columnWidths);
|
||||
@@ -129,7 +146,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderFooter(int $maxRowLength, array $columnWidths): void
|
||||
protected function renderFooter(int $maxRowLength, array $columnWidths): void
|
||||
{
|
||||
if ($this->rowHeaders) {
|
||||
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
|
||||
@@ -142,7 +159,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private function getColumnWidths(): array
|
||||
protected function getColumnWidths(): array
|
||||
{
|
||||
$columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix);
|
||||
$columnWidths = [];
|
||||
@@ -154,7 +171,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param mixed[] $columnData */
|
||||
private function getColumnWidth(array $columnData): int
|
||||
protected function getColumnWidth(array $columnData): int
|
||||
{
|
||||
$columnWidth = 0;
|
||||
$columnData = array_values($columnData);
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
enum TextGridRightAlign
|
||||
{
|
||||
case none;
|
||||
case numeric;
|
||||
case floatOrInt;
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\IReader;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\CsvNoEscape;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\IReader2 as IReader;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
|
||||
|
||||
@@ -24,6 +25,8 @@ abstract class IOFactory
|
||||
public const READER_HTML = 'Html';
|
||||
public const READER_CSV = 'Csv';
|
||||
|
||||
public const USE_CSV_NO_ESCAPE = [self::READER_CSV => CsvNoEscape::class];
|
||||
|
||||
public const WRITER_XLSX = 'Xlsx';
|
||||
public const WRITER_XLS = 'Xls';
|
||||
public const WRITER_ODS = 'Ods';
|
||||
@@ -54,6 +57,31 @@ abstract class IOFactory
|
||||
'Mpdf' => Writer\Pdf\Mpdf::class,
|
||||
];
|
||||
|
||||
/** @internal */
|
||||
public static function restoreDefaultReadersAndWriters(): void
|
||||
{
|
||||
self::$readers = [
|
||||
self::READER_XLSX => Reader\Xlsx::class,
|
||||
self::READER_XLS => Reader\Xls::class,
|
||||
self::READER_XML => Reader\Xml::class,
|
||||
self::READER_ODS => Reader\Ods::class,
|
||||
self::READER_SLK => Reader\Slk::class,
|
||||
self::READER_GNUMERIC => Reader\Gnumeric::class,
|
||||
self::READER_HTML => Reader\Html::class,
|
||||
self::READER_CSV => Reader\Csv::class,
|
||||
];
|
||||
self::$writers = [
|
||||
self::WRITER_XLS => Writer\Xls::class,
|
||||
self::WRITER_XLSX => Writer\Xlsx::class,
|
||||
self::WRITER_ODS => Writer\Ods::class,
|
||||
self::WRITER_CSV => Writer\Csv::class,
|
||||
self::WRITER_HTML => Writer\Html::class,
|
||||
'Tcpdf' => Writer\Pdf\Tcpdf::class,
|
||||
'Dompdf' => Writer\Pdf\Dompdf::class,
|
||||
'Mpdf' => Writer\Pdf\Mpdf::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Writer\IWriter.
|
||||
*/
|
||||
@@ -75,18 +103,22 @@ abstract class IOFactory
|
||||
|
||||
/**
|
||||
* Create IReader.
|
||||
*
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
* Array to use to find reader, self::$readers will be used if $readers is empty.
|
||||
*/
|
||||
public static function createReader(string $readerType): IReader
|
||||
public static function createReader(string $readerType, array $mergeArray = []): IReader
|
||||
{
|
||||
/** @var class-string<IReader> */
|
||||
$className = $readerType;
|
||||
if (!in_array($readerType, self::$readers, true)) {
|
||||
if (!isset(self::$readers[$readerType])) {
|
||||
$readers = empty($mergeArray) ? self::$readers : $mergeArray;
|
||||
if (!in_array($readerType, $readers, true)) {
|
||||
if (!isset($readers[$readerType])) {
|
||||
throw new Reader\Exception("No reader found for type $readerType");
|
||||
}
|
||||
|
||||
// Instantiate reader
|
||||
$className = self::$readers[$readerType];
|
||||
$className = $readers[$readerType];
|
||||
}
|
||||
|
||||
return new $className();
|
||||
@@ -106,10 +138,11 @@ abstract class IOFactory
|
||||
* list of Readers so it will only try the subset that you specify here.
|
||||
* Values in this list can be any of the constant values defined in the set
|
||||
* IOFactory::READER_*.
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
*/
|
||||
public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet
|
||||
public static function load(string $filename, int $flags = 0, ?array $readers = null, array $mergeArray = []): Spreadsheet
|
||||
{
|
||||
$reader = self::createReaderForFile($filename, $readers);
|
||||
$reader = self::createReaderForFile($filename, $readers, $mergeArray);
|
||||
|
||||
return $reader->load($filename, $flags);
|
||||
}
|
||||
@@ -118,10 +151,11 @@ abstract class IOFactory
|
||||
* Identify file type using automatic IReader resolution.
|
||||
*
|
||||
* @param string[] $readers
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
*/
|
||||
public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false): string
|
||||
public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false, array $mergeArray = []): string
|
||||
{
|
||||
$reader = self::createReaderForFile($filename, $readers);
|
||||
$reader = self::createReaderForFile($filename, $readers, $mergeArray);
|
||||
$className = $reader::class;
|
||||
if ($fullClassName) {
|
||||
return $className;
|
||||
@@ -139,12 +173,16 @@ abstract class IOFactory
|
||||
* list of Readers so it will only try the subset that you specify here.
|
||||
* Values in this list can be any of the constant values defined in the set
|
||||
* IOFactory::READER_*.
|
||||
* @param array<string, class-string<IReader>> $mergeArray supplied readers will be merged with
|
||||
* default readers, allowing specification of a partial list.
|
||||
*
|
||||
* @throws Reader\Exception
|
||||
*/
|
||||
public static function createReaderForFile(string $filename, ?array $readers = null): IReader
|
||||
public static function createReaderForFile(string $filename, ?array $readers = null, array $mergeArray = []): IReader
|
||||
{
|
||||
File::assertFile($filename);
|
||||
|
||||
$testReaders = self::$readers;
|
||||
$testReaders = array_merge(self::$readers, $mergeArray);
|
||||
if ($readers !== null) {
|
||||
$readers = array_map('strtoupper', $readers);
|
||||
$testReaders = array_filter(
|
||||
@@ -157,7 +195,7 @@ abstract class IOFactory
|
||||
// First, lucky guess by inspecting file extension
|
||||
$guessedReader = self::getReaderTypeFromExtension($filename);
|
||||
if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) {
|
||||
$reader = self::createReader($guessedReader);
|
||||
$reader = self::createReader($guessedReader, $testReaders);
|
||||
|
||||
// Let's see if we are lucky
|
||||
if ($reader->canRead($filename)) {
|
||||
@@ -170,7 +208,7 @@ abstract class IOFactory
|
||||
foreach ($testReaders as $readerType => $class) {
|
||||
// Ignore our original guess, we know that won't work
|
||||
if ($readerType !== $guessedReader) {
|
||||
$reader = self::createReader($readerType);
|
||||
$reader = self::createReader($readerType, $testReaders);
|
||||
if ($reader->canRead($filename)) {
|
||||
return $reader;
|
||||
}
|
||||
@@ -227,8 +265,7 @@ abstract class IOFactory
|
||||
*/
|
||||
public static function registerWriter(string $writerType, string $writerClass): void
|
||||
{
|
||||
// We want phpstan to validate caller, but still need this test
|
||||
if (!is_a($writerClass, IWriter::class, true)) { //* @phpstan-ignore-line
|
||||
if (!is_a($writerClass, IWriter::class, true)) {
|
||||
throw new Writer\Exception('Registered writers must implement ' . IWriter::class);
|
||||
}
|
||||
|
||||
@@ -242,11 +279,22 @@ abstract class IOFactory
|
||||
*/
|
||||
public static function registerReader(string $readerType, string $readerClass): void
|
||||
{
|
||||
// We want phpstan to validate caller, but still need this test
|
||||
if (!is_a($readerClass, IReader::class, true)) { //* @phpstan-ignore-line
|
||||
if (!is_a($readerClass, IReader::class, true)) {
|
||||
throw new Reader\Exception('Registered readers must implement ' . IReader::class);
|
||||
}
|
||||
|
||||
self::$readers[$readerType] = $readerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, class-string<IReader>>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function getReaders(): array
|
||||
{
|
||||
return self::$readers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
abstract class BaseReader implements IReader
|
||||
abstract class BaseReader implements IReader2
|
||||
{
|
||||
/**
|
||||
* Read data only?
|
||||
@@ -60,6 +60,14 @@ abstract class BaseReader implements IReader
|
||||
*/
|
||||
protected bool $createBlankSheetIfNoneRead = false;
|
||||
|
||||
/**
|
||||
* Enable drawing pass-through?
|
||||
* Identifies whether the Reader should preserve unsupported drawing elements (shapes, grouped images, etc.)
|
||||
* by storing the original XML for pass-through during write operations.
|
||||
* When enabled, drawings cannot be modified programmatically but are preserved exactly.
|
||||
*/
|
||||
protected bool $enableDrawingPassThrough = false;
|
||||
|
||||
/**
|
||||
* IReadFilter instance.
|
||||
*/
|
||||
@@ -72,6 +80,9 @@ abstract class BaseReader implements IReader
|
||||
|
||||
protected ?IValueBinder $valueBinder = null;
|
||||
|
||||
/** @var null|callable(string):bool function to return whether image path is okay */
|
||||
protected $isWhitelisted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->readFilter = new DefaultReadFilter();
|
||||
@@ -82,7 +93,7 @@ abstract class BaseReader implements IReader
|
||||
return $this->readDataOnly;
|
||||
}
|
||||
|
||||
public function setReadDataOnly(bool $readCellValuesOnly): self
|
||||
public function setReadDataOnly(bool $readCellValuesOnly): static
|
||||
{
|
||||
$this->readDataOnly = $readCellValuesOnly;
|
||||
|
||||
@@ -94,7 +105,7 @@ abstract class BaseReader implements IReader
|
||||
return $this->readEmptyCells;
|
||||
}
|
||||
|
||||
public function setReadEmptyCells(bool $readEmptyCells): self
|
||||
public function setReadEmptyCells(bool $readEmptyCells): static
|
||||
{
|
||||
$this->readEmptyCells = $readEmptyCells;
|
||||
|
||||
@@ -106,7 +117,7 @@ abstract class BaseReader implements IReader
|
||||
return $this->ignoreRowsWithNoCells;
|
||||
}
|
||||
|
||||
public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells): self
|
||||
public function setIgnoreRowsWithNoCells(bool $ignoreRowsWithNoCells): static
|
||||
{
|
||||
$this->ignoreRowsWithNoCells = $ignoreRowsWithNoCells;
|
||||
|
||||
@@ -118,13 +129,25 @@ abstract class BaseReader implements IReader
|
||||
return $this->includeCharts;
|
||||
}
|
||||
|
||||
public function setIncludeCharts(bool $includeCharts): self
|
||||
public function setIncludeCharts(bool $includeCharts): static
|
||||
{
|
||||
$this->includeCharts = $includeCharts;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnableDrawingPassThrough(): bool
|
||||
{
|
||||
return $this->enableDrawingPassThrough;
|
||||
}
|
||||
|
||||
public function setEnableDrawingPassThrough(bool $enableDrawingPassThrough): static
|
||||
{
|
||||
$this->enableDrawingPassThrough = $enableDrawingPassThrough;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return null|string[] */
|
||||
public function getLoadSheetsOnly(): ?array
|
||||
{
|
||||
@@ -132,7 +155,7 @@ abstract class BaseReader implements IReader
|
||||
}
|
||||
|
||||
/** @param null|string|string[] $sheetList */
|
||||
public function setLoadSheetsOnly(string|array|null $sheetList): self
|
||||
public function setLoadSheetsOnly(string|array|null $sheetList): static
|
||||
{
|
||||
if ($sheetList === null) {
|
||||
return $this->setLoadAllSheets();
|
||||
@@ -143,7 +166,7 @@ abstract class BaseReader implements IReader
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLoadAllSheets(): self
|
||||
public function setLoadAllSheets(): static
|
||||
{
|
||||
$this->loadSheetsOnly = null;
|
||||
|
||||
@@ -155,7 +178,7 @@ abstract class BaseReader implements IReader
|
||||
return $this->readFilter;
|
||||
}
|
||||
|
||||
public function setReadFilter(IReadFilter $readFilter): self
|
||||
public function setReadFilter(IReadFilter $readFilter): static
|
||||
{
|
||||
$this->readFilter = $readFilter;
|
||||
|
||||
@@ -163,11 +186,12 @@ abstract class BaseReader implements IReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow external images. Use with caution.
|
||||
* Improper specification of these within a spreadsheet
|
||||
* can subject the caller to security exploits.
|
||||
* USE WITH CAUTION (and in conjunction with setIsWhiteListed)!
|
||||
* Allow external images;
|
||||
* these can be specified within a spreadsheet
|
||||
* in a way that can subject the caller to security exploits.
|
||||
*/
|
||||
public function setAllowExternalImages(bool $allowExternalImages): self
|
||||
public function setAllowExternalImages(bool $allowExternalImages): static
|
||||
{
|
||||
$this->allowExternalImages = $allowExternalImages;
|
||||
|
||||
@@ -179,11 +203,27 @@ abstract class BaseReader implements IReader
|
||||
return $this->allowExternalImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* USE WITH CAUTION!
|
||||
* Supply a callback to determine whether a path should be whitelisted,
|
||||
* used in conjunction with setAllowExternalImages;
|
||||
* supplying a method which might return true
|
||||
* can subject the caller to security exploits.
|
||||
*
|
||||
* @param callable(string):bool $isWhitelisted
|
||||
*/
|
||||
public function setIsWhitelisted(callable $isWhitelisted): static
|
||||
{
|
||||
$this->isWhitelisted = $isWhitelisted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blank sheet if none are read,
|
||||
* possibly due to a typo when using LoadSheetsOnly.
|
||||
*/
|
||||
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self
|
||||
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): static
|
||||
{
|
||||
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
|
||||
|
||||
@@ -305,7 +345,7 @@ abstract class BaseReader implements IReader
|
||||
return $this->valueBinder;
|
||||
}
|
||||
|
||||
public function setValueBinder(?IValueBinder $valueBinder): self
|
||||
public function setValueBinder(?IValueBinder $valueBinder): static
|
||||
{
|
||||
$this->valueBinder = $valueBinder;
|
||||
|
||||
|
||||
@@ -34,47 +34,48 @@ class Csv extends BaseReader
|
||||
/**
|
||||
* Input encoding.
|
||||
*/
|
||||
private string $inputEncoding = 'UTF-8';
|
||||
protected string $inputEncoding = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Fallback encoding if guess strikes out.
|
||||
*/
|
||||
private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
|
||||
protected string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
|
||||
|
||||
/**
|
||||
* Delimiter.
|
||||
*/
|
||||
private ?string $delimiter = null;
|
||||
protected ?string $delimiter = null;
|
||||
|
||||
/**
|
||||
* Enclosure.
|
||||
*/
|
||||
private string $enclosure = '"';
|
||||
protected string $enclosure = '"';
|
||||
|
||||
/**
|
||||
* Sheet index to read.
|
||||
*/
|
||||
private int $sheetIndex = 0;
|
||||
protected int $sheetIndex = 0;
|
||||
|
||||
/**
|
||||
* Load rows contiguously.
|
||||
*/
|
||||
private bool $contiguous = false;
|
||||
protected bool $contiguous = false;
|
||||
|
||||
/**
|
||||
* The character that can escape the enclosure.
|
||||
* This will probably become unsupported in Php 9.
|
||||
* Not yet ready to mark deprecated in order to give users
|
||||
* a migration path.
|
||||
* Strongly encourage that this value be set to null-string.
|
||||
*/
|
||||
private ?string $escapeCharacter = null;
|
||||
protected ?string $escapeCharacter = null;
|
||||
|
||||
/**
|
||||
* Callback for setting defaults in construction.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
private static $constructorCallback;
|
||||
protected static $constructorCallback;
|
||||
|
||||
/** Changed from true to false in release 4.0.0 */
|
||||
public const DEFAULT_TEST_AUTODETECT = false;
|
||||
@@ -82,23 +83,23 @@ class Csv extends BaseReader
|
||||
/**
|
||||
* Attempt autodetect line endings (deprecated after PHP8.1)?
|
||||
*/
|
||||
private bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
|
||||
protected bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
|
||||
|
||||
protected bool $castFormattedNumberToNumeric = false;
|
||||
|
||||
protected bool $preserveNumericFormatting = false;
|
||||
|
||||
private bool $preserveNullString = false;
|
||||
protected bool $preserveNullString = false;
|
||||
|
||||
private bool $sheetNameIsFileName = false;
|
||||
protected bool $sheetNameIsFileName = false;
|
||||
|
||||
private string $getTrue = 'true';
|
||||
protected ?string $getTrue = null;
|
||||
|
||||
private string $getFalse = 'false';
|
||||
protected ?string $getFalse = null;
|
||||
|
||||
private string $thousandsSeparator = ',';
|
||||
protected ?string $thousandsSeparator = null;
|
||||
|
||||
private string $decimalSeparator = '.';
|
||||
protected ?string $decimalSeparator = null;
|
||||
|
||||
/**
|
||||
* Create a new CSV Reader instance.
|
||||
@@ -106,7 +107,7 @@ class Csv extends BaseReader
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$callback = self::$constructorCallback;
|
||||
$callback = static::$constructorCallback;
|
||||
if ($callback !== null) {
|
||||
$callback($this);
|
||||
}
|
||||
@@ -120,12 +121,12 @@ class Csv extends BaseReader
|
||||
*/
|
||||
public static function setConstructorCallback(?callable $callback): void
|
||||
{
|
||||
self::$constructorCallback = $callback;
|
||||
static::$constructorCallback = $callback;
|
||||
}
|
||||
|
||||
public static function getConstructorCallback(): ?callable
|
||||
{
|
||||
return self::$constructorCallback;
|
||||
return static::$constructorCallback;
|
||||
}
|
||||
|
||||
public function setInputEncoding(string $encoding): self
|
||||
@@ -248,7 +249,7 @@ class Csv extends BaseReader
|
||||
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
|
||||
}
|
||||
|
||||
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
|
||||
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1, true);
|
||||
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
|
||||
$worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
|
||||
|
||||
@@ -300,18 +301,27 @@ class Csv extends BaseReader
|
||||
}
|
||||
$this->openFile($filename);
|
||||
if ($this->inputEncoding !== 'UTF-8') {
|
||||
fclose($this->fileHandle);
|
||||
$entireFile = file_get_contents($filename);
|
||||
$fileHandle = fopen('php://memory', 'r+b');
|
||||
if ($fileHandle !== false && $entireFile !== false) {
|
||||
$this->fileHandle = $fileHandle;
|
||||
$data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
|
||||
fwrite($this->fileHandle, $data);
|
||||
$this->skipBOM();
|
||||
}
|
||||
$this->convertNonUtf8($filename);
|
||||
}
|
||||
}
|
||||
|
||||
protected function convertNonUtf8(string $filename): void
|
||||
{
|
||||
fclose($this->fileHandle);
|
||||
$entireFile = file_get_contents($filename);
|
||||
if ($entireFile === false) {
|
||||
throw new ReaderException("Unable to get contents of $filename"); // @codeCoverageIgnore
|
||||
}
|
||||
$fileHandle = fopen('php://memory', 'r+b');
|
||||
if ($fileHandle === false) {
|
||||
throw new ReaderException('Unable to open php://memory'); // @codeCoverageIgnore
|
||||
}
|
||||
$this->fileHandle = $fileHandle;
|
||||
$data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
|
||||
fwrite($this->fileHandle, $data);
|
||||
$this->skipBOM();
|
||||
}
|
||||
|
||||
public function setTestAutoDetect(bool $value): self
|
||||
{
|
||||
$this->testAutodetect = $value;
|
||||
@@ -417,10 +427,10 @@ class Csv extends BaseReader
|
||||
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
|
||||
$valueBinder = $this->valueBinder ?? Cell::getValueBinder();
|
||||
$preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
|
||||
$this->getTrue = Calculation::getTRUE();
|
||||
$this->getFalse = Calculation::getFALSE();
|
||||
$this->thousandsSeparator = StringHelper::getThousandsSeparator();
|
||||
$this->decimalSeparator = StringHelper::getDecimalSeparator();
|
||||
$this->getTrue ??= Calculation::getTRUE();
|
||||
$this->getFalse ??= Calculation::getFALSE();
|
||||
$this->thousandsSeparator ??= StringHelper::getThousandsSeparator();
|
||||
$this->decimalSeparator ??= StringHelper::getDecimalSeparator();
|
||||
while (is_array($rowData)) {
|
||||
$noOutputYet = true;
|
||||
$columnLetter = 'A';
|
||||
@@ -465,9 +475,9 @@ class Csv extends BaseReader
|
||||
private function convertBoolean(mixed &$rowDatum): void
|
||||
{
|
||||
if (is_string($rowDatum)) {
|
||||
if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
|
||||
if (strcasecmp((string) $this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
|
||||
$rowDatum = true;
|
||||
} elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
|
||||
} elseif (strcasecmp((string) $this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
|
||||
$rowDatum = false;
|
||||
}
|
||||
} else {
|
||||
@@ -483,15 +493,15 @@ class Csv extends BaseReader
|
||||
$numberFormatMask = '';
|
||||
if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
|
||||
$numeric = str_replace(
|
||||
[$this->thousandsSeparator, $this->decimalSeparator],
|
||||
[(string) $this->thousandsSeparator, (string) $this->decimalSeparator],
|
||||
['', '.'],
|
||||
$rowDatum
|
||||
);
|
||||
|
||||
if (is_numeric($numeric)) {
|
||||
$decimalPos = strpos($rowDatum, $this->decimalSeparator);
|
||||
$decimalPos = strpos($rowDatum, (string) $this->decimalSeparator);
|
||||
if ($this->preserveNumericFormatting === true) {
|
||||
$numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
|
||||
$numberFormatMask = (str_contains($rowDatum, (string) $this->thousandsSeparator))
|
||||
? '#,##0' : '0';
|
||||
if ($decimalPos !== false) {
|
||||
$decimals = strlen($rowDatum) - $decimalPos - 1;
|
||||
@@ -759,4 +769,32 @@ class Csv extends BaseReader
|
||||
{
|
||||
return $version < 90000 ? '\\' : '';
|
||||
}
|
||||
|
||||
public function setGetTrue(?string $getTrue): self
|
||||
{
|
||||
$this->getTrue = $getTrue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setGetFalse(?string $getFalse): self
|
||||
{
|
||||
$this->getFalse = $getFalse;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDecimalSeparator(?string $decimalSeparator): self
|
||||
{
|
||||
$this->decimalSeparator = $decimalSeparator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setThousandsSeparator(?string $thousandsSeparator): self
|
||||
{
|
||||
$this->thousandsSeparator = $thousandsSeparator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||
|
||||
class Delimiter
|
||||
{
|
||||
protected const POTENTIAL_DELIMETERS = [',', ';', "\t", '|', ':', ' ', '~'];
|
||||
protected const POTENTIAL_DELIMITERS = [',', ';', "\t", '|', ':', ' ', '~'];
|
||||
|
||||
/** @var resource */
|
||||
protected $fileHandle;
|
||||
@@ -34,7 +34,7 @@ class Delimiter
|
||||
|
||||
public function getDefaultDelimiter(): string
|
||||
{
|
||||
return self::POTENTIAL_DELIMETERS[0];
|
||||
return self::POTENTIAL_DELIMITERS[0];
|
||||
}
|
||||
|
||||
public function linesCounted(): int
|
||||
@@ -44,8 +44,8 @@ class Delimiter
|
||||
|
||||
protected function countPotentialDelimiters(): void
|
||||
{
|
||||
$this->counts = array_fill_keys(self::POTENTIAL_DELIMETERS, []);
|
||||
$delimiterKeys = array_flip(self::POTENTIAL_DELIMETERS);
|
||||
$this->counts = array_fill_keys(self::POTENTIAL_DELIMITERS, []);
|
||||
$delimiterKeys = array_flip(self::POTENTIAL_DELIMITERS);
|
||||
|
||||
// Count how many times each of the potential delimiters appears in each line
|
||||
$this->numberLines = 0;
|
||||
@@ -61,7 +61,7 @@ class Delimiter
|
||||
$distribution = array_count_values($splitString);
|
||||
$countLine = array_intersect_key($distribution, $delimiterKeys);
|
||||
|
||||
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
|
||||
foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
|
||||
$this->counts[$delimiter][] = $countLine[$delimiter] ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class Delimiter
|
||||
$meanSquareDeviations = [];
|
||||
$middleIdx = (int) floor(($this->numberLines - 1) / 2);
|
||||
|
||||
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
|
||||
foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
|
||||
$series = $this->counts[$delimiter];
|
||||
sort($series);
|
||||
|
||||
@@ -94,7 +94,7 @@ class Delimiter
|
||||
// ... and pick the delimiter with the smallest mean square deviation
|
||||
// (in case of ties, the order in potentialDelimiters is respected)
|
||||
$min = INF;
|
||||
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
|
||||
foreach (self::POTENTIAL_DELIMITERS as $delimiter) {
|
||||
if (!isset($meanSquareDeviations[$delimiter])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Reader;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
|
||||
class CsvNoEscape extends Csv
|
||||
{
|
||||
/**
|
||||
* The character that can escape the enclosure.
|
||||
* Non-null-string will probably become unsupported in Php 9.
|
||||
* It must always be null-string in this class.
|
||||
*/
|
||||
protected ?string $escapeCharacter = '';
|
||||
|
||||
/**
|
||||
* Should not be needed in this class, but we will permit it.
|
||||
*
|
||||
* @var ?callable
|
||||
*/
|
||||
protected static $constructorCallback;
|
||||
|
||||
protected bool $testAutodetect = false;
|
||||
|
||||
/**
|
||||
* Changing escape character not allowed in this class.
|
||||
*/
|
||||
public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self
|
||||
{
|
||||
if ($escapeCharacter !== '') {
|
||||
throw new ReaderException('Escape character must be null string');
|
||||
}
|
||||
|
||||
$this->escapeCharacter = $escapeCharacter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changing auto-detect not allowed in this class.
|
||||
*/
|
||||
public function setTestAutoDetect(bool $value): self
|
||||
{
|
||||
if ($value !== false) {
|
||||
throw new ReaderException('This class requires that testAutoDetect be false');
|
||||
}
|
||||
$this->testAutodetect = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Reader;
|
||||
|
||||
/**
|
||||
* Empty listWorksheetInfo and listWorksheetNames methods,
|
||||
* facilitating migration from IReader to IReader2.
|
||||
*/
|
||||
class EmptyWorksheetInfo
|
||||
{
|
||||
/**
|
||||
* Return empty array as worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
|
||||
*
|
||||
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
|
||||
*/
|
||||
public function listWorksheetInfo(string $filename): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return empty array as names of the worksheets from a file,
|
||||
* possibly without parsing the whole file to a Spreadsheet object.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function listWorksheetNames(string $filename): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user