Livraison d ela gestion des opérations v0.4.0

This commit is contained in:
d6soft
2025-06-24 13:01:43 +02:00
parent b9672a6228
commit 7763d02fae
819 changed files with 306790 additions and 145462 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
# Want to contribute?
If you would like to contribute, here are some notes and guidelines:
- All new development should be on feature/fix branches, which are then merged to the `master` branch once stable and approved; so the `master` branch is always the most up-to-date, working code
- If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number
- The code must work with all PHP versions that we support.
- You can call `composer versions` to test version compatibility.
- Code style should be maintained.
- `composer style` will identify any issues with Coding Style`.
- `composer fix` will fix most issues with Coding Style.
- All code changes must be validated by `composer check`.
- Please include Unit Tests to verify that a bug exists, and that this PR fixes it.
- Please include Unit Tests to show that a new Feature works as expected.
- Please don't "bundle" several changes into a single PR; submit a PR for each discrete change/fix.
- Remember to update documentation if necessary.
- [Helpful article about forking](https://help.github.com/articles/fork-a-repo/ "Forking a GitHub repository")
- [Helpful article about pull requests](https://help.github.com/articles/using-pull-requests/ "Pull Requests")
## Unit Tests
When writing Unit Tests, please
- Always try to write Unit Tests for both the happy and unhappy paths.
- Put all assertions in the Test itself, not in an abstract class that the Test extends (even if this means code duplication between tests).
- Include any necessary `setup()` and `tearDown()` in the Test itself.
- If you change any global settings (such as system locale, or Compatibility Mode for Excel Function tests), make sure that you reset to the default in the `tearDown()`.
- Use the `ExcelError` functions in assertions for Excel Error values in Excel Function implementations.
<br />Not only does it reduce the risk of typos; but at some point in the future, ExcelError values will be an object rather than a string, and we won't then need to update all the tests.
- Don't over-complicate test code by testing happy and unhappy paths in the same test.
This makes it easier to see exactly what is being tested when reviewing the PR. I want to be able to see it in the PR, not have to hunt in other unchanged classes to see what the test is doing.
## How to release
1. Complete CHANGELOG.md and commit
2. Create an annotated tag
1. `git tag -a 1.2.3`
2. Tag subject must be the version number, eg: `1.2.3`
3. Tag body must be a copy-paste of the changelog entries.
3. Push the tag with `git push --tags`, GitHub Actions will create a GitHub release automatically, and the release details will automatically be sent to packagist.
4. Github seems to remove markdown headings in the Release Notes, so you should edit to restore these.
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.)

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2025 PhpSpreadsheet Authors
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.

View File

@@ -0,0 +1,70 @@
# PhpSpreadsheet
[![Build Status](https://github.com/PHPOffice/PhpSpreadsheet/workflows/main/badge.svg)](https://github.com/PHPOffice/PhpSpreadsheet/actions)
[![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master)
[![Total Downloads](https://img.shields.io/packagist/dt/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet)
[![Latest Stable Version](https://img.shields.io/github/v/release/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet)
[![License](https://img.shields.io/github/license/PHPOffice/PhpSpreadsheet)](https://packagist.org/packages/phpoffice/phpspreadsheet)
[![Join the chat at https://gitter.im/PHPOffice/PhpSpreadsheet](https://img.shields.io/badge/GITTER-join%20chat-green.svg)](https://gitter.im/PHPOffice/PhpSpreadsheet)
PhpSpreadsheet is a library written in pure PHP and offers a set of classes that
allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc.
## Installation
See the [install instructions](https://phpspreadsheet.readthedocs.io/en/latest/#installation).
## Documentation
Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet).
Please ask your support questions on [StackOverflow](https://stackoverflow.com/questions/tagged/phpspreadsheet), or have a quick chat on [Gitter](https://gitter.im/PHPOffice/PhpSpreadsheet).
## Patreon
I am now running a [Patreon](https://www.patreon.com/MarkBaker) to support the work that I do on PhpSpreadsheet.
Supporters will receive access to articles about working with PhpSpreadsheet, and how to use some of its more advanced features.
Posts already available to Patreon supporters:
- The Dating Game
- A look at how MS Excel (and PhpSpreadsheet) handle date and time values.
- Looping the Loop
- Advice on Iterating through the rows and cells in a worksheet.
And for Patrons at levels actively using PhpSpreadsheet:
- Behind the Mask
- A look at Number Format Masks.
The Next Article (currently Work in Progress):
- Formula for Success
- How to debug formulae that don't produce the expected result.
My aim is to post at least one article each month, taking a detailed look at some feature of MS Excel and how to use that feature in PhpSpreadsheet, or on how to perform different activities in PhpSpreadsheet.
Planned posts for the future include topics like:
- Tables
- Structured References
- AutoFiltering
- Array Formulae
- Conditional Formatting
- Data Validation
- Value Binders
- Images
- Charts
After a period of six months exclusive to Patreon supporters, articles will be incorporated into the public documentation for the library.
## PHPExcel vs PhpSpreadsheet ?
PhpSpreadsheet is the next version of PHPExcel. It breaks compatibility to dramatically improve the code base quality (namespaces, PSR compliance, use of latest PHP language features, etc.).
Because all efforts have shifted to PhpSpreadsheet, PHPExcel will no longer be maintained. All contributions for PHPExcel, patches and new features, should target PhpSpreadsheet `master` branch.
Do you need to migrate? There is [an automated tool](/docs/topics/migration-from-PHPExcel.md) for that.
## License
PhpSpreadsheet is licensed under [MIT](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/LICENSE).

View File

@@ -0,0 +1,121 @@
{
"name": "phpoffice/phpspreadsheet",
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"keywords": [
"PHP",
"OpenXML",
"Excel",
"xlsx",
"xls",
"ods",
"gnumeric",
"spreadsheet"
],
"config": {
"platform": {
"php" : "8.1.99"
},
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"scripts": {
"check": [
"./bin/check-phpdoc-types",
"phpcs samples/ src/ tests/ --report=checkstyle",
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n",
"php-cs-fixer fix --ansi --dry-run --diff",
"phpunit --color=always",
"phpstan analyse --ansi --memory-limit=2048M"
],
"style": [
"phpcs samples/ src/ tests/ --report=checkstyle",
"php-cs-fixer fix --ansi --dry-run --diff"
],
"fix": [
"phpcbf samples/ src/ tests/ --report=checkstyle",
"php-cs-fixer fix"
],
"versions": [
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n"
]
},
"require": {
"php": "^8.1",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"composer/pcre": "^1 || ^2 || ^3",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.6 || ^10.5",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"ext-intl": "PHP Internationalization Functions",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers"
},
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"autoload-dev": {
"psr-4": {
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests",
"PhpOffice\\PhpSpreadsheetInfra\\": "infra"
}
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentHelper;
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentProcessor;
trait ArrayEnabled
{
private static bool $initializationNeeded = true;
private static ArrayArgumentHelper $arrayArgumentHelper;
/**
* @param array|false $arguments Can be changed to array for Php8.1+
*/
private static function initialiseHelper($arguments): void
{
if (self::$initializationNeeded === true) {
self::$arrayArgumentHelper = new ArrayArgumentHelper();
self::$initializationNeeded = false;
}
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().
*/
protected static function evaluateArrayArguments(callable $method, mixed ...$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.
*/
protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, mixed ...$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);
}
private static function testFalse(mixed $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.
*/
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, mixed ...$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.
*/
protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, mixed ...$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,136 @@
<?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(?string $str1, ?string $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(?string $str1, ?string $str2): int
{
return strcmp($str1 ?? '', $str2 ?? '');
}
public static function compare(mixed $operand1, mixed $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);
}
private static function evaluateComparison(mixed $operand1, mixed $operand2, string $operator, bool $useLowercaseFirstComparison): bool
{
return match ($operator) {
'=' => self::equal($operand1, $operand2),
'>' => self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison),
'<' => self::lessThan($operand1, $operand2, $useLowercaseFirstComparison),
'>=' => self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison),
'<=' => self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison),
'<>' => self::notEqual($operand1, $operand2),
default => throw new Exception('Unsupported binary comparison operator'),
};
}
private static function equal(mixed $operand1, mixed $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;
}
private static function greaterThanOrEqual(mixed $operand1, mixed $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;
}
private static function lessThanOrEqual(mixed $operand1, mixed $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;
}
private static function greaterThan(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool
{
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
}
private static function lessThan(mixed $operand1, mixed $operand2, bool $useLowercaseFirstComparison): bool
{
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
}
private static function notEqual(mixed $operand1, mixed $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,44 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|int|float
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Averages::average(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,45 @@
<?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|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): string|int
{
$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,44 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|int
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Counts::COUNTA(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,49 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): null|float|int|string
{
$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,45 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): null|float|string
{
$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,45 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnError = true): float|string|null
{
$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,44 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): string|float
{
$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,45 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): float|string
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return StandardDeviations::STDEV(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,45 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria): float|string
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return StandardDeviations::STDEVP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,44 @@
<?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 null|array|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.
*/
public static function evaluate(array $database, array|null|int|string $field, array $criteria, bool $returnNull = false): null|float|string
{
$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 null|array|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(array $database, array|null|int|string $field, array $criteria): string|float
{
$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 null|array|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(array $database, array|null|int|string $field, array $criteria): string|float
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return ExcelError::VALUE();
}
return Variances::VARP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@@ -0,0 +1,177 @@
<?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
{
abstract public static function evaluate(array $database, array|null|int|string $field, array $criteria): null|float|int|string;
/**
* 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, mixed $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(
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''),
$baseQuery
);
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : ($rowQuery[0] ?? '');
}
private static function buildCondition(mixed $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;
}
private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions): string
{
$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) && str_contains($dataValue, '"')) {
$dataValue = str_replace('"', '""', $dataValue);
}
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
}
return str_replace('[:' . $criterion . ']', $dataValue, $conditions);
}
}

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,60 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
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 DateTime|float|int|string 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(): DateTime|float|int|string
{
$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 DateTime|float|int|string 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(): DateTime|float|int|string
{
$dti = new DateTimeImmutable();
$dateArray = Helpers::dateParse($dti->format('c'));
return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : ExcelError::VALUE();
}
}

View File

@@ -0,0 +1,167 @@
<?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;
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|float|int|string $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|float|int|string $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|float|int|string $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 array|DateTime|float|int|string 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(array|float|int|string $year, array|float|int|string $month, array|float|int|string $day): float|int|DateTime|string|array
{
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);
$month = self::getMonth($month);
$day = self::getDay($day);
self::adjustYearMonth($year, $month, $baseYear);
} catch (Exception $e) {
return $e->getMessage();
}
// Execute function
$excelDateValue = SharedDateHelper::formattedPHPToExcel($year, $month, $day);
return Helpers::returnIn3FormatsFloat($excelDateValue);
}
/**
* Convert year from multiple formats to int.
*/
private static function getYear(mixed $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.
*/
private static function getMonth(mixed $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.
*/
private static function getDay(mixed $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,154 @@
<?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(mixed $dateValue): array|int|string
{
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);
SharedDateHelper::roundMicroseconds($PHPDateObject);
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(mixed $dateValue): array|string|int
{
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);
SharedDateHelper::roundMicroseconds($PHPDateObject);
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(mixed $dateValue): array|string|int
{
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);
SharedDateHelper::roundMicroseconds($PHPDateObject);
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(mixed $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,163 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
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 null|array|bool|float|int|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 array|DateTime|float|int|string 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(null|array|string|int|bool|float $dateValue): array|string|float|int|DateTime
{
if (is_array($dateValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
// try to parse as date iff there is at least one digit
if (is_string($dateValue) && preg_match('/\\d/', $dateValue) !== 1) {
return ExcelError::VALUE();
}
$dti = new DateTimeImmutable();
$baseYear = SharedDateHelper::getExcelCalendar();
$dateValue = trim((string) $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 ((!str_contains((string) $t, ':'))) ? 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 DateTime|float|int|string 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): string|float|int|DateTime
{
$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(array|DateTimeInterface|float|int|string $endDate, array|DateTimeInterface|float|int|string $startDate): array|int|string
{
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(mixed $startDate = 0, mixed $endDate = 0, mixed $method = false): array|string|int
{
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,153 @@
<?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(mixed $startDate, mixed $endDate, array|string $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.
*/
private static function replaceRetValue(bool|int $retVal, string $unit, string $compare): null|bool|int
{
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,287 @@
<?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(int|string $year): bool
{
$year = (int) $year;
return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0);
}
/**
* getDateValue.
*
* @return float Excel date/time serial value
*/
public static function getDateValue(mixed $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.
*
* @return float|string Excel date/time serial value, or string if error
*/
public static function getTimeValue(string $timeValue): string|float
{
$saveReturnDateType = Functions::getReturnDateType();
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
/** @var float|string $timeValue */
$timeValue = TimeValue::fromString($timeValue);
Functions::setReturnDateType($saveReturnDateType);
return $timeValue;
}
/**
* Adjust date by given months.
*/
public static function adjustDateByMonths(mixed $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.
*/
public static function replaceIfEmpty(mixed &$value, mixed $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.
*/
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int
{
$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) : $excelDateValue;
}
// RETURNDATE_UNIX_TIMESTAMP)
return SharedDateHelper::excelToTimestamp($excelDateValue);
}
/**
* Return result in one of three formats.
*/
public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime
{
$retType = Functions::getReturnDateType();
if ($retType === Functions::RETURNDATE_EXCEL) {
return $excelDateValue;
}
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
return SharedDateHelper::excelToTimestamp($excelDateValue);
}
// RETURNDATE_PHP_DATETIME_OBJECT
return SharedDateHelper::excelToDateTimeObject($excelDateValue);
}
/**
* Return result in one of three formats.
*/
public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int
{
$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 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.
*/
public static function nullFalseTrueToNumber(mixed &$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.
*/
public static function validateNumericNull(mixed $number): int|float
{
$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.
*
* @phpstan-assert float $number
*/
public static function validateNotNegative(mixed $number): float
{
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(array|bool $dateArray): array
{
return is_array($dateArray) ? $dateArray : ['error_count' => 1];
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
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|DateTime|float|int|string 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(mixed $dateValue, array|string|bool|float|int $adjustmentMonths): DateTime|float|int|string|array
{
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();
}
$dateValue = floor($dateValue);
$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|DateTime|float|int|string 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(mixed $dateValue, array|float|int|bool|string $adjustmentMonths): array|string|DateTime|float|int
{
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();
}
$dateValue = floor($dateValue);
$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(mixed $startDate, mixed $endDate, mixed ...$dateArgs): array|string|int
{
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 null|array|bool|float|int|string $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 null|array|bool|float|int|string $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 null|array|bool|float|int|string $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|DateTime|float|int|string 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(array|int|float|bool|null|string $hour, array|int|float|bool|null|string $minute, array|int|float|bool|null|string $second): array|string|float|int|DateTime
{
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(mixed $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,135 @@
<?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(mixed $timeValue): array|string|int
{
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);
SharedDateHelper::roundMicroseconds($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(mixed $timeValue): array|string|int
{
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);
SharedDateHelper::roundMicroseconds($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(mixed $timeValue): array|string|int
{
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);
SharedDateHelper::roundMicroseconds($timeValue);
return (int) $timeValue->format('s');
}
}

View File

@@ -0,0 +1,80 @@
<?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 null|array|bool|float|int|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 array|Datetime|float|int|string 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(null|array|string|int|bool|float $timeValue): array|string|Datetime|int|float
{
if (is_array($timeValue)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
// try to parse as time iff there is at least one digit
if (is_string($timeValue) && preg_match('/\\d/', $timeValue) !== 1) {
return ExcelError::VALUE();
}
$timeValue = trim((string) $timeValue, '"');
$timeValue = str_replace(['/', '.'], '-', $timeValue);
$arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: [];
if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) {
$arraySplit[0] = ((int) $arraySplit[0] % 24);
$timeValue = implode(':', $arraySplit);
}
$PHPDateArray = Helpers::dateParse($timeValue);
$retValue = ExcelError::VALUE();
if (Helpers::dateParseSucceeded($PHPDateArray)) {
$hour = $PHPDateArray['hour'];
$minute = $PHPDateArray['minute'];
$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,274 @@
<?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(mixed $dateValue, array|int|string|null $method = Constants::STARTWEEK_SUNDAY): array|int|string
{
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(mixed $dateValue): array|int|string
{
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|bool|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(null|array|float|int|string|bool $dateValue, mixed $style = 1): array|string|int
{
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(mixed $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(mixed $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.
*/
private static function validateDateValue(mixed $dateValue): float
{
if (is_bool($dateValue)) {
throw new Exception(ExcelError::VALUE());
}
return Helpers::getDateValue($dateValue);
}
/**
* Validate method parameter.
*/
private static function validateMethod(mixed $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,198 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use DateTime;
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|DateTime|float|int|string 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(mixed $startDate, array|int|string $endDays, mixed ...$dateArgs): array|float|int|DateTime|string
{
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.
*/
private static function incrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime
{
// 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.
*/
private static function decrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime
{
// 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,124 @@
<?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|int|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(mixed $startDate, mixed $endDate, array|int|string|null $method = 0): array|string|int|float
{
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();
}
return match ($method) {
0 => Functions::scalar(Days360::between($startDate, $endDate)) / 360,
1 => self::method1($startDate, $endDate),
2 => Functions::scalar(Difference::interval($startDate, $endDate)) / 360,
3 => Functions::scalar(Difference::interval($startDate, $endDate)) / 365,
4 => Functions::scalar(Days360::between($startDate, $endDate, true)) / 360,
default => ExcelError::NAN(),
};
}
/**
* Excel 1900 calendar treats date argument of null as 1900-01-00. Really.
*/
private static function excelBug(float $sDate, mixed $startDate, mixed $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,190 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class ArrayArgumentHelper
{
protected int $indexStart = 0;
protected array $arguments;
protected int $argumentCount;
protected array $rows;
protected array $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(
fn ($argument): int => is_countable($argument) ? count($argument) : 1,
$arguments
);
}
private function columns(array $arguments): array
{
return array_map(
function (mixed $argument): int {
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,
fn ($value): bool => $value > 1
);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class ArrayArgumentProcessor
{
private static ArrayArgumentHelper $arrayArgumentHelper;
public static function processArguments(
ArrayArgumentHelper $arrayArgumentHelper,
callable $method,
mixed ...$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!'];
}
private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, mixed ...$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;
}
private static function evaluateMatrixPair(callable $method, array $matrixIndexes, mixed ...$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;
}
private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, mixed ...$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.
*/
private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, mixed ...$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,201 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
class BranchPruner
{
protected bool $branchPruningEnabled;
/**
* Used to generate unique store keys.
*/
private int $branchStoreKeyCounter = 0;
/**
* currently pending storeKey (last item of the storeKeysStack.
*/
protected ?string $pendingStoreKey = null;
/**
* @var string[]
*/
protected array $storeKeysStack = [];
/**
* @var bool[]
*/
protected array $conditionMap = [];
/**
* @var bool[]
*/
protected array $thenMap = [];
/**
* @var bool[]
*/
protected array $elseMap = [];
/**
* @var int[]
*/
protected array $braceDepthMap = [];
protected ?string $currentCondition = null;
protected ?string $currentOnlyIf = null;
protected ?string $currentOnlyIfNot = null;
protected ?string $previousStoreKey = null;
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');
}
}
}
public function closingBrace(mixed $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,65 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
class CyclicReferenceStack
{
/**
* The call stack for calculated cells.
*
* @var mixed[]
*/
private array $stack = [];
/**
* Return the number of entries on the stack.
*/
public function count(): int
{
return count($this->stack);
}
/**
* Push a new entry onto the stack.
*/
public function push(mixed $value): void
{
$this->stack[$value] = $value;
}
/**
* Pop the last entry from the stack.
*/
public function pop(): mixed
{
return array_pop($this->stack);
}
/**
* Test to see if a specified entry exists on the stack.
*
* @param mixed $value The value to test
*/
public function onStack(mixed $value): bool
{
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(): array
{
return $this->stack;
}
}

View File

@@ -0,0 +1,147 @@
<?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';
// preg_quoted string for major currency symbols, with a %s for locale currency
private const CURRENCY_CONVERSION_LIST = '\$€£¥%s';
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
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
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
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
$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
{
$currencyRegexp = self::currencyMatcherRegexp();
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $operand);
$match = [];
if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Determine the sign
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
$decimalSeparator = StringHelper::getDecimalSeparator();
//Cast to a float
$intermediate = (string) ($match['PostfixedValue'] ?? $match['PrefixedValue']);
$intermediate = str_replace($decimalSeparator, '.', $intermediate);
if (is_numeric($intermediate)) {
$operand = (float) ($sign . str_replace($decimalSeparator, '.', $intermediate));
return true;
}
}
return false;
}
public static function currencyMatcherRegexp(): string
{
$currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode(), '/'));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
return '~^(?:(?: *(?<PrefixedSign>[-+])? *(?<PrefixedCurrency>[' . $currencyCodes . ']) *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?<PostfixedCurrency>[' . $currencyCodes . ']) *))$~ui';
}
}

View File

@@ -0,0 +1,126 @@
<?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.
*/
private bool $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.
*/
private bool $echoDebugLog = false;
/**
* The debug log generated by the calculation engine.
*
* @var string[]
*/
private array $debugLog = [];
/**
* The calculation engine cell reference stack.
*/
private CyclicReferenceStack $cellStack;
/**
* Instantiate a Calculation engine logger.
*/
public function __construct(CyclicReferenceStack $stack)
{
$this->cellStack = $stack;
}
/**
* Enable/Disable Calculation engine logging.
*/
public function setWriteDebugLog(bool $writeDebugLog): void
{
$this->writeDebugLog = $writeDebugLog;
}
/**
* Return whether calculation engine logging is enabled or disabled.
*/
public function getWriteDebugLog(): bool
{
return $this->writeDebugLog;
}
/**
* Enable/Disable echoing of debug log information.
*/
public function setEchoDebugLog(bool $echoDebugLog): void
{
$this->echoDebugLog = $echoDebugLog;
}
/**
* Return whether echoing of debug log information is enabled or disabled.
*/
public function getEchoDebugLog(): bool
{
return $this->echoDebugLog;
}
/**
* Write an entry to the calculation engine debug log.
*/
public function writeDebugLog(string $message, mixed ...$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(): array
{
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,355 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use Stringable;
final class StructuredReference implements Operand, Stringable
{
public const NAME = 'Structured Reference';
private const OPEN_BRACE = '[';
private const CLOSE_BRACE = ']';
private const ITEM_SPECIFIER_ALL = '#All';
private const ITEM_SPECIFIER_HEADERS = '#Headers';
private const ITEM_SPECIFIER_DATA = '#Data';
private const ITEM_SPECIFIER_TOTALS = '#Totals';
private const ITEM_SPECIFIER_THIS_ROW = '#This Row';
private const ITEM_SPECIFIER_ROWS_SET = [
self::ITEM_SPECIFIER_ALL,
self::ITEM_SPECIFIER_HEADERS,
self::ITEM_SPECIFIER_DATA,
self::ITEM_SPECIFIER_TOTALS,
];
private const TABLE_REFERENCE = '/([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu';
private string $value;
private string $tableName;
private Table $table;
private string $reference;
private ?int $headersRow;
private int $firstDataRow;
private int $lastDataRow;
private ?int $totalsRow;
private array $columns;
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 (str_contains($srStringRemainder, self::OPEN_BRACE)) {
++$srCount;
}
$val .= $srStringRemainder;
}
return new self($val);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
public function parse(Cell $cell): string
{
$this->getTableStructure($cell);
$cellRange = ($this->isRowReference()) ? $this->getRowReference($cell) : $this->getColumnReference();
$sheetName = '';
$worksheet = $this->table->getWorksheet();
if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) {
$sheetName = "'" . $worksheet->getTitle() . "'!";
}
return $sheetName . $cellRange;
}
private function isRowReference(): bool
{
return str_contains($this->value, '[@')
|| str_contains($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']');
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableStructure(Cell $cell): void
{
preg_match(self::TABLE_REFERENCE, $this->value, $matches);
$this->tableName = $matches[1];
$this->table = ($this->tableName === '')
? $this->getTableForCell($cell)
: $this->getTableByName($cell);
$this->reference = $matches[2];
$tableRange = Coordinate::getRangeBoundaries($this->table->getRange());
$this->headersRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
$this->firstDataRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
$this->totalsRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
$this->lastDataRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
$cellParam = $cell;
$worksheet = $this->table->getWorksheet();
if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) {
$cellParam = $worksheet->getCell('A1');
}
$this->columns = $this->getColumns($cellParam, $tableRange);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableForCell(Cell $cell): Table
{
$tables = $cell->getWorksheet()->getTableCollection();
foreach ($tables as $table) {
/** @var Table $table */
$range = $table->getRange();
if ($cell->isInRange($range) === true) {
$this->tableName = $table->getName();
return $table;
}
}
throw new Exception('Table for Structured Reference cannot be identified');
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getTableByName(Cell $cell): Table
{
$table = $cell->getWorksheet()->getTableByName($this->tableName);
if ($table === null) {
$spreadsheet = $cell->getWorksheet()->getParent();
if ($spreadsheet !== null) {
$table = $spreadsheet->getTableByName($this->tableName);
}
}
if ($table === null) {
throw new Exception("Table {$this->tableName} for Structured Reference cannot be located");
}
return $table;
}
private function getColumns(Cell $cell, array $tableRange): array
{
$worksheet = $cell->getWorksheet();
$cellReference = $cell->getCoordinate();
$columns = [];
$lastColumn = ++$tableRange[1][0];
for ($column = $tableRange[0][0]; $column !== $lastColumn; ++$column) {
$columns[$column] = $worksheet
->getCell($column . ($this->headersRow ?? ($this->firstDataRow - 1)))
->getCalculatedValue();
}
$worksheet->getCell($cellReference);
return $columns;
}
private function getRowReference(Cell $cell): string
{
$reference = str_replace("\u{a0}", ' ', $this->reference);
/** @var string $reference */
$reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
foreach ($this->columns as $columnId => $columnName) {
$columnName = str_replace("\u{a0}", ' ', $columnName);
$reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
}
return $this->validateParsedReference(trim($reference, '[]@, '));
}
private function adjustRowReference(string $columnName, string $reference, Cell $cell, string $columnId): string
{
if ($columnName !== '') {
$cellReference = $columnId . $cell->getRow();
$pattern1 = '/\[' . preg_quote($columnName, '/') . '\]/miu';
$pattern2 = '/@' . preg_quote($columnName, '/') . '/miu';
if (preg_match($pattern1, $reference) === 1) {
$reference = preg_replace($pattern1, $cellReference, $reference);
} elseif (preg_match($pattern2, $reference) === 1) {
$reference = preg_replace($pattern2, $cellReference, $reference);
}
/** @var string $reference */
}
return $reference;
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function getColumnReference(): string
{
$reference = str_replace("\u{a0}", ' ', $this->reference);
$startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow;
$endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow;
[$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
$reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
$reference = trim($reference, '[]@, ');
if (substr_count($reference, ':') > 1) {
$cells = explode(':', $reference);
$firstCell = array_shift($cells);
$lastCell = array_pop($cells);
$reference = "{$firstCell}:{$lastCell}";
}
return $this->validateParsedReference($reference);
}
/**
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
*/
private function validateParsedReference(string $reference): string
{
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
throw new Exception(
"Invalid Structured Reference {$this->reference} {$reference}",
Exception::CALCULATION_ENGINE_PUSH_TO_STACK
);
}
}
return $reference;
}
private function fullData(int $startRow, int $endRow): string
{
$columns = array_keys($this->columns);
$firstColumn = array_shift($columns);
$lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns);
return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}";
}
private function getMinimumRow(string $reference): int
{
return match ($reference) {
self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow,
self::ITEM_SPECIFIER_DATA => $this->firstDataRow,
self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow,
default => $this->headersRow ?? $this->firstDataRow,
};
}
private function getMaximumRow(string $reference): int
{
return match ($reference) {
self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow,
self::ITEM_SPECIFIER_DATA => $this->lastDataRow,
self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow,
default => $this->totalsRow ?? $this->lastDataRow,
};
}
public function value(): string
{
return $this->value;
}
/**
* @return array<int, int>
*/
private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
{
$rowsSelected = false;
foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
$pattern = '/\[' . $rowReference . '\]/mui';
if (preg_match($pattern, $reference) === 1) {
if (($rowReference === self::ITEM_SPECIFIER_HEADERS) && ($this->table->getShowHeaderRow() === false)) {
throw new Exception(
'Table Headers are Hidden, and should not be Referenced',
Exception::CALCULATION_ENGINE_PUSH_TO_STACK
);
}
$rowsSelected = true;
$startRow = min($startRow, $this->getMinimumRow($rowReference));
$endRow = max($endRow, $this->getMaximumRow($rowReference));
$reference = preg_replace($pattern, '', $reference) ?? '';
}
}
if ($rowsSelected === false) {
// If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
$startRow = $this->firstDataRow;
$endRow = $this->lastDataRow;
}
return [$startRow, $endRow];
}
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
{
$columnsSelected = false;
foreach ($this->columns as $columnId => $columnName) {
$columnName = str_replace("\u{a0}", ' ', $columnName ?? '');
$cellFrom = "{$columnId}{$startRow}";
$cellTo = "{$columnId}{$endRow}";
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
$pattern = '/\[' . preg_quote($columnName, '/') . '\]/mui';
if (preg_match($pattern, $reference) === 1) {
$columnsSelected = true;
$reference = preg_replace($pattern, $cellReference, $reference);
}
/** @var string $reference */
}
if ($columnsSelected === false) {
return $this->fullData($startRow, $endRow);
}
return $reference;
}
public function __toString(): string
{
return $this->value;
}
}

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 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(mixed $x, mixed $ord): array|string|float
{
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
{
return match ($ord) {
0 => self::besselI0($x),
1 => self::besselI1($x),
default => 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;
}
private static function besselI2(float $x, int $ord): float
{
if ($x === 0.0) {
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,176 @@
<?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(mixed $x, mixed $ord): array|string|float
{
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
{
return match ($ord) {
0 => self::besselJ0($x),
1 => self::besselJ1($x),
default => 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): float
{
$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): float
{
$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,130 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
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(mixed $x, mixed $ord): array|string|float
{
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
{
return match ($ord) {
0 => self::besselK0($x),
1 => self::besselK1($x),
default => 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,137 @@
<?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(mixed $x, mixed $ord): array|string|float
{
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
{
return match ($ord) {
0 => self::besselY0($x),
1 => self::besselY1($x),
default => 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,247 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
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.
*
* @return int[]
*/
private static function splitNumber(float|int $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 null|array|bool|float|int|string $number1 Or can be an array of values
* @param null|array|bool|float|int|string $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(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float
{
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 null|array|bool|float|int|string $number1 Or can be an array of values
* @param null|array|bool|float|int|string $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(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float
{
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 null|array|bool|float|int|string $number1 Or can be an array of values
* @param null|array|bool|float|int|string $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(null|array|bool|float|int|string $number1, null|array|bool|float|int|string $number2): array|string|int|float
{
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 null|array|bool|float|int|string $number Or can be an array of values
* @param null|array|bool|float|int|string $shiftAmount 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 BITLSHIFT(null|array|bool|float|int|string $number, null|array|bool|float|int|string $shiftAmount): array|string|float
{
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 null|array|bool|float|int|string $number Or can be an array of values
* @param null|array|bool|float|int|string $shiftAmount 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 BITRSHIFT(null|array|bool|float|int|string $number, null|array|bool|float|int|string $shiftAmount): array|string|float
{
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.
*/
private static function validateBitwiseArgument(mixed $value): float
{
$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.
*/
private static function validateShiftAmount(mixed $value): int
{
$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.
*/
private static function nullFalseTrueToNumber(mixed &$number): mixed
{
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|bool|float|int|string $a the first number
* Or can be an array of values
* @param array|bool|float|int|string $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(array|float|bool|string|int $a, array|float|bool|string|int $b = 0.0): array|string|int
{
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|bool|float|int|string $number the value to test against step
* Or can be an array of values
* @param null|array|bool|float|int|string $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(array|float|bool|string|int $number, $step = 0.0): array|string|int
{
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,120 @@
<?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(mixed $realNumber = 0.0, mixed $imaginary = 0.0, mixed $suffix = 'i'): array|string
{
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): array|string|float
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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): array|string|float
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
return ExcelError::NAN();
}
return $complex->getReal();
}
}

View File

@@ -0,0 +1,592 @@
<?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(array|string $complexNumber): array|float|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|float|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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|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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber): array|string
{
if (is_array($complexNumber)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
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(array|string $complexNumber, array|float|int|string $realNumber): array|string
{
if (is_array($complexNumber) || is_array($realNumber)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber, $realNumber);
}
try {
$complex = new ComplexObject($complexNumber);
} catch (ComplexException) {
return ExcelError::NAN();
}
if (!is_numeric($realNumber)) {
return ExcelError::VALUE();
}
return (string) $complex->pow((float) $realNumber);
}
}

View File

@@ -0,0 +1,128 @@
<?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(array|string $complexDividend, array|string $complexDivisor): array|string
{
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) {
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(array|string $complexNumber1, array|string $complexNumber2): array|string
{
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) {
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
*/
public static function IMSUM(...$complexNumbers): string
{
// 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) {
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
*/
public static function IMPRODUCT(...$complexNumbers): string
{
// 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) {
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(mixed $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(mixed $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|bool|float|int|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 && $value[0] === '1') {
// 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|bool|float|int|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 null|array|float|int|string $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): array|string
{
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 && $value[0] === '1') {
$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|bool|float|int|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 null|array|float|int|string $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): array|string
{
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 && $value[0] === '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|bool|float|int|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 null|array|float|int|string $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): array|string
{
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|bool|float|int|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 null|array|float|int|string $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): array|string
{
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|bool|float|int|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): array|string
{
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|bool|float|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): array|string
{
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|bool|float|int|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|bool|float|int|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): array|string
{
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|bool|float|int|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): array|string
{
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|bool|float|int|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|bool|float|int|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): array|string
{
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,679 @@
<?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 array<string, array{Group: string, UnitName: string, AllowPrefix: bool}>
*/
private static array $conversionUnits = [
// Weight and Mass
'g' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Gram', 'AllowPrefix' => true],
'sg' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Slug', 'AllowPrefix' => false],
'lbm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false],
'u' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U (atomic mass unit)', 'AllowPrefix' => true],
'ozm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false],
'grain' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Grain', 'AllowPrefix' => false],
'cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U.S. (short) hundredweight', 'AllowPrefix' => false],
'shweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'U.S. (short) hundredweight', 'AllowPrefix' => false],
'uk_cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false],
'lcwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false],
'hweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial hundredweight', 'AllowPrefix' => false],
'stone' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Stone', 'AllowPrefix' => false],
'ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Ton', 'AllowPrefix' => false],
'uk_ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false],
'LTON' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false],
'brton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'UnitName' => 'Imperial ton', 'AllowPrefix' => false],
// Distance
'm' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Meter', 'AllowPrefix' => true],
'mi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Statute mile', 'AllowPrefix' => false],
'Nmi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Nautical mile', 'AllowPrefix' => false],
'in' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Inch', 'AllowPrefix' => false],
'ft' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Foot', 'AllowPrefix' => false],
'yd' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Yard', 'AllowPrefix' => false],
'ang' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Angstrom', 'AllowPrefix' => true],
'ell' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Ell', 'AllowPrefix' => false],
'ly' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Light Year', 'AllowPrefix' => false],
'parsec' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Parsec', 'AllowPrefix' => false],
'pc' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Parsec', 'AllowPrefix' => false],
'Pica' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/72 in)', 'AllowPrefix' => false],
'Picapt' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/72 in)', 'AllowPrefix' => false],
'pica' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'Pica (1/6 in)', 'AllowPrefix' => false],
'survey_mi' => ['Group' => self::CATEGORY_DISTANCE, 'UnitName' => 'U.S survey mile (statute mile)', 'AllowPrefix' => false],
// Time
'yr' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Year', 'AllowPrefix' => false],
'day' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Day', 'AllowPrefix' => false],
'd' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Day', 'AllowPrefix' => false],
'hr' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Hour', 'AllowPrefix' => false],
'mn' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Minute', 'AllowPrefix' => false],
'min' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Minute', 'AllowPrefix' => false],
'sec' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Second', 'AllowPrefix' => true],
's' => ['Group' => self::CATEGORY_TIME, 'UnitName' => 'Second', 'AllowPrefix' => true],
// Pressure
'Pa' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Pascal', 'AllowPrefix' => true],
'p' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Pascal', 'AllowPrefix' => true],
'atm' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Atmosphere', 'AllowPrefix' => true],
'at' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Atmosphere', 'AllowPrefix' => true],
'mmHg' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'mm of Mercury', 'AllowPrefix' => true],
'psi' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'PSI', 'AllowPrefix' => true],
'Torr' => ['Group' => self::CATEGORY_PRESSURE, 'UnitName' => 'Torr', 'AllowPrefix' => true],
// Force
'N' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Newton', 'AllowPrefix' => true],
'dyn' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Dyne', 'AllowPrefix' => true],
'dy' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Dyne', 'AllowPrefix' => true],
'lbf' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Pound force', 'AllowPrefix' => false],
'pond' => ['Group' => self::CATEGORY_FORCE, 'UnitName' => 'Pond', 'AllowPrefix' => true],
// Energy
'J' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Joule', 'AllowPrefix' => true],
'e' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Erg', 'AllowPrefix' => true],
'c' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Thermodynamic calorie', 'AllowPrefix' => true],
'cal' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'IT calorie', 'AllowPrefix' => true],
'eV' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Electron volt', 'AllowPrefix' => true],
'ev' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Electron volt', 'AllowPrefix' => true],
'HPh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Horsepower-hour', 'AllowPrefix' => false],
'hh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Horsepower-hour', 'AllowPrefix' => false],
'Wh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Watt-hour', 'AllowPrefix' => true],
'wh' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Watt-hour', 'AllowPrefix' => true],
'flb' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'Foot-pound', 'AllowPrefix' => false],
'BTU' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'BTU', 'AllowPrefix' => false],
'btu' => ['Group' => self::CATEGORY_ENERGY, 'UnitName' => 'BTU', 'AllowPrefix' => false],
// Power
'HP' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Horsepower', 'AllowPrefix' => false],
'h' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Horsepower', 'AllowPrefix' => false],
'W' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Watt', 'AllowPrefix' => true],
'w' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Watt', 'AllowPrefix' => true],
'PS' => ['Group' => self::CATEGORY_POWER, 'UnitName' => 'Pferdestärke', 'AllowPrefix' => false],
// Magnetism
'T' => ['Group' => self::CATEGORY_MAGNETISM, 'UnitName' => 'Tesla', 'AllowPrefix' => true],
'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'UnitName' => 'Gauss', 'AllowPrefix' => true],
// Temperature
'C' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Celsius', 'AllowPrefix' => false],
'cel' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Celsius', 'AllowPrefix' => false],
'F' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Fahrenheit', 'AllowPrefix' => false],
'fah' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Fahrenheit', 'AllowPrefix' => false],
'K' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Kelvin', 'AllowPrefix' => false],
'kel' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Kelvin', 'AllowPrefix' => false],
'Rank' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Rankine', 'AllowPrefix' => false],
'Reau' => ['Group' => self::CATEGORY_TEMPERATURE, 'UnitName' => 'Degrees Réaumur', 'AllowPrefix' => false],
// Volume
'l' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true],
'L' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true],
'lt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Litre', 'AllowPrefix' => true],
'tsp' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Teaspoon', 'AllowPrefix' => false],
'tspm' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Modern Teaspoon', 'AllowPrefix' => false],
'tbs' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Tablespoon', 'AllowPrefix' => false],
'oz' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Fluid Ounce', 'AllowPrefix' => false],
'cup' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cup', 'AllowPrefix' => false],
'pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.S. Pint', 'AllowPrefix' => false],
'us_pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.S. Pint', 'AllowPrefix' => false],
'uk_pt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'U.K. Pint', 'AllowPrefix' => false],
'qt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Quart', 'AllowPrefix' => false],
'uk_qt' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Imperial Quart (UK)', 'AllowPrefix' => false],
'gal' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gallon', 'AllowPrefix' => false],
'uk_gal' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Imperial Gallon (UK)', 'AllowPrefix' => false],
'ang3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Angstrom', 'AllowPrefix' => true],
'ang^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Angstrom', 'AllowPrefix' => true],
'barrel' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'US Oil Barrel', 'AllowPrefix' => false],
'bushel' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'US Bushel', 'AllowPrefix' => false],
'in3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Inch', 'AllowPrefix' => false],
'in^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Inch', 'AllowPrefix' => false],
'ft3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Foot', 'AllowPrefix' => false],
'ft^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Foot', 'AllowPrefix' => false],
'ly3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Light Year', 'AllowPrefix' => false],
'ly^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Light Year', 'AllowPrefix' => false],
'm3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Meter', 'AllowPrefix' => true],
'm^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Meter', 'AllowPrefix' => true],
'mi3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Mile', 'AllowPrefix' => false],
'mi^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Mile', 'AllowPrefix' => false],
'yd3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Yard', 'AllowPrefix' => false],
'yd^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Yard', 'AllowPrefix' => false],
'Nmi3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Nautical Mile', 'AllowPrefix' => false],
'Nmi^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Nautical Mile', 'AllowPrefix' => false],
'Pica3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false],
'Pica^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false],
'Picapt3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false],
'Picapt^3' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Cubic Pica', 'AllowPrefix' => false],
'GRT' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gross Registered Ton', 'AllowPrefix' => false],
'regton' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Gross Registered Ton', 'AllowPrefix' => false],
'MTON' => ['Group' => self::CATEGORY_VOLUME, 'UnitName' => 'Measurement Ton (Freight Ton)', 'AllowPrefix' => false],
// Area
'ha' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Hectare', 'AllowPrefix' => true],
'uk_acre' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'International Acre', 'AllowPrefix' => false],
'us_acre' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'US Survey/Statute Acre', 'AllowPrefix' => false],
'ang2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Angstrom', 'AllowPrefix' => true],
'ang^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Angstrom', 'AllowPrefix' => true],
'ar' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Are', 'AllowPrefix' => true],
'ft2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Feet', 'AllowPrefix' => false],
'ft^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Feet', 'AllowPrefix' => false],
'in2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Inches', 'AllowPrefix' => false],
'in^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Inches', 'AllowPrefix' => false],
'ly2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Light Years', 'AllowPrefix' => false],
'ly^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Light Years', 'AllowPrefix' => false],
'm2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Meters', 'AllowPrefix' => true],
'm^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Meters', 'AllowPrefix' => true],
'Morgen' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Morgen', 'AllowPrefix' => false],
'mi2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Miles', 'AllowPrefix' => false],
'mi^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Miles', 'AllowPrefix' => false],
'Nmi2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Nautical Miles', 'AllowPrefix' => false],
'Nmi^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Nautical Miles', 'AllowPrefix' => false],
'Pica2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false],
'Pica^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false],
'Picapt2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false],
'Picapt^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Pica', 'AllowPrefix' => false],
'yd2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Yards', 'AllowPrefix' => false],
'yd^2' => ['Group' => self::CATEGORY_AREA, 'UnitName' => 'Square Yards', 'AllowPrefix' => false],
// Information
'byte' => ['Group' => self::CATEGORY_INFORMATION, 'UnitName' => 'Byte', 'AllowPrefix' => true],
'bit' => ['Group' => self::CATEGORY_INFORMATION, 'UnitName' => 'Bit', 'AllowPrefix' => true],
// Speed
'm/s' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per second', 'AllowPrefix' => true],
'm/sec' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per second', 'AllowPrefix' => true],
'm/h' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per hour', 'AllowPrefix' => true],
'm/hr' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Meters per hour', 'AllowPrefix' => true],
'mph' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Miles per hour', 'AllowPrefix' => false],
'admkn' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Admiralty Knot', 'AllowPrefix' => false],
'kn' => ['Group' => self::CATEGORY_SPEED, 'UnitName' => 'Knot', 'AllowPrefix' => false],
];
/**
* Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM().
*
* @var array<string, array{multiplier: float, name: string}>
*/
private static array $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 array<string, array{multiplier: float|int, name: string}>
*/
private static array $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 array<string, array<string, float>>
*/
private static array $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.
*/
public static function getConversionCategories(): array
{
$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
*/
public static function getConversionCategoryUnits(?string $category = null): array
{
$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
*/
public static function getConversionCategoryUnitDetails(?string $category = null): array
{
$conversionGroups = [];
foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) {
if (($category === null) || ($conversionGroup['Group'] == $category)) {
$conversionGroups[$conversionGroup['Group']][] = [
'unit' => $conversionUnit,
'description' => $conversionGroup['UnitName'],
];
}
}
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(): array
{
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(): array
{
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) {
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, $value);
}
$baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]);
return ($baseValue * self::$unitConversions[$fromCategory][$toUOM]) / $toMultiplier;
}
private static function getUOMDetails(string $uom): array
{
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');
}
protected static function convertTemperature(string $fromUOM, string $toUOM, float|int $value): float|int
{
$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): string
{
return match ($uom) {
'fah' => 'F',
'cel' => 'C',
'kel' => 'K',
default => $uom,
};
}
}

View File

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

View File

@@ -0,0 +1,109 @@
<?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 const TWO_SQRT_PI = 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(mixed $lower, mixed $upper = null): array|float|string
{
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(mixed $limit)
{
if (is_array($limit)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $limit);
}
return self::ERF($limit);
}
private static function makeFloat(mixed $value): float
{
return is_numeric($value) ? ((float) $value) : 0.0;
}
/**
* Method to calculate the erf value.
*/
public static function erfValue(float|int|string $value): float
{
$value = (float) $value;
if (abs($value) > 2.2) {
return 1 - self::makeFloat(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::TWO_SQRT_PI * $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(mixed $value)
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (is_numeric($value)) {
return self::erfcValue($value);
}
return ExcelError::VALUE();
}
private const ONE_SQRT_PI = 0.564189583547756287;
/**
* Method to calculate the erfc value.
*/
private static function erfcValue(float|int|string $value): float|int
{
$value = (float) $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::ONE_SQRT_PI * exp(-$value * $value) * $q2;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
class Exception extends PhpSpreadsheetException
{
public const CALCULATION_ENGINE_PUSH_TO_STACK = 1;
/**
* Error handler callback.
*/
public static function errorHandlerCallback(int $code, string $string, string $file, int $line): 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 */
$callable = [Exception::class, 'errorHandlerCallback'];
set_error_handler($callable, E_ALL);
}
/**
* Unregister errorhandler.
*/
public function __destruct()
{
restore_error_handler();
}
}

View File

@@ -0,0 +1,213 @@
<?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(
mixed $cost,
mixed $purchased,
mixed $firstPeriod,
mixed $salvage,
mixed $period,
mixed $rate,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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 */
$yearFrac = $yearFracx;
$amortiseCoeff = self::getAmortizationCoefficient($rate);
$rate *= $amortiseCoeff;
$rate = (float) (string) $rate; // ugly way to avoid rounding problem
$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) {
return match ($period - $n) {
1 => round($cost * 0.5, 0),
default => 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(
mixed $cost,
mixed $purchased,
mixed $firstPeriod,
mixed $salvage,
mixed $period,
mixed $rate,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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 */
$yearFrac = $yearFracx;
if (
$basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL
&& $yearFrac < 1
&& DateTimeExcel\Helpers::isLeapYear(Functions::scalar($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,41 @@
<?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
{
public static function validateRate(mixed $rate): float
{
$rate = self::validateFloat($rate);
return $rate;
}
public static function validatePeriodType(mixed $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;
}
public static function validatePresentValue(mixed $presentValue): float
{
return self::validateFloat($presentValue);
}
public static function validateFutureValue(mixed $futureValue): float
{
return self::validateFloat($futureValue);
}
}

View File

@@ -0,0 +1,195 @@
<?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.
*/
public static function futureValue(
mixed $rate,
mixed $numberOfPeriods,
mixed $payment = 0.0,
mixed $presentValue = 0.0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float {
$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(
mixed $rate,
mixed $numberOfPeriods,
mixed $payment = 0.0,
mixed $futureValue = 0.0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float {
$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(
mixed $rate,
mixed $payment,
mixed $presentValue,
mixed $futureValue = 0.0,
mixed $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;
}
private static function calculatePeriods(
float $rate,
float $payment,
float $presentValue,
float $futureValue,
int $type
): string|float {
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,138 @@
<?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.
*/
public static function interest(
mixed $rate,
mixed $periods,
mixed $presentValue,
mixed $start,
mixed $end,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float|int {
$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.
*/
public static function principal(
mixed $rate,
mixed $periods,
mixed $presentValue,
mixed $start,
mixed $end,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float|int {
$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,213 @@
<?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
*/
public static function payment(
mixed $interestRate,
mixed $period,
mixed $numberOfPeriods,
mixed $presentValue,
mixed $futureValue = 0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float {
$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
*/
public static function schedulePayment(mixed $interestRate, mixed $period, mixed $numberOfPeriods, mixed $principleRemaining): string|float
{
$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.
*/
public static function rate(
mixed $numberOfPeriods,
mixed $payment,
mixed $presentValue,
mixed $futureValue = 0.0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD,
mixed $guess = 0.1
): string|float {
$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::validateFloat($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(float $rate, float $numberOfPeriods, float $payment, float $presentValue, float $futureValue, int $type): string|float
{
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 float $interest;
protected float $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 = (float) $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(
mixed $interestRate,
mixed $numberOfPeriods,
mixed $presentValue,
mixed $futureValue = 0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float {
$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(
mixed $interestRate,
mixed $period,
mixed $numberOfPeriods,
mixed $presentValue,
mixed $futureValue = 0,
mixed $type = FinancialConstants::PAYMENT_END_OF_PERIOD
): string|float {
$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,107 @@
<?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
*/
public static function futureValue(mixed $principal, array $schedule): string|float
{
$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(mixed $rate, mixed $presentValue, mixed $futureValue): string|float
{
$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 array|float $periods The number of periods over which the investment is made
* @param array|float $presentValue Present Value
* @param array|float $futureValue Future Value
*
* @return float|string Result, or a string containing an error
*/
public static function interestRate(array|float $periods = 0.0, array|float $presentValue = 0.0, array|float $futureValue = 0.0): string|float
{
$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,301 @@
<?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
*/
public static function rate(array $values, array $dates, mixed $guess = self::DEFAULT_GUESS): float|string
{
$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)) {
return $f1;
}
if (!is_numeric($f2)) {
return $f2;
}
$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 self::xirrPart3($values, $dates, $x1, $x2);
}
// Newton-Raphson didn't work - try bisection
$x1 = $guess - 0.5;
$x2 = $guess + 0.5;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
if ($f1 * $f2 <= 0) {
$found = true;
break;
}
$x1 -= 0.5;
$x2 += 0.5;
}
if ($found) {
return self::xirrBisection($values, $dates, $x1, $x2);
}
return ExcelError::NAN();
}
/**
* 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 array|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.
*/
public static function presentValue(array|float $rate, array $values, array $dates): float|string
{
return self::xnpvOrdered($rate, $values, $dates, true);
}
private static function bothNegAndPos(bool $neg, bool $pos): bool
{
return $neg && $pos;
}
private static function xirrPart1(mixed &$values, mixed &$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 '';
}
private static function xirrPart3(array $values, array $dates, float $x1, float $x2): float|string
{
$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;
}
private static function xirrBisection(array $values, array $dates, float $x1, float $x2): string|float
{
$rslt = ExcelError::NAN();
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$rslt = ExcelError::NAN();
$f1 = self::xnpvOrdered($x1, $values, $dates, false, true);
$f2 = self::xnpvOrdered($x2, $values, $dates, false, true);
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
$f1 = (float) $f1;
$f2 = (float) $f2;
if (abs($f1) < self::FINANCIAL_PRECISION && abs($f2) < self::FINANCIAL_PRECISION) {
break;
}
if ($f1 * $f2 > 0) {
break;
}
$rslt = ($x1 + $x2) / 2;
$f3 = self::xnpvOrdered($rslt, $values, $dates, false, true);
if (!is_float($f3)) {
break;
}
if ($f3 * $f1 < 0) {
$x2 = $rslt;
} else {
$x1 = $rslt;
}
if (abs($f3) < self::FINANCIAL_PRECISION) {
break;
}
}
return $rslt;
}
private static function xnpvOrdered(mixed $rate, mixed $values, mixed $dates, bool $ordered = true, bool $capAtNegative1 = false): float|string
{
$rate = Functions::flattenSingleValue($rate);
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
$valCount = count($values);
try {
self::validateXnpv($rate, $values, $dates);
if ($capAtNegative1 && $rate <= -1) {
$rate = -1.0 + 1.0E-10;
}
$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 = Functions::scalar(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();
}
private static function validateXnpv(mixed $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,157 @@
<?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
*/
public static function rate(mixed $values, mixed $guess = 0.1): string|float
{
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(mixed $values, mixed $financeRate, mixed $reinvestmentRate): string|float
{
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 = 0.0;
foreach ($values as $i => $v) {
if ($v >= 0) {
$npvPos += $v / $rr ** $i;
} else {
$npvNeg += $v / $fr ** $i;
}
}
if ($npvNeg === 0.0 || $npvPos === 0.0) {
return ExcelError::DIV0();
}
$mirr = ((-$npvPos * $rr ** $n)
/ ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0;
return is_finite($mirr) ? $mirr : ExcelError::NAN();
}
/**
* NPV.
*
* Returns the Net Present Value of a cash flow series given a discount rate.
*
* @param array $args
*/
public static function presentValue(mixed $rate, ...$args): int|float
{
$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,407 @@
<?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
*/
public static function COUPDAYBS(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|int|float {
$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
*/
public static function COUPDAYS(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|int|float {
$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
*/
public static function COUPDAYSNC(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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 */
$daysPerYear = Helpers::daysPerYear(Functions::Scalar(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 float|string Excel date/time serial value or error message
*/
public static function COUPNCD(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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);
FinancialValidations::validateBasis($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
*/
public static function COUPNUM(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|int {
$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);
FinancialValidations::validateBasis($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 float|string Excel date/time serial value or error message
*/
public static function COUPPCD(
mixed $settlement,
mixed $maturity,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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);
FinancialValidations::validateBasis($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());
}
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Depreciation
{
private static float $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.
*/
public static function DB(mixed $cost, mixed $salvage, mixed $life, mixed $period, mixed $month = 12): string|float|int
{
$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).
*/
public static function DDB(mixed $cost, mixed $salvage, mixed $life, mixed $period, mixed $factor = 2.0): float|string
{
$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(mixed $cost, mixed $salvage, mixed $life): string|float
{
$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(mixed $cost, mixed $salvage, mixed $life, mixed $period): string|float
{
$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(mixed $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(mixed $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(mixed $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(mixed $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(mixed $month): int
{
$month = FinancialValidations::validateInt($month);
if ($month < 1) {
throw new Exception(ExcelError::NAN());
}
return $month;
}
private static function validateFactor(mixed $factor): float
{
$factor = FinancialValidations::validateFloat($factor);
if ($factor <= 0.0) {
throw new Exception(ExcelError::NAN());
}
return $factor;
}
}

View File

@@ -0,0 +1,127 @@
<?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(mixed $number, mixed $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
*/
public static function decimal(mixed $fractionalDollar = null, mixed $fraction = 0): array|string|float
{
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
*/
public static function fractional(mixed $decimalDollar = null, mixed $fraction = 0): array|string|float
{
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,122 @@
<?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
{
public static function validateDate(mixed $date): float
{
return DateTimeExcel\Helpers::getDateValue($date);
}
public static function validateSettlementDate(mixed $settlement): float
{
return self::validateDate($settlement);
}
public static function validateMaturityDate(mixed $maturity): float
{
return self::validateDate($maturity);
}
public static function validateFloat(mixed $value): float
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (float) $value;
}
public static function validateInt(mixed $value): int
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (int) floor((float) $value);
}
public static function validateRate(mixed $rate): float
{
$rate = self::validateFloat($rate);
if ($rate < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $rate;
}
public static function validateFrequency(mixed $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;
}
public static function validateBasis(mixed $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;
}
public static function validatePrice(mixed $price): float
{
$price = self::validateFloat($price);
if ($price < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $price;
}
public static function validateParValue(mixed $parValue): float
{
$parValue = self::validateFloat($parValue);
if ($parValue < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $parValue;
}
public static function validateYield(mixed $yield): float
{
$yield = self::validateFloat($yield);
if ($yield < 0.0) {
throw new Exception(ExcelError::NAN());
}
return $yield;
}
public static function validateDiscount(mixed $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): string|int
{
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,71 @@
<?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
*/
public static function effective(mixed $nominalRate = 0, mixed $periodsPerYear = 0): string|float
{
$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(mixed $effectiveRate = 0, mixed $periodsPerYear = 0): string|float
{
$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,151 @@
<?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 Unused by PhpSpreadsheet, and apparently by Excel (https://exceljet.net/functions/accrint-function)
*
* @return float|string Result, or a string containing an error
*/
public static function periodic(
mixed $issue,
mixed $firstInterest,
mixed $settlement,
mixed $rate,
mixed $parValue = 1000,
mixed $frequency = FinancialConstants::FREQUENCY_ANNUAL,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD,
mixed $calcMethod = self::ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT
) {
$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);
SecurityValidations::validateFrequency($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(
mixed $issue,
mixed $settlement,
mixed $rate,
mixed $parValue = 1000,
mixed $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;
}
}

View File

@@ -0,0 +1,283 @@
<?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 $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(
mixed $settlement,
mixed $maturity,
mixed $rate,
mixed $yield,
mixed $redemption,
mixed $frequency,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): string|float {
$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 = (float) Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis);
$e = (float) Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis);
$n = (int) Coupons::COUPNUM($settlement, $maturity, $frequency, $basis);
$a = (float) 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(
mixed $settlement,
mixed $maturity,
mixed $discount,
mixed $redemption,
mixed $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(
mixed $settlement,
mixed $maturity,
mixed $issue,
mixed $rate,
mixed $yield,
mixed $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 = Helpers::daysPerYear(Functions::scalar(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(
mixed $settlement,
mixed $maturity,
mixed $investment,
mixed $discount,
mixed $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 Functions::scalar($daysBetweenSettlementAndMaturity);
}
return $investment / (1 - ($discount * $daysBetweenSettlementAndMaturity));
}
}

View File

@@ -0,0 +1,134 @@
<?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
*/
public static function discount(
mixed $settlement,
mixed $maturity,
mixed $price,
mixed $redemption,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): float|string {
$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
*/
public static function interest(
mixed $settlement,
mixed $maturity,
mixed $investment,
mixed $redemption,
mixed $basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
): float|string {
$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,32 @@
<?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
{
public static function validateIssueDate(mixed $issue): float
{
return self::validateDate($issue);
}
public static function validateSecurityPeriod(mixed $settlement, mixed $maturity): void
{
if ($settlement >= $maturity) {
throw new Exception(ExcelError::NAN());
}
}
public static function validateRedemption(mixed $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(
mixed $settlement,
mixed $maturity,
mixed $price,
mixed $redemption,
mixed $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(Functions::scalar(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(
mixed $settlement,
mixed $maturity,
mixed $issue,
mixed $rate,
mixed $price,
mixed $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(Functions::scalar(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,146 @@
<?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(mixed $settlement, mixed $maturity, mixed $discount): string|float
{
$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(mixed $settlement, mixed $maturity, mixed $discount): string|float
{
$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 float|string $price The Treasury bill's price per $100 face value
*/
public static function yield(mixed $settlement, mixed $maturity, $price): string|float
{
$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,616 @@
<?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.
*/
private string $formula;
/**
* Tokens.
*
* @var FormulaToken[]
*/
private array $tokens = [];
/**
* Create a new FormulaParser.
*
* @param ?string $formula Formula to parse
*/
public function __construct(?string $formula = '')
{
// Check parameters
if ($formula === null) {
throw new Exception('Invalid parameter passed: formula');
}
// Initialise values
$this->formula = trim($formula);
// Parse!
$this->parseToTokens();
}
/**
* Get Formula.
*/
public function getFormula(): string
{
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.
*/
public function getTokenCount(): int
{
return count($this->tokens);
}
/**
* Get Tokens.
*
* @return FormulaToken[]
*/
public function getTokens(): array
{
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 (str_contains(self::OPERATORS_SN, $this->formula[$index])) {
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 ($value !== '') {
// unexpected
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
$value = '';
}
$inString = true;
++$index;
continue;
}
if ($this->formula[$index] == self::QUOTE_SINGLE) {
if ($value !== '') {
// 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 ($value !== '') {
// 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 ($value !== '') {
// 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 ($value !== '') {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
/** @var FormulaToken $tmp */
$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 ($value !== '') {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
/** @var FormulaToken $tmp */
$tmp = array_pop($stack);
$tmp->setValue('');
$tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
$tokens1[] = $tmp;
/** @var FormulaToken $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 ($value !== '') {
$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 ($value !== '') {
$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 (str_contains(self::OPERATORS_INFIX, $this->formula[$index])) {
if ($value !== '') {
$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 (str_contains(self::OPERATORS_POSTFIX, $this->formula[$index])) {
if ($value !== '') {
$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 ($value !== '') {
$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 ($value !== '') {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
/** @var FormulaToken $tmp */
$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 ($value !== '') {
$tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
$value = '';
}
/** @var FormulaToken $tmp */
$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 ($value !== '') {
$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->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 ($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 (str_contains('<>=', substr($token->getValue(), 0, 1))) {
$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 ($token->getValue() !== '') {
if (str_starts_with($token->getValue(), '@')) {
$token->setValue(substr($token->getValue(), 1));
}
}
}
$this->tokens[] = $token;
}
}
}

View File

@@ -0,0 +1,131 @@
<?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.
*/
private string $value;
/**
* Token Type (represented by TOKEN_TYPE_*).
*/
private string $tokenType;
/**
* Token SubType (represented by TOKEN_SUBTYPE_*).
*/
private string $tokenSubType;
/**
* Create a new FormulaToken.
*
* @param string $tokenType Token type (represented by TOKEN_TYPE_*)
* @param string $tokenSubType Token Subtype (represented by TOKEN_SUBTYPE_*)
*/
public function __construct(string $value, string $tokenType = self::TOKEN_TYPE_UNKNOWN, string $tokenSubType = self::TOKEN_SUBTYPE_NOTHING)
{
// Initialise values
$this->value = $value;
$this->tokenType = $tokenType;
$this->tokenSubType = $tokenSubType;
}
/**
* Get Value.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Set Value.
*/
public function setValue(string $value): void
{
$this->value = $value;
}
/**
* Get Token Type (represented by TOKEN_TYPE_*).
*/
public function getTokenType(): string
{
return $this->tokenType;
}
/**
* Set Token Type (represented by TOKEN_TYPE_*).
*/
public function setTokenType(string $value): void
{
$this->tokenType = $value;
}
/**
* Get Token SubType (represented by TOKEN_SUBTYPE_*).
*/
public function getTokenSubType(): string
{
return $this->tokenSubType;
}
/**
* Set Token SubType (represented by TOKEN_SUBTYPE_*).
*/
public function setTokenSubType(string $value): void
{
$this->tokenSubType = $value;
}
}

View File

@@ -0,0 +1,324 @@
<?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';
public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented';
/**
* Compatibility mode to use for error checking and responses.
*/
protected static string $compatibilityMode = self::COMPATIBILITY_EXCEL;
/**
* Data Type to use when returning date values.
*/
protected static string $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(string $compatibilityMode): bool
{
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(): string
{
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(string $returnDateType): bool
{
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(): string
{
return self::$returnDateType;
}
/**
* DUMMY.
*
* @return string #Not Yet Implemented
*/
public static function DUMMY(): string
{
return self::NOT_YET_IMPLEMENTED;
}
public static function isMatrixValue(mixed $idx): bool
{
return (substr_count($idx, '.') <= 1) || (preg_match('/\.[A-Z]/', $idx) > 0);
}
public static function isValue(mixed $idx): bool
{
return substr_count($idx, '.') === 0;
}
public static function isCellValue(mixed $idx): bool
{
return substr_count($idx, '.') > 1;
}
public static function ifCondition(mixed $condition): string
{
$condition = self::flattenSingleValue($condition);
if ($condition === '' || $condition === null) {
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(mixed $operand): mixed
{
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;
}
/**
* Convert a multi-dimensional array to a simple 1-dimensional array.
*
* @param mixed $array Array to be flattened
*
* @return array Flattened array
*/
public static function flattenArray(mixed $array): 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;
}
public static function scalar(mixed $value): mixed
{
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): 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
*/
public static function flattenSingleValue(mixed $value): mixed
{
while (is_array($value)) {
$value = array_shift($value);
}
return $value;
}
public static function expandDefinedName(string $coordinate, Cell $cell): string
{
$worksheet = $cell->getWorksheet();
$spreadsheet = $worksheet->getParentOrThrow();
// 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 (str_contains($coordinate, '!')) {
$coordinate = substr($coordinate, strrpos($coordinate, '!') + 1);
}
return $coordinate;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
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(mixed $value = ''): array|bool
{
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(mixed $value = '', bool $tryNotImplemented = false): array|bool
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (!is_string($value)) {
return false;
}
if ($tryNotImplemented && $value === Functions::NOT_YET_IMPLEMENTED) {
return true;
}
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(mixed $value = ''): array|bool
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return $value === ExcelError::NA();
}
}

View File

@@ -0,0 +1,155 @@
<?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
];
public static function throwError(mixed $value): string
{
return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value'];
}
/**
* ERROR_TYPE.
*
* @param mixed $value Value to check
*/
public static function type(mixed $value = ''): array|int|string
{
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,317 @@
<?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(mixed $value = null): array|bool
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
return $value === null;
}
/**
* IS_REF.
*
* @param mixed $value Value to check
*/
public static function isRef(mixed $value, ?Cell $cell = null): bool
{
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()->getParentOrThrow()->getSheetByName($worksheet) === null) {
return false;
}
[$column, $row] = Coordinate::indexesFromString($cellValue ?? '');
if ($column > 16384 || $row > 1048576) {
return false;
}
return true;
}
$namedRange = $cell->getWorksheet()->getParentOrThrow()->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(mixed $value = null): array|string|bool
{
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(mixed $value = null): array|string|bool
{
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(mixed $value = null): array|bool
{
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(mixed $value = null): array|bool
{
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(mixed $value = null): array|bool
{
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(mixed $value = null): array|bool
{
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)
*/
public static function isFormula(mixed $cellReference = '', ?Cell $cell = null): array|bool|string
{
if ($cell === null) {
return ExcelError::REF();
}
$fullCellReference = Functions::expandDefinedName((string) $cellReference, $cell);
if (str_contains($cellReference, '!')) {
$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()->getParentOrThrow()->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 (($value !== '') && ($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 int 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): int
{
$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 (($value !== '') && ($value[0] == '#')) {
return 16;
}
return 2;
}
return 0;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class MakeMatrix
{
/** @param array $args */
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,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