This commit is contained in:
steven 2025-08-11 22:23:30 +02:00
commit 72a26edcff
22092 changed files with 2101903 additions and 0 deletions

View file

@ -0,0 +1,123 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
class Address
{
use ArrayEnabled;
public const ADDRESS_ABSOLUTE = 1;
public const ADDRESS_COLUMN_RELATIVE = 2;
public const ADDRESS_ROW_RELATIVE = 3;
public const ADDRESS_RELATIVE = 4;
public const REFERENCE_STYLE_A1 = true;
public const REFERENCE_STYLE_R1C1 = false;
/**
* ADDRESS.
*
* Creates a cell address as text, given specified row and column numbers.
*
* Excel Function:
* =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText])
*
* @param mixed $row Row number (integer) to use in the cell reference
* Or can be an array of values
* @param mixed $column Column number (integer) to use in the cell reference
* Or can be an array of values
* @param mixed $relativity Integer flag indicating the type of reference to return
* 1 or omitted Absolute
* 2 Absolute row; relative column
* 3 Relative row; absolute column
* 4 Relative
* Or can be an array of values
* @param mixed $referenceStyle A logical (boolean) value that specifies the A1 or R1C1 reference style.
* TRUE or omitted ADDRESS returns an A1-style reference
* FALSE ADDRESS returns an R1C1-style reference
* Or can be an array of values
* @param mixed $sheetName Optional Name of worksheet to use
* Or can be an array of values
*
* @return array|string If an array of values is passed as the $testValue argument, then the returned result will also be
* an array with the same dimensions
*/
public static function cell(mixed $row, mixed $column, mixed $relativity = 1, mixed $referenceStyle = true, mixed $sheetName = ''): array|string
{
if (
is_array($row) || is_array($column)
|| is_array($relativity) || is_array($referenceStyle) || is_array($sheetName)
) {
return self::evaluateArrayArguments(
[self::class, __FUNCTION__],
$row,
$column,
$relativity,
$referenceStyle,
$sheetName
);
}
$relativity = $relativity ?? 1;
$referenceStyle = $referenceStyle ?? true;
if (($row < 1) || ($column < 1)) {
return ExcelError::VALUE();
}
$sheetName = self::sheetName($sheetName);
if (is_int($referenceStyle)) {
$referenceStyle = (bool) $referenceStyle;
}
if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) {
return self::formatAsA1($row, $column, $relativity, $sheetName);
}
return self::formatAsR1C1($row, $column, $relativity, $sheetName);
}
private static function sheetName(string $sheetName): string
{
if ($sheetName > '') {
if (str_contains($sheetName, ' ') || str_contains($sheetName, '[')) {
$sheetName = "'{$sheetName}'";
}
$sheetName .= '!';
}
return $sheetName;
}
private static function formatAsA1(int $row, int $column, int $relativity, string $sheetName): string
{
$rowRelative = $columnRelative = '$';
if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) {
$columnRelative = '';
}
if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) {
$rowRelative = '';
}
$column = Coordinate::stringFromColumnIndex($column);
return "{$sheetName}{$columnRelative}{$column}{$rowRelative}{$row}";
}
private static function formatAsR1C1(int $row, int $column, int $relativity, string $sheetName): string
{
if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) {
$column = "[{$column}]";
}
if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) {
$row = "[{$row}]";
}
[$rowChar, $colChar] = AddressHelper::getRowAndColumnChars();
return "{$sheetName}$rowChar{$row}$colChar{$column}";
}
}

View file

@ -0,0 +1,249 @@
<?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 PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class ExcelMatch
{
use ArrayEnabled;
public const MATCHTYPE_SMALLEST_VALUE = -1;
public const MATCHTYPE_FIRST_VALUE = 0;
public const MATCHTYPE_LARGEST_VALUE = 1;
/**
* MATCH.
*
* The MATCH function searches for a specified item in a range of cells
*
* Excel Function:
* =MATCH(lookup_value, lookup_array, [match_type])
*
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupArray The range of cells being searched
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
* If match_type is 1 or -1, the list has to be ordered.
*
* @return array|float|int|string The relative position of the found item
*/
public static function MATCH(mixed $lookupValue, mixed $lookupArray, mixed $matchType = self::MATCHTYPE_LARGEST_VALUE): array|string|int|float
{
if (is_array($lookupValue)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
}
$lookupArray = Functions::flattenArray($lookupArray);
try {
// Input validation
self::validateLookupValue($lookupValue);
$matchType = self::validateMatchType($matchType);
self::validateLookupArray($lookupArray);
$keySet = array_keys($lookupArray);
if ($matchType == self::MATCHTYPE_LARGEST_VALUE) {
// If match_type is 1 the list has to be processed from last to first
$lookupArray = array_reverse($lookupArray);
$keySet = array_reverse($keySet);
}
$lookupArray = self::prepareLookupArray($lookupArray, $matchType);
} catch (Exception $e) {
return $e->getMessage();
}
// 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);
}
$valueKey = match ($matchType) {
self::MATCHTYPE_LARGEST_VALUE => self::matchLargestValue($lookupArray, $lookupValue, $keySet),
self::MATCHTYPE_FIRST_VALUE => self::matchFirstValue($lookupArray, $lookupValue),
default => self::matchSmallestValue($lookupArray, $lookupValue),
};
if ($valueKey !== null) {
return ++$valueKey;
}
// Unsuccessful in finding a match, return #N/A error value
return ExcelError::NA();
}
private static function matchFirstValue(array $lookupArray, mixed $lookupValue): int|string|null
{
if (is_string($lookupValue)) {
$valueIsString = true;
$wildcard = WildcardMatch::wildcard($lookupValue);
} else {
$valueIsString = false;
$wildcard = '';
}
$valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
foreach ($lookupArray as $i => $lookupArrayValue) {
if (
$valueIsString
&& is_string($lookupArrayValue)
) {
if (WildcardMatch::compare($lookupArrayValue, $wildcard)) {
return $i; // wildcard match
}
} else {
if ($lookupArrayValue === $lookupValue) {
return $i; // exact match
}
if (
$valueIsNumeric
&& (is_float($lookupArrayValue) || is_int($lookupArrayValue))
&& $lookupArrayValue == $lookupValue
) {
return $i; // exact match
}
}
}
return null;
}
private static function matchLargestValue(array $lookupArray, mixed $lookupValue, array $keySet): mixed
{
if (is_string($lookupValue)) {
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
$wildcard = WildcardMatch::wildcard($lookupValue);
foreach (array_reverse($lookupArray) as $i => $lookupArrayValue) {
if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) {
return $i;
}
}
} else {
foreach ($lookupArray as $i => $lookupArrayValue) {
if ($lookupArrayValue === $lookupValue) {
return $keySet[$i];
}
}
}
}
$valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
foreach ($lookupArray as $i => $lookupArrayValue) {
if ($valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue))) {
if ($lookupArrayValue <= $lookupValue) {
return array_search($i, $keySet);
}
}
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
if ($typeMatch && ($lookupArrayValue <= $lookupValue)) {
return array_search($i, $keySet);
}
}
return null;
}
private static function matchSmallestValue(array $lookupArray, mixed $lookupValue): int|string|null
{
$valueKey = null;
if (is_string($lookupValue)) {
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
$wildcard = WildcardMatch::wildcard($lookupValue);
foreach ($lookupArray as $i => $lookupArrayValue) {
if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) {
return $i;
}
}
}
}
$valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
// The basic algorithm is:
// Iterate and keep the highest match until the next element is smaller than the searched value.
// Return immediately if perfect match is found
foreach ($lookupArray as $i => $lookupArrayValue) {
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
$bothNumeric = $valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue));
if ($lookupArrayValue === $lookupValue) {
// Another "special" case. If a perfect match is found,
// the algorithm gives up immediately
return $i;
}
if ($bothNumeric && $lookupValue == $lookupArrayValue) {
return $i; // exact match, as above
}
if (($typeMatch || $bothNumeric) && $lookupArrayValue >= $lookupValue) {
$valueKey = $i;
} elseif ($typeMatch && $lookupArrayValue < $lookupValue) {
//Excel algorithm gives up immediately if the first element is smaller than the searched value
break;
}
}
return $valueKey;
}
private static function validateLookupValue(mixed $lookupValue): void
{
// Lookup_value type has to be number, text, or logical values
if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
throw new Exception(ExcelError::NA());
}
}
private static function validateMatchType(mixed $matchType): int
{
// Match_type is 0, 1 or -1
// However Excel accepts other numeric values,
// including numeric strings and floats.
// It seems to just be interested in the sign.
if (!is_numeric($matchType)) {
throw new Exception(ExcelError::Value());
}
if ($matchType > 0) {
return self::MATCHTYPE_LARGEST_VALUE;
}
if ($matchType < 0) {
return self::MATCHTYPE_SMALLEST_VALUE;
}
return self::MATCHTYPE_FIRST_VALUE;
}
private static function validateLookupArray(array $lookupArray): void
{
// Lookup_array should not be empty
$lookupArraySize = count($lookupArray);
if ($lookupArraySize <= 0) {
throw new Exception(ExcelError::NA());
}
}
private static function prepareLookupArray(array $lookupArray, mixed $matchType): array
{
// Lookup_array should contain only number, text, or logical values, or empty (null) cells
foreach ($lookupArray as $i => $value) {
// check the type of the value
if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) {
throw new Exception(ExcelError::NA());
}
// Convert strings to lowercase for case-insensitive testing
if (is_string($value)) {
$lookupArray[$i] = StringHelper::strToLower($value);
}
if (
($value === null)
&& (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE))
) {
unset($lookupArray[$i]);
}
}
return $lookupArray;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Filter
{
public static function filter(array $lookupArray, mixed $matchArray, mixed $ifEmpty = null): mixed
{
if (!is_array($matchArray)) {
return ExcelError::VALUE();
}
$matchArray = self::enumerateArrayKeys($matchArray);
$result = (Matrix::isColumnVector($matchArray))
? self::filterByRow($lookupArray, $matchArray)
: self::filterByColumn($lookupArray, $matchArray);
if (empty($result)) {
return $ifEmpty ?? ExcelError::CALC();
}
return array_values(array_map('array_values', $result));
}
private static function enumerateArrayKeys(array $sortArray): array
{
array_walk(
$sortArray,
function (&$columns): void {
if (is_array($columns)) {
$columns = array_values($columns);
}
}
);
return array_values($sortArray);
}
private static function filterByRow(array $lookupArray, array $matchArray): array
{
$matchArray = array_values(array_column($matchArray, 0));
return array_filter(
array_values($lookupArray),
fn ($index): bool => (bool) $matchArray[$index],
ARRAY_FILTER_USE_KEY
);
}
private static function filterByColumn(array $lookupArray, array $matchArray): array
{
$lookupArray = Matrix::transpose($lookupArray);
if (count($matchArray) === 1) {
$matchArray = array_pop($matchArray);
}
array_walk(
$matchArray,
function (&$value): void {
$value = [$value];
}
);
$result = self::filterByRow($lookupArray, $matchArray);
return Matrix::transpose($result);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
class Formula
{
/**
* FORMULATEXT.
*
* @param mixed $cellReference The cell to check
* @param ?Cell $cell The current cell (containing this formula)
*/
public static function text(mixed $cellReference = '', ?Cell $cell = null): string
{
if ($cell === null) {
return ExcelError::REF();
}
preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches);
$cellReference = $matches[6] . $matches[7];
$worksheetName = trim($matches[3], "'");
$worksheet = (!empty($worksheetName))
? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName)
: $cell->getWorksheet();
if (
$worksheet === null
|| !$worksheet->cellExists($cellReference)
|| !$worksheet->getCell($cellReference)->isFormula()
) {
return ExcelError::NA();
}
return $worksheet->getCell($cellReference)->getValue();
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class HLookup extends LookupBase
{
use ArrayEnabled;
/**
* HLOOKUP
* The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value
* in the same column based on the index_number.
*
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupArray The range of cells being searched
* @param mixed $indexNumber The row number in table_array from which the matching value must be returned.
* The first row is 1.
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
*
* @return mixed The value of the found cell
*/
public static function lookup(mixed $lookupValue, mixed $lookupArray, mixed $indexNumber, mixed $notExactMatch = true): mixed
{
if (is_array($lookupValue) || is_array($indexNumber)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}
$notExactMatch = (bool) ($notExactMatch ?? true);
try {
self::validateLookupArray($lookupArray);
$lookupArray = self::convertLiteralArray($lookupArray);
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber);
} catch (Exception $e) {
return $e->getMessage();
}
$f = array_keys($lookupArray);
$firstRow = reset($f);
if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray))) {
return ExcelError::REF();
}
$firstkey = $f[0] - 1;
$returnColumn = $firstkey + $indexNumber;
$firstColumn = array_shift($f) ?? 1;
$rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
if ($rowNumber !== null) {
// otherwise return the appropriate value
return $lookupArray[$returnColumn][Coordinate::stringFromColumnIndex($rowNumber)];
}
return ExcelError::NA();
}
/**
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param int|string $column
*/
private static function hLookupSearch(mixed $lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
{
$lookupLower = StringHelper::strToLower((string) $lookupValue);
$rowNumber = null;
foreach ($lookupArray[$column] as $rowKey => $rowData) {
// break if we have passed possible keys
$bothNumeric = is_numeric($lookupValue) && is_numeric($rowData);
$bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData);
$cellDataLower = StringHelper::strToLower((string) $rowData);
if (
$notExactMatch
&& (($bothNumeric && $rowData > $lookupValue) || ($bothNotNumeric && $cellDataLower > $lookupLower))
) {
break;
}
$rowNumber = self::checkMatch(
$bothNumeric,
$bothNotNumeric,
$notExactMatch,
Coordinate::columnIndexFromString($rowKey),
$cellDataLower,
$lookupLower,
$rowNumber
);
}
return $rowNumber;
}
private static function convertLiteralArray(array $lookupArray): array
{
if (array_key_exists(0, $lookupArray)) {
$lookupArray2 = [];
$row = 0;
foreach ($lookupArray as $arrayVal) {
++$row;
if (!is_array($arrayVal)) {
$arrayVal = [$arrayVal];
}
$arrayVal2 = [];
foreach ($arrayVal as $key2 => $val2) {
$index = Coordinate::stringFromColumnIndex($key2 + 1);
$arrayVal2[$index] = $val2;
}
$lookupArray2[$row] = $arrayVal2;
}
$lookupArray = $lookupArray2;
}
return $lookupArray;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Helpers
{
public const CELLADDRESS_USE_A1 = true;
public const CELLADDRESS_USE_R1C1 = false;
private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string
{
if ($a1 === self::CELLADDRESS_USE_R1C1) {
$cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1);
if ($cellAddress2) {
$cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1);
}
}
return $cellAddress1 . ($cellAddress2 ? ":$cellAddress2" : '');
}
private static function adjustSheetTitle(string &$sheetTitle, ?string $value): void
{
if ($sheetTitle) {
$sheetTitle .= '!';
if (stripos($value ?? '', $sheetTitle) === 0) {
$sheetTitle = '';
}
}
}
public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array
{
$cellAddress1 = $cellAddress;
$cellAddress2 = null;
$namedRange = DefinedName::resolveName($cellAddress1, $sheet, $sheetName);
if ($namedRange !== null) {
$workSheet = $namedRange->getWorkSheet();
$sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle();
$value = (string) preg_replace('/^=/', '', $namedRange->getValue());
self::adjustSheetTitle($sheetTitle, $value);
$cellAddress1 = $sheetTitle . $value;
$cellAddress = $cellAddress1;
$a1 = self::CELLADDRESS_USE_A1;
}
if (str_contains($cellAddress, ':')) {
[$cellAddress1, $cellAddress2] = explode(':', $cellAddress);
}
$cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol);
return [$cellAddress1, $cellAddress2, $cellAddress];
}
public static function extractWorksheet(string $cellAddress, Cell $cell): array
{
$sheetName = '';
if (str_contains($cellAddress, '!')) {
[$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$sheetName = trim($sheetName, "'");
}
$worksheet = ($sheetName !== '')
? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($sheetName)
: $cell->getWorksheet();
return [$cellAddress, $worksheet, $sheetName];
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
class Hyperlink
{
/**
* HYPERLINK.
*
* Excel Function:
* =HYPERLINK(linkURL, [displayName])
*
* @param mixed $linkURL Expect string. Value to check, is also the value returned when no error
* @param mixed $displayName Expect string. Value to return when testValue is an error condition
* @param ?Cell $cell The cell to set the hyperlink in
*
* @return mixed The value of $displayName (or $linkURL if $displayName was blank)
*/
public static function set(mixed $linkURL = '', mixed $displayName = null, ?Cell $cell = null): mixed
{
$linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
$displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
if ((!is_object($cell)) || (trim($linkURL) == '')) {
return ExcelError::REF();
}
if ((is_object($displayName)) || trim($displayName) == '') {
$displayName = $linkURL;
}
$cell->getHyperlink()->setUrl($linkURL);
$cell->getHyperlink()->setTooltip($displayName);
return $displayName;
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Indirect
{
/**
* Determine whether cell address is in A1 (true) or R1C1 (false) format.
*
* @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1,
* but can be provided as numeric which is cast to bool
*/
private static function a1Format(mixed $a1fmt): bool
{
$a1fmt = Functions::flattenSingleValue($a1fmt);
if ($a1fmt === null) {
return Helpers::CELLADDRESS_USE_A1;
}
if (is_string($a1fmt)) {
throw new Exception(ExcelError::VALUE());
}
return (bool) $a1fmt;
}
/**
* Convert cellAddress to string, verify not null string.
*/
private static function validateAddress(array|string|null $cellAddress): string
{
$cellAddress = Functions::flattenSingleValue($cellAddress);
if (!is_string($cellAddress) || !$cellAddress) {
throw new Exception(ExcelError::REF());
}
return $cellAddress;
}
/**
* INDIRECT.
*
* Returns the reference specified by a text string.
* References are immediately evaluated to display their contents.
*
* Excel Function:
* =INDIRECT(cellAddress, bool) where the bool argument is optional
*
* @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula)
* @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1,
* but can be provided as numeric which is cast to bool
* @param Cell $cell The current cell (containing this formula)
*
* @return array|string An array containing a cell or range of cells, or a string on error
*/
public static function INDIRECT($cellAddress, mixed $a1fmt, Cell $cell): string|array
{
[$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate());
try {
$a1 = self::a1Format($a1fmt);
$cellAddress = self::validateAddress($cellAddress);
} catch (Exception $e) {
return $e->getMessage();
}
[$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell);
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) {
$cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress));
} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) {
$cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress));
}
try {
[$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol);
} catch (Exception) {
return ExcelError::REF();
}
if (
(!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches))
|| (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress2, $matches)))
) {
return ExcelError::REF();
}
return self::extractRequiredCells($worksheet, $cellAddress);
}
/**
* Extract range values.
*
* @return array Array of values in range if range contains more than one element.
* Otherwise, a single value is returned.
*/
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
{
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
->extractCellRange($cellAddress, $worksheet, false);
}
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) {
// Max 16,384 columns for Excel2007
$endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : AddressRange::MAX_COLUMN;
return "A{$start}:{$endColRef}{$end}";
} elseif (ctype_alpha($start) && strlen($start) <= 3) {
// Max 1,048,576 rows for Excel2007
$endRowRef = ($worksheet !== null) ? $worksheet->getHighestDataRow($start) : AddressRange::MAX_ROW;
return "{$start}1:{$end}{$endRowRef}";
}
return "{$start}:{$end}";
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
class Lookup
{
use ArrayEnabled;
/**
* LOOKUP
* The LOOKUP function searches for value either from a one-row or one-column range or from an array.
*
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupVector The range of cells being searched
* @param null|mixed $resultVector The column from which the matching value must be returned
*
* @return mixed The value of the found cell
*/
public static function lookup(mixed $lookupValue, mixed $lookupVector, $resultVector = null): mixed
{
if (is_array($lookupValue)) {
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupVector, $resultVector);
}
if (!is_array($lookupVector)) {
return ExcelError::NA();
}
$hasResultVector = isset($resultVector);
$lookupRows = self::rowCount($lookupVector);
$lookupColumns = self::columnCount($lookupVector);
// we correctly orient our results
if (($lookupRows === 1 && $lookupColumns > 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) {
$lookupVector = LookupRef\Matrix::transpose($lookupVector);
$lookupRows = self::rowCount($lookupVector);
$lookupColumns = self::columnCount($lookupVector);
}
$resultVector = self::verifyResultVector($resultVector ?? $lookupVector);
if ($lookupRows === 2 && !$hasResultVector) {
$resultVector = array_pop($lookupVector);
$lookupVector = array_shift($lookupVector);
}
if ($lookupColumns !== 2) {
$lookupVector = self::verifyLookupValues($lookupVector, $resultVector);
}
return VLookup::lookup($lookupValue, $lookupVector, 2);
}
private static function verifyLookupValues(array $lookupVector, array $resultVector): array
{
foreach ($lookupVector as &$value) {
if (is_array($value)) {
$k = array_keys($value);
$key1 = $key2 = array_shift($k);
++$key2;
$dataValue1 = $value[$key1];
} else {
$key1 = 0;
$key2 = 1;
$dataValue1 = $value;
}
$dataValue2 = array_shift($resultVector);
if (is_array($dataValue2)) {
$dataValue2 = array_shift($dataValue2);
}
$value = [$key1 => $dataValue1, $key2 => $dataValue2];
}
unset($value);
return $lookupVector;
}
private static function verifyResultVector(array $resultVector): array
{
$resultRows = self::rowCount($resultVector);
$resultColumns = self::columnCount($resultVector);
// we correctly orient our results
if ($resultRows === 1 && $resultColumns > 1) {
$resultVector = LookupRef\Matrix::transpose($resultVector);
}
return $resultVector;
}
private static function rowCount(array $dataArray): int
{
return count($dataArray);
}
private static function columnCount(array $dataArray): int
{
$rowKeys = array_keys($dataArray);
$row = array_shift($rowKeys);
return count($dataArray[$row]);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
abstract class LookupBase
{
protected static function validateLookupArray(mixed $lookup_array): void
{
if (!is_array($lookup_array)) {
throw new Exception(ExcelError::REF());
}
}
/** @param float|int|string $index_number */
protected static function validateIndexLookup(array $lookup_array, $index_number): int
{
// index_number must be a number greater than or equal to 1.
// Excel results are inconsistent when index is non-numeric.
// VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but
// VLOOKUP(whatever, whatever, cellref) yields REF error
// when cellref is '=SQRT(-1)'. So just try our best here.
// Similar results if string (literal yields VALUE, cellRef REF).
if (!is_numeric($index_number)) {
throw new Exception(ExcelError::throwError($index_number));
}
if ($index_number < 1) {
throw new Exception(ExcelError::VALUE());
}
// index_number must be less than or equal to the number of columns in lookup_array
if (empty($lookup_array)) {
throw new Exception(ExcelError::REF());
}
return (int) $index_number;
}
protected static function checkMatch(
bool $bothNumeric,
bool $bothNotNumeric,
bool $notExactMatch,
int $rowKey,
string $cellDataLower,
string $lookupLower,
?int $rowNumber
): ?int {
// remember the last key, but only if datatypes match
if ($bothNumeric || $bothNotNumeric) {
// Spreadsheets software returns first exact match,
// we have sorted and we might have broken key orders
// we want the first one (by its initial index)
if ($notExactMatch) {
$rowNumber = $rowKey;
} elseif (($cellDataLower == $lookupLower) && (($rowNumber === null) || ($rowKey < $rowNumber))) {
$rowNumber = $rowKey;
}
}
return $rowNumber;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class LookupRefValidations
{
public static function validateInt(mixed $value): int
{
if (!is_numeric($value)) {
if (ErrorValue::isError($value)) {
throw new Exception($value);
}
throw new Exception(ExcelError::VALUE());
}
return (int) floor((float) $value);
}
public static function validatePositiveInt(mixed $value, bool $allowZero = true): int
{
$value = self::validateInt($value);
if (($allowZero === false && $value <= 0) || $value < 0) {
throw new Exception(ExcelError::VALUE());
}
return $value;
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Matrix
{
use ArrayEnabled;
/**
* Helper function; NOT an implementation of any Excel Function.
*/
public static function isColumnVector(array $values): bool
{
return count($values, COUNT_RECURSIVE) === (count($values, COUNT_NORMAL) * 2);
}
/**
* Helper function; NOT an implementation of any Excel Function.
*/
public static function isRowVector(array $values): bool
{
return count($values, COUNT_RECURSIVE) > 1
&& (count($values, COUNT_NORMAL) === 1 || count($values, COUNT_RECURSIVE) === count($values, COUNT_NORMAL));
}
/**
* TRANSPOSE.
*
* @param array|mixed $matrixData A matrix of values
*/
public static function transpose($matrixData): array
{
$returnMatrix = [];
if (!is_array($matrixData)) {
$matrixData = [[$matrixData]];
}
$column = 0;
foreach ($matrixData as $matrixRow) {
$row = 0;
foreach ($matrixRow as $matrixCell) {
$returnMatrix[$row][$column] = $matrixCell;
++$row;
}
++$column;
}
return $returnMatrix;
}
/**
* INDEX.
*
* Uses an index to choose a value from a reference or array
*
* Excel Function:
* =INDEX(range_array, row_num, [column_num], [area_num])
*
* @param mixed $matrix A range of cells or an array constant
* @param mixed $rowNum The row in the array or range from which to return a value.
* If row_num is omitted, column_num is required.
* Or can be an array of values
* @param mixed $columnNum The column in the array or range from which to return a value.
* If column_num is omitted, row_num is required.
* Or can be an array of values
*
* TODO Provide support for area_num, currently not supported
*
* @return mixed the value of a specified cell or array of cells
* If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result
* will also be an array with the same dimensions
*/
public static function index(mixed $matrix, mixed $rowNum = 0, mixed $columnNum = null): mixed
{
if (is_array($rowNum) || is_array($columnNum)) {
return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum);
}
$rowNum = $rowNum ?? 0;
$originalColumnNum = $columnNum;
$columnNum = $columnNum ?? 0;
try {
$rowNum = LookupRefValidations::validatePositiveInt($rowNum);
$columnNum = LookupRefValidations::validatePositiveInt($columnNum);
} catch (Exception $e) {
return $e->getMessage();
}
if (!is_array($matrix) || ($rowNum > count($matrix))) {
return ExcelError::REF();
}
$rowKeys = array_keys($matrix);
$columnKeys = @array_keys($matrix[$rowKeys[0]]);
if ($columnNum > count($columnKeys)) {
return ExcelError::REF();
}
if ($originalColumnNum === null && 1 < count($columnKeys)) {
return ExcelError::REF();
}
if ($columnNum === 0) {
return self::extractRowValue($matrix, $rowKeys, $rowNum);
}
$columnNum = $columnKeys[--$columnNum];
if ($rowNum === 0) {
return array_map(
fn ($value): array => [$value],
array_column($matrix, $columnNum)
);
}
$rowNum = $rowKeys[--$rowNum];
return $matrix[$rowNum][$columnNum];
}
private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum): mixed
{
if ($rowNum === 0) {
return $matrix;
}
$rowNum = $rowKeys[--$rowNum];
$row = $matrix[$rowNum];
if (is_array($row)) {
return [$rowNum => $row];
}
return $row;
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Offset
{
/**
* OFFSET.
*
* Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
* The reference that is returned can be a single cell or a range of cells. You can specify the number of rows and
* the number of columns to be returned.
*
* Excel Function:
* =OFFSET(cellAddress, rows, cols, [height], [width])
*
* @param null|string $cellAddress The reference from which you want to base the offset.
* Reference must refer to a cell or range of adjacent cells;
* otherwise, OFFSET returns the #VALUE! error value.
* @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to.
* Using 5 as the rows argument specifies that the upper-left cell in the
* reference is five rows below reference. Rows can be positive (which means
* below the starting reference) or negative (which means above the starting
* reference).
* @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell
* of the result to refer to. Using 5 as the cols argument specifies that the
* upper-left cell in the reference is five columns to the right of reference.
* Cols can be positive (which means to the right of the starting reference)
* or negative (which means to the left of the starting reference).
* @param mixed $height The height, in number of rows, that you want the returned reference to be.
* Height must be a positive number.
* @param mixed $width The width, in number of columns, that you want the returned reference to be.
* Width must be a positive number.
*
* @return array|string An array containing a cell or range of cells, or a string on error
*/
public static function OFFSET(?string $cellAddress = null, mixed $rows = 0, mixed $columns = 0, mixed $height = null, mixed $width = null, ?Cell $cell = null): string|array
{
$rows = Functions::flattenSingleValue($rows);
$columns = Functions::flattenSingleValue($columns);
$height = Functions::flattenSingleValue($height);
$width = Functions::flattenSingleValue($width);
if ($cellAddress === null || $cellAddress === '') {
return ExcelError::VALUE();
}
if (!is_object($cell)) {
return ExcelError::REF();
}
[$cellAddress, $worksheet] = self::extractWorksheet($cellAddress, $cell);
$startCell = $endCell = $cellAddress;
if (strpos($cellAddress, ':')) {
[$startCell, $endCell] = explode(':', $cellAddress);
}
[$startCellColumn, $startCellRow] = Coordinate::coordinateFromString($startCell);
[$endCellColumn, $endCellRow] = Coordinate::coordinateFromString($endCell);
$startCellRow += $rows;
$startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1;
$startCellColumn += $columns;
if (($startCellRow <= 0) || ($startCellColumn < 0)) {
return ExcelError::REF();
}
$endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns);
$startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
$endCellRow = self::adustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
if (($endCellRow <= 0) || ($endCellColumn < 0)) {
return ExcelError::REF();
}
$endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1);
$cellAddress = "{$startCellColumn}{$startCellRow}";
if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) {
$cellAddress .= ":{$endCellColumn}{$endCellRow}";
}
return self::extractRequiredCells($worksheet, $cellAddress);
}
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
{
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
->extractCellRange($cellAddress, $worksheet, false);
}
private static function extractWorksheet(?string $cellAddress, Cell $cell): array
{
$cellAddress = self::assessCellAddress($cellAddress ?? '', $cell);
$sheetName = '';
if (str_contains($cellAddress, '!')) {
[$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$sheetName = trim($sheetName, "'");
}
$worksheet = ($sheetName !== '')
? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($sheetName)
: $cell->getWorksheet();
return [$cellAddress, $worksheet];
}
private static function assessCellAddress(string $cellAddress, Cell $cell): string
{
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) {
$cellAddress = Functions::expandDefinedName($cellAddress, $cell);
}
return $cellAddress;
}
private static function adjustEndCellColumnForWidth(string $endCellColumn, mixed $width, int $startCellColumn, mixed $columns): int
{
$endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1;
if (($width !== null) && (!is_object($width))) {
$endCellColumn = $startCellColumn + (int) $width - 1;
} else {
$endCellColumn += (int) $columns;
}
return $endCellColumn;
}
private static function adustEndCellRowForHeight(mixed $height, int $startCellRow, mixed $rows, mixed $endCellRow): int
{
if (($height !== null) && (!is_object($height))) {
$endCellRow = $startCellRow + (int) $height - 1;
} else {
$endCellRow += (int) $rows;
}
return $endCellRow;
}
}

View file

@ -0,0 +1,210 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
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 $cell): int
{
return ($cell !== null) ? Coordinate::columnIndexFromString($cell->getColumn()) : 1;
}
/**
* COLUMN.
*
* Returns the column number of the given cell reference
* If the cell reference is a range of cells, COLUMN returns the column numbers of each column
* in the reference as a horizontal array.
* If cell reference is omitted, and the function is being called through the calculation engine,
* then it is assumed to be the reference of the cell in which the COLUMN function appears;
* otherwise this function returns 1.
*
* Excel Function:
* =COLUMN([cellAddress])
*
* @param null|array|string $cellAddress A reference to a range of cells for which you want the column numbers
*
* @return int|int[]
*/
public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|array
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellColumn($cell);
}
if (is_array($cellAddress)) {
foreach ($cellAddress as $columnKey => $value) {
$columnKey = (string) preg_replace('/[^a-z]/i', '', $columnKey);
return Coordinate::columnIndexFromString($columnKey);
}
return self::cellColumn($cell);
}
$cellAddress = $cellAddress ?? '';
if ($cell != null) {
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell);
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName);
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$cellAddress ??= '';
if (str_contains($cellAddress, ':')) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = (string) preg_replace('/[^a-z]/i', '', $startAddress);
$endAddress = (string) preg_replace('/[^a-z]/i', '', $endAddress);
return range(
Coordinate::columnIndexFromString($startAddress),
Coordinate::columnIndexFromString($endAddress)
);
}
$cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress);
return Coordinate::columnIndexFromString($cellAddress);
}
/**
* COLUMNS.
*
* Returns the number of columns in an array or reference.
*
* Excel Function:
* =COLUMNS(cellAddress)
*
* @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells
* for which you want the number of columns
*
* @return int|string The number of columns in cellAddress, or a string if arguments are invalid
*/
public static function COLUMNS($cellAddress = null)
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
if (!is_array($cellAddress)) {
return ExcelError::VALUE();
}
reset($cellAddress);
$isMatrix = (is_numeric(key($cellAddress)));
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress);
if ($isMatrix) {
return $rows;
}
return $columns;
}
private static function cellRow(?Cell $cell): int
{
return ($cell !== null) ? $cell->getRow() : 1;
}
/**
* ROW.
*
* Returns the row number of the given cell reference
* If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference
* as a vertical array.
* If cell reference is omitted, and the function is being called through the calculation engine,
* then it is assumed to be the reference of the cell in which the ROW function appears;
* otherwise this function returns 1.
*
* Excel Function:
* =ROW([cellAddress])
*
* @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers
*
* @return int|mixed[]
*/
public static function ROW($cellAddress = null, ?Cell $cell = null): int|array
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellRow($cell);
}
if (is_array($cellAddress)) {
foreach ($cellAddress as $rowKey => $rowValue) {
foreach ($rowValue as $columnKey => $cellValue) {
return (int) preg_replace('/\D/', '', $rowKey);
}
}
return self::cellRow($cell);
}
$cellAddress = $cellAddress ?? '';
if ($cell !== null) {
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell);
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName);
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$cellAddress ??= '';
if (str_contains($cellAddress, ':')) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = (int) (string) preg_replace('/\D/', '', $startAddress);
$endAddress = (int) (string) preg_replace('/\D/', '', $endAddress);
return array_map(
fn ($value): array => [$value],
range($startAddress, $endAddress)
);
}
[$cellAddress] = explode(':', $cellAddress);
return (int) preg_replace('/\D/', '', $cellAddress);
}
/**
* ROWS.
*
* Returns the number of rows in an array or reference.
*
* Excel Function:
* =ROWS(cellAddress)
*
* @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells
* for which you want the number of rows
*
* @return int|string The number of rows in cellAddress, or a string if arguments are invalid
*/
public static function ROWS($cellAddress = null)
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return 1;
}
if (!is_array($cellAddress)) {
return ExcelError::VALUE();
}
reset($cellAddress);
$isMatrix = (is_numeric(key($cellAddress)));
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress);
if ($isMatrix) {
return $columns;
}
return $rows;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Selection
{
use ArrayEnabled;
/**
* CHOOSE.
*
* Uses lookup_value to return a value from the list of value arguments.
* Use CHOOSE to select one of up to 254 values based on the lookup_value.
*
* Excel Function:
* =CHOOSE(index_num, value1, [value2], ...)
*
* @param mixed $chosenEntry The entry to select from the list (indexed from 1)
* @param mixed ...$chooseArgs Data values
*
* @return mixed The selected value
*/
public static function choose(mixed $chosenEntry, mixed ...$chooseArgs): mixed
{
if (is_array($chosenEntry)) {
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $chosenEntry, ...$chooseArgs);
}
$entryCount = count($chooseArgs) - 1;
if (is_numeric($chosenEntry)) {
--$chosenEntry;
} else {
return ExcelError::VALUE();
}
$chosenEntry = floor($chosenEntry);
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
return ExcelError::VALUE();
}
if (is_array($chooseArgs[$chosenEntry])) {
return Functions::flattenArray($chooseArgs[$chosenEntry]);
}
return $chooseArgs[$chosenEntry];
}
}

View file

@ -0,0 +1,309 @@
<?php
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;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class Sort extends LookupRefValidations
{
public const ORDER_ASCENDING = 1;
public const ORDER_DESCENDING = -1;
/**
* SORT
* The SORT function returns a sorted array of the elements in an 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.
*
* @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
* Ascending = 1 (self::ORDER_ASCENDING)
* Descending = -1 (self::ORDER_DESCENDING)
* @param mixed $byColumn Whether the sort should be determined by row (the default) or by column
*
* @return mixed The sorted values from the sort range
*/
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 = self::enumerateArrayKeys($sortArray);
$byColumn = (bool) $byColumn;
$lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]);
try {
// If $sortIndex and $sortOrder are scalars, then convert them into arrays
if (is_scalar($sortIndex)) {
$sortIndex = [$sortIndex];
$sortOrder = is_scalar($sortOrder) ? [$sortOrder] : $sortOrder;
}
// but the values of those array arguments still need validation
$sortOrder = (empty($sortOrder) ? [self::ORDER_ASCENDING] : $sortOrder);
self::validateArrayArgumentsForSort($sortIndex, $sortOrder, $lookupIndexSize);
} catch (Exception $e) {
return $e->getMessage();
}
// We want a simple, enumrated array of arrays where we can reference column by its index number.
$sortArray = array_values(array_map('array_values', $sortArray));
return ($byColumn === true)
? self::sortByColumn($sortArray, $sortIndex, $sortOrder)
: self::sortByRow($sortArray, $sortIndex, $sortOrder);
}
/**
* SORTBY
* 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.
*
* @param mixed $sortArray The range of cells being sorted
* @param mixed $args
* At least one additional argument must be provided, The vector or range to sort on
* After that, arguments are passed as pairs:
* sort order: ascending or descending
* Ascending = 1 (self::ORDER_ASCENDING)
* Descending = -1 (self::ORDER_DESCENDING)
* additional arrays or ranges for multi-level sorting
*
* @return mixed The sorted values from the sort range
*/
public static function sortBy(mixed $sortArray, mixed ...$args): mixed
{
if (!is_array($sortArray)) {
// Scalars are always returned "as is"
return $sortArray;
}
$sortArray = self::enumerateArrayKeys($sortArray);
$lookupArraySize = count($sortArray);
$argumentCount = count($args);
try {
$sortBy = $sortOrder = [];
for ($i = 0; $i < $argumentCount; $i += 2) {
$sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
$sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
}
} catch (Exception $e) {
return $e->getMessage();
}
return self::processSortBy($sortArray, $sortBy, $sortOrder);
}
private static function enumerateArrayKeys(array $sortArray): array
{
array_walk(
$sortArray,
function (&$columns): void {
if (is_array($columns)) {
$columns = array_values($columns);
}
}
);
return array_values($sortArray);
}
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) {
throw new Exception(ExcelError::VALUE());
}
$sortOrder = self::validateSortOrder($sortOrder);
}
private static function validateSortVector(mixed $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) {
throw new Exception(ExcelError::VALUE());
}
return $sortVector;
}
private static function validateSortOrder(mixed $sortOrder): int
{
$sortOrder = self::validateInt($sortOrder);
if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) {
throw new Exception(ExcelError::VALUE());
}
return $sortOrder;
}
private static function validateArrayArgumentsForSort(array &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
{
// It doesn't matter if they're row or column vectors, it works either way
$sortIndex = Functions::flattenArray($sortIndex);
$sortOrder = Functions::flattenArray($sortOrder);
if (
count($sortOrder) === 0 || count($sortOrder) > $sortArraySize
|| (count($sortOrder) > count($sortIndex))
) {
throw new Exception(ExcelError::VALUE());
}
if (count($sortIndex) > count($sortOrder)) {
// If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated.
$sortOrder = array_merge(
$sortOrder,
array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder))
);
}
foreach ($sortIndex as $key => &$value) {
self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize);
}
}
private static function prepareSortVectorValues(array $sortVector): array
{
// Strings should be sorted case-insensitive; with booleans converted to locale-strings
return array_map(
function ($value) {
if (is_bool($value)) {
return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
} elseif (is_string($value)) {
return StringHelper::strToLower($value);
}
return $value;
},
$sortVector
);
}
/**
* @param array[] $sortIndex
* @param int[] $sortOrder
*/
private static function processSortBy(array $sortArray, array $sortIndex, array $sortOrder): array
{
$sortArguments = [];
$sortData = [];
foreach ($sortIndex as $index => $sortValues) {
$sortData[] = $sortValues;
$sortArguments[] = self::prepareSortVectorValues($sortValues);
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
}
$sortVector = self::executeVectorSortQuery($sortData, $sortArguments);
return self::sortLookupArrayFromVector($sortArray, $sortVector);
}
/**
* @param int[] $sortIndex
* @param int[] $sortOrder
*/
private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array
{
$sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder);
return self::sortLookupArrayFromVector($sortArray, $sortVector);
}
/**
* @param int[] $sortIndex
* @param int[] $sortOrder
*/
private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array
{
$sortArray = Matrix::transpose($sortArray);
$result = self::sortByRow($sortArray, $sortIndex, $sortOrder);
return Matrix::transpose($result);
}
/**
* @param int[] $sortIndex
* @param int[] $sortOrder
*/
private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array
{
$sortArguments = [];
$sortData = [];
foreach ($sortIndex as $index => $sortIndexValue) {
$sortValues = array_column($sortArray, $sortIndexValue - 1);
$sortData[] = $sortValues;
$sortArguments[] = self::prepareSortVectorValues($sortValues);
$sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
}
$sortData = self::executeVectorSortQuery($sortData, $sortArguments);
return $sortData;
}
private static function executeVectorSortQuery(array $sortData, array $sortArguments): array
{
$sortData = Matrix::transpose($sortData);
// We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys.
$sortDataIndexed = [];
foreach ($sortData as $key => $value) {
$sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value;
}
unset($sortData);
$sortArguments[] = &$sortDataIndexed;
array_multisort(...$sortArguments);
// After the sort, we restore the numeric keys that will now be in the correct, sorted order
$sortedData = [];
foreach (array_keys($sortDataIndexed) as $key) {
$sortedData[] = Coordinate::columnIndexFromString($key) - 1;
}
return $sortedData;
}
private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array
{
// Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays
$sortedArray = [];
foreach ($sortVector as $index) {
$sortedArray[] = $sortArray[$index];
}
return $sortedArray;
// uksort(
// $lookupArray,
// function (int $a, int $b) use (array $sortVector) {
// return $sortVector[$a] <=> $sortVector[$b];
// }
// );
//
// return $lookupArray;
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class Unique
{
/**
* UNIQUE
* The UNIQUE function searches for value either from a one-row or one-column range or from an array.
*
* @param mixed $lookupVector The range of cells being searched
* @param mixed $byColumn Whether the uniqueness should be determined by row (the default) or by column
* @param mixed $exactlyOnce Whether the function should return only entries that occur just once in the list
*
* @return mixed The unique values from the search range
*/
public static function unique(mixed $lookupVector, mixed $byColumn = false, mixed $exactlyOnce = false): mixed
{
if (!is_array($lookupVector)) {
// Scalars are always returned "as is"
return $lookupVector;
}
$byColumn = (bool) $byColumn;
$exactlyOnce = (bool) $exactlyOnce;
return ($byColumn === true)
? self::uniqueByColumn($lookupVector, $exactlyOnce)
: self::uniqueByRow($lookupVector, $exactlyOnce);
}
private static function uniqueByRow(array $lookupVector, bool $exactlyOnce): mixed
{
// When not $byColumn, we count whole rows or values, not individual values
// so implode each row into a single string value
array_walk(
$lookupVector,
function (array &$value): void {
$value = implode(chr(0x00), $value);
}
);
$result = self::countValuesCaseInsensitive($lookupVector);
if ($exactlyOnce === true) {
$result = self::exactlyOnceFilter($result);
}
if (count($result) === 0) {
return ExcelError::CALC();
}
$result = array_keys($result);
// restore rows from their strings
array_walk(
$result,
function (string &$value): void {
$value = explode(chr(0x00), $value);
}
);
return (count($result) === 1) ? array_pop($result) : $result;
}
private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce): mixed
{
$flattenedLookupVector = Functions::flattenArray($lookupVector);
if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
// We're looking at a full column check (multiple rows)
$transpose = Matrix::transpose($lookupVector);
$result = self::uniqueByRow($transpose, $exactlyOnce);
return (is_array($result)) ? Matrix::transpose($result) : $result;
}
$result = self::countValuesCaseInsensitive($flattenedLookupVector);
if ($exactlyOnce === true) {
$result = self::exactlyOnceFilter($result);
}
if (count($result) === 0) {
return ExcelError::CALC();
}
$result = array_keys($result);
return $result;
}
private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
{
$caseInsensitiveCounts = array_count_values(
array_map(
fn (string $value): string => StringHelper::strToUpper($value),
$caseSensitiveLookupValues
)
);
$caseSensitiveCounts = [];
foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
if (is_numeric($caseInsensitiveKey)) {
$caseSensitiveCounts[$caseInsensitiveKey] = $count;
} else {
foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
$caseSensitiveCounts[$caseSensitiveValue] = $count;
break;
}
}
}
}
return $caseSensitiveCounts;
}
private static function exactlyOnceFilter(array $values): array
{
return array_filter(
$values,
fn ($value): bool => $value === 1
);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class VLookup extends LookupBase
{
use ArrayEnabled;
/**
* VLOOKUP
* The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value
* in the same row based on the index_number.
*
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupArray The range of cells being searched
* @param mixed $indexNumber The column number in table_array from which the matching value must be returned.
* The first column is 1.
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
*
* @return mixed The value of the found cell
*/
public static function lookup(mixed $lookupValue, mixed $lookupArray, mixed $indexNumber, mixed $notExactMatch = true): mixed
{
if (is_array($lookupValue) || is_array($indexNumber)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}
$notExactMatch = (bool) ($notExactMatch ?? true);
try {
self::validateLookupArray($lookupArray);
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber);
} catch (Exception $e) {
return $e->getMessage();
}
$f = array_keys($lookupArray);
$firstRow = array_pop($f);
if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray[$firstRow]))) {
return ExcelError::REF();
}
$columnKeys = array_keys($lookupArray[$firstRow]);
$returnColumn = $columnKeys[--$indexNumber];
$firstColumn = array_shift($columnKeys) ?? 1;
if (!$notExactMatch) {
/** @var callable $callable */
$callable = [self::class, 'vlookupSort'];
uasort($lookupArray, $callable);
}
$rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
if ($rowNumber !== null) {
// return the appropriate value
return $lookupArray[$rowNumber][$returnColumn];
}
return ExcelError::NA();
}
private static function vlookupSort(array $a, array $b): int
{
reset($a);
$firstColumn = key($a);
$aLower = StringHelper::strToLower((string) $a[$firstColumn]);
$bLower = StringHelper::strToLower((string) $b[$firstColumn]);
if ($aLower == $bLower) {
return 0;
}
return ($aLower < $bLower) ? -1 : 1;
}
/**
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param int|string $column
*/
private static function vLookupSearch(mixed $lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
{
$lookupLower = StringHelper::strToLower((string) $lookupValue);
$rowNumber = null;
foreach ($lookupArray as $rowKey => $rowData) {
$bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]);
$bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]);
$cellDataLower = StringHelper::strToLower((string) $rowData[$column]);
// break if we have passed possible keys
if (
$notExactMatch
&& (($bothNumeric && ($rowData[$column] > $lookupValue))
|| ($bothNotNumeric && ($cellDataLower > $lookupLower)))
) {
break;
}
$rowNumber = self::checkMatch(
$bothNumeric,
$bothNotNumeric,
$notExactMatch,
$rowKey,
$cellDataLower,
$lookupLower,
$rowNumber
);
}
return $rowNumber;
}
}