Skip to content

Commit 08f2f1b

Browse files
authored
Merge pull request #4579 from oleibman/issue1637
Handle Google-only Formulas Exported from Google Sheets
2 parents 653645c + 14a989a commit 08f2f1b

File tree

14 files changed

+154
-27
lines changed

14 files changed

+154
-27
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Calculation extends CalculationLocale
3535
// Opening bracket
3636
const CALCULATION_REGEXP_OPENBRACE = '\(';
3737
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
38-
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
38+
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?((?:__xludf\.)?[\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
3939
// Cell reference (cell or range of cells, with or without a sheet reference)
4040
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
4141
// Used only to detect spill operator #
@@ -1738,6 +1738,16 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
17381738
break;
17391739
// Binary Operators
17401740
case ':': // Range
1741+
if ($operand1Data['type'] === 'Error') {
1742+
$stack->push($operand1Data['type'], $operand1Data['value'], null);
1743+
1744+
break;
1745+
}
1746+
if ($operand2Data['type'] === 'Error') {
1747+
$stack->push($operand2Data['type'], $operand2Data['value'], null);
1748+
1749+
break;
1750+
}
17411751
if ($operand1Data['type'] === 'Defined Name') {
17421752
/** @var array{reference: string} $operand1Data */
17431753
if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) {
@@ -2101,6 +2111,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
21012111
$nextArg = '';
21022112
}
21032113
}
2114+
} elseif (($arg['type'] ?? '') === 'Error') {
2115+
$argValue = $arg['value'];
2116+
if (is_scalar($argValue)) {
2117+
$nextArg = $argValue;
2118+
} elseif (empty($argValue)) {
2119+
$nextArg = '';
2120+
}
21042121
}
21052122
$args[] = $nextArg;
21062123
if ($functionName !== 'MKMATRIX') {
@@ -2231,11 +2248,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null,
22312248
}
22322249
}
22332250
if ($namedRange === null) {
2234-
return $this->raiseFormulaError("undefined name '$definedName'");
2251+
$result = ExcelError::NAME();
2252+
$stack->push('Error', $result, null);
2253+
$this->debugLog->writeDebugLog("Error $result");
2254+
} else {
2255+
$result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== '');
22352256
}
22362257

2237-
$result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== '');
2238-
22392258
if (isset($storeKey)) {
22402259
$branchStore[$storeKey] = $result;
22412260
}
@@ -2501,6 +2520,8 @@ protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throw
25012520
$this->formulaError = $errorMessage;
25022521
$this->cyclicReferenceStack->clear();
25032522
$suppress = $this->suppressFormulaErrors;
2523+
$suppressed = $suppress ? ' $suppressed' : '';
2524+
$this->debugLog->writeDebugLog("Raise Error$suppressed $errorMessage");
25042525
if (!$suppress) {
25052526
throw new Exception($errorMessage, $code, $exception);
25062527
}
@@ -2808,7 +2829,13 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
28082829
$this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result));
28092830
}
28102831

2811-
$stack->push('Defined Name', $result, $namedRange->getName());
2832+
$y = $namedRange->getWorksheet()?->getTitle();
2833+
$x = $namedRange->getLocalOnly();
2834+
if ($x && $y !== null) {
2835+
$stack->push('Defined Name', $result, "'$y'!" . $namedRange->getName());
2836+
} else {
2837+
$stack->push('Defined Name', $result, $namedRange->getName());
2838+
}
28122839

28132840
return $result;
28142841
}

src/PhpSpreadsheet/Calculation/Information/Value.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
88
use PhpOffice\PhpSpreadsheet\Cell\Cell;
99
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10+
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
1011
use PhpOffice\PhpSpreadsheet\NamedRange;
1112
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1213
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -226,8 +227,15 @@ public static function isFormula(mixed $cellReference = '', ?Cell $cell = null):
226227
$worksheet = (!empty($worksheetName))
227228
? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName)
228229
: $cell->getWorksheet();
230+
if ($worksheet === null) {
231+
return ExcelError::REF();
232+
}
229233

230-
return ($worksheet !== null) ? $worksheet->getCell($fullCellReference)->isFormula() : ExcelError::REF();
234+
try {
235+
return $worksheet->getCell($fullCellReference)->isFormula();
236+
} catch (SpreadsheetException) {
237+
return true;
238+
}
231239
}
232240

233241
/**

src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
67
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
78
use PhpOffice\PhpSpreadsheet\Cell\Cell;
89
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10+
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
911
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
1012

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

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

82-
return Coordinate::columnIndexFromString($cellAddress);
84+
try {
85+
return Coordinate::columnIndexFromString($cellAddress);
86+
} catch (SpreadsheetException) {
87+
return ExcelError::NAME();
88+
}
8389
}
8490

8591
/**
@@ -100,6 +106,9 @@ public static function COLUMNS($cellAddress = null)
100106
if (self::cellAddressNullOrWhitespace($cellAddress)) {
101107
return 1;
102108
}
109+
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
110+
return $cellAddress;
111+
}
103112
if (!is_array($cellAddress)) {
104113
return ExcelError::VALUE();
105114
}
@@ -115,9 +124,18 @@ public static function COLUMNS($cellAddress = null)
115124
return $columns;
116125
}
117126

118-
private static function cellRow(?Cell $cell): int
127+
private static function cellRow(?Cell $cell): int|string
128+
{
129+
return ($cell !== null) ? self::convert0ToName($cell->getRow()) : 1;
130+
}
131+
132+
private static function convert0ToName(int|string $result): int|string
119133
{
120-
return ($cell !== null) ? $cell->getRow() : 1;
134+
if (is_int($result) && ($result <= 0 || $result > 1048576)) {
135+
return ExcelError::NAME();
136+
}
137+
138+
return $result;
121139
}
122140

123141
/**
@@ -135,9 +153,9 @@ private static function cellRow(?Cell $cell): int
135153
*
136154
* @param null|mixed[][]|string $cellAddress A reference to a range of cells for which you want the row numbers
137155
*
138-
* @return int|mixed[]
156+
* @return int|mixed[]|string
139157
*/
140-
public static function ROW($cellAddress = null, ?Cell $cell = null): int|array
158+
public static function ROW($cellAddress = null, ?Cell $cell = null): int|string|array
141159
{
142160
if (self::cellAddressNullOrWhitespace($cellAddress)) {
143161
return self::cellRow($cell);
@@ -172,7 +190,7 @@ public static function ROW($cellAddress = null, ?Cell $cell = null): int|array
172190
}
173191
[$cellAddress] = explode(':', $cellAddress);
174192

175-
return (int) preg_replace('/\D/', '', $cellAddress);
193+
return self::convert0ToName((int) preg_replace('/\D/', '', $cellAddress));
176194
}
177195

178196
/**
@@ -193,6 +211,9 @@ public static function ROWS($cellAddress = null)
193211
if (self::cellAddressNullOrWhitespace($cellAddress)) {
194212
return 1;
195213
}
214+
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
215+
return $cellAddress;
216+
}
196217
if (!is_array($cellAddress)) {
197218
return ExcelError::VALUE();
198219
}

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,9 @@ public function getAllSheets(): array
705705
*/
706706
public function getSheetByName(string $worksheetName): ?Worksheet
707707
{
708-
$trimWorksheetName = trim($worksheetName, "'");
708+
$trimWorksheetName = StringHelper::strToUpper(trim($worksheetName, "'"));
709709
foreach ($this->workSheetCollection as $worksheet) {
710-
if (strcasecmp($worksheet->getTitle(), $trimWorksheetName) === 0) {
710+
if (StringHelper::strToUpper($worksheet->getTitle()) === $trimWorksheetName) {
711711
return $worksheet;
712712
}
713713
}
@@ -1031,13 +1031,34 @@ public function getDefinedName(string $definedName, ?Worksheet $worksheet = null
10311031
if ($definedName !== '') {
10321032
$definedName = StringHelper::strToUpper($definedName);
10331033
// first look for global defined name
1034-
if (isset($this->definedNames[$definedName])) {
1035-
$returnValue = $this->definedNames[$definedName];
1034+
foreach ($this->definedNames as $dn) {
1035+
$upper = StringHelper::strToUpper($dn->getName());
1036+
if (
1037+
!$dn->getLocalOnly()
1038+
&& $definedName === $upper
1039+
) {
1040+
$returnValue = $dn;
1041+
1042+
break;
1043+
}
10361044
}
10371045

10381046
// then look for local defined name (has priority over global defined name if both names exist)
1039-
if (($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
1040-
$returnValue = $this->definedNames[$worksheet->getTitle() . '!' . $definedName];
1047+
if ($worksheet !== null) {
1048+
$wsTitle = StringHelper::strToUpper($worksheet->getTitle());
1049+
$definedName = (string) preg_replace('/^.*!/', '', $definedName);
1050+
foreach ($this->definedNames as $dn) {
1051+
$sheet = $dn->getScope() ?? $dn->getWorksheet();
1052+
$upper = StringHelper::strToUpper($dn->getName());
1053+
$upperTitle = StringHelper::strToUpper((string) $sheet?->getTitle());
1054+
if (
1055+
$dn->getLocalOnly()
1056+
&& $upper === $definedName
1057+
&& $upperTitle === $wsTitle
1058+
) {
1059+
return $dn;
1060+
}
1061+
}
10411062
}
10421063
}
10431064

tests/PhpSpreadsheetTests/Calculation/DefinedNamesCalculationTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ public function testNamedRangeCalculations1(string $cellAddress, float $expected
2121

2222
$calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
2323
self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}");
24+
$spreadsheet->disconnectWorksheets();
25+
}
26+
27+
public function testNamedRangeCalculationsIfError(): void
28+
{
29+
$inputFileType = 'Xlsx';
30+
$inputFileName = __DIR__ . '/../../data/Calculation/DefinedNames/NamedRanges.xlsx';
31+
32+
$reader = IOFactory::createReader($inputFileType);
33+
$spreadsheet = $reader->load($inputFileName);
34+
$sheet = $spreadsheet->getActiveSheet();
35+
$sheet->getCell('E1')
36+
->setValue('=IFERROR(CHARGE_RATE, 999)');
37+
$sheet->getCell('F1')
38+
->setValue('=IFERROR(CHARGE_RATX, 999)');
39+
self::assertSame(7.5, $sheet->getCell('E1')->getCalculatedValue());
40+
self::assertSame(999, $sheet->getCell('F1')->getCalculatedValue());
41+
$spreadsheet->disconnectWorksheets();
2442
}
2543

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

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

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

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

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

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

6990
public static function namedRangeCalculationProvider1(): array

tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsFormulaTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ public function testIsFormula(): void
8181
$sheet1->getCell('G5')->setValue('=ISFORMULA(range2f)');
8282
$sheet1->getCell('G6')->setValue('=ISFORMULA(range2t)');
8383
$sheet1->getCell('G7')->setValue('=ISFORMULA(range2ft)');
84-
self::assertSame('#NAME?', $sheet1->getCell('G1')->getCalculatedValue());
84+
self::assertTrue(
85+
$sheet1->getCell('G1')->getCalculatedValue()
86+
);
8587
self::assertFalse($sheet1->getCell('G3')->getCalculatedValue());
8688
self::assertTrue($sheet1->getCell('G4')->getCalculatedValue());
8789
self::assertFalse($sheet1->getCell('G5')->getCalculatedValue());

tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public static function providerIsRef(): array
3636
'quoted sheet name' => [true, "'Worksheet2'!B1:B2"],
3737
'quoted sheet name with apostrophe' => [true, "'Work''sheet2'!B1:B2"],
3838
'named range' => [true, 'NAMED_RANGE'],
39-
'unknown named range' => ['#NAME?', 'xNAMED_RANGE'],
39+
'unknown named range' => [false, 'xNAMED_RANGE'],
4040
'indirect to a cell reference' => [true, 'INDIRECT("A1")'],
4141
'indirect to a worksheet/cell reference' => [true, 'INDIRECT("\'Worksheet\'!A1")'],
4242
'indirect to invalid worksheet/cell reference' => [false, 'INDIRECT("\'Invalid Worksheet\'!A1")'],

tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public function testColumnOnSpreadsheet(mixed $expectedResult, string $cellRefer
2121

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

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

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

6768
$sheet1->getCell('B3')->setValue('=COLUMN(newnr)');
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class Issue1637Test extends TestCase
11+
{
12+
private static string $testbook = 'tests/data/Reader/XLSX/issue.1637.xlsx';
13+
14+
public function testXludf(): void
15+
{
16+
$reader = new Xlsx();
17+
$spreadsheet = $reader->load(self::$testbook);
18+
$sheet = $spreadsheet->getActiveSheet();
19+
self::assertSame(
20+
'=IFERROR(__xludf.DUMMYFUNCTION("flatten(A1:A5, B1:B5)"),1.0)',
21+
$sheet->getCell('C1')->getValue()
22+
);
23+
self::assertSame(1.0, $sheet->getCell('C1')->getCalculatedValue());
24+
$spreadsheet->disconnectWorksheets();
25+
}
26+
}

tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
'unknown name' => ['#NAME?', 'namedrange2'],
1818
'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'],
1919
'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'],
20-
//'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'], // needs investigation
20+
'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'],
2121
];

0 commit comments

Comments
 (0)