Skip to content

Commit de9c461

Browse files
committed
Merge remote-tracking branch 'origin/master'
# Conflicts: # src/PhpSpreadsheet/Calculation/Calculation.php # src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php
2 parents 8d7130c + cff73fb commit de9c461

File tree

22 files changed

+450
-108
lines changed

22 files changed

+450
-108
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12-
- Nothing
12+
- Support for Structured References in the Calculation Engine [PR #3261](https://github.com/PHPOffice/PhpSpreadsheet/pull/3261)
1313

1414
### Changed
1515

docs/topics/reading-and-writing-to-file.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,22 @@ code:
184184
it disables several Office2007 file format options, resulting in a
185185
lower-featured Office2007 spreadsheet.
186186

187+
### Form Control Fields
188+
189+
PhpSpreadsheet offers limited support for Forms Controls (buttons,
190+
checkboxes, etc.). The support is available only for Excel 2007 format,
191+
and is offered solely to allow loading a spreadsheet with such controls
192+
and saving it as a new file.
193+
Support is not available for adding such elements to the spreadsheet,
194+
nor even to locate them to determine their properties
195+
(so you can't modify or delete them).
196+
Modifications to a worksheet with controls are "caveat emptor";
197+
some modifications will work correctly,
198+
but others are very likely to cause problems,
199+
e.g. adding a comment to the worksheet,
200+
or inserting or deleting rows or columns in a manner that would
201+
cause the controls to change location.
202+
187203
## Excel 5 (BIFF) file format
188204

189205
Xls file format is the old Excel file format, implemented in
@@ -1099,7 +1115,7 @@ Flags that are available that can be passed to the Reader in this way include:
10991115
| Slk | N/A | NO | NO |
11001116
| Csv | N/A | NO | NO |
11011117

1102-
Likewise, when saving a file using a Writer, loaded charts wil not be saved unless you explicitly tell the Writer to include them:
1118+
Likewise, when saving a file using a Writer, loaded charts will not be saved unless you explicitly tell the Writer to include them:
11031119

11041120
```php
11051121
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\IOFactory;
4+
5+
require __DIR__ . '/../Header.php';
6+
7+
$helper->log('Start');
8+
9+
$inputFileType = 'Xlsx';
10+
$inputFileName = __DIR__ . '/sampleData/formscomments.xlsx';
11+
12+
$helper->log('Loading file ' . $inputFileName . ' using IOFactory with a defined reader type of ' . $inputFileType);
13+
$reader = IOFactory::createReader($inputFileType);
14+
$helper->log('Loading all WorkSheets');
15+
$reader->setLoadAllSheets();
16+
$spreadsheet = $reader->load($inputFileName);
17+
18+
// Save
19+
$helper->write($spreadsheet, __FILE__, ['Xlsx']);
20+
$spreadsheet->disconnectWorksheets();
21+
22+
$helper->log('end');

samples/Reader/22_Reader_issue1767.php

+1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717

1818
// Save
1919
$helper->write($spreadsheet, __FILE__);
20+
$spreadsheet->disconnectWorksheets();
2021

2122
$helper->log('end');
126 KB
Binary file not shown.

src/PhpSpreadsheet/Calculation/Calculation.php

+14-10
Original file line numberDiff line numberDiff line change
@@ -4732,24 +4732,28 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
47324732
}
47334733

47344734
if ($token instanceof Operands\StructuredReference) {
4735-
// The next step is converting any structured reference to a cell value of range
4736-
// to a new $token value (a cell range), which can then be processed in the following code.
4737-
var_dump($token);
4738-
47394735
if ($cell === null) {
47404736
return $this->raiseFormulaError('Structured References must exist in a Cell context');
47414737
}
47424738

47434739
try {
4744-
$token->parse($cell);
4740+
$cellRange = $token->parse($cell);
4741+
if (strpos($cellRange, ':') !== false) {
4742+
$this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell Range %s', $token->value(), $cellRange);
4743+
$rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $token->value(), $cell);
4744+
$stack->push('Value', $rangeValue);
4745+
$this->debugLog->writeDebugLog('Evaluated Structured Reference %s as a value %s', $token->value(), $this->showValue($rangeValue));
4746+
} else {
4747+
$this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell %s', $token->value(), $cellRange);
4748+
$cellValue = $cell->getWorksheet()->getCell($cellRange)->getCalculatedValue(false);
4749+
$stack->push('Cell Reference', $cellValue, $cellRange);
4750+
$this->debugLog->writeDebugLog('Evaluated Structured Reference %s as a value %s', $token->value(), $this->showValue($cellValue));
4751+
}
47454752
} catch (Exception $e) {
47464753
return $this->raiseFormulaError($e->getMessage());
47474754
}
4748-
die();
4749-
}
4750-
4751-
// 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
4752-
if (!is_numeric($token) && !is_object($token) && isset(self::BINARY_OPERATORS[$token])) {
4755+
} elseif (!is_numeric($token) && !is_object($token) && isset(self::BINARY_OPERATORS[$token])) {
4756+
// 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
47534757
// We must have two operands, error if we don't
47544758
if (($operand2Data = $stack->pop()) === null) {
47554759
return $this->raiseFormulaError('Internal error - Operand value missing from stack');

src/PhpSpreadsheet/Calculation/Engine/Operands/StructuredReference.php

+67-49
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ final class StructuredReference implements Operand
3434

3535
private string $tableName;
3636

37-
private Table $table;
38-
3937
private string $reference;
4038

4139
private ?int $headersRow;
@@ -104,16 +102,16 @@ private function getTableStructure(Cell $cell): void
104102
preg_match(self::TABLE_REFERENCE, $this->value, $matches);
105103

106104
$this->tableName = $matches[1];
107-
$this->table = ($this->tableName === '')
105+
$table = ($this->tableName === '')
108106
? $this->getTableForCell($cell)
109107
: $this->getTableByName($cell);
110108
$this->reference = $matches[2];
111-
$tableRange = Coordinate::getRangeBoundaries($this->table->getRange());
109+
$tableRange = Coordinate::getRangeBoundaries($table->getRange());
112110

113-
$this->headersRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
114-
$this->firstDataRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
115-
$this->totalsRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
116-
$this->lastDataRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
111+
$this->headersRow = ($table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
112+
$this->firstDataRow = ($table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
113+
$this->totalsRow = ($table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
114+
$this->lastDataRow = ($table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
117115

118116
$this->columns = $this->getColumns($cell, $tableRange);
119117
}
@@ -180,16 +178,18 @@ private function getRowReference(Cell $cell): string
180178
foreach ($this->columns as $columnId => $columnName) {
181179
$columnName = str_replace("\u{a0}", ' ', $columnName);
182180
$cellReference = $columnId . $cell->getRow();
181+
$pattern1 = '/\[' . preg_quote($columnName) . '\]/miu';
182+
$pattern2 = '/@' . preg_quote($columnName) . '/miu';
183183
/** @var string $reference */
184-
if (stripos($reference, '[' . $columnName . ']') !== false) {
185-
$reference = preg_replace('/\[' . preg_quote($columnName) . '\]/miu', $cellReference, $reference);
186-
} elseif (stripos($reference, $columnName) !== false) {
187-
$reference = preg_replace('/@' . preg_quote($columnName) . '/miu', $cellReference, $reference);
184+
if (preg_match($pattern1, $reference) === 1) {
185+
$reference = preg_replace($pattern1, $cellReference, $reference);
186+
} elseif (preg_match($pattern2, $reference) === 1) {
187+
$reference = preg_replace($pattern2, $cellReference, $reference);
188188
}
189189
}
190190

191191
/** @var string $reference */
192-
return $this->validateParsedReference(trim($reference, '[]@ '));
192+
return $this->validateParsedReference(trim($reference, '[]@, '));
193193
}
194194

195195
/**
@@ -202,43 +202,10 @@ private function getColumnReference(): string
202202
$startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow;
203203
$endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow;
204204

205-
$rowsSelected = false;
206-
foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
207-
/** @var string $reference */
208-
if (stripos($reference, '[' . $rowReference . ']') !== false) {
209-
$rowsSelected = true;
210-
$startRow = min($startRow, $this->getMinimumRow($rowReference));
211-
$endRow = max($endRow, $this->getMaximumRow($rowReference));
212-
$reference = preg_replace('/\[' . $rowReference . '\],/mui', '', $reference);
213-
}
214-
}
215-
if ($rowsSelected === false) {
216-
// If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
217-
$startRow = $this->firstDataRow;
218-
$endRow = $this->lastDataRow;
219-
}
205+
[$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
206+
$reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
220207

221-
$columnsSelected = false;
222-
foreach ($this->columns as $columnId => $columnName) {
223-
$columnName = str_replace("\u{a0}", ' ', $columnName);
224-
$cellFrom = "{$columnId}{$startRow}";
225-
$cellTo = "{$columnId}{$endRow}";
226-
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
227-
/** @var string $reference */
228-
if (stripos($reference, '[' . $columnName . ']') !== false) {
229-
$columnsSelected = true;
230-
$reference = preg_replace('/\[' . preg_quote($columnName) . '\]/miu', $cellReference, $reference);
231-
} elseif (stripos($reference, $columnName) !== false) {
232-
$reference = preg_replace('/@' . preg_quote($columnName) . '/miu', $cellReference, $reference);
233-
$columnsSelected = true;
234-
}
235-
}
236-
if ($columnsSelected === false) {
237-
return $this->fullData($startRow, $endRow);
238-
}
239-
240-
/** @var string $reference */
241-
$reference = trim($reference, '[]@ ');
208+
$reference = trim($reference, '[]@, ');
242209
if (substr_count($reference, ':') > 1) {
243210
$cells = explode(':', $reference);
244211
$firstCell = array_shift($cells);
@@ -284,6 +251,8 @@ private function getMinimumRow(string $reference): int
284251
case self::ITEM_SPECIFIER_TOTALS:
285252
return $this->totalsRow ?? $this->lastDataRow;
286253
}
254+
255+
return $this->headersRow ?? $this->firstDataRow;
287256
}
288257

289258
private function getMaximumRow(string $reference): int
@@ -297,10 +266,59 @@ private function getMaximumRow(string $reference): int
297266
case self::ITEM_SPECIFIER_TOTALS:
298267
return $this->totalsRow ?? $this->lastDataRow;
299268
}
269+
270+
return $this->totalsRow ?? $this->lastDataRow;
300271
}
301272

302273
public function value(): string
303274
{
304275
return $this->value;
305276
}
277+
278+
/**
279+
* @return array<int, int>
280+
*/
281+
private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
282+
{
283+
$rowsSelected = false;
284+
foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
285+
$pattern = '/\[' . $rowReference . '\]/mui';
286+
/** @var string $reference */
287+
if (preg_match($pattern, $reference) === 1) {
288+
$rowsSelected = true;
289+
$startRow = min($startRow, $this->getMinimumRow($rowReference));
290+
$endRow = max($endRow, $this->getMaximumRow($rowReference));
291+
$reference = preg_replace($pattern, '', $reference);
292+
}
293+
}
294+
if ($rowsSelected === false) {
295+
// If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
296+
$startRow = $this->firstDataRow;
297+
$endRow = $this->lastDataRow;
298+
}
299+
300+
return [$startRow, $endRow];
301+
}
302+
303+
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
304+
{
305+
$columnsSelected = false;
306+
foreach ($this->columns as $columnId => $columnName) {
307+
$columnName = str_replace("\u{a0}", ' ', $columnName);
308+
$cellFrom = "{$columnId}{$startRow}";
309+
$cellTo = "{$columnId}{$endRow}";
310+
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
311+
$pattern = '/\[' . preg_quote($columnName) . '\]/mui';
312+
if (preg_match($pattern, $reference) === 1) {
313+
$columnsSelected = true;
314+
$reference = preg_replace($pattern, $cellReference, $reference);
315+
}
316+
/** @var string $reference */
317+
}
318+
if ($columnsSelected === false) {
319+
return $this->fullData($startRow, $endRow);
320+
}
321+
322+
return $reference;
323+
}
306324
}

src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php

+72-6
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,11 @@ public static function rate($values, $dates, $guess = self::DEFAULT_GUESS)
5151
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
5252
$found = false;
5353
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
54-
if (!is_numeric($f1) || !is_numeric($f2)) {
55-
break;
54+
if (!is_numeric($f1)) {
55+
return $f1;
56+
}
57+
if (!is_numeric($f2)) {
58+
return $f2;
5659
}
5760
$f1 = (float) $f1;
5861
$f2 = (float) $f2;
@@ -68,11 +71,32 @@ public static function rate($values, $dates, $guess = self::DEFAULT_GUESS)
6871
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
6972
}
7073
}
71-
if (!$found) {
72-
return ExcelError::NAN();
74+
if ($found) {
75+
return self::xirrPart3($values, $dates, $x1, $x2);
7376
}
7477

75-
return self::xirrPart3($values, $dates, $x1, $x2);
78+
// Newton-Raphson didn't work - try bisection
79+
$x1 = $guess - 0.5;
80+
$x2 = $guess + 0.5;
81+
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
82+
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
83+
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
84+
if (!is_numeric($f1) || !is_numeric($f2)) {
85+
break;
86+
}
87+
if ($f1 * $f2 <= 0) {
88+
$found = true;
89+
90+
break;
91+
}
92+
$x1 -= 0.5;
93+
$x2 += 0.5;
94+
}
95+
if ($found) {
96+
return self::xirrBisection($values, $dates, $x1, $x2);
97+
}
98+
99+
return ExcelError::NAN();
76100
}
77101

78102
/**
@@ -190,14 +214,53 @@ private static function xirrPart3(array $values, array $dates, float $x1, float
190214
return $rslt;
191215
}
192216

217+
/**
218+
* @return float|string
219+
*/
220+
private static function xirrBisection(array $values, array $dates, float $x1, float $x2)
221+
{
222+
$rslt = ExcelError::NAN();
223+
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
224+
$rslt = ExcelError::NAN();
225+
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
226+
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
227+
if (!is_numeric($f1) || !is_numeric($f2)) {
228+
break;
229+
}
230+
$f1 = (float) $f1;
231+
$f2 = (float) $f2;
232+
if (abs($f1) < self::FINANCIAL_PRECISION && abs($f2) < self::FINANCIAL_PRECISION) {
233+
break;
234+
}
235+
if ($f1 * $f2 > 0) {
236+
break;
237+
}
238+
$rslt = ($x1 + $x2) / 2;
239+
$f3 = self::xnpvOrdered($rslt, $values, $dates, false, true);
240+
if (!is_float($f3)) {
241+
break;
242+
}
243+
if ($f3 * $f1 < 0) {
244+
$x2 = $rslt;
245+
} else {
246+
$x1 = $rslt;
247+
}
248+
if (abs($f3) < self::FINANCIAL_PRECISION) {
249+
break;
250+
}
251+
}
252+
253+
return $rslt;
254+
}
255+
193256
/**
194257
* @param mixed $rate
195258
* @param mixed $values
196259
* @param mixed $dates
197260
*
198261
* @return float|string
199262
*/
200-
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true)
263+
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true, bool $capAtNegative1 = false)
201264
{
202265
$rate = Functions::flattenSingleValue($rate);
203266
$values = Functions::flattenArray($values);
@@ -206,6 +269,9 @@ private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true
206269

207270
try {
208271
self::validateXnpv($rate, $values, $dates);
272+
if ($capAtNegative1 && $rate <= -1) {
273+
$rate = -1.0 + 1.0E-10;
274+
}
209275
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
210276
} catch (Exception $e) {
211277
return $e->getMessage();

0 commit comments

Comments
 (0)