commitall

This commit is contained in:
Sampanna Rimal
2024-07-10 18:28:19 +05:45
parent 140abda4e6
commit 9cd05ef3cb
15723 changed files with 4818733 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentHelper;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentProcessor;
trait ArrayEnabled
{
/**
* @var ArrayArgumentHelper
*/
private static $arrayArgumentHelper;
/**
* @param array|false $arguments Can be changed to array for Php8.1+
*/
private static function initialiseHelper($arguments): void
{
if (self::$arrayArgumentHelper === null) {
self::$arrayArgumentHelper = new ArrayArgumentHelper();
}
self::$arrayArgumentHelper->initialise(($arguments === false) ? [] : $arguments);
}
/**
* Handles array argument processing when the function accepts a single argument that can be an array argument.
* Example use for:
* DAYOFMONTH() or FACT().
*/
protected static function evaluateSingleArgumentArray(callable $method, array $values): array
{
$result = [];
foreach ($values as $value) {
$result[] = $method($value);
}
return $result;
}
/**
* Handles array argument processing when the function accepts multiple arguments,
* and any of them can be an array argument.
* Example use for:
* ROUND() or DATE().
*
* @param mixed ...$arguments
*/
protected static function evaluateArrayArguments(callable $method, ...$arguments): array
{
self::initialiseHelper($arguments);
$arguments = self::$arrayArgumentHelper->arguments();
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
}
/**
* Handles array argument processing when the function accepts multiple arguments,
* but only the first few (up to limit) can be an array arguments.
* Example use for:
* NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need
* to be treated as a such rather than as an array arguments.
*
* @param mixed ...$arguments
*/
protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array
{
self::initialiseHelper(array_slice($arguments, 0, $limit));
$trailingArguments = array_slice($arguments, $limit);
$arguments = self::$arrayArgumentHelper->arguments();
$arguments = array_merge($arguments, $trailingArguments);
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
}
/**
* @param mixed $value
*/
private static function testFalse($value): bool
{
return $value === false;
}
/**
* Handles array argument processing when the function accepts multiple arguments,
* but only the last few (from start) can be an array arguments.
* Example use for:
* Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset
* rather than as an array argument.
*
* @param mixed ...$arguments
*/
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array
{
$arrayArgumentsSubset = array_combine(
range($start, count($arguments) - $start),
array_slice($arguments, $start)
);
if (self::testFalse($arrayArgumentsSubset)) {
return ['#VALUE!'];
}
self::initialiseHelper($arrayArgumentsSubset);
$leadingArguments = array_slice($arguments, 0, $start);
$arguments = self::$arrayArgumentHelper->arguments();
$arguments = array_merge($leadingArguments, $arguments);
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
}
/**
* Handles array argument processing when the function accepts multiple arguments,
* and any of them can be an array argument except for the one specified by ignore.
* Example use for:
* HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database
* rather than as an array argument.
*
* @param mixed ...$arguments
*/
protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, ...$arguments): array
{
$leadingArguments = array_slice($arguments, 0, $ignore);
$ignoreArgument = array_slice($arguments, $ignore, 1);
$trailingArguments = array_slice($arguments, $ignore + 1);
self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments));
$arguments = self::$arrayArgumentHelper->arguments();
array_splice($arguments, $ignore, 1, $ignoreArgument);
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class BinaryComparison
{
/**
* Epsilon Precision used for comparisons in calculations.
*/
private const DELTA = 0.1e-12;
/**
* Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
*
* @param null|string $str1 First string value for the comparison
* @param null|string $str2 Second string value for the comparison
*/
private static function strcmpLowercaseFirst($str1, $str2): int
{
$inversedStr1 = StringHelper::strCaseReverse($str1 ?? '');
$inversedStr2 = StringHelper::strCaseReverse($str2 ?? '');
return strcmp($inversedStr1, $inversedStr2);
}
/**
* PHP8.1 deprecates passing null to strcmp.
*
* @param null|string $str1 First string value for the comparison
* @param null|string $str2 Second string value for the comparison
*/
private static function strcmpAllowNull($str1, $str2): int
{
return strcmp($str1 ?? '', $str2 ?? '');
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
public static function compare($operand1, $operand2, string $operator): bool
{
// Simple validate the two operands if they are string values
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) {
$operand1 = Calculation::unwrapResult($operand1);
}
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) {
$operand2 = Calculation::unwrapResult($operand2);
}
// Use case insensitive comparaison if not OpenOffice mode
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
if (is_string($operand1)) {
$operand1 = StringHelper::strToUpper($operand1);
}
if (is_string($operand2)) {
$operand2 = StringHelper::strToUpper($operand2);
}
}
$useLowercaseFirstComparison = is_string($operand1) &&
is_string($operand2) &&
Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE;
return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison);
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function evaluateComparison($operand1, $operand2, string $operator, bool $useLowercaseFirstComparison): bool
{
switch ($operator) {
// Equality
case '=':
return self::equal($operand1, $operand2);
// Greater than
case '>':
return self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison);
// Less than
case '<':
return self::lessThan($operand1, $operand2, $useLowercaseFirstComparison);
// Greater than or equal
case '>=':
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison);
// Less than or equal
case '<=':
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison);
// Inequality
case '<>':
return self::notEqual($operand1, $operand2);
default:
throw new Exception('Unsupported binary comparison operator');
}
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function equal($operand1, $operand2): bool
{
if (is_numeric($operand1) && is_numeric($operand2)) {
$result = (abs($operand1 - $operand2) < self::DELTA);
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
$result = $operand1 == $operand2;
} else {
$result = self::strcmpAllowNull($operand1, $operand2) == 0;
}
return $result;
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function greaterThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool
{
if (is_numeric($operand1) && is_numeric($operand2)) {
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 > $operand2));
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
$result = $operand1 >= $operand2;
} elseif ($useLowercaseFirstComparison) {
$result = self::strcmpLowercaseFirst($operand1, $operand2) >= 0;
} else {
$result = self::strcmpAllowNull($operand1, $operand2) >= 0;
}
return $result;
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function lessThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool
{
if (is_numeric($operand1) && is_numeric($operand2)) {
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 < $operand2));
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
$result = $operand1 <= $operand2;
} elseif ($useLowercaseFirstComparison) {
$result = self::strcmpLowercaseFirst($operand1, $operand2) <= 0;
} else {
$result = self::strcmpAllowNull($operand1, $operand2) <= 0;
}
return $result;
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function greaterThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool
{
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function lessThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool
{
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
}
/**
* @param mixed $operand1
* @param mixed $operand2
*/
private static function notEqual($operand1, $operand2): bool
{
return self::equal($operand1, $operand2) !== true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
abstract class Category
{
// Function categories
const CATEGORY_CUBE = 'Cube';
const CATEGORY_DATABASE = 'Database';
const CATEGORY_DATE_AND_TIME = 'Date and Time';
const CATEGORY_ENGINEERING = 'Engineering';
const CATEGORY_FINANCIAL = 'Financial';
const CATEGORY_INFORMATION = 'Information';
const CATEGORY_LOGICAL = 'Logical';
const CATEGORY_LOOKUP_AND_REFERENCE = 'Lookup and Reference';
const CATEGORY_MATH_AND_TRIG = 'Math and Trig';
const CATEGORY_STATISTICAL = 'Statistical';
const CATEGORY_TEXT_AND_DATA = 'Text and Data';
const CATEGORY_WEB = 'Web';
const CATEGORY_UNCATEGORISED = 'Uncategorised';
}

View File

@@ -0,0 +1,430 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
/**
* @deprecated 1.17.0
*
* @codeCoverageIgnore
*/
class Database
{
/**
* DAVERAGE.
*
* Averages the values in a column of a list or database that match conditions you specify.
*
* Excel Function:
* DAVERAGE(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DAverage class instead
* @see Database\DAverage::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function DAVERAGE($database, $field, $criteria)
{
return Database\DAverage::evaluate($database, $field, $criteria);
}
/**
* DCOUNT.
*
* Counts the cells that contain numbers in a column of a list or database that match conditions
* that you specify.
*
* Excel Function:
* DCOUNT(database,[field],criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DCount class instead
* @see Database\DCount::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param null|int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int|string
*
* @TODO The field argument is optional. If field is omitted, DCOUNT counts all records in the
* database that match the criteria.
*/
public static function DCOUNT($database, $field, $criteria)
{
return Database\DCount::evaluate($database, $field, $criteria);
}
/**
* DCOUNTA.
*
* Counts the nonblank cells in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DCOUNTA(database,[field],criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DCountA class instead
* @see Database\DCountA::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int|string
*/
public static function DCOUNTA($database, $field, $criteria)
{
return Database\DCountA::evaluate($database, $field, $criteria);
}
/**
* DGET.
*
* Extracts a single value from a column of a list or database that matches conditions that you
* specify.
*
* Excel Function:
* DGET(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DGet class instead
* @see Database\DGet::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return mixed
*/
public static function DGET($database, $field, $criteria)
{
return Database\DGet::evaluate($database, $field, $criteria);
}
/**
* DMAX.
*
* Returns the largest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMAX(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DMax class instead
* @see Database\DMax::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function DMAX($database, $field, $criteria)
{
return Database\DMax::evaluate($database, $field, $criteria);
}
/**
* DMIN.
*
* Returns the smallest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMIN(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DMin class instead
* @see Database\DMin::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function DMIN($database, $field, $criteria)
{
return Database\DMin::evaluate($database, $field, $criteria);
}
/**
* DPRODUCT.
*
* Multiplies the values in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DPRODUCT(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DProduct class instead
* @see Database\DProduct::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function DPRODUCT($database, $field, $criteria)
{
return Database\DProduct::evaluate($database, $field, $criteria);
}
/**
* DSTDEV.
*
* Estimates the standard deviation of a population based on a sample by using the numbers in a
* column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEV(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DStDev class instead
* @see Database\DStDev::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function DSTDEV($database, $field, $criteria)
{
return Database\DStDev::evaluate($database, $field, $criteria);
}
/**
* DSTDEVP.
*
* Calculates the standard deviation of a population based on the entire population by using the
* numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEVP(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DStDevP class instead
* @see Database\DStDevP::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function DSTDEVP($database, $field, $criteria)
{
return Database\DStDevP::evaluate($database, $field, $criteria);
}
/**
* DSUM.
*
* Adds the numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSUM(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DSum class instead
* @see Database\DSum::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function DSUM($database, $field, $criteria)
{
return Database\DSum::evaluate($database, $field, $criteria);
}
/**
* DVAR.
*
* Estimates the variance of a population based on a sample by using the numbers in a column
* of a list or database that match conditions that you specify.
*
* Excel Function:
* DVAR(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DVar class instead
* @see Database\DVar::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function DVAR($database, $field, $criteria)
{
return Database\DVar::evaluate($database, $field, $criteria);
}
/**
* DVARP.
*
* Calculates the variance of a population based on the entire population by using the numbers
* in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DVARP(database,field,criteria)
*
* @deprecated 1.17.0
* Use the evaluate() method in the Database\DVarP class instead
* @see Database\DVarP::evaluate()
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function DVARP($database, $field, $criteria)
{
return Database\DVarP::evaluate($database, $field, $criteria);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages;
class DAverage extends DatabaseAbstract
{
/**
* DAVERAGE.
*
* Averages the values in a column of a list or database that match conditions you specify.
*
* Excel Function:
* DAVERAGE(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Averages::average(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts;
class DCount extends DatabaseAbstract
{
/**
* DCOUNT.
*
* Counts the cells that contain numbers in a column of a list or database that match conditions
* that you specify.
*
* Excel Function:
* DCOUNT(database,[field],criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param null|int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int|string
*/
public static function evaluate($database, $field, $criteria, bool $returnError = true)
{
$field = self::fieldExtract($database, $field);
if ($returnError && $field === null) {
return ExcelError::VALUE();
}
return Counts::COUNT(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts;
class DCountA extends DatabaseAbstract
{
/**
* DCOUNTA.
*
* Counts the nonblank cells in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DCOUNTA(database,[field],criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Counts::COUNTA(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class DGet extends DatabaseAbstract
{
/**
* DGET.
*
* Extracts a single value from a column of a list or database that matches conditions that you
* specify.
*
* Excel Function:
* DGET(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return mixed
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
$columnData = self::getFilteredColumn($database, $field, $criteria);
if (count($columnData) > 1) {
return ExcelError::NAN();
}
$row = array_pop($columnData);
return array_pop($row);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum;
class DMax extends DatabaseAbstract
{
/**
* DMAX.
*
* Returns the largest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMAX(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function evaluate($database, $field, $criteria, bool $returnError = true)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return $returnError ? ExcelError::VALUE() : null;
}
return Maximum::max(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum;
class DMin extends DatabaseAbstract
{
/**
* DMIN.
*
* Returns the smallest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMIN(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function evaluate($database, $field, $criteria, bool $returnError = true)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return $returnError ? ExcelError::VALUE() : null;
}
return Minimum::min(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class DProduct extends DatabaseAbstract
{
/**
* DPRODUCT.
*
* Multiplies the values in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DPRODUCT(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return MathTrig\Operations::product(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations;
class DStDev extends DatabaseAbstract
{
/**
* DSTDEV.
*
* Estimates the standard deviation of a population based on a sample by using the numbers in a
* column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEV(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return StandardDeviations::STDEV(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations;
class DStDevP extends DatabaseAbstract
{
/**
* DSTDEVP.
*
* Calculates the standard deviation of a population based on the entire population by using the
* numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEVP(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return StandardDeviations::STDEVP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class DSum extends DatabaseAbstract
{
/**
* DSUM.
*
* Adds the numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSUM(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return null|float|string
*/
public static function evaluate($database, $field, $criteria, bool $returnNull = false)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return $returnNull ? null : ExcelError::VALUE();
}
return MathTrig\Sum::sumIgnoringStrings(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances;
class DVar extends DatabaseAbstract
{
/**
* DVAR.
*
* Estimates the variance of a population based on a sample by using the numbers in a column
* of a list or database that match conditions that you specify.
*
* Excel Function:
* DVAR(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Variances::VAR(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances;
class DVarP extends DatabaseAbstract
{
/**
* DVARP.
*
* Calculates the variance of a population based on the entire population by using the numbers
* in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DVARP(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Variances::VARP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
abstract class DatabaseAbstract
{
/**
* @param array $database
* @param int|string $field
* @param array $criteria
*
* @return null|float|int|string
*/
abstract public static function evaluate($database, $field, $criteria);
/**
* fieldExtract.
*
* Extracts the column ID to use for the data field.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
*/
protected static function fieldExtract(array $database, $field): ?int
{
$field = strtoupper(Functions::flattenSingleValue($field) ?? '');
if ($field === '') {
return null;
}
$fieldNames = array_map('strtoupper', array_shift($database));
if (is_numeric($field)) {
$field = (int) $field - 1;
if ($field < 0 || $field >= count($fieldNames)) {
return null;
}
return $field;
}
$key = array_search($field, array_values($fieldNames), true);
return ($key !== false) ? (int) $key : null;
}
/**
* filter.
*
* Parses the selection criteria, extracts the database rows that match those criteria, and
* returns that subset of rows.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return mixed[]
*/
protected static function filter(array $database, array $criteria): array
{
$fieldNames = array_shift($database);
$criteriaNames = array_shift($criteria);
// Convert the criteria into a set of AND/OR conditions with [:placeholders]
$query = self::buildQuery($criteriaNames, $criteria);
// Loop through each row of the database
return self::executeQuery($database, $query, $criteriaNames, $fieldNames);
}
protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array
{
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
$defaultReturnColumnValue = ($field === null) ? 1 : null;
// extract an array of values for the requested column
$columnData = [];
foreach ($database as $rowKey => $row) {
$keys = array_keys($row);
$key = $keys[$field] ?? null;
$columnKey = $key ?? 'A';
$columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue;
}
return $columnData;
}
private static function buildQuery(array $criteriaNames, array $criteria): string
{
$baseQuery = [];
foreach ($criteria as $key => $criterion) {
foreach ($criterion as $field => $value) {
$criterionName = $criteriaNames[$field];
if ($value !== null) {
$condition = self::buildCondition($value, $criterionName);
$baseQuery[$key][] = $condition;
}
}
}
$rowQuery = array_map(
function ($rowValue) {
return (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? '');
},
$baseQuery
);
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : ($rowQuery[0] ?? '');
}
/**
* @param mixed $criterion
*/
private static function buildCondition($criterion, string $criterionName): string
{
$ifCondition = Functions::ifCondition($criterion);
// Check for wildcard characters used in the condition
$result = preg_match('/(?<operator>[^"]*)(?<operand>".*[*?].*")/ui', $ifCondition, $matches);
if ($result !== 1) {
return "[:{$criterionName}]{$ifCondition}";
}
$trueFalse = ($matches['operator'] !== '<>');
$wildcard = WildcardMatch::wildcard($matches['operand']);
$condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})";
if ($trueFalse === false) {
$condition = "NOT({$condition})";
}
return $condition;
}
private static function executeQuery(array $database, string $query, array $criteria, array $fields): array
{
foreach ($database as $dataRow => $dataValues) {
// Substitute actual values from the database row for our [:placeholders]
$conditions = $query;
foreach ($criteria as $criterion) {
$conditions = self::processCondition($criterion, $fields, $dataValues, $conditions);
}
// evaluate the criteria against the row data
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions);
// If the row failed to meet the criteria, remove it from the database
if ($result !== true) {
unset($database[$dataRow]);
}
}
return $database;
}
/**
* @return mixed
*/
private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions)
{
$key = array_search($criterion, $fields, true);
$dataValue = 'NULL';
if (is_bool($dataValues[$key])) {
$dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE';
} elseif ($dataValues[$key] !== null) {
$dataValue = $dataValues[$key];
// escape quotes if we have a string containing quotes
if (is_string($dataValue) && strpos($dataValue, '"') !== false) {
$dataValue = str_replace('"', '""', $dataValue);
}
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
}
return str_replace('[:' . $criterion . ']', $dataValue, $conditions);
}
}

View File

@@ -0,0 +1,890 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use DateTimeInterface;
/**
* @deprecated 1.18.0
*/
class DateTime
{
/**
* Identify if a year is a leap year or not.
*
* @deprecated 1.18.0
* Use the isLeapYear method in the DateTimeExcel\Helpers class instead
* @see DateTimeExcel\Helpers::isLeapYear()
*
* @param int|string $year The year to test
*
* @return bool TRUE if the year is a leap year, otherwise FALSE
*/
public static function isLeapYear($year)
{
return DateTimeExcel\Helpers::isLeapYear($year);
}
/**
* getDateValue.
*
* @deprecated 1.18.0
* Use the getDateValue method in the DateTimeExcel\Helpers class instead
* @see DateTimeExcel\Helpers::getDateValue()
*
* @param mixed $dateValue
*
* @return mixed Excel date/time serial value, or string if error
*/
public static function getDateValue($dateValue)
{
try {
return DateTimeExcel\Helpers::getDateValue($dateValue);
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* DATETIMENOW.
*
* Returns the current date and time.
* The NOW function is useful when you need to display the current date and time on a worksheet or
* calculate a value based on the current date and time, and have that value updated each time you
* open the worksheet.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* NOW()
*
* @deprecated 1.18.0
* Use the now method in the DateTimeExcel\Current class instead
* @see DateTimeExcel\Current::now()
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function DATETIMENOW()
{
return DateTimeExcel\Current::now();
}
/**
* DATENOW.
*
* Returns the current date.
* The NOW function is useful when you need to display the current date and time on a worksheet or
* calculate a value based on the current date and time, and have that value updated each time you
* open the worksheet.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TODAY()
*
* @deprecated 1.18.0
* Use the today method in the DateTimeExcel\Current class instead
* @see DateTimeExcel\Current::today()
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function DATENOW()
{
return DateTimeExcel\Current::today();
}
/**
* DATE.
*
* The DATE function returns a value that represents a particular date.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
*
* Excel Function:
* DATE(year,month,day)
*
* @deprecated 1.18.0
* Use the fromYMD method in the DateTimeExcel\Date class instead
* @see DateTimeExcel\Date::fromYMD()
*
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
*
* @param int $year The value of the year argument can include one to four digits.
* Excel interprets the year argument according to the configured
* date system: 1900 or 1904.
* If year is between 0 (zero) and 1899 (inclusive), Excel adds that
* value to 1900 to calculate the year. For example, DATE(108,1,2)
* returns January 2, 2008 (1900+108).
* If year is between 1900 and 9999 (inclusive), Excel uses that
* value as the year. For example, DATE(2008,1,2) returns January 2,
* 2008.
* If year is less than 0 or is 10000 or greater, Excel returns the
* #NUM! error value.
* @param int $month A positive or negative integer representing the month of the year
* from 1 to 12 (January to December).
* If month is greater than 12, month adds that number of months to
* the first month in the year specified. For example, DATE(2008,14,2)
* returns the serial number representing February 2, 2009.
* If month is less than 1, month subtracts the magnitude of that
* number of months, plus 1, from the first month in the year
* specified. For example, DATE(2008,-3,2) returns the serial number
* representing September 2, 2007.
* @param int $day A positive or negative integer representing the day of the month
* from 1 to 31.
* If day is greater than the number of days in the month specified,
* day adds that number of days to the first day in the month. For
* example, DATE(2008,1,35) returns the serial number representing
* February 4, 2008.
* If day is less than 1, day subtracts the magnitude that number of
* days, plus one, from the first day of the month specified. For
* example, DATE(2008,1,-15) returns the serial number representing
* December 16, 2007.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function DATE($year = 0, $month = 1, $day = 1)
{
return DateTimeExcel\Date::fromYMD($year, $month, $day);
}
/**
* TIME.
*
* The TIME function returns a value that represents a particular time.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TIME(hour,minute,second)
*
* @deprecated 1.18.0
* Use the fromHMS method in the DateTimeExcel\Time class instead
* @see DateTimeExcel\Time::fromHMS()
*
* @param int $hour A number from 0 (zero) to 32767 representing the hour.
* Any value greater than 23 will be divided by 24 and the remainder
* will be treated as the hour value. For example, TIME(27,0,0) =
* TIME(3,0,0) = .125 or 3:00 AM.
* @param int $minute A number from 0 to 32767 representing the minute.
* Any value greater than 59 will be converted to hours and minutes.
* For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM.
* @param int $second A number from 0 to 32767 representing the second.
* Any value greater than 59 will be converted to hours, minutes,
* and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148
* or 12:33:20 AM
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function TIME($hour = 0, $minute = 0, $second = 0)
{
return DateTimeExcel\Time::fromHMS($hour, $minute, $second);
}
/**
* DATEVALUE.
*
* Returns a value that represents a particular date.
* Use DATEVALUE to convert a date represented by a text string to an Excel or PHP date/time stamp
* value.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* DATEVALUE(dateValue)
*
* @deprecated 1.18.0
* Use the fromString method in the DateTimeExcel\DateValue class instead
* @see DateTimeExcel\DateValue::fromString()
*
* @param string $dateValue Text that represents a date in a Microsoft Excel date format.
* For example, "1/30/2008" or "30-Jan-2008" are text strings within
* quotation marks that represent dates. Using the default date
* system in Excel for Windows, date_text must represent a date from
* January 1, 1900, to December 31, 9999. Using the default date
* system in Excel for the Macintosh, date_text must represent a date
* from January 1, 1904, to December 31, 9999. DATEVALUE returns the
* #VALUE! error value if date_text is out of this range.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function DATEVALUE($dateValue)
{
return DateTimeExcel\DateValue::fromString($dateValue);
}
/**
* TIMEVALUE.
*
* Returns a value that represents a particular time.
* Use TIMEVALUE to convert a time represented by a text string to an Excel or PHP date/time stamp
* value.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TIMEVALUE(timeValue)
*
* @deprecated 1.18.0
* Use the fromString method in the DateTimeExcel\TimeValue class instead
* @see DateTimeExcel\TimeValue::fromString()
*
* @param string $timeValue A text string that represents a time in any one of the Microsoft
* Excel time formats; for example, "6:45 PM" and "18:45" text strings
* within quotation marks that represent time.
* Date information in time_text is ignored.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function TIMEVALUE($timeValue)
{
return DateTimeExcel\TimeValue::fromString($timeValue);
}
/**
* DATEDIF.
*
* Excel Function:
* DATEDIF(startdate, enddate, unit)
*
* @deprecated 1.18.0
* Use the interval method in the DateTimeExcel\Difference class instead
* @see DateTimeExcel\Difference::interval()
*
* @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object
* or a standard date string
* @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object
* or a standard date string
* @param array|string $unit
*
* @return array|int|string Interval between the dates
*/
public static function DATEDIF($startDate = 0, $endDate = 0, $unit = 'D')
{
return DateTimeExcel\Difference::interval($startDate, $endDate, $unit);
}
/**
* DAYS.
*
* Returns the number of days between two dates
*
* Excel Function:
* DAYS(endDate, startDate)
*
* @deprecated 1.18.0
* Use the between method in the DateTimeExcel\Days class instead
* @see DateTimeExcel\Days::between()
*
* @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float),
* PHP date timestamp (integer), PHP DateTime object, or a standard date string
* @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float),
* PHP date timestamp (integer), PHP DateTime object, or a standard date string
*
* @return array|int|string Number of days between start date and end date or an error
*/
public static function DAYS($endDate = 0, $startDate = 0)
{
return DateTimeExcel\Days::between($endDate, $startDate);
}
/**
* DAYS360.
*
* Returns the number of days between two dates based on a 360-day year (twelve 30-day months),
* which is used in some accounting calculations. Use this function to help compute payments if
* your accounting system is based on twelve 30-day months.
*
* Excel Function:
* DAYS360(startDate,endDate[,method])
*
* @deprecated 1.18.0
* Use the between method in the DateTimeExcel\Days360 class instead
* @see DateTimeExcel\Days360::between()
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param array|bool $method US or European Method
* FALSE or omitted: U.S. (NASD) method. If the starting date is
* the last day of a month, it becomes equal to the 30th of the
* same month. If the ending date is the last day of a month and
* the starting date is earlier than the 30th of a month, the
* ending date becomes equal to the 1st of the next month;
* otherwise the ending date becomes equal to the 30th of the
* same month.
* TRUE: European method. Starting dates and ending dates that
* occur on the 31st of a month become equal to the 30th of the
* same month.
*
* @return array|int|string Number of days between start date and end date
*/
public static function DAYS360($startDate = 0, $endDate = 0, $method = false)
{
return DateTimeExcel\Days360::between($startDate, $endDate, $method);
}
/**
* YEARFRAC.
*
* Calculates the fraction of the year represented by the number of whole days between two dates
* (the start_date and the end_date).
* Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or
* obligations to assign to a specific term.
*
* Excel Function:
* YEARFRAC(startDate,endDate[,method])
*
* @deprecated 1.18.0
* Use the fraction method in the DateTimeExcel\YearFrac class instead
* @see DateTimeExcel\YearFrac::fraction()
*
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html
* for description of algorithm used in Excel
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param array|int $method Method used for the calculation
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return array|float|string fraction of the year, or a string containing an error
*/
public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0)
{
return DateTimeExcel\YearFrac::fraction($startDate, $endDate, $method);
}
/**
* NETWORKDAYS.
*
* Returns the number of whole working days between start_date and end_date. Working days
* exclude weekends and any dates identified in holidays.
* Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days
* worked during a specific term.
*
* Excel Function:
* NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]])
*
* @deprecated 1.18.0
* Use the count method in the DateTimeExcel\NetworkDays class instead
* @see DateTimeExcel\NetworkDays::count()
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param mixed $dateArgs
*
* @return array|int|string Interval between the dates
*/
public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs)
{
return DateTimeExcel\NetworkDays::count($startDate, $endDate, ...$dateArgs);
}
/**
* WORKDAY.
*
* Returns the date that is the indicated number of working days before or after a date (the
* starting date). Working days exclude weekends and any dates identified as holidays.
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected
* delivery times, or the number of days of work performed.
*
* Excel Function:
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]])
*
* @deprecated 1.18.0
* Use the date method in the DateTimeExcel\WorkDay class instead
* @see DateTimeExcel\WorkDay::date()
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param int $endDays The number of nonweekend and nonholiday days before or after
* startDate. A positive value for days yields a future date; a
* negative value yields a past date.
* @param mixed $dateArgs
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function WORKDAY($startDate, $endDays, ...$dateArgs)
{
return DateTimeExcel\WorkDay::date($startDate, $endDays, ...$dateArgs);
}
/**
* DAYOFMONTH.
*
* Returns the day of the month, for a specified date. The day is given as an integer
* ranging from 1 to 31.
*
* Excel Function:
* DAY(dateValue)
*
* @deprecated 1.18.0
* Use the day method in the DateTimeExcel\DateParts class instead
* @see DateTimeExcel\DateParts::day()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*
* @return array|int|string Day of the month
*/
public static function DAYOFMONTH($dateValue = 1)
{
return DateTimeExcel\DateParts::day($dateValue);
}
/**
* WEEKDAY.
*
* Returns the day of the week for a specified date. The day is given as an integer
* ranging from 0 to 7 (dependent on the requested style).
*
* Excel Function:
* WEEKDAY(dateValue[,style])
*
* @deprecated 1.18.0
* Use the day method in the DateTimeExcel\Week class instead
* @see DateTimeExcel\Week::day()
*
* @param float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param int $style A number that determines the type of return value
* 1 or omitted Numbers 1 (Sunday) through 7 (Saturday).
* 2 Numbers 1 (Monday) through 7 (Sunday).
* 3 Numbers 0 (Monday) through 6 (Sunday).
*
* @return array|int|string Day of the week value
*/
public static function WEEKDAY($dateValue = 1, $style = 1)
{
return DateTimeExcel\Week::day($dateValue, $style);
}
/**
* STARTWEEK_SUNDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_SUNDAY
* @see DateTimeExcel\Constants::STARTWEEK_SUNDAY
*/
const STARTWEEK_SUNDAY = 1;
/**
* STARTWEEK_MONDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY
*/
const STARTWEEK_MONDAY = 2;
/**
* STARTWEEK_MONDAY_ALT.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY_ALT
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY_ALT
*/
const STARTWEEK_MONDAY_ALT = 11;
/**
* STARTWEEK_TUESDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_TUESDAY
* @see DateTimeExcel\Constants::STARTWEEK_TUESDAY
*/
const STARTWEEK_TUESDAY = 12;
/**
* STARTWEEK_WEDNESDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_WEDNESDAY
* @see DateTimeExcel\Constants::STARTWEEK_WEDNESDAY
*/
const STARTWEEK_WEDNESDAY = 13;
/**
* STARTWEEK_THURSDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_THURSDAY
* @see DateTimeExcel\Constants::STARTWEEK_THURSDAY
*/
const STARTWEEK_THURSDAY = 14;
/**
* STARTWEEK_FRIDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_FRIDAY
* @see DateTimeExcel\Constants::STARTWEEK_FRIDAY
*/
const STARTWEEK_FRIDAY = 15;
/**
* STARTWEEK_SATURDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_SATURDAY
* @see DateTimeExcel\Constants::STARTWEEK_SATURDAY
*/
const STARTWEEK_SATURDAY = 16;
/**
* STARTWEEK_SUNDAY_ALT.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_SUNDAY_ALT
* @see DateTimeExcel\Constants::STARTWEEK_SUNDAY_ALT
*/
const STARTWEEK_SUNDAY_ALT = 17;
/**
* DOW_SUNDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_SUNDAY
* @see DateTimeExcel\Constants::DOW_SUNDAY
*/
const DOW_SUNDAY = 1;
/**
* DOW_MONDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_MONDAY
* @see DateTimeExcel\Constants::DOW_MONDAY
*/
const DOW_MONDAY = 2;
/**
* DOW_TUESDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_TUESDAY
* @see DateTimeExcel\Constants::DOW_TUESDAY
*/
const DOW_TUESDAY = 3;
/**
* DOW_WEDNESDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_WEDNESDAY
* @see DateTimeExcel\Constants::DOW_WEDNESDAY
*/
const DOW_WEDNESDAY = 4;
/**
* DOW_THURSDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_THURSDAY
* @see DateTimeExcel\Constants::DOW_THURSDAY
*/
const DOW_THURSDAY = 5;
/**
* DOW_FRIDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_FRIDAY
* @see DateTimeExcel\Constants::DOW_FRIDAY
*/
const DOW_FRIDAY = 6;
/**
* DOW_SATURDAY.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::DOW_SATURDAY
* @see DateTimeExcel\Constants::DOW_SATURDAY
*/
const DOW_SATURDAY = 7;
/**
* STARTWEEK_MONDAY_ISO.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY_ISO
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY_ISO
*/
const STARTWEEK_MONDAY_ISO = 21;
/**
* METHODARR.
*
* @deprecated 1.18.0
* Use DateTimeExcel\Constants::METHODARR
* @see DateTimeExcel\Constants::METHODARR
*/
const METHODARR = [
self::STARTWEEK_SUNDAY => self::DOW_SUNDAY,
self::DOW_MONDAY,
self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY,
self::DOW_TUESDAY,
self::DOW_WEDNESDAY,
self::DOW_THURSDAY,
self::DOW_FRIDAY,
self::DOW_SATURDAY,
self::DOW_SUNDAY,
self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO,
];
/**
* WEEKNUM.
*
* Returns the week of the year for a specified date.
* The WEEKNUM function considers the week containing January 1 to be the first week of the year.
* However, there is a European standard that defines the first week as the one with the majority
* of days (four or more) falling in the new year. This means that for years in which there are
* three days or less in the first week of January, the WEEKNUM function returns week numbers
* that are incorrect according to the European standard.
*
* Excel Function:
* WEEKNUM(dateValue[,style])
*
* @deprecated 1.18.0
* Use the number method in the DateTimeExcel\Week class instead
* @see DateTimeExcel\Week::number()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param int $method Week begins on Sunday or Monday
* 1 or omitted Week begins on Sunday.
* 2 Week begins on Monday.
* 11 Week begins on Monday.
* 12 Week begins on Tuesday.
* 13 Week begins on Wednesday.
* 14 Week begins on Thursday.
* 15 Week begins on Friday.
* 16 Week begins on Saturday.
* 17 Week begins on Sunday.
* 21 ISO (Jan. 4 is week 1, begins on Monday).
*
* @return array|int|string Week Number
*/
public static function WEEKNUM($dateValue = 1, $method = /** @scrutinizer ignore-deprecated */ self::STARTWEEK_SUNDAY)
{
return DateTimeExcel\Week::number($dateValue, $method);
}
/**
* ISOWEEKNUM.
*
* Returns the ISO 8601 week number of the year for a specified date.
*
* Excel Function:
* ISOWEEKNUM(dateValue)
*
* @deprecated 1.18.0
* Use the isoWeekNumber method in the DateTimeExcel\Week class instead
* @see DateTimeExcel\Week::isoWeekNumber()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*
* @return array|int|string Week Number
*/
public static function ISOWEEKNUM($dateValue = 1)
{
return DateTimeExcel\Week::isoWeekNumber($dateValue);
}
/**
* MONTHOFYEAR.
*
* Returns the month of a date represented by a serial number.
* The month is given as an integer, ranging from 1 (January) to 12 (December).
*
* Excel Function:
* MONTH(dateValue)
*
* @deprecated 1.18.0
* Use the month method in the DateTimeExcel\DateParts class instead
* @see DateTimeExcel\DateParts::month()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*
* @return array|int|string Month of the year
*/
public static function MONTHOFYEAR($dateValue = 1)
{
return DateTimeExcel\DateParts::month($dateValue);
}
/**
* YEAR.
*
* Returns the year corresponding to a date.
* The year is returned as an integer in the range 1900-9999.
*
* Excel Function:
* YEAR(dateValue)
*
* @deprecated 1.18.0
* Use the ear method in the DateTimeExcel\DateParts class instead
* @see DateTimeExcel\DateParts::year()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*
* @return array|int|string Year
*/
public static function YEAR($dateValue = 1)
{
return DateTimeExcel\DateParts::year($dateValue);
}
/**
* HOUROFDAY.
*
* Returns the hour of a time value.
* The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.).
*
* Excel Function:
* HOUR(timeValue)
*
* @deprecated 1.18.0
* Use the hour method in the DateTimeExcel\TimeParts class instead
* @see DateTimeExcel\TimeParts::hour()
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
*
* @return array|int|string Hour
*/
public static function HOUROFDAY($timeValue = 0)
{
return DateTimeExcel\TimeParts::hour($timeValue);
}
/**
* MINUTE.
*
* Returns the minutes of a time value.
* The minute is given as an integer, ranging from 0 to 59.
*
* Excel Function:
* MINUTE(timeValue)
*
* @deprecated 1.18.0
* Use the minute method in the DateTimeExcel\TimeParts class instead
* @see DateTimeExcel\TimeParts::minute()
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
*
* @return array|int|string Minute
*/
public static function MINUTE($timeValue = 0)
{
return DateTimeExcel\TimeParts::minute($timeValue);
}
/**
* SECOND.
*
* Returns the seconds of a time value.
* The second is given as an integer in the range 0 (zero) to 59.
*
* Excel Function:
* SECOND(timeValue)
*
* @deprecated 1.18.0
* Use the second method in the DateTimeExcel\TimeParts class instead
* @see DateTimeExcel\TimeParts::second()
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
*
* @return array|int|string Second
*/
public static function SECOND($timeValue = 0)
{
return DateTimeExcel\TimeParts::second($timeValue);
}
/**
* EDATE.
*
* Returns the serial number that represents the date that is the indicated number of months
* before or after a specified date (the start_date).
* Use EDATE to calculate maturity dates or due dates that fall on the same day of the month
* as the date of issue.
*
* Excel Function:
* EDATE(dateValue,adjustmentMonths)
*
* @deprecated 1.18.0
* Use the adjust method in the DateTimeExcel\Edate class instead
* @see DateTimeExcel\Month::adjust()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param int $adjustmentMonths The number of months before or after start_date.
* A positive value for months yields a future date;
* a negative value yields a past date.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function EDATE($dateValue = 1, $adjustmentMonths = 0)
{
return DateTimeExcel\Month::adjust($dateValue, $adjustmentMonths);
}
/**
* EOMONTH.
*
* Returns the date value for the last day of the month that is the indicated number of months
* before or after start_date.
* Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month.
*
* Excel Function:
* EOMONTH(dateValue,adjustmentMonths)
*
* @deprecated 1.18.0
* Use the lastDay method in the DateTimeExcel\EoMonth class instead
* @see DateTimeExcel\Month::lastDay()
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* @param int $adjustmentMonths The number of months before or after start_date.
* A positive value for months yields a future date;
* a negative value yields a past date.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function EOMONTH($dateValue = 1, $adjustmentMonths = 0)
{
return DateTimeExcel\Month::lastDay($dateValue, $adjustmentMonths);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
class Constants
{
// Constants currently used by WeekNum; will eventually be used by WEEKDAY
const STARTWEEK_SUNDAY = 1;
const STARTWEEK_MONDAY = 2;
const STARTWEEK_MONDAY_ALT = 11;
const STARTWEEK_TUESDAY = 12;
const STARTWEEK_WEDNESDAY = 13;
const STARTWEEK_THURSDAY = 14;
const STARTWEEK_FRIDAY = 15;
const STARTWEEK_SATURDAY = 16;
const STARTWEEK_SUNDAY_ALT = 17;
const DOW_SUNDAY = 1;
const DOW_MONDAY = 2;
const DOW_TUESDAY = 3;
const DOW_WEDNESDAY = 4;
const DOW_THURSDAY = 5;
const DOW_FRIDAY = 6;
const DOW_SATURDAY = 7;
const STARTWEEK_MONDAY_ISO = 21;
const METHODARR = [
self::STARTWEEK_SUNDAY => self::DOW_SUNDAY,
self::DOW_MONDAY,
self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY,
self::DOW_TUESDAY,
self::DOW_WEDNESDAY,
self::DOW_THURSDAY,
self::DOW_FRIDAY,
self::DOW_SATURDAY,
self::DOW_SUNDAY,
self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO,
];
}

View File

@@ -0,0 +1,59 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTimeImmutable;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Current
{
/**
* DATENOW.
*
* Returns the current date.
* The NOW function is useful when you need to display the current date and time on a worksheet or
* calculate a value based on the current date and time, and have that value updated each time you
* open the worksheet.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TODAY()
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function today()
{
$dti = new DateTimeImmutable();
$dateArray = Helpers::dateParse($dti->format('c'));
return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : ExcelError::VALUE();
}
/**
* DATETIMENOW.
*
* Returns the current date and time.
* The NOW function is useful when you need to display the current date and time on a worksheet or
* calculate a value based on the current date and time, and have that value updated each time you
* open the worksheet.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* NOW()
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function now()
{
$dti = new DateTimeImmutable();
$dateArray = Helpers::dateParse($dti->format('c'));
return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : ExcelError::VALUE();
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class Date
{
use ArrayEnabled;
/**
* DATE.
*
* The DATE function returns a value that represents a particular date.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* DATE(year,month,day)
*
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
*
* @param array|int $year The value of the year argument can include one to four digits.
* Excel interprets the year argument according to the configured
* date system: 1900 or 1904.
* If year is between 0 (zero) and 1899 (inclusive), Excel adds that
* value to 1900 to calculate the year. For example, DATE(108,1,2)
* returns January 2, 2008 (1900+108).
* If year is between 1900 and 9999 (inclusive), Excel uses that
* value as the year. For example, DATE(2008,1,2) returns January 2,
* 2008.
* If year is less than 0 or is 10000 or greater, Excel returns the
* #NUM! error value.
* @param array|int $month A positive or negative integer representing the month of the year
* from 1 to 12 (January to December).
* If month is greater than 12, month adds that number of months to
* the first month in the year specified. For example, DATE(2008,14,2)
* returns the serial number representing February 2, 2009.
* If month is less than 1, month subtracts the magnitude of that
* number of months, plus 1, from the first month in the year
* specified. For example, DATE(2008,-3,2) returns the serial number
* representing September 2, 2007.
* @param array|int $day A positive or negative integer representing the day of the month
* from 1 to 31.
* If day is greater than the number of days in the month specified,
* day adds that number of days to the first day in the month. For
* example, DATE(2008,1,35) returns the serial number representing
* February 4, 2008.
* If day is less than 1, day subtracts the magnitude that number of
* days, plus one, from the first day of the month specified. For
* example, DATE(2008,1,-15) returns the serial number representing
* December 16, 2007.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function fromYMD($year, $month, $day)
{
if (is_array($year) || is_array($month) || is_array($day)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $year, $month, $day);
}
$baseYear = SharedDateHelper::getExcelCalendar();
try {
$year = self::getYear($year, $baseYear); // must be int - Scrutinizer is wrong
$month = self::getMonth($month);
$day = self::getDay($day);
self::adjustYearMonth(/** @scrutinizer ignore-type */ $year, $month, $baseYear);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$excelDateValue = SharedDateHelper::formattedPHPToExcel(/** @scrutinizer ignore-type */ $year, $month, $day);
return Helpers::returnIn3FormatsFloat($excelDateValue);
}
/**
* Convert year from multiple formats to int.
*
* @param mixed $year
*/
private static function getYear($year, int $baseYear): int
{
$year = ($year !== null) ? StringHelper::testStringAsNumeric((string) $year) : 0;
if (!is_numeric($year)) {
throw new Exception(ExcelError::VALUE());
}
$year = (int) $year;
if ($year < ($baseYear - 1900)) {
throw new Exception(ExcelError::NAN());
}
if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) {
throw new Exception(ExcelError::NAN());
}
if (($year < $baseYear) && ($year >= ($baseYear - 1900))) {
$year += 1900;
}
return (int) $year;
}
/**
* Convert month from multiple formats to int.
*
* @param mixed $month
*/
private static function getMonth($month): int
{
if (($month !== null) && (!is_numeric($month))) {
$month = SharedDateHelper::monthStringToNumber($month);
}
$month = ($month !== null) ? StringHelper::testStringAsNumeric((string) $month) : 0;
if (!is_numeric($month)) {
throw new Exception(ExcelError::VALUE());
}
return (int) $month;
}
/**
* Convert day from multiple formats to int.
*
* @param mixed $day
*/
private static function getDay($day): int
{
if (($day !== null) && (!is_numeric($day))) {
$day = SharedDateHelper::dayStringToNumber($day);
}
$day = ($day !== null) ? StringHelper::testStringAsNumeric((string) $day) : 0;
if (!is_numeric($day)) {
throw new Exception(ExcelError::VALUE());
}
return (int) $day;
}
private static function adjustYearMonth(int &$year, int &$month, int $baseYear): void
{
if ($month < 1) {
// Handle year/month adjustment if month < 1
--$month;
$year += ceil($month / 12) - 1;
$month = 13 - abs($month % 12);
} elseif ($month > 12) {
// Handle year/month adjustment if month > 12
$year += floor($month / 12);
$month = ($month % 12);
}
// Re-validate the year parameter after adjustments
if (($year < $baseYear) || ($year >= 10000)) {
throw new Exception(ExcelError::NAN());
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class DateParts
{
use ArrayEnabled;
/**
* DAYOFMONTH.
*
* Returns the day of the month, for a specified date. The day is given as an integer
* ranging from 1 to 31.
*
* Excel Function:
* DAY(dateValue)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
*
* @return array|int|string Day of the month
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function day($dateValue)
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
$weirdResult = self::weirdCondition($dateValue);
if ($weirdResult >= 0) {
return $weirdResult;
}
try {
$dateValue = Helpers::getDateValue($dateValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
return (int) $PHPDateObject->format('j');
}
/**
* MONTHOFYEAR.
*
* Returns the month of a date represented by a serial number.
* The month is given as an integer, ranging from 1 (January) to 12 (December).
*
* Excel Function:
* MONTH(dateValue)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
*
* @return array|int|string Month of the year
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function month($dateValue)
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
try {
$dateValue = Helpers::getDateValue($dateValue);
} catch (Exception $e) {
return $e->getMessage();
}
if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) {
return 1;
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
return (int) $PHPDateObject->format('n');
}
/**
* YEAR.
*
* Returns the year corresponding to a date.
* The year is returned as an integer in the range 1900-9999.
*
* Excel Function:
* YEAR(dateValue)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
*
* @return array|int|string Year
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function year($dateValue)
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
try {
$dateValue = Helpers::getDateValue($dateValue);
} catch (Exception $e) {
return $e->getMessage();
}
if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) {
return 1900;
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
return (int) $PHPDateObject->format('Y');
}
/**
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*/
private static function weirdCondition($dateValue): int
{
// Excel does not treat 0 consistently for DAY vs. (MONTH or YEAR)
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900 && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
if (is_bool($dateValue)) {
return (int) $dateValue;
}
if ($dateValue === null) {
return 0;
}
if (is_numeric($dateValue) && $dateValue < 1 && $dateValue >= 0) {
return 0;
}
}
return -1;
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTimeImmutable;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class DateValue
{
use ArrayEnabled;
/**
* DATEVALUE.
*
* Returns a value that represents a particular date.
* Use DATEVALUE to convert a date represented by a text string to an Excel or PHP date/time stamp
* value.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* DATEVALUE(dateValue)
*
* @param array|string $dateValue Text that represents a date in a Microsoft Excel date format.
* For example, "1/30/2008" or "30-Jan-2008" are text strings within
* quotation marks that represent dates. Using the default date
* system in Excel for Windows, date_text must represent a date from
* January 1, 1900, to December 31, 9999. Using the default date
* system in Excel for the Macintosh, date_text must represent a date
* from January 1, 1904, to December 31, 9999. DATEVALUE returns the
* #VALUE! error value if date_text is out of this range.
* Or can be an array of date values
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function fromString($dateValue)
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
$dti = new DateTimeImmutable();
$baseYear = SharedDateHelper::getExcelCalendar();
$dateValue = trim($dateValue ?? '', '"');
// Strip any ordinals because they're allowed in Excel (English only)
$dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue);
// Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany)
$dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue);
$yearFound = false;
$t1 = explode(' ', $dateValue);
$t = '';
foreach ($t1 as &$t) {
if ((is_numeric($t)) && ($t > 31)) {
if ($yearFound) {
return ExcelError::VALUE();
}
if ($t < 100) {
$t += 1900;
}
$yearFound = true;
}
}
if (count($t1) === 1) {
// We've been fed a time value without any date
return ((strpos((string) $t, ':') === false)) ? ExcelError::Value() : 0.0;
}
unset($t);
$dateValue = self::t1ToString($t1, $dti, $yearFound);
$PHPDateArray = self::setUpArray($dateValue, $dti);
return self::finalResults($PHPDateArray, $dti, $baseYear);
}
private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string
{
if (count($t1) == 2) {
// We only have two parts of the date: either day/month or month/year
if ($yearFound) {
array_unshift($t1, 1);
} else {
if (is_numeric($t1[1]) && $t1[1] > 29) {
$t1[1] += 1900;
array_unshift($t1, 1);
} else {
$t1[] = $dti->format('Y');
}
}
}
$dateValue = implode(' ', $t1);
return $dateValue;
}
/**
* Parse date.
*/
private static function setUpArray(string $dateValue, DateTimeImmutable $dti): array
{
$PHPDateArray = Helpers::dateParse($dateValue);
if (!Helpers::dateParseSucceeded($PHPDateArray)) {
// If original count was 1, we've already returned.
// If it was 2, we added another.
// Therefore, neither of the first 2 stroks below can fail.
$testVal1 = strtok($dateValue, '- ');
$testVal2 = strtok('- ');
$testVal3 = strtok('- ') ?: $dti->format('Y');
Helpers::adjustYear((string) $testVal1, (string) $testVal2, $testVal3);
$PHPDateArray = Helpers::dateParse($testVal1 . '-' . $testVal2 . '-' . $testVal3);
if (!Helpers::dateParseSucceeded($PHPDateArray)) {
$PHPDateArray = Helpers::dateParse($testVal2 . '-' . $testVal1 . '-' . $testVal3);
}
}
return $PHPDateArray;
}
/**
* Final results.
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
private static function finalResults(array $PHPDateArray, DateTimeImmutable $dti, int $baseYear)
{
$retValue = ExcelError::Value();
if (Helpers::dateParseSucceeded($PHPDateArray)) {
// Execute function
Helpers::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y'));
if ($PHPDateArray['year'] < $baseYear) {
return ExcelError::VALUE();
}
Helpers::replaceIfEmpty($PHPDateArray['month'], $dti->format('m'));
Helpers::replaceIfEmpty($PHPDateArray['day'], $dti->format('d'));
$PHPDateArray['hour'] = 0;
$PHPDateArray['minute'] = 0;
$PHPDateArray['second'] = 0;
$month = (int) $PHPDateArray['month'];
$day = (int) $PHPDateArray['day'];
$year = (int) $PHPDateArray['year'];
if (!checkdate($month, $day, $year)) {
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE();
}
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true);
}
return $retValue;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTimeInterface;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Days
{
use ArrayEnabled;
/**
* DAYS.
*
* Returns the number of days between two dates
*
* Excel Function:
* DAYS(endDate, startDate)
*
* @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float),
* PHP date timestamp (integer), PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float),
* PHP date timestamp (integer), PHP DateTime object, or a standard date string
* Or can be an array of date values
*
* @return array|int|string Number of days between start date and end date or an error
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function between($endDate, $startDate)
{
if (is_array($endDate) || is_array($startDate)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $endDate, $startDate);
}
try {
$startDate = Helpers::getDateValue($startDate);
$endDate = Helpers::getDateValue($endDate);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate);
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate);
$days = ExcelError::VALUE();
$diff = $PHPStartDateObject->diff($PHPEndDateObject);
if ($diff !== false && !is_bool($diff->days)) {
$days = $diff->days;
if ($diff->invert) {
$days = -$days;
}
}
return $days;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Days360
{
use ArrayEnabled;
/**
* DAYS360.
*
* Returns the number of days between two dates based on a 360-day year (twelve 30-day months),
* which is used in some accounting calculations. Use this function to help compute payments if
* your accounting system is based on twelve 30-day months.
*
* Excel Function:
* DAYS360(startDate,endDate[,method])
*
* @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|mixed $method US or European Method as a bool
* FALSE or omitted: U.S. (NASD) method. If the starting date is
* the last day of a month, it becomes equal to the 30th of the
* same month. If the ending date is the last day of a month and
* the starting date is earlier than the 30th of a month, the
* ending date becomes equal to the 1st of the next month;
* otherwise the ending date becomes equal to the 30th of the
* same month.
* TRUE: European method. Starting dates and ending dates that
* occur on the 31st of a month become equal to the 30th of the
* same month.
* Or can be an array of methods
*
* @return array|int|string Number of days between start date and end date
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function between($startDate = 0, $endDate = 0, $method = false)
{
if (is_array($startDate) || is_array($endDate) || is_array($method)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method);
}
try {
$startDate = Helpers::getDateValue($startDate);
$endDate = Helpers::getDateValue($endDate);
} catch (Exception $e) {
return $e->getMessage();
}
if (!is_bool($method)) {
return ExcelError::VALUE();
}
// Execute function
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate);
$startDay = $PHPStartDateObject->format('j');
$startMonth = $PHPStartDateObject->format('n');
$startYear = $PHPStartDateObject->format('Y');
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate);
$endDay = $PHPEndDateObject->format('j');
$endMonth = $PHPEndDateObject->format('n');
$endYear = $PHPEndDateObject->format('Y');
return self::dateDiff360((int) $startDay, (int) $startMonth, (int) $startYear, (int) $endDay, (int) $endMonth, (int) $endYear, !$method);
}
/**
* Return the number of days between two dates based on a 360 day calendar.
*/
private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int
{
$startDay = self::getStartDay($startDay, $startMonth, $startYear, $methodUS);
$endDay = self::getEndDay($endDay, $endMonth, $endYear, $startDay, $methodUS);
return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360;
}
private static function getStartDay(int $startDay, int $startMonth, int $startYear, bool $methodUS): int
{
if ($startDay == 31) {
--$startDay;
} elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !Helpers::isLeapYear($startYear))))) {
$startDay = 30;
}
return $startDay;
}
private static function getEndDay(int $endDay, int &$endMonth, int &$endYear, int $startDay, bool $methodUS): int
{
if ($endDay == 31) {
if ($methodUS && $startDay != 30) {
$endDay = 1;
if ($endMonth == 12) {
++$endYear;
$endMonth = 1;
} else {
++$endMonth;
}
} else {
$endDay = 30;
}
}
return $endDay;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateInterval;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Difference
{
use ArrayEnabled;
/**
* DATEDIF.
*
* @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object
* or a standard date string
* Or can be an array of date values
* @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object
* or a standard date string
* Or can be an array of date values
* @param array|string $unit
* Or can be an array of unit values
*
* @return array|int|string Interval between the dates
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function interval($startDate, $endDate, $unit = 'D')
{
if (is_array($startDate) || is_array($endDate) || is_array($unit)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $unit);
}
try {
$startDate = Helpers::getDateValue($startDate);
$endDate = Helpers::getDateValue($endDate);
$difference = self::initialDiff($startDate, $endDate);
$unit = strtoupper($unit);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate);
$startDays = (int) $PHPStartDateObject->format('j');
//$startMonths = (int) $PHPStartDateObject->format('n');
$startYears = (int) $PHPStartDateObject->format('Y');
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate);
$endDays = (int) $PHPEndDateObject->format('j');
//$endMonths = (int) $PHPEndDateObject->format('n');
$endYears = (int) $PHPEndDateObject->format('Y');
$PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject);
$retVal = false;
$retVal = self::replaceRetValue($retVal, $unit, 'D') ?? self::datedifD($difference);
$retVal = self::replaceRetValue($retVal, $unit, 'M') ?? self::datedifM($PHPDiffDateObject);
$retVal = self::replaceRetValue($retVal, $unit, 'MD') ?? self::datedifMD($startDays, $endDays, $PHPEndDateObject, $PHPDiffDateObject);
$retVal = self::replaceRetValue($retVal, $unit, 'Y') ?? self::datedifY($PHPDiffDateObject);
$retVal = self::replaceRetValue($retVal, $unit, 'YD') ?? self::datedifYD($difference, $startYears, $endYears, $PHPStartDateObject, $PHPEndDateObject);
$retVal = self::replaceRetValue($retVal, $unit, 'YM') ?? self::datedifYM($PHPDiffDateObject);
return is_bool($retVal) ? ExcelError::VALUE() : $retVal;
}
private static function initialDiff(float $startDate, float $endDate): float
{
// Validate parameters
if ($startDate > $endDate) {
throw new Exception(ExcelError::NAN());
}
return $endDate - $startDate;
}
/**
* Decide whether it's time to set retVal.
*
* @param bool|int $retVal
*
* @return null|bool|int
*/
private static function replaceRetValue($retVal, string $unit, string $compare)
{
if ($retVal !== false || $unit !== $compare) {
return $retVal;
}
return null;
}
private static function datedifD(float $difference): int
{
return (int) $difference;
}
private static function datedifM(DateInterval $PHPDiffDateObject): int
{
return 12 * (int) $PHPDiffDateObject->format('%y') + (int) $PHPDiffDateObject->format('%m');
}
private static function datedifMD(int $startDays, int $endDays, DateTime $PHPEndDateObject, DateInterval $PHPDiffDateObject): int
{
if ($endDays < $startDays) {
$retVal = $endDays;
$PHPEndDateObject->modify('-' . $endDays . ' days');
$adjustDays = (int) $PHPEndDateObject->format('j');
$retVal += ($adjustDays - $startDays);
} else {
$retVal = (int) $PHPDiffDateObject->format('%d');
}
return $retVal;
}
private static function datedifY(DateInterval $PHPDiffDateObject): int
{
return (int) $PHPDiffDateObject->format('%y');
}
private static function datedifYD(float $difference, int $startYears, int $endYears, DateTime $PHPStartDateObject, DateTime $PHPEndDateObject): int
{
$retVal = (int) $difference;
if ($endYears > $startYears) {
$isLeapStartYear = $PHPStartDateObject->format('L');
$wasLeapEndYear = $PHPEndDateObject->format('L');
// Adjust end year to be as close as possible as start year
while ($PHPEndDateObject >= $PHPStartDateObject) {
$PHPEndDateObject->modify('-1 year');
//$endYears = $PHPEndDateObject->format('Y');
}
$PHPEndDateObject->modify('+1 year');
// Get the result
$retVal = (int) $PHPEndDateObject->diff($PHPStartDateObject)->days;
// Adjust for leap years cases
$isLeapEndYear = $PHPEndDateObject->format('L');
$limit = new DateTime($PHPEndDateObject->format('Y-02-29'));
if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) {
--$retVal;
}
}
return (int) $retVal;
}
private static function datedifYM(DateInterval $PHPDiffDateObject): int
{
return (int) $PHPDiffDateObject->format('%m');
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Helpers
{
/**
* Identify if a year is a leap year or not.
*
* @param int|string $year The year to test
*
* @return bool TRUE if the year is a leap year, otherwise FALSE
*/
public static function isLeapYear($year): bool
{
return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0);
}
/**
* getDateValue.
*
* @param mixed $dateValue
*
* @return float Excel date/time serial value
*/
public static function getDateValue($dateValue, bool $allowBool = true): float
{
if (is_object($dateValue)) {
$retval = SharedDateHelper::PHPToExcel($dateValue);
if (is_bool($retval)) {
throw new Exception(ExcelError::VALUE());
}
return $retval;
}
self::nullFalseTrueToNumber($dateValue, $allowBool);
if (!is_numeric($dateValue)) {
$saveReturnDateType = Functions::getReturnDateType();
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
$dateValue = DateValue::fromString($dateValue);
Functions::setReturnDateType($saveReturnDateType);
if (!is_numeric($dateValue)) {
throw new Exception(ExcelError::VALUE());
}
}
if ($dateValue < 0 && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) {
throw new Exception(ExcelError::NAN());
}
return (float) $dateValue;
}
/**
* getTimeValue.
*
* @param string $timeValue
*
* @return mixed Excel date/time serial value, or string if error
*/
public static function getTimeValue($timeValue)
{
$saveReturnDateType = Functions::getReturnDateType();
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
$timeValue = TimeValue::fromString($timeValue);
Functions::setReturnDateType($saveReturnDateType);
return $timeValue;
}
/**
* Adjust date by given months.
*
* @param mixed $dateValue
*/
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime
{
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
$oMonth = (int) $PHPDateObject->format('m');
$oYear = (int) $PHPDateObject->format('Y');
$adjustmentMonthsString = (string) $adjustmentMonths;
if ($adjustmentMonths > 0) {
$adjustmentMonthsString = '+' . $adjustmentMonths;
}
if ($adjustmentMonths != 0) {
$PHPDateObject->modify($adjustmentMonthsString . ' months');
}
$nMonth = (int) $PHPDateObject->format('m');
$nYear = (int) $PHPDateObject->format('Y');
$monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12);
if ($monthDiff != $adjustmentMonths) {
$adjustDays = (int) $PHPDateObject->format('d');
$adjustDaysString = '-' . $adjustDays . ' days';
$PHPDateObject->modify($adjustDaysString);
}
return $PHPDateObject;
}
/**
* Help reduce perceived complexity of some tests.
*
* @param mixed $value
* @param mixed $altValue
*/
public static function replaceIfEmpty(&$value, $altValue): void
{
$value = $value ?: $altValue;
}
/**
* Adjust year in ambiguous situations.
*/
public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void
{
if (!is_numeric($testVal1) || $testVal1 < 31) {
if (!is_numeric($testVal2) || $testVal2 < 12) {
if (is_numeric($testVal3) && $testVal3 < 12) {
$testVal3 += 2000;
}
}
}
}
/**
* Return result in one of three formats.
*
* @return mixed
*/
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false)
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
return new DateTime(
$dateArray['year']
. '-' . $dateArray['month']
. '-' . $dateArray['day']
. ' ' . $dateArray['hour']
. ':' . $dateArray['minute']
. ':' . $dateArray['second']
);
}
$excelDateValue =
SharedDateHelper::formattedPHPToExcel(
$dateArray['year'],
$dateArray['month'],
$dateArray['day'],
$dateArray['hour'],
$dateArray['minute'],
$dateArray['second']
);
if ($retType === Functions::RETURNDATE_EXCEL) {
return $noFrac ? floor($excelDateValue) : (float) $excelDateValue;
}
// RETURNDATE_UNIX_TIMESTAMP)
return (int) SharedDateHelper::excelToTimestamp($excelDateValue);
}
/**
* Return result in one of three formats.
*
* @return mixed
*/
public static function returnIn3FormatsFloat(float $excelDateValue)
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_EXCEL) {
return $excelDateValue;
}
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
return (int) SharedDateHelper::excelToTimestamp($excelDateValue);
}
// RETURNDATE_PHP_DATETIME_OBJECT
return SharedDateHelper::excelToDateTimeObject($excelDateValue);
}
/**
* Return result in one of three formats.
*
* @return mixed
*/
public static function returnIn3FormatsObject(DateTime $PHPDateObject)
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
return $PHPDateObject;
}
if ($retType === Functions::RETURNDATE_EXCEL) {
return (float) SharedDateHelper::PHPToExcel($PHPDateObject);
}
// RETURNDATE_UNIX_TIMESTAMP
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject);
$stamp = is_bool($stamp) ? ((int) $stamp) : $stamp;
return (int) SharedDateHelper::excelToTimestamp($stamp);
}
private static function baseDate(): int
{
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
return 0;
}
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) {
return 0;
}
return 1;
}
/**
* Many functions accept null/false/true argument treated as 0/0/1.
*
* @param mixed $number
*/
public static function nullFalseTrueToNumber(&$number, bool $allowBool = true): void
{
$number = Functions::flattenSingleValue($number);
$nullVal = self::baseDate();
if ($number === null) {
$number = $nullVal;
} elseif ($allowBool && is_bool($number)) {
$number = $nullVal + (int) $number;
}
}
/**
* Many functions accept null argument treated as 0.
*
* @param mixed $number
*
* @return float|int
*/
public static function validateNumericNull($number)
{
$number = Functions::flattenSingleValue($number);
if ($number === null) {
return 0;
}
if (is_int($number)) {
return $number;
}
if (is_numeric($number)) {
return (float) $number;
}
throw new Exception(ExcelError::VALUE());
}
/**
* Many functions accept null/false/true argument treated as 0/0/1.
*
* @param mixed $number
*
* @return float
*/
public static function validateNotNegative($number)
{
if (!is_numeric($number)) {
throw new Exception(ExcelError::VALUE());
}
if ($number >= 0) {
return (float) $number;
}
throw new Exception(ExcelError::NAN());
}
public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void
{
$isoDate = $PHPDateObject->format('c');
if ($isoDate < '1900-03-01') {
$PHPDateObject->modify($mod);
}
}
public static function dateParse(string $string): array
{
return self::forceArray(date_parse($string));
}
public static function dateParseSucceeded(array $dateArray): bool
{
return $dateArray['error_count'] === 0;
}
/**
* Despite documentation, date_parse probably never returns false.
* Just in case, this routine helps guarantee it.
*
* @param array|false $dateArray
*/
private static function forceArray($dateArray): array
{
return is_array($dateArray) ? $dateArray : ['error_count' => 1];
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class Month
{
use ArrayEnabled;
/**
* EDATE.
*
* Returns the serial number that represents the date that is the indicated number of months
* before or after a specified date (the start_date).
* Use EDATE to calculate maturity dates or due dates that fall on the same day of the month
* as the date of issue.
*
* Excel Function:
* EDATE(dateValue,adjustmentMonths)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|int $adjustmentMonths The number of months before or after start_date.
* A positive value for months yields a future date;
* a negative value yields a past date.
* Or can be an array of adjustment values
*
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of values is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function adjust($dateValue, $adjustmentMonths)
{
if (is_array($dateValue) || is_array($adjustmentMonths)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths);
}
try {
$dateValue = Helpers::getDateValue($dateValue, false);
$adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths);
} catch (Exception $e) {
return $e->getMessage();
}
$adjustmentMonths = floor($adjustmentMonths);
// Execute function
$PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths);
return Helpers::returnIn3FormatsObject($PHPDateObject);
}
/**
* EOMONTH.
*
* Returns the date value for the last day of the month that is the indicated number of months
* before or after start_date.
* Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month.
*
* Excel Function:
* EOMONTH(dateValue,adjustmentMonths)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|int $adjustmentMonths The number of months before or after start_date.
* A positive value for months yields a future date;
* a negative value yields a past date.
* Or can be an array of adjustment values
*
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of values is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function lastDay($dateValue, $adjustmentMonths)
{
if (is_array($dateValue) || is_array($adjustmentMonths)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths);
}
try {
$dateValue = Helpers::getDateValue($dateValue, false);
$adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths);
} catch (Exception $e) {
return $e->getMessage();
}
$adjustmentMonths = floor($adjustmentMonths);
// Execute function
$PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths + 1);
$adjustDays = (int) $PHPDateObject->format('d');
$adjustDaysString = '-' . $adjustDays . ' days';
$PHPDateObject->modify($adjustDaysString);
return Helpers::returnIn3FormatsObject($PHPDateObject);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class NetworkDays
{
use ArrayEnabled;
/**
* NETWORKDAYS.
*
* Returns the number of whole working days between start_date and end_date. Working days
* exclude weekends and any dates identified in holidays.
* Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days
* worked during a specific term.
*
* Excel Function:
* NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]])
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation
*
* @return array|int|string Interval between the dates
* If an array of values is passed for the $startDate or $endDate arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function count($startDate, $endDate, ...$dateArgs)
{
if (is_array($startDate) || is_array($endDate)) {
return self::evaluateArrayArgumentsSubset(
[self::class, __FUNCTION__],
2,
$startDate,
$endDate,
...$dateArgs
);
}
try {
// Retrieve the mandatory start and end date that are referenced in the function definition
$sDate = Helpers::getDateValue($startDate);
$eDate = Helpers::getDateValue($endDate);
$startDate = min($sDate, $eDate);
$endDate = max($sDate, $eDate);
// Get the optional days
$dateArgs = Functions::flattenArray($dateArgs);
// Test any extra holiday parameters
$holidayArray = [];
foreach ($dateArgs as $holidayDate) {
$holidayArray[] = Helpers::getDateValue($holidayDate);
}
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$startDow = self::calcStartDow($startDate);
$endDow = self::calcEndDow($endDate);
$wholeWeekDays = (int) floor(($endDate - $startDate) / 7) * 5;
$partWeekDays = self::calcPartWeekDays($startDow, $endDow);
// Test any extra holiday parameters
$holidayCountedArray = [];
foreach ($holidayArray as $holidayDate) {
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) {
if ((Week::day($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) {
--$partWeekDays;
$holidayCountedArray[] = $holidayDate;
}
}
}
return self::applySign($wholeWeekDays + $partWeekDays, $sDate, $eDate);
}
private static function calcStartDow(float $startDate): int
{
$startDow = 6 - (int) Week::day($startDate, 2);
if ($startDow < 0) {
$startDow = 5;
}
return $startDow;
}
private static function calcEndDow(float $endDate): int
{
$endDow = (int) Week::day($endDate, 2);
if ($endDow >= 6) {
$endDow = 0;
}
return $endDow;
}
private static function calcPartWeekDays(int $startDow, int $endDow): int
{
$partWeekDays = $endDow + $startDow;
if ($partWeekDays > 5) {
$partWeekDays -= 5;
}
return $partWeekDays;
}
private static function applySign(int $result, float $sDate, float $eDate): int
{
return ($sDate > $eDate) ? -$result : $result;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Time
{
use ArrayEnabled;
/**
* TIME.
*
* The TIME function returns a value that represents a particular time.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TIME(hour,minute,second)
*
* @param array|int $hour A number from 0 (zero) to 32767 representing the hour.
* Any value greater than 23 will be divided by 24 and the remainder
* will be treated as the hour value. For example, TIME(27,0,0) =
* TIME(3,0,0) = .125 or 3:00 AM.
* @param array|int $minute A number from 0 to 32767 representing the minute.
* Any value greater than 59 will be converted to hours and minutes.
* For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM.
* @param array|int $second A number from 0 to 32767 representing the second.
* Any value greater than 59 will be converted to hours, minutes,
* and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148
* or 12:33:20 AM
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function fromHMS($hour, $minute, $second)
{
if (is_array($hour) || is_array($minute) || is_array($second)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $hour, $minute, $second);
}
try {
$hour = self::toIntWithNullBool($hour);
$minute = self::toIntWithNullBool($minute);
$second = self::toIntWithNullBool($second);
} catch (Exception $e) {
return $e->getMessage();
}
self::adjustSecond($second, $minute);
self::adjustMinute($minute, $hour);
if ($hour > 23) {
$hour = $hour % 24;
} elseif ($hour < 0) {
return ExcelError::NAN();
}
// Execute function
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_EXCEL) {
$calendar = SharedDateHelper::getExcelCalendar();
$date = (int) ($calendar !== SharedDateHelper::CALENDAR_WINDOWS_1900);
return (float) SharedDateHelper::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second);
}
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
return (int) SharedDateHelper::excelToTimestamp(SharedDateHelper::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600
}
// RETURNDATE_PHP_DATETIME_OBJECT
// Hour has already been normalized (0-23) above
$phpDateObject = new DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second);
return $phpDateObject;
}
private static function adjustSecond(int &$second, int &$minute): void
{
if ($second < 0) {
$minute += floor($second / 60);
$second = 60 - abs($second % 60);
if ($second == 60) {
$second = 0;
}
} elseif ($second >= 60) {
$minute += floor($second / 60);
$second = $second % 60;
}
}
private static function adjustMinute(int &$minute, int &$hour): void
{
if ($minute < 0) {
$hour += floor($minute / 60);
$minute = 60 - abs($minute % 60);
if ($minute == 60) {
$minute = 0;
}
} elseif ($minute >= 60) {
$hour += floor($minute / 60);
$minute = $minute % 60;
}
}
/**
* @param mixed $value expect int
*/
private static function toIntWithNullBool($value): int
{
$value = $value ?? 0;
if (is_bool($value)) {
$value = (int) $value;
}
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (int) $value;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class TimeParts
{
use ArrayEnabled;
/**
* HOUROFDAY.
*
* Returns the hour of a time value.
* The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.).
*
* Excel Function:
* HOUR(timeValue)
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
* Or can be an array of date/time values
*
* @return array|int|string Hour
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function hour($timeValue)
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
try {
Helpers::nullFalseTrueToNumber($timeValue);
if (!is_numeric($timeValue)) {
$timeValue = Helpers::getTimeValue($timeValue);
}
Helpers::validateNotNegative($timeValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
return (int) $timeValue->format('H');
}
/**
* MINUTE.
*
* Returns the minutes of a time value.
* The minute is given as an integer, ranging from 0 to 59.
*
* Excel Function:
* MINUTE(timeValue)
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
* Or can be an array of date/time values
*
* @return array|int|string Minute
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function minute($timeValue)
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
try {
Helpers::nullFalseTrueToNumber($timeValue);
if (!is_numeric($timeValue)) {
$timeValue = Helpers::getTimeValue($timeValue);
}
Helpers::validateNotNegative($timeValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
return (int) $timeValue->format('i');
}
/**
* SECOND.
*
* Returns the seconds of a time value.
* The minute is given as an integer, ranging from 0 to 59.
*
* Excel Function:
* SECOND(timeValue)
*
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard time string
* Or can be an array of date/time values
*
* @return array|int|string Second
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function second($timeValue)
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
try {
Helpers::nullFalseTrueToNumber($timeValue);
if (!is_numeric($timeValue)) {
$timeValue = Helpers::getTimeValue($timeValue);
}
Helpers::validateNotNegative($timeValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$timeValue = fmod($timeValue, 1);
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
return (int) $timeValue->format('s');
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use Datetime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class TimeValue
{
use ArrayEnabled;
/**
* TIMEVALUE.
*
* Returns a value that represents a particular time.
* Use TIMEVALUE to convert a time represented by a text string to an Excel or PHP date/time stamp
* value.
*
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way.
*
* Excel Function:
* TIMEVALUE(timeValue)
*
* @param array|string $timeValue A text string that represents a time in any one of the Microsoft
* Excel time formats; for example, "6:45 PM" and "18:45" text strings
* within quotation marks that represent time.
* Date information in time_text is ignored.
* Or can be an array of date/time values
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function fromString($timeValue)
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
$timeValue = trim($timeValue ?? '', '"');
$timeValue = str_replace(['/', '.'], '-', $timeValue);
$arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: [];
if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) {
$arraySplit[0] = ($arraySplit[0] % 24);
$timeValue = implode(':', $arraySplit);
}
$PHPDateArray = Helpers::dateParse($timeValue);
$retValue = ExcelError::VALUE();
if (Helpers::dateParseSucceeded($PHPDateArray)) {
/** @var int */
$hour = $PHPDateArray['hour'];
/** @var int */
$minute = $PHPDateArray['minute'];
/** @var int */
$second = $PHPDateArray['second'];
// OpenOffice-specific code removed - it works just like Excel
$excelDateValue = SharedDateHelper::formattedPHPToExcel(1900, 1, 1, $hour, $minute, $second) - 1;
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_EXCEL) {
$retValue = (float) $excelDateValue;
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
$retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600;
} else {
$retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
}
}
return $retValue;
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class Week
{
use ArrayEnabled;
/**
* WEEKNUM.
*
* Returns the week of the year for a specified date.
* The WEEKNUM function considers the week containing January 1 to be the first week of the year.
* However, there is a European standard that defines the first week as the one with the majority
* of days (four or more) falling in the new year. This means that for years in which there are
* three days or less in the first week of January, the WEEKNUM function returns week numbers
* that are incorrect according to the European standard.
*
* Excel Function:
* WEEKNUM(dateValue[,style])
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|int $method Week begins on Sunday or Monday
* 1 or omitted Week begins on Sunday.
* 2 Week begins on Monday.
* 11 Week begins on Monday.
* 12 Week begins on Tuesday.
* 13 Week begins on Wednesday.
* 14 Week begins on Thursday.
* 15 Week begins on Friday.
* 16 Week begins on Saturday.
* 17 Week begins on Sunday.
* 21 ISO (Jan. 4 is week 1, begins on Monday).
* Or can be an array of methods
*
* @return array|int|string Week Number
* If an array of values is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function number($dateValue, $method = Constants::STARTWEEK_SUNDAY)
{
if (is_array($dateValue) || is_array($method)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $method);
}
$origDateValueNull = empty($dateValue);
try {
$method = self::validateMethod($method);
if ($dateValue === null) { // boolean not allowed
$dateValue = (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 || $method === Constants::DOW_SUNDAY) ? 0 : 1;
}
$dateValue = self::validateDateValue($dateValue);
if (!$dateValue && self::buggyWeekNum1900($method)) {
// This seems to be an additional Excel bug.
return 0;
}
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
if ($method == Constants::STARTWEEK_MONDAY_ISO) {
Helpers::silly1900($PHPDateObject);
return (int) $PHPDateObject->format('W');
}
if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) {
return 0;
}
Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches
$dayOfYear = (int) $PHPDateObject->format('z');
$PHPDateObject->modify('-' . $dayOfYear . ' days');
$firstDayOfFirstWeek = (int) $PHPDateObject->format('w');
$daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7;
$daysInFirstWeek += 7 * !$daysInFirstWeek;
$endFirstWeek = $daysInFirstWeek - 1;
$weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7);
return (int) $weekOfYear;
}
/**
* ISOWEEKNUM.
*
* Returns the ISO 8601 week number of the year for a specified date.
*
* Excel Function:
* ISOWEEKNUM(dateValue)
*
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
*
* @return array|int|string Week Number
* If an array of numbers is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isoWeekNumber($dateValue)
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
if (self::apparentBug($dateValue)) {
return 52;
}
try {
$dateValue = Helpers::getDateValue($dateValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
Helpers::silly1900($PHPDateObject);
return (int) $PHPDateObject->format('W');
}
/**
* WEEKDAY.
*
* Returns the day of the week for a specified date. The day is given as an integer
* ranging from 0 to 7 (dependent on the requested style).
*
* Excel Function:
* WEEKDAY(dateValue[,style])
*
* @param null|array|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param mixed $style A number that determines the type of return value
* 1 or omitted Numbers 1 (Sunday) through 7 (Saturday).
* 2 Numbers 1 (Monday) through 7 (Sunday).
* 3 Numbers 0 (Monday) through 6 (Sunday).
* Or can be an array of styles
*
* @return array|int|string Day of the week value
* If an array of values is passed as the argument, then the returned result will also be an array
* with the same dimensions
*/
public static function day($dateValue, $style = 1)
{
if (is_array($dateValue) || is_array($style)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $style);
}
try {
$dateValue = Helpers::getDateValue($dateValue);
$style = self::validateStyle($style);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
Helpers::silly1900($PHPDateObject);
$DoW = (int) $PHPDateObject->format('w');
switch ($style) {
case 1:
++$DoW;
break;
case 2:
$DoW = self::dow0Becomes7($DoW);
break;
case 3:
$DoW = self::dow0Becomes7($DoW) - 1;
break;
}
return $DoW;
}
/**
* @param mixed $style expect int
*/
private static function validateStyle($style): int
{
if (!is_numeric($style)) {
throw new Exception(ExcelError::VALUE());
}
$style = (int) $style;
if (($style < 1) || ($style > 3)) {
throw new Exception(ExcelError::NAN());
}
return $style;
}
private static function dow0Becomes7(int $DoW): int
{
return ($DoW === 0) ? 7 : $DoW;
}
/**
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
*/
private static function apparentBug($dateValue): bool
{
if (SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) {
if (is_bool($dateValue)) {
return true;
}
if (is_numeric($dateValue) && !((int) $dateValue)) {
return true;
}
}
return false;
}
/**
* Validate dateValue parameter.
*
* @param mixed $dateValue
*/
private static function validateDateValue($dateValue): float
{
if (is_bool($dateValue)) {
throw new Exception(ExcelError::VALUE());
}
return Helpers::getDateValue($dateValue);
}
/**
* Validate method parameter.
*
* @param mixed $method
*/
private static function validateMethod($method): int
{
if ($method === null) {
$method = Constants::STARTWEEK_SUNDAY;
}
if (!is_numeric($method)) {
throw new Exception(ExcelError::VALUE());
}
$method = (int) $method;
if (!array_key_exists($method, Constants::METHODARR)) {
throw new Exception(ExcelError::NAN());
}
$method = Constants::METHODARR[$method];
return $method;
}
private static function buggyWeekNum1900(int $method): bool
{
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900;
}
private static function buggyWeekNum1904(int $method, bool $origNull, DateTime $dateObject): bool
{
// This appears to be another Excel bug.
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 &&
!$origNull && $dateObject->format('Y-m-d') === '1904-01-01';
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class WorkDay
{
use ArrayEnabled;
/**
* WORKDAY.
*
* Returns the date that is the indicated number of working days before or after a date (the
* starting date). Working days exclude weekends and any dates identified as holidays.
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected
* delivery times, or the number of days of work performed.
*
* Excel Function:
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]])
*
* @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of date values
* @param array|int $endDays The number of nonweekend and nonholiday days before or after
* startDate. A positive value for days yields a future date; a
* negative value yields a past date.
* Or can be an array of int values
* @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation
*
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function date($startDate, $endDays, ...$dateArgs)
{
if (is_array($startDate) || is_array($endDays)) {
return self::evaluateArrayArgumentsSubset(
[self::class, __FUNCTION__],
2,
$startDate,
$endDays,
...$dateArgs
);
}
// Retrieve the mandatory start date and days that are referenced in the function definition
try {
$startDate = Helpers::getDateValue($startDate);
$endDays = Helpers::validateNumericNull($endDays);
$holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs));
} catch (Exception $e) {
return $e->getMessage();
}
$startDate = (float) floor($startDate);
$endDays = (int) floor($endDays);
// If endDays is 0, we always return startDate
if ($endDays == 0) {
return $startDate;
}
if ($endDays < 0) {
return self::decrementing($startDate, $endDays, $holidayArray);
}
return self::incrementing($startDate, $endDays, $holidayArray);
}
/**
* Use incrementing logic to determine Workday.
*
* @return mixed
*/
private static function incrementing(float $startDate, int $endDays, array $holidayArray)
{
// Adjust the start date if it falls over a weekend
$startDoW = self::getWeekDay($startDate, 3);
if ($startDoW >= 5) {
$startDate += 7 - $startDoW;
--$endDays;
}
// Add endDays
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7);
$endDays = $endDays % 5;
while ($endDays > 0) {
++$endDate;
// Adjust the calculated end date if it falls over a weekend
$endDow = self::getWeekDay($endDate, 3);
if ($endDow >= 5) {
$endDate += 7 - $endDow;
}
--$endDays;
}
// Test any extra holiday parameters
if (!empty($holidayArray)) {
$endDate = self::incrementingArray($startDate, $endDate, $holidayArray);
}
return Helpers::returnIn3FormatsFloat($endDate);
}
private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float
{
$holidayCountedArray = $holidayDates = [];
foreach ($holidayArray as $holidayDate) {
if (self::getWeekDay($holidayDate, 3) < 5) {
$holidayDates[] = $holidayDate;
}
}
sort($holidayDates, SORT_NUMERIC);
foreach ($holidayDates as $holidayDate) {
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) {
if (!in_array($holidayDate, $holidayCountedArray)) {
++$endDate;
$holidayCountedArray[] = $holidayDate;
}
}
// Adjust the calculated end date if it falls over a weekend
$endDoW = self::getWeekDay($endDate, 3);
if ($endDoW >= 5) {
$endDate += 7 - $endDoW;
}
}
return $endDate;
}
/**
* Use decrementing logic to determine Workday.
*
* @return mixed
*/
private static function decrementing(float $startDate, int $endDays, array $holidayArray)
{
// Adjust the start date if it falls over a weekend
$startDoW = self::getWeekDay($startDate, 3);
if ($startDoW >= 5) {
$startDate += -$startDoW + 4;
++$endDays;
}
// Add endDays
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7);
$endDays = $endDays % 5;
while ($endDays < 0) {
--$endDate;
// Adjust the calculated end date if it falls over a weekend
$endDow = self::getWeekDay($endDate, 3);
if ($endDow >= 5) {
$endDate += 4 - $endDow;
}
++$endDays;
}
// Test any extra holiday parameters
if (!empty($holidayArray)) {
$endDate = self::decrementingArray($startDate, $endDate, $holidayArray);
}
return Helpers::returnIn3FormatsFloat($endDate);
}
private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float
{
$holidayCountedArray = $holidayDates = [];
foreach ($holidayArray as $holidayDate) {
if (self::getWeekDay($holidayDate, 3) < 5) {
$holidayDates[] = $holidayDate;
}
}
rsort($holidayDates, SORT_NUMERIC);
foreach ($holidayDates as $holidayDate) {
if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) {
if (!in_array($holidayDate, $holidayCountedArray)) {
--$endDate;
$holidayCountedArray[] = $holidayDate;
}
}
// Adjust the calculated end date if it falls over a weekend
$endDoW = self::getWeekDay($endDate, 3);
/** int $endDoW */
if ($endDoW >= 5) {
$endDate += -$endDoW + 4;
}
}
return $endDate;
}
private static function getWeekDay(float $date, int $wd): int
{
$result = Functions::scalar(Week::day($date, $wd));
return is_int($result) ? $result : -1;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
class YearFrac
{
use ArrayEnabled;
/**
* YEARFRAC.
*
* Calculates the fraction of the year represented by the number of whole days between two dates
* (the start_date and the end_date).
* Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or
* obligations to assign to a specific term.
*
* Excel Function:
* YEARFRAC(startDate,endDate[,method])
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html
* for description of algorithm used in Excel
*
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of values
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer),
* PHP DateTime object, or a standard date string
* Or can be an array of methods
* @param array|int $method Method used for the calculation
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
* Or can be an array of methods
*
* @return array|float|string fraction of the year, or a string containing an error
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function fraction($startDate, $endDate, $method = 0)
{
if (is_array($startDate) || is_array($endDate) || is_array($method)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method);
}
try {
$method = (int) Helpers::validateNumericNull($method);
$sDate = Helpers::getDateValue($startDate);
$eDate = Helpers::getDateValue($endDate);
$sDate = self::excelBug($sDate, $startDate, $endDate, $method);
$eDate = self::excelBug($eDate, $endDate, $startDate, $method);
$startDate = min($sDate, $eDate);
$endDate = max($sDate, $eDate);
} catch (Exception $e) {
return $e->getMessage();
}
switch ($method) {
case 0:
return Functions::scalar(Days360::between($startDate, $endDate)) / 360;
case 1:
return self::method1($startDate, $endDate);
case 2:
return Functions::scalar(Difference::interval($startDate, $endDate)) / 360;
case 3:
return Functions::scalar(Difference::interval($startDate, $endDate)) / 365;
case 4:
return Functions::scalar(Days360::between($startDate, $endDate, true)) / 360;
}
return ExcelError::NAN();
}
/**
* Excel 1900 calendar treats date argument of null as 1900-01-00. Really.
*
* @param mixed $startDate
* @param mixed $endDate
*/
private static function excelBug(float $sDate, $startDate, $endDate, int $method): float
{
if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE && SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) {
if ($endDate === null && $startDate !== null) {
if (DateParts::month($sDate) == 12 && DateParts::day($sDate) === 31 && $method === 0) {
$sDate += 2;
} else {
++$sDate;
}
}
}
return $sDate;
}
private static function method1(float $startDate, float $endDate): float
{
$days = Functions::scalar(Difference::interval($startDate, $endDate));
$startYear = (int) DateParts::year($startDate);
$endYear = (int) DateParts::year($endDate);
$years = $endYear - $startYear + 1;
$startMonth = (int) DateParts::month($startDate);
$startDay = (int) DateParts::day($startDate);
$endMonth = (int) DateParts::month($endDate);
$endDay = (int) DateParts::day($endDate);
$startMonthDay = 100 * $startMonth + $startDay;
$endMonthDay = 100 * $endMonth + $endDay;
if ($years == 1) {
$tmpCalcAnnualBasis = 365 + (int) Helpers::isLeapYear($endYear);
} elseif ($years == 2 && $startMonthDay >= $endMonthDay) {
if (Helpers::isLeapYear($startYear)) {
$tmpCalcAnnualBasis = 365 + (int) ($startMonthDay <= 229);
} elseif (Helpers::isLeapYear($endYear)) {
$tmpCalcAnnualBasis = 365 + (int) ($endMonthDay >= 229);
} else {
$tmpCalcAnnualBasis = 365;
}
} else {
$tmpCalcAnnualBasis = 0;
for ($year = $startYear; $year <= $endYear; ++$year) {
$tmpCalcAnnualBasis += 365 + (int) Helpers::isLeapYear($year);
}
$tmpCalcAnnualBasis /= $years;
}
return $days / $tmpCalcAnnualBasis;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class ArrayArgumentHelper
{
/**
* @var int
*/
protected $indexStart = 0;
/**
* @var array
*/
protected $arguments;
/**
* @var int
*/
protected $argumentCount;
/**
* @var array
*/
protected $rows;
/**
* @var array
*/
protected $columns;
public function initialise(array $arguments): void
{
$keys = array_keys($arguments);
$this->indexStart = (int) array_shift($keys);
$this->rows = $this->rows($arguments);
$this->columns = $this->columns($arguments);
$this->argumentCount = count($arguments);
$this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns);
$this->rows = $this->rows($arguments);
$this->columns = $this->columns($arguments);
if ($this->arrayArguments() > 2) {
throw new Exception('Formulae with more than two array arguments are not supported');
}
}
public function arguments(): array
{
return $this->arguments;
}
public function hasArrayArgument(): bool
{
return $this->arrayArguments() > 0;
}
public function getFirstArrayArgumentNumber(): int
{
$rowArrays = $this->filterArray($this->rows);
$columnArrays = $this->filterArray($this->columns);
for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) {
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
return ++$index;
}
}
return 0;
}
public function getSingleRowVector(): ?int
{
$rowVectors = $this->getRowVectors();
return count($rowVectors) === 1 ? array_pop($rowVectors) : null;
}
private function getRowVectors(): array
{
$rowVectors = [];
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
if ($this->rows[$index] === 1 && $this->columns[$index] > 1) {
$rowVectors[] = $index;
}
}
return $rowVectors;
}
public function getSingleColumnVector(): ?int
{
$columnVectors = $this->getColumnVectors();
return count($columnVectors) === 1 ? array_pop($columnVectors) : null;
}
private function getColumnVectors(): array
{
$columnVectors = [];
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
if ($this->rows[$index] > 1 && $this->columns[$index] === 1) {
$columnVectors[] = $index;
}
}
return $columnVectors;
}
public function getMatrixPair(): array
{
for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) {
for ($j = $i + 1; $j < $this->argumentCount; ++$j) {
if (isset($this->rows[$i], $this->rows[$j])) {
return [$i, $j];
}
}
}
return [];
}
public function isVector(int $argument): bool
{
return $this->rows[$argument] === 1 || $this->columns[$argument] === 1;
}
public function isRowVector(int $argument): bool
{
return $this->rows[$argument] === 1;
}
public function isColumnVector(int $argument): bool
{
return $this->columns[$argument] === 1;
}
public function rowCount(int $argument): int
{
return $this->rows[$argument];
}
public function columnCount(int $argument): int
{
return $this->columns[$argument];
}
private function rows(array $arguments): array
{
return array_map(
function ($argument) {
return is_countable($argument) ? count($argument) : 1;
},
$arguments
);
}
private function columns(array $arguments): array
{
return array_map(
function ($argument) {
return is_array($argument) && is_array($argument[array_keys($argument)[0]])
? count($argument[array_keys($argument)[0]])
: 1;
},
$arguments
);
}
public function arrayArguments(): int
{
$count = 0;
foreach (array_keys($this->arguments) as $argument) {
if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) {
++$count;
}
}
return $count;
}
private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array
{
foreach ($arguments as $index => $argument) {
if ($rows[$index] === 1 && $columns[$index] === 1) {
while (is_array($argument)) {
$argument = array_pop($argument);
}
$arguments[$index] = $argument;
}
}
return $arguments;
}
private function filterArray(array $array): array
{
return array_filter(
$array,
function ($value) {
return $value > 1;
}
);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class ArrayArgumentProcessor
{
/**
* @var ArrayArgumentHelper
*/
private static $arrayArgumentHelper;
/**
* @param mixed ...$arguments
*/
public static function processArguments(
ArrayArgumentHelper $arrayArgumentHelper,
callable $method,
...$arguments
): array {
self::$arrayArgumentHelper = $arrayArgumentHelper;
if (self::$arrayArgumentHelper->hasArrayArgument() === false) {
return [$method(...$arguments)];
}
if (self::$arrayArgumentHelper->arrayArguments() === 1) {
$nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber();
return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments);
}
$singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector();
$singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector();
if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) {
// Basic logic for a single row vector and a single column vector
return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments);
}
$matrixPair = self::$arrayArgumentHelper->getMatrixPair();
if ($matrixPair !== []) {
if (
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === true &&
self::$arrayArgumentHelper->isVector($matrixPair[1]) === false) ||
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === false &&
self::$arrayArgumentHelper->isVector($matrixPair[1]) === true)
) {
// Logic for a matrix and a vector (row or column)
return self::evaluateVectorMatrixPair($method, $matrixPair, ...$arguments);
}
// Logic for matrix/matrix, column vector/column vector or row vector/row vector
return self::evaluateMatrixPair($method, $matrixPair, ...$arguments);
}
// Still need to work out the logic for more than two array arguments,
// For the moment, we're throwing an Exception when we initialise the ArrayArgumentHelper
return ['#VALUE!'];
}
/**
* @param mixed ...$arguments
*/
private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array
{
$matrix2 = array_pop($matrixIndexes);
/** @var array $matrixValues2 */
$matrixValues2 = $arguments[$matrix2];
$matrix1 = array_pop($matrixIndexes);
/** @var array $matrixValues1 */
$matrixValues1 = $arguments[$matrix1];
$rows = min(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2]));
$columns = min(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2]));
if ($rows === 1) {
$rows = max(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2]));
}
if ($columns === 1) {
$columns = max(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2]));
}
$result = [];
for ($rowIndex = 0; $rowIndex < $rows; ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $columns; ++$columnIndex) {
$rowIndex1 = self::$arrayArgumentHelper->isRowVector($matrix1) ? 0 : $rowIndex;
$columnIndex1 = self::$arrayArgumentHelper->isColumnVector($matrix1) ? 0 : $columnIndex;
$value1 = $matrixValues1[$rowIndex1][$columnIndex1];
$rowIndex2 = self::$arrayArgumentHelper->isRowVector($matrix2) ? 0 : $rowIndex;
$columnIndex2 = self::$arrayArgumentHelper->isColumnVector($matrix2) ? 0 : $columnIndex;
$value2 = $matrixValues2[$rowIndex2][$columnIndex2];
$arguments[$matrix1] = $value1;
$arguments[$matrix2] = $value2;
$result[$rowIndex][$columnIndex] = $method(...$arguments);
}
}
return $result;
}
/**
* @param mixed ...$arguments
*/
private static function evaluateMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array
{
$matrix2 = array_pop($matrixIndexes);
/** @var array $matrixValues2 */
$matrixValues2 = $arguments[$matrix2];
$matrix1 = array_pop($matrixIndexes);
/** @var array $matrixValues1 */
$matrixValues1 = $arguments[$matrix1];
$result = [];
foreach ($matrixValues1 as $rowIndex => $row) {
foreach ($row as $columnIndex => $value1) {
if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) {
continue;
}
$value2 = $matrixValues2[$rowIndex][$columnIndex];
$arguments[$matrix1] = $value1;
$arguments[$matrix2] = $value2;
$result[$rowIndex][$columnIndex] = $method(...$arguments);
}
}
return $result;
}
/**
* @param mixed ...$arguments
*/
private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, ...$arguments): array
{
$rowVector = Functions::flattenArray($arguments[$rowIndex]);
$columnVector = Functions::flattenArray($arguments[$columnIndex]);
$result = [];
foreach ($columnVector as $column) {
$rowResults = [];
foreach ($rowVector as $row) {
$arguments[$rowIndex] = $row;
$arguments[$columnIndex] = $column;
$rowResults[] = $method(...$arguments);
}
$result[] = $rowResults;
}
return $result;
}
/**
* Note, offset is from 1 (for the first argument) rather than from 0.
*
* @param mixed ...$arguments
*/
private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, ...$arguments): array
{
$values = array_slice($arguments, $nthArgument - 1, 1);
/** @var array $values */
$values = array_pop($values);
$result = [];
foreach ($values as $value) {
$arguments[$nthArgument - 1] = $value;
$result[] = $method(...$arguments);
}
return $result;
}
}

View File

@@ -0,0 +1,223 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class BranchPruner
{
/**
* @var bool
*/
protected $branchPruningEnabled = true;
/**
* Used to generate unique store keys.
*
* @var int
*/
private $branchStoreKeyCounter = 0;
/**
* currently pending storeKey (last item of the storeKeysStack.
*
* @var ?string
*/
protected $pendingStoreKey;
/**
* @var string[]
*/
protected $storeKeysStack = [];
/**
* @var bool[]
*/
protected $conditionMap = [];
/**
* @var bool[]
*/
protected $thenMap = [];
/**
* @var bool[]
*/
protected $elseMap = [];
/**
* @var int[]
*/
protected $braceDepthMap = [];
/**
* @var null|string
*/
protected $currentCondition;
/**
* @var null|string
*/
protected $currentOnlyIf;
/**
* @var null|string
*/
protected $currentOnlyIfNot;
/**
* @var null|string
*/
protected $previousStoreKey;
public function __construct(bool $branchPruningEnabled)
{
$this->branchPruningEnabled = $branchPruningEnabled;
}
public function clearBranchStore(): void
{
$this->branchStoreKeyCounter = 0;
}
public function initialiseForLoop(): void
{
$this->currentCondition = null;
$this->currentOnlyIf = null;
$this->currentOnlyIfNot = null;
$this->previousStoreKey = null;
$this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack);
if ($this->branchPruningEnabled) {
$this->initialiseCondition();
$this->initialiseThen();
$this->initialiseElse();
}
}
private function initialiseCondition(): void
{
if (isset($this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) {
$this->currentCondition = $this->pendingStoreKey;
$stackDepth = count($this->storeKeysStack);
if ($stackDepth > 1) {
// nested if
$this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2];
}
}
}
private function initialiseThen(): void
{
if (isset($this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) {
$this->currentOnlyIf = $this->pendingStoreKey;
} elseif (
isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey])
&& $this->thenMap[$this->previousStoreKey]
) {
$this->currentOnlyIf = $this->previousStoreKey;
}
}
private function initialiseElse(): void
{
if (isset($this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) {
$this->currentOnlyIfNot = $this->pendingStoreKey;
} elseif (
isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey])
&& $this->elseMap[$this->previousStoreKey]
) {
$this->currentOnlyIfNot = $this->previousStoreKey;
}
}
public function decrementDepth(): void
{
if (!empty($this->pendingStoreKey)) {
--$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function incrementDepth(): void
{
if (!empty($this->pendingStoreKey)) {
++$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function functionCall(string $functionName): void
{
if ($this->branchPruningEnabled && ($functionName === 'IF(')) {
// we handle a new if
$this->pendingStoreKey = $this->getUnusedBranchStoreKey();
$this->storeKeysStack[] = $this->pendingStoreKey;
$this->conditionMap[$this->pendingStoreKey] = true;
$this->braceDepthMap[$this->pendingStoreKey] = 0;
} elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) {
// this is not an if but we go deeper
++$this->braceDepthMap[$this->pendingStoreKey];
}
}
public function argumentSeparator(): void
{
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) {
// We must go to the IF next argument
if ($this->conditionMap[$this->pendingStoreKey]) {
$this->conditionMap[$this->pendingStoreKey] = false;
$this->thenMap[$this->pendingStoreKey] = true;
} elseif ($this->thenMap[$this->pendingStoreKey]) {
$this->thenMap[$this->pendingStoreKey] = false;
$this->elseMap[$this->pendingStoreKey] = true;
} elseif ($this->elseMap[$this->pendingStoreKey]) {
throw new Exception('Reaching fourth argument of an IF');
}
}
}
/**
* @param mixed $value
*/
public function closingBrace($value): void
{
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) {
// we are closing an IF(
if ($value !== 'IF(') {
throw new Exception('Parser bug we should be in an "IF("');
}
if ($this->conditionMap[$this->pendingStoreKey]) {
throw new Exception('We should not be expecting a condition');
}
$this->thenMap[$this->pendingStoreKey] = false;
$this->elseMap[$this->pendingStoreKey] = false;
--$this->braceDepthMap[$this->pendingStoreKey];
array_pop($this->storeKeysStack);
$this->pendingStoreKey = null;
}
}
public function currentCondition(): ?string
{
return $this->currentCondition;
}
public function currentOnlyIf(): ?string
{
return $this->currentOnlyIf;
}
public function currentOnlyIfNot(): ?string
{
return $this->currentOnlyIfNot;
}
private function getUnusedBranchStoreKey(): string
{
$storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
++$this->branchStoreKeyCounter;
return $storeKeyValue;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
class CyclicReferenceStack
{
/**
* The call stack for calculated cells.
*
* @var mixed[]
*/
private $stack = [];
/**
* Return the number of entries on the stack.
*
* @return int
*/
public function count()
{
return count($this->stack);
}
/**
* Push a new entry onto the stack.
*
* @param mixed $value
*/
public function push($value): void
{
$this->stack[$value] = $value;
}
/**
* Pop the last entry from the stack.
*
* @return mixed
*/
public function pop()
{
return array_pop($this->stack);
}
/**
* Test to see if a specified entry exists on the stack.
*
* @param mixed $value The value to test
*
* @return bool
*/
public function onStack($value)
{
return isset($this->stack[$value]);
}
/**
* Clear the stack.
*/
public function clear(): void
{
$this->stack = [];
}
/**
* Return an array of all entries on the stack.
*
* @return mixed[]
*/
public function showStack()
{
return $this->stack;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class FormattedNumber
{
/** Constants */
/** Regular Expressions */
private const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
private const STRING_REGEXP_PERCENT = '~^(?:(?: *(?<PrefixedSign>[-+])? *\% *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i';
private const STRING_CONVERSION_LIST = [
[self::class, 'convertToNumberIfNumeric'],
[self::class, 'convertToNumberIfFraction'],
[self::class, 'convertToNumberIfPercent'],
[self::class, 'convertToNumberIfCurrency'],
];
/**
* Identify whether a string contains a formatted numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfFormatted(string &$operand): bool
{
foreach (self::STRING_CONVERSION_LIST as $conversionMethod) {
if ($conversionMethod($operand) === true) {
return true;
}
}
return false;
}
/**
* Identify whether a string contains a numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfNumeric(string &$operand): bool
{
$value = preg_replace(['/(\d),(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand));
if (is_numeric($value)) {
$operand = (float) $value;
return true;
}
return false;
}
/**
* Identify whether a string contains a fractional numeric value,
* and convert it to a numeric if it is.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfFraction(string &$operand): bool
{
if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) {
$sign = ($match[1] === '-') ? '-' : '+';
$wholePart = ($match[3] === '') ? '' : ($sign . $match[3]);
$fractionFormula = '=' . $wholePart . $sign . $match[4];
$operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula);
return true;
}
return false;
}
/**
* Identify whether a string contains a percentage, and if so,
* convert it to a numeric.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfPercent(string &$operand): bool
{
$value = preg_replace('/(\d),(\d)/u', '$1$2', $operand);
$match = [];
if ($value !== null && preg_match(self::STRING_REGEXP_PERCENT, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Calculate the percentage
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;
return true;
}
return false;
}
/**
* Identify whether a string contains a currency value, and if so,
* convert it to a numeric.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfCurrency(string &$operand): bool
{
$quotedCurrencyCode = preg_quote(StringHelper::getCurrencyCode());
$value = preg_replace('/(\d),(\d)/u', '$1$2', $operand);
$regExp = '~^(?:(?: *(?<PrefixedSign>[-+])? *' . $quotedCurrencyCode . ' *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *' . $quotedCurrencyCode . ' *))$~ui';
$match = [];
if ($value !== null && preg_match($regExp, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Determine the sign
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
//Cast to a float
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue']));
return true;
}
return false;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
class Logger
{
/**
* Flag to determine whether a debug log should be generated by the calculation engine
* If true, then a debug log will be generated
* If false, then a debug log will not be generated.
*
* @var bool
*/
private $writeDebugLog = false;
/**
* Flag to determine whether a debug log should be echoed by the calculation engine
* If true, then a debug log will be echoed
* If false, then a debug log will not be echoed
* A debug log can only be echoed if it is generated.
*
* @var bool
*/
private $echoDebugLog = false;
/**
* The debug log generated by the calculation engine.
*
* @var string[]
*/
private $debugLog = [];
/**
* The calculation engine cell reference stack.
*
* @var CyclicReferenceStack
*/
private $cellStack;
/**
* Instantiate a Calculation engine logger.
*/
public function __construct(CyclicReferenceStack $stack)
{
$this->cellStack = $stack;
}
/**
* Enable/Disable Calculation engine logging.
*
* @param bool $writeDebugLog
*/
public function setWriteDebugLog($writeDebugLog): void
{
$this->writeDebugLog = $writeDebugLog;
}
/**
* Return whether calculation engine logging is enabled or disabled.
*
* @return bool
*/
public function getWriteDebugLog()
{
return $this->writeDebugLog;
}
/**
* Enable/Disable echoing of debug log information.
*
* @param bool $echoDebugLog
*/
public function setEchoDebugLog($echoDebugLog): void
{
$this->echoDebugLog = $echoDebugLog;
}
/**
* Return whether echoing of debug log information is enabled or disabled.
*
* @return bool
*/
public function getEchoDebugLog()
{
return $this->echoDebugLog;
}
/**
* Write an entry to the calculation engine debug log.
*
* @param mixed $args
*/
public function writeDebugLog(string $message, ...$args): void
{
// Only write the debug log if logging is enabled
if ($this->writeDebugLog) {
$message = sprintf($message, ...$args);
$cellReference = implode(' -> ', $this->cellStack->showStack());
if ($this->echoDebugLog) {
echo $cellReference,
($this->cellStack->count() > 0 ? ' => ' : ''),
$message,
PHP_EOL;
}
$this->debugLog[] = $cellReference .
($this->cellStack->count() > 0 ? ' => ' : '') .
$message;
}
}
/**
* Write a series of entries to the calculation engine debug log.
*
* @param string[] $args
*/
public function mergeDebugLog(array $args): void
{
if ($this->writeDebugLog) {
foreach ($args as $entry) {
$this->writeDebugLog($entry);
}
}
}
/**
* Clear the calculation engine debug log.
*/
public function clearLog(): void
{
$this->debugLog = [];
}
/**
* Return the calculation engine debug log.
*
* @return string[]
*/
public function getLog()
{
return $this->debugLog;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
interface Operand
{
public static function fromParser(string $formula, int $index, array $matches): self;
public function value(): string;
}

View File

@@ -0,0 +1,49 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
final class StructuredReference implements Operand
{
public const NAME = 'Structured Reference';
private const OPEN_BRACE = '[';
private const CLOSE_BRACE = ']';
private string $value;
public function __construct(string $structuredReference)
{
$this->value = $structuredReference;
}
public static function fromParser(string $formula, int $index, array $matches): self
{
$val = $matches[0];
$srCount = substr_count($val, self::OPEN_BRACE)
- substr_count($val, self::CLOSE_BRACE);
while ($srCount > 0) {
$srIndex = strlen($val);
$srStringRemainder = substr($formula, $index + $srIndex);
$closingPos = strpos($srStringRemainder, self::CLOSE_BRACE);
if ($closingPos === false) {
throw new Exception("Formula Error: No closing ']' to match opening '['");
}
$srStringRemainder = substr($srStringRemainder, 0, $closingPos + 1);
--$srCount;
if (strpos($srStringRemainder, self::OPEN_BRACE) !== false) {
++$srCount;
}
$val .= $srStringRemainder;
}
return new self($val);
}
public function value(): string
{
return $this->value;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class BesselI
{
use ArrayEnabled;
/**
* BESSELI.
*
* Returns the modified Bessel function In(x), which is equivalent to the Bessel function evaluated
* for purely imaginary arguments
*
* Excel Function:
* BESSELI(x,ord)
*
* NOTE: The MS Excel implementation of the BESSELI function is still not accurate.
* This code provides a more accurate calculation
*
* @param mixed $x A float value at which to evaluate the function.
* If x is nonnumeric, BESSELI returns the #VALUE! error value.
* Or can be an array of values
* @param mixed $ord The integer order of the Bessel function.
* If ord is not an integer, it is truncated.
* If $ord is nonnumeric, BESSELI returns the #VALUE! error value.
* If $ord < 0, BESSELI returns the #NUM! error value.
* Or can be an array of values
*
* @return array|float|string Result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BESSELI($x, $ord)
{
if (is_array($x) || is_array($ord)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord);
}
try {
$x = EngineeringValidations::validateFloat($x);
$ord = EngineeringValidations::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if ($ord < 0) {
return ExcelError::NAN();
}
$fResult = self::calculate($x, $ord);
return (is_nan($fResult)) ? ExcelError::NAN() : $fResult;
}
private static function calculate(float $x, int $ord): float
{
// special cases
switch ($ord) {
case 0:
return self::besselI0($x);
case 1:
return self::besselI1($x);
}
return self::besselI2($x, $ord);
}
private static function besselI0(float $x): float
{
$ax = abs($x);
if ($ax < 3.75) {
$y = $x / 3.75;
$y = $y * $y;
return 1.0 + $y * (3.5156229 + $y * (3.0899424 + $y * (1.2067492
+ $y * (0.2659732 + $y * (0.360768e-1 + $y * 0.45813e-2)))));
}
$y = 3.75 / $ax;
return (exp($ax) / sqrt($ax)) * (0.39894228 + $y * (0.1328592e-1 + $y * (0.225319e-2 + $y * (-0.157565e-2
+ $y * (0.916281e-2 + $y * (-0.2057706e-1 + $y * (0.2635537e-1 +
$y * (-0.1647633e-1 + $y * 0.392377e-2))))))));
}
private static function besselI1(float $x): float
{
$ax = abs($x);
if ($ax < 3.75) {
$y = $x / 3.75;
$y = $y * $y;
$ans = $ax * (0.5 + $y * (0.87890594 + $y * (0.51498869 + $y * (0.15084934 + $y * (0.2658733e-1 +
$y * (0.301532e-2 + $y * 0.32411e-3))))));
return ($x < 0.0) ? -$ans : $ans;
}
$y = 3.75 / $ax;
$ans = 0.2282967e-1 + $y * (-0.2895312e-1 + $y * (0.1787654e-1 - $y * 0.420059e-2));
$ans = 0.39894228 + $y * (-0.3988024e-1 + $y * (-0.362018e-2 + $y * (0.163801e-2 +
$y * (-0.1031555e-1 + $y * $ans))));
$ans *= exp($ax) / sqrt($ax);
return ($x < 0.0) ? -$ans : $ans;
}
/**
* Sop to Scrutinizer.
*
* @var float
*/
private static $zeroPointZero = 0.0;
private static function besselI2(float $x, int $ord): float
{
if ($x === self::$zeroPointZero) {
return 0.0;
}
$tox = 2.0 / abs($x);
$bip = 0;
$ans = 0.0;
$bi = 1.0;
for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) {
$bim = $bip + $j * $tox * $bi;
$bip = $bi;
$bi = $bim;
if (abs($bi) > 1.0e+12) {
$ans *= 1.0e-12;
$bi *= 1.0e-12;
$bip *= 1.0e-12;
}
if ($j === $ord) {
$ans = $bip;
}
}
$ans *= self::besselI0($x) / $bi;
return ($x < 0.0 && (($ord % 2) === 1)) ? -$ans : $ans;
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class BesselJ
{
use ArrayEnabled;
/**
* BESSELJ.
*
* Returns the Bessel function
*
* Excel Function:
* BESSELJ(x,ord)
*
* NOTE: The MS Excel implementation of the BESSELJ function is still not accurate, particularly for higher order
* values with x < -8 and x > 8. This code provides a more accurate calculation
*
* @param mixed $x A float value at which to evaluate the function.
* If x is nonnumeric, BESSELJ returns the #VALUE! error value.
* Or can be an array of values
* @param mixed $ord The integer order of the Bessel function.
* If ord is not an integer, it is truncated.
* If $ord is nonnumeric, BESSELJ returns the #VALUE! error value.
* If $ord < 0, BESSELJ returns the #NUM! error value.
* Or can be an array of values
*
* @return array|float|string Result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BESSELJ($x, $ord)
{
if (is_array($x) || is_array($ord)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord);
}
try {
$x = EngineeringValidations::validateFloat($x);
$ord = EngineeringValidations::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if ($ord < 0) {
return ExcelError::NAN();
}
$fResult = self::calculate($x, $ord);
return (is_nan($fResult)) ? ExcelError::NAN() : $fResult;
}
private static function calculate(float $x, int $ord): float
{
// special cases
switch ($ord) {
case 0:
return self::besselJ0($x);
case 1:
return self::besselJ1($x);
}
return self::besselJ2($x, $ord);
}
private static function besselJ0(float $x): float
{
$ax = abs($x);
if ($ax < 8.0) {
$y = $x * $x;
$ans1 = 57568490574.0 + $y * (-13362590354.0 + $y * (651619640.7 + $y * (-11214424.18 + $y *
(77392.33017 + $y * (-184.9052456)))));
$ans2 = 57568490411.0 + $y * (1029532985.0 + $y * (9494680.718 + $y * (59272.64853 + $y *
(267.8532712 + $y * 1.0))));
return $ans1 / $ans2;
}
$z = 8.0 / $ax;
$y = $z * $z;
$xx = $ax - 0.785398164;
$ans1 = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6)));
$ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y *
(0.7621095161e-6 - $y * 0.934935152e-7)));
return sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2);
}
private static function besselJ1(float $x): float
{
$ax = abs($x);
if ($ax < 8.0) {
$y = $x * $x;
$ans1 = $x * (72362614232.0 + $y * (-7895059235.0 + $y * (242396853.1 + $y *
(-2972611.439 + $y * (15704.48260 + $y * (-30.16036606))))));
$ans2 = 144725228442.0 + $y * (2300535178.0 + $y * (18583304.74 + $y * (99447.43394 + $y *
(376.9991397 + $y * 1.0))));
return $ans1 / $ans2;
}
$z = 8.0 / $ax;
$y = $z * $z;
$xx = $ax - 2.356194491;
$ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6))));
$ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y *
(-0.88228987e-6 + $y * 0.105787412e-6)));
$ans = sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2);
return ($x < 0.0) ? -$ans : $ans;
}
private static function besselJ2(float $x, int $ord): float
{
$ax = abs($x);
if ($ax === 0.0) {
return 0.0;
}
if ($ax > $ord) {
return self::besselj2a($ax, $ord, $x);
}
return self::besselj2b($ax, $ord, $x);
}
private static function besselj2a(float $ax, int $ord, float $x)
{
$tox = 2.0 / $ax;
$bjm = self::besselJ0($ax);
$bj = self::besselJ1($ax);
for ($j = 1; $j < $ord; ++$j) {
$bjp = $j * $tox * $bj - $bjm;
$bjm = $bj;
$bj = $bjp;
}
$ans = $bj;
return ($x < 0.0 && ($ord % 2) == 1) ? -$ans : $ans;
}
private static function besselj2b(float $ax, int $ord, float $x)
{
$tox = 2.0 / $ax;
$jsum = false;
$bjp = $ans = $sum = 0.0;
$bj = 1.0;
for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) {
$bjm = $j * $tox * $bj - $bjp;
$bjp = $bj;
$bj = $bjm;
if (abs($bj) > 1.0e+10) {
$bj *= 1.0e-10;
$bjp *= 1.0e-10;
$ans *= 1.0e-10;
$sum *= 1.0e-10;
}
if ($jsum === true) {
$sum += $bj;
}
$jsum = $jsum === false;
if ($j === $ord) {
$ans = $bjp;
}
}
$sum = 2.0 * $sum - $bj;
$ans /= $sum;
return ($x < 0.0 && ($ord % 2) === 1) ? -$ans : $ans;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class BesselK
{
use ArrayEnabled;
/**
* BESSELK.
*
* Returns the modified Bessel function Kn(x), which is equivalent to the Bessel functions evaluated
* for purely imaginary arguments.
*
* Excel Function:
* BESSELK(x,ord)
*
* @param mixed $x A float value at which to evaluate the function.
* If x is nonnumeric, BESSELK returns the #VALUE! error value.
* Or can be an array of values
* @param mixed $ord The integer order of the Bessel function.
* If ord is not an integer, it is truncated.
* If $ord is nonnumeric, BESSELK returns the #VALUE! error value.
* If $ord < 0, BESSELKI returns the #NUM! error value.
* Or can be an array of values
*
* @return array|float|string Result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BESSELK($x, $ord)
{
if (is_array($x) || is_array($ord)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord);
}
try {
$x = EngineeringValidations::validateFloat($x);
$ord = EngineeringValidations::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if (($ord < 0) || ($x <= 0.0)) {
return ExcelError::NAN();
}
$fBk = self::calculate($x, $ord);
return (is_nan($fBk)) ? ExcelError::NAN() : $fBk;
}
private static function calculate(float $x, int $ord): float
{
// special cases
switch ($ord) {
case 0:
return self::besselK0($x);
case 1:
return self::besselK1($x);
}
return self::besselK2($x, $ord);
}
/**
* Mollify Phpstan.
*
* @codeCoverageIgnore
*/
private static function callBesselI(float $x, int $ord): float
{
$rslt = BesselI::BESSELI($x, $ord);
if (!is_float($rslt)) {
throw new Exception('Unexpected array or string');
}
return $rslt;
}
private static function besselK0(float $x): float
{
if ($x <= 2) {
$fNum2 = $x * 0.5;
$y = ($fNum2 * $fNum2);
return -log($fNum2) * self::callBesselI($x, 0) +
(-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y *
(0.10750e-3 + $y * 0.74e-5))))));
}
$y = 2 / $x;
return exp(-$x) / sqrt($x) *
(1.25331414 + $y * (-0.7832358e-1 + $y * (0.2189568e-1 + $y * (-0.1062446e-1 + $y *
(0.587872e-2 + $y * (-0.251540e-2 + $y * 0.53208e-3))))));
}
private static function besselK1(float $x): float
{
if ($x <= 2) {
$fNum2 = $x * 0.5;
$y = ($fNum2 * $fNum2);
return log($fNum2) * self::callBesselI($x, 1) +
(1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y *
(-0.110404e-2 + $y * (-0.4686e-4))))))) / $x;
}
$y = 2 / $x;
return exp(-$x) / sqrt($x) *
(1.25331414 + $y * (0.23498619 + $y * (-0.3655620e-1 + $y * (0.1504268e-1 + $y * (-0.780353e-2 + $y *
(0.325614e-2 + $y * (-0.68245e-3)))))));
}
private static function besselK2(float $x, int $ord): float
{
$fTox = 2 / $x;
$fBkm = self::besselK0($x);
$fBk = self::besselK1($x);
for ($n = 1; $n < $ord; ++$n) {
$fBkp = $fBkm + $n * $fTox * $fBk;
$fBkm = $fBk;
$fBk = $fBkp;
}
return $fBk;
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class BesselY
{
use ArrayEnabled;
/**
* BESSELY.
*
* Returns the Bessel function, which is also called the Weber function or the Neumann function.
*
* Excel Function:
* BESSELY(x,ord)
*
* @param mixed $x A float value at which to evaluate the function.
* If x is nonnumeric, BESSELY returns the #VALUE! error value.
* Or can be an array of values
* @param mixed $ord The integer order of the Bessel function.
* If ord is not an integer, it is truncated.
* If $ord is nonnumeric, BESSELY returns the #VALUE! error value.
* If $ord < 0, BESSELY returns the #NUM! error value.
* Or can be an array of values
*
* @return array|float|string Result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BESSELY($x, $ord)
{
if (is_array($x) || is_array($ord)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord);
}
try {
$x = EngineeringValidations::validateFloat($x);
$ord = EngineeringValidations::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if (($ord < 0) || ($x <= 0.0)) {
return ExcelError::NAN();
}
$fBy = self::calculate($x, $ord);
return (is_nan($fBy)) ? ExcelError::NAN() : $fBy;
}
private static function calculate(float $x, int $ord): float
{
// special cases
switch ($ord) {
case 0:
return self::besselY0($x);
case 1:
return self::besselY1($x);
}
return self::besselY2($x, $ord);
}
/**
* Mollify Phpstan.
*
* @codeCoverageIgnore
*/
private static function callBesselJ(float $x, int $ord): float
{
$rslt = BesselJ::BESSELJ($x, $ord);
if (!is_float($rslt)) {
throw new Exception('Unexpected array or string');
}
return $rslt;
}
private static function besselY0(float $x): float
{
if ($x < 8.0) {
$y = ($x * $x);
$ans1 = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y *
(-86327.92757 + $y * 228.4622733))));
$ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y *
(47447.26470 + $y * (226.1030244 + $y))));
return $ans1 / $ans2 + 0.636619772 * self::callBesselJ($x, 0) * log($x);
}
$z = 8.0 / $x;
$y = ($z * $z);
$xx = $x - 0.785398164;
$ans1 = 1 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6)));
$ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 + $y *
(-0.934945152e-7))));
return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2);
}
private static function besselY1(float $x): float
{
if ($x < 8.0) {
$y = ($x * $x);
$ans1 = $x * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y *
(0.7349264551e9 + $y * (-0.4237922726e7 + $y * 0.8511937935e4)))));
$ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y *
(0.1020426050e6 + $y * (0.3549632885e3 + $y)))));
return ($ans1 / $ans2) + 0.636619772 * (self::callBesselJ($x, 1) * log($x) - 1 / $x);
}
$z = 8.0 / $x;
$y = $z * $z;
$xx = $x - 2.356194491;
$ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6))));
$ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y *
(-0.88228987e-6 + $y * 0.105787412e-6)));
return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2);
}
private static function besselY2(float $x, int $ord): float
{
$fTox = 2.0 / $x;
$fBym = self::besselY0($x);
$fBy = self::besselY1($x);
for ($n = 1; $n < $ord; ++$n) {
$fByp = $n * $fTox * $fBy - $fBym;
$fBym = $fBy;
$fBy = $fByp;
}
return $fBy;
}
}

View File

@@ -0,0 +1,277 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class BitWise
{
use ArrayEnabled;
const SPLIT_DIVISOR = 2 ** 24;
/**
* Split a number into upper and lower portions for full 32-bit support.
*
* @param float|int $number
*
* @return int[]
*/
private static function splitNumber($number): array
{
return [(int) floor($number / self::SPLIT_DIVISOR), (int) fmod($number, self::SPLIT_DIVISOR)];
}
/**
* BITAND.
*
* Returns the bitwise AND of two integer values.
*
* Excel Function:
* BITAND(number1, number2)
*
* @param array|int $number1
* Or can be an array of values
* @param array|int $number2
* Or can be an array of values
*
* @return array|int|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BITAND($number1, $number2)
{
if (is_array($number1) || is_array($number2)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2);
}
try {
$number1 = self::validateBitwiseArgument($number1);
$number2 = self::validateBitwiseArgument($number2);
} catch (Exception $e) {
return $e->getMessage();
}
$split1 = self::splitNumber($number1);
$split2 = self::splitNumber($number2);
return self::SPLIT_DIVISOR * ($split1[0] & $split2[0]) + ($split1[1] & $split2[1]);
}
/**
* BITOR.
*
* Returns the bitwise OR of two integer values.
*
* Excel Function:
* BITOR(number1, number2)
*
* @param array|int $number1
* Or can be an array of values
* @param array|int $number2
* Or can be an array of values
*
* @return array|int|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BITOR($number1, $number2)
{
if (is_array($number1) || is_array($number2)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2);
}
try {
$number1 = self::validateBitwiseArgument($number1);
$number2 = self::validateBitwiseArgument($number2);
} catch (Exception $e) {
return $e->getMessage();
}
$split1 = self::splitNumber($number1);
$split2 = self::splitNumber($number2);
return self::SPLIT_DIVISOR * ($split1[0] | $split2[0]) + ($split1[1] | $split2[1]);
}
/**
* BITXOR.
*
* Returns the bitwise XOR of two integer values.
*
* Excel Function:
* BITXOR(number1, number2)
*
* @param array|int $number1
* Or can be an array of values
* @param array|int $number2
* Or can be an array of values
*
* @return array|int|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BITXOR($number1, $number2)
{
if (is_array($number1) || is_array($number2)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2);
}
try {
$number1 = self::validateBitwiseArgument($number1);
$number2 = self::validateBitwiseArgument($number2);
} catch (Exception $e) {
return $e->getMessage();
}
$split1 = self::splitNumber($number1);
$split2 = self::splitNumber($number2);
return self::SPLIT_DIVISOR * ($split1[0] ^ $split2[0]) + ($split1[1] ^ $split2[1]);
}
/**
* BITLSHIFT.
*
* Returns the number value shifted left by shift_amount bits.
*
* Excel Function:
* BITLSHIFT(number, shift_amount)
*
* @param array|int $number
* Or can be an array of values
* @param array|int $shiftAmount
* Or can be an array of values
*
* @return array|float|int|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BITLSHIFT($number, $shiftAmount)
{
if (is_array($number) || is_array($shiftAmount)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount);
}
try {
$number = self::validateBitwiseArgument($number);
$shiftAmount = self::validateShiftAmount($shiftAmount);
} catch (Exception $e) {
return $e->getMessage();
}
$result = floor($number * (2 ** $shiftAmount));
if ($result > 2 ** 48 - 1) {
return ExcelError::NAN();
}
return $result;
}
/**
* BITRSHIFT.
*
* Returns the number value shifted right by shift_amount bits.
*
* Excel Function:
* BITRSHIFT(number, shift_amount)
*
* @param array|int $number
* Or can be an array of values
* @param array|int $shiftAmount
* Or can be an array of values
*
* @return array|float|int|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function BITRSHIFT($number, $shiftAmount)
{
if (is_array($number) || is_array($shiftAmount)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount);
}
try {
$number = self::validateBitwiseArgument($number);
$shiftAmount = self::validateShiftAmount($shiftAmount);
} catch (Exception $e) {
return $e->getMessage();
}
$result = floor($number / (2 ** $shiftAmount));
if ($result > 2 ** 48 - 1) { // possible because shiftAmount can be negative
return ExcelError::NAN();
}
return $result;
}
/**
* Validate arguments passed to the bitwise functions.
*
* @param mixed $value
*
* @return float
*/
private static function validateBitwiseArgument($value)
{
$value = self::nullFalseTrueToNumber($value);
if (is_numeric($value)) {
$value = (float) $value;
if ($value == floor($value)) {
if (($value > 2 ** 48 - 1) || ($value < 0)) {
throw new Exception(ExcelError::NAN());
}
return floor($value);
}
throw new Exception(ExcelError::NAN());
}
throw new Exception(ExcelError::VALUE());
}
/**
* Validate arguments passed to the bitwise functions.
*
* @param mixed $value
*
* @return int
*/
private static function validateShiftAmount($value)
{
$value = self::nullFalseTrueToNumber($value);
if (is_numeric($value)) {
if (abs($value) > 53) {
throw new Exception(ExcelError::NAN());
}
return (int) $value;
}
throw new Exception(ExcelError::VALUE());
}
/**
* Many functions accept null/false/true argument treated as 0/0/1.
*
* @param mixed $number
*
* @return mixed
*/
private static function nullFalseTrueToNumber(&$number)
{
if ($number === null) {
$number = 0;
} elseif (is_bool($number)) {
$number = (int) $number;
}
return $number;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class Compare
{
use ArrayEnabled;
/**
* DELTA.
*
* Excel Function:
* DELTA(a[,b])
*
* Tests whether two values are equal. Returns 1 if number1 = number2; returns 0 otherwise.
* Use this function to filter a set of values. For example, by summing several DELTA
* functions you calculate the count of equal pairs. This function is also known as the
* Kronecker Delta function.
*
* @param array|float $a the first number
* Or can be an array of values
* @param array|float $b The second number. If omitted, b is assumed to be zero.
* Or can be an array of values
*
* @return array|int|string (string in the event of an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function DELTA($a, $b = 0.0)
{
if (is_array($a) || is_array($b)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $a, $b);
}
try {
$a = EngineeringValidations::validateFloat($a);
$b = EngineeringValidations::validateFloat($b);
} catch (Exception $e) {
return $e->getMessage();
}
return (int) (abs($a - $b) < 1.0e-15);
}
/**
* GESTEP.
*
* Excel Function:
* GESTEP(number[,step])
*
* Returns 1 if number >= step; returns 0 (zero) otherwise
* Use this function to filter a set of values. For example, by summing several GESTEP
* functions you calculate the count of values that exceed a threshold.
*
* @param array|float $number the value to test against step
* Or can be an array of values
* @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero.
* Or can be an array of values
*
* @return array|int|string (string in the event of an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function GESTEP($number, $step = 0.0)
{
if (is_array($number) || is_array($step)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $step);
}
try {
$number = EngineeringValidations::validateFloat($number);
$step = EngineeringValidations::validateFloat($step ?? 0.0);
} catch (Exception $e) {
return $e->getMessage();
}
return (int) ($number >= $step);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use Complex\Complex as ComplexObject;
use Complex\Exception as ComplexException;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Complex
{
use ArrayEnabled;
/**
* COMPLEX.
*
* Converts real and imaginary coefficients into a complex number of the form x +/- yi or x +/- yj.
*
* Excel Function:
* COMPLEX(realNumber,imaginary[,suffix])
*
* @param mixed $realNumber the real float coefficient of the complex number
* Or can be an array of values
* @param mixed $imaginary the imaginary float coefficient of the complex number
* Or can be an array of values
* @param mixed $suffix The character suffix for the imaginary component of the complex number.
* If omitted, the suffix is assumed to be "i".
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function COMPLEX($realNumber = 0.0, $imaginary = 0.0, $suffix = 'i')
{
if (is_array($realNumber) || is_array($imaginary) || is_array($suffix)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $realNumber, $imaginary, $suffix);
}
$realNumber = $realNumber ?? 0.0;
$imaginary = $imaginary ?? 0.0;
$suffix = $suffix ?? 'i';
try {
$realNumber = EngineeringValidations::validateFloat($realNumber);
$imaginary = EngineeringValidations::validateFloat($imaginary);
} catch (Exception $e) {
return $e->getMessage();
}
if (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) {
$complex = new ComplexObject($realNumber, $imaginary, $suffix);
return (string) $complex;
}
return ExcelError::VALUE();
}
/**
* IMAGINARY.
*
* Returns the imaginary coefficient of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMAGINARY(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the imaginary
* coefficient
* Or can be an array of values
*
* @return array|float|string (string if an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMAGINARY($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return $complex->getImaginary();
}
/**
* IMREAL.
*
* Returns the real coefficient of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMREAL(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the real coefficient
* Or can be an array of values
*
* @return array|float|string (string if an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMREAL($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return $complex->getReal();
}
}

View File

@@ -0,0 +1,611 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use Complex\Complex as ComplexObject;
use Complex\Exception as ComplexException;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ComplexFunctions
{
use ArrayEnabled;
/**
* IMABS.
*
* Returns the absolute value (modulus) of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMABS(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the absolute value
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMABS($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return $complex->abs();
}
/**
* IMARGUMENT.
*
* Returns the argument theta of a complex number, i.e. the angle in radians from the real
* axis to the representation of the number in polar coordinates.
*
* Excel Function:
* IMARGUMENT(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the argument theta
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMARGUMENT($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) {
return ExcelError::DIV0();
}
return $complex->argument();
}
/**
* IMCONJUGATE.
*
* Returns the complex conjugate of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCONJUGATE(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the conjugate
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCONJUGATE($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->conjugate();
}
/**
* IMCOS.
*
* Returns the cosine of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCOS(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the cosine
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCOS($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->cos();
}
/**
* IMCOSH.
*
* Returns the hyperbolic cosine of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCOSH(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the hyperbolic cosine
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCOSH($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->cosh();
}
/**
* IMCOT.
*
* Returns the cotangent of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCOT(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the cotangent
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCOT($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->cot();
}
/**
* IMCSC.
*
* Returns the cosecant of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCSC(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the cosecant
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCSC($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->csc();
}
/**
* IMCSCH.
*
* Returns the hyperbolic cosecant of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMCSCH(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMCSCH($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->csch();
}
/**
* IMSIN.
*
* Returns the sine of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMSIN(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the sine
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSIN($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->sin();
}
/**
* IMSINH.
*
* Returns the hyperbolic sine of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMSINH(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the hyperbolic sine
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSINH($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->sinh();
}
/**
* IMSEC.
*
* Returns the secant of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMSEC(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the secant
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSEC($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->sec();
}
/**
* IMSECH.
*
* Returns the hyperbolic secant of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMSECH(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the hyperbolic secant
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSECH($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->sech();
}
/**
* IMTAN.
*
* Returns the tangent of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMTAN(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the tangent
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMTAN($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->tan();
}
/**
* IMSQRT.
*
* Returns the square root of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMSQRT(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the square root
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSQRT($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
$theta = self::IMARGUMENT($complexNumber);
if ($theta === ExcelError::DIV0()) {
return '0';
}
return (string) $complex->sqrt();
}
/**
* IMLN.
*
* Returns the natural logarithm of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMLN(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the natural logarithm
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMLN($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) {
return ExcelError::NAN();
}
return (string) $complex->ln();
}
/**
* IMLOG10.
*
* Returns the common logarithm (base 10) of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMLOG10(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the common logarithm
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMLOG10($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) {
return ExcelError::NAN();
}
return (string) $complex->log10();
}
/**
* IMLOG2.
*
* Returns the base-2 logarithm of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMLOG2(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the base-2 logarithm
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMLOG2($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) {
return ExcelError::NAN();
}
return (string) $complex->log2();
}
/**
* IMEXP.
*
* Returns the exponential of a complex number in x + yi or x + yj text format.
*
* Excel Function:
* IMEXP(complexNumber)
*
* @param array|string $complexNumber the complex number for which you want the exponential
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMEXP($complexNumber)
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $complex->exp();
}
/**
* IMPOWER.
*
* Returns a complex number in x + yi or x + yj text format raised to a power.
*
* Excel Function:
* IMPOWER(complexNumber,realNumber)
*
* @param array|string $complexNumber the complex number you want to raise to a power
* Or can be an array of values
* @param array|float|int|string $realNumber the power to which you want to raise the complex number
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMPOWER($complexNumber, $realNumber)
{
if (is_array($complexNumber) || is_array($realNumber)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber, $realNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException $e) {
return ExcelError::NAN();
}
if (!is_numeric($realNumber)) {
return ExcelError::VALUE();
}
return (string) $complex->pow((float) $realNumber);
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use Complex\Complex as ComplexObject;
use Complex\Exception as ComplexException;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ComplexOperations
{
use ArrayEnabled;
/**
* IMDIV.
*
* Returns the quotient of two complex numbers in x + yi or x + yj text format.
*
* Excel Function:
* IMDIV(complexDividend,complexDivisor)
*
* @param array|string $complexDividend the complex numerator or dividend
* Or can be an array of values
* @param array|string $complexDivisor the complex denominator or divisor
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMDIV($complexDividend, $complexDivisor)
{
if (is_array($complexDividend) || is_array($complexDivisor)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexDividend, $complexDivisor);
}
try {
return (string) (new ComplexObject($complexDividend))->divideby(new ComplexObject($complexDivisor));
} catch (ComplexException $e) {
return ExcelError::NAN();
}
}
/**
* IMSUB.
*
* Returns the difference of two complex numbers in x + yi or x + yj text format.
*
* Excel Function:
* IMSUB(complexNumber1,complexNumber2)
*
* @param array|string $complexNumber1 the complex number from which to subtract complexNumber2
* Or can be an array of values
* @param array|string $complexNumber2 the complex number to subtract from complexNumber1
* Or can be an array of values
*
* @return array|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function IMSUB($complexNumber1, $complexNumber2)
{
if (is_array($complexNumber1) || is_array($complexNumber2)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber1, $complexNumber2);
}
try {
return (string) (new ComplexObject($complexNumber1))->subtract(new ComplexObject($complexNumber2));
} catch (ComplexException $e) {
return ExcelError::NAN();
}
}
/**
* IMSUM.
*
* Returns the sum of two or more complex numbers in x + yi or x + yj text format.
*
* Excel Function:
* IMSUM(complexNumber[,complexNumber[,...]])
*
* @param string ...$complexNumbers Series of complex numbers to add
*
* @return string
*/
public static function IMSUM(...$complexNumbers)
{
// Return value
$returnValue = new ComplexObject(0.0);
$aArgs = Functions::flattenArray($complexNumbers);
try {
// Loop through the arguments
foreach ($aArgs as $complex) {
$returnValue = $returnValue->add(new ComplexObject($complex));
}
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $returnValue;
}
/**
* IMPRODUCT.
*
* Returns the product of two or more complex numbers in x + yi or x + yj text format.
*
* Excel Function:
* IMPRODUCT(complexNumber[,complexNumber[,...]])
*
* @param string ...$complexNumbers Series of complex numbers to multiply
*
* @return string
*/
public static function IMPRODUCT(...$complexNumbers)
{
// Return value
$returnValue = new ComplexObject(1.0);
$aArgs = Functions::flattenArray($complexNumbers);
try {
// Loop through the arguments
foreach ($aArgs as $complex) {
$returnValue = $returnValue->multiply(new ComplexObject($complex));
}
} catch (ComplexException $e) {
return ExcelError::NAN();
}
return (string) $returnValue;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
class Constants
{
/**
* EULER.
*/
public const EULER = 2.71828182845904523536;
}

View File

@@ -0,0 +1,69 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
abstract class ConvertBase
{
use ArrayEnabled;
protected static function validateValue($value): string
{
if (is_bool($value)) {
if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) {
throw new Exception(ExcelError::VALUE());
}
$value = (int) $value;
}
if (is_numeric($value)) {
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
$value = floor((float) $value);
}
}
return strtoupper((string) $value);
}
protected static function validatePlaces($places = null): ?int
{
if ($places === null) {
return $places;
}
if (is_numeric($places)) {
if ($places < 0 || $places > 10) {
throw new Exception(ExcelError::NAN());
}
return (int) $places;
}
throw new Exception(ExcelError::VALUE());
}
/**
* Formats a number base string value with leading zeroes.
*
* @param string $value The "number" to pad
* @param ?int $places The length that we want to pad this value
*
* @return string The padded "number"
*/
protected static function nbrConversionFormat(string $value, ?int $places): string
{
if ($places !== null) {
if (strlen($value) <= $places) {
return substr(str_pad($value, $places, '0', STR_PAD_LEFT), -10);
}
return ExcelError::NAN();
}
return substr($value, -10);
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ConvertBinary extends ConvertBase
{
/**
* toDecimal.
*
* Return a binary value as decimal.
*
* Excel Function:
* BIN2DEC(x)
*
* @param array|string $value The binary number (as a string) that you want to convert. The number
* cannot contain more than 10 characters (10 bits). The most significant
* bit of number is the sign bit. The remaining 9 bits are magnitude bits.
* Negative numbers are represented using two's-complement notation.
* If number is not a valid binary number, or if number contains more than
* 10 characters (10 bits), BIN2DEC returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toDecimal($value)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
$value = self::validateValue($value);
$value = self::validateBinary($value);
} catch (Exception $e) {
return $e->getMessage();
}
if (strlen($value) == 10) {
// Two's Complement
$value = substr($value, -9);
return '-' . (512 - bindec($value));
}
return (string) bindec($value);
}
/**
* toHex.
*
* Return a binary value as hex.
*
* Excel Function:
* BIN2HEX(x[,places])
*
* @param array|string $value The binary number (as a string) that you want to convert. The number
* cannot contain more than 10 characters (10 bits). The most significant
* bit of number is the sign bit. The remaining 9 bits are magnitude bits.
* Negative numbers are represented using two's-complement notation.
* If number is not a valid binary number, or if number contains more than
* 10 characters (10 bits), BIN2HEX returns the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, BIN2HEX uses the
* minimum number of characters necessary. Places is useful for padding the
* return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, BIN2HEX returns the #VALUE! error value.
* If places is negative, BIN2HEX returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toHex($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateBinary($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
if (strlen($value) == 10) {
$high2 = substr($value, 0, 2);
$low8 = substr($value, 2);
$xarr = ['00' => '00000000', '01' => '00000001', '10' => 'FFFFFFFE', '11' => 'FFFFFFFF'];
return $xarr[$high2] . strtoupper(substr('0' . dechex((int) bindec($low8)), -2));
}
$hexVal = (string) strtoupper(dechex((int) bindec($value)));
return self::nbrConversionFormat($hexVal, $places);
}
/**
* toOctal.
*
* Return a binary value as octal.
*
* Excel Function:
* BIN2OCT(x[,places])
*
* @param array|string $value The binary number (as a string) that you want to convert. The number
* cannot contain more than 10 characters (10 bits). The most significant
* bit of number is the sign bit. The remaining 9 bits are magnitude bits.
* Negative numbers are represented using two's-complement notation.
* If number is not a valid binary number, or if number contains more than
* 10 characters (10 bits), BIN2OCT returns the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, BIN2OCT uses the
* minimum number of characters necessary. Places is useful for padding the
* return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, BIN2OCT returns the #VALUE! error value.
* If places is negative, BIN2OCT returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toOctal($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateBinary($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
if (strlen($value) == 10 && substr($value, 0, 1) === '1') { // Two's Complement
return str_repeat('7', 6) . strtoupper(decoct((int) bindec("11$value")));
}
$octVal = (string) decoct((int) bindec($value));
return self::nbrConversionFormat($octVal, $places);
}
protected static function validateBinary(string $value): string
{
if ((strlen($value) > preg_match_all('/[01]/', $value)) || (strlen($value) > 10)) {
throw new Exception(ExcelError::NAN());
}
return $value;
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ConvertDecimal extends ConvertBase
{
const LARGEST_OCTAL_IN_DECIMAL = 536870911;
const SMALLEST_OCTAL_IN_DECIMAL = -536870912;
const LARGEST_BINARY_IN_DECIMAL = 511;
const SMALLEST_BINARY_IN_DECIMAL = -512;
const LARGEST_HEX_IN_DECIMAL = 549755813887;
const SMALLEST_HEX_IN_DECIMAL = -549755813888;
/**
* toBinary.
*
* Return a decimal value as binary.
*
* Excel Function:
* DEC2BIN(x[,places])
*
* @param array|string $value The decimal integer you want to convert. If number is negative,
* valid place values are ignored and DEC2BIN returns a 10-character
* (10-bit) binary number in which the most significant bit is the sign
* bit. The remaining 9 bits are magnitude bits. Negative numbers are
* represented using two's-complement notation.
* If number < -512 or if number > 511, DEC2BIN returns the #NUM! error
* value.
* If number is nonnumeric, DEC2BIN returns the #VALUE! error value.
* If DEC2BIN requires more than places characters, it returns the #NUM!
* error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, DEC2BIN uses
* the minimum number of characters necessary. Places is useful for
* padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, DEC2BIN returns the #VALUE! error value.
* If places is zero or negative, DEC2BIN returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toBinary($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateDecimal($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$value = (int) floor((float) $value);
if ($value > self::LARGEST_BINARY_IN_DECIMAL || $value < self::SMALLEST_BINARY_IN_DECIMAL) {
return ExcelError::NAN();
}
$r = decbin($value);
// Two's Complement
$r = substr($r, -10);
return self::nbrConversionFormat($r, $places);
}
/**
* toHex.
*
* Return a decimal value as hex.
*
* Excel Function:
* DEC2HEX(x[,places])
*
* @param array|string $value The decimal integer you want to convert. If number is negative,
* places is ignored and DEC2HEX returns a 10-character (40-bit)
* hexadecimal number in which the most significant bit is the sign
* bit. The remaining 39 bits are magnitude bits. Negative numbers
* are represented using two's-complement notation.
* If number < -549,755,813,888 or if number > 549,755,813,887,
* DEC2HEX returns the #NUM! error value.
* If number is nonnumeric, DEC2HEX returns the #VALUE! error value.
* If DEC2HEX requires more than places characters, it returns the
* #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, DEC2HEX uses
* the minimum number of characters necessary. Places is useful for
* padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, DEC2HEX returns the #VALUE! error value.
* If places is zero or negative, DEC2HEX returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toHex($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateDecimal($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$value = floor((float) $value);
if ($value > self::LARGEST_HEX_IN_DECIMAL || $value < self::SMALLEST_HEX_IN_DECIMAL) {
return ExcelError::NAN();
}
$r = strtoupper(dechex((int) $value));
$r = self::hex32bit($value, $r);
return self::nbrConversionFormat($r, $places);
}
public static function hex32bit(float $value, string $hexstr, bool $force = false): string
{
if (PHP_INT_SIZE === 4 || $force) {
if ($value >= 2 ** 32) {
$quotient = (int) ($value / (2 ** 32));
return strtoupper(substr('0' . dechex($quotient), -2) . $hexstr);
}
if ($value < -(2 ** 32)) {
$quotient = 256 - (int) ceil((-$value) / (2 ** 32));
return strtoupper(substr('0' . dechex($quotient), -2) . substr("00000000$hexstr", -8));
}
if ($value < 0) {
return "FF$hexstr";
}
}
return $hexstr;
}
/**
* toOctal.
*
* Return an decimal value as octal.
*
* Excel Function:
* DEC2OCT(x[,places])
*
* @param array|string $value The decimal integer you want to convert. If number is negative,
* places is ignored and DEC2OCT returns a 10-character (30-bit)
* octal number in which the most significant bit is the sign bit.
* The remaining 29 bits are magnitude bits. Negative numbers are
* represented using two's-complement notation.
* If number < -536,870,912 or if number > 536,870,911, DEC2OCT
* returns the #NUM! error value.
* If number is nonnumeric, DEC2OCT returns the #VALUE! error value.
* If DEC2OCT requires more than places characters, it returns the
* #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, DEC2OCT uses
* the minimum number of characters necessary. Places is useful for
* padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, DEC2OCT returns the #VALUE! error value.
* If places is zero or negative, DEC2OCT returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toOctal($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateDecimal($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$value = (int) floor((float) $value);
if ($value > self::LARGEST_OCTAL_IN_DECIMAL || $value < self::SMALLEST_OCTAL_IN_DECIMAL) {
return ExcelError::NAN();
}
$r = decoct($value);
$r = substr($r, -10);
return self::nbrConversionFormat($r, $places);
}
protected static function validateDecimal(string $value): string
{
if (strlen($value) > preg_match_all('/[-0123456789.]/', $value)) {
throw new Exception(ExcelError::VALUE());
}
return $value;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ConvertHex extends ConvertBase
{
/**
* toBinary.
*
* Return a hex value as binary.
*
* Excel Function:
* HEX2BIN(x[,places])
*
* @param array|string $value The hexadecimal number you want to convert.
* Number cannot contain more than 10 characters.
* The most significant bit of number is the sign bit (40th bit from the right).
* The remaining 9 bits are magnitude bits.
* Negative numbers are represented using two's-complement notation.
* If number is negative, HEX2BIN ignores places and returns a 10-character binary number.
* If number is negative, it cannot be less than FFFFFFFE00,
* and if number is positive, it cannot be greater than 1FF.
* If number is not a valid hexadecimal number, HEX2BIN returns the #NUM! error value.
* If HEX2BIN requires more than places characters, it returns the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted,
* HEX2BIN uses the minimum number of characters necessary. Places
* is useful for padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, HEX2BIN returns the #VALUE! error value.
* If places is negative, HEX2BIN returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toBinary($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateHex($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$dec = self::toDecimal($value);
return ConvertDecimal::toBinary($dec, $places);
}
/**
* toDecimal.
*
* Return a hex value as decimal.
*
* Excel Function:
* HEX2DEC(x)
*
* @param array|string $value The hexadecimal number you want to convert. This number cannot
* contain more than 10 characters (40 bits). The most significant
* bit of number is the sign bit. The remaining 39 bits are magnitude
* bits. Negative numbers are represented using two's-complement
* notation.
* If number is not a valid hexadecimal number, HEX2DEC returns the
* #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toDecimal($value)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
$value = self::validateValue($value);
$value = self::validateHex($value);
} catch (Exception $e) {
return $e->getMessage();
}
if (strlen($value) > 10) {
return ExcelError::NAN();
}
$binX = '';
foreach (str_split($value) as $char) {
$binX .= str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT);
}
if (strlen($binX) == 40 && $binX[0] == '1') {
for ($i = 0; $i < 40; ++$i) {
$binX[$i] = ($binX[$i] == '1' ? '0' : '1');
}
return (string) ((bindec($binX) + 1) * -1);
}
return (string) bindec($binX);
}
/**
* toOctal.
*
* Return a hex value as octal.
*
* Excel Function:
* HEX2OCT(x[,places])
*
* @param array|string $value The hexadecimal number you want to convert. Number cannot
* contain more than 10 characters. The most significant bit of
* number is the sign bit. The remaining 39 bits are magnitude
* bits. Negative numbers are represented using two's-complement
* notation.
* If number is negative, HEX2OCT ignores places and returns a
* 10-character octal number.
* If number is negative, it cannot be less than FFE0000000, and
* if number is positive, it cannot be greater than 1FFFFFFF.
* If number is not a valid hexadecimal number, HEX2OCT returns
* the #NUM! error value.
* If HEX2OCT requires more than places characters, it returns
* the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, HEX2OCT
* uses the minimum number of characters necessary. Places is
* useful for padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, HEX2OCT returns the #VALUE! error
* value.
* If places is negative, HEX2OCT returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toOctal($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateHex($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$decimal = self::toDecimal($value);
return ConvertDecimal::toOctal($decimal, $places);
}
protected static function validateHex(string $value): string
{
if (strlen($value) > preg_match_all('/[0123456789ABCDEF]/', $value)) {
throw new Exception(ExcelError::NAN());
}
return $value;
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ConvertOctal extends ConvertBase
{
/**
* toBinary.
*
* Return an octal value as binary.
*
* Excel Function:
* OCT2BIN(x[,places])
*
* @param array|string $value The octal number you want to convert. Number may not
* contain more than 10 characters. The most significant
* bit of number is the sign bit. The remaining 29 bits
* are magnitude bits. Negative numbers are represented
* using two's-complement notation.
* If number is negative, OCT2BIN ignores places and returns
* a 10-character binary number.
* If number is negative, it cannot be less than 7777777000,
* and if number is positive, it cannot be greater than 777.
* If number is not a valid octal number, OCT2BIN returns
* the #NUM! error value.
* If OCT2BIN requires more than places characters, it
* returns the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted,
* OCT2BIN uses the minimum number of characters necessary.
* Places is useful for padding the return value with
* leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, OCT2BIN returns the #VALUE!
* error value.
* If places is negative, OCT2BIN returns the #NUM! error
* value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toBinary($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateOctal($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
return ConvertDecimal::toBinary(self::toDecimal($value), $places);
}
/**
* toDecimal.
*
* Return an octal value as decimal.
*
* Excel Function:
* OCT2DEC(x)
*
* @param array|string $value The octal number you want to convert. Number may not contain
* more than 10 octal characters (30 bits). The most significant
* bit of number is the sign bit. The remaining 29 bits are
* magnitude bits. Negative numbers are represented using
* two's-complement notation.
* If number is not a valid octal number, OCT2DEC returns the
* #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toDecimal($value)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
$value = self::validateValue($value);
$value = self::validateOctal($value);
} catch (Exception $e) {
return $e->getMessage();
}
$binX = '';
foreach (str_split($value) as $char) {
$binX .= str_pad(decbin((int) $char), 3, '0', STR_PAD_LEFT);
}
if (strlen($binX) == 30 && $binX[0] == '1') {
for ($i = 0; $i < 30; ++$i) {
$binX[$i] = ($binX[$i] == '1' ? '0' : '1');
}
return (string) ((bindec($binX) + 1) * -1);
}
return (string) bindec($binX);
}
/**
* toHex.
*
* Return an octal value as hex.
*
* Excel Function:
* OCT2HEX(x[,places])
*
* @param array|string $value The octal number you want to convert. Number may not contain
* more than 10 octal characters (30 bits). The most significant
* bit of number is the sign bit. The remaining 29 bits are
* magnitude bits. Negative numbers are represented using
* two's-complement notation.
* If number is negative, OCT2HEX ignores places and returns a
* 10-character hexadecimal number.
* If number is not a valid octal number, OCT2HEX returns the
* #NUM! error value.
* If OCT2HEX requires more than places characters, it returns
* the #NUM! error value.
* Or can be an array of values
* @param array|int $places The number of characters to use. If places is omitted, OCT2HEX
* uses the minimum number of characters necessary. Places is useful
* for padding the return value with leading 0s (zeros).
* If places is not an integer, it is truncated.
* If places is nonnumeric, OCT2HEX returns the #VALUE! error value.
* If places is negative, OCT2HEX returns the #NUM! error value.
* Or can be an array of values
*
* @return array|string Result, or an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function toHex($value, $places = null)
{
if (is_array($value) || is_array($places)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places);
}
try {
$value = self::validateValue($value);
$value = self::validateOctal($value);
$places = self::validatePlaces($places);
} catch (Exception $e) {
return $e->getMessage();
}
$hexVal = strtoupper(dechex((int) self::toDecimal($value)));
$hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF{$hexVal}" : $hexVal;
return self::nbrConversionFormat($hexVal, $places);
}
protected static function validateOctal(string $value): string
{
$numDigits = (int) preg_match_all('/[01234567]/', $value);
if (strlen($value) > $numDigits || $numDigits > 10) {
throw new Exception(ExcelError::NAN());
}
return $value;
}
}

View File

@@ -0,0 +1,694 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ConvertUOM
{
use ArrayEnabled;
public const CATEGORY_WEIGHT_AND_MASS = 'Weight and Mass';
public const CATEGORY_DISTANCE = 'Distance';
public const CATEGORY_TIME = 'Time';
public const CATEGORY_PRESSURE = 'Pressure';
public const CATEGORY_FORCE = 'Force';
public const CATEGORY_ENERGY = 'Energy';
public const CATEGORY_POWER = 'Power';
public const CATEGORY_MAGNETISM = 'Magnetism';
public const CATEGORY_TEMPERATURE = 'Temperature';
public const CATEGORY_VOLUME = 'Volume and Liquid Measure';
public const CATEGORY_AREA = 'Area';
public const CATEGORY_INFORMATION = 'Information';
public const CATEGORY_SPEED = 'Speed';
/**
* Details of the Units of measure that can be used in CONVERTUOM().
*
* @var mixed[]
*/
private static $conversionUnits = [
// Weight and Mass
'g' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Gram', 'AllowPrefix' => true],
'sg' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Slug', 'AllowPrefix' => false],
'lbm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false],
'u' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U (atomic mass unit)', 'AllowPrefix' => true],
'ozm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false],
'grain' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Grain', 'AllowPrefix' => false],
'cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false],
'shweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false],
'uk_cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false],
'lcwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false],
'hweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false],
'stone' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Stone', 'AllowPrefix' => false],
'ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ton', 'AllowPrefix' => false],
'uk_ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false],
'LTON' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false],
'brton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false],
// Distance
'm' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Meter', 'AllowPrefix' => true],
'mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Statute mile', 'AllowPrefix' => false],
'Nmi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Nautical mile', 'AllowPrefix' => false],
'in' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Inch', 'AllowPrefix' => false],
'ft' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Foot', 'AllowPrefix' => false],
'yd' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Yard', 'AllowPrefix' => false],
'ang' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Angstrom', 'AllowPrefix' => true],
'ell' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Ell', 'AllowPrefix' => false],
'ly' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Light Year', 'AllowPrefix' => false],
'parsec' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false],
'pc' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false],
'Pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false],
'Picapt' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false],
'pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/6 in)', 'AllowPrefix' => false],
'survey_mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'U.S survey mile (statute mile)', 'AllowPrefix' => false],
// Time
'yr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Year', 'AllowPrefix' => false],
'day' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false],
'd' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false],
'hr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Hour', 'AllowPrefix' => false],
'mn' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false],
'min' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false],
'sec' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true],
's' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true],
// Pressure
'Pa' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true],
'p' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true],
'atm' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true],
'at' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true],
'mmHg' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'mm of Mercury', 'AllowPrefix' => true],
'psi' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'PSI', 'AllowPrefix' => true],
'Torr' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Torr', 'AllowPrefix' => true],
// Force
'N' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Newton', 'AllowPrefix' => true],
'dyn' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true],
'dy' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true],
'lbf' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pound force', 'AllowPrefix' => false],
'pond' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pond', 'AllowPrefix' => true],
// Energy
'J' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Joule', 'AllowPrefix' => true],
'e' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Erg', 'AllowPrefix' => true],
'c' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Thermodynamic calorie', 'AllowPrefix' => true],
'cal' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'IT calorie', 'AllowPrefix' => true],
'eV' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true],
'ev' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true],
'HPh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false],
'hh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false],
'Wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true],
'wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true],
'flb' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Foot-pound', 'AllowPrefix' => false],
'BTU' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false],
'btu' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false],
// Power
'HP' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false],
'h' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false],
'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true],
'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true],
'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false],
// Magnetism
'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true],
'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true],
// Temperature
'C' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false],
'cel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false],
'F' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false],
'fah' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false],
'K' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false],
'kel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false],
'Rank' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Rankine', 'AllowPrefix' => false],
'Reau' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Réaumur', 'AllowPrefix' => false],
// Volume
'l' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true],
'L' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true],
'lt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true],
'tsp' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Teaspoon', 'AllowPrefix' => false],
'tspm' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Modern Teaspoon', 'AllowPrefix' => false],
'tbs' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Tablespoon', 'AllowPrefix' => false],
'oz' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Fluid Ounce', 'AllowPrefix' => false],
'cup' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cup', 'AllowPrefix' => false],
'pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false],
'us_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false],
'uk_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.K. Pint', 'AllowPrefix' => false],
'qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Quart', 'AllowPrefix' => false],
'uk_qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Quart (UK)', 'AllowPrefix' => false],
'gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gallon', 'AllowPrefix' => false],
'uk_gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Gallon (UK)', 'AllowPrefix' => false],
'ang3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true],
'ang^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true],
'barrel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Oil Barrel', 'AllowPrefix' => false],
'bushel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Bushel', 'AllowPrefix' => false],
'in3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false],
'in^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false],
'ft3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false],
'ft^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false],
'ly3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false],
'ly^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false],
'm3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true],
'm^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true],
'mi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false],
'mi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false],
'yd3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false],
'yd^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false],
'Nmi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false],
'Nmi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false],
'Pica3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false],
'Pica^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false],
'Picapt3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false],
'Picapt^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false],
'GRT' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false],
'regton' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false],
'MTON' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Measurement Ton (Freight Ton)', 'AllowPrefix' => false],
// Area
'ha' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Hectare', 'AllowPrefix' => true],
'uk_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'International Acre', 'AllowPrefix' => false],
'us_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'US Survey/Statute Acre', 'AllowPrefix' => false],
'ang2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true],
'ang^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true],
'ar' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Are', 'AllowPrefix' => true],
'ft2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false],
'ft^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false],
'in2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false],
'in^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false],
'ly2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false],
'ly^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false],
'm2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true],
'm^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true],
'Morgen' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Morgen', 'AllowPrefix' => false],
'mi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false],
'mi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false],
'Nmi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false],
'Nmi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false],
'Pica2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false],
'Pica^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false],
'Picapt2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false],
'Picapt^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false],
'yd2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false],
'yd^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false],
// Information
'byte' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Byte', 'AllowPrefix' => true],
'bit' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Bit', 'AllowPrefix' => true],
// Speed
'm/s' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true],
'm/sec' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true],
'm/h' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true],
'm/hr' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true],
'mph' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Miles per hour', 'AllowPrefix' => false],
'admkn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Admiralty Knot', 'AllowPrefix' => false],
'kn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Knot', 'AllowPrefix' => false],
];
/**
* Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM().
*
* @var mixed[]
*/
private static $conversionMultipliers = [
'Y' => ['multiplier' => 1E24, 'name' => 'yotta'],
'Z' => ['multiplier' => 1E21, 'name' => 'zetta'],
'E' => ['multiplier' => 1E18, 'name' => 'exa'],
'P' => ['multiplier' => 1E15, 'name' => 'peta'],
'T' => ['multiplier' => 1E12, 'name' => 'tera'],
'G' => ['multiplier' => 1E9, 'name' => 'giga'],
'M' => ['multiplier' => 1E6, 'name' => 'mega'],
'k' => ['multiplier' => 1E3, 'name' => 'kilo'],
'h' => ['multiplier' => 1E2, 'name' => 'hecto'],
'e' => ['multiplier' => 1E1, 'name' => 'dekao'],
'da' => ['multiplier' => 1E1, 'name' => 'dekao'],
'd' => ['multiplier' => 1E-1, 'name' => 'deci'],
'c' => ['multiplier' => 1E-2, 'name' => 'centi'],
'm' => ['multiplier' => 1E-3, 'name' => 'milli'],
'u' => ['multiplier' => 1E-6, 'name' => 'micro'],
'n' => ['multiplier' => 1E-9, 'name' => 'nano'],
'p' => ['multiplier' => 1E-12, 'name' => 'pico'],
'f' => ['multiplier' => 1E-15, 'name' => 'femto'],
'a' => ['multiplier' => 1E-18, 'name' => 'atto'],
'z' => ['multiplier' => 1E-21, 'name' => 'zepto'],
'y' => ['multiplier' => 1E-24, 'name' => 'yocto'],
];
/**
* Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM().
*
* @var mixed[]
*/
private static $binaryConversionMultipliers = [
'Yi' => ['multiplier' => 2 ** 80, 'name' => 'yobi'],
'Zi' => ['multiplier' => 2 ** 70, 'name' => 'zebi'],
'Ei' => ['multiplier' => 2 ** 60, 'name' => 'exbi'],
'Pi' => ['multiplier' => 2 ** 50, 'name' => 'pebi'],
'Ti' => ['multiplier' => 2 ** 40, 'name' => 'tebi'],
'Gi' => ['multiplier' => 2 ** 30, 'name' => 'gibi'],
'Mi' => ['multiplier' => 2 ** 20, 'name' => 'mebi'],
'ki' => ['multiplier' => 2 ** 10, 'name' => 'kibi'],
];
/**
* Details of the Units of measure conversion factors, organised by group.
*
* @var mixed[]
*/
private static $unitConversions = [
// Conversion uses gram (g) as an intermediate unit
self::CATEGORY_WEIGHT_AND_MASS => [
'g' => 1.0,
'sg' => 6.85217658567918E-05,
'lbm' => 2.20462262184878E-03,
'u' => 6.02214179421676E+23,
'ozm' => 3.52739619495804E-02,
'grain' => 1.54323583529414E+01,
'cwt' => 2.20462262184878E-05,
'shweight' => 2.20462262184878E-05,
'uk_cwt' => 1.96841305522212E-05,
'lcwt' => 1.96841305522212E-05,
'hweight' => 1.96841305522212E-05,
'stone' => 1.57473044417770E-04,
'ton' => 1.10231131092439E-06,
'uk_ton' => 9.84206527611061E-07,
'LTON' => 9.84206527611061E-07,
'brton' => 9.84206527611061E-07,
],
// Conversion uses meter (m) as an intermediate unit
self::CATEGORY_DISTANCE => [
'm' => 1.0,
'mi' => 6.21371192237334E-04,
'Nmi' => 5.39956803455724E-04,
'in' => 3.93700787401575E+01,
'ft' => 3.28083989501312E+00,
'yd' => 1.09361329833771E+00,
'ang' => 1.0E+10,
'ell' => 8.74890638670166E-01,
'ly' => 1.05700083402462E-16,
'parsec' => 3.24077928966473E-17,
'pc' => 3.24077928966473E-17,
'Pica' => 2.83464566929134E+03,
'Picapt' => 2.83464566929134E+03,
'pica' => 2.36220472440945E+02,
'survey_mi' => 6.21369949494950E-04,
],
// Conversion uses second (s) as an intermediate unit
self::CATEGORY_TIME => [
'yr' => 3.16880878140289E-08,
'day' => 1.15740740740741E-05,
'd' => 1.15740740740741E-05,
'hr' => 2.77777777777778E-04,
'mn' => 1.66666666666667E-02,
'min' => 1.66666666666667E-02,
'sec' => 1.0,
's' => 1.0,
],
// Conversion uses Pascal (Pa) as an intermediate unit
self::CATEGORY_PRESSURE => [
'Pa' => 1.0,
'p' => 1.0,
'atm' => 9.86923266716013E-06,
'at' => 9.86923266716013E-06,
'mmHg' => 7.50063755419211E-03,
'psi' => 1.45037737730209E-04,
'Torr' => 7.50061682704170E-03,
],
// Conversion uses Newton (N) as an intermediate unit
self::CATEGORY_FORCE => [
'N' => 1.0,
'dyn' => 1.0E+5,
'dy' => 1.0E+5,
'lbf' => 2.24808923655339E-01,
'pond' => 1.01971621297793E+02,
],
// Conversion uses Joule (J) as an intermediate unit
self::CATEGORY_ENERGY => [
'J' => 1.0,
'e' => 9.99999519343231E+06,
'c' => 2.39006249473467E-01,
'cal' => 2.38846190642017E-01,
'eV' => 6.24145700000000E+18,
'ev' => 6.24145700000000E+18,
'HPh' => 3.72506430801000E-07,
'hh' => 3.72506430801000E-07,
'Wh' => 2.77777916238711E-04,
'wh' => 2.77777916238711E-04,
'flb' => 2.37304222192651E+01,
'BTU' => 9.47815067349015E-04,
'btu' => 9.47815067349015E-04,
],
// Conversion uses Horsepower (HP) as an intermediate unit
self::CATEGORY_POWER => [
'HP' => 1.0,
'h' => 1.0,
'W' => 7.45699871582270E+02,
'w' => 7.45699871582270E+02,
'PS' => 1.01386966542400E+00,
],
// Conversion uses Tesla (T) as an intermediate unit
self::CATEGORY_MAGNETISM => [
'T' => 1.0,
'ga' => 10000.0,
],
// Conversion uses litre (l) as an intermediate unit
self::CATEGORY_VOLUME => [
'l' => 1.0,
'L' => 1.0,
'lt' => 1.0,
'tsp' => 2.02884136211058E+02,
'tspm' => 2.0E+02,
'tbs' => 6.76280454036860E+01,
'oz' => 3.38140227018430E+01,
'cup' => 4.22675283773038E+00,
'pt' => 2.11337641886519E+00,
'us_pt' => 2.11337641886519E+00,
'uk_pt' => 1.75975398639270E+00,
'qt' => 1.05668820943259E+00,
'uk_qt' => 8.79876993196351E-01,
'gal' => 2.64172052358148E-01,
'uk_gal' => 2.19969248299088E-01,
'ang3' => 1.0E+27,
'ang^3' => 1.0E+27,
'barrel' => 6.28981077043211E-03,
'bushel' => 2.83775932584017E-02,
'in3' => 6.10237440947323E+01,
'in^3' => 6.10237440947323E+01,
'ft3' => 3.53146667214886E-02,
'ft^3' => 3.53146667214886E-02,
'ly3' => 1.18093498844171E-51,
'ly^3' => 1.18093498844171E-51,
'm3' => 1.0E-03,
'm^3' => 1.0E-03,
'mi3' => 2.39912758578928E-13,
'mi^3' => 2.39912758578928E-13,
'yd3' => 1.30795061931439E-03,
'yd^3' => 1.30795061931439E-03,
'Nmi3' => 1.57426214685811E-13,
'Nmi^3' => 1.57426214685811E-13,
'Pica3' => 2.27769904358706E+07,
'Pica^3' => 2.27769904358706E+07,
'Picapt3' => 2.27769904358706E+07,
'Picapt^3' => 2.27769904358706E+07,
'GRT' => 3.53146667214886E-04,
'regton' => 3.53146667214886E-04,
'MTON' => 8.82866668037215E-04,
],
// Conversion uses hectare (ha) as an intermediate unit
self::CATEGORY_AREA => [
'ha' => 1.0,
'uk_acre' => 2.47105381467165E+00,
'us_acre' => 2.47104393046628E+00,
'ang2' => 1.0E+24,
'ang^2' => 1.0E+24,
'ar' => 1.0E+02,
'ft2' => 1.07639104167097E+05,
'ft^2' => 1.07639104167097E+05,
'in2' => 1.55000310000620E+07,
'in^2' => 1.55000310000620E+07,
'ly2' => 1.11725076312873E-28,
'ly^2' => 1.11725076312873E-28,
'm2' => 1.0E+04,
'm^2' => 1.0E+04,
'Morgen' => 4.0E+00,
'mi2' => 3.86102158542446E-03,
'mi^2' => 3.86102158542446E-03,
'Nmi2' => 2.91553349598123E-03,
'Nmi^2' => 2.91553349598123E-03,
'Pica2' => 8.03521607043214E+10,
'Pica^2' => 8.03521607043214E+10,
'Picapt2' => 8.03521607043214E+10,
'Picapt^2' => 8.03521607043214E+10,
'yd2' => 1.19599004630108E+04,
'yd^2' => 1.19599004630108E+04,
],
// Conversion uses bit (bit) as an intermediate unit
self::CATEGORY_INFORMATION => [
'bit' => 1.0,
'byte' => 0.125,
],
// Conversion uses Meters per Second (m/s) as an intermediate unit
self::CATEGORY_SPEED => [
'm/s' => 1.0,
'm/sec' => 1.0,
'm/h' => 3.60E+03,
'm/hr' => 3.60E+03,
'mph' => 2.23693629205440E+00,
'admkn' => 1.94260256941567E+00,
'kn' => 1.94384449244060E+00,
],
];
/**
* getConversionGroups
* Returns a list of the different conversion groups for UOM conversions.
*
* @return array
*/
public static function getConversionCategories()
{
$conversionGroups = [];
foreach (self::$conversionUnits as $conversionUnit) {
$conversionGroups[] = $conversionUnit['Group'];
}
return array_merge(array_unique($conversionGroups));
}
/**
* getConversionGroupUnits
* Returns an array of units of measure, for a specified conversion group, or for all groups.
*
* @param string $category The group whose units of measure you want to retrieve
*
* @return array
*/
public static function getConversionCategoryUnits($category = null)
{
$conversionGroups = [];
foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) {
if (($category === null) || ($conversionGroup['Group'] == $category)) {
$conversionGroups[$conversionGroup['Group']][] = $conversionUnit;
}
}
return $conversionGroups;
}
/**
* getConversionGroupUnitDetails.
*
* @param string $category The group whose units of measure you want to retrieve
*
* @return array
*/
public static function getConversionCategoryUnitDetails($category = null)
{
$conversionGroups = [];
foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) {
if (($category === null) || ($conversionGroup['Group'] == $category)) {
$conversionGroups[$conversionGroup['Group']][] = [
'unit' => $conversionUnit,
'description' => $conversionGroup['Unit Name'],
];
}
}
return $conversionGroups;
}
/**
* getConversionMultipliers
* Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM().
*
* @return mixed[]
*/
public static function getConversionMultipliers()
{
return self::$conversionMultipliers;
}
/**
* getBinaryConversionMultipliers
* Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM().
*
* @return mixed[]
*/
public static function getBinaryConversionMultipliers()
{
return self::$binaryConversionMultipliers;
}
/**
* CONVERT.
*
* Converts a number from one measurement system to another.
* For example, CONVERT can translate a table of distances in miles to a table of distances
* in kilometers.
*
* Excel Function:
* CONVERT(value,fromUOM,toUOM)
*
* @param array|float|int|string $value the value in fromUOM to convert
* Or can be an array of values
* @param array|string $fromUOM the units for value
* Or can be an array of values
* @param array|string $toUOM the units for the result
* Or can be an array of values
*
* @return array|float|string Result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function CONVERT($value, $fromUOM, $toUOM)
{
if (is_array($value) || is_array($fromUOM) || is_array($toUOM)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $fromUOM, $toUOM);
}
if (!is_numeric($value)) {
return ExcelError::VALUE();
}
try {
[$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM);
[$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM);
} catch (Exception $e) {
return ExcelError::NA();
}
if ($fromCategory !== $toCategory) {
return ExcelError::NA();
}
// @var float $value
$value *= $fromMultiplier;
if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) {
// We've already factored $fromMultiplier into the value, so we need
// to reverse it again
return $value / $fromMultiplier;
} elseif ($fromUOM === $toUOM) {
return $value / $toMultiplier;
} elseif ($fromCategory === self::CATEGORY_TEMPERATURE) {
return self::convertTemperature($fromUOM, $toUOM, /** @scrutinizer ignore-type */ $value);
}
$baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]);
return ($baseValue * self::$unitConversions[$fromCategory][$toUOM]) / $toMultiplier;
}
private static function getUOMDetails(string $uom)
{
if (isset(self::$conversionUnits[$uom])) {
$unitCategory = self::$conversionUnits[$uom]['Group'];
return [$uom, $unitCategory, 1.0];
}
// Check 1-character standard metric multiplier prefixes
$multiplierType = substr($uom, 0, 1);
$uom = substr($uom, 1);
if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) {
if (self::$conversionUnits[$uom]['AllowPrefix'] === false) {
throw new Exception('Prefix not allowed for UoM');
}
$unitCategory = self::$conversionUnits[$uom]['Group'];
return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']];
}
$multiplierType .= substr($uom, 0, 1);
$uom = substr($uom, 1);
// Check 2-character standard metric multiplier prefixes
if (isset(self::$conversionUnits[$uom], self::$conversionMultipliers[$multiplierType])) {
if (self::$conversionUnits[$uom]['AllowPrefix'] === false) {
throw new Exception('Prefix not allowed for UoM');
}
$unitCategory = self::$conversionUnits[$uom]['Group'];
return [$uom, $unitCategory, self::$conversionMultipliers[$multiplierType]['multiplier']];
}
// Check 2-character binary multiplier prefixes
if (isset(self::$conversionUnits[$uom], self::$binaryConversionMultipliers[$multiplierType])) {
if (self::$conversionUnits[$uom]['AllowPrefix'] === false) {
throw new Exception('Prefix not allowed for UoM');
}
$unitCategory = self::$conversionUnits[$uom]['Group'];
if ($unitCategory !== 'Information') {
throw new Exception('Binary Prefix is only allowed for Information UoM');
}
return [$uom, $unitCategory, self::$binaryConversionMultipliers[$multiplierType]['multiplier']];
}
throw new Exception('UoM Not Found');
}
/**
* @param float|int $value
*
* @return float|int
*/
protected static function convertTemperature(string $fromUOM, string $toUOM, $value)
{
$fromUOM = self::resolveTemperatureSynonyms($fromUOM);
$toUOM = self::resolveTemperatureSynonyms($toUOM);
if ($fromUOM === $toUOM) {
return $value;
}
// Convert to Kelvin
switch ($fromUOM) {
case 'F':
$value = ($value - 32) / 1.8 + 273.15;
break;
case 'C':
$value += 273.15;
break;
case 'Rank':
$value /= 1.8;
break;
case 'Reau':
$value = $value * 1.25 + 273.15;
break;
}
// Convert from Kelvin
switch ($toUOM) {
case 'F':
$value = ($value - 273.15) * 1.8 + 32.00;
break;
case 'C':
$value -= 273.15;
break;
case 'Rank':
$value *= 1.8;
break;
case 'Reau':
$value = ($value - 273.15) * 0.80000;
break;
}
return $value;
}
private static function resolveTemperatureSynonyms(string $uom)
{
switch ($uom) {
case 'fah':
return 'F';
case 'cel':
return 'C';
case 'kel':
return 'K';
}
return $uom;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class EngineeringValidations
{
/**
* @param mixed $value
*/
public static function validateFloat($value): float
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (float) $value;
}
/**
* @param mixed $value
*/
public static function validateInt($value): int
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (int) floor((float) $value);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Erf
{
use ArrayEnabled;
private static $twoSqrtPi = 1.128379167095512574;
/**
* ERF.
*
* Returns the error function integrated between the lower and upper bound arguments.
*
* Note: In Excel 2007 or earlier, if you input a negative value for the upper or lower bound arguments,
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
* improved, so that it can now calculate the function for both positive and negative ranges.
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments.
*
* Excel Function:
* ERF(lower[,upper])
*
* @param mixed $lower Lower bound float for integrating ERF
* Or can be an array of values
* @param mixed $upper Upper bound float for integrating ERF.
* If omitted, ERF integrates between zero and lower_limit
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function ERF($lower, $upper = null)
{
if (is_array($lower) || is_array($upper)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $lower, $upper);
}
if (is_numeric($lower)) {
if ($upper === null) {
return self::erfValue($lower);
}
if (is_numeric($upper)) {
return self::erfValue($upper) - self::erfValue($lower);
}
}
return ExcelError::VALUE();
}
/**
* ERFPRECISE.
*
* Returns the error function integrated between the lower and upper bound arguments.
*
* Excel Function:
* ERF.PRECISE(limit)
*
* @param mixed $limit Float bound for integrating ERF, other bound is zero
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function ERFPRECISE($limit)
{
if (is_array($limit)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $limit);
}
return self::ERF($limit);
}
//
// Private method to calculate the erf value
//
public static function erfValue($value)
{
if (abs($value) > 2.2) {
return 1 - ErfC::ERFC($value);
}
$sum = $term = $value;
$xsqr = ($value * $value);
$j = 1;
do {
$term *= $xsqr / $j;
$sum -= $term / (2 * $j + 1);
++$j;
$term *= $xsqr / $j;
$sum += $term / (2 * $j + 1);
++$j;
if ($sum == 0.0) {
break;
}
} while (abs($term / $sum) > Functions::PRECISION);
return self::$twoSqrtPi * $sum;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ErfC
{
use ArrayEnabled;
/**
* ERFC.
*
* Returns the complementary ERF function integrated between x and infinity
*
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument,
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
* improved, so that it can now calculate the function for both positive and negative x values.
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments.
*
* Excel Function:
* ERFC(x)
*
* @param mixed $value The float lower bound for integrating ERFC
* Or can be an array of values
*
* @return array|float|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function ERFC($value)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (is_numeric($value)) {
return self::erfcValue($value);
}
return ExcelError::VALUE();
}
//
// Private method to calculate the erfc value
//
private static $oneSqrtPi = 0.564189583547756287;
private static function erfcValue($value)
{
if (abs($value) < 2.2) {
return 1 - Erf::erfValue($value);
}
if ($value < 0) {
return 2 - self::erfcValue(-$value);
}
$a = $n = 1;
$b = $c = $value;
$d = ($value * $value) + 0.5;
$q2 = $b / $d;
do {
$t = $a * $n + $b * $value;
$a = $b;
$b = $t;
$t = $c * $n + $d * $value;
$c = $d;
$d = $t;
$n += 0.5;
$q1 = $q2;
$q2 = $b / $d;
} while ((abs($q1 - $q2) / $q2) > Functions::PRECISION);
return self::$oneSqrtPi * exp(-$value * $value) * $q2;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
class Exception extends PhpSpreadsheetException
{
/**
* Error handler callback.
*
* @param mixed $code
* @param mixed $string
* @param mixed $file
* @param mixed $line
* @param mixed $context
*/
public static function errorHandlerCallback($code, $string, $file, $line, /** @scrutinizer ignore-unused */ $context): void
{
$e = new self($string, $code);
$e->line = $line;
$e->file = $file;
throw $e;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
class ExceptionHandler
{
/**
* Register errorhandler.
*/
public function __construct()
{
/** @var callable */
$callable = [Exception::class, 'errorHandlerCallback'];
set_error_handler($callable, E_ALL);
}
/**
* Unregister errorhandler.
*/
public function __destruct()
{
restore_error_handler();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Amortization
{
/**
* AMORDEGRC.
*
* Returns the depreciation for each accounting period.
* This function is provided for the French accounting system. If an asset is purchased in
* the middle of the accounting period, the prorated depreciation is taken into account.
* The function is similar to AMORLINC, except that a depreciation coefficient is applied in
* the calculation depending on the life of the assets.
* This function will return the depreciation until the last period of the life of the assets
* or until the cumulated value of depreciation is greater than the cost of the assets minus
* the salvage value.
*
* Excel Function:
* AMORDEGRC(cost,purchased,firstPeriod,salvage,period,rate[,basis])
*
* @param mixed $cost The float cost of the asset
* @param mixed $purchased Date of the purchase of the asset
* @param mixed $firstPeriod Date of the end of the first period
* @param mixed $salvage The salvage value at the end of the life of the asset
* @param mixed $period the period (float)
* @param mixed $rate rate of depreciation (float)
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string (string containing the error type if there is an error)
*/
public static function AMORDEGRC(
$cost,
$purchased,
$firstPeriod,
$salvage,
$period,
$rate,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$cost = Functions::flattenSingleValue($cost);
$purchased = Functions::flattenSingleValue($purchased);
$firstPeriod = Functions::flattenSingleValue($firstPeriod);
$salvage = Functions::flattenSingleValue($salvage);
$period = Functions::flattenSingleValue($period);
$rate = Functions::flattenSingleValue($rate);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$cost = FinancialValidations::validateFloat($cost);
$purchased = FinancialValidations::validateDate($purchased);
$firstPeriod = FinancialValidations::validateDate($firstPeriod);
$salvage = FinancialValidations::validateFloat($salvage);
$period = FinancialValidations::validateInt($period);
$rate = FinancialValidations::validateFloat($rate);
$basis = FinancialValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis);
if (is_string($yearFracx)) {
return $yearFracx;
}
/** @var float */
$yearFrac = $yearFracx;
$amortiseCoeff = self::getAmortizationCoefficient($rate);
$rate *= $amortiseCoeff;
$fNRate = round($yearFrac * $rate * $cost, 0);
$cost -= $fNRate;
$fRest = $cost - $salvage;
for ($n = 0; $n < $period; ++$n) {
$fNRate = round($rate * $cost, 0);
$fRest -= $fNRate;
if ($fRest < 0.0) {
switch ($period - $n) {
case 0:
case 1:
return round($cost * 0.5, 0);
default:
return 0.0;
}
}
$cost -= $fNRate;
}
return $fNRate;
}
/**
* AMORLINC.
*
* Returns the depreciation for each accounting period.
* This function is provided for the French accounting system. If an asset is purchased in
* the middle of the accounting period, the prorated depreciation is taken into account.
*
* Excel Function:
* AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis])
*
* @param mixed $cost The cost of the asset as a float
* @param mixed $purchased Date of the purchase of the asset
* @param mixed $firstPeriod Date of the end of the first period
* @param mixed $salvage The salvage value at the end of the life of the asset
* @param mixed $period The period as a float
* @param mixed $rate Rate of depreciation as float
* @param mixed $basis Integer indicating the type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string (string containing the error type if there is an error)
*/
public static function AMORLINC(
$cost,
$purchased,
$firstPeriod,
$salvage,
$period,
$rate,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$cost = Functions::flattenSingleValue($cost);
$purchased = Functions::flattenSingleValue($purchased);
$firstPeriod = Functions::flattenSingleValue($firstPeriod);
$salvage = Functions::flattenSingleValue($salvage);
$period = Functions::flattenSingleValue($period);
$rate = Functions::flattenSingleValue($rate);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$cost = FinancialValidations::validateFloat($cost);
$purchased = FinancialValidations::validateDate($purchased);
$firstPeriod = FinancialValidations::validateDate($firstPeriod);
$salvage = FinancialValidations::validateFloat($salvage);
$period = FinancialValidations::validateFloat($period);
$rate = FinancialValidations::validateFloat($rate);
$basis = FinancialValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$fOneRate = $cost * $rate;
$fCostDelta = $cost - $salvage;
// Note, quirky variation for leap years on the YEARFRAC for this function
$purchasedYear = DateTimeExcel\DateParts::year($purchased);
$yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis);
if (is_string($yearFracx)) {
return $yearFracx;
}
/** @var float */
$yearFrac = $yearFracx;
if (
($basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) &&
($yearFrac < 1) && (Functions::scalar(DateTimeExcel\Helpers::isLeapYear($purchasedYear)))
) {
$yearFrac *= 365 / 366;
}
$f0Rate = $yearFrac * $rate * $cost;
$nNumOfFullPeriods = (int) (($cost - $salvage - $f0Rate) / $fOneRate);
if ($period == 0) {
return $f0Rate;
} elseif ($period <= $nNumOfFullPeriods) {
return $fOneRate;
} elseif ($period == ($nNumOfFullPeriods + 1)) {
return $fCostDelta - $fOneRate * $nNumOfFullPeriods - $f0Rate;
}
return 0.0;
}
private static function getAmortizationCoefficient(float $rate): float
{
// The depreciation coefficients are:
// Life of assets (1/rate) Depreciation coefficient
// Less than 3 years 1
// Between 3 and 4 years 1.5
// Between 5 and 6 years 2
// More than 6 years 2.5
$fUsePer = 1.0 / $rate;
if ($fUsePer < 3.0) {
return 1.0;
} elseif ($fUsePer < 4.0) {
return 1.5;
} elseif ($fUsePer <= 6.0) {
return 2.0;
}
return 2.5;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class CashFlowValidations extends FinancialValidations
{
/**
* @param mixed $rate
*/
public static function validateRate($rate): float
{
$rate = self::validateFloat($rate);
return $rate;
}
/**
* @param mixed $type
*/
public static function validatePeriodType($type): int
{
$rate = self::validateInt($type);
if (
$type !== FinancialConstants::PAYMENT_END_OF_PERIOD &&
$type !== FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD
) {
throw new Exception(ExcelError::NAN());
}
return $rate;
}
/**
* @param mixed $presentValue
*/
public static function validatePresentValue($presentValue): float
{
return self::validateFloat($presentValue);
}
/**
* @param mixed $futureValue
*/
public static function validateFutureValue($futureValue): float
{
return self::validateFloat($futureValue);
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Periodic
{
/**
* FV.
*
* Returns the Future Value of a cash flow with constant payments and interest rate (annuities).
*
* Excel Function:
* FV(rate,nper,pmt[,pv[,type]])
*
* @param mixed $rate The interest rate per period
* @param mixed $numberOfPeriods Total number of payment periods in an annuity as an integer
* @param mixed $payment The payment made each period: it cannot change over the
* life of the annuity. Typically, pmt contains principal
* and interest but no other fees or taxes.
* @param mixed $presentValue present Value, or the lump-sum amount that a series of
* future payments is worth right now
* @param mixed $type A number 0 or 1 and indicates when payments are due:
* 0 or omitted At the end of the period.
* 1 At the beginning of the period.
*
* @return float|string
*/
public static function futureValue(
$rate,
$numberOfPeriods,
$payment = 0.0,
$presentValue = 0.0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$rate = Functions::flattenSingleValue($rate);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment);
$presentValue = ($presentValue === null) ? 0.0 : Functions::flattenSingleValue($presentValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$rate = CashFlowValidations::validateRate($rate);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$payment = CashFlowValidations::validateFloat($payment);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
return self::calculateFutureValue($rate, $numberOfPeriods, $payment, $presentValue, $type);
}
/**
* PV.
*
* Returns the Present Value of a cash flow with constant payments and interest rate (annuities).
*
* @param mixed $rate Interest rate per period
* @param mixed $numberOfPeriods Number of periods as an integer
* @param mixed $payment Periodic payment (annuity)
* @param mixed $futureValue Future Value
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period
*
* @return float|string Result, or a string containing an error
*/
public static function presentValue(
$rate,
$numberOfPeriods,
$payment = 0.0,
$futureValue = 0.0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$rate = Functions::flattenSingleValue($rate);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$rate = CashFlowValidations::validateRate($rate);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$payment = CashFlowValidations::validateFloat($payment);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($numberOfPeriods < 0) {
return ExcelError::NAN();
}
return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type);
}
/**
* NPER.
*
* Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate.
*
* @param mixed $rate Interest rate per period
* @param mixed $payment Periodic payment (annuity)
* @param mixed $presentValue Present Value
* @param mixed $futureValue Future Value
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period
*
* @return float|string Result, or a string containing an error
*/
public static function periods(
$rate,
$payment,
$presentValue,
$futureValue = 0.0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$rate = Functions::flattenSingleValue($rate);
$payment = Functions::flattenSingleValue($payment);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$rate = CashFlowValidations::validateRate($rate);
$payment = CashFlowValidations::validateFloat($payment);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($payment == 0.0) {
return ExcelError::NAN();
}
return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type);
}
private static function calculateFutureValue(
float $rate,
int $numberOfPeriods,
float $payment,
float $presentValue,
int $type
): float {
if ($rate !== null && $rate != 0) {
return -$presentValue *
(1 + $rate) ** $numberOfPeriods - $payment * (1 + $rate * $type) * ((1 + $rate) ** $numberOfPeriods - 1)
/ $rate;
}
return -$presentValue - $payment * $numberOfPeriods;
}
private static function calculatePresentValue(
float $rate,
int $numberOfPeriods,
float $payment,
float $futureValue,
int $type
): float {
if ($rate != 0.0) {
return (-$payment * (1 + $rate * $type)
* (((1 + $rate) ** $numberOfPeriods - 1) / $rate) - $futureValue) / (1 + $rate) ** $numberOfPeriods;
}
return -$futureValue - $payment * $numberOfPeriods;
}
/**
* @return float|string
*/
private static function calculatePeriods(
float $rate,
float $payment,
float $presentValue,
float $futureValue,
int $type
) {
if ($rate != 0.0) {
if ($presentValue == 0.0) {
return ExcelError::NAN();
}
return log(($payment * (1 + $rate * $type) / $rate - $futureValue) /
($presentValue + $payment * (1 + $rate * $type) / $rate)) / log(1 + $rate);
}
return (-$presentValue - $futureValue) / $payment;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Cumulative
{
/**
* CUMIPMT.
*
* Returns the cumulative interest paid on a loan between the start and end periods.
*
* Excel Function:
* CUMIPMT(rate,nper,pv,start,end[,type])
*
* @param mixed $rate The Interest rate
* @param mixed $periods The total number of payment periods
* @param mixed $presentValue Present Value
* @param mixed $start The first period in the calculation.
* Payment periods are numbered beginning with 1.
* @param mixed $end the last period in the calculation
* @param mixed $type A number 0 or 1 and indicates when payments are due:
* 0 or omitted At the end of the period.
* 1 At the beginning of the period.
*
* @return float|string
*/
public static function interest(
$rate,
$periods,
$presentValue,
$start,
$end,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$rate = Functions::flattenSingleValue($rate);
$periods = Functions::flattenSingleValue($periods);
$presentValue = Functions::flattenSingleValue($presentValue);
$start = Functions::flattenSingleValue($start);
$end = Functions::flattenSingleValue($end);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$rate = CashFlowValidations::validateRate($rate);
$periods = CashFlowValidations::validateInt($periods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$start = CashFlowValidations::validateInt($start);
$end = CashFlowValidations::validateInt($end);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($start < 1 || $start > $end) {
return ExcelError::NAN();
}
// Calculate
$interest = 0;
for ($per = $start; $per <= $end; ++$per) {
$ipmt = Interest::payment($rate, $per, $periods, $presentValue, 0, $type);
if (is_string($ipmt)) {
return $ipmt;
}
$interest += $ipmt;
}
return $interest;
}
/**
* CUMPRINC.
*
* Returns the cumulative principal paid on a loan between the start and end periods.
*
* Excel Function:
* CUMPRINC(rate,nper,pv,start,end[,type])
*
* @param mixed $rate The Interest rate
* @param mixed $periods The total number of payment periods as an integer
* @param mixed $presentValue Present Value
* @param mixed $start The first period in the calculation.
* Payment periods are numbered beginning with 1.
* @param mixed $end the last period in the calculation
* @param mixed $type A number 0 or 1 and indicates when payments are due:
* 0 or omitted At the end of the period.
* 1 At the beginning of the period.
*
* @return float|string
*/
public static function principal(
$rate,
$periods,
$presentValue,
$start,
$end,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$rate = Functions::flattenSingleValue($rate);
$periods = Functions::flattenSingleValue($periods);
$presentValue = Functions::flattenSingleValue($presentValue);
$start = Functions::flattenSingleValue($start);
$end = Functions::flattenSingleValue($end);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$rate = CashFlowValidations::validateRate($rate);
$periods = CashFlowValidations::validateInt($periods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$start = CashFlowValidations::validateInt($start);
$end = CashFlowValidations::validateInt($end);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($start < 1 || $start > $end) {
return ExcelError::VALUE();
}
// Calculate
$principal = 0;
for ($per = $start; $per <= $end; ++$per) {
$ppmt = Payments::interestPayment($rate, $per, $periods, $presentValue, 0, $type);
if (is_string($ppmt)) {
return $ppmt;
}
$principal += $ppmt;
}
return $principal;
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Interest
{
private const FINANCIAL_MAX_ITERATIONS = 128;
private const FINANCIAL_PRECISION = 1.0e-08;
/**
* IPMT.
*
* Returns the interest payment for a given period for an investment based on periodic, constant payments
* and a constant interest rate.
*
* Excel Function:
* IPMT(rate,per,nper,pv[,fv][,type])
*
* @param mixed $interestRate Interest rate per period
* @param mixed $period Period for which we want to find the interest
* @param mixed $numberOfPeriods Number of periods
* @param mixed $presentValue Present Value
* @param mixed $futureValue Future Value
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period
*
* @return float|string
*/
public static function payment(
$interestRate,
$period,
$numberOfPeriods,
$presentValue,
$futureValue = 0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$interestRate = Functions::flattenSingleValue($interestRate);
$period = Functions::flattenSingleValue($period);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$interestRate = CashFlowValidations::validateRate($interestRate);
$period = CashFlowValidations::validateInt($period);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($period <= 0 || $period > $numberOfPeriods) {
return ExcelError::NAN();
}
// Calculate
$interestAndPrincipal = new InterestAndPrincipal(
$interestRate,
$period,
$numberOfPeriods,
$presentValue,
$futureValue,
$type
);
return $interestAndPrincipal->interest();
}
/**
* ISPMT.
*
* Returns the interest payment for an investment based on an interest rate and a constant payment schedule.
*
* Excel Function:
* =ISPMT(interest_rate, period, number_payments, pv)
*
* @param mixed $interestRate is the interest rate for the investment
* @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments.
* @param mixed $numberOfPeriods is the number of payments for the annuity
* @param mixed $principleRemaining is the loan amount or present value of the payments
*
* @return float|string
*/
public static function schedulePayment($interestRate, $period, $numberOfPeriods, $principleRemaining)
{
$interestRate = Functions::flattenSingleValue($interestRate);
$period = Functions::flattenSingleValue($period);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$principleRemaining = Functions::flattenSingleValue($principleRemaining);
try {
$interestRate = CashFlowValidations::validateRate($interestRate);
$period = CashFlowValidations::validateInt($period);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$principleRemaining = CashFlowValidations::validateFloat($principleRemaining);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($period <= 0 || $period > $numberOfPeriods) {
return ExcelError::NAN();
}
// Return value
$returnValue = 0;
// Calculate
$principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0);
for ($i = 0; $i <= $period; ++$i) {
$returnValue = $interestRate * $principleRemaining * -1;
$principleRemaining -= $principlePayment;
// principle needs to be 0 after the last payment, don't let floating point screw it up
if ($i == $numberOfPeriods) {
$returnValue = 0.0;
}
}
return $returnValue;
}
/**
* RATE.
*
* Returns the interest rate per period of an annuity.
* RATE is calculated by iteration and can have zero or more solutions.
* If the successive results of RATE do not converge to within 0.0000001 after 20 iterations,
* RATE returns the #NUM! error value.
*
* Excel Function:
* RATE(nper,pmt,pv[,fv[,type[,guess]]])
*
* @param mixed $numberOfPeriods The total number of payment periods in an annuity
* @param mixed $payment The payment made each period and cannot change over the life of the annuity.
* Typically, pmt includes principal and interest but no other fees or taxes.
* @param mixed $presentValue The present value - the total amount that a series of future payments is worth now
* @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made.
* If fv is omitted, it is assumed to be 0 (the future value of a loan,
* for example, is 0).
* @param mixed $type A number 0 or 1 and indicates when payments are due:
* 0 or omitted At the end of the period.
* 1 At the beginning of the period.
* @param mixed $guess Your guess for what the rate will be.
* If you omit guess, it is assumed to be 10 percent.
*
* @return float|string
*/
public static function rate(
$numberOfPeriods,
$payment,
$presentValue,
$futureValue = 0.0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD,
$guess = 0.1
) {
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$payment = Functions::flattenSingleValue($payment);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
$guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess);
try {
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$payment = CashFlowValidations::validateFloat($payment);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
$guess = CashFlowValidations::validateFloat($guess);
} catch (Exception $e) {
return $e->getMessage();
}
$rate = $guess;
// rest of code adapted from python/numpy
$close = false;
$iter = 0;
while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) {
$nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type);
if (!is_numeric($nextdiff)) {
break;
}
$rate1 = $rate - $nextdiff;
$close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION;
++$iter;
$rate = $rate1;
}
return $close ? $rate : ExcelError::NAN();
}
private static function rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type)
{
if ($rate == 0.0) {
return ExcelError::NAN();
}
$tt1 = ($rate + 1) ** $numberOfPeriods;
$tt2 = ($rate + 1) ** ($numberOfPeriods - 1);
$numerator = $futureValue + $tt1 * $presentValue + $payment * ($tt1 - 1) * ($rate * $type + 1) / $rate;
$denominator = $numberOfPeriods * $tt2 * $presentValue - $payment * ($tt1 - 1)
* ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods
* $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate;
if ($denominator == 0) {
return ExcelError::NAN();
}
return $numerator / $denominator;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
class InterestAndPrincipal
{
protected $interest;
protected $principal;
public function __construct(
float $rate = 0.0,
int $period = 0,
int $numberOfPeriods = 0,
float $presentValue = 0,
float $futureValue = 0,
int $type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$payment = Payments::annuity($rate, $numberOfPeriods, $presentValue, $futureValue, $type);
$capital = $presentValue;
$interest = 0.0;
$principal = 0.0;
for ($i = 1; $i <= $period; ++$i) {
$interest = ($type === FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD && $i == 1) ? 0 : -$capital * $rate;
$principal = $payment - $interest;
$capital += $principal;
}
$this->interest = $interest;
$this->principal = $principal;
}
public function interest(): float
{
return $this->interest;
}
public function principal(): float
{
return $this->principal;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Payments
{
/**
* PMT.
*
* Returns the constant payment (annuity) for a cash flow with a constant interest rate.
*
* @param mixed $interestRate Interest rate per period
* @param mixed $numberOfPeriods Number of periods
* @param mixed $presentValue Present Value
* @param mixed $futureValue Future Value
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period
*
* @return float|string Result, or a string containing an error
*/
public static function annuity(
$interestRate,
$numberOfPeriods,
$presentValue,
$futureValue = 0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$interestRate = Functions::flattenSingleValue($interestRate);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$interestRate = CashFlowValidations::validateRate($interestRate);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Calculate
if ($interestRate != 0.0) {
return (-$futureValue - $presentValue * (1 + $interestRate) ** $numberOfPeriods) /
(1 + $interestRate * $type) / (((1 + $interestRate) ** $numberOfPeriods - 1) / $interestRate);
}
return (-$presentValue - $futureValue) / $numberOfPeriods;
}
/**
* PPMT.
*
* Returns the interest payment for a given period for an investment based on periodic, constant payments
* and a constant interest rate.
*
* @param mixed $interestRate Interest rate per period
* @param mixed $period Period for which we want to find the interest
* @param mixed $numberOfPeriods Number of periods
* @param mixed $presentValue Present Value
* @param mixed $futureValue Future Value
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period
*
* @return float|string Result, or a string containing an error
*/
public static function interestPayment(
$interestRate,
$period,
$numberOfPeriods,
$presentValue,
$futureValue = 0,
$type = FinancialConstants::PAYMENT_END_OF_PERIOD
) {
$interestRate = Functions::flattenSingleValue($interestRate);
$period = Functions::flattenSingleValue($period);
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue);
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type);
try {
$interestRate = CashFlowValidations::validateRate($interestRate);
$period = CashFlowValidations::validateInt($period);
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
$type = CashFlowValidations::validatePeriodType($type);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($period <= 0 || $period > $numberOfPeriods) {
return ExcelError::NAN();
}
// Calculate
$interestAndPrincipal = new InterestAndPrincipal(
$interestRate,
$period,
$numberOfPeriods,
$presentValue,
$futureValue,
$type
);
return $interestAndPrincipal->principal();
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Single
{
/**
* FVSCHEDULE.
*
* Returns the future value of an initial principal after applying a series of compound interest rates.
* Use FVSCHEDULE to calculate the future value of an investment with a variable or adjustable rate.
*
* Excel Function:
* FVSCHEDULE(principal,schedule)
*
* @param mixed $principal the present value
* @param float[] $schedule an array of interest rates to apply
*
* @return float|string
*/
public static function futureValue($principal, $schedule)
{
$principal = Functions::flattenSingleValue($principal);
$schedule = Functions::flattenArray($schedule);
try {
$principal = CashFlowValidations::validateFloat($principal);
foreach ($schedule as $rate) {
$rate = CashFlowValidations::validateFloat($rate);
$principal *= 1 + $rate;
}
} catch (Exception $e) {
return $e->getMessage();
}
return $principal;
}
/**
* PDURATION.
*
* Calculates the number of periods required for an investment to reach a specified value.
*
* @param mixed $rate Interest rate per period
* @param mixed $presentValue Present Value
* @param mixed $futureValue Future Value
*
* @return float|string Result, or a string containing an error
*/
public static function periods($rate, $presentValue, $futureValue)
{
$rate = Functions::flattenSingleValue($rate);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = Functions::flattenSingleValue($futureValue);
try {
$rate = CashFlowValidations::validateRate($rate);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) {
return ExcelError::NAN();
}
return (log($futureValue) - log($presentValue)) / log(1 + $rate);
}
/**
* RRI.
*
* Calculates the interest rate required for an investment to grow to a specified future value .
*
* @param float $periods The number of periods over which the investment is made
* @param float $presentValue Present Value
* @param float $futureValue Future Value
*
* @return float|string Result, or a string containing an error
*/
public static function interestRate($periods = 0.0, $presentValue = 0.0, $futureValue = 0.0)
{
$periods = Functions::flattenSingleValue($periods);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = Functions::flattenSingleValue($futureValue);
try {
$periods = CashFlowValidations::validateFloat($periods);
$presentValue = CashFlowValidations::validatePresentValue($presentValue);
$futureValue = CashFlowValidations::validateFutureValue($futureValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) {
return ExcelError::NAN();
}
return ($futureValue / $presentValue) ** (1 / $periods) - 1;
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class NonPeriodic
{
const FINANCIAL_MAX_ITERATIONS = 128;
const FINANCIAL_PRECISION = 1.0e-08;
const DEFAULT_GUESS = 0.1;
/**
* XIRR.
*
* Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic.
*
* Excel Function:
* =XIRR(values,dates,guess)
*
* @param float[] $values A series of cash flow payments
* The series of values must contain at least one positive value & one negative value
* @param mixed[] $dates A series of payment dates
* The first payment date indicates the beginning of the schedule of payments
* All other dates must be later than this date, but they may occur in any order
* @param mixed $guess An optional guess at the expected answer
*
* @return float|string
*/
public static function rate($values, $dates, $guess = self::DEFAULT_GUESS)
{
$rslt = self::xirrPart1($values, $dates);
if ($rslt !== '') {
return $rslt;
}
// create an initial range, with a root somewhere between 0 and guess
$guess = Functions::flattenSingleValue($guess) ?? self::DEFAULT_GUESS;
if (!is_numeric($guess)) {
return ExcelError::VALUE();
}
$guess = ($guess + 0.0) ?: self::DEFAULT_GUESS;
$x1 = 0.0;
$x2 = $guess + 0.0;
$f1 = self::xnpvOrdered($x1, $values, $dates, false);
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
$found = false;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
$f1 = (float) $f1;
$f2 = (float) $f2;
if (($f1 * $f2) < 0.0) {
$found = true;
break;
} elseif (abs($f1) < abs($f2)) {
$x1 += 1.6 * ($x1 - $x2);
$f1 = self::xnpvOrdered($x1, $values, $dates, false);
} else {
$x2 += 1.6 * ($x2 - $x1);
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
}
}
if (!$found) {
return ExcelError::NAN();
}
return self::xirrPart3($values, $dates, $x1, $x2);
}
/**
* XNPV.
*
* Returns the net present value for a schedule of cash flows that is not necessarily periodic.
* To calculate the net present value for a series of cash flows that is periodic, use the NPV function.
*
* Excel Function:
* =XNPV(rate,values,dates)
*
* @param float $rate the discount rate to apply to the cash flows
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates.
* The first payment is optional and corresponds to a cost or payment that occurs
* at the beginning of the investment.
* If the first value is a cost or payment, it must be a negative value.
* All succeeding payments are discounted based on a 365-day year.
* The series of values must contain at least one positive value and one negative value.
* @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments.
* The first payment date indicates the beginning of the schedule of payments.
* All other dates must be later than this date, but they may occur in any order.
*
* @return float|string
*/
public static function presentValue($rate, $values, $dates)
{
return self::xnpvOrdered($rate, $values, $dates, true);
}
private static function bothNegAndPos(bool $neg, bool $pos): bool
{
return $neg && $pos;
}
/**
* @param mixed $values
* @param mixed $dates
*/
private static function xirrPart1(&$values, &$dates): string
{
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
$valuesIsArray = count($values) > 1;
$datesIsArray = count($dates) > 1;
if (!$valuesIsArray && !$datesIsArray) {
return ExcelError::NA();
}
if (count($values) != count($dates)) {
return ExcelError::NAN();
}
$datesCount = count($dates);
for ($i = 0; $i < $datesCount; ++$i) {
try {
$dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
}
return self::xirrPart2($values);
}
private static function xirrPart2(array &$values): string
{
$valCount = count($values);
$foundpos = false;
$foundneg = false;
for ($i = 0; $i < $valCount; ++$i) {
$fld = $values[$i];
if (!is_numeric($fld)) {
return ExcelError::VALUE();
} elseif ($fld > 0) {
$foundpos = true;
} elseif ($fld < 0) {
$foundneg = true;
}
}
if (!self::bothNegAndPos($foundneg, $foundpos)) {
return ExcelError::NAN();
}
return '';
}
/**
* @return float|string
*/
private static function xirrPart3(array $values, array $dates, float $x1, float $x2)
{
$f = self::xnpvOrdered($x1, $values, $dates, false);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
$rslt = ExcelError::VALUE();
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = (float) self::xnpvOrdered($x_mid, $values, $dates, false);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
$rslt = $x_mid;
break;
}
}
return $rslt;
}
/**
* @param mixed $rate
* @param mixed $values
* @param mixed $dates
*
* @return float|string
*/
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true)
{
$rate = Functions::flattenSingleValue($rate);
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
$valCount = count($values);
try {
self::validateXnpv($rate, $values, $dates);
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
} catch (Exception $e) {
return $e->getMessage();
}
$xnpv = 0.0;
for ($i = 0; $i < $valCount; ++$i) {
if (!is_numeric($values[$i])) {
return ExcelError::VALUE();
}
try {
$datei = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
if ($date0 > $datei) {
$dif = $ordered ? ExcelError::NAN() : -((int) DateTimeExcel\Difference::interval($datei, $date0, 'd'));
} else {
$dif = DateTimeExcel\Difference::interval($date0, $datei, 'd');
}
if (!is_numeric($dif)) {
return $dif;
}
if ($rate <= -1.0) {
$xnpv += -abs($values[$i]) / (-1 - $rate) ** ($dif / 365);
} else {
$xnpv += $values[$i] / (1 + $rate) ** ($dif / 365);
}
}
return is_finite($xnpv) ? $xnpv : ExcelError::VALUE();
}
/**
* @param mixed $rate
*/
private static function validateXnpv($rate, array $values, array $dates): void
{
if (!is_numeric($rate)) {
throw new Exception(ExcelError::VALUE());
}
$valCount = count($values);
if ($valCount != count($dates)) {
throw new Exception(ExcelError::NAN());
}
if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) {
throw new Exception(ExcelError::NAN());
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Periodic
{
const FINANCIAL_MAX_ITERATIONS = 128;
const FINANCIAL_PRECISION = 1.0e-08;
/**
* IRR.
*
* Returns the internal rate of return for a series of cash flows represented by the numbers in values.
* These cash flows do not have to be even, as they would be for an annuity. However, the cash flows must occur
* at regular intervals, such as monthly or annually. The internal rate of return is the interest rate received
* for an investment consisting of payments (negative values) and income (positive values) that occur at regular
* periods.
*
* Excel Function:
* IRR(values[,guess])
*
* @param mixed $values An array or a reference to cells that contain numbers for which you want
* to calculate the internal rate of return.
* Values must contain at least one positive value and one negative value to
* calculate the internal rate of return.
* @param mixed $guess A number that you guess is close to the result of IRR
*
* @return float|string
*/
public static function rate($values, $guess = 0.1)
{
if (!is_array($values)) {
return ExcelError::VALUE();
}
$values = Functions::flattenArray($values);
$guess = Functions::flattenSingleValue($guess);
// create an initial range, with a root somewhere between 0 and guess
$x1 = 0.0;
$x2 = $guess;
$f1 = self::presentValue($x1, $values);
$f2 = self::presentValue($x2, $values);
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (($f1 * $f2) < 0.0) {
break;
}
if (abs($f1) < abs($f2)) {
$f1 = self::presentValue($x1 += 1.6 * ($x1 - $x2), $values);
} else {
$f2 = self::presentValue($x2 += 1.6 * ($x2 - $x1), $values);
}
}
if (($f1 * $f2) > 0.0) {
return ExcelError::VALUE();
}
$f = self::presentValue($x1, $values);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = self::presentValue($x_mid, $values);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
return $x_mid;
}
}
return ExcelError::VALUE();
}
/**
* MIRR.
*
* Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both
* the cost of the investment and the interest received on reinvestment of cash.
*
* Excel Function:
* MIRR(values,finance_rate, reinvestment_rate)
*
* @param mixed $values An array or a reference to cells that contain a series of payments and
* income occurring at regular intervals.
* Payments are negative value, income is positive values.
* @param mixed $financeRate The interest rate you pay on the money used in the cash flows
* @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them
*
* @return float|string Result, or a string containing an error
*/
public static function modifiedRate($values, $financeRate, $reinvestmentRate)
{
if (!is_array($values)) {
return ExcelError::DIV0();
}
$values = Functions::flattenArray($values);
$financeRate = Functions::flattenSingleValue($financeRate);
$reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate);
$n = count($values);
$rr = 1.0 + $reinvestmentRate;
$fr = 1.0 + $financeRate;
$npvPos = $npvNeg = self::$zeroPointZero;
foreach ($values as $i => $v) {
if ($v >= 0) {
$npvPos += $v / $rr ** $i;
} else {
$npvNeg += $v / $fr ** $i;
}
}
if ($npvNeg === self::$zeroPointZero || $npvPos === self::$zeroPointZero) {
return ExcelError::DIV0();
}
$mirr = ((-$npvPos * $rr ** $n)
/ ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0;
return is_finite($mirr) ? $mirr : ExcelError::NAN();
}
/**
* Sop to Scrutinizer.
*
* @var float
*/
private static $zeroPointZero = 0.0;
/**
* NPV.
*
* Returns the Net Present Value of a cash flow series given a discount rate.
*
* @param mixed $rate
*
* @return float
*/
public static function presentValue($rate, ...$args)
{
$returnValue = 0;
$rate = Functions::flattenSingleValue($rate);
$aArgs = Functions::flattenArray($args);
// Calculate
$countArgs = count($aArgs);
for ($i = 1; $i <= $countArgs; ++$i) {
// Is it a numeric value?
if (is_numeric($aArgs[$i - 1])) {
$returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i;
}
}
return $returnValue;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
class Constants
{
public const BASIS_DAYS_PER_YEAR_NASD = 0;
public const BASIS_DAYS_PER_YEAR_ACTUAL = 1;
public const BASIS_DAYS_PER_YEAR_360 = 2;
public const BASIS_DAYS_PER_YEAR_365 = 3;
public const BASIS_DAYS_PER_YEAR_360_EUROPEAN = 4;
public const FREQUENCY_ANNUAL = 1;
public const FREQUENCY_SEMI_ANNUAL = 2;
public const FREQUENCY_QUARTERLY = 4;
public const PAYMENT_END_OF_PERIOD = 0;
public const PAYMENT_BEGINNING_OF_PERIOD = 1;
}

View File

@@ -0,0 +1,426 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date;
class Coupons
{
private const PERIOD_DATE_PREVIOUS = false;
private const PERIOD_DATE_NEXT = true;
/**
* COUPDAYBS.
*
* Returns the number of days from the beginning of the coupon period to the settlement date.
*
* Excel Function:
* COUPDAYBS(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year (int).
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string
*/
public static function COUPDAYBS(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis);
if (is_string($daysPerYear)) {
return ExcelError::VALUE();
}
$prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
if ($basis === FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) {
return abs((float) DateTimeExcel\Days::between($prev, $settlement));
}
return (float) DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear;
}
/**
* COUPDAYS.
*
* Returns the number of days in the coupon period that contains the settlement date.
*
* Excel Function:
* COUPDAYS(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string
*/
public static function COUPDAYS(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
switch ($basis) {
case FinancialConstants::BASIS_DAYS_PER_YEAR_365:
// Actual/365
return 365 / $frequency;
case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL:
// Actual/actual
if ($frequency == FinancialConstants::FREQUENCY_ANNUAL) {
$daysPerYear = (int) Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis);
return $daysPerYear / $frequency;
}
$prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
$next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT);
return $next - $prev;
default:
// US (NASD) 30/360, Actual/360 or European 30/360
return 360 / $frequency;
}
}
/**
* COUPDAYSNC.
*
* Returns the number of days from the settlement date to the next coupon date.
*
* Excel Function:
* COUPDAYSNC(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int) .
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string
*/
public static function COUPDAYSNC(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
/** @var int */
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis);
$next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT);
if ($basis === FinancialConstants::BASIS_DAYS_PER_YEAR_NASD) {
$settlementDate = Date::excelToDateTimeObject($settlement);
$settlementEoM = Helpers::isLastDayOfMonth($settlementDate);
if ($settlementEoM) {
++$settlement;
}
}
return (float) DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear;
}
/**
* COUPNCD.
*
* Returns the next coupon date after the settlement date.
*
* Excel Function:
* COUPNCD(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function COUPNCD(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
self::doNothing($basis);
} catch (Exception $e) {
return $e->getMessage();
}
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT);
}
/**
* COUPNUM.
*
* Returns the number of coupons payable between the settlement date and maturity date,
* rounded up to the nearest whole coupon.
*
* Excel Function:
* COUPNUM(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return int|string
*/
public static function COUPNUM(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
self::doNothing($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$yearsBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction(
$settlement,
$maturity,
FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
);
return (int) ceil((float) $yearsBetweenSettlementAndMaturity * $frequency);
}
/**
* COUPPCD.
*
* Returns the previous coupon date before the settlement date.
*
* Excel Function:
* COUPPCD(settlement,maturity,frequency[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use (int).
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object,
* depending on the value of the ReturnDateType flag
*/
public static function COUPPCD(
$settlement,
$maturity,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
self::validateCouponPeriod($settlement, $maturity);
$frequency = FinancialValidations::validateFrequency($frequency);
$basis = FinancialValidations::validateBasis($basis);
self::doNothing($basis);
} catch (Exception $e) {
return $e->getMessage();
}
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
}
private static function monthsDiff(DateTime $result, int $months, string $plusOrMinus, int $day, bool $lastDayFlag): void
{
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), 1);
$result->modify("$plusOrMinus $months months");
$daysInMonth = (int) $result->format('t');
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), $lastDayFlag ? $daysInMonth : min($day, $daysInMonth));
}
private static function couponFirstPeriodDate(float $settlement, float $maturity, int $frequency, bool $next): float
{
$months = 12 / $frequency;
$result = Date::excelToDateTimeObject($maturity);
$day = (int) $result->format('d');
$lastDayFlag = Helpers::isLastDayOfMonth($result);
while ($settlement < Date::PHPToExcel($result)) {
self::monthsDiff($result, $months, '-', $day, $lastDayFlag);
}
if ($next === true) {
self::monthsDiff($result, $months, '+', $day, $lastDayFlag);
}
return (float) Date::PHPToExcel($result);
}
private static function validateCouponPeriod(float $settlement, float $maturity): void
{
if ($settlement >= $maturity) {
throw new Exception(ExcelError::NAN());
}
}
/** @param mixed $basis */
private static function doNothing($basis): bool
{
return $basis;
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Depreciation
{
/** @var float */
private static $zeroPointZero = 0.0;
/**
* DB.
*
* Returns the depreciation of an asset for a specified period using the
* fixed-declining balance method.
* This form of depreciation is used if you want to get a higher depreciation value
* at the beginning of the depreciation (as opposed to linear depreciation). The
* depreciation value is reduced with every depreciation period by the depreciation
* already deducted from the initial cost.
*
* Excel Function:
* DB(cost,salvage,life,period[,month])
*
* @param mixed $cost Initial cost of the asset
* @param mixed $salvage Value at the end of the depreciation.
* (Sometimes called the salvage value of the asset)
* @param mixed $life Number of periods over which the asset is depreciated.
* (Sometimes called the useful life of the asset)
* @param mixed $period The period for which you want to calculate the
* depreciation. Period must use the same units as life.
* @param mixed $month Number of months in the first year. If month is omitted,
* it defaults to 12.
*
* @return float|string
*/
public static function DB($cost, $salvage, $life, $period, $month = 12)
{
$cost = Functions::flattenSingleValue($cost);
$salvage = Functions::flattenSingleValue($salvage);
$life = Functions::flattenSingleValue($life);
$period = Functions::flattenSingleValue($period);
$month = Functions::flattenSingleValue($month);
try {
$cost = self::validateCost($cost);
$salvage = self::validateSalvage($salvage);
$life = self::validateLife($life);
$period = self::validatePeriod($period);
$month = self::validateMonth($month);
} catch (Exception $e) {
return $e->getMessage();
}
if ($cost === self::$zeroPointZero) {
return 0.0;
}
// Set Fixed Depreciation Rate
$fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life);
$fixedDepreciationRate = round($fixedDepreciationRate, 3);
// Loop through each period calculating the depreciation
// TODO Handle period value between 0 and 1 (e.g. 0.5)
$previousDepreciation = 0;
$depreciation = 0;
for ($per = 1; $per <= $period; ++$per) {
if ($per == 1) {
$depreciation = $cost * $fixedDepreciationRate * $month / 12;
} elseif ($per == ($life + 1)) {
$depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate * (12 - $month) / 12;
} else {
$depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate;
}
$previousDepreciation += $depreciation;
}
return $depreciation;
}
/**
* DDB.
*
* Returns the depreciation of an asset for a specified period using the
* double-declining balance method or some other method you specify.
*
* Excel Function:
* DDB(cost,salvage,life,period[,factor])
*
* @param mixed $cost Initial cost of the asset
* @param mixed $salvage Value at the end of the depreciation.
* (Sometimes called the salvage value of the asset)
* @param mixed $life Number of periods over which the asset is depreciated.
* (Sometimes called the useful life of the asset)
* @param mixed $period The period for which you want to calculate the
* depreciation. Period must use the same units as life.
* @param mixed $factor The rate at which the balance declines.
* If factor is omitted, it is assumed to be 2 (the
* double-declining balance method).
*
* @return float|string
*/
public static function DDB($cost, $salvage, $life, $period, $factor = 2.0)
{
$cost = Functions::flattenSingleValue($cost);
$salvage = Functions::flattenSingleValue($salvage);
$life = Functions::flattenSingleValue($life);
$period = Functions::flattenSingleValue($period);
$factor = Functions::flattenSingleValue($factor);
try {
$cost = self::validateCost($cost);
$salvage = self::validateSalvage($salvage);
$life = self::validateLife($life);
$period = self::validatePeriod($period);
$factor = self::validateFactor($factor);
} catch (Exception $e) {
return $e->getMessage();
}
if ($period > $life) {
return ExcelError::NAN();
}
// Loop through each period calculating the depreciation
// TODO Handling for fractional $period values
$previousDepreciation = 0;
$depreciation = 0;
for ($per = 1; $per <= $period; ++$per) {
$depreciation = min(
($cost - $previousDepreciation) * ($factor / $life),
($cost - $salvage - $previousDepreciation)
);
$previousDepreciation += $depreciation;
}
return $depreciation;
}
/**
* SLN.
*
* Returns the straight-line depreciation of an asset for one period
*
* @param mixed $cost Initial cost of the asset
* @param mixed $salvage Value at the end of the depreciation
* @param mixed $life Number of periods over which the asset is depreciated
*
* @return float|string Result, or a string containing an error
*/
public static function SLN($cost, $salvage, $life)
{
$cost = Functions::flattenSingleValue($cost);
$salvage = Functions::flattenSingleValue($salvage);
$life = Functions::flattenSingleValue($life);
try {
$cost = self::validateCost($cost, true);
$salvage = self::validateSalvage($salvage, true);
$life = self::validateLife($life, true);
} catch (Exception $e) {
return $e->getMessage();
}
if ($life === self::$zeroPointZero) {
return ExcelError::DIV0();
}
return ($cost - $salvage) / $life;
}
/**
* SYD.
*
* Returns the sum-of-years' digits depreciation of an asset for a specified period.
*
* @param mixed $cost Initial cost of the asset
* @param mixed $salvage Value at the end of the depreciation
* @param mixed $life Number of periods over which the asset is depreciated
* @param mixed $period Period
*
* @return float|string Result, or a string containing an error
*/
public static function SYD($cost, $salvage, $life, $period)
{
$cost = Functions::flattenSingleValue($cost);
$salvage = Functions::flattenSingleValue($salvage);
$life = Functions::flattenSingleValue($life);
$period = Functions::flattenSingleValue($period);
try {
$cost = self::validateCost($cost, true);
$salvage = self::validateSalvage($salvage);
$life = self::validateLife($life);
$period = self::validatePeriod($period);
} catch (Exception $e) {
return $e->getMessage();
}
if ($period > $life) {
return ExcelError::NAN();
}
$syd = (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1));
return $syd;
}
private static function validateCost($cost, bool $negativeValueAllowed = false): float
{
$cost = FinancialValidations::validateFloat($cost);
if ($cost < 0.0 && $negativeValueAllowed === false) {
throw new Exception(ExcelError::NAN());
}
return $cost;
}
private static function validateSalvage($salvage, bool $negativeValueAllowed = false): float
{
$salvage = FinancialValidations::validateFloat($salvage);
if ($salvage < 0.0 && $negativeValueAllowed === false) {
throw new Exception(ExcelError::NAN());
}
return $salvage;
}
private static function validateLife($life, bool $negativeValueAllowed = false): float
{
$life = FinancialValidations::validateFloat($life);
if ($life < 0.0 && $negativeValueAllowed === false) {
throw new Exception(ExcelError::NAN());
}
return $life;
}
private static function validatePeriod($period, bool $negativeValueAllowed = false): float
{
$period = FinancialValidations::validateFloat($period);
if ($period <= 0.0 && $negativeValueAllowed === false) {
throw new Exception(ExcelError::NAN());
}
return $period;
}
private static function validateMonth($month): int
{
$month = FinancialValidations::validateInt($month);
if ($month < 1) {
throw new Exception(ExcelError::NAN());
}
return $month;
}
private static function validateFactor($factor): float
{
$factor = FinancialValidations::validateFloat($factor);
if ($factor <= 0.0) {
throw new Exception(ExcelError::NAN());
}
return $factor;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
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\TextData\Format;
class Dollar
{
use ArrayEnabled;
/**
* DOLLAR.
*
* This function converts a number to text using currency format, with the decimals rounded to the specified place.
* The format used is $#,##0.00_);($#,##0.00)..
*
* @param mixed $number The value to format, or can be an array of numbers
* Or can be an array of values
* @param mixed $precision The number of digits to display to the right of the decimal point (as an integer).
* If precision is negative, number is rounded to the left of the decimal point.
* If you omit precision, it is assumed to be 2
* Or can be an array of precision values
*
* @return array|string
* If an array of values is passed for either of the arguments, then the returned result
* will also be an array with matching dimensions
*/
public static function format($number, $precision = 2)
{
return Format::DOLLAR($number, $precision);
}
/**
* DOLLARDE.
*
* Converts a dollar price expressed as an integer part and a fraction
* part into a dollar price expressed as a decimal number.
* Fractional dollar numbers are sometimes used for security prices.
*
* Excel Function:
* DOLLARDE(fractional_dollar,fraction)
*
* @param mixed $fractionalDollar Fractional Dollar
* Or can be an array of values
* @param mixed $fraction Fraction
* Or can be an array of values
*
* @return array|float|string
*/
public static function decimal($fractionalDollar = null, $fraction = 0)
{
if (is_array($fractionalDollar) || is_array($fraction)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $fractionalDollar, $fraction);
}
try {
$fractionalDollar = FinancialValidations::validateFloat(
Functions::flattenSingleValue($fractionalDollar) ?? 0.0
);
$fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction));
} catch (Exception $e) {
return $e->getMessage();
}
// Additional parameter validations
if ($fraction < 0) {
return ExcelError::NAN();
}
if ($fraction == 0) {
return ExcelError::DIV0();
}
$dollars = ($fractionalDollar < 0) ? ceil($fractionalDollar) : floor($fractionalDollar);
$cents = fmod($fractionalDollar, 1.0);
$cents /= $fraction;
$cents *= 10 ** ceil(log10($fraction));
return $dollars + $cents;
}
/**
* DOLLARFR.
*
* Converts a dollar price expressed as a decimal number into a dollar price
* expressed as a fraction.
* Fractional dollar numbers are sometimes used for security prices.
*
* Excel Function:
* DOLLARFR(decimal_dollar,fraction)
*
* @param mixed $decimalDollar Decimal Dollar
* Or can be an array of values
* @param mixed $fraction Fraction
* Or can be an array of values
*
* @return array|float|string
*/
public static function fractional($decimalDollar = null, $fraction = 0)
{
if (is_array($decimalDollar) || is_array($fraction)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $decimalDollar, $fraction);
}
try {
$decimalDollar = FinancialValidations::validateFloat(
Functions::flattenSingleValue($decimalDollar) ?? 0.0
);
$fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction));
} catch (Exception $e) {
return $e->getMessage();
}
// Additional parameter validations
if ($fraction < 0) {
return ExcelError::NAN();
}
if ($fraction == 0) {
return ExcelError::DIV0();
}
$dollars = ($decimalDollar < 0.0) ? ceil($decimalDollar) : floor($decimalDollar);
$cents = fmod($decimalDollar, 1);
$cents *= $fraction;
$cents *= 10 ** (-ceil(log10($fraction)));
return $dollars + $cents;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class FinancialValidations
{
/**
* @param mixed $date
*/
public static function validateDate($date): float
{
return DateTimeExcel\Helpers::getDateValue($date);
}
/**
* @param mixed $settlement
*/
public static function validateSettlementDate($settlement): float
{
return self::validateDate($settlement);
}
/**
* @param mixed $maturity
*/
public static function validateMaturityDate($maturity): float
{
return self::validateDate($maturity);
}
/**
* @param mixed $value
*/
public static function validateFloat($value): float
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (float) $value;
}
/**
* @param mixed $value
*/
public static function validateInt($value): int
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (int) floor((float) $value);
}
/**
* @param mixed $rate
*/
public static function validateRate($rate): float
{
$rate = self::validateFloat($rate);
if ($rate < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $rate;
}
/**
* @param mixed $frequency
*/
public static function validateFrequency($frequency): int
{
$frequency = self::validateInt($frequency);
if (
($frequency !== FinancialConstants::FREQUENCY_ANNUAL) &&
($frequency !== FinancialConstants::FREQUENCY_SEMI_ANNUAL) &&
($frequency !== FinancialConstants::FREQUENCY_QUARTERLY)
) {
throw new Exception(ExcelError::NAN());
}
return $frequency;
}
/**
* @param mixed $basis
*/
public static function validateBasis($basis): int
{
if (!is_numeric($basis)) {
throw new Exception(ExcelError::VALUE());
}
$basis = (int) $basis;
if (($basis < 0) || ($basis > 4)) {
throw new Exception(ExcelError::NAN());
}
return $basis;
}
/**
* @param mixed $price
*/
public static function validatePrice($price): float
{
$price = self::validateFloat($price);
if ($price < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $price;
}
/**
* @param mixed $parValue
*/
public static function validateParValue($parValue): float
{
$parValue = self::validateFloat($parValue);
if ($parValue < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $parValue;
}
/**
* @param mixed $yield
*/
public static function validateYield($yield): float
{
$yield = self::validateFloat($yield);
if ($yield < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $yield;
}
/**
* @param mixed $discount
*/
public static function validateDiscount($discount): float
{
$discount = self::validateFloat($discount);
if ($discount <= 0.0) {
throw new Exception(ExcelError::NAN());
}
return $discount;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use DateTimeInterface;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Helpers
{
/**
* daysPerYear.
*
* Returns the number of days in a specified year, as defined by the "basis" value
*
* @param int|string $year The year against which we're testing
* @param int|string $basis The type of day count:
* 0 or omitted US (NASD) 360
* 1 Actual (365 or 366 in a leap year)
* 2 360
* 3 365
* 4 European 360
*
* @return int|string Result, or a string containing an error
*/
public static function daysPerYear($year, $basis = 0)
{
if (!is_numeric($basis)) {
return ExcelError::NAN();
}
switch ($basis) {
case FinancialConstants::BASIS_DAYS_PER_YEAR_NASD:
case FinancialConstants::BASIS_DAYS_PER_YEAR_360:
case FinancialConstants::BASIS_DAYS_PER_YEAR_360_EUROPEAN:
return 360;
case FinancialConstants::BASIS_DAYS_PER_YEAR_365:
return 365;
case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL:
return (DateTimeExcel\Helpers::isLeapYear($year)) ? 366 : 365;
}
return ExcelError::NAN();
}
/**
* isLastDayOfMonth.
*
* Returns a boolean TRUE/FALSE indicating if this date is the last date of the month
*
* @param DateTimeInterface $date The date for testing
*/
public static function isLastDayOfMonth(DateTimeInterface $date): bool
{
return $date->format('d') === $date->format('t');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class InterestRate
{
/**
* EFFECT.
*
* Returns the effective interest rate given the nominal rate and the number of
* compounding payments per year.
*
* Excel Function:
* EFFECT(nominal_rate,npery)
*
* @param mixed $nominalRate Nominal interest rate as a float
* @param mixed $periodsPerYear Integer number of compounding payments per year
*
* @return float|string
*/
public static function effective($nominalRate = 0, $periodsPerYear = 0)
{
$nominalRate = Functions::flattenSingleValue($nominalRate);
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear);
try {
$nominalRate = FinancialValidations::validateFloat($nominalRate);
$periodsPerYear = FinancialValidations::validateInt($periodsPerYear);
} catch (Exception $e) {
return $e->getMessage();
}
if ($nominalRate <= 0 || $periodsPerYear < 1) {
return ExcelError::NAN();
}
return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1;
}
/**
* NOMINAL.
*
* Returns the nominal interest rate given the effective rate and the number of compounding payments per year.
*
* @param mixed $effectiveRate Effective interest rate as a float
* @param mixed $periodsPerYear Integer number of compounding payments per year
*
* @return float|string Result, or a string containing an error
*/
public static function nominal($effectiveRate = 0, $periodsPerYear = 0)
{
$effectiveRate = Functions::flattenSingleValue($effectiveRate);
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear);
try {
$effectiveRate = FinancialValidations::validateFloat($effectiveRate);
$periodsPerYear = FinancialValidations::validateInt($periodsPerYear);
} catch (Exception $e) {
return $e->getMessage();
}
if ($effectiveRate <= 0 || $periodsPerYear < 1) {
return ExcelError::NAN();
}
// Calculate
return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\YearFrac;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class AccruedInterest
{
public const ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT = true;
public const ACCRINT_CALCMODE_FIRST_INTEREST_TO_SETTLEMENT = false;
/**
* ACCRINT.
*
* Returns the accrued interest for a security that pays periodic interest.
*
* Excel Function:
* ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis][,calc_method])
*
* @param mixed $issue the security's issue date
* @param mixed $firstInterest the security's first interest date
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date
* when the security is traded to the buyer.
* @param mixed $rate The security's annual coupon rate
* @param mixed $parValue The security's par value.
* If you omit par, ACCRINT uses $1,000.
* @param mixed $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
* @param mixed $calcMethod
*
* @return float|string Result, or a string containing an error
*/
public static function periodic(
$issue,
$firstInterest,
$settlement,
$rate,
$parValue = 1000,
$frequency = FinancialConstants::FREQUENCY_ANNUAL,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD,
$calcMethod = self::ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT
) {
self::doNothing($calcMethod);
$issue = Functions::flattenSingleValue($issue);
$firstInterest = Functions::flattenSingleValue($firstInterest);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue);
$frequency = ($frequency === null)
? FinancialConstants::FREQUENCY_ANNUAL
: Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$issue = SecurityValidations::validateIssueDate($issue);
$settlement = SecurityValidations::validateSettlementDate($settlement);
SecurityValidations::validateSecurityPeriod($issue, $settlement);
$rate = SecurityValidations::validateRate($rate);
$parValue = SecurityValidations::validateParValue($parValue);
$frequency = SecurityValidations::validateFrequency($frequency);
self::doNothing($frequency);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis));
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenFirstInterestAndSettlement = Functions::scalar(YearFrac::fraction($firstInterest, $settlement, $basis));
if (!is_numeric($daysBetweenFirstInterestAndSettlement)) {
// return date error
return $daysBetweenFirstInterestAndSettlement;
}
return $parValue * $rate * $daysBetweenIssueAndSettlement;
}
/**
* ACCRINTM.
*
* Returns the accrued interest for a security that pays interest at maturity.
*
* Excel Function:
* ACCRINTM(issue,settlement,rate[,par[,basis]])
*
* @param mixed $issue The security's issue date
* @param mixed $settlement The security's settlement (or maturity) date
* @param mixed $rate The security's annual coupon rate
* @param mixed $parValue The security's par value.
* If you omit parValue, ACCRINT uses $1,000.
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function atMaturity(
$issue,
$settlement,
$rate,
$parValue = 1000,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$issue = Functions::flattenSingleValue($issue);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$issue = SecurityValidations::validateIssueDate($issue);
$settlement = SecurityValidations::validateSettlementDate($settlement);
SecurityValidations::validateSecurityPeriod($issue, $settlement);
$rate = SecurityValidations::validateRate($rate);
$parValue = SecurityValidations::validateParValue($parValue);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis));
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
return $parValue * $rate * $daysBetweenIssueAndSettlement;
}
/** @param mixed $arg */
private static function doNothing($arg): bool
{
return (bool) $arg;
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Price
{
/**
* PRICE.
*
* Returns the price per $100 face value of a security that pays periodic interest.
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $rate the security's annual coupon rate
* @param mixed $yield the security's annual yield
* @param mixed $redemption The number of coupon payments per year.
* For annual payments, frequency = 1;
* for semiannual, frequency = 2;
* for quarterly, frequency = 4.
* @param mixed $frequency
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function price(
$settlement,
$maturity,
$rate,
$yield,
$redemption,
$frequency,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$redemption = Functions::flattenSingleValue($redemption);
$frequency = Functions::flattenSingleValue($frequency);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$rate = SecurityValidations::validateRate($rate);
$yield = SecurityValidations::validateYield($yield);
$redemption = SecurityValidations::validateRedemption($redemption);
$frequency = SecurityValidations::validateFrequency($frequency);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$dsc = Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis);
$e = Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis);
$n = Coupons::COUPNUM($settlement, $maturity, $frequency, $basis);
$a = Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis);
$baseYF = 1.0 + ($yield / $frequency);
$rfp = 100 * ($rate / $frequency);
$de = $dsc / $e;
$result = $redemption / $baseYF ** (--$n + $de);
for ($k = 0; $k <= $n; ++$k) {
$result += $rfp / ($baseYF ** ($k + $de));
}
$result -= $rfp * ($a / $e);
return $result;
}
/**
* PRICEDISC.
*
* Returns the price per $100 face value of a discounted security.
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $discount The security's discount rate
* @param mixed $redemption The security's redemption value per $100 face value
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function priceDiscounted(
$settlement,
$maturity,
$discount,
$redemption,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$discount = Functions::flattenSingleValue($discount);
$redemption = Functions::flattenSingleValue($redemption);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$discount = SecurityValidations::validateDiscount($discount);
$redemption = SecurityValidations::validateRedemption($redemption);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity);
}
/**
* PRICEMAT.
*
* Returns the price per $100 face value of a security that pays interest at maturity.
*
* @param mixed $settlement The security's settlement date.
* The security's settlement date is the date after the issue date when the
* security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $issue The security's issue date
* @param mixed $rate The security's interest rate at date of issue
* @param mixed $yield The security's annual yield
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function priceAtMaturity(
$settlement,
$maturity,
$issue,
$rate,
$yield,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$issue = Functions::flattenSingleValue($issue);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$issue = SecurityValidations::validateIssueDate($issue);
$rate = SecurityValidations::validateRate($rate);
$yield = SecurityValidations::validateYield($yield);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysPerYear = Functions::scalar(Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis));
if (!is_numeric($daysPerYear)) {
return $daysPerYear;
}
$daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis));
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenIssueAndSettlement *= $daysPerYear;
$daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis));
if (!is_numeric($daysBetweenIssueAndMaturity)) {
// return date error
return $daysBetweenIssueAndMaturity;
}
$daysBetweenIssueAndMaturity *= $daysPerYear;
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
$daysBetweenSettlementAndMaturity *= $daysPerYear;
return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) /
(1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) -
(($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100);
}
/**
* RECEIVED.
*
* Returns the amount received at maturity for a fully invested Security.
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $investment The amount invested in the security
* @param mixed $discount The security's discount rate
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function received(
$settlement,
$maturity,
$investment,
$discount,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$investment = Functions::flattenSingleValue($investment);
$discount = Functions::flattenSingleValue($discount);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$investment = SecurityValidations::validateFloat($investment);
$discount = SecurityValidations::validateDiscount($discount);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
if ($investment <= 0) {
return ExcelError::NAN();
}
$daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis);
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return $investment / (1 - ($discount * $daysBetweenSettlementAndMaturity));
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Rates
{
/**
* DISC.
*
* Returns the discount rate for a security.
*
* Excel Function:
* DISC(settlement,maturity,price,redemption[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $price The security's price per $100 face value
* @param mixed $redemption The security's redemption value per $100 face value
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string
*/
public static function discount(
$settlement,
$maturity,
$price,
$redemption,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$price = Functions::flattenSingleValue($price);
$redemption = Functions::flattenSingleValue($redemption);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$price = SecurityValidations::validatePrice($price);
$redemption = SecurityValidations::validateRedemption($redemption);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
if ($price <= 0.0) {
return ExcelError::NAN();
}
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return (1 - $price / $redemption) / $daysBetweenSettlementAndMaturity;
}
/**
* INTRATE.
*
* Returns the interest rate for a fully invested security.
*
* Excel Function:
* INTRATE(settlement,maturity,investment,redemption[,basis])
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $investment the amount invested in the security
* @param mixed $redemption the amount to be received at maturity
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string
*/
public static function interest(
$settlement,
$maturity,
$investment,
$redemption,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$investment = Functions::flattenSingleValue($investment);
$redemption = Functions::flattenSingleValue($redemption);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$investment = SecurityValidations::validateFloat($investment);
$redemption = SecurityValidations::validateRedemption($redemption);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
if ($investment <= 0) {
return ExcelError::NAN();
}
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return (($redemption / $investment) - 1) / ($daysBetweenSettlementAndMaturity);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class SecurityValidations extends FinancialValidations
{
/**
* @param mixed $issue
*/
public static function validateIssueDate($issue): float
{
return self::validateDate($issue);
}
/**
* @param mixed $settlement
* @param mixed $maturity
*/
public static function validateSecurityPeriod($settlement, $maturity): void
{
if ($settlement >= $maturity) {
throw new Exception(ExcelError::NAN());
}
}
/**
* @param mixed $redemption
*/
public static function validateRedemption($redemption): float
{
$redemption = self::validateFloat($redemption);
if ($redemption <= 0.0) {
throw new Exception(ExcelError::NAN());
}
return $redemption;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Yields
{
/**
* YIELDDISC.
*
* Returns the annual yield of a security that pays interest at maturity.
*
* @param mixed $settlement The security's settlement date.
* The security's settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $price The security's price per $100 face value
* @param mixed $redemption The security's redemption value per $100 face value
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function yieldDiscounted(
$settlement,
$maturity,
$price,
$redemption,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$price = Functions::flattenSingleValue($price);
$redemption = Functions::flattenSingleValue($redemption);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$price = SecurityValidations::validatePrice($price);
$redemption = SecurityValidations::validateRedemption($redemption);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis);
if (!is_numeric($daysPerYear)) {
return $daysPerYear;
}
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
$daysBetweenSettlementAndMaturity *= $daysPerYear;
return (($redemption - $price) / $price) * ($daysPerYear / $daysBetweenSettlementAndMaturity);
}
/**
* YIELDMAT.
*
* Returns the annual yield of a security that pays interest at maturity.
*
* @param mixed $settlement The security's settlement date.
* The security's settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $issue The security's issue date
* @param mixed $rate The security's interest rate at date of issue
* @param mixed $price The security's price per $100 face value
* @param mixed $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function yieldAtMaturity(
$settlement,
$maturity,
$issue,
$rate,
$price,
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
) {
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$issue = Functions::flattenSingleValue($issue);
$rate = Functions::flattenSingleValue($rate);
$price = Functions::flattenSingleValue($price);
$basis = ($basis === null)
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
: Functions::flattenSingleValue($basis);
try {
$settlement = SecurityValidations::validateSettlementDate($settlement);
$maturity = SecurityValidations::validateMaturityDate($maturity);
SecurityValidations::validateSecurityPeriod($settlement, $maturity);
$issue = SecurityValidations::validateIssueDate($issue);
$rate = SecurityValidations::validateRate($rate);
$price = SecurityValidations::validatePrice($price);
$basis = SecurityValidations::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis);
if (!is_numeric($daysPerYear)) {
return $daysPerYear;
}
$daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis));
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenIssueAndSettlement *= $daysPerYear;
$daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis));
if (!is_numeric($daysBetweenIssueAndMaturity)) {
// return date error
return $daysBetweenIssueAndMaturity;
}
$daysBetweenIssueAndMaturity *= $daysPerYear;
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis));
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
$daysBetweenSettlementAndMaturity *= $daysPerYear;
return ((1 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate) -
(($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) /
(($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) *
($daysPerYear / $daysBetweenSettlementAndMaturity);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class TreasuryBill
{
/**
* TBILLEQ.
*
* Returns the bond-equivalent yield for a Treasury bill.
*
* @param mixed $settlement The Treasury bill's settlement date.
* The Treasury bill's settlement date is the date after the issue date
* when the Treasury bill is traded to the buyer.
* @param mixed $maturity The Treasury bill's maturity date.
* The maturity date is the date when the Treasury bill expires.
* @param mixed $discount The Treasury bill's discount rate
*
* @return float|string Result, or a string containing an error
*/
public static function bondEquivalentYield($settlement, $maturity, $discount)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$discount = Functions::flattenSingleValue($discount);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
$discount = FinancialValidations::validateFloat($discount);
} catch (Exception $e) {
return $e->getMessage();
}
if ($discount <= 0) {
return ExcelError::NAN();
}
$daysBetweenSettlementAndMaturity = $maturity - $settlement;
$daysPerYear = Helpers::daysPerYear(
Functions::scalar(DateTimeExcel\DateParts::year($maturity)),
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL
);
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) {
return ExcelError::NAN();
}
return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity);
}
/**
* TBILLPRICE.
*
* Returns the price per $100 face value for a Treasury bill.
*
* @param mixed $settlement The Treasury bill's settlement date.
* The Treasury bill's settlement date is the date after the issue date
* when the Treasury bill is traded to the buyer.
* @param mixed $maturity The Treasury bill's maturity date.
* The maturity date is the date when the Treasury bill expires.
* @param mixed $discount The Treasury bill's discount rate
*
* @return float|string Result, or a string containing an error
*/
public static function price($settlement, $maturity, $discount)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$discount = Functions::flattenSingleValue($discount);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
$discount = FinancialValidations::validateFloat($discount);
} catch (Exception $e) {
return $e->getMessage();
}
if ($discount <= 0) {
return ExcelError::NAN();
}
$daysBetweenSettlementAndMaturity = $maturity - $settlement;
$daysPerYear = Helpers::daysPerYear(
Functions::scalar(DateTimeExcel\DateParts::year($maturity)),
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL
);
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) {
return ExcelError::NAN();
}
$price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360));
if ($price < 0.0) {
return ExcelError::NAN();
}
return $price;
}
/**
* TBILLYIELD.
*
* Returns the yield for a Treasury bill.
*
* @param mixed $settlement The Treasury bill's settlement date.
* The Treasury bill's settlement date is the date after the issue date when
* the Treasury bill is traded to the buyer.
* @param mixed $maturity The Treasury bill's maturity date.
* The maturity date is the date when the Treasury bill expires.
* @param mixed $price The Treasury bill's price per $100 face value
*
* @return float|string
*/
public static function yield($settlement, $maturity, $price)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$price = Functions::flattenSingleValue($price);
try {
$settlement = FinancialValidations::validateSettlementDate($settlement);
$maturity = FinancialValidations::validateMaturityDate($maturity);
$price = FinancialValidations::validatePrice($price);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenSettlementAndMaturity = $maturity - $settlement;
$daysPerYear = Helpers::daysPerYear(
Functions::scalar(DateTimeExcel\DateParts::year($maturity)),
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL
);
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) {
return ExcelError::NAN();
}
return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity);
}
}

View File

@@ -0,0 +1,630 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
/**
* PARTLY BASED ON:
* Copyright (c) 2007 E. W. Bachtal, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* The software is provided "as is", without warranty of any kind, express or implied, including but not
* limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In
* no event shall the authors or copyright holders be liable for any claim, damages or other liability,
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
* software or the use or other dealings in the software.
*
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
*/
class FormulaParser
{
// Character constants
const QUOTE_DOUBLE = '"';
const QUOTE_SINGLE = '\'';
const BRACKET_CLOSE = ']';
const BRACKET_OPEN = '[';
const BRACE_OPEN = '{';
const BRACE_CLOSE = '}';
const PAREN_OPEN = '(';
const PAREN_CLOSE = ')';
const SEMICOLON = ';';
const WHITESPACE = ' ';
const COMMA = ',';
const ERROR_START = '#';
const OPERATORS_SN = '+-';
const OPERATORS_INFIX = '+-*/^&=><';
const OPERATORS_POSTFIX = '%';
/**
* Formula.
*
* @var string
*/
private $formula;
/**
* Tokens.
*
* @var FormulaToken[]
*/
private $tokens = [];
/**
* Create a new FormulaParser.
*
* @param ?string $formula Formula to parse
*/
public function __construct($formula = '')
{
// Check parameters
if ($formula === null) {
throw new Exception('Invalid parameter passed: formula');
}
// Initialise values
$this->formula = trim($formula);
// Parse!
$this->parseToTokens();
}
/**
* Get Formula.
*
* @return string
*/
public function getFormula()
{
return $this->formula;
}
/**
* Get Token.
*
* @param int $id Token id
*/
public function getToken(int $id = 0): FormulaToken
{
if (isset($this->tokens[$id])) {
return $this->tokens[$id];
}
throw new Exception("Token with id $id does not exist.");
}
/**
* Get Token count.
*
* @return int
*/
public function getTokenCount()
{
return count($this->tokens);
}
/**
* Get Tokens.
*
* @return FormulaToken[]
*/
public function getTokens()
{
return $this->tokens;
}
/**
* Parse to tokens.
*/
private function parseToTokens(): void
{
// No attempt is made to verify formulas; assumes formulas are derived from Excel, where
// they can only exist if valid; stack overflows/underflows sunk as nulls without exceptions.
// Check if the formula has a valid starting =
$formulaLength = strlen($this->formula);
if ($formulaLength < 2 || $this->formula[0] != '=') {
return;
}
// Helper variables
$tokens1 = $tokens2 = $stack = [];
$inString = $inPath = $inRange = $inError = false;
$nextToken = null;
//$token = $previousToken = null;
$index = 1;
$value = '';
$ERRORS = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A'];
$COMPARATORS_MULTI = ['>=', '<=', '<>'];
while ($index < $formulaLength) {
// state-dependent character evaluation (order is important)
// double-quoted strings
// embeds are doubled
// end marks token
if ($inString) {
if ($this->formula[$index] == self::QUOTE_DOUBLE) {
if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_DOUBLE)) {
$value .= self::QUOTE_DOUBLE;
++$index;
} else {
$inString = false;
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_TEXT);
$value = '';
}
} else {
$value .= $this->formula[$index];
}
++$index;
continue;
}
// single-quoted strings (links)
// embeds are double
// end does not mark a token
if ($inPath) {
if ($this->formula[$index] == self::QUOTE_SINGLE) {
if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_SINGLE)) {
$value .= self::QUOTE_SINGLE;
++$index;
} else {
$inPath = false;
}
} else {
$value .= $this->formula[$index];
}
++$index;
continue;
}
// bracked strings (R1C1 range index or linked workbook name)
// no embeds (changed to "()" by Excel)
// end does not mark a token
if ($inRange) {
if ($this->formula[$index] == self::BRACKET_CLOSE) {
$inRange = false;
}
$value .= $this->formula[$index];
++$index;
continue;
}
// error values
// end marks a token, determined from absolute list of values
if ($inError) {
$value .= $this->formula[$index];
++$index;
if (in_array($value, $ERRORS)) {
$inError = false;
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_ERROR);
$value = '';
}
continue;
}
// scientific notation check
if (strpos(self::OPERATORS_SN, $this->formula[$index]) !== false) {
if (strlen($value) > 1) {
if (preg_match('/^[1-9]{1}(\\.\\d+)?E{1}$/', $this->formula[$index]) != 0) {
$value .= $this->formula[$index];
++$index;
continue;
}
}
}
// independent character evaluation (order not important)
// establish state-dependent character evaluations
if ($this->formula[$index] == self::QUOTE_DOUBLE) {
if (strlen($value) > 0) {
// unexpected
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
$value = '';
}
$inString = true;
++$index;
continue;
}
if ($this->formula[$index] == self::QUOTE_SINGLE) {
if (strlen($value) > 0) {
// unexpected
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
$value = '';
}
$inPath = true;
++$index;
continue;
}
if ($this->formula[$index] == self::BRACKET_OPEN) {
$inRange = true;
$value .= self::BRACKET_OPEN;
++$index;
continue;
}
if ($this->formula[$index] == self::ERROR_START) {
if (strlen($value) > 0) {
// unexpected
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
$value = '';
}
$inError = true;
$value .= self::ERROR_START;
++$index;
continue;
}
// mark start and end of arrays and array rows
if ($this->formula[$index] == self::BRACE_OPEN) {
if (strlen($value) > 0) {
// unexpected
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
$value = '';
}
$tmp = new FormulaToken('ARRAY', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
$tokens1[] = $tmp;
$stack[] = clone $tmp;
$tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
$tokens1[] = $tmp;
$stack[] = clone $tmp;
++$index;
continue;
}
if ($this->formula[$index] == self::SEMICOLON) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$tokens1[] = $tmp;
$tmp = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
$tokens1[] = $tmp;
$tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
$tokens1[] = $tmp;
$stack[] = clone $tmp;
++$index;
continue;
}
if ($this->formula[$index] == self::BRACE_CLOSE) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$tokens1[] = $tmp;
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$tokens1[] = $tmp;
++$index;
continue;
}
// trim white-space
if ($this->formula[$index] == self::WHITESPACE) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tokens1[] = new FormulaToken('', FormulaToken::TOKEN_TYPE_WHITESPACE);
++$index;
while (($this->formula[$index] == self::WHITESPACE) && ($index < $formulaLength)) {
++$index;
}
continue;
}
// multi-character comparators
if (($index + 2) <= $formulaLength) {
if (in_array(substr($this->formula, $index, 2), $COMPARATORS_MULTI)) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tokens1[] = new FormulaToken(substr($this->formula, $index, 2), FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_LOGICAL);
$index += 2;
continue;
}
}
// standard infix operators
if (strpos(self::OPERATORS_INFIX, $this->formula[$index]) !== false) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORINFIX);
++$index;
continue;
}
// standard postfix operators (only one)
if (strpos(self::OPERATORS_POSTFIX, $this->formula[$index]) !== false) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX);
++$index;
continue;
}
// start subexpression or function
if ($this->formula[$index] == self::PAREN_OPEN) {
if (strlen($value) > 0) {
$tmp = new FormulaToken($value, FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
$tokens1[] = $tmp;
$stack[] = clone $tmp;
$value = '';
} else {
$tmp = new FormulaToken('', FormulaToken::TOKEN_TYPE_SUBEXPRESSION, FormulaToken::TOKEN_SUBTYPE_START);
$tokens1[] = $tmp;
$stack[] = clone $tmp;
}
++$index;
continue;
}
// function, subexpression, or array parameters, or operand unions
if ($this->formula[$index] == self::COMMA) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$stack[] = $tmp;
if ($tmp->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
$tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_UNION);
} else {
$tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
}
++$index;
continue;
}
// stop subexpression
if ($this->formula[$index] == self::PAREN_CLOSE) {
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$tokens1[] = $tmp;
++$index;
continue;
}
// token accumulation
$value .= $this->formula[$index];
++$index;
}
// dump remaining accumulation
if (strlen($value) > 0) {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
}
// move tokenList to new set, excluding unnecessary white-space tokens and converting necessary ones to intersections
$tokenCount = count($tokens1);
for ($i = 0; $i < $tokenCount; ++$i) {
$token = $tokens1[$i];
if (isset($tokens1[$i - 1])) {
$previousToken = $tokens1[$i - 1];
} else {
$previousToken = null;
}
if (isset($tokens1[$i + 1])) {
$nextToken = $tokens1[$i + 1];
} else {
$nextToken = null;
}
if ($token === null) {
continue;
}
if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
$tokens2[] = $token;
continue;
}
if ($previousToken === null) {
continue;
}
if (
!(
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
)
) {
continue;
}
if ($nextToken === null) {
continue;
}
if (
!(
(($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
(($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
)
) {
continue;
}
$tokens2[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_INTERSECTION);
}
// move tokens to final list, switching infix "-" operators to prefix when appropriate, switching infix "+" operators
// to noop when appropriate, identifying operand and infix-operator subtypes, and pulling "@" from function names
$this->tokens = [];
$tokenCount = count($tokens2);
for ($i = 0; $i < $tokenCount; ++$i) {
$token = $tokens2[$i];
if (isset($tokens2[$i - 1])) {
$previousToken = $tokens2[$i - 1];
} else {
$previousToken = null;
}
//if (isset($tokens2[$i + 1])) {
// $nextToken = $tokens2[$i + 1];
//} else {
// $nextToken = null;
//}
if ($token === null) {
continue;
}
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
if ($i == 0) {
$token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
} elseif (
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) &&
($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) &&
($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
) {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
} else {
$token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
}
$this->tokens[] = $token;
continue;
}
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '+') {
if ($i == 0) {
continue;
} elseif (
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) &&
($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) &&
($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
) {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
} else {
continue;
}
$this->tokens[] = $token;
continue;
}
if (
$token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX &&
$token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING
) {
if (strpos('<>=', substr($token->getValue(), 0, 1)) !== false) {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
} elseif ($token->getValue() == '&') {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_CONCATENATION);
} else {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
}
$this->tokens[] = $token;
continue;
}
if (
$token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND &&
$token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING
) {
if (!is_numeric($token->getValue())) {
if (strtoupper($token->getValue()) == 'TRUE' || strtoupper($token->getValue()) == 'FALSE') {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
} else {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_RANGE);
}
} else {
$token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_NUMBER);
}
$this->tokens[] = $token;
continue;
}
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
if (strlen($token->getValue()) > 0) {
if (substr($token->getValue(), 0, 1) == '@') {
$token->setValue(substr($token->getValue(), 1));
}
}
}
$this->tokens[] = $token;
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
/**
* PARTLY BASED ON:
* Copyright (c) 2007 E. W. Bachtal, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* The software is provided "as is", without warranty of any kind, express or implied, including but not
* limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In
* no event shall the authors or copyright holders be liable for any claim, damages or other liability,
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
* software or the use or other dealings in the software.
*
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
*/
class FormulaToken
{
// Token types
const TOKEN_TYPE_NOOP = 'Noop';
const TOKEN_TYPE_OPERAND = 'Operand';
const TOKEN_TYPE_FUNCTION = 'Function';
const TOKEN_TYPE_SUBEXPRESSION = 'Subexpression';
const TOKEN_TYPE_ARGUMENT = 'Argument';
const TOKEN_TYPE_OPERATORPREFIX = 'OperatorPrefix';
const TOKEN_TYPE_OPERATORINFIX = 'OperatorInfix';
const TOKEN_TYPE_OPERATORPOSTFIX = 'OperatorPostfix';
const TOKEN_TYPE_WHITESPACE = 'Whitespace';
const TOKEN_TYPE_UNKNOWN = 'Unknown';
// Token subtypes
const TOKEN_SUBTYPE_NOTHING = 'Nothing';
const TOKEN_SUBTYPE_START = 'Start';
const TOKEN_SUBTYPE_STOP = 'Stop';
const TOKEN_SUBTYPE_TEXT = 'Text';
const TOKEN_SUBTYPE_NUMBER = 'Number';
const TOKEN_SUBTYPE_LOGICAL = 'Logical';
const TOKEN_SUBTYPE_ERROR = 'Error';
const TOKEN_SUBTYPE_RANGE = 'Range';
const TOKEN_SUBTYPE_MATH = 'Math';
const TOKEN_SUBTYPE_CONCATENATION = 'Concatenation';
const TOKEN_SUBTYPE_INTERSECTION = 'Intersection';
const TOKEN_SUBTYPE_UNION = 'Union';
/**
* Value.
*
* @var string
*/
private $value;
/**
* Token Type (represented by TOKEN_TYPE_*).
*
* @var string
*/
private $tokenType;
/**
* Token SubType (represented by TOKEN_SUBTYPE_*).
*
* @var string
*/
private $tokenSubType;
/**
* Create a new FormulaToken.
*
* @param string $value
* @param string $tokenType Token type (represented by TOKEN_TYPE_*)
* @param string $tokenSubType Token Subtype (represented by TOKEN_SUBTYPE_*)
*/
public function __construct($value, $tokenType = self::TOKEN_TYPE_UNKNOWN, $tokenSubType = self::TOKEN_SUBTYPE_NOTHING)
{
// Initialise values
$this->value = $value;
$this->tokenType = $tokenType;
$this->tokenSubType = $tokenSubType;
}
/**
* Get Value.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Set Value.
*
* @param string $value
*/
public function setValue($value): void
{
$this->value = $value;
}
/**
* Get Token Type (represented by TOKEN_TYPE_*).
*
* @return string
*/
public function getTokenType()
{
return $this->tokenType;
}
/**
* Set Token Type (represented by TOKEN_TYPE_*).
*
* @param string $value
*/
public function setTokenType($value): void
{
$this->tokenType = $value;
}
/**
* Get Token SubType (represented by TOKEN_SUBTYPE_*).
*
* @return string
*/
public function getTokenSubType()
{
return $this->tokenSubType;
}
/**
* Set Token SubType (represented by TOKEN_SUBTYPE_*).
*
* @param string $value
*/
public function setTokenSubType($value): void
{
$this->tokenSubType = $value;
}
}

View File

@@ -0,0 +1,668 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Shared\Date;
class Functions
{
const PRECISION = 8.88E-016;
/**
* 2 / PI.
*/
const M_2DIVPI = 0.63661977236758134307553505349006;
const COMPATIBILITY_EXCEL = 'Excel';
const COMPATIBILITY_GNUMERIC = 'Gnumeric';
const COMPATIBILITY_OPENOFFICE = 'OpenOfficeCalc';
/** Use of RETURNDATE_PHP_NUMERIC is discouraged - not 32-bit Y2038-safe, no timezone. */
const RETURNDATE_PHP_NUMERIC = 'P';
/** Use of RETURNDATE_UNIX_TIMESTAMP is discouraged - not 32-bit Y2038-safe, no timezone. */
const RETURNDATE_UNIX_TIMESTAMP = 'P';
const RETURNDATE_PHP_OBJECT = 'O';
const RETURNDATE_PHP_DATETIME_OBJECT = 'O';
const RETURNDATE_EXCEL = 'E';
/**
* Compatibility mode to use for error checking and responses.
*
* @var string
*/
protected static $compatibilityMode = self::COMPATIBILITY_EXCEL;
/**
* Data Type to use when returning date values.
*
* @var string
*/
protected static $returnDateType = self::RETURNDATE_EXCEL;
/**
* Set the Compatibility Mode.
*
* @param string $compatibilityMode Compatibility Mode
* Permitted values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
*
* @return bool (Success or Failure)
*/
public static function setCompatibilityMode($compatibilityMode)
{
if (
($compatibilityMode == self::COMPATIBILITY_EXCEL) ||
($compatibilityMode == self::COMPATIBILITY_GNUMERIC) ||
($compatibilityMode == self::COMPATIBILITY_OPENOFFICE)
) {
self::$compatibilityMode = $compatibilityMode;
return true;
}
return false;
}
/**
* Return the current Compatibility Mode.
*
* @return string Compatibility Mode
* Possible Return values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
*/
public static function getCompatibilityMode()
{
return self::$compatibilityMode;
}
/**
* Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP DateTime Object).
*
* @param string $returnDateType Return Date Format
* Permitted values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL 'E'
*
* @return bool Success or failure
*/
public static function setReturnDateType($returnDateType)
{
if (
($returnDateType == self::RETURNDATE_UNIX_TIMESTAMP) ||
($returnDateType == self::RETURNDATE_PHP_DATETIME_OBJECT) ||
($returnDateType == self::RETURNDATE_EXCEL)
) {
self::$returnDateType = $returnDateType;
return true;
}
return false;
}
/**
* Return the current Return Date Format for functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object).
*
* @return string Return Date Format
* Possible Return values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL ' 'E'
*/
public static function getReturnDateType()
{
return self::$returnDateType;
}
/**
* DUMMY.
*
* @return string #Not Yet Implemented
*/
public static function DUMMY()
{
return '#Not Yet Implemented';
}
public static function isMatrixValue($idx)
{
return (substr_count($idx, '.') <= 1) || (preg_match('/\.[A-Z]/', $idx) > 0);
}
public static function isValue($idx)
{
return substr_count($idx, '.') === 0;
}
public static function isCellValue($idx)
{
return substr_count($idx, '.') > 1;
}
public static function ifCondition($condition)
{
$condition = self::flattenSingleValue($condition);
if ($condition === '') {
return '=""';
}
if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) {
$condition = self::operandSpecialHandling($condition);
if (is_bool($condition)) {
return '=' . ($condition ? 'TRUE' : 'FALSE');
} elseif (!is_numeric($condition)) {
if ($condition !== '""') { // Not an empty string
// Escape any quotes in the string value
$condition = (string) preg_replace('/"/ui', '""', $condition);
}
$condition = Calculation::wrapResult(strtoupper($condition));
}
return str_replace('""""', '""', '=' . $condition);
}
preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches);
[, $operator, $operand] = $matches;
$operand = self::operandSpecialHandling($operand);
if (is_numeric(trim($operand, '"'))) {
$operand = trim($operand, '"');
} elseif (!is_numeric($operand) && $operand !== 'FALSE' && $operand !== 'TRUE') {
$operand = str_replace('"', '""', $operand);
$operand = Calculation::wrapResult(strtoupper($operand));
}
return str_replace('""""', '""', $operator . $operand);
}
private static function operandSpecialHandling($operand)
{
if (is_numeric($operand) || is_bool($operand)) {
return $operand;
} elseif (strtoupper($operand) === Calculation::getTRUE() || strtoupper($operand) === Calculation::getFALSE()) {
return strtoupper($operand);
}
// Check for percentage
if (preg_match('/^\-?\d*\.?\d*\s?\%$/', $operand)) {
return ((float) rtrim($operand, '%')) / 100;
}
// Check for dates
if (($dateValueOperand = Date::stringToExcel($operand)) !== false) {
return $dateValueOperand;
}
return $operand;
}
/**
* NULL.
*
* Returns the error value #NULL!
*
* @deprecated 1.23.0 Use the null() method in the Information\ExcelError class instead
* @see Information\ExcelError::null()
*
* @return string #NULL!
*/
public static function null()
{
return Information\ExcelError::null();
}
/**
* NaN.
*
* Returns the error value #NUM!
*
* @deprecated 1.23.0 Use the NAN() method in the Information\Error class instead
* @see Information\ExcelError::NAN()
*
* @return string #NUM!
*/
public static function NAN()
{
return Information\ExcelError::NAN();
}
/**
* REF.
*
* Returns the error value #REF!
*
* @deprecated 1.23.0 Use the REF() method in the Information\ExcelError class instead
* @see Information\ExcelError::REF()
*
* @return string #REF!
*/
public static function REF()
{
return Information\ExcelError::REF();
}
/**
* NA.
*
* Excel Function:
* =NA()
*
* Returns the error value #N/A
* #N/A is the error value that means "no value is available."
*
* @deprecated 1.23.0 Use the NA() method in the Information\ExcelError class instead
* @see Information\ExcelError::NA()
*
* @return string #N/A!
*/
public static function NA()
{
return Information\ExcelError::NA();
}
/**
* VALUE.
*
* Returns the error value #VALUE!
*
* @deprecated 1.23.0 Use the VALUE() method in the Information\ExcelError class instead
* @see Information\ExcelError::VALUE()
*
* @return string #VALUE!
*/
public static function VALUE()
{
return Information\ExcelError::VALUE();
}
/**
* NAME.
*
* Returns the error value #NAME?
*
* @deprecated 1.23.0 Use the NAME() method in the Information\ExcelError class instead
* @see Information\ExcelError::NAME()
*
* @return string #NAME?
*/
public static function NAME()
{
return Information\ExcelError::NAME();
}
/**
* DIV0.
*
* @deprecated 1.23.0 Use the DIV0() method in the Information\ExcelError class instead
* @see Information\ExcelError::DIV0()
*
* @return string #Not Yet Implemented
*/
public static function DIV0()
{
return Information\ExcelError::DIV0();
}
/**
* ERROR_TYPE.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the type() method in the Information\ExcelError class instead
* @see Information\ExcelError::type()
*
* @return array|int|string
*/
public static function errorType($value = '')
{
return Information\ExcelError::type($value);
}
/**
* IS_BLANK.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isBlank() method in the Information\Value class instead
* @see Information\Value::isBlank()
*
* @return array|bool
*/
public static function isBlank($value = null)
{
return Information\Value::isBlank($value);
}
/**
* IS_ERR.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isErr() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isErr()
*
* @return array|bool
*/
public static function isErr($value = '')
{
return Information\ErrorValue::isErr($value);
}
/**
* IS_ERROR.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isError() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isError()
*
* @return array|bool
*/
public static function isError($value = '')
{
return Information\ErrorValue::isError($value);
}
/**
* IS_NA.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isNa() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isNa()
*
* @return array|bool
*/
public static function isNa($value = '')
{
return Information\ErrorValue::isNa($value);
}
/**
* IS_EVEN.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isEven() method in the Information\Value class instead
* @see Information\Value::isEven()
*
* @return array|bool|string
*/
public static function isEven($value = null)
{
return Information\Value::isEven($value);
}
/**
* IS_ODD.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isOdd() method in the Information\Value class instead
* @see Information\Value::isOdd()
*
* @return array|bool|string
*/
public static function isOdd($value = null)
{
return Information\Value::isOdd($value);
}
/**
* IS_NUMBER.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isNumber() method in the Information\Value class instead
* @see Information\Value::isNumber()
*
* @return array|bool
*/
public static function isNumber($value = null)
{
return Information\Value::isNumber($value);
}
/**
* IS_LOGICAL.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isLogical() method in the Information\Value class instead
* @see Information\Value::isLogical()
*
* @return array|bool
*/
public static function isLogical($value = null)
{
return Information\Value::isLogical($value);
}
/**
* IS_TEXT.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isText() method in the Information\Value class instead
* @see Information\Value::isText()
*
* @return array|bool
*/
public static function isText($value = null)
{
return Information\Value::isText($value);
}
/**
* IS_NONTEXT.
*
* @param mixed $value Value to check
*
* @deprecated 1.23.0 Use the isNonText() method in the Information\Value class instead
* @see Information\Value::isNonText()
*
* @return array|bool
*/
public static function isNonText($value = null)
{
return Information\Value::isNonText($value);
}
/**
* N.
*
* Returns a value converted to a number
*
* @deprecated 1.23.0 Use the asNumber() method in the Information\Value class instead
* @see Information\Value::asNumber()
*
* @param null|mixed $value The value you want converted
*
* @return number|string N converts values listed in the following table
* If value is or refers to N returns
* A number That number
* A date The serial number of that date
* TRUE 1
* FALSE 0
* An error value The error value
* Anything else 0
*/
public static function n($value = null)
{
return Information\Value::asNumber($value);
}
/**
* TYPE.
*
* Returns a number that identifies the type of a value
*
* @deprecated 1.23.0 Use the type() method in the Information\Value class instead
* @see Information\Value::type()
*
* @param null|mixed $value The value you want tested
*
* @return number N converts values listed in the following table
* If value is or refers to N returns
* A number 1
* Text 2
* Logical Value 4
* An error value 16
* Array or Matrix 64
*/
public static function TYPE($value = null)
{
return Information\Value::type($value);
}
/**
* Convert a multi-dimensional array to a simple 1-dimensional array.
*
* @param array|mixed $array Array to be flattened
*
* @return array Flattened array
*/
public static function flattenArray($array)
{
if (!is_array($array)) {
return (array) $array;
}
$flattened = [];
$stack = array_values($array);
while (!empty($stack)) {
$value = array_shift($stack);
if (is_array($value)) {
array_unshift($stack, ...array_values($value));
} else {
$flattened[] = $value;
}
}
return $flattened;
}
/**
* @param mixed $value
*
* @return null|mixed
*/
public static function scalar($value)
{
if (!is_array($value)) {
return $value;
}
do {
$value = array_pop($value);
} while (is_array($value));
return $value;
}
/**
* Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing.
*
* @param array|mixed $array Array to be flattened
*
* @return array Flattened array
*/
public static function flattenArrayIndexed($array)
{
if (!is_array($array)) {
return (array) $array;
}
$arrayValues = [];
foreach ($array as $k1 => $value) {
if (is_array($value)) {
foreach ($value as $k2 => $val) {
if (is_array($val)) {
foreach ($val as $k3 => $v) {
$arrayValues[$k1 . '.' . $k2 . '.' . $k3] = $v;
}
} else {
$arrayValues[$k1 . '.' . $k2] = $val;
}
}
} else {
$arrayValues[$k1] = $value;
}
}
return $arrayValues;
}
/**
* Convert an array to a single scalar value by extracting the first element.
*
* @param mixed $value Array or scalar value
*
* @return mixed
*/
public static function flattenSingleValue($value = '')
{
while (is_array($value)) {
$value = array_shift($value);
}
return $value;
}
/**
* ISFORMULA.
*
* @deprecated 1.23.0 Use the isFormula() method in the Information\Value class instead
* @see Information\Value::isFormula()
*
* @param mixed $cellReference The cell to check
* @param ?Cell $cell The current cell (containing this formula)
*
* @return array|bool|string
*/
public static function isFormula($cellReference = '', ?Cell $cell = null)
{
return Information\Value::isFormula($cellReference, $cell);
}
public static function expandDefinedName(string $coordinate, Cell $cell): string
{
$worksheet = $cell->getWorksheet();
$spreadsheet = $worksheet->getParent();
// Uppercase coordinate
$pCoordinatex = strtoupper($coordinate);
// Eliminate leading equal sign
$pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex);
$defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet);
if ($defined !== null) {
$worksheet2 = $defined->getWorkSheet();
if (!$defined->isFormula() && $worksheet2 !== null) {
$coordinate = "'" . $worksheet2->getTitle() . "'!" .
(string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue()));
}
}
return $coordinate;
}
public static function trimTrailingRange(string $coordinate): string
{
return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate);
}
public static function trimSheetFromCellReference(string $coordinate): string
{
if (strpos($coordinate, '!') !== false) {
$coordinate = substr($coordinate, strrpos($coordinate, '!') + 1);
}
return $coordinate;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
class ErrorValue
{
use ArrayEnabled;
/**
* IS_ERR.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isErr($value = '')
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return self::isError($value) && (!self::isNa(($value)));
}
/**
* IS_ERROR.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isError($value = '')
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (!is_string($value)) {
return false;
}
return in_array($value, ExcelError::ERROR_CODES, true);
}
/**
* IS_NA.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isNa($value = '')
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return $value === ExcelError::NA();
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
class ExcelError
{
use ArrayEnabled;
/**
* List of error codes.
*
* @var array<string, string>
*/
public const ERROR_CODES = [
'null' => '#NULL!', // 1
'divisionbyzero' => '#DIV/0!', // 2
'value' => '#VALUE!', // 3
'reference' => '#REF!', // 4
'name' => '#NAME?', // 5
'num' => '#NUM!', // 6
'na' => '#N/A', // 7
'gettingdata' => '#GETTING_DATA', // 8
'spill' => '#SPILL!', // 9
'connect' => '#CONNECT!', //10
'blocked' => '#BLOCKED!', //11
'unknown' => '#UNKNOWN!', //12
'field' => '#FIELD!', //13
'calculation' => '#CALC!', //14
];
/**
* List of error codes. Replaced by constant;
* previously it was public and updateable, allowing
* user to make inappropriate alterations.
*
* @deprecated 1.25.0 Use ERROR_CODES constant instead.
*
* @var array<string, string>
*/
public static $errorCodes = self::ERROR_CODES;
/**
* @param mixed $value
*/
public static function throwError($value): string
{
return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value'];
}
/**
* ERROR_TYPE.
*
* @param mixed $value Value to check
*
* @return array|int|string
*/
public static function type($value = '')
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
$i = 1;
foreach (self::ERROR_CODES as $errorCode) {
if ($value === $errorCode) {
return $i;
}
++$i;
}
return self::NA();
}
/**
* NULL.
*
* Returns the error value #NULL!
*
* @return string #NULL!
*/
public static function null(): string
{
return self::ERROR_CODES['null'];
}
/**
* NaN.
*
* Returns the error value #NUM!
*
* @return string #NUM!
*/
public static function NAN(): string
{
return self::ERROR_CODES['num'];
}
/**
* REF.
*
* Returns the error value #REF!
*
* @return string #REF!
*/
public static function REF(): string
{
return self::ERROR_CODES['reference'];
}
/**
* NA.
*
* Excel Function:
* =NA()
*
* Returns the error value #N/A
* #N/A is the error value that means "no value is available."
*
* @return string #N/A!
*/
public static function NA(): string
{
return self::ERROR_CODES['na'];
}
/**
* VALUE.
*
* Returns the error value #VALUE!
*
* @return string #VALUE!
*/
public static function VALUE(): string
{
return self::ERROR_CODES['value'];
}
/**
* NAME.
*
* Returns the error value #NAME?
*
* @return string #NAME?
*/
public static function NAME(): string
{
return self::ERROR_CODES['name'];
}
/**
* DIV0.
*
* @return string #DIV/0!
*/
public static function DIV0(): string
{
return self::ERROR_CODES['divisionbyzero'];
}
/**
* CALC.
*
* @return string #CALC!
*/
public static function CALC(): string
{
return self::ERROR_CODES['calculation'];
}
}

View File

@@ -0,0 +1,328 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\NamedRange;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Value
{
use ArrayEnabled;
/**
* IS_BLANK.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isBlank($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return $value === null;
}
/**
* IS_REF.
*
* @param mixed $value Value to check
*
* @return bool
*/
public static function isRef($value, ?Cell $cell = null)
{
if ($cell === null || $value === $cell->getCoordinate()) {
return false;
}
$cellValue = Functions::trimTrailingRange($value);
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/ui', $cellValue) === 1) {
[$worksheet, $cellValue] = Worksheet::extractSheetTitle($cellValue, true);
if (!empty($worksheet) && $cell->getWorksheet()->getParent()->getSheetByName($worksheet) === null) {
return false;
}
[$column, $row] = Coordinate::indexesFromString($cellValue);
if ($column > 16384 || $row > 1048576) {
return false;
}
return true;
}
$namedRange = $cell->getWorksheet()->getParent()->getNamedRange($value);
return $namedRange instanceof NamedRange;
}
/**
* IS_EVEN.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isEven($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if ($value === null) {
return ExcelError::NAME();
} elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) {
return ExcelError::VALUE();
}
return ((int) fmod($value, 2)) === 0;
}
/**
* IS_ODD.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool|string
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isOdd($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if ($value === null) {
return ExcelError::NAME();
} elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) {
return ExcelError::VALUE();
}
return ((int) fmod($value, 2)) !== 0;
}
/**
* IS_NUMBER.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isNumber($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (is_string($value)) {
return false;
}
return is_numeric($value);
}
/**
* IS_LOGICAL.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isLogical($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return is_bool($value);
}
/**
* IS_TEXT.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isText($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return is_string($value) && !ErrorValue::isError($value);
}
/**
* IS_NONTEXT.
*
* @param mixed $value Value to check
* Or can be an array of values
*
* @return array|bool
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function isNonText($value = null)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return !self::isText($value);
}
/**
* ISFORMULA.
*
* @param mixed $cellReference The cell to check
* @param ?Cell $cell The current cell (containing this formula)
*
* @return array|bool|string
*/
public static function isFormula($cellReference = '', ?Cell $cell = null)
{
if ($cell === null) {
return ExcelError::REF();
}
$fullCellReference = Functions::expandDefinedName((string) $cellReference, $cell);
if (strpos($cellReference, '!') !== false) {
$cellReference = Functions::trimSheetFromCellReference($cellReference);
$cellReferences = Coordinate::extractAllCellReferencesInRange($cellReference);
if (count($cellReferences) > 1) {
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $cellReferences, $cell);
}
}
$fullCellReference = Functions::trimTrailingRange($fullCellReference);
preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $fullCellReference, $matches);
$fullCellReference = $matches[6] . $matches[7];
$worksheetName = str_replace("''", "'", trim($matches[2], "'"));
$worksheet = (!empty($worksheetName))
? $cell->getWorksheet()->getParent()->getSheetByName($worksheetName)
: $cell->getWorksheet();
return ($worksheet !== null) ? $worksheet->getCell($fullCellReference)->isFormula() : ExcelError::REF();
}
/**
* N.
*
* Returns a value converted to a number
*
* @param null|mixed $value The value you want converted
*
* @return number|string N converts values listed in the following table
* If value is or refers to N returns
* A number That number value
* A date The Excel serialized number of that date
* TRUE 1
* FALSE 0
* An error value The error value
* Anything else 0
*/
public static function asNumber($value = null)
{
while (is_array($value)) {
$value = array_shift($value);
}
switch (gettype($value)) {
case 'double':
case 'float':
case 'integer':
return $value;
case 'boolean':
return (int) $value;
case 'string':
// Errors
if ((strlen($value) > 0) && ($value[0] == '#')) {
return $value;
}
break;
}
return 0;
}
/**
* TYPE.
*
* Returns a number that identifies the type of a value
*
* @param null|mixed $value The value you want tested
*
* @return number N converts values listed in the following table
* If value is or refers to N returns
* A number 1
* Text 2
* Logical Value 4
* An error value 16
* Array or Matrix 64
*/
public static function type($value = null)
{
$value = Functions::flattenArrayIndexed($value);
if (is_array($value) && (count($value) > 1)) {
end($value);
$a = key($value);
// Range of cells is an error
if (Functions::isCellValue($a)) {
return 16;
// Test for Matrix
} elseif (Functions::isMatrixValue($a)) {
return 64;
}
} elseif (empty($value)) {
// Empty Cell
return 1;
}
$value = Functions::flattenSingleValue($value);
if (($value === null) || (is_float($value)) || (is_int($value))) {
return 1;
} elseif (is_bool($value)) {
return 4;
} elseif (is_array($value)) {
return 64;
} elseif (is_string($value)) {
// Errors
if ((strlen($value) > 0) && ($value[0] == '#')) {
return 16;
}
return 2;
}
return 0;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class MakeMatrix
{
public static function make(...$args): array
{
return $args;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class WildcardMatch
{
private const SEARCH_SET = [
'~~', // convert double tilde to unprintable value
'~\\*', // convert tilde backslash asterisk to [*] (matches literal asterisk in regexp)
'\\*', // convert backslash asterisk to .* (matches string of any length in regexp)
'~\\?', // convert tilde backslash question to [?] (matches literal question mark in regexp)
'\\?', // convert backslash question to . (matches one character in regexp)
"\x1c", // convert original double tilde to single tilde
];
private const REPLACEMENT_SET = [
"\x1c",
'[*]',
'.*',
'[?]',
'.',
'~',
];
public static function wildcard(string $wildcard): string
{
// Preg Escape the wildcard, but protecting the Excel * and ? search characters
return str_replace(self::SEARCH_SET, self::REPLACEMENT_SET, preg_quote($wildcard, '/'));
}
public static function compare(?string $value, string $wildcard): bool
{
if ($value === '' || $value === null) {
return false;
}
return (bool) preg_match("/^{$wildcard}\$/mui", $value);
}
}

View File

@@ -0,0 +1,303 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Logical\Boolean;
/**
* @deprecated 1.17.0
*/
class Logical
{
/**
* TRUE.
*
* Returns the boolean TRUE.
*
* Excel Function:
* =TRUE()
*
* @deprecated 1.17.0
* Use the TRUE() method in the Logical\Boolean class instead
* @see Logical\Boolean::TRUE()
*
* @return bool True
*/
public static function true(): bool
{
return Boolean::true();
}
/**
* FALSE.
*
* Returns the boolean FALSE.
*
* Excel Function:
* =FALSE()
*
* @deprecated 1.17.0
* Use the FALSE() method in the Logical\Boolean class instead
* @see Logical\Boolean::FALSE()
*
* @return bool False
*/
public static function false(): bool
{
return Boolean::false();
}
/**
* LOGICAL_AND.
*
* Returns boolean TRUE if all its arguments are TRUE; returns FALSE if one or more argument is FALSE.
*
* Excel Function:
* =AND(logical1[,logical2[, ...]])
*
* The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays
* or references that contain logical values.
*
* Boolean arguments are treated as True or False as appropriate
* Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False
* If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string
* holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value
*
* @deprecated 1.17.0
* Use the logicalAnd() method in the Logical\Operations class instead
* @see Logical\Operations::logicalAnd()
*
* @param mixed ...$args Data values
*
* @return bool|string the logical AND of the arguments
*/
public static function logicalAnd(...$args)
{
return Logical\Operations::logicalAnd(...$args);
}
/**
* LOGICAL_OR.
*
* Returns boolean TRUE if any argument is TRUE; returns FALSE if all arguments are FALSE.
*
* Excel Function:
* =OR(logical1[,logical2[, ...]])
*
* The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays
* or references that contain logical values.
*
* Boolean arguments are treated as True or False as appropriate
* Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False
* If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string
* holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value
*
* @deprecated 1.17.0
* Use the logicalOr() method in the Logical\Operations class instead
* @see Logical\Operations::logicalOr()
*
* @param mixed $args Data values
*
* @return bool|string the logical OR of the arguments
*/
public static function logicalOr(...$args)
{
return Logical\Operations::logicalOr(...$args);
}
/**
* LOGICAL_XOR.
*
* Returns the Exclusive Or logical operation for one or more supplied conditions.
* i.e. the Xor function returns TRUE if an odd number of the supplied conditions evaluate to TRUE,
* and FALSE otherwise.
*
* Excel Function:
* =XOR(logical1[,logical2[, ...]])
*
* The arguments must evaluate to logical values such as TRUE or FALSE, or the arguments must be arrays
* or references that contain logical values.
*
* Boolean arguments are treated as True or False as appropriate
* Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False
* If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string
* holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value
*
* @deprecated 1.17.0
* Use the logicalXor() method in the Logical\Operations class instead
* @see Logical\Operations::logicalXor()
*
* @param mixed $args Data values
*
* @return bool|string the logical XOR of the arguments
*/
public static function logicalXor(...$args)
{
return Logical\Operations::logicalXor(...$args);
}
/**
* NOT.
*
* Returns the boolean inverse of the argument.
*
* Excel Function:
* =NOT(logical)
*
* The argument must evaluate to a logical value such as TRUE or FALSE
*
* Boolean arguments are treated as True or False as appropriate
* Integer or floating point arguments are treated as True, except for 0 or 0.0 which are False
* If any argument value is a string, or a Null, the function returns a #VALUE! error, unless the string
* holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value
*
* @deprecated 1.17.0
* Use the NOT() method in the Logical\Operations class instead
* @see Logical\Operations::NOT()
*
* @param mixed $logical A value or expression that can be evaluated to TRUE or FALSE
*
* @return array|bool|string the boolean inverse of the argument
*/
public static function NOT($logical = false)
{
return Logical\Operations::NOT($logical);
}
/**
* STATEMENT_IF.
*
* Returns one value if a condition you specify evaluates to TRUE and another value if it evaluates to FALSE.
*
* Excel Function:
* =IF(condition[,returnIfTrue[,returnIfFalse]])
*
* Condition is any value or expression that can be evaluated to TRUE or FALSE.
* For example, A10=100 is a logical expression; if the value in cell A10 is equal to 100,
* the expression evaluates to TRUE. Otherwise, the expression evaluates to FALSE.
* This argument can use any comparison calculation operator.
* ReturnIfTrue is the value that is returned if condition evaluates to TRUE.
* For example, if this argument is the text string "Within budget" and the condition argument
* evaluates to TRUE, then the IF function returns the text "Within budget"
* If condition is TRUE and ReturnIfTrue is blank, this argument returns 0 (zero).
* To display the word TRUE, use the logical value TRUE for this argument.
* ReturnIfTrue can be another formula.
* ReturnIfFalse is the value that is returned if condition evaluates to FALSE.
* For example, if this argument is the text string "Over budget" and the condition argument
* evaluates to FALSE, then the IF function returns the text "Over budget".
* If condition is FALSE and ReturnIfFalse is omitted, then the logical value FALSE is returned.
* If condition is FALSE and ReturnIfFalse is blank, then the value 0 (zero) is returned.
* ReturnIfFalse can be another formula.
*
* @deprecated 1.17.0
* Use the statementIf() method in the Logical\Conditional class instead
* @see Logical\Conditional::statementIf()
*
* @param mixed $condition Condition to evaluate
* @param mixed $returnIfTrue Value to return when condition is true
* @param mixed $returnIfFalse Optional value to return when condition is false
*
* @return mixed The value of returnIfTrue or returnIfFalse determined by condition
*/
public static function statementIf($condition = true, $returnIfTrue = 0, $returnIfFalse = false)
{
return Logical\Conditional::statementIf($condition, $returnIfTrue, $returnIfFalse);
}
/**
* STATEMENT_SWITCH.
*
* Returns corresponding with first match (any data type such as a string, numeric, date, etc).
*
* Excel Function:
* =SWITCH (expression, value1, result1, value2, result2, ... value_n, result_n [, default])
*
* Expression
* The expression to compare to a list of values.
* value1, value2, ... value_n
* A list of values that are compared to expression.
* The SWITCH function is looking for the first value that matches the expression.
* result1, result2, ... result_n
* A list of results. The SWITCH function returns the corresponding result when a value
* matches expression.
* default
* Optional. It is the default to return if expression does not match any of the values
* (value1, value2, ... value_n).
*
* @deprecated 1.17.0
* Use the statementSwitch() method in the Logical\Conditional class instead
* @see Logical\Conditional::statementSwitch()
*
* @param mixed $arguments Statement arguments
*
* @return mixed The value of matched expression
*/
public static function statementSwitch(...$arguments)
{
return Logical\Conditional::statementSwitch(...$arguments);
}
/**
* IFERROR.
*
* Excel Function:
* =IFERROR(testValue,errorpart)
*
* @deprecated 1.17.0
* Use the IFERROR() method in the Logical\Conditional class instead
* @see Logical\Conditional::IFERROR()
*
* @param mixed $testValue Value to check, is also the value returned when no error
* @param mixed $errorpart Value to return when testValue is an error condition
*
* @return mixed The value of errorpart or testValue determined by error condition
*/
public static function IFERROR($testValue = '', $errorpart = '')
{
return Logical\Conditional::IFERROR($testValue, $errorpart);
}
/**
* IFNA.
*
* Excel Function:
* =IFNA(testValue,napart)
*
* @deprecated 1.17.0
* Use the IFNA() method in the Logical\Conditional class instead
* @see Logical\Conditional::IFNA()
*
* @param mixed $testValue Value to check, is also the value returned when not an NA
* @param mixed $napart Value to return when testValue is an NA condition
*
* @return mixed The value of errorpart or testValue determined by error condition
*/
public static function IFNA($testValue = '', $napart = '')
{
return Logical\Conditional::IFNA($testValue, $napart);
}
/**
* IFS.
*
* Excel Function:
* =IFS(testValue1;returnIfTrue1;testValue2;returnIfTrue2;...;testValue_n;returnIfTrue_n)
*
* testValue1 ... testValue_n
* Conditions to Evaluate
* returnIfTrue1 ... returnIfTrue_n
* Value returned if corresponding testValue (nth) was true
*
* @deprecated 1.17.0
* Use the IFS() method in the Logical\Conditional class instead
* @see Logical\Conditional::IFS()
*
* @param mixed ...$arguments Statement arguments
*
* @return mixed|string The value of returnIfTrue_n, if testValue_n was true. #N/A if none of testValues was true
*/
public static function IFS(...$arguments)
{
return Logical\Conditional::IFS(...$arguments);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Logical;
class Boolean
{
/**
* TRUE.
*
* Returns the boolean TRUE.
*
* Excel Function:
* =TRUE()
*
* @return bool True
*/
public static function true(): bool
{
return true;
}
/**
* FALSE.
*
* Returns the boolean FALSE.
*
* Excel Function:
* =FALSE()
*
* @return bool False
*/
public static function false(): bool
{
return false;
}
}

Some files were not shown because too many files have changed in this diff Show More