Skip to content

Improved Support for INDIRECT, ROW, and COLUMN Functions #1995

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

Closed
wants to merge 9 commits into from
Closed
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
6 changes: 3 additions & 3 deletions src/PhpSpreadsheet/Calculation/LookupRef.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,16 @@ public static function HYPERLINK($linkURL = '', $displayName = null, ?Cell $pCel
*
* NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
*
* @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
* @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
* @param Cell $pCell The current cell (containing this formula)
*
* @return array|string An array containing a cell or range of cells, or a string on error
*
* @TODO Support for the optional a1 parameter introduced in Excel 2010
*/
public static function INDIRECT($cellAddress = null, ?Cell $pCell = null)
public static function INDIRECT($cellAddress, Cell $pCell)
{
return Indirect::INDIRECT($cellAddress, $pCell);
return Indirect::INDIRECT($cellAddress, true, $pCell);
}

/**
Expand Down
50 changes: 50 additions & 0 deletions src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;

use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

class Helpers
{
private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string
{
if (!$a1) {
$cellAddress1 = AddressHelper::convertToA1($cellAddress1);
if ($cellAddress2) {
$cellAddress2 = AddressHelper::convertToA1($cellAddress2);
}
}

return $cellAddress1 . ($cellAddress2 ? ":$cellAddress2" : '');
}

public static function extractCellAddresses(string $cellAddress, bool $a1, Spreadsheet $spreadsheet, Worksheet $sheet): array
{
$cellAddress1 = $cellAddress;
$cellAddress2 = null;
$namedRanges = $spreadsheet->getNamedRanges();
foreach ($namedRanges as $namedRange) {
$scope = $namedRange->getScope();
if ($cellAddress1 === $namedRange->getName() && ($scope === null || $scope === $sheet)) {
$sheet = '';
if ($namedRange->getWorkSheet() !== null) {
$sheet = $namedRange->getWorkSheet()->getTitle() . '!';
}
$value = preg_replace('/^=/', '', $namedRange->getValue());
$cellAddress1 = $sheet . $value;
$cellAddress = $cellAddress1;
$a1 = true;

break;
}
}
if (strpos($cellAddress, ':') !== false) {
[$cellAddress1, $cellAddress2] = explode(':', $cellAddress);
}
$cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1);

return [$cellAddress1, $cellAddress2, $cellAddress];
}
}
62 changes: 46 additions & 16 deletions src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,75 @@

namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;

use Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

class Indirect
{
/**
* Determine whether cell address is in A1 (true) or R1C1 (false) format.
*
* @param null|bool|float|int|string $a1fmt
*/
private static function a1Format($a1fmt): bool
{
$a1fmt = Functions::flattenSingleValue($a1fmt);
if ($a1fmt === null) {
return true;
}
if (is_string($a1fmt)) {
throw new Exception(Functions::VALUE());
}

return (bool) $a1fmt;
}

/**
* Convert cellAddress to string, verify not null string.
*
* @param array|string $cellAddress
*/
private static function validateAddress($cellAddress): string
{
$cellAddress = Functions::flattenSingleValue($cellAddress);
if (!is_string($cellAddress) || !$cellAddress) {
throw new Exception(Functions::REF());
}

return $cellAddress;
}

/**
* INDIRECT.
*
* Returns the reference specified by a text string.
* References are immediately evaluated to display their contents.
*
* Excel Function:
* =INDIRECT(cellAddress)
* =INDIRECT(cellAddress, bool) where the bool argument is optional
*
* NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010
*
* @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
* @param null|Cell $pCell The current cell (containing this formula)
* @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
* @param null|bool|float|int|string $a1fmt
* @param Cell $pCell The current cell (containing this formula)
*
* @return array|string An array containing a cell or range of cells, or a string on error
*
* @TODO Support for the optional a1 parameter introduced in Excel 2010
*/
public static function INDIRECT($cellAddress = null, ?Cell $pCell = null)
public static function INDIRECT($cellAddress, $a1fmt, Cell $pCell)
{
$cellAddress = Functions::flattenSingleValue($cellAddress);
if ($cellAddress === null || $cellAddress === '' || !is_object($pCell)) {
return Functions::REF();
try {
$a1 = self::a1Format($a1fmt);
$spreadsheet = $pCell->getWorksheet()->getParent();
$cellAddress = self::validateAddress($cellAddress);
} catch (Exception $e) {
return $e->getMessage();
}

[$cellAddress, $pSheet] = self::extractWorksheet($cellAddress, $pCell);

$cellAddress1 = $cellAddress;
$cellAddress2 = null;
if (strpos($cellAddress, ':') !== false) {
[$cellAddress1, $cellAddress2] = explode(':', $cellAddress);
}
[$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $spreadsheet, $pCell->getWorkSheet());

if (
(!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
Expand Down
52 changes: 41 additions & 11 deletions src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@

class RowColumnInformation
{
/**
* Test if cellAddress is null or whitespace string.
*
* @param null|array|string $cellAddress A reference to a range of cells
*/
private static function cellAddressNullOrWhitespace($cellAddress): bool
{
return $cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '');
}

private static function cellColumn(?Cell $pCell): int
{
return ($pCell !== null) ? (int) Coordinate::columnIndexFromString($pCell->getColumn()) : 1;
}

/**
* COLUMN.
*
Expand All @@ -27,10 +42,10 @@ class RowColumnInformation
*
* @return int|int[]
*/
public static function COLUMN($cellAddress = null, ?Cell $cell = null)
public static function COLUMN($cellAddress = null, ?Cell $pCell = null)
{
if ($cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '')) {
return ($cell !== null) ? (int) Coordinate::columnIndexFromString($cell->getColumn()) : 1;
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellColumn($pCell);
}

if (is_array($cellAddress)) {
Expand All @@ -41,7 +56,11 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null)
}
}

[, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true);
$cellAddress = is_string($cellAddress) ? $cellAddress : '';
if ($pCell) {
[, , $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet()->getParent(), $pCell->getWorksheet());
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if (strpos($cellAddress, ':') !== false) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = preg_replace('/[^a-z]/i', '', $startAddress);
Expand Down Expand Up @@ -73,9 +92,10 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null)
*/
public static function COLUMNS($cellAddress = null)
{
if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) {
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
} elseif (!is_array($cellAddress)) {
}
if (!is_array($cellAddress)) {
return Functions::VALUE();
}

Expand All @@ -90,6 +110,11 @@ public static function COLUMNS($cellAddress = null)
return $columns;
}

private static function cellRow(?Cell $pCell): int
{
return ($pCell !== null) ? $pCell->getRow() : 1;
}

/**
* ROW.
*
Expand All @@ -109,8 +134,8 @@ public static function COLUMNS($cellAddress = null)
*/
public static function ROW($cellAddress = null, ?Cell $pCell = null)
{
if ($cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '')) {
return ($pCell !== null) ? $pCell->getRow() : 1;
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellRow($pCell);
}

if (is_array($cellAddress)) {
Expand All @@ -121,7 +146,11 @@ public static function ROW($cellAddress = null, ?Cell $pCell = null)
}
}

[, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true);
$cellAddress = is_string($cellAddress) ? $cellAddress : '';
if ($pCell) {
[, , $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet()->getParent(), $pCell->getWorksheet());
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if (strpos($cellAddress, ':') !== false) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = preg_replace('/\D/', '', $startAddress);
Expand Down Expand Up @@ -154,9 +183,10 @@ function ($value) {
*/
public static function ROWS($cellAddress = null)
{
if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) {
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
} elseif (!is_array($cellAddress)) {
}
if (!is_array($cellAddress)) {
return Functions::VALUE();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;

use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;

class AllSetupTeardown extends TestCase
{
/**
* @var string
*/
protected $compatibilityMode;

/**
* @var Spreadsheet
*/
protected $spreadsheet;

/**
* @var Worksheet
*/
protected $sheet;

protected function setUp(): void
{
$this->compatibilityMode = Functions::getCompatibilityMode();
$this->spreadsheet = new Spreadsheet();
$this->sheet = $this->spreadsheet->getActiveSheet();
}

protected function tearDown(): void
{
Functions::setCompatibilityMode($this->compatibilityMode);
$this->spreadsheet->disconnectWorksheets();
}

protected static function setOpenOffice(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE);
}

protected static function setGnumeric(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC);
}

/**
* @param mixed $expectedResult
*/
protected function mightHaveException($expectedResult): void
{
if ($expectedResult === 'exception') {
$this->expectException(CalcException::class);
}
}

/**
* @param mixed $value
*/
protected function setCell(string $cell, $value): void
{
if ($value !== null) {
if (is_string($value) && is_numeric($value)) {
$this->sheet->getCell($cell)->setValueExplicit($value, DataType::TYPE_STRING);
} else {
$this->sheet->getCell($cell)->setValue($value);
}
}
}
}
Loading