Skip to content

Handle Google-only Formulas Exported from Google Sheets #4579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Calculation extends CalculationLocale
// Opening bracket
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\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
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)
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
// Used only to detect spill operator #
Expand Down Expand Up @@ -1727,6 +1727,16 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
break;
// Binary Operators
case ':': // Range
if ($operand1Data['type'] === 'Error') {
$stack->push($operand1Data['type'], $operand1Data['value'], null);

break;
}
if ($operand2Data['type'] === 'Error') {
$stack->push($operand2Data['type'], $operand2Data['value'], null);

break;
}
if ($operand1Data['type'] === 'Defined Name') {
/** @var array{reference: string} $operand1Data */
if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) {
Expand Down Expand Up @@ -2087,6 +2097,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
$nextArg = '';
}
}
} elseif (($arg['type'] ?? '') === 'Error') {
$argValue = $arg['value'];
if (is_scalar($argValue)) {
$nextArg = $argValue;
} elseif (empty($argValue)) {
$nextArg = '';
}
}
$args[] = $nextArg;
if ($functionName !== 'MKMATRIX') {
Expand Down Expand Up @@ -2217,11 +2234,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
}
}
if ($namedRange === null) {
return $this->raiseFormulaError("undefined name '$definedName'");
$result = ExcelError::NAME();
$stack->push('Error', $result, null);
$this->debugLog->writeDebugLog("Error $result");
} else {
$result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== '');
}

$result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== '');

if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
Expand Down Expand Up @@ -2487,6 +2506,8 @@ protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throw
$this->formulaError = $errorMessage;
$this->cyclicReferenceStack->clear();
$suppress = $this->suppressFormulaErrors;
$suppressed = $suppress ? ' $suppressed' : '';
$this->debugLog->writeDebugLog("Raise Error$suppressed $errorMessage");
if (!$suppress) {
throw new Exception($errorMessage, $code, $exception);
}
Expand Down Expand Up @@ -2788,7 +2809,13 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
$this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result));
}

$stack->push('Defined Name', $result, $namedRange->getName());
$y = $namedRange->getWorksheet()?->getTitle();
$x = $namedRange->getLocalOnly();
if ($x && $y !== null) {
$stack->push('Defined Name', $result, "'$y'!" . $namedRange->getName());
} else {
$stack->push('Defined Name', $result, $namedRange->getName());
}

return $result;
}
Expand Down
10 changes: 9 additions & 1 deletion src/PhpSpreadsheet/Calculation/Information/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\NamedRange;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
Expand Down Expand Up @@ -226,8 +227,15 @@ public static function isFormula(mixed $cellReference = '', ?Cell $cell = null):
$worksheet = (!empty($worksheetName))
? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName)
: $cell->getWorksheet();
if ($worksheet === null) {
return ExcelError::REF();
}

return ($worksheet !== null) ? $worksheet->getCell($fullCellReference)->isFormula() : ExcelError::REF();
try {
return $worksheet->getCell($fullCellReference)->isFormula();
} catch (SpreadsheetException) {
return true;
}
}

/**
Expand Down
37 changes: 29 additions & 8 deletions src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
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\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

class RowColumnInformation
Expand Down Expand Up @@ -40,9 +42,9 @@ private static function cellColumn(?Cell $cell): int
*
* @param null|mixed[]|string $cellAddress A reference to a range of cells for which you want the column numbers
*
* @return int|int[]
* @return int|int[]|string
*/
public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|array
public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|string|array
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellColumn($cell);
Expand Down Expand Up @@ -79,7 +81,11 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|arra

$cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress);

return Coordinate::columnIndexFromString($cellAddress);
try {
return Coordinate::columnIndexFromString($cellAddress);
} catch (SpreadsheetException) {
return ExcelError::NAME();
}
}

/**
Expand All @@ -100,6 +106,9 @@ public static function COLUMNS($cellAddress = null)
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
return $cellAddress;
}
if (!is_array($cellAddress)) {
return ExcelError::VALUE();
}
Expand All @@ -115,9 +124,18 @@ public static function COLUMNS($cellAddress = null)
return $columns;
}

private static function cellRow(?Cell $cell): int
private static function cellRow(?Cell $cell): int|string
{
return ($cell !== null) ? self::convert0ToName($cell->getRow()) : 1;
}

private static function convert0ToName(int|string $result): int|string
{
return ($cell !== null) ? $cell->getRow() : 1;
if (is_int($result) && ($result <= 0 || $result > 1048576)) {
return ExcelError::NAME();
}

return $result;
}

/**
Expand All @@ -135,9 +153,9 @@ private static function cellRow(?Cell $cell): int
*
* @param null|mixed[][]|string $cellAddress A reference to a range of cells for which you want the row numbers
*
* @return int|mixed[]
* @return int|mixed[]|string
*/
public static function ROW($cellAddress = null, ?Cell $cell = null): int|array
public static function ROW($cellAddress = null, ?Cell $cell = null): int|string|array
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellRow($cell);
Expand Down Expand Up @@ -172,7 +190,7 @@ public static function ROW($cellAddress = null, ?Cell $cell = null): int|array
}
[$cellAddress] = explode(':', $cellAddress);

return (int) preg_replace('/\D/', '', $cellAddress);
return self::convert0ToName((int) preg_replace('/\D/', '', $cellAddress));
}

/**
Expand All @@ -193,6 +211,9 @@ public static function ROWS($cellAddress = null)
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
return $cellAddress;
}
if (!is_array($cellAddress)) {
return ExcelError::VALUE();
}
Expand Down
33 changes: 27 additions & 6 deletions src/PhpSpreadsheet/Spreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -691,9 +691,9 @@ public function getAllSheets(): array
*/
public function getSheetByName(string $worksheetName): ?Worksheet
{
$trimWorksheetName = trim($worksheetName, "'");
$trimWorksheetName = StringHelper::strToUpper(trim($worksheetName, "'"));
foreach ($this->workSheetCollection as $worksheet) {
if (strcasecmp($worksheet->getTitle(), $trimWorksheetName) === 0) {
if (StringHelper::strToUpper($worksheet->getTitle()) === $trimWorksheetName) {
return $worksheet;
}
}
Expand Down Expand Up @@ -1017,13 +1017,34 @@ public function getDefinedName(string $definedName, ?Worksheet $worksheet = null
if ($definedName !== '') {
$definedName = StringHelper::strToUpper($definedName);
// first look for global defined name
if (isset($this->definedNames[$definedName])) {
$returnValue = $this->definedNames[$definedName];
foreach ($this->definedNames as $dn) {
$upper = StringHelper::strToUpper($dn->getName());
if (
!$dn->getLocalOnly()
&& $definedName === $upper
) {
$returnValue = $dn;

break;
}
}

// then look for local defined name (has priority over global defined name if both names exist)
if (($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
$returnValue = $this->definedNames[$worksheet->getTitle() . '!' . $definedName];
if ($worksheet !== null) {
$wsTitle = StringHelper::strToUpper($worksheet->getTitle());
$definedName = (string) preg_replace('/^.*!/', '', $definedName);
foreach ($this->definedNames as $dn) {
$sheet = $dn->getScope() ?? $dn->getWorksheet();
$upper = StringHelper::strToUpper($dn->getName());
$upperTitle = StringHelper::strToUpper((string) $sheet?->getTitle());
if (
$dn->getLocalOnly()
&& $upper === $definedName
&& $upperTitle === $wsTitle
) {
return $dn;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ public function testNamedRangeCalculations1(string $cellAddress, float $expected

$calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}");
$spreadsheet->disconnectWorksheets();
}

public function testNamedRangeCalculationsIfError(): void
{
$inputFileType = 'Xlsx';
$inputFileName = __DIR__ . '/../../data/Calculation/DefinedNames/NamedRanges.xlsx';

$reader = IOFactory::createReader($inputFileType);
$spreadsheet = $reader->load($inputFileName);
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('E1')
->setValue('=IFERROR(CHARGE_RATE, 999)');
$sheet->getCell('F1')
->setValue('=IFERROR(CHARGE_RATX, 999)');
self::assertSame(7.5, $sheet->getCell('E1')->getCalculatedValue());
self::assertSame(999, $sheet->getCell('F1')->getCalculatedValue());
$spreadsheet->disconnectWorksheets();
}

#[DataProvider('namedRangeCalculationProvider2')]
Expand All @@ -36,6 +54,7 @@ public function testNamedRangeCalculationsWithAdjustedRateValue(string $cellAddr

$calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}");
$spreadsheet->disconnectWorksheets();
}

#[DataProvider('namedRangeCalculationProvider1')]
Expand All @@ -49,6 +68,7 @@ public function testNamedFormulaCalculations1(string $cellAddress, float $expect

$calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}");
$spreadsheet->disconnectWorksheets();
}

#[DataProvider('namedRangeCalculationProvider2')]
Expand All @@ -64,6 +84,7 @@ public function testNamedFormulaeCalculationsWithAdjustedRateValue(string $cellA

$calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}");
$spreadsheet->disconnectWorksheets();
}

public static function namedRangeCalculationProvider1(): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ public function testIsFormula(): void
$sheet1->getCell('G5')->setValue('=ISFORMULA(range2f)');
$sheet1->getCell('G6')->setValue('=ISFORMULA(range2t)');
$sheet1->getCell('G7')->setValue('=ISFORMULA(range2ft)');
self::assertSame('#NAME?', $sheet1->getCell('G1')->getCalculatedValue());
self::assertTrue(
$sheet1->getCell('G1')->getCalculatedValue()
);
self::assertFalse($sheet1->getCell('G3')->getCalculatedValue());
self::assertTrue($sheet1->getCell('G4')->getCalculatedValue());
self::assertFalse($sheet1->getCell('G5')->getCalculatedValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static function providerIsRef(): array
'quoted sheet name' => [true, "'Worksheet2'!B1:B2"],
'quoted sheet name with apostrophe' => [true, "'Work''sheet2'!B1:B2"],
'named range' => [true, 'NAMED_RANGE'],
'unknown named range' => ['#NAME?', 'xNAMED_RANGE'],
'unknown named range' => [false, 'xNAMED_RANGE'],
'indirect to a cell reference' => [true, 'INDIRECT("A1")'],
'indirect to a worksheet/cell reference' => [true, 'INDIRECT("\'Worksheet\'!A1")'],
'indirect to invalid worksheet/cell reference' => [false, 'INDIRECT("\'Invalid Worksheet\'!A1")'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function testColumnOnSpreadsheet(mixed $expectedResult, string $cellRefer

$sheet1 = $this->getSpreadsheet()->createSheet();
$sheet1->setTitle('OtherSheet');
$this->getSpreadsheet()->addNamedRange(new NamedRange('localname', $sheet1, '$F$6:$H$6', true));

if ($cellReference === 'omitted') {
$sheet->getCell('B3')->setValue('=COLUMN()');
Expand Down Expand Up @@ -61,7 +62,7 @@ public function testCOLUMNSheetWithApostrophe(): void
$sheet = $this->getSheet();

$sheet1 = $this->getSpreadsheet()->createSheet();
$sheet1->setTitle("apo''strophe");
$sheet1->setTitle("apo'strophe");
$this->getSpreadsheet()->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$5', true)); // defined locally, only usable on sheet1

$sheet1->getCell('B3')->setValue('=COLUMN(newnr)');
Expand Down
26 changes: 26 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Xlsx/Issue1637Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;

use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PHPUnit\Framework\TestCase;

class Issue1637Test extends TestCase
{
private static string $testbook = 'tests/data/Reader/XLSX/issue.1637.xlsx';

public function testXludf(): void
{
$reader = new Xlsx();
$spreadsheet = $reader->load(self::$testbook);
$sheet = $spreadsheet->getActiveSheet();
self::assertSame(
'=IFERROR(__xludf.DUMMYFUNCTION("flatten(A1:A5, B1:B5)"),1.0)',
$sheet->getCell('C1')->getValue()
);
self::assertSame(1.0, $sheet->getCell('C1')->getCalculatedValue());
$spreadsheet->disconnectWorksheets();
}
}
2 changes: 1 addition & 1 deletion tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
'unknown name' => ['#NAME?', 'namedrange2'],
'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'],
'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'],
//'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'], // needs investigation
'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'],
];
2 changes: 1 addition & 1 deletion tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
'unknown name' => ['#NAME?', 'namedrange2'],
'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'],
'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'],
//'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function
'qualified name' => [6, 'OtherSheet!localname'],
];
2 changes: 1 addition & 1 deletion tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
'unknown name' => ['#NAME?', 'InvalidCellAddress'],
'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'],
'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'],
//'qualified out of scope $F$6:$H$6' => [1, 'OtherSheet!localname'], // needs investigation
'qualified out of scope $F$6:$H$6' => [1, 'OtherSheet!localname'],
];
2 changes: 1 addition & 1 deletion tests/data/Calculation/LookupRef/ROWonSpreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
'unknown name' => ['#NAME?', 'namedrange2'],
'unknown name as first part of range' => ['#NAME?', 'InvalidCell:A2'],
'unknown name as second part of range' => ['#NAME?', 'A2:InvalidCell'],
//'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function
'qualified name' => [6, 'OtherSheet!localname'],
];
Binary file added tests/data/Reader/XLSX/issue.1637.xlsx
Binary file not shown.