init commit

This commit is contained in:
2026-03-17 09:56:00 +08:00
commit e2c8ae752d
6827 changed files with 1211784 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
class Address
{
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
* @param mixed $column Column number (integer) to use in the cell reference
* @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
* @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
* @param mixed $sheetName Optional Name of worksheet to use
*
* @return string
*/
public static function cell($row, $column, $relativity = 1, $referenceStyle = true, $sheetName = '')
{
$row = Functions::flattenSingleValue($row);
$column = Functions::flattenSingleValue($column);
$relativity = ($relativity === null) ? 1 : Functions::flattenSingleValue($relativity);
$referenceStyle = ($referenceStyle === null) ? true : Functions::flattenSingleValue($referenceStyle);
$sheetName = Functions::flattenSingleValue($sheetName);
if (($row < 1) || ($column < 1)) {
return Functions::VALUE();
}
$sheetName = self::sheetName($sheetName);
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)
{
if ($sheetName > '') {
if (strpos($sheetName, ' ') !== false || strpos($sheetName, '[') !== false) {
$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}]";
}
return "{$sheetName}R{$row}C{$column}";
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class ExcelMatch
{
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 int|string The relative position of the found item
*/
public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE)
{
$lookupArray = Functions::flattenArray($lookupArray);
$lookupValue = Functions::flattenSingleValue($lookupValue);
$matchType = ($matchType === null)
? self::MATCHTYPE_LARGEST_VALUE
: (int) Functions::flattenSingleValue($matchType);
try {
// Input validation
self::validateLookupValue($lookupValue);
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 = null;
switch ($matchType) {
case self::MATCHTYPE_LARGEST_VALUE:
$valueKey = self::matchLargestValue($lookupArray, $lookupValue, $keySet);
break;
case self::MATCHTYPE_FIRST_VALUE:
$valueKey = self::matchFirstValue($lookupArray, $lookupValue);
break;
case self::MATCHTYPE_SMALLEST_VALUE:
default:
$valueKey = self::matchSmallestValue($lookupArray, $lookupValue);
}
if ($valueKey !== null) {
return ++$valueKey;
}
// Unsuccessful in finding a match, return #N/A error value
return Functions::NA();
}
private static function matchFirstValue($lookupArray, $lookupValue)
{
$wildcardLookup = ((bool) preg_match('/([\?\*])/', $lookupValue));
$wildcard = WildcardMatch::wildcard($lookupValue);
foreach ($lookupArray as $i => $lookupArrayValue) {
$typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) ||
(is_numeric($lookupValue) && is_numeric($lookupArrayValue)));
if (
$typeMatch && is_string($lookupValue) &&
$wildcardLookup && WildcardMatch::compare($lookupArrayValue, $wildcard)
) {
// wildcard match
return $i;
} elseif ($lookupArrayValue === $lookupValue) {
// exact match
return $i;
}
}
return null;
}
private static function matchLargestValue($lookupArray, $lookupValue, $keySet)
{
foreach ($lookupArray as $i => $lookupArrayValue) {
$typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) ||
(is_numeric($lookupValue) && is_numeric($lookupArrayValue)));
if ($typeMatch && ($lookupArrayValue <= $lookupValue)) {
return array_search($i, $keySet);
}
}
return null;
}
private static function matchSmallestValue($lookupArray, $lookupValue)
{
$valueKey = null;
// 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);
if ($lookupArrayValue === $lookupValue) {
// Another "special" case. If a perfect match is found,
// the algorithm gives up immediately
return $i;
} elseif ($typeMatch && $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($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(Functions::NA());
}
}
private static function validateMatchType($matchType): void
{
// Match_type is 0, 1 or -1
if (
($matchType !== self::MATCHTYPE_FIRST_VALUE) &&
($matchType !== self::MATCHTYPE_LARGEST_VALUE) && ($matchType !== self::MATCHTYPE_SMALLEST_VALUE)
) {
throw new Exception(Functions::NA());
}
}
private static function validateLookupArray($lookupArray): void
{
// Lookup_array should not be empty
$lookupArraySize = count($lookupArray);
if ($lookupArraySize <= 0) {
throw new Exception(Functions::NA());
}
}
private static function prepareLookupArray($lookupArray, $matchType)
{
// 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(Functions::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,43 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
class Formula
{
/**
* FORMULATEXT.
*
* @param mixed $cellReference The cell to check
* @param Cell $pCell The current cell (containing this formula)
*
* @return string
*/
public static function text($cellReference = '', ?Cell $pCell = null)
{
if ($pCell === null) {
return Functions::REF();
}
preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches);
$cellReference = $matches[6] . $matches[7];
$worksheetName = trim($matches[3], "'");
$worksheet = (!empty($worksheetName))
? $pCell->getWorksheet()->getParent()->getSheetByName($worksheetName)
: $pCell->getWorksheet();
if (
$worksheet === null ||
!$worksheet->cellExists($cellReference) ||
!$worksheet->getCell($cellReference)->isFormula()
) {
return Functions::NA();
}
return $worksheet->getCell($cellReference)->getValue();
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class HLookup extends LookupBase
{
/**
* 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($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{
$lookupValue = Functions::flattenSingleValue($lookupValue);
$indexNumber = Functions::flattenSingleValue($indexNumber);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
$lookupArray = self::convertLiteralArray($lookupArray);
try {
$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 Functions::REF();
}
$firstkey = $f[0] - 1;
$returnColumn = $firstkey + $indexNumber;
$firstColumn = array_shift($f);
$rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
if ($rowNumber !== null) {
// otherwise return the appropriate value
return $lookupArray[$returnColumn][Coordinate::stringFromColumnIndex($rowNumber)];
}
return Functions::NA();
}
/**
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $column The column to look up
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
*/
private static function hLookupSearch($lookupValue, array $lookupArray, $column, $notExactMatch): ?int
{
$lookupLower = StringHelper::strToLower($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($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): string
{
if ($a1 === self::CELLADDRESS_USE_R1C1) {
$cellAddress1 = AddressHelper::convertToA1($cellAddress1);
if ($cellAddress2) {
$cellAddress2 = AddressHelper::convertToA1($cellAddress2);
}
}
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 = ''): array
{
$cellAddress1 = $cellAddress;
$cellAddress2 = null;
$namedRange = DefinedName::resolveName($cellAddress1, $sheet, $sheetName);
if ($namedRange !== null) {
$workSheet = $namedRange->getWorkSheet();
$sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle();
$value = preg_replace('/^=/', '', $namedRange->getValue());
self::adjustSheetTitle($sheetTitle, $value);
$cellAddress1 = $sheetTitle . $value;
$cellAddress = $cellAddress1;
$a1 = self::CELLADDRESS_USE_A1;
}
if (strpos($cellAddress, ':') !== false) {
[$cellAddress1, $cellAddress2] = explode(':', $cellAddress);
}
$cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1);
return [$cellAddress1, $cellAddress2, $cellAddress];
}
public static function extractWorksheet(string $cellAddress, Cell $pCell): array
{
$sheetName = '';
if (strpos($cellAddress, '!') !== false) {
[$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$sheetName = trim($sheetName, "'");
}
$worksheet = ($sheetName !== '')
? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName)
: $pCell->getWorksheet();
return [$cellAddress, $worksheet, $sheetName];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
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 $pCell The cell to set the hyperlink in
*
* @return mixed The value of $displayName (or $linkURL if $displayName was blank)
*/
public static function set($linkURL = '', $displayName = null, ?Cell $pCell = null)
{
$linkURL = ($linkURL === null) ? '' : Functions::flattenSingleValue($linkURL);
$displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
if ((!is_object($pCell)) || (trim($linkURL) == '')) {
return Functions::REF();
}
if ((is_object($displayName)) || trim($displayName) == '') {
$displayName = $linkURL;
}
$pCell->getHyperlink()->setUrl($linkURL);
$pCell->getHyperlink()->setTooltip($displayName);
return $displayName;
}
}

View File

@@ -0,0 +1,97 @@
<?php
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 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($a1fmt): bool
{
$a1fmt = Functions::flattenSingleValue($a1fmt);
if ($a1fmt === null) {
return Helpers::CELLADDRESS_USE_A1;
}
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, 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 $pCell 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, $a1fmt, Cell $pCell)
{
try {
$a1 = self::a1Format($a1fmt);
$cellAddress = self::validateAddress($cellAddress);
} catch (Exception $e) {
return $e->getMessage();
}
[$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell);
[$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $pCell->getWorkSheet(), $sheetName);
if (
(!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) ||
(($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches)))
) {
return Functions::REF();
}
return self::extractRequiredCells($worksheet, $cellAddress);
}
/**
* Extract range values.
*
* @return mixed 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)
{
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
->extractCellRange($cellAddress, $worksheet, false);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
class Lookup
{
/**
* 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($lookupValue, $lookupVector, $resultVector = null)
{
$lookupValue = Functions::flattenSingleValue($lookupValue);
if (!is_array($lookupVector)) {
return Functions::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($lookupVector, $resultVector);
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 $lookupVector, $resultVector)
{
if ($resultVector === null) {
$resultVector = $lookupVector;
}
$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,48 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
abstract class LookupBase
{
protected static function validateIndexLookup($lookup_array, $index_number)
{
// index_number must be a number greater than or equal to 1
if (!is_numeric($index_number) || $index_number < 1) {
throw new Exception(Functions::VALUE());
}
// index_number must be less than or equal to the number of columns in lookup_array
if ((!is_array($lookup_array)) || (empty($lookup_array))) {
throw new Exception(Functions::REF());
}
return (int) $index_number;
}
protected static function checkMatch(
bool $bothNumeric,
bool $bothNotNumeric,
$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,39 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class LookupRefValidations
{
/**
* @param mixed $value
*/
public static function validateInt($value): int
{
if (!is_numeric($value)) {
if (Functions::isError($value)) {
throw new Exception($value);
}
throw new Exception(Functions::VALUE());
}
return (int) floor((float) $value);
}
/**
* @param mixed $value
*/
public static function validatePositiveInt($value, bool $allowZero = true): int
{
$value = self::validateInt($value);
if (($allowZero === false && $value <= 0) || $value < 0) {
throw new Exception(Functions::VALUE());
}
return $value;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Matrix
{
/**
* TRANSPOSE.
*
* @param array|mixed $matrixData A matrix of values
*
* @return array
*/
public static function transpose($matrixData)
{
$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.
* @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.
*
* TODO Provide support for area_num, currently not supported
*
* @return mixed the value of a specified cell or array of cells
*/
public static function index($matrix, $rowNum = 0, $columnNum = 0)
{
$rowNum = ($rowNum === null) ? 0 : Functions::flattenSingleValue($rowNum);
$columnNum = ($columnNum === null) ? 0 : Functions::flattenSingleValue($columnNum);
try {
$rowNum = LookupRefValidations::validatePositiveInt($rowNum);
$columnNum = LookupRefValidations::validatePositiveInt($columnNum);
} catch (Exception $e) {
return $e->getMessage();
}
if (!is_array($matrix) || ($rowNum > count($matrix))) {
return Functions::REF();
}
$rowKeys = array_keys($matrix);
$columnKeys = @array_keys($matrix[$rowKeys[0]]);
if ($columnNum > count($columnKeys)) {
return Functions::REF();
}
if ($columnNum === 0) {
return self::extractRowValue($matrix, $rowKeys, $rowNum);
}
$columnNum = $columnKeys[--$columnNum];
if ($rowNum === 0) {
return array_map(
function ($value) {
return [$value];
},
array_column($matrix, $columnNum)
);
}
$rowNum = $rowKeys[--$rowNum];
return $matrix[$rowNum][$columnNum];
}
private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum)
{
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,136 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
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|int|string An array containing a cell or range of cells, or a string on error
*/
public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, ?Cell $pCell = null)
{
$rows = Functions::flattenSingleValue($rows);
$columns = Functions::flattenSingleValue($columns);
$height = Functions::flattenSingleValue($height);
$width = Functions::flattenSingleValue($width);
if ($cellAddress === null || $cellAddress === '') {
return 0;
}
if (!is_object($pCell)) {
return Functions::REF();
}
[$cellAddress, $worksheet] = self::extractWorksheet($cellAddress, $pCell);
$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 Functions::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 Functions::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)
{
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
->extractCellRange($cellAddress, $worksheet, false);
}
private static function extractWorksheet($cellAddress, Cell $pCell): array
{
$sheetName = '';
if (strpos($cellAddress, '!') !== false) {
[$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
$sheetName = trim($sheetName, "'");
}
$worksheet = ($sheetName !== '')
? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName)
: $pCell->getWorksheet();
return [$cellAddress, $worksheet];
}
private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns)
{
$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($height, int $startCellRow, $rows, $endCellRow): int
{
if (($height !== null) && (!is_object($height))) {
$endCellRow = $startCellRow + (int) $height - 1;
} else {
$endCellRow += (int) $rows;
}
return $endCellRow;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
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 $pCell): int
{
return ($pCell !== null) ? (int) Coordinate::columnIndexFromString($pCell->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 $pCell = null)
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellColumn($pCell);
}
if (is_array($cellAddress)) {
foreach ($cellAddress as $columnKey => $value) {
$columnKey = preg_replace('/[^a-z]/i', '', $columnKey);
return (int) Coordinate::columnIndexFromString($columnKey);
}
return self::cellColumn($pCell);
}
$cellAddress = $cellAddress ?? '';
if ($pCell != null) {
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell);
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet(), $sheetName);
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if (strpos($cellAddress, ':') !== false) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = preg_replace('/[^a-z]/i', '', $startAddress);
$endAddress = preg_replace('/[^a-z]/i', '', $endAddress);
return range(
(int) Coordinate::columnIndexFromString($startAddress),
(int) Coordinate::columnIndexFromString($endAddress)
);
}
$cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress);
return (int) 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 Functions::VALUE();
}
reset($cellAddress);
$isMatrix = (is_numeric(key($cellAddress)));
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress);
if ($isMatrix) {
return $rows;
}
return $columns;
}
private static function cellRow(?Cell $pCell): int
{
return ($pCell !== null) ? $pCell->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[]|string
*/
public static function ROW($cellAddress = null, ?Cell $pCell = null)
{
if (self::cellAddressNullOrWhitespace($cellAddress)) {
return self::cellRow($pCell);
}
if (is_array($cellAddress)) {
foreach ($cellAddress as $rowKey => $rowValue) {
foreach ($rowValue as $columnKey => $cellValue) {
return (int) preg_replace('/\D/', '', $rowKey);
}
}
return self::cellRow($pCell);
}
$cellAddress = $cellAddress ?? '';
if ($pCell !== null) {
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell);
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet(), $sheetName);
}
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true);
if (strpos($cellAddress, ':') !== false) {
[$startAddress, $endAddress] = explode(':', $cellAddress);
$startAddress = preg_replace('/\D/', '', $startAddress);
$endAddress = preg_replace('/\D/', '', $endAddress);
return array_map(
function ($value) {
return [$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 Functions::VALUE();
}
reset($cellAddress);
$isMatrix = (is_numeric(key($cellAddress)));
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress);
if ($isMatrix) {
return $columns;
}
return $rows;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Selection
{
/**
* 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 ...$chooseArgs Data values
*
* @return mixed The selected value
*/
public static function choose(...$chooseArgs)
{
$chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
$entryCount = count($chooseArgs) - 1;
if (is_array($chosenEntry)) {
$chosenEntry = array_shift($chosenEntry);
}
if (is_numeric($chosenEntry)) {
--$chosenEntry;
} else {
return Functions::VALUE();
}
$chosenEntry = floor($chosenEntry);
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
return Functions::VALUE();
}
if (is_array($chooseArgs[$chosenEntry])) {
return Functions::flattenArray($chooseArgs[$chosenEntry]);
}
return $chooseArgs[$chosenEntry];
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class VLookup extends LookupBase
{
/**
* 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($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{
$lookupValue = Functions::flattenSingleValue($lookupValue);
$indexNumber = Functions::flattenSingleValue($indexNumber);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
try {
$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 Functions::REF();
}
$columnKeys = array_keys($lookupArray[$firstRow]);
$returnColumn = $columnKeys[--$indexNumber];
$firstColumn = array_shift($columnKeys);
if (!$notExactMatch) {
uasort($lookupArray, ['self', 'vlookupSort']);
}
$rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
if ($rowNumber !== null) {
// return the appropriate value
return $lookupArray[$rowNumber][$returnColumn];
}
return Functions::NA();
}
private static function vlookupSort($a, $b)
{
reset($a);
$firstColumn = key($a);
$aLower = StringHelper::strToLower($a[$firstColumn]);
$bLower = StringHelper::strToLower($b[$firstColumn]);
if ($aLower == $bLower) {
return 0;
}
return ($aLower < $bLower) ? -1 : 1;
}
private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch)
{
$lookupLower = StringHelper::strToLower($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($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;
}
}