forked from SimplesIP/pabx-app
bruno
1 year ago
607 changed files with 146548 additions and 1 deletions
@ -0,0 +1,115 @@
|
||||
<?php |
||||
|
||||
/* Autoload através do autoload.json |
||||
* |
||||
* O método get_NamespaceToPathname fará essa conversão |
||||
* depois que o json estiver carregado. |
||||
* |
||||
* A json precisa manter o padrão |
||||
* "before_id": "<Namespace+File>", |
||||
* "after_id": "<pathname>" |
||||
* |
||||
*/ |
||||
|
||||
|
||||
class JsonAutoload { |
||||
|
||||
private $json_file; |
||||
private $json_data_dec; |
||||
private $replace_namespace; |
||||
private $namespace_to_change; |
||||
private $amount_replace; |
||||
|
||||
|
||||
function __construct( ) { |
||||
|
||||
$this->amount_replace = 0; |
||||
$this->replace_namespace = array( ); |
||||
$this->namespace_to_change = array( ); |
||||
$this->ExistFileJSON( ); |
||||
$this->ReadJson( ); |
||||
$this->get_JsonLibrary(); |
||||
|
||||
} |
||||
|
||||
|
||||
public function get_NamespaceToPathname( $classname ){ |
||||
|
||||
$result = str_replace( $this->namespace_to_change, $this->replace_namespace, $classname ); |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
|
||||
private function obtain_replace_namespace( $lib, $json_replace ){ |
||||
|
||||
$amount_replace = count( $json_replace["replace"] ); |
||||
|
||||
$odd = ($amount_replace % 2); |
||||
if( $odd == 1 ){ |
||||
$this->ERROR_OBJ("Campo Replace em autoload.json tem quantidade ímpar. Deve ser par em $lib"); |
||||
} |
||||
|
||||
foreach ($json_replace["replace"] as $key => $value) { |
||||
|
||||
if( str_contains( $key, "before") ){ |
||||
$this->namespace_to_change[$this->amount_replace] = $value; |
||||
} |
||||
|
||||
if( str_contains( $key, "after") ){ |
||||
$this->replace_namespace[$this->amount_replace] = $value; |
||||
} |
||||
|
||||
|
||||
if(count($this->replace_namespace) == count($this->namespace_to_change)){ |
||||
++$this->amount_replace; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private function get_JsonLibrary( ){ |
||||
|
||||
foreach ( $this->json_data_dec as $lib => $value ) { |
||||
|
||||
if(!is_array($value["replace"])){ |
||||
$this->ERROR_OBJ("Está faltando campo replace em $lib no arquivo {$this->json_file}"); |
||||
} |
||||
|
||||
$this->obtain_replace_namespace( $lib, $value ); |
||||
} |
||||
} |
||||
|
||||
private function ExistFileJSON( ){ |
||||
|
||||
if( !file_exists( $this->FileName() )){ |
||||
$this->ERROR_OBJ("Não foi possível encontrar o json para autoload"); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private function ReadJson( ){ |
||||
|
||||
$json_file = file_get_contents( $this->FileName() ); |
||||
|
||||
$this->json_data_dec = json_decode( $json_file, true ); |
||||
|
||||
if(!$this->json_data_dec){ |
||||
$this->ERROR_OBJ("não foi possivel carregar {$this->json_file}"); |
||||
} |
||||
} |
||||
|
||||
private function FileName( ){ |
||||
|
||||
$this->json_file = dirname(__FILE__)."/autoload.json"; |
||||
|
||||
return $this->json_file; |
||||
} |
||||
|
||||
private function ERROR_OBJ( $str ){ |
||||
echo $str; |
||||
die; |
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,35 @@
|
||||
{ |
||||
|
||||
"phpoffice":{ |
||||
|
||||
"global_namespace": "PhpOffice", |
||||
"pathname": "include/phpoffice", |
||||
|
||||
"replace":{ |
||||
"before_1": "PhpOffice\\PhpSpreadsheet\\", |
||||
"after_1": "phpoffice/phpspreadsheet/src/PhpSpreadsheet/" |
||||
} |
||||
}, |
||||
|
||||
|
||||
|
||||
"psr": { |
||||
|
||||
"global_namespace": "Psr", |
||||
"pathname": "include/psr", |
||||
|
||||
"replace":{ |
||||
"before_1": "Psr\\Http\\Client\\", |
||||
"after_1" : "psr/http-client/src/", |
||||
|
||||
"before_2": "Psr\\Http\\Message\\", |
||||
"after_2" : "psr/http-message/src/", |
||||
|
||||
"before_3": "Psr\\SimpleCache\\", |
||||
"after_3" : "psr/simple-cache/src/" |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,48 @@
|
||||
<?php |
||||
|
||||
require_once("JsonAutoload.php"); |
||||
|
||||
class Autoload { |
||||
|
||||
|
||||
public function __construct(){ |
||||
spl_autoload_extensions(".php"); |
||||
spl_autoload_register(array($this, 'SIP_autoload')); |
||||
|
||||
} |
||||
|
||||
/* selecionar qual biblioteca chamar */ |
||||
private function SIP_Autoload( $classname ){ |
||||
$classname = $this->LibAutoload( $classname, new JsonAutoload() ); |
||||
|
||||
$path_filename = preg_replace( "/\\\\/", "/", "$classname"); |
||||
$file_to_load = "$path_library$path_filename.php"; |
||||
|
||||
$this->include_file( $file_to_load ); |
||||
} |
||||
|
||||
|
||||
private function LibAutoload($classname, $autoload_json){ |
||||
$classname = $autoload_json->get_NamespaceToPathname( $classname ); |
||||
|
||||
return $classname; |
||||
} |
||||
|
||||
private function include_file( $file ){ |
||||
try { |
||||
require_once( $file ); |
||||
} |
||||
catch(Exception $e){ |
||||
echo "$file\n", $e->getMessage(), "\n"; |
||||
die(); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
new Autoload(); |
||||
|
||||
|
@ -0,0 +1,227 @@
|
||||
<?php |
||||
|
||||
$finder = PhpCsFixer\Finder::create() |
||||
->exclude('vendor') |
||||
->notPath('src/PhpSpreadsheet/Writer/ZipStream3.php') |
||||
->in(__DIR__); |
||||
|
||||
$config = new PhpCsFixer\Config(); |
||||
$config |
||||
->setRiskyAllowed(true) |
||||
->setFinder($finder) |
||||
->setCacheFile(sys_get_temp_dir() . '/php-cs-fixer' . preg_replace('~\W~', '-', __DIR__)) |
||||
->setRules([ |
||||
'align_multiline_comment' => true, |
||||
'array_indentation' => true, |
||||
'array_syntax' => ['syntax' => 'short'], |
||||
'backtick_to_shell_exec' => true, |
||||
'binary_operator_spaces' => true, |
||||
'blank_line_after_namespace' => true, |
||||
'blank_line_after_opening_tag' => true, |
||||
'blank_line_before_statement' => true, |
||||
'braces' => true, |
||||
'cast_spaces' => true, |
||||
'class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'one']], // const are often grouped with other related const |
||||
'class_definition' => false, |
||||
'class_keyword_remove' => false, // ::class keyword gives us better support in IDE |
||||
'combine_consecutive_issets' => true, |
||||
'combine_consecutive_unsets' => true, |
||||
'combine_nested_dirname' => true, |
||||
'comment_to_phpdoc' => false, // interferes with annotations |
||||
'compact_nullable_typehint' => true, |
||||
'concat_space' => ['spacing' => 'one'], |
||||
'constant_case' => true, |
||||
'date_time_immutable' => false, // Break our unit tests |
||||
'declare_equal_normalize' => true, |
||||
'declare_strict_types' => false, // Too early to adopt strict types |
||||
'dir_constant' => true, |
||||
'doctrine_annotation_array_assignment' => true, |
||||
'doctrine_annotation_braces' => true, |
||||
'doctrine_annotation_indentation' => true, |
||||
'doctrine_annotation_spaces' => true, |
||||
'elseif' => true, |
||||
'encoding' => true, |
||||
'ereg_to_preg' => true, |
||||
'escape_implicit_backslashes' => true, |
||||
'explicit_indirect_variable' => false, // I feel it makes the code actually harder to read |
||||
'explicit_string_variable' => false, // I feel it makes the code actually harder to read |
||||
'final_class' => false, // We need non-final classes |
||||
'final_internal_class' => true, |
||||
'final_public_method_for_abstract_class' => false, // We need non-final methods |
||||
'self_static_accessor' => true, |
||||
'fopen_flag_order' => true, |
||||
'fopen_flags' => true, |
||||
'full_opening_tag' => true, |
||||
'fully_qualified_strict_types' => true, |
||||
'function_declaration' => true, |
||||
'function_to_constant' => true, |
||||
'function_typehint_space' => true, |
||||
'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright']], |
||||
'global_namespace_import' => true, |
||||
'header_comment' => false, // We don't use common header in all our files |
||||
'heredoc_indentation' => true, |
||||
'heredoc_to_nowdoc' => false, // Not sure about this one |
||||
'implode_call' => true, |
||||
'include' => true, |
||||
'increment_style' => true, |
||||
'indentation_type' => true, |
||||
'is_null' => true, |
||||
'line_ending' => true, |
||||
'linebreak_after_opening_tag' => true, |
||||
'list_syntax' => ['syntax' => 'short'], |
||||
'logical_operators' => true, |
||||
'lowercase_cast' => true, |
||||
'lowercase_keywords' => true, |
||||
'lowercase_static_reference' => true, |
||||
'magic_constant_casing' => true, |
||||
'magic_method_casing' => true, |
||||
'mb_str_functions' => false, // No, too dangerous to change that |
||||
'method_argument_space' => true, |
||||
'method_chaining_indentation' => true, |
||||
'modernize_types_casting' => true, |
||||
'multiline_comment_opening_closing' => true, |
||||
'multiline_whitespace_before_semicolons' => true, |
||||
'native_constant_invocation' => false, // Micro optimization that look messy |
||||
'native_function_casing' => true, |
||||
'native_function_invocation' => false, // I suppose this would be best, but I am still unconvinced about the visual aspect of it |
||||
'native_function_type_declaration_casing' => true, |
||||
'new_with_braces' => true, |
||||
'no_alias_functions' => true, |
||||
'no_alternative_syntax' => true, |
||||
'no_binary_string' => true, |
||||
'no_blank_lines_after_class_opening' => true, |
||||
'no_blank_lines_after_phpdoc' => true, |
||||
'no_blank_lines_before_namespace' => false, // we want 1 blank line before namespace |
||||
'no_break_comment' => true, |
||||
'no_closing_tag' => true, |
||||
'no_empty_comment' => true, |
||||
'no_empty_phpdoc' => true, |
||||
'no_empty_statement' => true, |
||||
'no_extra_blank_lines' => true, |
||||
'no_homoglyph_names' => true, |
||||
'no_leading_import_slash' => true, |
||||
'no_leading_namespace_whitespace' => true, |
||||
'no_mixed_echo_print' => true, |
||||
'no_multiline_whitespace_around_double_arrow' => true, |
||||
'no_null_property_initialization' => true, |
||||
'no_php4_constructor' => true, |
||||
'no_short_bool_cast' => true, |
||||
'echo_tag_syntax' => ['format' => 'long'], |
||||
'no_singleline_whitespace_before_semicolons' => true, |
||||
'no_spaces_after_function_name' => true, |
||||
'no_spaces_around_offset' => true, |
||||
'no_spaces_inside_parenthesis' => true, |
||||
'no_superfluous_elseif' => false, // Might be risky on a huge code base |
||||
'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], |
||||
'no_trailing_comma_in_list_call' => true, |
||||
'no_trailing_comma_in_singleline_array' => true, |
||||
'no_trailing_whitespace' => true, |
||||
'no_trailing_whitespace_in_comment' => true, |
||||
'no_unneeded_control_parentheses' => true, |
||||
'no_unneeded_curly_braces' => true, |
||||
'no_unneeded_final_method' => true, |
||||
'no_unreachable_default_argument_value' => true, |
||||
'no_unset_cast' => true, |
||||
'no_unset_on_property' => true, |
||||
'no_unused_imports' => true, |
||||
'no_useless_else' => true, |
||||
'no_useless_return' => true, |
||||
'no_whitespace_before_comma_in_array' => true, |
||||
'no_whitespace_in_blank_line' => true, |
||||
'non_printable_character' => true, |
||||
'normalize_index_brace' => true, |
||||
'not_operator_with_space' => false, // No we prefer to keep '!' without spaces |
||||
'not_operator_with_successor_space' => false, // idem |
||||
'nullable_type_declaration_for_default_null_value' => true, |
||||
'object_operator_without_whitespace' => true, |
||||
'ordered_class_elements' => false, // We prefer to keep some freedom |
||||
'ordered_imports' => true, |
||||
'ordered_interfaces' => true, |
||||
'php_unit_construct' => true, |
||||
'php_unit_dedicate_assert' => true, |
||||
'php_unit_dedicate_assert_internal_type' => true, |
||||
'php_unit_expectation' => true, |
||||
'php_unit_fqcn_annotation' => true, |
||||
'php_unit_internal_class' => false, // Because tests are excluded from package |
||||
'php_unit_method_casing' => true, |
||||
'php_unit_mock' => true, |
||||
'php_unit_mock_short_will_return' => true, |
||||
'php_unit_namespaced' => true, |
||||
'php_unit_no_expectation_annotation' => true, |
||||
'phpdoc_order_by_value' => ['annotations' => ['covers']], |
||||
'php_unit_set_up_tear_down_visibility' => true, |
||||
'php_unit_size_class' => false, // That seems extra work to maintain for little benefits |
||||
'php_unit_strict' => false, // We sometime actually need assertEquals |
||||
'php_unit_test_annotation' => true, |
||||
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], |
||||
'php_unit_test_class_requires_covers' => false, // We don't care as much as we should about coverage |
||||
'phpdoc_add_missing_param_annotation' => false, // Don't add things that bring no value |
||||
'phpdoc_align' => false, // Waste of time |
||||
'phpdoc_annotation_without_dot' => true, |
||||
'phpdoc_indent' => true, |
||||
//'phpdoc_inline_tag' => true, |
||||
'phpdoc_line_span' => false, // Unfortunately our old comments turn even uglier with this |
||||
'phpdoc_no_access' => true, |
||||
'phpdoc_no_alias_tag' => true, |
||||
'phpdoc_no_empty_return' => true, |
||||
'phpdoc_no_package' => true, |
||||
'phpdoc_no_useless_inheritdoc' => true, |
||||
'phpdoc_order' => true, |
||||
'phpdoc_return_self_reference' => true, |
||||
'phpdoc_scalar' => true, |
||||
'phpdoc_separation' => true, |
||||
'phpdoc_single_line_var_spacing' => true, |
||||
'phpdoc_summary' => true, |
||||
'phpdoc_to_comment' => false, // interferes with annotations |
||||
'phpdoc_to_param_type' => false, // Because experimental, but interesting for one shot use |
||||
'phpdoc_to_return_type' => false, // idem |
||||
'phpdoc_trim' => true, |
||||
'phpdoc_trim_consecutive_blank_line_separation' => true, |
||||
'phpdoc_types' => true, |
||||
'phpdoc_types_order' => true, |
||||
'phpdoc_var_annotation_correct_order' => true, |
||||
'phpdoc_var_without_name' => true, |
||||
'pow_to_exponentiation' => true, |
||||
'protected_to_private' => true, |
||||
//'psr0' => true, |
||||
//'psr4' => true, |
||||
'random_api_migration' => true, |
||||
'return_assignment' => false, // Sometimes useful for clarity or debug |
||||
'return_type_declaration' => true, |
||||
'self_accessor' => true, |
||||
'self_static_accessor' => true, |
||||
'semicolon_after_instruction' => false, // Buggy in `samples/index.php` |
||||
'set_type_to_cast' => true, |
||||
'short_scalar_cast' => true, |
||||
'simple_to_complex_string_variable' => false, // Would differ from TypeScript without obvious advantages |
||||
'simplified_null_return' => false, // Even if technically correct we prefer to be explicit |
||||
'single_blank_line_at_eof' => true, |
||||
'single_blank_line_before_namespace' => true, |
||||
'single_class_element_per_statement' => true, |
||||
'single_import_per_statement' => true, |
||||
'single_line_after_imports' => true, |
||||
'single_line_comment_style' => true, |
||||
'single_line_throw' => false, // I don't see any reason for having a special case for Exception |
||||
'single_quote' => true, |
||||
'single_trait_insert_per_statement' => true, |
||||
'space_after_semicolon' => true, |
||||
'standardize_increment' => true, |
||||
'standardize_not_equals' => true, |
||||
'static_lambda' => false, // Risky if we can't guarantee nobody use `bindTo()` |
||||
'strict_comparison' => false, // No, too dangerous to change that |
||||
'strict_param' => false, // No, too dangerous to change that |
||||
'string_line_ending' => true, |
||||
'switch_case_semicolon_to_colon' => true, |
||||
'switch_case_space' => true, |
||||
'ternary_operator_spaces' => true, |
||||
'ternary_to_null_coalescing' => true, |
||||
'trailing_comma_in_multiline' => true, |
||||
'trim_array_spaces' => true, |
||||
'unary_operator_spaces' => true, |
||||
'visibility_required' => ['elements' => ['property', 'method']], // not const |
||||
'void_return' => true, |
||||
'whitespace_after_comma_in_array' => true, |
||||
'yoda_style' => false, |
||||
]); |
||||
|
||||
return $config; |
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0"?> |
||||
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PHP_CodeSniffer" |
||||
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> |
||||
|
||||
<file>samples</file> |
||||
<file>src</file> |
||||
<file>tests</file> |
||||
|
||||
<exclude-pattern>samples/Header.php</exclude-pattern> |
||||
<exclude-pattern>*/tests/Core/*/*Test\.(inc|css|js)$</exclude-pattern> |
||||
|
||||
<arg name="report-width" value="200"/> |
||||
<arg name="parallel" value="80"/> |
||||
<arg name="cache" value="/tmp/.phpspreadsheet.phpcs-cache"/> |
||||
<arg name="colors"/> |
||||
<arg value="np"/> |
||||
|
||||
<!-- Include the whole PSR12 standard --> |
||||
<rule ref="PSR12"> |
||||
<exclude name="PSR2.Methods.MethodDeclaration.Underscore"/> |
||||
</rule> |
||||
</ruleset> |
File diff suppressed because it is too large
Load Diff
@ -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 (currently PHP 7.4 to PHP 8.2). |
||||
- 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.) |
||||
|
@ -0,0 +1,21 @@
|
||||
MIT License |
||||
|
||||
Copyright (c) 2019 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. |
@ -0,0 +1,144 @@
|
||||
# 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. |
||||
|
||||
## PHP Version Support |
||||
|
||||
LTS: Support for PHP versions will only be maintained for a period of six months beyond the |
||||
[end of life](https://www.php.net/supported-versions) of that PHP version. |
||||
|
||||
Currently the required PHP minimum version is PHP __7.4__, and we [will support that version](https://www.php.net/eol.php) until 28th June 2023. |
||||
|
||||
See the `composer.json` for other requirements. |
||||
|
||||
## Installation |
||||
|
||||
Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: |
||||
|
||||
```sh |
||||
composer require phpoffice/phpspreadsheet |
||||
``` |
||||
|
||||
If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: |
||||
```json |
||||
{ |
||||
"require": { |
||||
"phpoffice/phpspreadsheet": "^1.28" |
||||
}, |
||||
"config": { |
||||
"platform": { |
||||
"php": "7.4" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
and then run |
||||
```sh |
||||
composer install |
||||
``` |
||||
to ensure that the correct dependencies are retrieved to match your deployment environment. |
||||
|
||||
See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. |
||||
|
||||
### Additional Installation Options |
||||
|
||||
If you want to write to PDF, or to include Charts when you write to HTML or PDF, then you will need to install additional libraries: |
||||
|
||||
#### PDF |
||||
|
||||
For PDF Generation, you can install any of the following, and then configure PhpSpreadsheet to indicate which library you are going to use: |
||||
- mpdf/mpdf |
||||
- dompdf/dompdf |
||||
- tecnickcom/tcpdf |
||||
|
||||
and configure PhpSpreadsheet using: |
||||
|
||||
```php |
||||
// Dompdf, Mpdf or Tcpdf (as appropriate) |
||||
$className = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class; |
||||
IOFactory::registerWriter('Pdf', $className); |
||||
``` |
||||
or the appropriate PDF Writer wrapper for the library that you have chosen to install. |
||||
|
||||
#### Chart Export |
||||
|
||||
For Chart export, we support following packages, which you will also need to install yourself using `composer require` |
||||
- [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0. |
||||
You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) |
||||
- [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) - up to date fork with modern PHP versions support and some bugs fixed. |
||||
|
||||
and then configure PhpSpreadsheet using: |
||||
```php |
||||
// to use jpgraph/jpgraph |
||||
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); |
||||
//or |
||||
// to use mitoteam/jpgraph |
||||
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); |
||||
``` |
||||
|
||||
One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts; or to render a Chart to an Image format from within your code. |
||||
They are not necessary to define charts for writing to `Xlsx` files. |
||||
Other file formats don't support writing Charts. |
||||
|
||||
## 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). |
@ -0,0 +1,117 @@
|
||||
{ |
||||
"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": { |
||||
"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": [ |
||||
"phpcs src/ tests/ --report=checkstyle", |
||||
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.4- -n", |
||||
"php-cs-fixer fix --ansi --dry-run --diff", |
||||
"phpunit --color=always", |
||||
"phpstan analyse --ansi --memory-limit=2048M" |
||||
], |
||||
"style": [ |
||||
"phpcs src/ tests/ --report=checkstyle", |
||||
"php-cs-fixer fix --ansi --dry-run --diff" |
||||
], |
||||
"fix": [ |
||||
"phpcbf src/ tests/ --report=checkstyle", |
||||
"php-cs-fixer fix" |
||||
], |
||||
"versions": [ |
||||
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.4- -n" |
||||
] |
||||
}, |
||||
"require": { |
||||
"php": "^7.4 || ^8.0", |
||||
"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": "*", |
||||
"ezyang/htmlpurifier": "^4.15", |
||||
"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": "^1.0 || ^2.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": "^8.5 || ^9.0 || ^10.0", |
||||
"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" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
parameters: |
||||
ignoreErrors: |
||||
- |
||||
message: "#^Cannot call method getTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" |
||||
count: 4 |
||||
path: src/PhpSpreadsheet/Calculation/FormulaParser.php |
||||
|
||||
- |
||||
message: "#^Cannot call method getTokenType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" |
||||
count: 9 |
||||
path: src/PhpSpreadsheet/Calculation/FormulaParser.php |
||||
|
||||
- |
||||
message: "#^Cannot call method setTokenSubType\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" |
||||
count: 5 |
||||
path: src/PhpSpreadsheet/Calculation/FormulaParser.php |
||||
|
||||
- |
||||
message: "#^Cannot call method setValue\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken\\|null\\.$#" |
||||
count: 5 |
||||
path: src/PhpSpreadsheet/Calculation/FormulaParser.php |
||||
|
||||
- |
||||
message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\FormulaToken and null will always evaluate to false\\.$#" |
||||
count: 1 |
||||
path: src/PhpSpreadsheet/Calculation/FormulaParser.php |
@ -0,0 +1,86 @@
|
||||
<?php |
||||
|
||||
$config = []; |
||||
|
||||
if (PHP_VERSION_ID < 80000) { |
||||
// GdImage not available before PHP8 |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Method .* has invalid return type GdImage\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/Drawing.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Property .* has unknown class GdImage as its type\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Method .* has invalid return type GdImage\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Class GdImage not found\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', |
||||
'count' => 1, |
||||
]; |
||||
// GdImage with Phpstan 1.9.2 |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '~Class GdImage not found.*$~', |
||||
'path' => __DIR__ . '/tests/PhpSpreadsheetTests/Worksheet/MemoryDrawingTest.php', |
||||
'count' => 3, |
||||
]; |
||||
// Erroneous analysis by Phpstan before PHP8 - 3rd parameter is nullable |
||||
// Fixed for Php7 with Phpstan 1.9. |
||||
//$config['parameters']['ignoreErrors'][] = [ |
||||
// 'message' => '#^Parameter \\#3 \\$namespace of method XMLWriter\\:\\:startElementNs\\(\\) expects string, null given\\.$#', |
||||
// 'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php', |
||||
// 'count' => 8, |
||||
//]; |
||||
// Erroneous analysis by Phpstan before PHP8 - mb_strlen does not return false |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:countCharacters\\(\\) should return int but returns int(<0, max>)?\\|false\\.$#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/StringHelper.php', |
||||
'count' => 1, |
||||
]; |
||||
// New with Phpstan 1.9.2 for Php7 only |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, array<int, mixed>\\|false given.$#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Parameter \\#1 \\$input of function array_chunk expects array, array<int, float\\|int>\\|false given.$#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Parameter \\#2 \\$array of function array_map expects array, array<int, float|int>\\|false given.$#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/Random.php', |
||||
'count' => 1, |
||||
]; |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, array<int, mixed>\\|false given.$#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/TextData/Text.php', |
||||
'count' => 1, |
||||
]; |
||||
} else { |
||||
// Flagged in Php8+ - unsure how to correct code |
||||
$config['parameters']['ignoreErrors'][] = [ |
||||
'message' => '#^Binary operation "/" between float and array[|]float[|]int[|]string results in an error.#', |
||||
'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php', |
||||
'count' => 1, |
||||
]; |
||||
} |
||||
|
||||
return $config; |
@ -0,0 +1,27 @@
|
||||
includes: |
||||
- phpstan-baseline.neon |
||||
- phpstan-conditional.php |
||||
- vendor/phpstan/phpstan-phpunit/extension.neon |
||||
- vendor/phpstan/phpstan-phpunit/rules.neon |
||||
|
||||
parameters: |
||||
level: 8 |
||||
paths: |
||||
- src/ |
||||
- tests/ |
||||
excludePaths: |
||||
- src/PhpSpreadsheet/Chart/Renderer/JpGraph.php |
||||
- src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php |
||||
- src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php |
||||
- src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php |
||||
- src/PhpSpreadsheet/Writer/ZipStream2.php |
||||
- src/PhpSpreadsheet/Writer/ZipStream3.php |
||||
parallel: |
||||
processTimeout: 300.0 |
||||
checkMissingIterableValueType: false |
||||
ignoreErrors: |
||||
- '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy|imagepng) expects (GdImage|resource), GdImage\|resource given\.$~' |
||||
- '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' |
||||
# Accept a bit anything for assert methods |
||||
- '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' |
||||
- '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' |
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="./tests/bootstrap.php" backupGlobals="true" colors="true" cacheResultFile="/tmp/.phpspreadsheet.phpunit.result.cache"> |
||||
<coverage/> |
||||
<php> |
||||
<ini name="memory_limit" value="2048M"/> |
||||
</php> |
||||
<testsuite name="PhpSpreadsheet Unit Test Suite"> |
||||
<directory>./tests/PhpSpreadsheetTests</directory> |
||||
</testsuite> |
||||
<source> |
||||
<include> |
||||
<directory suffix=".php">./src</directory> |
||||
</include> |
||||
</source> |
||||
</phpunit> |
@ -0,0 +1,133 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentHelper; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentProcessor; |
||||
|
||||
trait ArrayEnabled |
||||
{ |
||||
/** |
||||
* @var ArrayArgumentHelper |
||||
*/ |
||||
private static $arrayArgumentHelper; |
||||
|
||||
/** |
||||
* @param array|false $arguments Can be changed to array for Php8.1+ |
||||
*/ |
||||
private static function initialiseHelper($arguments): void |
||||
{ |
||||
if (self::$arrayArgumentHelper === null) { |
||||
self::$arrayArgumentHelper = new ArrayArgumentHelper(); |
||||
} |
||||
self::$arrayArgumentHelper->initialise(($arguments === false) ? [] : $arguments); |
||||
} |
||||
|
||||
/** |
||||
* Handles array argument processing when the function accepts a single argument that can be an array argument. |
||||
* Example use for: |
||||
* DAYOFMONTH() or FACT(). |
||||
*/ |
||||
protected static function evaluateSingleArgumentArray(callable $method, array $values): array |
||||
{ |
||||
$result = []; |
||||
foreach ($values as $value) { |
||||
$result[] = $method($value); |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* Handles array argument processing when the function accepts multiple arguments, |
||||
* and any of them can be an array argument. |
||||
* Example use for: |
||||
* ROUND() or DATE(). |
||||
* |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
protected static function evaluateArrayArguments(callable $method, ...$arguments): array |
||||
{ |
||||
self::initialiseHelper($arguments); |
||||
$arguments = self::$arrayArgumentHelper->arguments(); |
||||
|
||||
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); |
||||
} |
||||
|
||||
/** |
||||
* Handles array argument processing when the function accepts multiple arguments, |
||||
* but only the first few (up to limit) can be an array arguments. |
||||
* Example use for: |
||||
* NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need |
||||
* to be treated as a such rather than as an array arguments. |
||||
* |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array |
||||
{ |
||||
self::initialiseHelper(array_slice($arguments, 0, $limit)); |
||||
$trailingArguments = array_slice($arguments, $limit); |
||||
$arguments = self::$arrayArgumentHelper->arguments(); |
||||
$arguments = array_merge($arguments, $trailingArguments); |
||||
|
||||
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
private static function testFalse($value): bool |
||||
{ |
||||
return $value === false; |
||||
} |
||||
|
||||
/** |
||||
* Handles array argument processing when the function accepts multiple arguments, |
||||
* but only the last few (from start) can be an array arguments. |
||||
* Example use for: |
||||
* Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset |
||||
* rather than as an array argument. |
||||
* |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array |
||||
{ |
||||
$arrayArgumentsSubset = array_combine( |
||||
range($start, count($arguments) - $start), |
||||
array_slice($arguments, $start) |
||||
); |
||||
if (self::testFalse($arrayArgumentsSubset)) { |
||||
return ['#VALUE!']; |
||||
} |
||||
|
||||
self::initialiseHelper($arrayArgumentsSubset); |
||||
$leadingArguments = array_slice($arguments, 0, $start); |
||||
$arguments = self::$arrayArgumentHelper->arguments(); |
||||
$arguments = array_merge($leadingArguments, $arguments); |
||||
|
||||
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); |
||||
} |
||||
|
||||
/** |
||||
* Handles array argument processing when the function accepts multiple arguments, |
||||
* and any of them can be an array argument except for the one specified by ignore. |
||||
* Example use for: |
||||
* HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database |
||||
* rather than as an array argument. |
||||
* |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, ...$arguments): array |
||||
{ |
||||
$leadingArguments = array_slice($arguments, 0, $ignore); |
||||
$ignoreArgument = array_slice($arguments, $ignore, 1); |
||||
$trailingArguments = array_slice($arguments, $ignore + 1); |
||||
|
||||
self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments)); |
||||
$arguments = self::$arrayArgumentHelper->arguments(); |
||||
|
||||
array_splice($arguments, $ignore, 1, $ignoreArgument); |
||||
|
||||
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); |
||||
} |
||||
} |
@ -0,0 +1,181 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
||||
class BinaryComparison |
||||
{ |
||||
/** |
||||
* Epsilon Precision used for comparisons in calculations. |
||||
*/ |
||||
private const DELTA = 0.1e-12; |
||||
|
||||
/** |
||||
* Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters. |
||||
* |
||||
* @param null|string $str1 First string value for the comparison |
||||
* @param null|string $str2 Second string value for the comparison |
||||
*/ |
||||
private static function strcmpLowercaseFirst($str1, $str2): int |
||||
{ |
||||
$inversedStr1 = StringHelper::strCaseReverse($str1 ?? ''); |
||||
$inversedStr2 = StringHelper::strCaseReverse($str2 ?? ''); |
||||
|
||||
return strcmp($inversedStr1, $inversedStr2); |
||||
} |
||||
|
||||
/** |
||||
* PHP8.1 deprecates passing null to strcmp. |
||||
* |
||||
* @param null|string $str1 First string value for the comparison |
||||
* @param null|string $str2 Second string value for the comparison |
||||
*/ |
||||
private static function strcmpAllowNull($str1, $str2): int |
||||
{ |
||||
return strcmp($str1 ?? '', $str2 ?? ''); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
public static function compare($operand1, $operand2, string $operator): bool |
||||
{ |
||||
// Simple validate the two operands if they are string values |
||||
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) { |
||||
$operand1 = Calculation::unwrapResult($operand1); |
||||
} |
||||
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) { |
||||
$operand2 = Calculation::unwrapResult($operand2); |
||||
} |
||||
|
||||
// Use case insensitive comparaison if not OpenOffice mode |
||||
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) { |
||||
if (is_string($operand1)) { |
||||
$operand1 = StringHelper::strToUpper($operand1); |
||||
} |
||||
if (is_string($operand2)) { |
||||
$operand2 = StringHelper::strToUpper($operand2); |
||||
} |
||||
} |
||||
|
||||
$useLowercaseFirstComparison = is_string($operand1) && |
||||
is_string($operand2) && |
||||
Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE; |
||||
|
||||
return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function evaluateComparison($operand1, $operand2, string $operator, bool $useLowercaseFirstComparison): bool |
||||
{ |
||||
switch ($operator) { |
||||
// Equality |
||||
case '=': |
||||
return self::equal($operand1, $operand2); |
||||
// Greater than |
||||
case '>': |
||||
return self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison); |
||||
// Less than |
||||
case '<': |
||||
return self::lessThan($operand1, $operand2, $useLowercaseFirstComparison); |
||||
// Greater than or equal |
||||
case '>=': |
||||
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison); |
||||
// Less than or equal |
||||
case '<=': |
||||
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison); |
||||
// Inequality |
||||
case '<>': |
||||
return self::notEqual($operand1, $operand2); |
||||
default: |
||||
throw new Exception('Unsupported binary comparison operator'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function equal($operand1, $operand2): bool |
||||
{ |
||||
if (is_numeric($operand1) && is_numeric($operand2)) { |
||||
$result = (abs($operand1 - $operand2) < self::DELTA); |
||||
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { |
||||
$result = $operand1 == $operand2; |
||||
} else { |
||||
$result = self::strcmpAllowNull($operand1, $operand2) == 0; |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function greaterThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool |
||||
{ |
||||
if (is_numeric($operand1) && is_numeric($operand2)) { |
||||
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 > $operand2)); |
||||
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { |
||||
$result = $operand1 >= $operand2; |
||||
} elseif ($useLowercaseFirstComparison) { |
||||
$result = self::strcmpLowercaseFirst($operand1, $operand2) >= 0; |
||||
} else { |
||||
$result = self::strcmpAllowNull($operand1, $operand2) >= 0; |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function lessThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool |
||||
{ |
||||
if (is_numeric($operand1) && is_numeric($operand2)) { |
||||
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 < $operand2)); |
||||
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { |
||||
$result = $operand1 <= $operand2; |
||||
} elseif ($useLowercaseFirstComparison) { |
||||
$result = self::strcmpLowercaseFirst($operand1, $operand2) <= 0; |
||||
} else { |
||||
$result = self::strcmpAllowNull($operand1, $operand2) <= 0; |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function greaterThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool |
||||
{ |
||||
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function lessThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool |
||||
{ |
||||
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $operand1 |
||||
* @param mixed $operand2 |
||||
*/ |
||||
private static function notEqual($operand1, $operand2): bool |
||||
{ |
||||
return self::equal($operand1, $operand2) !== true; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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'; |
||||
} |
@ -0,0 +1,430 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
||||
/** |
||||
* @deprecated 1.17.0 |
||||
* |
||||
* @codeCoverageIgnore |
||||
*/ |
||||
class Database |
||||
{ |
||||
/** |
||||
* DAVERAGE. |
||||
* |
||||
* Averages the values in a column of a list or database that match conditions you specify. |
||||
* |
||||
* Excel Function: |
||||
* DAVERAGE(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DAverage class instead |
||||
* @see Database\DAverage::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DAVERAGE($database, $field, $criteria) |
||||
{ |
||||
return Database\DAverage::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DCOUNT. |
||||
* |
||||
* Counts the cells that contain numbers in a column of a list or database that match conditions |
||||
* that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DCOUNT(database,[field],criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DCount class instead |
||||
* @see Database\DCount::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param null|int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return int|string |
||||
* |
||||
* @TODO The field argument is optional. If field is omitted, DCOUNT counts all records in the |
||||
* database that match the criteria. |
||||
*/ |
||||
public static function DCOUNT($database, $field, $criteria) |
||||
{ |
||||
return Database\DCount::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DCOUNTA. |
||||
* |
||||
* Counts the nonblank cells in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DCOUNTA(database,[field],criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DCountA class instead |
||||
* @see Database\DCountA::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return int|string |
||||
*/ |
||||
public static function DCOUNTA($database, $field, $criteria) |
||||
{ |
||||
return Database\DCountA::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DGET. |
||||
* |
||||
* Extracts a single value from a column of a list or database that matches conditions that you |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DGET(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DGet class instead |
||||
* @see Database\DGet::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public static function DGET($database, $field, $criteria) |
||||
{ |
||||
return Database\DGet::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DMAX. |
||||
* |
||||
* Returns the largest number in a column of a list or database that matches conditions you that |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DMAX(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DMax class instead |
||||
* @see Database\DMax::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function DMAX($database, $field, $criteria) |
||||
{ |
||||
return Database\DMax::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DMIN. |
||||
* |
||||
* Returns the smallest number in a column of a list or database that matches conditions you that |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DMIN(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DMin class instead |
||||
* @see Database\DMin::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function DMIN($database, $field, $criteria) |
||||
{ |
||||
return Database\DMin::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DPRODUCT. |
||||
* |
||||
* Multiplies the values in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DPRODUCT(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DProduct class instead |
||||
* @see Database\DProduct::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DPRODUCT($database, $field, $criteria) |
||||
{ |
||||
return Database\DProduct::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DSTDEV. |
||||
* |
||||
* Estimates the standard deviation of a population based on a sample by using the numbers in a |
||||
* column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSTDEV(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DStDev class instead |
||||
* @see Database\DStDev::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DSTDEV($database, $field, $criteria) |
||||
{ |
||||
return Database\DStDev::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DSTDEVP. |
||||
* |
||||
* Calculates the standard deviation of a population based on the entire population by using the |
||||
* numbers in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSTDEVP(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DStDevP class instead |
||||
* @see Database\DStDevP::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DSTDEVP($database, $field, $criteria) |
||||
{ |
||||
return Database\DStDevP::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DSUM. |
||||
* |
||||
* Adds the numbers in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSUM(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DSum class instead |
||||
* @see Database\DSum::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function DSUM($database, $field, $criteria) |
||||
{ |
||||
return Database\DSum::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DVAR. |
||||
* |
||||
* Estimates the variance of a population based on a sample by using the numbers in a column |
||||
* of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DVAR(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DVar class instead |
||||
* @see Database\DVar::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string (string if result is an error) |
||||
*/ |
||||
public static function DVAR($database, $field, $criteria) |
||||
{ |
||||
return Database\DVar::evaluate($database, $field, $criteria); |
||||
} |
||||
|
||||
/** |
||||
* DVARP. |
||||
* |
||||
* Calculates the variance of a population based on the entire population by using the numbers |
||||
* in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DVARP(database,field,criteria) |
||||
* |
||||
* @deprecated 1.17.0 |
||||
* Use the evaluate() method in the Database\DVarP class instead |
||||
* @see Database\DVarP::evaluate() |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string (string if result is an error) |
||||
*/ |
||||
public static function DVARP($database, $field, $criteria) |
||||
{ |
||||
return Database\DVarP::evaluate($database, $field, $criteria); |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; |
||||
|
||||
class DAverage extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DAVERAGE. |
||||
* |
||||
* Averages the values in a column of a list or database that match conditions you specify. |
||||
* |
||||
* Excel Function: |
||||
* DAVERAGE(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return Averages::average( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; |
||||
|
||||
class DCount extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DCOUNT. |
||||
* |
||||
* Counts the cells that contain numbers in a column of a list or database that match conditions |
||||
* that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DCOUNT(database,[field],criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param null|int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return int|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria, bool $returnError = true) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($returnError && $field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return Counts::COUNT( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; |
||||
|
||||
class DCountA extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DCOUNTA. |
||||
* |
||||
* Counts the nonblank cells in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DCOUNTA(database,[field],criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return int|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return Counts::COUNTA( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class DGet extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DGET. |
||||
* |
||||
* Extracts a single value from a column of a list or database that matches conditions that you |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DGET(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
$columnData = self::getFilteredColumn($database, $field, $criteria); |
||||
if (count($columnData) > 1) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$row = array_pop($columnData); |
||||
|
||||
return array_pop($row); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum; |
||||
|
||||
class DMax extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DMAX. |
||||
* |
||||
* Returns the largest number in a column of a list or database that matches conditions you that |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DMAX(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria, bool $returnError = true) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return $returnError ? ExcelError::VALUE() : null; |
||||
} |
||||
|
||||
return Maximum::max( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum; |
||||
|
||||
class DMin extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DMIN. |
||||
* |
||||
* Returns the smallest number in a column of a list or database that matches conditions you that |
||||
* specify. |
||||
* |
||||
* Excel Function: |
||||
* DMIN(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria, bool $returnError = true) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return $returnError ? ExcelError::VALUE() : null; |
||||
} |
||||
|
||||
return Minimum::min( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
||||
class DProduct extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DPRODUCT. |
||||
* |
||||
* Multiplies the values in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DPRODUCT(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return MathTrig\Operations::product( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; |
||||
|
||||
class DStDev extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DSTDEV. |
||||
* |
||||
* Estimates the standard deviation of a population based on a sample by using the numbers in a |
||||
* column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSTDEV(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return StandardDeviations::STDEV( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; |
||||
|
||||
class DStDevP extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DSTDEVP. |
||||
* |
||||
* Calculates the standard deviation of a population based on the entire population by using the |
||||
* numbers in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSTDEVP(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return StandardDeviations::STDEVP( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
||||
class DSum extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DSUM. |
||||
* |
||||
* Adds the numbers in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DSUM(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return null|float|string |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria, bool $returnNull = false) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return $returnNull ? null : ExcelError::VALUE(); |
||||
} |
||||
|
||||
return MathTrig\Sum::sumIgnoringStrings( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; |
||||
|
||||
class DVar extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DVAR. |
||||
* |
||||
* Estimates the variance of a population based on a sample by using the numbers in a column |
||||
* of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DVAR(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string (string if result is an error) |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return Variances::VAR( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; |
||||
|
||||
class DVarP extends DatabaseAbstract |
||||
{ |
||||
/** |
||||
* DVARP. |
||||
* |
||||
* Calculates the variance of a population based on the entire population by using the numbers |
||||
* in a column of a list or database that match conditions that you specify. |
||||
* |
||||
* Excel Function: |
||||
* DVARP(database,field,criteria) |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param int|string $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return float|string (string if result is an error) |
||||
*/ |
||||
public static function evaluate($database, $field, $criteria) |
||||
{ |
||||
$field = self::fieldExtract($database, $field); |
||||
if ($field === null) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return Variances::VARP( |
||||
self::getFilteredColumn($database, $field, $criteria) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,192 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Database; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; |
||||
|
||||
abstract class DatabaseAbstract |
||||
{ |
||||
/** |
||||
* @param array $database |
||||
* @param int|string $field |
||||
* @param array $criteria |
||||
* |
||||
* @return null|float|int|string |
||||
*/ |
||||
abstract public static function evaluate($database, $field, $criteria); |
||||
|
||||
/** |
||||
* fieldExtract. |
||||
* |
||||
* Extracts the column ID to use for the data field. |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param mixed $field Indicates which column is used in the function. Enter the |
||||
* column label enclosed between double quotation marks, such as |
||||
* "Age" or "Yield," or a number (without quotation marks) that |
||||
* represents the position of the column within the list: 1 for |
||||
* the first column, 2 for the second column, and so on. |
||||
*/ |
||||
protected static function fieldExtract(array $database, $field): ?int |
||||
{ |
||||
$field = strtoupper(Functions::flattenSingleValue($field) ?? ''); |
||||
if ($field === '') { |
||||
return null; |
||||
} |
||||
|
||||
$fieldNames = array_map('strtoupper', array_shift($database)); |
||||
if (is_numeric($field)) { |
||||
$field = (int) $field - 1; |
||||
if ($field < 0 || $field >= count($fieldNames)) { |
||||
return null; |
||||
} |
||||
|
||||
return $field; |
||||
} |
||||
$key = array_search($field, array_values($fieldNames), true); |
||||
|
||||
return ($key !== false) ? (int) $key : null; |
||||
} |
||||
|
||||
/** |
||||
* filter. |
||||
* |
||||
* Parses the selection criteria, extracts the database rows that match those criteria, and |
||||
* returns that subset of rows. |
||||
* |
||||
* @param mixed[] $database The range of cells that makes up the list or database. |
||||
* A database is a list of related data in which rows of related |
||||
* information are records, and columns of data are fields. The |
||||
* first row of the list contains labels for each column. |
||||
* @param mixed[] $criteria The range of cells that contains the conditions you specify. |
||||
* You can use any range for the criteria argument, as long as it |
||||
* includes at least one column label and at least one cell below |
||||
* the column label in which you specify a condition for the |
||||
* column. |
||||
* |
||||
* @return mixed[] |
||||
*/ |
||||
protected static function filter(array $database, array $criteria): array |
||||
{ |
||||
$fieldNames = array_shift($database); |
||||
$criteriaNames = array_shift($criteria); |
||||
|
||||
// Convert the criteria into a set of AND/OR conditions with [:placeholders] |
||||
$query = self::buildQuery($criteriaNames, $criteria); |
||||
|
||||
// Loop through each row of the database |
||||
return self::executeQuery($database, $query, $criteriaNames, $fieldNames); |
||||
} |
||||
|
||||
protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array |
||||
{ |
||||
// reduce the database to a set of rows that match all the criteria |
||||
$database = self::filter($database, $criteria); |
||||
$defaultReturnColumnValue = ($field === null) ? 1 : null; |
||||
|
||||
// extract an array of values for the requested column |
||||
$columnData = []; |
||||
foreach ($database as $rowKey => $row) { |
||||
$keys = array_keys($row); |
||||
$key = $keys[$field] ?? null; |
||||
$columnKey = $key ?? 'A'; |
||||
$columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue; |
||||
} |
||||
|
||||
return $columnData; |
||||
} |
||||
|
||||
private static function buildQuery(array $criteriaNames, array $criteria): string |
||||
{ |
||||
$baseQuery = []; |
||||
foreach ($criteria as $key => $criterion) { |
||||
foreach ($criterion as $field => $value) { |
||||
$criterionName = $criteriaNames[$field]; |
||||
if ($value !== null) { |
||||
$condition = self::buildCondition($value, $criterionName); |
||||
$baseQuery[$key][] = $condition; |
||||
} |
||||
} |
||||
} |
||||
|
||||
$rowQuery = array_map( |
||||
function ($rowValue) { |
||||
return (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''); |
||||
}, |
||||
$baseQuery |
||||
); |
||||
|
||||
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : ($rowQuery[0] ?? ''); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $criterion |
||||
*/ |
||||
private static function buildCondition($criterion, string $criterionName): string |
||||
{ |
||||
$ifCondition = Functions::ifCondition($criterion); |
||||
|
||||
// Check for wildcard characters used in the condition |
||||
$result = preg_match('/(?<operator>[^"]*)(?<operand>".*[*?].*")/ui', $ifCondition, $matches); |
||||
if ($result !== 1) { |
||||
return "[:{$criterionName}]{$ifCondition}"; |
||||
} |
||||
|
||||
$trueFalse = ($matches['operator'] !== '<>'); |
||||
$wildcard = WildcardMatch::wildcard($matches['operand']); |
||||
$condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})"; |
||||
if ($trueFalse === false) { |
||||
$condition = "NOT({$condition})"; |
||||
} |
||||
|
||||
return $condition; |
||||
} |
||||
|
||||
private static function executeQuery(array $database, string $query, array $criteria, array $fields): array |
||||
{ |
||||
foreach ($database as $dataRow => $dataValues) { |
||||
// Substitute actual values from the database row for our [:placeholders] |
||||
$conditions = $query; |
||||
foreach ($criteria as $criterion) { |
||||
$conditions = self::processCondition($criterion, $fields, $dataValues, $conditions); |
||||
} |
||||
|
||||
// evaluate the criteria against the row data |
||||
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions); |
||||
|
||||
// If the row failed to meet the criteria, remove it from the database |
||||
if ($result !== true) { |
||||
unset($database[$dataRow]); |
||||
} |
||||
} |
||||
|
||||
return $database; |
||||
} |
||||
|
||||
/** |
||||
* @return mixed |
||||
*/ |
||||
private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions) |
||||
{ |
||||
$key = array_search($criterion, $fields, true); |
||||
|
||||
$dataValue = 'NULL'; |
||||
if (is_bool($dataValues[$key])) { |
||||
$dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; |
||||
} elseif ($dataValues[$key] !== null) { |
||||
$dataValue = $dataValues[$key]; |
||||
// escape quotes if we have a string containing quotes |
||||
if (is_string($dataValue) && strpos($dataValue, '"') !== false) { |
||||
$dataValue = str_replace('"', '""', $dataValue); |
||||
} |
||||
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; |
||||
} |
||||
|
||||
return str_replace('[:' . $criterion . ']', $dataValue, $conditions); |
||||
} |
||||
} |
@ -0,0 +1,890 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
||||
use DateTimeInterface; |
||||
|
||||
/** |
||||
* @deprecated 1.18.0 |
||||
*/ |
||||
class DateTime |
||||
{ |
||||
/** |
||||
* Identify if a year is a leap year or not. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the isLeapYear method in the DateTimeExcel\Helpers class instead |
||||
* @see DateTimeExcel\Helpers::isLeapYear() |
||||
* |
||||
* @param int|string $year The year to test |
||||
* |
||||
* @return bool TRUE if the year is a leap year, otherwise FALSE |
||||
*/ |
||||
public static function isLeapYear($year) |
||||
{ |
||||
return DateTimeExcel\Helpers::isLeapYear($year); |
||||
} |
||||
|
||||
/** |
||||
* getDateValue. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the getDateValue method in the DateTimeExcel\Helpers class instead |
||||
* @see DateTimeExcel\Helpers::getDateValue() |
||||
* |
||||
* @param mixed $dateValue |
||||
* |
||||
* @return mixed Excel date/time serial value, or string if error |
||||
*/ |
||||
public static function getDateValue($dateValue) |
||||
{ |
||||
try { |
||||
return DateTimeExcel\Helpers::getDateValue($dateValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* DATETIMENOW. |
||||
* |
||||
* Returns the current date and time. |
||||
* The NOW function is useful when you need to display the current date and time on a worksheet or |
||||
* calculate a value based on the current date and time, and have that value updated each time you |
||||
* open the worksheet. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* NOW() |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the now method in the DateTimeExcel\Current class instead |
||||
* @see DateTimeExcel\Current::now() |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function DATETIMENOW() |
||||
{ |
||||
return DateTimeExcel\Current::now(); |
||||
} |
||||
|
||||
/** |
||||
* DATENOW. |
||||
* |
||||
* Returns the current date. |
||||
* The NOW function is useful when you need to display the current date and time on a worksheet or |
||||
* calculate a value based on the current date and time, and have that value updated each time you |
||||
* open the worksheet. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* TODAY() |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the today method in the DateTimeExcel\Current class instead |
||||
* @see DateTimeExcel\Current::today() |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function DATENOW() |
||||
{ |
||||
return DateTimeExcel\Current::today(); |
||||
} |
||||
|
||||
/** |
||||
* DATE. |
||||
* |
||||
* The DATE function returns a value that represents a particular date. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* |
||||
* Excel Function: |
||||
* DATE(year,month,day) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the fromYMD method in the DateTimeExcel\Date class instead |
||||
* @see DateTimeExcel\Date::fromYMD() |
||||
* |
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function. |
||||
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted, |
||||
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language. |
||||
* |
||||
* @param int $year The value of the year argument can include one to four digits. |
||||
* Excel interprets the year argument according to the configured |
||||
* date system: 1900 or 1904. |
||||
* If year is between 0 (zero) and 1899 (inclusive), Excel adds that |
||||
* value to 1900 to calculate the year. For example, DATE(108,1,2) |
||||
* returns January 2, 2008 (1900+108). |
||||
* If year is between 1900 and 9999 (inclusive), Excel uses that |
||||
* value as the year. For example, DATE(2008,1,2) returns January 2, |
||||
* 2008. |
||||
* If year is less than 0 or is 10000 or greater, Excel returns the |
||||
* #NUM! error value. |
||||
* @param int $month A positive or negative integer representing the month of the year |
||||
* from 1 to 12 (January to December). |
||||
* If month is greater than 12, month adds that number of months to |
||||
* the first month in the year specified. For example, DATE(2008,14,2) |
||||
* returns the serial number representing February 2, 2009. |
||||
* If month is less than 1, month subtracts the magnitude of that |
||||
* number of months, plus 1, from the first month in the year |
||||
* specified. For example, DATE(2008,-3,2) returns the serial number |
||||
* representing September 2, 2007. |
||||
* @param int $day A positive or negative integer representing the day of the month |
||||
* from 1 to 31. |
||||
* If day is greater than the number of days in the month specified, |
||||
* day adds that number of days to the first day in the month. For |
||||
* example, DATE(2008,1,35) returns the serial number representing |
||||
* February 4, 2008. |
||||
* If day is less than 1, day subtracts the magnitude that number of |
||||
* days, plus one, from the first day of the month specified. For |
||||
* example, DATE(2008,1,-15) returns the serial number representing |
||||
* December 16, 2007. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function DATE($year = 0, $month = 1, $day = 1) |
||||
{ |
||||
return DateTimeExcel\Date::fromYMD($year, $month, $day); |
||||
} |
||||
|
||||
/** |
||||
* TIME. |
||||
* |
||||
* The TIME function returns a value that represents a particular time. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* TIME(hour,minute,second) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the fromHMS method in the DateTimeExcel\Time class instead |
||||
* @see DateTimeExcel\Time::fromHMS() |
||||
* |
||||
* @param int $hour A number from 0 (zero) to 32767 representing the hour. |
||||
* Any value greater than 23 will be divided by 24 and the remainder |
||||
* will be treated as the hour value. For example, TIME(27,0,0) = |
||||
* TIME(3,0,0) = .125 or 3:00 AM. |
||||
* @param int $minute A number from 0 to 32767 representing the minute. |
||||
* Any value greater than 59 will be converted to hours and minutes. |
||||
* For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM. |
||||
* @param int $second A number from 0 to 32767 representing the second. |
||||
* Any value greater than 59 will be converted to hours, minutes, |
||||
* and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148 |
||||
* or 12:33:20 AM |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function TIME($hour = 0, $minute = 0, $second = 0) |
||||
{ |
||||
return DateTimeExcel\Time::fromHMS($hour, $minute, $second); |
||||
} |
||||
|
||||
/** |
||||
* DATEVALUE. |
||||
* |
||||
* Returns a value that represents a particular date. |
||||
* Use DATEVALUE to convert a date represented by a text string to an Excel or PHP date/time stamp |
||||
* value. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* DATEVALUE(dateValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the fromString method in the DateTimeExcel\DateValue class instead |
||||
* @see DateTimeExcel\DateValue::fromString() |
||||
* |
||||
* @param string $dateValue Text that represents a date in a Microsoft Excel date format. |
||||
* For example, "1/30/2008" or "30-Jan-2008" are text strings within |
||||
* quotation marks that represent dates. Using the default date |
||||
* system in Excel for Windows, date_text must represent a date from |
||||
* January 1, 1900, to December 31, 9999. Using the default date |
||||
* system in Excel for the Macintosh, date_text must represent a date |
||||
* from January 1, 1904, to December 31, 9999. DATEVALUE returns the |
||||
* #VALUE! error value if date_text is out of this range. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function DATEVALUE($dateValue) |
||||
{ |
||||
return DateTimeExcel\DateValue::fromString($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* TIMEVALUE. |
||||
* |
||||
* Returns a value that represents a particular time. |
||||
* Use TIMEVALUE to convert a time represented by a text string to an Excel or PHP date/time stamp |
||||
* value. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* TIMEVALUE(timeValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the fromString method in the DateTimeExcel\TimeValue class instead |
||||
* @see DateTimeExcel\TimeValue::fromString() |
||||
* |
||||
* @param string $timeValue A text string that represents a time in any one of the Microsoft |
||||
* Excel time formats; for example, "6:45 PM" and "18:45" text strings |
||||
* within quotation marks that represent time. |
||||
* Date information in time_text is ignored. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function TIMEVALUE($timeValue) |
||||
{ |
||||
return DateTimeExcel\TimeValue::fromString($timeValue); |
||||
} |
||||
|
||||
/** |
||||
* DATEDIF. |
||||
* |
||||
* Excel Function: |
||||
* DATEDIF(startdate, enddate, unit) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the interval method in the DateTimeExcel\Difference class instead |
||||
* @see DateTimeExcel\Difference::interval() |
||||
* |
||||
* @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object |
||||
* or a standard date string |
||||
* @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object |
||||
* or a standard date string |
||||
* @param array|string $unit |
||||
* |
||||
* @return array|int|string Interval between the dates |
||||
*/ |
||||
public static function DATEDIF($startDate = 0, $endDate = 0, $unit = 'D') |
||||
{ |
||||
return DateTimeExcel\Difference::interval($startDate, $endDate, $unit); |
||||
} |
||||
|
||||
/** |
||||
* DAYS. |
||||
* |
||||
* Returns the number of days between two dates |
||||
* |
||||
* Excel Function: |
||||
* DAYS(endDate, startDate) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the between method in the DateTimeExcel\Days class instead |
||||
* @see DateTimeExcel\Days::between() |
||||
* |
||||
* @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float), |
||||
* PHP date timestamp (integer), PHP DateTime object, or a standard date string |
||||
* @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float), |
||||
* PHP date timestamp (integer), PHP DateTime object, or a standard date string |
||||
* |
||||
* @return array|int|string Number of days between start date and end date or an error |
||||
*/ |
||||
public static function DAYS($endDate = 0, $startDate = 0) |
||||
{ |
||||
return DateTimeExcel\Days::between($endDate, $startDate); |
||||
} |
||||
|
||||
/** |
||||
* DAYS360. |
||||
* |
||||
* Returns the number of days between two dates based on a 360-day year (twelve 30-day months), |
||||
* which is used in some accounting calculations. Use this function to help compute payments if |
||||
* your accounting system is based on twelve 30-day months. |
||||
* |
||||
* Excel Function: |
||||
* DAYS360(startDate,endDate[,method]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the between method in the DateTimeExcel\Days360 class instead |
||||
* @see DateTimeExcel\Days360::between() |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param array|bool $method US or European Method |
||||
* FALSE or omitted: U.S. (NASD) method. If the starting date is |
||||
* the last day of a month, it becomes equal to the 30th of the |
||||
* same month. If the ending date is the last day of a month and |
||||
* the starting date is earlier than the 30th of a month, the |
||||
* ending date becomes equal to the 1st of the next month; |
||||
* otherwise the ending date becomes equal to the 30th of the |
||||
* same month. |
||||
* TRUE: European method. Starting dates and ending dates that |
||||
* occur on the 31st of a month become equal to the 30th of the |
||||
* same month. |
||||
* |
||||
* @return array|int|string Number of days between start date and end date |
||||
*/ |
||||
public static function DAYS360($startDate = 0, $endDate = 0, $method = false) |
||||
{ |
||||
return DateTimeExcel\Days360::between($startDate, $endDate, $method); |
||||
} |
||||
|
||||
/** |
||||
* YEARFRAC. |
||||
* |
||||
* Calculates the fraction of the year represented by the number of whole days between two dates |
||||
* (the start_date and the end_date). |
||||
* Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or |
||||
* obligations to assign to a specific term. |
||||
* |
||||
* Excel Function: |
||||
* YEARFRAC(startDate,endDate[,method]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the fraction method in the DateTimeExcel\YearFrac class instead |
||||
* @see DateTimeExcel\YearFrac::fraction() |
||||
* |
||||
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html |
||||
* for description of algorithm used in Excel |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param array|int $method Method used for the calculation |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return array|float|string fraction of the year, or a string containing an error |
||||
*/ |
||||
public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0) |
||||
{ |
||||
return DateTimeExcel\YearFrac::fraction($startDate, $endDate, $method); |
||||
} |
||||
|
||||
/** |
||||
* NETWORKDAYS. |
||||
* |
||||
* Returns the number of whole working days between start_date and end_date. Working days |
||||
* exclude weekends and any dates identified in holidays. |
||||
* Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days |
||||
* worked during a specific term. |
||||
* |
||||
* Excel Function: |
||||
* NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the count method in the DateTimeExcel\NetworkDays class instead |
||||
* @see DateTimeExcel\NetworkDays::count() |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param mixed $dateArgs |
||||
* |
||||
* @return array|int|string Interval between the dates |
||||
*/ |
||||
public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs) |
||||
{ |
||||
return DateTimeExcel\NetworkDays::count($startDate, $endDate, ...$dateArgs); |
||||
} |
||||
|
||||
/** |
||||
* WORKDAY. |
||||
* |
||||
* Returns the date that is the indicated number of working days before or after a date (the |
||||
* starting date). Working days exclude weekends and any dates identified as holidays. |
||||
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected |
||||
* delivery times, or the number of days of work performed. |
||||
* |
||||
* Excel Function: |
||||
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the date method in the DateTimeExcel\WorkDay class instead |
||||
* @see DateTimeExcel\WorkDay::date() |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param int $endDays The number of nonweekend and nonholiday days before or after |
||||
* startDate. A positive value for days yields a future date; a |
||||
* negative value yields a past date. |
||||
* @param mixed $dateArgs |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function WORKDAY($startDate, $endDays, ...$dateArgs) |
||||
{ |
||||
return DateTimeExcel\WorkDay::date($startDate, $endDays, ...$dateArgs); |
||||
} |
||||
|
||||
/** |
||||
* DAYOFMONTH. |
||||
* |
||||
* Returns the day of the month, for a specified date. The day is given as an integer |
||||
* ranging from 1 to 31. |
||||
* |
||||
* Excel Function: |
||||
* DAY(dateValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the day method in the DateTimeExcel\DateParts class instead |
||||
* @see DateTimeExcel\DateParts::day() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* |
||||
* @return array|int|string Day of the month |
||||
*/ |
||||
public static function DAYOFMONTH($dateValue = 1) |
||||
{ |
||||
return DateTimeExcel\DateParts::day($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* WEEKDAY. |
||||
* |
||||
* Returns the day of the week for a specified date. The day is given as an integer |
||||
* ranging from 0 to 7 (dependent on the requested style). |
||||
* |
||||
* Excel Function: |
||||
* WEEKDAY(dateValue[,style]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the day method in the DateTimeExcel\Week class instead |
||||
* @see DateTimeExcel\Week::day() |
||||
* |
||||
* @param float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param int $style A number that determines the type of return value |
||||
* 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). |
||||
* 2 Numbers 1 (Monday) through 7 (Sunday). |
||||
* 3 Numbers 0 (Monday) through 6 (Sunday). |
||||
* |
||||
* @return array|int|string Day of the week value |
||||
*/ |
||||
public static function WEEKDAY($dateValue = 1, $style = 1) |
||||
{ |
||||
return DateTimeExcel\Week::day($dateValue, $style); |
||||
} |
||||
|
||||
/** |
||||
* STARTWEEK_SUNDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_SUNDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_SUNDAY |
||||
*/ |
||||
const STARTWEEK_SUNDAY = 1; |
||||
|
||||
/** |
||||
* STARTWEEK_MONDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY |
||||
*/ |
||||
const STARTWEEK_MONDAY = 2; |
||||
|
||||
/** |
||||
* STARTWEEK_MONDAY_ALT. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY_ALT |
||||
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY_ALT |
||||
*/ |
||||
const STARTWEEK_MONDAY_ALT = 11; |
||||
|
||||
/** |
||||
* STARTWEEK_TUESDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_TUESDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_TUESDAY |
||||
*/ |
||||
const STARTWEEK_TUESDAY = 12; |
||||
|
||||
/** |
||||
* STARTWEEK_WEDNESDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_WEDNESDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_WEDNESDAY |
||||
*/ |
||||
const STARTWEEK_WEDNESDAY = 13; |
||||
|
||||
/** |
||||
* STARTWEEK_THURSDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_THURSDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_THURSDAY |
||||
*/ |
||||
const STARTWEEK_THURSDAY = 14; |
||||
|
||||
/** |
||||
* STARTWEEK_FRIDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_FRIDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_FRIDAY |
||||
*/ |
||||
const STARTWEEK_FRIDAY = 15; |
||||
|
||||
/** |
||||
* STARTWEEK_SATURDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_SATURDAY |
||||
* @see DateTimeExcel\Constants::STARTWEEK_SATURDAY |
||||
*/ |
||||
const STARTWEEK_SATURDAY = 16; |
||||
|
||||
/** |
||||
* STARTWEEK_SUNDAY_ALT. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_SUNDAY_ALT |
||||
* @see DateTimeExcel\Constants::STARTWEEK_SUNDAY_ALT |
||||
*/ |
||||
const STARTWEEK_SUNDAY_ALT = 17; |
||||
|
||||
/** |
||||
* DOW_SUNDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_SUNDAY |
||||
* @see DateTimeExcel\Constants::DOW_SUNDAY |
||||
*/ |
||||
const DOW_SUNDAY = 1; |
||||
|
||||
/** |
||||
* DOW_MONDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_MONDAY |
||||
* @see DateTimeExcel\Constants::DOW_MONDAY |
||||
*/ |
||||
const DOW_MONDAY = 2; |
||||
|
||||
/** |
||||
* DOW_TUESDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_TUESDAY |
||||
* @see DateTimeExcel\Constants::DOW_TUESDAY |
||||
*/ |
||||
const DOW_TUESDAY = 3; |
||||
|
||||
/** |
||||
* DOW_WEDNESDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_WEDNESDAY |
||||
* @see DateTimeExcel\Constants::DOW_WEDNESDAY |
||||
*/ |
||||
const DOW_WEDNESDAY = 4; |
||||
|
||||
/** |
||||
* DOW_THURSDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_THURSDAY |
||||
* @see DateTimeExcel\Constants::DOW_THURSDAY |
||||
*/ |
||||
const DOW_THURSDAY = 5; |
||||
|
||||
/** |
||||
* DOW_FRIDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_FRIDAY |
||||
* @see DateTimeExcel\Constants::DOW_FRIDAY |
||||
*/ |
||||
const DOW_FRIDAY = 6; |
||||
|
||||
/** |
||||
* DOW_SATURDAY. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::DOW_SATURDAY |
||||
* @see DateTimeExcel\Constants::DOW_SATURDAY |
||||
*/ |
||||
const DOW_SATURDAY = 7; |
||||
|
||||
/** |
||||
* STARTWEEK_MONDAY_ISO. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::STARTWEEK_MONDAY_ISO |
||||
* @see DateTimeExcel\Constants::STARTWEEK_MONDAY_ISO |
||||
*/ |
||||
const STARTWEEK_MONDAY_ISO = 21; |
||||
|
||||
/** |
||||
* METHODARR. |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use DateTimeExcel\Constants::METHODARR |
||||
* @see DateTimeExcel\Constants::METHODARR |
||||
*/ |
||||
const METHODARR = [ |
||||
self::STARTWEEK_SUNDAY => self::DOW_SUNDAY, |
||||
self::DOW_MONDAY, |
||||
self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY, |
||||
self::DOW_TUESDAY, |
||||
self::DOW_WEDNESDAY, |
||||
self::DOW_THURSDAY, |
||||
self::DOW_FRIDAY, |
||||
self::DOW_SATURDAY, |
||||
self::DOW_SUNDAY, |
||||
self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO, |
||||
]; |
||||
|
||||
/** |
||||
* WEEKNUM. |
||||
* |
||||
* Returns the week of the year for a specified date. |
||||
* The WEEKNUM function considers the week containing January 1 to be the first week of the year. |
||||
* However, there is a European standard that defines the first week as the one with the majority |
||||
* of days (four or more) falling in the new year. This means that for years in which there are |
||||
* three days or less in the first week of January, the WEEKNUM function returns week numbers |
||||
* that are incorrect according to the European standard. |
||||
* |
||||
* Excel Function: |
||||
* WEEKNUM(dateValue[,style]) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the number method in the DateTimeExcel\Week class instead |
||||
* @see DateTimeExcel\Week::number() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param int $method Week begins on Sunday or Monday |
||||
* 1 or omitted Week begins on Sunday. |
||||
* 2 Week begins on Monday. |
||||
* 11 Week begins on Monday. |
||||
* 12 Week begins on Tuesday. |
||||
* 13 Week begins on Wednesday. |
||||
* 14 Week begins on Thursday. |
||||
* 15 Week begins on Friday. |
||||
* 16 Week begins on Saturday. |
||||
* 17 Week begins on Sunday. |
||||
* 21 ISO (Jan. 4 is week 1, begins on Monday). |
||||
* |
||||
* @return array|int|string Week Number |
||||
*/ |
||||
public static function WEEKNUM($dateValue = 1, $method = /** @scrutinizer ignore-deprecated */ self::STARTWEEK_SUNDAY) |
||||
{ |
||||
return DateTimeExcel\Week::number($dateValue, $method); |
||||
} |
||||
|
||||
/** |
||||
* ISOWEEKNUM. |
||||
* |
||||
* Returns the ISO 8601 week number of the year for a specified date. |
||||
* |
||||
* Excel Function: |
||||
* ISOWEEKNUM(dateValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the isoWeekNumber method in the DateTimeExcel\Week class instead |
||||
* @see DateTimeExcel\Week::isoWeekNumber() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* |
||||
* @return array|int|string Week Number |
||||
*/ |
||||
public static function ISOWEEKNUM($dateValue = 1) |
||||
{ |
||||
return DateTimeExcel\Week::isoWeekNumber($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* MONTHOFYEAR. |
||||
* |
||||
* Returns the month of a date represented by a serial number. |
||||
* The month is given as an integer, ranging from 1 (January) to 12 (December). |
||||
* |
||||
* Excel Function: |
||||
* MONTH(dateValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the month method in the DateTimeExcel\DateParts class instead |
||||
* @see DateTimeExcel\DateParts::month() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* |
||||
* @return array|int|string Month of the year |
||||
*/ |
||||
public static function MONTHOFYEAR($dateValue = 1) |
||||
{ |
||||
return DateTimeExcel\DateParts::month($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* YEAR. |
||||
* |
||||
* Returns the year corresponding to a date. |
||||
* The year is returned as an integer in the range 1900-9999. |
||||
* |
||||
* Excel Function: |
||||
* YEAR(dateValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the ear method in the DateTimeExcel\DateParts class instead |
||||
* @see DateTimeExcel\DateParts::year() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* |
||||
* @return array|int|string Year |
||||
*/ |
||||
public static function YEAR($dateValue = 1) |
||||
{ |
||||
return DateTimeExcel\DateParts::year($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* HOUROFDAY. |
||||
* |
||||
* Returns the hour of a time value. |
||||
* The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.). |
||||
* |
||||
* Excel Function: |
||||
* HOUR(timeValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the hour method in the DateTimeExcel\TimeParts class instead |
||||
* @see DateTimeExcel\TimeParts::hour() |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* |
||||
* @return array|int|string Hour |
||||
*/ |
||||
public static function HOUROFDAY($timeValue = 0) |
||||
{ |
||||
return DateTimeExcel\TimeParts::hour($timeValue); |
||||
} |
||||
|
||||
/** |
||||
* MINUTE. |
||||
* |
||||
* Returns the minutes of a time value. |
||||
* The minute is given as an integer, ranging from 0 to 59. |
||||
* |
||||
* Excel Function: |
||||
* MINUTE(timeValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the minute method in the DateTimeExcel\TimeParts class instead |
||||
* @see DateTimeExcel\TimeParts::minute() |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* |
||||
* @return array|int|string Minute |
||||
*/ |
||||
public static function MINUTE($timeValue = 0) |
||||
{ |
||||
return DateTimeExcel\TimeParts::minute($timeValue); |
||||
} |
||||
|
||||
/** |
||||
* SECOND. |
||||
* |
||||
* Returns the seconds of a time value. |
||||
* The second is given as an integer in the range 0 (zero) to 59. |
||||
* |
||||
* Excel Function: |
||||
* SECOND(timeValue) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the second method in the DateTimeExcel\TimeParts class instead |
||||
* @see DateTimeExcel\TimeParts::second() |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* |
||||
* @return array|int|string Second |
||||
*/ |
||||
public static function SECOND($timeValue = 0) |
||||
{ |
||||
return DateTimeExcel\TimeParts::second($timeValue); |
||||
} |
||||
|
||||
/** |
||||
* EDATE. |
||||
* |
||||
* Returns the serial number that represents the date that is the indicated number of months |
||||
* before or after a specified date (the start_date). |
||||
* Use EDATE to calculate maturity dates or due dates that fall on the same day of the month |
||||
* as the date of issue. |
||||
* |
||||
* Excel Function: |
||||
* EDATE(dateValue,adjustmentMonths) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the adjust method in the DateTimeExcel\Edate class instead |
||||
* @see DateTimeExcel\Month::adjust() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param int $adjustmentMonths The number of months before or after start_date. |
||||
* A positive value for months yields a future date; |
||||
* a negative value yields a past date. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function EDATE($dateValue = 1, $adjustmentMonths = 0) |
||||
{ |
||||
return DateTimeExcel\Month::adjust($dateValue, $adjustmentMonths); |
||||
} |
||||
|
||||
/** |
||||
* EOMONTH. |
||||
* |
||||
* Returns the date value for the last day of the month that is the indicated number of months |
||||
* before or after start_date. |
||||
* Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. |
||||
* |
||||
* Excel Function: |
||||
* EOMONTH(dateValue,adjustmentMonths) |
||||
* |
||||
* @deprecated 1.18.0 |
||||
* Use the lastDay method in the DateTimeExcel\EoMonth class instead |
||||
* @see DateTimeExcel\Month::lastDay() |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* @param int $adjustmentMonths The number of months before or after start_date. |
||||
* A positive value for months yields a future date; |
||||
* a negative value yields a past date. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function EOMONTH($dateValue = 1, $adjustmentMonths = 0) |
||||
{ |
||||
return DateTimeExcel\Month::lastDay($dateValue, $adjustmentMonths); |
||||
} |
||||
} |
@ -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, |
||||
]; |
||||
} |
@ -0,0 +1,59 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTimeImmutable; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Current |
||||
{ |
||||
/** |
||||
* DATENOW. |
||||
* |
||||
* Returns the current date. |
||||
* The NOW function is useful when you need to display the current date and time on a worksheet or |
||||
* calculate a value based on the current date and time, and have that value updated each time you |
||||
* open the worksheet. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* TODAY() |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function today() |
||||
{ |
||||
$dti = new DateTimeImmutable(); |
||||
$dateArray = Helpers::dateParse($dti->format('c')); |
||||
|
||||
return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : ExcelError::VALUE(); |
||||
} |
||||
|
||||
/** |
||||
* DATETIMENOW. |
||||
* |
||||
* Returns the current date and time. |
||||
* The NOW function is useful when you need to display the current date and time on a worksheet or |
||||
* calculate a value based on the current date and time, and have that value updated each time you |
||||
* open the worksheet. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* NOW() |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function now() |
||||
{ |
||||
$dti = new DateTimeImmutable(); |
||||
$dateArray = Helpers::dateParse($dti->format('c')); |
||||
|
||||
return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : ExcelError::VALUE(); |
||||
} |
||||
} |
@ -0,0 +1,172 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
||||
class Date |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DATE. |
||||
* |
||||
* The DATE function returns a value that represents a particular date. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* DATE(year,month,day) |
||||
* |
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function. |
||||
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted, |
||||
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language. |
||||
* |
||||
* @param array|int $year The value of the year argument can include one to four digits. |
||||
* Excel interprets the year argument according to the configured |
||||
* date system: 1900 or 1904. |
||||
* If year is between 0 (zero) and 1899 (inclusive), Excel adds that |
||||
* value to 1900 to calculate the year. For example, DATE(108,1,2) |
||||
* returns January 2, 2008 (1900+108). |
||||
* If year is between 1900 and 9999 (inclusive), Excel uses that |
||||
* value as the year. For example, DATE(2008,1,2) returns January 2, |
||||
* 2008. |
||||
* If year is less than 0 or is 10000 or greater, Excel returns the |
||||
* #NUM! error value. |
||||
* @param array|int $month A positive or negative integer representing the month of the year |
||||
* from 1 to 12 (January to December). |
||||
* If month is greater than 12, month adds that number of months to |
||||
* the first month in the year specified. For example, DATE(2008,14,2) |
||||
* returns the serial number representing February 2, 2009. |
||||
* If month is less than 1, month subtracts the magnitude of that |
||||
* number of months, plus 1, from the first month in the year |
||||
* specified. For example, DATE(2008,-3,2) returns the serial number |
||||
* representing September 2, 2007. |
||||
* @param array|int $day A positive or negative integer representing the day of the month |
||||
* from 1 to 31. |
||||
* If day is greater than the number of days in the month specified, |
||||
* day adds that number of days to the first day in the month. For |
||||
* example, DATE(2008,1,35) returns the serial number representing |
||||
* February 4, 2008. |
||||
* If day is less than 1, day subtracts the magnitude that number of |
||||
* days, plus one, from the first day of the month specified. For |
||||
* example, DATE(2008,1,-15) returns the serial number representing |
||||
* December 16, 2007. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function fromYMD($year, $month, $day) |
||||
{ |
||||
if (is_array($year) || is_array($month) || is_array($day)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $year, $month, $day); |
||||
} |
||||
|
||||
$baseYear = SharedDateHelper::getExcelCalendar(); |
||||
|
||||
try { |
||||
$year = self::getYear($year, $baseYear); // must be int - Scrutinizer is wrong |
||||
$month = self::getMonth($month); |
||||
$day = self::getDay($day); |
||||
self::adjustYearMonth(/** @scrutinizer ignore-type */ $year, $month, $baseYear); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$excelDateValue = SharedDateHelper::formattedPHPToExcel(/** @scrutinizer ignore-type */ $year, $month, $day); |
||||
|
||||
return Helpers::returnIn3FormatsFloat($excelDateValue); |
||||
} |
||||
|
||||
/** |
||||
* Convert year from multiple formats to int. |
||||
* |
||||
* @param mixed $year |
||||
*/ |
||||
private static function getYear($year, int $baseYear): int |
||||
{ |
||||
$year = ($year !== null) ? StringHelper::testStringAsNumeric((string) $year) : 0; |
||||
if (!is_numeric($year)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
$year = (int) $year; |
||||
|
||||
if ($year < ($baseYear - 1900)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
if (($year < $baseYear) && ($year >= ($baseYear - 1900))) { |
||||
$year += 1900; |
||||
} |
||||
|
||||
return (int) $year; |
||||
} |
||||
|
||||
/** |
||||
* Convert month from multiple formats to int. |
||||
* |
||||
* @param mixed $month |
||||
*/ |
||||
private static function getMonth($month): int |
||||
{ |
||||
if (($month !== null) && (!is_numeric($month))) { |
||||
$month = SharedDateHelper::monthStringToNumber($month); |
||||
} |
||||
|
||||
$month = ($month !== null) ? StringHelper::testStringAsNumeric((string) $month) : 0; |
||||
if (!is_numeric($month)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (int) $month; |
||||
} |
||||
|
||||
/** |
||||
* Convert day from multiple formats to int. |
||||
* |
||||
* @param mixed $day |
||||
*/ |
||||
private static function getDay($day): int |
||||
{ |
||||
if (($day !== null) && (!is_numeric($day))) { |
||||
$day = SharedDateHelper::dayStringToNumber($day); |
||||
} |
||||
|
||||
$day = ($day !== null) ? StringHelper::testStringAsNumeric((string) $day) : 0; |
||||
if (!is_numeric($day)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (int) $day; |
||||
} |
||||
|
||||
private static function adjustYearMonth(int &$year, int &$month, int $baseYear): void |
||||
{ |
||||
if ($month < 1) { |
||||
// Handle year/month adjustment if month < 1 |
||||
--$month; |
||||
$year += ceil($month / 12) - 1; |
||||
$month = 13 - abs($month % 12); |
||||
} elseif ($month > 12) { |
||||
// Handle year/month adjustment if month > 12 |
||||
$year += floor($month / 12); |
||||
$month = ($month % 12); |
||||
} |
||||
|
||||
// Re-validate the year parameter after adjustments |
||||
if (($year < $baseYear) || ($year >= 10000)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,151 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class DateParts |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DAYOFMONTH. |
||||
* |
||||
* Returns the day of the month, for a specified date. The day is given as an integer |
||||
* ranging from 1 to 31. |
||||
* |
||||
* Excel Function: |
||||
* DAY(dateValue) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* |
||||
* @return array|int|string Day of the month |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function day($dateValue) |
||||
{ |
||||
if (is_array($dateValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
} |
||||
|
||||
$weirdResult = self::weirdCondition($dateValue); |
||||
if ($weirdResult >= 0) { |
||||
return $weirdResult; |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
||||
return (int) $PHPDateObject->format('j'); |
||||
} |
||||
|
||||
/** |
||||
* MONTHOFYEAR. |
||||
* |
||||
* Returns the month of a date represented by a serial number. |
||||
* The month is given as an integer, ranging from 1 (January) to 12 (December). |
||||
* |
||||
* Excel Function: |
||||
* MONTH(dateValue) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* |
||||
* @return array|int|string Month of the year |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function month($dateValue) |
||||
{ |
||||
if (is_array($dateValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) { |
||||
return 1; |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
||||
return (int) $PHPDateObject->format('n'); |
||||
} |
||||
|
||||
/** |
||||
* YEAR. |
||||
* |
||||
* Returns the year corresponding to a date. |
||||
* The year is returned as an integer in the range 1900-9999. |
||||
* |
||||
* Excel Function: |
||||
* YEAR(dateValue) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* |
||||
* @return array|int|string Year |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function year($dateValue) |
||||
{ |
||||
if (is_array($dateValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($dateValue < 1 && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900) { |
||||
return 1900; |
||||
} |
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
||||
return (int) $PHPDateObject->format('Y'); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
*/ |
||||
private static function weirdCondition($dateValue): int |
||||
{ |
||||
// Excel does not treat 0 consistently for DAY vs. (MONTH or YEAR) |
||||
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900 && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { |
||||
if (is_bool($dateValue)) { |
||||
return (int) $dateValue; |
||||
} |
||||
if ($dateValue === null) { |
||||
return 0; |
||||
} |
||||
if (is_numeric($dateValue) && $dateValue < 1 && $dateValue >= 0) { |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
return -1; |
||||
} |
||||
} |
@ -0,0 +1,162 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTimeImmutable; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class DateValue |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DATEVALUE. |
||||
* |
||||
* Returns a value that represents a particular date. |
||||
* Use DATEVALUE to convert a date represented by a text string to an Excel or PHP date/time stamp |
||||
* value. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* DATEVALUE(dateValue) |
||||
* |
||||
* @param null|array|string $dateValue Text that represents a date in a Microsoft Excel date format. |
||||
* For example, "1/30/2008" or "30-Jan-2008" are text strings within |
||||
* quotation marks that represent dates. Using the default date |
||||
* system in Excel for Windows, date_text must represent a date from |
||||
* January 1, 1900, to December 31, 9999. Using the default date |
||||
* system in Excel for the Macintosh, date_text must represent a date |
||||
* from January 1, 1904, to December 31, 9999. DATEVALUE returns the |
||||
* #VALUE! error value if date_text is out of this range. |
||||
* Or can be an array of date values |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function fromString($dateValue) |
||||
{ |
||||
if (is_array($dateValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
} |
||||
|
||||
// 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($dateValue ?? '', '"'); |
||||
// Strip any ordinals because they're allowed in Excel (English only) |
||||
$dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); |
||||
// Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) |
||||
$dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue); |
||||
|
||||
$yearFound = false; |
||||
$t1 = explode(' ', $dateValue); |
||||
$t = ''; |
||||
foreach ($t1 as &$t) { |
||||
if ((is_numeric($t)) && ($t > 31)) { |
||||
if ($yearFound) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
if ($t < 100) { |
||||
$t += 1900; |
||||
} |
||||
$yearFound = true; |
||||
} |
||||
} |
||||
if (count($t1) === 1) { |
||||
// We've been fed a time value without any date |
||||
return ((strpos((string) $t, ':') === false)) ? ExcelError::Value() : 0.0; |
||||
} |
||||
unset($t); |
||||
|
||||
$dateValue = self::t1ToString($t1, $dti, $yearFound); |
||||
|
||||
$PHPDateArray = self::setUpArray($dateValue, $dti); |
||||
|
||||
return self::finalResults($PHPDateArray, $dti, $baseYear); |
||||
} |
||||
|
||||
private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string |
||||
{ |
||||
if (count($t1) == 2) { |
||||
// We only have two parts of the date: either day/month or month/year |
||||
if ($yearFound) { |
||||
array_unshift($t1, 1); |
||||
} else { |
||||
if (is_numeric($t1[1]) && $t1[1] > 29) { |
||||
$t1[1] += 1900; |
||||
array_unshift($t1, 1); |
||||
} else { |
||||
$t1[] = $dti->format('Y'); |
||||
} |
||||
} |
||||
} |
||||
$dateValue = implode(' ', $t1); |
||||
|
||||
return $dateValue; |
||||
} |
||||
|
||||
/** |
||||
* Parse date. |
||||
*/ |
||||
private static function setUpArray(string $dateValue, DateTimeImmutable $dti): array |
||||
{ |
||||
$PHPDateArray = Helpers::dateParse($dateValue); |
||||
if (!Helpers::dateParseSucceeded($PHPDateArray)) { |
||||
// If original count was 1, we've already returned. |
||||
// If it was 2, we added another. |
||||
// Therefore, neither of the first 2 stroks below can fail. |
||||
$testVal1 = strtok($dateValue, '- '); |
||||
$testVal2 = strtok('- '); |
||||
$testVal3 = strtok('- ') ?: $dti->format('Y'); |
||||
Helpers::adjustYear((string) $testVal1, (string) $testVal2, $testVal3); |
||||
$PHPDateArray = Helpers::dateParse($testVal1 . '-' . $testVal2 . '-' . $testVal3); |
||||
if (!Helpers::dateParseSucceeded($PHPDateArray)) { |
||||
$PHPDateArray = Helpers::dateParse($testVal2 . '-' . $testVal1 . '-' . $testVal3); |
||||
} |
||||
} |
||||
|
||||
return $PHPDateArray; |
||||
} |
||||
|
||||
/** |
||||
* Final results. |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
private static function finalResults(array $PHPDateArray, DateTimeImmutable $dti, int $baseYear) |
||||
{ |
||||
$retValue = ExcelError::Value(); |
||||
if (Helpers::dateParseSucceeded($PHPDateArray)) { |
||||
// Execute function |
||||
Helpers::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); |
||||
if ($PHPDateArray['year'] < $baseYear) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
Helpers::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); |
||||
Helpers::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); |
||||
$PHPDateArray['hour'] = 0; |
||||
$PHPDateArray['minute'] = 0; |
||||
$PHPDateArray['second'] = 0; |
||||
$month = (int) $PHPDateArray['month']; |
||||
$day = (int) $PHPDateArray['day']; |
||||
$year = (int) $PHPDateArray['year']; |
||||
if (!checkdate($month, $day, $year)) { |
||||
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE(); |
||||
} |
||||
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true); |
||||
} |
||||
|
||||
return $retValue; |
||||
} |
||||
} |
@ -0,0 +1,62 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTimeInterface; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Days |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DAYS. |
||||
* |
||||
* Returns the number of days between two dates |
||||
* |
||||
* Excel Function: |
||||
* DAYS(endDate, startDate) |
||||
* |
||||
* @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float), |
||||
* PHP date timestamp (integer), PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float), |
||||
* PHP date timestamp (integer), PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* |
||||
* @return array|int|string Number of days between start date and end date or an error |
||||
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function between($endDate, $startDate) |
||||
{ |
||||
if (is_array($endDate) || is_array($startDate)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $endDate, $startDate); |
||||
} |
||||
|
||||
try { |
||||
$startDate = Helpers::getDateValue($startDate); |
||||
$endDate = Helpers::getDateValue($endDate); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); |
||||
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); |
||||
|
||||
$days = ExcelError::VALUE(); |
||||
$diff = $PHPStartDateObject->diff($PHPEndDateObject); |
||||
if ($diff !== false && !is_bool($diff->days)) { |
||||
$days = $diff->days; |
||||
if ($diff->invert) { |
||||
$days = -$days; |
||||
} |
||||
} |
||||
|
||||
return $days; |
||||
} |
||||
} |
@ -0,0 +1,118 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Days360 |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DAYS360. |
||||
* |
||||
* Returns the number of days between two dates based on a 360-day year (twelve 30-day months), |
||||
* which is used in some accounting calculations. Use this function to help compute payments if |
||||
* your accounting system is based on twelve 30-day months. |
||||
* |
||||
* Excel Function: |
||||
* DAYS360(startDate,endDate[,method]) |
||||
* |
||||
* @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|mixed $method US or European Method as a bool |
||||
* FALSE or omitted: U.S. (NASD) method. If the starting date is |
||||
* the last day of a month, it becomes equal to the 30th of the |
||||
* same month. If the ending date is the last day of a month and |
||||
* the starting date is earlier than the 30th of a month, the |
||||
* ending date becomes equal to the 1st of the next month; |
||||
* otherwise the ending date becomes equal to the 30th of the |
||||
* same month. |
||||
* TRUE: European method. Starting dates and ending dates that |
||||
* occur on the 31st of a month become equal to the 30th of the |
||||
* same month. |
||||
* Or can be an array of methods |
||||
* |
||||
* @return array|int|string Number of days between start date and end date |
||||
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function between($startDate = 0, $endDate = 0, $method = false) |
||||
{ |
||||
if (is_array($startDate) || is_array($endDate) || is_array($method)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); |
||||
} |
||||
|
||||
try { |
||||
$startDate = Helpers::getDateValue($startDate); |
||||
$endDate = Helpers::getDateValue($endDate); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (!is_bool($method)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); |
||||
$startDay = $PHPStartDateObject->format('j'); |
||||
$startMonth = $PHPStartDateObject->format('n'); |
||||
$startYear = $PHPStartDateObject->format('Y'); |
||||
|
||||
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); |
||||
$endDay = $PHPEndDateObject->format('j'); |
||||
$endMonth = $PHPEndDateObject->format('n'); |
||||
$endYear = $PHPEndDateObject->format('Y'); |
||||
|
||||
return self::dateDiff360((int) $startDay, (int) $startMonth, (int) $startYear, (int) $endDay, (int) $endMonth, (int) $endYear, !$method); |
||||
} |
||||
|
||||
/** |
||||
* Return the number of days between two dates based on a 360 day calendar. |
||||
*/ |
||||
private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int |
||||
{ |
||||
$startDay = self::getStartDay($startDay, $startMonth, $startYear, $methodUS); |
||||
$endDay = self::getEndDay($endDay, $endMonth, $endYear, $startDay, $methodUS); |
||||
|
||||
return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360; |
||||
} |
||||
|
||||
private static function getStartDay(int $startDay, int $startMonth, int $startYear, bool $methodUS): int |
||||
{ |
||||
if ($startDay == 31) { |
||||
--$startDay; |
||||
} elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !Helpers::isLeapYear($startYear))))) { |
||||
$startDay = 30; |
||||
} |
||||
|
||||
return $startDay; |
||||
} |
||||
|
||||
private static function getEndDay(int $endDay, int &$endMonth, int &$endYear, int $startDay, bool $methodUS): int |
||||
{ |
||||
if ($endDay == 31) { |
||||
if ($methodUS && $startDay != 30) { |
||||
$endDay = 1; |
||||
if ($endMonth == 12) { |
||||
++$endYear; |
||||
$endMonth = 1; |
||||
} else { |
||||
++$endMonth; |
||||
} |
||||
} else { |
||||
$endDay = 30; |
||||
} |
||||
} |
||||
|
||||
return $endDay; |
||||
} |
||||
} |
@ -0,0 +1,158 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateInterval; |
||||
use DateTime; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Difference |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DATEDIF. |
||||
* |
||||
* @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object |
||||
* or a standard date string |
||||
* Or can be an array of date values |
||||
* @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object |
||||
* or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|string $unit |
||||
* Or can be an array of unit values |
||||
* |
||||
* @return array|int|string Interval between the dates |
||||
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function interval($startDate, $endDate, $unit = 'D') |
||||
{ |
||||
if (is_array($startDate) || is_array($endDate) || is_array($unit)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $unit); |
||||
} |
||||
|
||||
try { |
||||
$startDate = Helpers::getDateValue($startDate); |
||||
$endDate = Helpers::getDateValue($endDate); |
||||
$difference = self::initialDiff($startDate, $endDate); |
||||
$unit = strtoupper($unit); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); |
||||
$startDays = (int) $PHPStartDateObject->format('j'); |
||||
//$startMonths = (int) $PHPStartDateObject->format('n'); |
||||
$startYears = (int) $PHPStartDateObject->format('Y'); |
||||
|
||||
$PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); |
||||
$endDays = (int) $PHPEndDateObject->format('j'); |
||||
//$endMonths = (int) $PHPEndDateObject->format('n'); |
||||
$endYears = (int) $PHPEndDateObject->format('Y'); |
||||
|
||||
$PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject); |
||||
|
||||
$retVal = false; |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'D') ?? self::datedifD($difference); |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'M') ?? self::datedifM($PHPDiffDateObject); |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'MD') ?? self::datedifMD($startDays, $endDays, $PHPEndDateObject, $PHPDiffDateObject); |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'Y') ?? self::datedifY($PHPDiffDateObject); |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'YD') ?? self::datedifYD($difference, $startYears, $endYears, $PHPStartDateObject, $PHPEndDateObject); |
||||
$retVal = self::replaceRetValue($retVal, $unit, 'YM') ?? self::datedifYM($PHPDiffDateObject); |
||||
|
||||
return is_bool($retVal) ? ExcelError::VALUE() : $retVal; |
||||
} |
||||
|
||||
private static function initialDiff(float $startDate, float $endDate): float |
||||
{ |
||||
// Validate parameters |
||||
if ($startDate > $endDate) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $endDate - $startDate; |
||||
} |
||||
|
||||
/** |
||||
* Decide whether it's time to set retVal. |
||||
* |
||||
* @param bool|int $retVal |
||||
* |
||||
* @return null|bool|int |
||||
*/ |
||||
private static function replaceRetValue($retVal, string $unit, string $compare) |
||||
{ |
||||
if ($retVal !== false || $unit !== $compare) { |
||||
return $retVal; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
private static function datedifD(float $difference): int |
||||
{ |
||||
return (int) $difference; |
||||
} |
||||
|
||||
private static function datedifM(DateInterval $PHPDiffDateObject): int |
||||
{ |
||||
return 12 * (int) $PHPDiffDateObject->format('%y') + (int) $PHPDiffDateObject->format('%m'); |
||||
} |
||||
|
||||
private static function datedifMD(int $startDays, int $endDays, DateTime $PHPEndDateObject, DateInterval $PHPDiffDateObject): int |
||||
{ |
||||
if ($endDays < $startDays) { |
||||
$retVal = $endDays; |
||||
$PHPEndDateObject->modify('-' . $endDays . ' days'); |
||||
$adjustDays = (int) $PHPEndDateObject->format('j'); |
||||
$retVal += ($adjustDays - $startDays); |
||||
} else { |
||||
$retVal = (int) $PHPDiffDateObject->format('%d'); |
||||
} |
||||
|
||||
return $retVal; |
||||
} |
||||
|
||||
private static function datedifY(DateInterval $PHPDiffDateObject): int |
||||
{ |
||||
return (int) $PHPDiffDateObject->format('%y'); |
||||
} |
||||
|
||||
private static function datedifYD(float $difference, int $startYears, int $endYears, DateTime $PHPStartDateObject, DateTime $PHPEndDateObject): int |
||||
{ |
||||
$retVal = (int) $difference; |
||||
if ($endYears > $startYears) { |
||||
$isLeapStartYear = $PHPStartDateObject->format('L'); |
||||
$wasLeapEndYear = $PHPEndDateObject->format('L'); |
||||
|
||||
// Adjust end year to be as close as possible as start year |
||||
while ($PHPEndDateObject >= $PHPStartDateObject) { |
||||
$PHPEndDateObject->modify('-1 year'); |
||||
//$endYears = $PHPEndDateObject->format('Y'); |
||||
} |
||||
$PHPEndDateObject->modify('+1 year'); |
||||
|
||||
// Get the result |
||||
$retVal = (int) $PHPEndDateObject->diff($PHPStartDateObject)->days; |
||||
|
||||
// Adjust for leap years cases |
||||
$isLeapEndYear = $PHPEndDateObject->format('L'); |
||||
$limit = new DateTime($PHPEndDateObject->format('Y-02-29')); |
||||
if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) { |
||||
--$retVal; |
||||
} |
||||
} |
||||
|
||||
return (int) $retVal; |
||||
} |
||||
|
||||
private static function datedifYM(DateInterval $PHPDiffDateObject): int |
||||
{ |
||||
return (int) $PHPDiffDateObject->format('%m'); |
||||
} |
||||
} |
@ -0,0 +1,307 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTime; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Helpers |
||||
{ |
||||
/** |
||||
* Identify if a year is a leap year or not. |
||||
* |
||||
* @param int|string $year The year to test |
||||
* |
||||
* @return bool TRUE if the year is a leap year, otherwise FALSE |
||||
*/ |
||||
public static function isLeapYear($year): bool |
||||
{ |
||||
return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0); |
||||
} |
||||
|
||||
/** |
||||
* getDateValue. |
||||
* |
||||
* @param mixed $dateValue |
||||
* |
||||
* @return float Excel date/time serial value |
||||
*/ |
||||
public static function getDateValue($dateValue, bool $allowBool = true): float |
||||
{ |
||||
if (is_object($dateValue)) { |
||||
$retval = SharedDateHelper::PHPToExcel($dateValue); |
||||
if (is_bool($retval)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return $retval; |
||||
} |
||||
|
||||
self::nullFalseTrueToNumber($dateValue, $allowBool); |
||||
if (!is_numeric($dateValue)) { |
||||
$saveReturnDateType = Functions::getReturnDateType(); |
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); |
||||
$dateValue = DateValue::fromString($dateValue); |
||||
Functions::setReturnDateType($saveReturnDateType); |
||||
if (!is_numeric($dateValue)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
} |
||||
if ($dateValue < 0 && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return (float) $dateValue; |
||||
} |
||||
|
||||
/** |
||||
* getTimeValue. |
||||
* |
||||
* @param string $timeValue |
||||
* |
||||
* @return mixed Excel date/time serial value, or string if error |
||||
*/ |
||||
public static function getTimeValue($timeValue) |
||||
{ |
||||
$saveReturnDateType = Functions::getReturnDateType(); |
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); |
||||
$timeValue = TimeValue::fromString($timeValue); |
||||
Functions::setReturnDateType($saveReturnDateType); |
||||
|
||||
return $timeValue; |
||||
} |
||||
|
||||
/** |
||||
* Adjust date by given months. |
||||
* |
||||
* @param mixed $dateValue |
||||
*/ |
||||
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime |
||||
{ |
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
$oMonth = (int) $PHPDateObject->format('m'); |
||||
$oYear = (int) $PHPDateObject->format('Y'); |
||||
|
||||
$adjustmentMonthsString = (string) $adjustmentMonths; |
||||
if ($adjustmentMonths > 0) { |
||||
$adjustmentMonthsString = '+' . $adjustmentMonths; |
||||
} |
||||
if ($adjustmentMonths != 0) { |
||||
$PHPDateObject->modify($adjustmentMonthsString . ' months'); |
||||
} |
||||
$nMonth = (int) $PHPDateObject->format('m'); |
||||
$nYear = (int) $PHPDateObject->format('Y'); |
||||
|
||||
$monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12); |
||||
if ($monthDiff != $adjustmentMonths) { |
||||
$adjustDays = (int) $PHPDateObject->format('d'); |
||||
$adjustDaysString = '-' . $adjustDays . ' days'; |
||||
$PHPDateObject->modify($adjustDaysString); |
||||
} |
||||
|
||||
return $PHPDateObject; |
||||
} |
||||
|
||||
/** |
||||
* Help reduce perceived complexity of some tests. |
||||
* |
||||
* @param mixed $value |
||||
* @param mixed $altValue |
||||
*/ |
||||
public static function replaceIfEmpty(&$value, $altValue): void |
||||
{ |
||||
$value = $value ?: $altValue; |
||||
} |
||||
|
||||
/** |
||||
* Adjust year in ambiguous situations. |
||||
*/ |
||||
public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void |
||||
{ |
||||
if (!is_numeric($testVal1) || $testVal1 < 31) { |
||||
if (!is_numeric($testVal2) || $testVal2 < 12) { |
||||
if (is_numeric($testVal3) && $testVal3 < 12) { |
||||
$testVal3 += 2000; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return result in one of three formats. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) |
||||
{ |
||||
$retType = Functions::getReturnDateType(); |
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { |
||||
return new DateTime( |
||||
$dateArray['year'] |
||||
. '-' . $dateArray['month'] |
||||
. '-' . $dateArray['day'] |
||||
. ' ' . $dateArray['hour'] |
||||
. ':' . $dateArray['minute'] |
||||
. ':' . $dateArray['second'] |
||||
); |
||||
} |
||||
$excelDateValue = |
||||
SharedDateHelper::formattedPHPToExcel( |
||||
$dateArray['year'], |
||||
$dateArray['month'], |
||||
$dateArray['day'], |
||||
$dateArray['hour'], |
||||
$dateArray['minute'], |
||||
$dateArray['second'] |
||||
); |
||||
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; |
||||
} |
||||
// RETURNDATE_UNIX_TIMESTAMP) |
||||
|
||||
return (int) SharedDateHelper::excelToTimestamp($excelDateValue); |
||||
} |
||||
|
||||
/** |
||||
* Return result in one of three formats. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public static function returnIn3FormatsFloat(float $excelDateValue) |
||||
{ |
||||
$retType = Functions::getReturnDateType(); |
||||
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
return $excelDateValue; |
||||
} |
||||
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { |
||||
return (int) SharedDateHelper::excelToTimestamp($excelDateValue); |
||||
} |
||||
// RETURNDATE_PHP_DATETIME_OBJECT |
||||
|
||||
return SharedDateHelper::excelToDateTimeObject($excelDateValue); |
||||
} |
||||
|
||||
/** |
||||
* Return result in one of three formats. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public static function returnIn3FormatsObject(DateTime $PHPDateObject) |
||||
{ |
||||
$retType = Functions::getReturnDateType(); |
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { |
||||
return $PHPDateObject; |
||||
} |
||||
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
return (float) SharedDateHelper::PHPToExcel($PHPDateObject); |
||||
} |
||||
// RETURNDATE_UNIX_TIMESTAMP |
||||
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject); |
||||
$stamp = is_bool($stamp) ? ((int) $stamp) : $stamp; |
||||
|
||||
return (int) SharedDateHelper::excelToTimestamp($stamp); |
||||
} |
||||
|
||||
private static function baseDate(): int |
||||
{ |
||||
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { |
||||
return 0; |
||||
} |
||||
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) { |
||||
return 0; |
||||
} |
||||
|
||||
return 1; |
||||
} |
||||
|
||||
/** |
||||
* Many functions accept null/false/true argument treated as 0/0/1. |
||||
* |
||||
* @param mixed $number |
||||
*/ |
||||
public static function nullFalseTrueToNumber(&$number, bool $allowBool = true): void |
||||
{ |
||||
$number = Functions::flattenSingleValue($number); |
||||
$nullVal = self::baseDate(); |
||||
if ($number === null) { |
||||
$number = $nullVal; |
||||
} elseif ($allowBool && is_bool($number)) { |
||||
$number = $nullVal + (int) $number; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Many functions accept null argument treated as 0. |
||||
* |
||||
* @param mixed $number |
||||
* |
||||
* @return float|int |
||||
*/ |
||||
public static function validateNumericNull($number) |
||||
{ |
||||
$number = Functions::flattenSingleValue($number); |
||||
if ($number === null) { |
||||
return 0; |
||||
} |
||||
if (is_int($number)) { |
||||
return $number; |
||||
} |
||||
if (is_numeric($number)) { |
||||
return (float) $number; |
||||
} |
||||
|
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
/** |
||||
* Many functions accept null/false/true argument treated as 0/0/1. |
||||
* |
||||
* @param mixed $number |
||||
* |
||||
* @return float |
||||
*/ |
||||
public static function validateNotNegative($number) |
||||
{ |
||||
if (!is_numeric($number)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
if ($number >= 0) { |
||||
return (float) $number; |
||||
} |
||||
|
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void |
||||
{ |
||||
$isoDate = $PHPDateObject->format('c'); |
||||
if ($isoDate < '1900-03-01') { |
||||
$PHPDateObject->modify($mod); |
||||
} |
||||
} |
||||
|
||||
public static function dateParse(string $string): array |
||||
{ |
||||
return self::forceArray(date_parse($string)); |
||||
} |
||||
|
||||
public static function dateParseSucceeded(array $dateArray): bool |
||||
{ |
||||
return $dateArray['error_count'] === 0; |
||||
} |
||||
|
||||
/** |
||||
* Despite documentation, date_parse probably never returns false. |
||||
* Just in case, this routine helps guarantee it. |
||||
* |
||||
* @param array|false $dateArray |
||||
*/ |
||||
private static function forceArray($dateArray): array |
||||
{ |
||||
return is_array($dateArray) ? $dateArray : ['error_count' => 1]; |
||||
} |
||||
} |
@ -0,0 +1,103 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
||||
class Month |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* EDATE. |
||||
* |
||||
* Returns the serial number that represents the date that is the indicated number of months |
||||
* before or after a specified date (the start_date). |
||||
* Use EDATE to calculate maturity dates or due dates that fall on the same day of the month |
||||
* as the date of issue. |
||||
* |
||||
* Excel Function: |
||||
* EDATE(dateValue,adjustmentMonths) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|int $adjustmentMonths The number of months before or after start_date. |
||||
* A positive value for months yields a future date; |
||||
* a negative value yields a past date. |
||||
* Or can be an array of adjustment values |
||||
* |
||||
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function adjust($dateValue, $adjustmentMonths) |
||||
{ |
||||
if (is_array($dateValue) || is_array($adjustmentMonths)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue, false); |
||||
$adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
$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|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function lastDay($dateValue, $adjustmentMonths) |
||||
{ |
||||
if (is_array($dateValue) || is_array($adjustmentMonths)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue, false); |
||||
$adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
$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); |
||||
} |
||||
} |
@ -0,0 +1,119 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
||||
class NetworkDays |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* NETWORKDAYS. |
||||
* |
||||
* Returns the number of whole working days between start_date and end_date. Working days |
||||
* exclude weekends and any dates identified in holidays. |
||||
* Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days |
||||
* worked during a specific term. |
||||
* |
||||
* Excel Function: |
||||
* NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]]) |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation |
||||
* |
||||
* @return array|int|string Interval between the dates |
||||
* If an array of values is passed for the $startDate or $endDate arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function count($startDate, $endDate, ...$dateArgs) |
||||
{ |
||||
if (is_array($startDate) || is_array($endDate)) { |
||||
return self::evaluateArrayArgumentsSubset( |
||||
[self::class, __FUNCTION__], |
||||
2, |
||||
$startDate, |
||||
$endDate, |
||||
...$dateArgs |
||||
); |
||||
} |
||||
|
||||
try { |
||||
// Retrieve the mandatory start and end date that are referenced in the function definition |
||||
$sDate = Helpers::getDateValue($startDate); |
||||
$eDate = Helpers::getDateValue($endDate); |
||||
$startDate = min($sDate, $eDate); |
||||
$endDate = max($sDate, $eDate); |
||||
// Get the optional days |
||||
$dateArgs = Functions::flattenArray($dateArgs); |
||||
// Test any extra holiday parameters |
||||
$holidayArray = []; |
||||
foreach ($dateArgs as $holidayDate) { |
||||
$holidayArray[] = Helpers::getDateValue($holidayDate); |
||||
} |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$startDow = self::calcStartDow($startDate); |
||||
$endDow = self::calcEndDow($endDate); |
||||
$wholeWeekDays = (int) floor(($endDate - $startDate) / 7) * 5; |
||||
$partWeekDays = self::calcPartWeekDays($startDow, $endDow); |
||||
|
||||
// Test any extra holiday parameters |
||||
$holidayCountedArray = []; |
||||
foreach ($holidayArray as $holidayDate) { |
||||
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { |
||||
if ((Week::day($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) { |
||||
--$partWeekDays; |
||||
$holidayCountedArray[] = $holidayDate; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return self::applySign($wholeWeekDays + $partWeekDays, $sDate, $eDate); |
||||
} |
||||
|
||||
private static function calcStartDow(float $startDate): int |
||||
{ |
||||
$startDow = 6 - (int) Week::day($startDate, 2); |
||||
if ($startDow < 0) { |
||||
$startDow = 5; |
||||
} |
||||
|
||||
return $startDow; |
||||
} |
||||
|
||||
private static function calcEndDow(float $endDate): int |
||||
{ |
||||
$endDow = (int) Week::day($endDate, 2); |
||||
if ($endDow >= 6) { |
||||
$endDow = 0; |
||||
} |
||||
|
||||
return $endDow; |
||||
} |
||||
|
||||
private static function calcPartWeekDays(int $startDow, int $endDow): int |
||||
{ |
||||
$partWeekDays = $endDow + $startDow; |
||||
if ($partWeekDays > 5) { |
||||
$partWeekDays -= 5; |
||||
} |
||||
|
||||
return $partWeekDays; |
||||
} |
||||
|
||||
private static function applySign(int $result, float $sDate, float $eDate): int |
||||
{ |
||||
return ($sDate > $eDate) ? -$result : $result; |
||||
} |
||||
} |
@ -0,0 +1,130 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTime; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Time |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* TIME. |
||||
* |
||||
* The TIME function returns a value that represents a particular time. |
||||
* |
||||
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time |
||||
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
* |
||||
* Excel Function: |
||||
* TIME(hour,minute,second) |
||||
* |
||||
* @param array|int $hour A number from 0 (zero) to 32767 representing the hour. |
||||
* Any value greater than 23 will be divided by 24 and the remainder |
||||
* will be treated as the hour value. For example, TIME(27,0,0) = |
||||
* TIME(3,0,0) = .125 or 3:00 AM. |
||||
* @param array|int $minute A number from 0 to 32767 representing the minute. |
||||
* Any value greater than 59 will be converted to hours and minutes. |
||||
* For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM. |
||||
* @param array|int $second A number from 0 to 32767 representing the second. |
||||
* Any value greater than 59 will be converted to hours, minutes, |
||||
* and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148 |
||||
* or 12:33:20 AM |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
* |
||||
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function fromHMS($hour, $minute, $second) |
||||
{ |
||||
if (is_array($hour) || is_array($minute) || is_array($second)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $hour, $minute, $second); |
||||
} |
||||
|
||||
try { |
||||
$hour = self::toIntWithNullBool($hour); |
||||
$minute = self::toIntWithNullBool($minute); |
||||
$second = self::toIntWithNullBool($second); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
self::adjustSecond($second, $minute); |
||||
self::adjustMinute($minute, $hour); |
||||
|
||||
if ($hour > 23) { |
||||
$hour = $hour % 24; |
||||
} elseif ($hour < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Execute function |
||||
$retType = Functions::getReturnDateType(); |
||||
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
$calendar = SharedDateHelper::getExcelCalendar(); |
||||
$date = (int) ($calendar !== SharedDateHelper::CALENDAR_WINDOWS_1900); |
||||
|
||||
return (float) SharedDateHelper::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); |
||||
} |
||||
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { |
||||
return (int) SharedDateHelper::excelToTimestamp(SharedDateHelper::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 |
||||
} |
||||
// RETURNDATE_PHP_DATETIME_OBJECT |
||||
// Hour has already been normalized (0-23) above |
||||
$phpDateObject = new DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); |
||||
|
||||
return $phpDateObject; |
||||
} |
||||
|
||||
private static function adjustSecond(int &$second, int &$minute): void |
||||
{ |
||||
if ($second < 0) { |
||||
$minute += floor($second / 60); |
||||
$second = 60 - abs($second % 60); |
||||
if ($second == 60) { |
||||
$second = 0; |
||||
} |
||||
} elseif ($second >= 60) { |
||||
$minute += floor($second / 60); |
||||
$second = $second % 60; |
||||
} |
||||
} |
||||
|
||||
private static function adjustMinute(int &$minute, int &$hour): void |
||||
{ |
||||
if ($minute < 0) { |
||||
$hour += floor($minute / 60); |
||||
$minute = 60 - abs($minute % 60); |
||||
if ($minute == 60) { |
||||
$minute = 0; |
||||
} |
||||
} elseif ($minute >= 60) { |
||||
$hour += floor($minute / 60); |
||||
$minute = $minute % 60; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value expect int |
||||
*/ |
||||
private static function toIntWithNullBool($value): int |
||||
{ |
||||
$value = $value ?? 0; |
||||
if (is_bool($value)) { |
||||
$value = (int) $value; |
||||
} |
||||
if (!is_numeric($value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (int) $value; |
||||
} |
||||
} |
@ -0,0 +1,132 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class TimeParts |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* HOUROFDAY. |
||||
* |
||||
* Returns the hour of a time value. |
||||
* The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.). |
||||
* |
||||
* Excel Function: |
||||
* HOUR(timeValue) |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* Or can be an array of date/time values |
||||
* |
||||
* @return array|int|string Hour |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function hour($timeValue) |
||||
{ |
||||
if (is_array($timeValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
} |
||||
|
||||
try { |
||||
Helpers::nullFalseTrueToNumber($timeValue); |
||||
if (!is_numeric($timeValue)) { |
||||
$timeValue = Helpers::getTimeValue($timeValue); |
||||
} |
||||
Helpers::validateNotNegative($timeValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$timeValue = fmod($timeValue, 1); |
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
||||
return (int) $timeValue->format('H'); |
||||
} |
||||
|
||||
/** |
||||
* MINUTE. |
||||
* |
||||
* Returns the minutes of a time value. |
||||
* The minute is given as an integer, ranging from 0 to 59. |
||||
* |
||||
* Excel Function: |
||||
* MINUTE(timeValue) |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* Or can be an array of date/time values |
||||
* |
||||
* @return array|int|string Minute |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function minute($timeValue) |
||||
{ |
||||
if (is_array($timeValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
} |
||||
|
||||
try { |
||||
Helpers::nullFalseTrueToNumber($timeValue); |
||||
if (!is_numeric($timeValue)) { |
||||
$timeValue = Helpers::getTimeValue($timeValue); |
||||
} |
||||
Helpers::validateNotNegative($timeValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$timeValue = fmod($timeValue, 1); |
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
||||
return (int) $timeValue->format('i'); |
||||
} |
||||
|
||||
/** |
||||
* SECOND. |
||||
* |
||||
* Returns the seconds of a time value. |
||||
* The minute is given as an integer, ranging from 0 to 59. |
||||
* |
||||
* Excel Function: |
||||
* SECOND(timeValue) |
||||
* |
||||
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard time string |
||||
* Or can be an array of date/time values |
||||
* |
||||
* @return array|int|string Second |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function second($timeValue) |
||||
{ |
||||
if (is_array($timeValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
} |
||||
|
||||
try { |
||||
Helpers::nullFalseTrueToNumber($timeValue); |
||||
if (!is_numeric($timeValue)) { |
||||
$timeValue = Helpers::getTimeValue($timeValue); |
||||
} |
||||
Helpers::validateNotNegative($timeValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$timeValue = fmod($timeValue, 1); |
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
||||
return (int) $timeValue->format('s'); |
||||
} |
||||
} |
@ -0,0 +1,83 @@
|
||||
<?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|string $timeValue A text string that represents a time in any one of the Microsoft |
||||
* Excel time formats; for example, "6:45 PM" and "18:45" text strings |
||||
* within quotation marks that represent time. |
||||
* Date information in time_text is ignored. |
||||
* Or can be an array of date/time values |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function fromString($timeValue) |
||||
{ |
||||
if (is_array($timeValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
} |
||||
|
||||
// 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($timeValue ?? '', '"'); |
||||
$timeValue = str_replace(['/', '.'], '-', $timeValue); |
||||
|
||||
$arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; |
||||
if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) { |
||||
$arraySplit[0] = ($arraySplit[0] % 24); |
||||
$timeValue = implode(':', $arraySplit); |
||||
} |
||||
|
||||
$PHPDateArray = Helpers::dateParse($timeValue); |
||||
$retValue = ExcelError::VALUE(); |
||||
if (Helpers::dateParseSucceeded($PHPDateArray)) { |
||||
/** @var int */ |
||||
$hour = $PHPDateArray['hour']; |
||||
/** @var int */ |
||||
$minute = $PHPDateArray['minute']; |
||||
/** @var int */ |
||||
$second = $PHPDateArray['second']; |
||||
// OpenOffice-specific code removed - it works just like Excel |
||||
$excelDateValue = SharedDateHelper::formattedPHPToExcel(1900, 1, 1, $hour, $minute, $second) - 1; |
||||
|
||||
$retType = Functions::getReturnDateType(); |
||||
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
$retValue = (float) $excelDateValue; |
||||
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { |
||||
$retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600; |
||||
} else { |
||||
$retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); |
||||
} |
||||
} |
||||
|
||||
return $retValue; |
||||
} |
||||
} |
@ -0,0 +1,278 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use DateTime; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class Week |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* WEEKNUM. |
||||
* |
||||
* Returns the week of the year for a specified date. |
||||
* The WEEKNUM function considers the week containing January 1 to be the first week of the year. |
||||
* However, there is a European standard that defines the first week as the one with the majority |
||||
* of days (four or more) falling in the new year. This means that for years in which there are |
||||
* three days or less in the first week of January, the WEEKNUM function returns week numbers |
||||
* that are incorrect according to the European standard. |
||||
* |
||||
* Excel Function: |
||||
* WEEKNUM(dateValue[,style]) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|int $method Week begins on Sunday or Monday |
||||
* 1 or omitted Week begins on Sunday. |
||||
* 2 Week begins on Monday. |
||||
* 11 Week begins on Monday. |
||||
* 12 Week begins on Tuesday. |
||||
* 13 Week begins on Wednesday. |
||||
* 14 Week begins on Thursday. |
||||
* 15 Week begins on Friday. |
||||
* 16 Week begins on Saturday. |
||||
* 17 Week begins on Sunday. |
||||
* 21 ISO (Jan. 4 is week 1, begins on Monday). |
||||
* Or can be an array of methods |
||||
* |
||||
* @return array|int|string Week Number |
||||
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function number($dateValue, $method = Constants::STARTWEEK_SUNDAY) |
||||
{ |
||||
if (is_array($dateValue) || is_array($method)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $method); |
||||
} |
||||
|
||||
$origDateValueNull = empty($dateValue); |
||||
|
||||
try { |
||||
$method = self::validateMethod($method); |
||||
if ($dateValue === null) { // boolean not allowed |
||||
$dateValue = (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 || $method === Constants::DOW_SUNDAY) ? 0 : 1; |
||||
} |
||||
$dateValue = self::validateDateValue($dateValue); |
||||
if (!$dateValue && self::buggyWeekNum1900($method)) { |
||||
// This seems to be an additional Excel bug. |
||||
return 0; |
||||
} |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
if ($method == Constants::STARTWEEK_MONDAY_ISO) { |
||||
Helpers::silly1900($PHPDateObject); |
||||
|
||||
return (int) $PHPDateObject->format('W'); |
||||
} |
||||
if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { |
||||
return 0; |
||||
} |
||||
Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches |
||||
$dayOfYear = (int) $PHPDateObject->format('z'); |
||||
$PHPDateObject->modify('-' . $dayOfYear . ' days'); |
||||
$firstDayOfFirstWeek = (int) $PHPDateObject->format('w'); |
||||
$daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; |
||||
$daysInFirstWeek += 7 * !$daysInFirstWeek; |
||||
$endFirstWeek = $daysInFirstWeek - 1; |
||||
$weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); |
||||
|
||||
return (int) $weekOfYear; |
||||
} |
||||
|
||||
/** |
||||
* ISOWEEKNUM. |
||||
* |
||||
* Returns the ISO 8601 week number of the year for a specified date. |
||||
* |
||||
* Excel Function: |
||||
* ISOWEEKNUM(dateValue) |
||||
* |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* |
||||
* @return array|int|string Week Number |
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function isoWeekNumber($dateValue) |
||||
{ |
||||
if (is_array($dateValue)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
} |
||||
|
||||
if (self::apparentBug($dateValue)) { |
||||
return 52; |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
Helpers::silly1900($PHPDateObject); |
||||
|
||||
return (int) $PHPDateObject->format('W'); |
||||
} |
||||
|
||||
/** |
||||
* WEEKDAY. |
||||
* |
||||
* Returns the day of the week for a specified date. The day is given as an integer |
||||
* ranging from 0 to 7 (dependent on the requested style). |
||||
* |
||||
* Excel Function: |
||||
* WEEKDAY(dateValue[,style]) |
||||
* |
||||
* @param null|array|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param mixed $style A number that determines the type of return value |
||||
* 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). |
||||
* 2 Numbers 1 (Monday) through 7 (Sunday). |
||||
* 3 Numbers 0 (Monday) through 6 (Sunday). |
||||
* Or can be an array of styles |
||||
* |
||||
* @return array|int|string Day of the week value |
||||
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function day($dateValue, $style = 1) |
||||
{ |
||||
if (is_array($dateValue) || is_array($style)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $style); |
||||
} |
||||
|
||||
try { |
||||
$dateValue = Helpers::getDateValue($dateValue); |
||||
$style = self::validateStyle($style); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Execute function |
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
Helpers::silly1900($PHPDateObject); |
||||
$DoW = (int) $PHPDateObject->format('w'); |
||||
|
||||
switch ($style) { |
||||
case 1: |
||||
++$DoW; |
||||
|
||||
break; |
||||
case 2: |
||||
$DoW = self::dow0Becomes7($DoW); |
||||
|
||||
break; |
||||
case 3: |
||||
$DoW = self::dow0Becomes7($DoW) - 1; |
||||
|
||||
break; |
||||
} |
||||
|
||||
return $DoW; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $style expect int |
||||
*/ |
||||
private static function validateStyle($style): int |
||||
{ |
||||
if (!is_numeric($style)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
$style = (int) $style; |
||||
if (($style < 1) || ($style > 3)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $style; |
||||
} |
||||
|
||||
private static function dow0Becomes7(int $DoW): int |
||||
{ |
||||
return ($DoW === 0) ? 7 : $DoW; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
*/ |
||||
private static function apparentBug($dateValue): bool |
||||
{ |
||||
if (SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { |
||||
if (is_bool($dateValue)) { |
||||
return true; |
||||
} |
||||
if (is_numeric($dateValue) && !((int) $dateValue)) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Validate dateValue parameter. |
||||
* |
||||
* @param mixed $dateValue |
||||
*/ |
||||
private static function validateDateValue($dateValue): float |
||||
{ |
||||
if (is_bool($dateValue)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return Helpers::getDateValue($dateValue); |
||||
} |
||||
|
||||
/** |
||||
* Validate method parameter. |
||||
* |
||||
* @param mixed $method |
||||
*/ |
||||
private static function validateMethod($method): int |
||||
{ |
||||
if ($method === null) { |
||||
$method = Constants::STARTWEEK_SUNDAY; |
||||
} |
||||
|
||||
if (!is_numeric($method)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
$method = (int) $method; |
||||
if (!array_key_exists($method, Constants::METHODARR)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
$method = Constants::METHODARR[$method]; |
||||
|
||||
return $method; |
||||
} |
||||
|
||||
private static function buggyWeekNum1900(int $method): bool |
||||
{ |
||||
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900; |
||||
} |
||||
|
||||
private static function buggyWeekNum1904(int $method, bool $origNull, DateTime $dateObject): bool |
||||
{ |
||||
// This appears to be another Excel bug. |
||||
|
||||
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 && |
||||
!$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; |
||||
} |
||||
} |
@ -0,0 +1,201 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
||||
class WorkDay |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* WORKDAY. |
||||
* |
||||
* Returns the date that is the indicated number of working days before or after a date (the |
||||
* starting date). Working days exclude weekends and any dates identified as holidays. |
||||
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected |
||||
* delivery times, or the number of days of work performed. |
||||
* |
||||
* Excel Function: |
||||
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) |
||||
* |
||||
* @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of date values |
||||
* @param array|int $endDays The number of nonweekend and nonholiday days before or after |
||||
* startDate. A positive value for days yields a future date; a |
||||
* negative value yields a past date. |
||||
* Or can be an array of int values |
||||
* @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation |
||||
* |
||||
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function date($startDate, $endDays, ...$dateArgs) |
||||
{ |
||||
if (is_array($startDate) || is_array($endDays)) { |
||||
return self::evaluateArrayArgumentsSubset( |
||||
[self::class, __FUNCTION__], |
||||
2, |
||||
$startDate, |
||||
$endDays, |
||||
...$dateArgs |
||||
); |
||||
} |
||||
|
||||
// Retrieve the mandatory start date and days that are referenced in the function definition |
||||
try { |
||||
$startDate = Helpers::getDateValue($startDate); |
||||
$endDays = Helpers::validateNumericNull($endDays); |
||||
$holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs)); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$startDate = (float) floor($startDate); |
||||
$endDays = (int) floor($endDays); |
||||
// If endDays is 0, we always return startDate |
||||
if ($endDays == 0) { |
||||
return $startDate; |
||||
} |
||||
if ($endDays < 0) { |
||||
return self::decrementing($startDate, $endDays, $holidayArray); |
||||
} |
||||
|
||||
return self::incrementing($startDate, $endDays, $holidayArray); |
||||
} |
||||
|
||||
/** |
||||
* Use incrementing logic to determine Workday. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
private static function incrementing(float $startDate, int $endDays, array $holidayArray) |
||||
{ |
||||
// Adjust the start date if it falls over a weekend |
||||
$startDoW = self::getWeekDay($startDate, 3); |
||||
if ($startDoW >= 5) { |
||||
$startDate += 7 - $startDoW; |
||||
--$endDays; |
||||
} |
||||
|
||||
// Add endDays |
||||
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
||||
$endDays = $endDays % 5; |
||||
while ($endDays > 0) { |
||||
++$endDate; |
||||
// Adjust the calculated end date if it falls over a weekend |
||||
$endDow = self::getWeekDay($endDate, 3); |
||||
if ($endDow >= 5) { |
||||
$endDate += 7 - $endDow; |
||||
} |
||||
--$endDays; |
||||
} |
||||
|
||||
// Test any extra holiday parameters |
||||
if (!empty($holidayArray)) { |
||||
$endDate = self::incrementingArray($startDate, $endDate, $holidayArray); |
||||
} |
||||
|
||||
return Helpers::returnIn3FormatsFloat($endDate); |
||||
} |
||||
|
||||
private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float |
||||
{ |
||||
$holidayCountedArray = $holidayDates = []; |
||||
foreach ($holidayArray as $holidayDate) { |
||||
if (self::getWeekDay($holidayDate, 3) < 5) { |
||||
$holidayDates[] = $holidayDate; |
||||
} |
||||
} |
||||
sort($holidayDates, SORT_NUMERIC); |
||||
foreach ($holidayDates as $holidayDate) { |
||||
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { |
||||
if (!in_array($holidayDate, $holidayCountedArray)) { |
||||
++$endDate; |
||||
$holidayCountedArray[] = $holidayDate; |
||||
} |
||||
} |
||||
// Adjust the calculated end date if it falls over a weekend |
||||
$endDoW = self::getWeekDay($endDate, 3); |
||||
if ($endDoW >= 5) { |
||||
$endDate += 7 - $endDoW; |
||||
} |
||||
} |
||||
|
||||
return $endDate; |
||||
} |
||||
|
||||
/** |
||||
* Use decrementing logic to determine Workday. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
private static function decrementing(float $startDate, int $endDays, array $holidayArray) |
||||
{ |
||||
// Adjust the start date if it falls over a weekend |
||||
$startDoW = self::getWeekDay($startDate, 3); |
||||
if ($startDoW >= 5) { |
||||
$startDate += -$startDoW + 4; |
||||
++$endDays; |
||||
} |
||||
|
||||
// Add endDays |
||||
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
||||
$endDays = $endDays % 5; |
||||
while ($endDays < 0) { |
||||
--$endDate; |
||||
// Adjust the calculated end date if it falls over a weekend |
||||
$endDow = self::getWeekDay($endDate, 3); |
||||
if ($endDow >= 5) { |
||||
$endDate += 4 - $endDow; |
||||
} |
||||
++$endDays; |
||||
} |
||||
|
||||
// Test any extra holiday parameters |
||||
if (!empty($holidayArray)) { |
||||
$endDate = self::decrementingArray($startDate, $endDate, $holidayArray); |
||||
} |
||||
|
||||
return Helpers::returnIn3FormatsFloat($endDate); |
||||
} |
||||
|
||||
private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float |
||||
{ |
||||
$holidayCountedArray = $holidayDates = []; |
||||
foreach ($holidayArray as $holidayDate) { |
||||
if (self::getWeekDay($holidayDate, 3) < 5) { |
||||
$holidayDates[] = $holidayDate; |
||||
} |
||||
} |
||||
rsort($holidayDates, SORT_NUMERIC); |
||||
foreach ($holidayDates as $holidayDate) { |
||||
if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { |
||||
if (!in_array($holidayDate, $holidayCountedArray)) { |
||||
--$endDate; |
||||
$holidayCountedArray[] = $holidayDate; |
||||
} |
||||
} |
||||
// Adjust the calculated end date if it falls over a weekend |
||||
$endDoW = self::getWeekDay($endDate, 3); |
||||
/** int $endDoW */ |
||||
if ($endDoW >= 5) { |
||||
$endDate += -$endDoW + 4; |
||||
} |
||||
} |
||||
|
||||
return $endDate; |
||||
} |
||||
|
||||
private static function getWeekDay(float $date, int $wd): int |
||||
{ |
||||
$result = Functions::scalar(Week::day($date, $wd)); |
||||
|
||||
return is_int($result) ? $result : -1; |
||||
} |
||||
} |
@ -0,0 +1,133 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
||||
class YearFrac |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* YEARFRAC. |
||||
* |
||||
* Calculates the fraction of the year represented by the number of whole days between two dates |
||||
* (the start_date and the end_date). |
||||
* Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or |
||||
* obligations to assign to a specific term. |
||||
* |
||||
* Excel Function: |
||||
* YEARFRAC(startDate,endDate[,method]) |
||||
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html |
||||
* for description of algorithm used in Excel |
||||
* |
||||
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of values |
||||
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
* PHP DateTime object, or a standard date string |
||||
* Or can be an array of methods |
||||
* @param array|int $method Method used for the calculation |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* Or can be an array of methods |
||||
* |
||||
* @return array|float|string fraction of the year, or a string containing an error |
||||
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function fraction($startDate, $endDate, $method = 0) |
||||
{ |
||||
if (is_array($startDate) || is_array($endDate) || is_array($method)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); |
||||
} |
||||
|
||||
try { |
||||
$method = (int) Helpers::validateNumericNull($method); |
||||
$sDate = Helpers::getDateValue($startDate); |
||||
$eDate = Helpers::getDateValue($endDate); |
||||
$sDate = self::excelBug($sDate, $startDate, $endDate, $method); |
||||
$eDate = self::excelBug($eDate, $endDate, $startDate, $method); |
||||
$startDate = min($sDate, $eDate); |
||||
$endDate = max($sDate, $eDate); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
switch ($method) { |
||||
case 0: |
||||
return Functions::scalar(Days360::between($startDate, $endDate)) / 360; |
||||
case 1: |
||||
return self::method1($startDate, $endDate); |
||||
case 2: |
||||
return Functions::scalar(Difference::interval($startDate, $endDate)) / 360; |
||||
case 3: |
||||
return Functions::scalar(Difference::interval($startDate, $endDate)) / 365; |
||||
case 4: |
||||
return Functions::scalar(Days360::between($startDate, $endDate, true)) / 360; |
||||
} |
||||
|
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
/** |
||||
* Excel 1900 calendar treats date argument of null as 1900-01-00. Really. |
||||
* |
||||
* @param mixed $startDate |
||||
* @param mixed $endDate |
||||
*/ |
||||
private static function excelBug(float $sDate, $startDate, $endDate, int $method): float |
||||
{ |
||||
if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE && SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { |
||||
if ($endDate === null && $startDate !== null) { |
||||
if (DateParts::month($sDate) == 12 && DateParts::day($sDate) === 31 && $method === 0) { |
||||
$sDate += 2; |
||||
} else { |
||||
++$sDate; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return $sDate; |
||||
} |
||||
|
||||
private static function method1(float $startDate, float $endDate): float |
||||
{ |
||||
$days = Functions::scalar(Difference::interval($startDate, $endDate)); |
||||
$startYear = (int) DateParts::year($startDate); |
||||
$endYear = (int) DateParts::year($endDate); |
||||
$years = $endYear - $startYear + 1; |
||||
$startMonth = (int) DateParts::month($startDate); |
||||
$startDay = (int) DateParts::day($startDate); |
||||
$endMonth = (int) DateParts::month($endDate); |
||||
$endDay = (int) DateParts::day($endDate); |
||||
$startMonthDay = 100 * $startMonth + $startDay; |
||||
$endMonthDay = 100 * $endMonth + $endDay; |
||||
if ($years == 1) { |
||||
$tmpCalcAnnualBasis = 365 + (int) Helpers::isLeapYear($endYear); |
||||
} elseif ($years == 2 && $startMonthDay >= $endMonthDay) { |
||||
if (Helpers::isLeapYear($startYear)) { |
||||
$tmpCalcAnnualBasis = 365 + (int) ($startMonthDay <= 229); |
||||
} elseif (Helpers::isLeapYear($endYear)) { |
||||
$tmpCalcAnnualBasis = 365 + (int) ($endMonthDay >= 229); |
||||
} else { |
||||
$tmpCalcAnnualBasis = 365; |
||||
} |
||||
} else { |
||||
$tmpCalcAnnualBasis = 0; |
||||
for ($year = $startYear; $year <= $endYear; ++$year) { |
||||
$tmpCalcAnnualBasis += 365 + (int) Helpers::isLeapYear($year); |
||||
} |
||||
$tmpCalcAnnualBasis /= $years; |
||||
} |
||||
|
||||
return $days / $tmpCalcAnnualBasis; |
||||
} |
||||
} |
@ -0,0 +1,209 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
||||
class ArrayArgumentHelper |
||||
{ |
||||
/** |
||||
* @var int |
||||
*/ |
||||
protected $indexStart = 0; |
||||
|
||||
/** |
||||
* @var array |
||||
*/ |
||||
protected $arguments; |
||||
|
||||
/** |
||||
* @var int |
||||
*/ |
||||
protected $argumentCount; |
||||
|
||||
/** |
||||
* @var array |
||||
*/ |
||||
protected $rows; |
||||
|
||||
/** |
||||
* @var array |
||||
*/ |
||||
protected $columns; |
||||
|
||||
public function initialise(array $arguments): void |
||||
{ |
||||
$keys = array_keys($arguments); |
||||
$this->indexStart = (int) array_shift($keys); |
||||
$this->rows = $this->rows($arguments); |
||||
$this->columns = $this->columns($arguments); |
||||
|
||||
$this->argumentCount = count($arguments); |
||||
$this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns); |
||||
|
||||
$this->rows = $this->rows($arguments); |
||||
$this->columns = $this->columns($arguments); |
||||
|
||||
if ($this->arrayArguments() > 2) { |
||||
throw new Exception('Formulae with more than two array arguments are not supported'); |
||||
} |
||||
} |
||||
|
||||
public function arguments(): array |
||||
{ |
||||
return $this->arguments; |
||||
} |
||||
|
||||
public function hasArrayArgument(): bool |
||||
{ |
||||
return $this->arrayArguments() > 0; |
||||
} |
||||
|
||||
public function getFirstArrayArgumentNumber(): int |
||||
{ |
||||
$rowArrays = $this->filterArray($this->rows); |
||||
$columnArrays = $this->filterArray($this->columns); |
||||
|
||||
for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) { |
||||
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) { |
||||
return ++$index; |
||||
} |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
public function getSingleRowVector(): ?int |
||||
{ |
||||
$rowVectors = $this->getRowVectors(); |
||||
|
||||
return count($rowVectors) === 1 ? array_pop($rowVectors) : null; |
||||
} |
||||
|
||||
private function getRowVectors(): array |
||||
{ |
||||
$rowVectors = []; |
||||
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { |
||||
if ($this->rows[$index] === 1 && $this->columns[$index] > 1) { |
||||
$rowVectors[] = $index; |
||||
} |
||||
} |
||||
|
||||
return $rowVectors; |
||||
} |
||||
|
||||
public function getSingleColumnVector(): ?int |
||||
{ |
||||
$columnVectors = $this->getColumnVectors(); |
||||
|
||||
return count($columnVectors) === 1 ? array_pop($columnVectors) : null; |
||||
} |
||||
|
||||
private function getColumnVectors(): array |
||||
{ |
||||
$columnVectors = []; |
||||
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { |
||||
if ($this->rows[$index] > 1 && $this->columns[$index] === 1) { |
||||
$columnVectors[] = $index; |
||||
} |
||||
} |
||||
|
||||
return $columnVectors; |
||||
} |
||||
|
||||
public function getMatrixPair(): array |
||||
{ |
||||
for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) { |
||||
for ($j = $i + 1; $j < $this->argumentCount; ++$j) { |
||||
if (isset($this->rows[$i], $this->rows[$j])) { |
||||
return [$i, $j]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return []; |
||||
} |
||||
|
||||
public function isVector(int $argument): bool |
||||
{ |
||||
return $this->rows[$argument] === 1 || $this->columns[$argument] === 1; |
||||
} |
||||
|
||||
public function isRowVector(int $argument): bool |
||||
{ |
||||
return $this->rows[$argument] === 1; |
||||
} |
||||
|
||||
public function isColumnVector(int $argument): bool |
||||
{ |
||||
return $this->columns[$argument] === 1; |
||||
} |
||||
|
||||
public function rowCount(int $argument): int |
||||
{ |
||||
return $this->rows[$argument]; |
||||
} |
||||
|
||||
public function columnCount(int $argument): int |
||||
{ |
||||
return $this->columns[$argument]; |
||||
} |
||||
|
||||
private function rows(array $arguments): array |
||||
{ |
||||
return array_map( |
||||
function ($argument) { |
||||
return is_countable($argument) ? count($argument) : 1; |
||||
}, |
||||
$arguments |
||||
); |
||||
} |
||||
|
||||
private function columns(array $arguments): array |
||||
{ |
||||
return array_map( |
||||
function ($argument) { |
||||
return is_array($argument) && is_array($argument[array_keys($argument)[0]]) |
||||
? count($argument[array_keys($argument)[0]]) |
||||
: 1; |
||||
}, |
||||
$arguments |
||||
); |
||||
} |
||||
|
||||
public function arrayArguments(): int |
||||
{ |
||||
$count = 0; |
||||
foreach (array_keys($this->arguments) as $argument) { |
||||
if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) { |
||||
++$count; |
||||
} |
||||
} |
||||
|
||||
return $count; |
||||
} |
||||
|
||||
private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array |
||||
{ |
||||
foreach ($arguments as $index => $argument) { |
||||
if ($rows[$index] === 1 && $columns[$index] === 1) { |
||||
while (is_array($argument)) { |
||||
$argument = array_pop($argument); |
||||
} |
||||
$arguments[$index] = $argument; |
||||
} |
||||
} |
||||
|
||||
return $arguments; |
||||
} |
||||
|
||||
private function filterArray(array $array): array |
||||
{ |
||||
return array_filter( |
||||
$array, |
||||
function ($value) { |
||||
return $value > 1; |
||||
} |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,175 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
||||
class ArrayArgumentProcessor |
||||
{ |
||||
/** |
||||
* @var ArrayArgumentHelper |
||||
*/ |
||||
private static $arrayArgumentHelper; |
||||
|
||||
/** |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
public static function processArguments( |
||||
ArrayArgumentHelper $arrayArgumentHelper, |
||||
callable $method, |
||||
...$arguments |
||||
): array { |
||||
self::$arrayArgumentHelper = $arrayArgumentHelper; |
||||
|
||||
if (self::$arrayArgumentHelper->hasArrayArgument() === false) { |
||||
return [$method(...$arguments)]; |
||||
} |
||||
|
||||
if (self::$arrayArgumentHelper->arrayArguments() === 1) { |
||||
$nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber(); |
||||
|
||||
return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments); |
||||
} |
||||
|
||||
$singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector(); |
||||
$singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector(); |
||||
|
||||
if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) { |
||||
// Basic logic for a single row vector and a single column vector |
||||
return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments); |
||||
} |
||||
|
||||
$matrixPair = self::$arrayArgumentHelper->getMatrixPair(); |
||||
if ($matrixPair !== []) { |
||||
if ( |
||||
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === true && |
||||
self::$arrayArgumentHelper->isVector($matrixPair[1]) === false) || |
||||
(self::$arrayArgumentHelper->isVector($matrixPair[0]) === false && |
||||
self::$arrayArgumentHelper->isVector($matrixPair[1]) === true) |
||||
) { |
||||
// Logic for a matrix and a vector (row or column) |
||||
return self::evaluateVectorMatrixPair($method, $matrixPair, ...$arguments); |
||||
} |
||||
// Logic for matrix/matrix, column vector/column vector or row vector/row vector |
||||
return self::evaluateMatrixPair($method, $matrixPair, ...$arguments); |
||||
} |
||||
|
||||
// Still need to work out the logic for more than two array arguments, |
||||
// For the moment, we're throwing an Exception when we initialise the ArrayArgumentHelper |
||||
return ['#VALUE!']; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array |
||||
{ |
||||
$matrix2 = array_pop($matrixIndexes); |
||||
/** @var array $matrixValues2 */ |
||||
$matrixValues2 = $arguments[$matrix2]; |
||||
$matrix1 = array_pop($matrixIndexes); |
||||
/** @var array $matrixValues1 */ |
||||
$matrixValues1 = $arguments[$matrix1]; |
||||
|
||||
$rows = min(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2])); |
||||
$columns = min(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2])); |
||||
|
||||
if ($rows === 1) { |
||||
$rows = max(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2])); |
||||
} |
||||
if ($columns === 1) { |
||||
$columns = max(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2])); |
||||
} |
||||
|
||||
$result = []; |
||||
for ($rowIndex = 0; $rowIndex < $rows; ++$rowIndex) { |
||||
for ($columnIndex = 0; $columnIndex < $columns; ++$columnIndex) { |
||||
$rowIndex1 = self::$arrayArgumentHelper->isRowVector($matrix1) ? 0 : $rowIndex; |
||||
$columnIndex1 = self::$arrayArgumentHelper->isColumnVector($matrix1) ? 0 : $columnIndex; |
||||
$value1 = $matrixValues1[$rowIndex1][$columnIndex1]; |
||||
$rowIndex2 = self::$arrayArgumentHelper->isRowVector($matrix2) ? 0 : $rowIndex; |
||||
$columnIndex2 = self::$arrayArgumentHelper->isColumnVector($matrix2) ? 0 : $columnIndex; |
||||
$value2 = $matrixValues2[$rowIndex2][$columnIndex2]; |
||||
$arguments[$matrix1] = $value1; |
||||
$arguments[$matrix2] = $value2; |
||||
|
||||
$result[$rowIndex][$columnIndex] = $method(...$arguments); |
||||
} |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
private static function evaluateMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array |
||||
{ |
||||
$matrix2 = array_pop($matrixIndexes); |
||||
/** @var array $matrixValues2 */ |
||||
$matrixValues2 = $arguments[$matrix2]; |
||||
$matrix1 = array_pop($matrixIndexes); |
||||
/** @var array $matrixValues1 */ |
||||
$matrixValues1 = $arguments[$matrix1]; |
||||
|
||||
$result = []; |
||||
foreach ($matrixValues1 as $rowIndex => $row) { |
||||
foreach ($row as $columnIndex => $value1) { |
||||
if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) { |
||||
continue; |
||||
} |
||||
|
||||
$value2 = $matrixValues2[$rowIndex][$columnIndex]; |
||||
$arguments[$matrix1] = $value1; |
||||
$arguments[$matrix2] = $value2; |
||||
|
||||
$result[$rowIndex][$columnIndex] = $method(...$arguments); |
||||
} |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, ...$arguments): array |
||||
{ |
||||
$rowVector = Functions::flattenArray($arguments[$rowIndex]); |
||||
$columnVector = Functions::flattenArray($arguments[$columnIndex]); |
||||
|
||||
$result = []; |
||||
foreach ($columnVector as $column) { |
||||
$rowResults = []; |
||||
foreach ($rowVector as $row) { |
||||
$arguments[$rowIndex] = $row; |
||||
$arguments[$columnIndex] = $column; |
||||
|
||||
$rowResults[] = $method(...$arguments); |
||||
} |
||||
$result[] = $rowResults; |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* Note, offset is from 1 (for the first argument) rather than from 0. |
||||
* |
||||
* @param mixed ...$arguments |
||||
*/ |
||||
private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, ...$arguments): array |
||||
{ |
||||
$values = array_slice($arguments, $nthArgument - 1, 1); |
||||
/** @var array $values */ |
||||
$values = array_pop($values); |
||||
|
||||
$result = []; |
||||
foreach ($values as $value) { |
||||
$arguments[$nthArgument - 1] = $value; |
||||
$result[] = $method(...$arguments); |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
} |
@ -0,0 +1,223 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
||||
class BranchPruner |
||||
{ |
||||
/** |
||||
* @var bool |
||||
*/ |
||||
protected $branchPruningEnabled = true; |
||||
|
||||
/** |
||||
* Used to generate unique store keys. |
||||
* |
||||
* @var int |
||||
*/ |
||||
private $branchStoreKeyCounter = 0; |
||||
|
||||
/** |
||||
* currently pending storeKey (last item of the storeKeysStack. |
||||
* |
||||
* @var ?string |
||||
*/ |
||||
protected $pendingStoreKey; |
||||
|
||||
/** |
||||
* @var string[] |
||||
*/ |
||||
protected $storeKeysStack = []; |
||||
|
||||
/** |
||||
* @var bool[] |
||||
*/ |
||||
protected $conditionMap = []; |
||||
|
||||
/** |
||||
* @var bool[] |
||||
*/ |
||||
protected $thenMap = []; |
||||
|
||||
/** |
||||
* @var bool[] |
||||
*/ |
||||
protected $elseMap = []; |
||||
|
||||
/** |
||||
* @var int[] |
||||
*/ |
||||
protected $braceDepthMap = []; |
||||
|
||||
/** |
||||
* @var null|string |
||||
*/ |
||||
protected $currentCondition; |
||||
|
||||
/** |
||||
* @var null|string |
||||
*/ |
||||
protected $currentOnlyIf; |
||||
|
||||
/** |
||||
* @var null|string |
||||
*/ |
||||
protected $currentOnlyIfNot; |
||||
|
||||
/** |
||||
* @var null|string |
||||
*/ |
||||
protected $previousStoreKey; |
||||
|
||||
public function __construct(bool $branchPruningEnabled) |
||||
{ |
||||
$this->branchPruningEnabled = $branchPruningEnabled; |
||||
} |
||||
|
||||
public function clearBranchStore(): void |
||||
{ |
||||
$this->branchStoreKeyCounter = 0; |
||||
} |
||||
|
||||
public function initialiseForLoop(): void |
||||
{ |
||||
$this->currentCondition = null; |
||||
$this->currentOnlyIf = null; |
||||
$this->currentOnlyIfNot = null; |
||||
$this->previousStoreKey = null; |
||||
$this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack); |
||||
|
||||
if ($this->branchPruningEnabled) { |
||||
$this->initialiseCondition(); |
||||
$this->initialiseThen(); |
||||
$this->initialiseElse(); |
||||
} |
||||
} |
||||
|
||||
private function initialiseCondition(): void |
||||
{ |
||||
if (isset($this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) { |
||||
$this->currentCondition = $this->pendingStoreKey; |
||||
$stackDepth = count($this->storeKeysStack); |
||||
if ($stackDepth > 1) { |
||||
// nested if |
||||
$this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private function initialiseThen(): void |
||||
{ |
||||
if (isset($this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) { |
||||
$this->currentOnlyIf = $this->pendingStoreKey; |
||||
} elseif ( |
||||
isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey]) |
||||
&& $this->thenMap[$this->previousStoreKey] |
||||
) { |
||||
$this->currentOnlyIf = $this->previousStoreKey; |
||||
} |
||||
} |
||||
|
||||
private function initialiseElse(): void |
||||
{ |
||||
if (isset($this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) { |
||||
$this->currentOnlyIfNot = $this->pendingStoreKey; |
||||
} elseif ( |
||||
isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey]) |
||||
&& $this->elseMap[$this->previousStoreKey] |
||||
) { |
||||
$this->currentOnlyIfNot = $this->previousStoreKey; |
||||
} |
||||
} |
||||
|
||||
public function decrementDepth(): void |
||||
{ |
||||
if (!empty($this->pendingStoreKey)) { |
||||
--$this->braceDepthMap[$this->pendingStoreKey]; |
||||
} |
||||
} |
||||
|
||||
public function incrementDepth(): void |
||||
{ |
||||
if (!empty($this->pendingStoreKey)) { |
||||
++$this->braceDepthMap[$this->pendingStoreKey]; |
||||
} |
||||
} |
||||
|
||||
public function functionCall(string $functionName): void |
||||
{ |
||||
if ($this->branchPruningEnabled && ($functionName === 'IF(')) { |
||||
// we handle a new if |
||||
$this->pendingStoreKey = $this->getUnusedBranchStoreKey(); |
||||
$this->storeKeysStack[] = $this->pendingStoreKey; |
||||
$this->conditionMap[$this->pendingStoreKey] = true; |
||||
$this->braceDepthMap[$this->pendingStoreKey] = 0; |
||||
} elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) { |
||||
// this is not an if but we go deeper |
||||
++$this->braceDepthMap[$this->pendingStoreKey]; |
||||
} |
||||
} |
||||
|
||||
public function argumentSeparator(): void |
||||
{ |
||||
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) { |
||||
// We must go to the IF next argument |
||||
if ($this->conditionMap[$this->pendingStoreKey]) { |
||||
$this->conditionMap[$this->pendingStoreKey] = false; |
||||
$this->thenMap[$this->pendingStoreKey] = true; |
||||
} elseif ($this->thenMap[$this->pendingStoreKey]) { |
||||
$this->thenMap[$this->pendingStoreKey] = false; |
||||
$this->elseMap[$this->pendingStoreKey] = true; |
||||
} elseif ($this->elseMap[$this->pendingStoreKey]) { |
||||
throw new Exception('Reaching fourth argument of an IF'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
public function closingBrace($value): void |
||||
{ |
||||
if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) { |
||||
// we are closing an IF( |
||||
if ($value !== 'IF(') { |
||||
throw new Exception('Parser bug we should be in an "IF("'); |
||||
} |
||||
|
||||
if ($this->conditionMap[$this->pendingStoreKey]) { |
||||
throw new Exception('We should not be expecting a condition'); |
||||
} |
||||
|
||||
$this->thenMap[$this->pendingStoreKey] = false; |
||||
$this->elseMap[$this->pendingStoreKey] = false; |
||||
--$this->braceDepthMap[$this->pendingStoreKey]; |
||||
array_pop($this->storeKeysStack); |
||||
$this->pendingStoreKey = null; |
||||
} |
||||
} |
||||
|
||||
public function currentCondition(): ?string |
||||
{ |
||||
return $this->currentCondition; |
||||
} |
||||
|
||||
public function currentOnlyIf(): ?string |
||||
{ |
||||
return $this->currentOnlyIf; |
||||
} |
||||
|
||||
public function currentOnlyIfNot(): ?string |
||||
{ |
||||
return $this->currentOnlyIfNot; |
||||
} |
||||
|
||||
private function getUnusedBranchStoreKey(): string |
||||
{ |
||||
$storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter; |
||||
++$this->branchStoreKeyCounter; |
||||
|
||||
return $storeKeyValue; |
||||
} |
||||
} |
@ -0,0 +1,73 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; |
||||
|
||||
class CyclicReferenceStack |
||||
{ |
||||
/** |
||||
* The call stack for calculated cells. |
||||
* |
||||
* @var mixed[] |
||||
*/ |
||||
private $stack = []; |
||||
|
||||
/** |
||||
* Return the number of entries on the stack. |
||||
* |
||||
* @return int |
||||
*/ |
||||
public function count() |
||||
{ |
||||
return count($this->stack); |
||||
} |
||||
|
||||
/** |
||||
* Push a new entry onto the stack. |
||||
* |
||||
* @param mixed $value |
||||
*/ |
||||
public function push($value): void |
||||
{ |
||||
$this->stack[$value] = $value; |
||||
} |
||||
|
||||
/** |
||||
* Pop the last entry from the stack. |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
public function pop() |
||||
{ |
||||
return array_pop($this->stack); |
||||
} |
||||
|
||||
/** |
||||
* Test to see if a specified entry exists on the stack. |
||||
* |
||||
* @param mixed $value The value to test |
||||
* |
||||
* @return bool |
||||
*/ |
||||
public function onStack($value) |
||||
{ |
||||
return isset($this->stack[$value]); |
||||
} |
||||
|
||||
/** |
||||
* Clear the stack. |
||||
*/ |
||||
public function clear(): void |
||||
{ |
||||
$this->stack = []; |
||||
} |
||||
|
||||
/** |
||||
* Return an array of all entries on the stack. |
||||
* |
||||
* @return mixed[] |
||||
*/ |
||||
public function showStack() |
||||
{ |
||||
return $this->stack; |
||||
} |
||||
} |
@ -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'; |
||||
} |
||||
} |
@ -0,0 +1,142 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; |
||||
|
||||
class Logger |
||||
{ |
||||
/** |
||||
* Flag to determine whether a debug log should be generated by the calculation engine |
||||
* If true, then a debug log will be generated |
||||
* If false, then a debug log will not be generated. |
||||
* |
||||
* @var bool |
||||
*/ |
||||
private $writeDebugLog = false; |
||||
|
||||
/** |
||||
* Flag to determine whether a debug log should be echoed by the calculation engine |
||||
* If true, then a debug log will be echoed |
||||
* If false, then a debug log will not be echoed |
||||
* A debug log can only be echoed if it is generated. |
||||
* |
||||
* @var bool |
||||
*/ |
||||
private $echoDebugLog = false; |
||||
|
||||
/** |
||||
* The debug log generated by the calculation engine. |
||||
* |
||||
* @var string[] |
||||
*/ |
||||
private $debugLog = []; |
||||
|
||||
/** |
||||
* The calculation engine cell reference stack. |
||||
* |
||||
* @var CyclicReferenceStack |
||||
*/ |
||||
private $cellStack; |
||||
|
||||
/** |
||||
* Instantiate a Calculation engine logger. |
||||
*/ |
||||
public function __construct(CyclicReferenceStack $stack) |
||||
{ |
||||
$this->cellStack = $stack; |
||||
} |
||||
|
||||
/** |
||||
* Enable/Disable Calculation engine logging. |
||||
* |
||||
* @param bool $writeDebugLog |
||||
*/ |
||||
public function setWriteDebugLog($writeDebugLog): void |
||||
{ |
||||
$this->writeDebugLog = $writeDebugLog; |
||||
} |
||||
|
||||
/** |
||||
* Return whether calculation engine logging is enabled or disabled. |
||||
* |
||||
* @return bool |
||||
*/ |
||||
public function getWriteDebugLog() |
||||
{ |
||||
return $this->writeDebugLog; |
||||
} |
||||
|
||||
/** |
||||
* Enable/Disable echoing of debug log information. |
||||
* |
||||
* @param bool $echoDebugLog |
||||
*/ |
||||
public function setEchoDebugLog($echoDebugLog): void |
||||
{ |
||||
$this->echoDebugLog = $echoDebugLog; |
||||
} |
||||
|
||||
/** |
||||
* Return whether echoing of debug log information is enabled or disabled. |
||||
* |
||||
* @return bool |
||||
*/ |
||||
public function getEchoDebugLog() |
||||
{ |
||||
return $this->echoDebugLog; |
||||
} |
||||
|
||||
/** |
||||
* Write an entry to the calculation engine debug log. |
||||
* |
||||
* @param mixed $args |
||||
*/ |
||||
public function writeDebugLog(string $message, ...$args): void |
||||
{ |
||||
// Only write the debug log if logging is enabled |
||||
if ($this->writeDebugLog) { |
||||
$message = sprintf($message, ...$args); |
||||
$cellReference = implode(' -> ', $this->cellStack->showStack()); |
||||
if ($this->echoDebugLog) { |
||||
echo $cellReference, |
||||
($this->cellStack->count() > 0 ? ' => ' : ''), |
||||
$message, |
||||
PHP_EOL; |
||||
} |
||||
$this->debugLog[] = $cellReference . |
||||
($this->cellStack->count() > 0 ? ' => ' : '') . |
||||
$message; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Write a series of entries to the calculation engine debug log. |
||||
* |
||||
* @param string[] $args |
||||
*/ |
||||
public function mergeDebugLog(array $args): void |
||||
{ |
||||
if ($this->writeDebugLog) { |
||||
foreach ($args as $entry) { |
||||
$this->writeDebugLog($entry); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Clear the calculation engine debug log. |
||||
*/ |
||||
public function clearLog(): void |
||||
{ |
||||
$this->debugLog = []; |
||||
} |
||||
|
||||
/** |
||||
* Return the calculation engine debug log. |
||||
* |
||||
* @return string[] |
||||
*/ |
||||
public function getLog() |
||||
{ |
||||
return $this->debugLog; |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -0,0 +1,344 @@
|
||||
<?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; |
||||
|
||||
final class StructuredReference implements Operand |
||||
{ |
||||
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 (strpos($srStringRemainder, self::OPEN_BRACE) !== false) { |
||||
++$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(); |
||||
|
||||
return $cellRange; |
||||
} |
||||
|
||||
private function isRowReference(): bool |
||||
{ |
||||
return strpos($this->value, '[@') !== false |
||||
|| strpos($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']') !== false; |
||||
} |
||||
|
||||
/** |
||||
* @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]; |
||||
|
||||
$this->columns = $this->getColumns($cell, $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) { |
||||
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); |
||||
} |
||||
|
||||
/** @var string $reference */ |
||||
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 |
||||
{ |
||||
switch ($reference) { |
||||
case self::ITEM_SPECIFIER_ALL: |
||||
case self::ITEM_SPECIFIER_HEADERS: |
||||
return $this->headersRow ?? $this->firstDataRow; |
||||
case self::ITEM_SPECIFIER_DATA: |
||||
return $this->firstDataRow; |
||||
case self::ITEM_SPECIFIER_TOTALS: |
||||
return $this->totalsRow ?? $this->lastDataRow; |
||||
} |
||||
|
||||
return $this->headersRow ?? $this->firstDataRow; |
||||
} |
||||
|
||||
private function getMaximumRow(string $reference): int |
||||
{ |
||||
switch ($reference) { |
||||
case self::ITEM_SPECIFIER_HEADERS: |
||||
return $this->headersRow ?? $this->firstDataRow; |
||||
case self::ITEM_SPECIFIER_DATA: |
||||
return $this->lastDataRow; |
||||
case self::ITEM_SPECIFIER_ALL: |
||||
case self::ITEM_SPECIFIER_TOTALS: |
||||
return $this->totalsRow ?? $this->lastDataRow; |
||||
} |
||||
|
||||
return $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'; |
||||
/** @var string $reference */ |
||||
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; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,152 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class BesselI |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* BESSELI. |
||||
* |
||||
* Returns the modified Bessel function In(x), which is equivalent to the Bessel function evaluated |
||||
* for purely imaginary arguments |
||||
* |
||||
* Excel Function: |
||||
* BESSELI(x,ord) |
||||
* |
||||
* NOTE: The MS Excel implementation of the BESSELI function is still not accurate. |
||||
* This code provides a more accurate calculation |
||||
* |
||||
* @param mixed $x A float value at which to evaluate the function. |
||||
* If x is nonnumeric, BESSELI returns the #VALUE! error value. |
||||
* Or can be an array of values |
||||
* @param mixed $ord The integer order of the Bessel function. |
||||
* If ord is not an integer, it is truncated. |
||||
* If $ord is nonnumeric, BESSELI returns the #VALUE! error value. |
||||
* If $ord < 0, BESSELI returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string Result, or a string containing an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BESSELI($x, $ord) |
||||
{ |
||||
if (is_array($x) || is_array($ord)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); |
||||
} |
||||
|
||||
try { |
||||
$x = EngineeringValidations::validateFloat($x); |
||||
$ord = EngineeringValidations::validateInt($ord); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($ord < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$fResult = self::calculate($x, $ord); |
||||
|
||||
return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; |
||||
} |
||||
|
||||
private static function calculate(float $x, int $ord): float |
||||
{ |
||||
// special cases |
||||
switch ($ord) { |
||||
case 0: |
||||
return self::besselI0($x); |
||||
case 1: |
||||
return self::besselI1($x); |
||||
} |
||||
|
||||
return self::besselI2($x, $ord); |
||||
} |
||||
|
||||
private static function besselI0(float $x): float |
||||
{ |
||||
$ax = abs($x); |
||||
|
||||
if ($ax < 3.75) { |
||||
$y = $x / 3.75; |
||||
$y = $y * $y; |
||||
|
||||
return 1.0 + $y * (3.5156229 + $y * (3.0899424 + $y * (1.2067492 |
||||
+ $y * (0.2659732 + $y * (0.360768e-1 + $y * 0.45813e-2))))); |
||||
} |
||||
|
||||
$y = 3.75 / $ax; |
||||
|
||||
return (exp($ax) / sqrt($ax)) * (0.39894228 + $y * (0.1328592e-1 + $y * (0.225319e-2 + $y * (-0.157565e-2 |
||||
+ $y * (0.916281e-2 + $y * (-0.2057706e-1 + $y * (0.2635537e-1 + |
||||
$y * (-0.1647633e-1 + $y * 0.392377e-2)))))))); |
||||
} |
||||
|
||||
private static function besselI1(float $x): float |
||||
{ |
||||
$ax = abs($x); |
||||
|
||||
if ($ax < 3.75) { |
||||
$y = $x / 3.75; |
||||
$y = $y * $y; |
||||
$ans = $ax * (0.5 + $y * (0.87890594 + $y * (0.51498869 + $y * (0.15084934 + $y * (0.2658733e-1 + |
||||
$y * (0.301532e-2 + $y * 0.32411e-3)))))); |
||||
|
||||
return ($x < 0.0) ? -$ans : $ans; |
||||
} |
||||
|
||||
$y = 3.75 / $ax; |
||||
$ans = 0.2282967e-1 + $y * (-0.2895312e-1 + $y * (0.1787654e-1 - $y * 0.420059e-2)); |
||||
$ans = 0.39894228 + $y * (-0.3988024e-1 + $y * (-0.362018e-2 + $y * (0.163801e-2 + |
||||
$y * (-0.1031555e-1 + $y * $ans)))); |
||||
$ans *= exp($ax) / sqrt($ax); |
||||
|
||||
return ($x < 0.0) ? -$ans : $ans; |
||||
} |
||||
|
||||
/** |
||||
* Sop to Scrutinizer. |
||||
* |
||||
* @var float |
||||
*/ |
||||
private static $zeroPointZero = 0.0; |
||||
|
||||
private static function besselI2(float $x, int $ord): float |
||||
{ |
||||
if ($x === self::$zeroPointZero) { |
||||
return 0.0; |
||||
} |
||||
|
||||
$tox = 2.0 / abs($x); |
||||
$bip = 0; |
||||
$ans = 0.0; |
||||
$bi = 1.0; |
||||
|
||||
for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) { |
||||
$bim = $bip + $j * $tox * $bi; |
||||
$bip = $bi; |
||||
$bi = $bim; |
||||
|
||||
if (abs($bi) > 1.0e+12) { |
||||
$ans *= 1.0e-12; |
||||
$bi *= 1.0e-12; |
||||
$bip *= 1.0e-12; |
||||
} |
||||
|
||||
if ($j === $ord) { |
||||
$ans = $bip; |
||||
} |
||||
} |
||||
|
||||
$ans *= self::besselI0($x) / $bi; |
||||
|
||||
return ($x < 0.0 && (($ord % 2) === 1)) ? -$ans : $ans; |
||||
} |
||||
} |
@ -0,0 +1,180 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class BesselJ |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* BESSELJ. |
||||
* |
||||
* Returns the Bessel function |
||||
* |
||||
* Excel Function: |
||||
* BESSELJ(x,ord) |
||||
* |
||||
* NOTE: The MS Excel implementation of the BESSELJ function is still not accurate, particularly for higher order |
||||
* values with x < -8 and x > 8. This code provides a more accurate calculation |
||||
* |
||||
* @param mixed $x A float value at which to evaluate the function. |
||||
* If x is nonnumeric, BESSELJ returns the #VALUE! error value. |
||||
* Or can be an array of values |
||||
* @param mixed $ord The integer order of the Bessel function. |
||||
* If ord is not an integer, it is truncated. |
||||
* If $ord is nonnumeric, BESSELJ returns the #VALUE! error value. |
||||
* If $ord < 0, BESSELJ returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string Result, or a string containing an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BESSELJ($x, $ord) |
||||
{ |
||||
if (is_array($x) || is_array($ord)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); |
||||
} |
||||
|
||||
try { |
||||
$x = EngineeringValidations::validateFloat($x); |
||||
$ord = EngineeringValidations::validateInt($ord); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($ord < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$fResult = self::calculate($x, $ord); |
||||
|
||||
return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; |
||||
} |
||||
|
||||
private static function calculate(float $x, int $ord): float |
||||
{ |
||||
// special cases |
||||
switch ($ord) { |
||||
case 0: |
||||
return self::besselJ0($x); |
||||
case 1: |
||||
return self::besselJ1($x); |
||||
} |
||||
|
||||
return self::besselJ2($x, $ord); |
||||
} |
||||
|
||||
private static function besselJ0(float $x): float |
||||
{ |
||||
$ax = abs($x); |
||||
|
||||
if ($ax < 8.0) { |
||||
$y = $x * $x; |
||||
$ans1 = 57568490574.0 + $y * (-13362590354.0 + $y * (651619640.7 + $y * (-11214424.18 + $y * |
||||
(77392.33017 + $y * (-184.9052456))))); |
||||
$ans2 = 57568490411.0 + $y * (1029532985.0 + $y * (9494680.718 + $y * (59272.64853 + $y * |
||||
(267.8532712 + $y * 1.0)))); |
||||
|
||||
return $ans1 / $ans2; |
||||
} |
||||
|
||||
$z = 8.0 / $ax; |
||||
$y = $z * $z; |
||||
$xx = $ax - 0.785398164; |
||||
$ans1 = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); |
||||
$ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * |
||||
(0.7621095161e-6 - $y * 0.934935152e-7))); |
||||
|
||||
return sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); |
||||
} |
||||
|
||||
private static function besselJ1(float $x): float |
||||
{ |
||||
$ax = abs($x); |
||||
|
||||
if ($ax < 8.0) { |
||||
$y = $x * $x; |
||||
$ans1 = $x * (72362614232.0 + $y * (-7895059235.0 + $y * (242396853.1 + $y * |
||||
(-2972611.439 + $y * (15704.48260 + $y * (-30.16036606)))))); |
||||
$ans2 = 144725228442.0 + $y * (2300535178.0 + $y * (18583304.74 + $y * (99447.43394 + $y * |
||||
(376.9991397 + $y * 1.0)))); |
||||
|
||||
return $ans1 / $ans2; |
||||
} |
||||
|
||||
$z = 8.0 / $ax; |
||||
$y = $z * $z; |
||||
$xx = $ax - 2.356194491; |
||||
|
||||
$ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); |
||||
$ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * |
||||
(-0.88228987e-6 + $y * 0.105787412e-6))); |
||||
$ans = sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); |
||||
|
||||
return ($x < 0.0) ? -$ans : $ans; |
||||
} |
||||
|
||||
private static function besselJ2(float $x, int $ord): float |
||||
{ |
||||
$ax = abs($x); |
||||
if ($ax === 0.0) { |
||||
return 0.0; |
||||
} |
||||
|
||||
if ($ax > $ord) { |
||||
return self::besselj2a($ax, $ord, $x); |
||||
} |
||||
|
||||
return self::besselj2b($ax, $ord, $x); |
||||
} |
||||
|
||||
private static function besselj2a(float $ax, int $ord, float $x): 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; |
||||
} |
||||
} |
@ -0,0 +1,135 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class BesselK |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* BESSELK. |
||||
* |
||||
* Returns the modified Bessel function Kn(x), which is equivalent to the Bessel functions evaluated |
||||
* for purely imaginary arguments. |
||||
* |
||||
* Excel Function: |
||||
* BESSELK(x,ord) |
||||
* |
||||
* @param mixed $x A float value at which to evaluate the function. |
||||
* If x is nonnumeric, BESSELK returns the #VALUE! error value. |
||||
* Or can be an array of values |
||||
* @param mixed $ord The integer order of the Bessel function. |
||||
* If ord is not an integer, it is truncated. |
||||
* If $ord is nonnumeric, BESSELK returns the #VALUE! error value. |
||||
* If $ord < 0, BESSELKI returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string Result, or a string containing an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BESSELK($x, $ord) |
||||
{ |
||||
if (is_array($x) || is_array($ord)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); |
||||
} |
||||
|
||||
try { |
||||
$x = EngineeringValidations::validateFloat($x); |
||||
$ord = EngineeringValidations::validateInt($ord); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (($ord < 0) || ($x <= 0.0)) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$fBk = self::calculate($x, $ord); |
||||
|
||||
return (is_nan($fBk)) ? ExcelError::NAN() : $fBk; |
||||
} |
||||
|
||||
private static function calculate(float $x, int $ord): float |
||||
{ |
||||
// special cases |
||||
switch ($ord) { |
||||
case 0: |
||||
return self::besselK0($x); |
||||
case 1: |
||||
return self::besselK1($x); |
||||
} |
||||
|
||||
return self::besselK2($x, $ord); |
||||
} |
||||
|
||||
/** |
||||
* Mollify Phpstan. |
||||
* |
||||
* @codeCoverageIgnore |
||||
*/ |
||||
private static function callBesselI(float $x, int $ord): float |
||||
{ |
||||
$rslt = BesselI::BESSELI($x, $ord); |
||||
if (!is_float($rslt)) { |
||||
throw new Exception('Unexpected array or string'); |
||||
} |
||||
|
||||
return $rslt; |
||||
} |
||||
|
||||
private static function besselK0(float $x): float |
||||
{ |
||||
if ($x <= 2) { |
||||
$fNum2 = $x * 0.5; |
||||
$y = ($fNum2 * $fNum2); |
||||
|
||||
return -log($fNum2) * self::callBesselI($x, 0) + |
||||
(-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y * |
||||
(0.10750e-3 + $y * 0.74e-5)))))); |
||||
} |
||||
|
||||
$y = 2 / $x; |
||||
|
||||
return exp(-$x) / sqrt($x) * |
||||
(1.25331414 + $y * (-0.7832358e-1 + $y * (0.2189568e-1 + $y * (-0.1062446e-1 + $y * |
||||
(0.587872e-2 + $y * (-0.251540e-2 + $y * 0.53208e-3)))))); |
||||
} |
||||
|
||||
private static function besselK1(float $x): float |
||||
{ |
||||
if ($x <= 2) { |
||||
$fNum2 = $x * 0.5; |
||||
$y = ($fNum2 * $fNum2); |
||||
|
||||
return log($fNum2) * self::callBesselI($x, 1) + |
||||
(1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y * |
||||
(-0.110404e-2 + $y * (-0.4686e-4))))))) / $x; |
||||
} |
||||
|
||||
$y = 2 / $x; |
||||
|
||||
return exp(-$x) / sqrt($x) * |
||||
(1.25331414 + $y * (0.23498619 + $y * (-0.3655620e-1 + $y * (0.1504268e-1 + $y * (-0.780353e-2 + $y * |
||||
(0.325614e-2 + $y * (-0.68245e-3))))))); |
||||
} |
||||
|
||||
private static function besselK2(float $x, int $ord): float |
||||
{ |
||||
$fTox = 2 / $x; |
||||
$fBkm = self::besselK0($x); |
||||
$fBk = self::besselK1($x); |
||||
for ($n = 1; $n < $ord; ++$n) { |
||||
$fBkp = $fBkm + $n * $fTox * $fBk; |
||||
$fBkm = $fBk; |
||||
$fBk = $fBkp; |
||||
} |
||||
|
||||
return $fBk; |
||||
} |
||||
} |
@ -0,0 +1,141 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class BesselY |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* BESSELY. |
||||
* |
||||
* Returns the Bessel function, which is also called the Weber function or the Neumann function. |
||||
* |
||||
* Excel Function: |
||||
* BESSELY(x,ord) |
||||
* |
||||
* @param mixed $x A float value at which to evaluate the function. |
||||
* If x is nonnumeric, BESSELY returns the #VALUE! error value. |
||||
* Or can be an array of values |
||||
* @param mixed $ord The integer order of the Bessel function. |
||||
* If ord is not an integer, it is truncated. |
||||
* If $ord is nonnumeric, BESSELY returns the #VALUE! error value. |
||||
* If $ord < 0, BESSELY returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string Result, or a string containing an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BESSELY($x, $ord) |
||||
{ |
||||
if (is_array($x) || is_array($ord)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); |
||||
} |
||||
|
||||
try { |
||||
$x = EngineeringValidations::validateFloat($x); |
||||
$ord = EngineeringValidations::validateInt($ord); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (($ord < 0) || ($x <= 0.0)) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$fBy = self::calculate($x, $ord); |
||||
|
||||
return (is_nan($fBy)) ? ExcelError::NAN() : $fBy; |
||||
} |
||||
|
||||
private static function calculate(float $x, int $ord): float |
||||
{ |
||||
// special cases |
||||
switch ($ord) { |
||||
case 0: |
||||
return self::besselY0($x); |
||||
case 1: |
||||
return self::besselY1($x); |
||||
} |
||||
|
||||
return self::besselY2($x, $ord); |
||||
} |
||||
|
||||
/** |
||||
* Mollify Phpstan. |
||||
* |
||||
* @codeCoverageIgnore |
||||
*/ |
||||
private static function callBesselJ(float $x, int $ord): float |
||||
{ |
||||
$rslt = BesselJ::BESSELJ($x, $ord); |
||||
if (!is_float($rslt)) { |
||||
throw new Exception('Unexpected array or string'); |
||||
} |
||||
|
||||
return $rslt; |
||||
} |
||||
|
||||
private static function besselY0(float $x): float |
||||
{ |
||||
if ($x < 8.0) { |
||||
$y = ($x * $x); |
||||
$ans1 = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y * |
||||
(-86327.92757 + $y * 228.4622733)))); |
||||
$ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * |
||||
(47447.26470 + $y * (226.1030244 + $y)))); |
||||
|
||||
return $ans1 / $ans2 + 0.636619772 * self::callBesselJ($x, 0) * log($x); |
||||
} |
||||
|
||||
$z = 8.0 / $x; |
||||
$y = ($z * $z); |
||||
$xx = $x - 0.785398164; |
||||
$ans1 = 1 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); |
||||
$ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 + $y * |
||||
(-0.934945152e-7)))); |
||||
|
||||
return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); |
||||
} |
||||
|
||||
private static function besselY1(float $x): float |
||||
{ |
||||
if ($x < 8.0) { |
||||
$y = ($x * $x); |
||||
$ans1 = $x * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y * |
||||
(0.7349264551e9 + $y * (-0.4237922726e7 + $y * 0.8511937935e4))))); |
||||
$ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * |
||||
(0.1020426050e6 + $y * (0.3549632885e3 + $y))))); |
||||
|
||||
return ($ans1 / $ans2) + 0.636619772 * (self::callBesselJ($x, 1) * log($x) - 1 / $x); |
||||
} |
||||
|
||||
$z = 8.0 / $x; |
||||
$y = $z * $z; |
||||
$xx = $x - 2.356194491; |
||||
$ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); |
||||
$ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * |
||||
(-0.88228987e-6 + $y * 0.105787412e-6))); |
||||
|
||||
return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); |
||||
} |
||||
|
||||
private static function besselY2(float $x, int $ord): float |
||||
{ |
||||
$fTox = 2.0 / $x; |
||||
$fBym = self::besselY0($x); |
||||
$fBy = self::besselY1($x); |
||||
for ($n = 1; $n < $ord; ++$n) { |
||||
$fByp = $n * $fTox * $fBy - $fBym; |
||||
$fBym = $fBy; |
||||
$fBy = $fByp; |
||||
} |
||||
|
||||
return $fBy; |
||||
} |
||||
} |
@ -0,0 +1,277 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class BitWise |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
const SPLIT_DIVISOR = 2 ** 24; |
||||
|
||||
/** |
||||
* Split a number into upper and lower portions for full 32-bit support. |
||||
* |
||||
* @param float|int $number |
||||
* |
||||
* @return int[] |
||||
*/ |
||||
private static function splitNumber($number): array |
||||
{ |
||||
return [(int) floor($number / self::SPLIT_DIVISOR), (int) fmod($number, self::SPLIT_DIVISOR)]; |
||||
} |
||||
|
||||
/** |
||||
* BITAND. |
||||
* |
||||
* Returns the bitwise AND of two integer values. |
||||
* |
||||
* Excel Function: |
||||
* BITAND(number1, number2) |
||||
* |
||||
* @param array|int $number1 |
||||
* Or can be an array of values |
||||
* @param array|int $number2 |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|int|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BITAND($number1, $number2) |
||||
{ |
||||
if (is_array($number1) || is_array($number2)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); |
||||
} |
||||
|
||||
try { |
||||
$number1 = self::validateBitwiseArgument($number1); |
||||
$number2 = self::validateBitwiseArgument($number2); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
$split1 = self::splitNumber($number1); |
||||
$split2 = self::splitNumber($number2); |
||||
|
||||
return self::SPLIT_DIVISOR * ($split1[0] & $split2[0]) + ($split1[1] & $split2[1]); |
||||
} |
||||
|
||||
/** |
||||
* BITOR. |
||||
* |
||||
* Returns the bitwise OR of two integer values. |
||||
* |
||||
* Excel Function: |
||||
* BITOR(number1, number2) |
||||
* |
||||
* @param array|int $number1 |
||||
* Or can be an array of values |
||||
* @param array|int $number2 |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|int|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BITOR($number1, $number2) |
||||
{ |
||||
if (is_array($number1) || is_array($number2)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); |
||||
} |
||||
|
||||
try { |
||||
$number1 = self::validateBitwiseArgument($number1); |
||||
$number2 = self::validateBitwiseArgument($number2); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$split1 = self::splitNumber($number1); |
||||
$split2 = self::splitNumber($number2); |
||||
|
||||
return self::SPLIT_DIVISOR * ($split1[0] | $split2[0]) + ($split1[1] | $split2[1]); |
||||
} |
||||
|
||||
/** |
||||
* BITXOR. |
||||
* |
||||
* Returns the bitwise XOR of two integer values. |
||||
* |
||||
* Excel Function: |
||||
* BITXOR(number1, number2) |
||||
* |
||||
* @param array|int $number1 |
||||
* Or can be an array of values |
||||
* @param array|int $number2 |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|int|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BITXOR($number1, $number2) |
||||
{ |
||||
if (is_array($number1) || is_array($number2)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); |
||||
} |
||||
|
||||
try { |
||||
$number1 = self::validateBitwiseArgument($number1); |
||||
$number2 = self::validateBitwiseArgument($number2); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$split1 = self::splitNumber($number1); |
||||
$split2 = self::splitNumber($number2); |
||||
|
||||
return self::SPLIT_DIVISOR * ($split1[0] ^ $split2[0]) + ($split1[1] ^ $split2[1]); |
||||
} |
||||
|
||||
/** |
||||
* BITLSHIFT. |
||||
* |
||||
* Returns the number value shifted left by shift_amount bits. |
||||
* |
||||
* Excel Function: |
||||
* BITLSHIFT(number, shift_amount) |
||||
* |
||||
* @param array|int $number |
||||
* Or can be an array of values |
||||
* @param array|int $shiftAmount |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|int|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BITLSHIFT($number, $shiftAmount) |
||||
{ |
||||
if (is_array($number) || is_array($shiftAmount)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); |
||||
} |
||||
|
||||
try { |
||||
$number = self::validateBitwiseArgument($number); |
||||
$shiftAmount = self::validateShiftAmount($shiftAmount); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$result = floor($number * (2 ** $shiftAmount)); |
||||
if ($result > 2 ** 48 - 1) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* BITRSHIFT. |
||||
* |
||||
* Returns the number value shifted right by shift_amount bits. |
||||
* |
||||
* Excel Function: |
||||
* BITRSHIFT(number, shift_amount) |
||||
* |
||||
* @param array|int $number |
||||
* Or can be an array of values |
||||
* @param array|int $shiftAmount |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|int|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function BITRSHIFT($number, $shiftAmount) |
||||
{ |
||||
if (is_array($number) || is_array($shiftAmount)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); |
||||
} |
||||
|
||||
try { |
||||
$number = self::validateBitwiseArgument($number); |
||||
$shiftAmount = self::validateShiftAmount($shiftAmount); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$result = floor($number / (2 ** $shiftAmount)); |
||||
if ($result > 2 ** 48 - 1) { // possible because shiftAmount can be negative |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $result; |
||||
} |
||||
|
||||
/** |
||||
* Validate arguments passed to the bitwise functions. |
||||
* |
||||
* @param mixed $value |
||||
* |
||||
* @return float |
||||
*/ |
||||
private static function validateBitwiseArgument($value) |
||||
{ |
||||
$value = self::nullFalseTrueToNumber($value); |
||||
|
||||
if (is_numeric($value)) { |
||||
$value = (float) $value; |
||||
if ($value == floor($value)) { |
||||
if (($value > 2 ** 48 - 1) || ($value < 0)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return floor($value); |
||||
} |
||||
|
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
/** |
||||
* Validate arguments passed to the bitwise functions. |
||||
* |
||||
* @param mixed $value |
||||
* |
||||
* @return int |
||||
*/ |
||||
private static function validateShiftAmount($value) |
||||
{ |
||||
$value = self::nullFalseTrueToNumber($value); |
||||
|
||||
if (is_numeric($value)) { |
||||
if (abs($value) > 53) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return (int) $value; |
||||
} |
||||
|
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
/** |
||||
* Many functions accept null/false/true argument treated as 0/0/1. |
||||
* |
||||
* @param mixed $number |
||||
* |
||||
* @return mixed |
||||
*/ |
||||
private static function nullFalseTrueToNumber(&$number) |
||||
{ |
||||
if ($number === null) { |
||||
$number = 0; |
||||
} elseif (is_bool($number)) { |
||||
$number = (int) $number; |
||||
} |
||||
|
||||
return $number; |
||||
} |
||||
} |
@ -0,0 +1,82 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
||||
class Compare |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DELTA. |
||||
* |
||||
* Excel Function: |
||||
* DELTA(a[,b]) |
||||
* |
||||
* Tests whether two values are equal. Returns 1 if number1 = number2; returns 0 otherwise. |
||||
* Use this function to filter a set of values. For example, by summing several DELTA |
||||
* functions you calculate the count of equal pairs. This function is also known as the |
||||
* Kronecker Delta function. |
||||
* |
||||
* @param array|float $a the first number |
||||
* Or can be an array of values |
||||
* @param array|float $b The second number. If omitted, b is assumed to be zero. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|int|string (string in the event of an error) |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function DELTA($a, $b = 0.0) |
||||
{ |
||||
if (is_array($a) || is_array($b)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $a, $b); |
||||
} |
||||
|
||||
try { |
||||
$a = EngineeringValidations::validateFloat($a); |
||||
$b = EngineeringValidations::validateFloat($b); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return (int) (abs($a - $b) < 1.0e-15); |
||||
} |
||||
|
||||
/** |
||||
* GESTEP. |
||||
* |
||||
* Excel Function: |
||||
* GESTEP(number[,step]) |
||||
* |
||||
* Returns 1 if number >= step; returns 0 (zero) otherwise |
||||
* Use this function to filter a set of values. For example, by summing several GESTEP |
||||
* functions you calculate the count of values that exceed a threshold. |
||||
* |
||||
* @param array|float $number the value to test against step |
||||
* Or can be an array of values |
||||
* @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|int|string (string in the event of an error) |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function GESTEP($number, $step = 0.0) |
||||
{ |
||||
if (is_array($number) || is_array($step)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $step); |
||||
} |
||||
|
||||
try { |
||||
$number = EngineeringValidations::validateFloat($number); |
||||
$step = EngineeringValidations::validateFloat($step ?? 0.0); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return (int) ($number >= $step); |
||||
} |
||||
} |
@ -0,0 +1,121 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use Complex\Complex as ComplexObject; |
||||
use Complex\Exception as ComplexException; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Complex |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* COMPLEX. |
||||
* |
||||
* Converts real and imaginary coefficients into a complex number of the form x +/- yi or x +/- yj. |
||||
* |
||||
* Excel Function: |
||||
* COMPLEX(realNumber,imaginary[,suffix]) |
||||
* |
||||
* @param mixed $realNumber the real float coefficient of the complex number |
||||
* Or can be an array of values |
||||
* @param mixed $imaginary the imaginary float coefficient of the complex number |
||||
* Or can be an array of values |
||||
* @param mixed $suffix The character suffix for the imaginary component of the complex number. |
||||
* If omitted, the suffix is assumed to be "i". |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function COMPLEX($realNumber = 0.0, $imaginary = 0.0, $suffix = 'i') |
||||
{ |
||||
if (is_array($realNumber) || is_array($imaginary) || is_array($suffix)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $realNumber, $imaginary, $suffix); |
||||
} |
||||
|
||||
$realNumber = $realNumber ?? 0.0; |
||||
$imaginary = $imaginary ?? 0.0; |
||||
$suffix = $suffix ?? 'i'; |
||||
|
||||
try { |
||||
$realNumber = EngineeringValidations::validateFloat($realNumber); |
||||
$imaginary = EngineeringValidations::validateFloat($imaginary); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (($suffix === 'i') || ($suffix === 'j') || ($suffix === '')) { |
||||
$complex = new ComplexObject($realNumber, $imaginary, $suffix); |
||||
|
||||
return (string) $complex; |
||||
} |
||||
|
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
/** |
||||
* IMAGINARY. |
||||
* |
||||
* Returns the imaginary coefficient of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMAGINARY(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the imaginary |
||||
* coefficient |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string (string if an error) |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMAGINARY($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $complex->getImaginary(); |
||||
} |
||||
|
||||
/** |
||||
* IMREAL. |
||||
* |
||||
* Returns the real coefficient of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMREAL(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the real coefficient |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string (string if an error) |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMREAL($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $complex->getReal(); |
||||
} |
||||
} |
@ -0,0 +1,611 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use Complex\Complex as ComplexObject; |
||||
use Complex\Exception as ComplexException; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ComplexFunctions |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* IMABS. |
||||
* |
||||
* Returns the absolute value (modulus) of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMABS(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the absolute value |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMABS($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $complex->abs(); |
||||
} |
||||
|
||||
/** |
||||
* IMARGUMENT. |
||||
* |
||||
* Returns the argument theta of a complex number, i.e. the angle in radians from the real |
||||
* axis to the representation of the number in polar coordinates. |
||||
* |
||||
* Excel Function: |
||||
* IMARGUMENT(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the argument theta |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMARGUMENT($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
|
||||
return $complex->argument(); |
||||
} |
||||
|
||||
/** |
||||
* IMCONJUGATE. |
||||
* |
||||
* Returns the complex conjugate of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCONJUGATE(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the conjugate |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCONJUGATE($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->conjugate(); |
||||
} |
||||
|
||||
/** |
||||
* IMCOS. |
||||
* |
||||
* Returns the cosine of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCOS(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the cosine |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCOS($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->cos(); |
||||
} |
||||
|
||||
/** |
||||
* IMCOSH. |
||||
* |
||||
* Returns the hyperbolic cosine of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCOSH(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the hyperbolic cosine |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCOSH($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->cosh(); |
||||
} |
||||
|
||||
/** |
||||
* IMCOT. |
||||
* |
||||
* Returns the cotangent of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCOT(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the cotangent |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCOT($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->cot(); |
||||
} |
||||
|
||||
/** |
||||
* IMCSC. |
||||
* |
||||
* Returns the cosecant of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCSC(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the cosecant |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCSC($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->csc(); |
||||
} |
||||
|
||||
/** |
||||
* IMCSCH. |
||||
* |
||||
* Returns the hyperbolic cosecant of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMCSCH(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMCSCH($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->csch(); |
||||
} |
||||
|
||||
/** |
||||
* IMSIN. |
||||
* |
||||
* Returns the sine of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSIN(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the sine |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSIN($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->sin(); |
||||
} |
||||
|
||||
/** |
||||
* IMSINH. |
||||
* |
||||
* Returns the hyperbolic sine of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSINH(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the hyperbolic sine |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSINH($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->sinh(); |
||||
} |
||||
|
||||
/** |
||||
* IMSEC. |
||||
* |
||||
* Returns the secant of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSEC(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the secant |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSEC($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->sec(); |
||||
} |
||||
|
||||
/** |
||||
* IMSECH. |
||||
* |
||||
* Returns the hyperbolic secant of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSECH(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the hyperbolic secant |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSECH($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->sech(); |
||||
} |
||||
|
||||
/** |
||||
* IMTAN. |
||||
* |
||||
* Returns the tangent of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMTAN(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the tangent |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMTAN($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->tan(); |
||||
} |
||||
|
||||
/** |
||||
* IMSQRT. |
||||
* |
||||
* Returns the square root of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSQRT(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the square root |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSQRT($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$theta = self::IMARGUMENT($complexNumber); |
||||
if ($theta === ExcelError::DIV0()) { |
||||
return '0'; |
||||
} |
||||
|
||||
return (string) $complex->sqrt(); |
||||
} |
||||
|
||||
/** |
||||
* IMLN. |
||||
* |
||||
* Returns the natural logarithm of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMLN(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the natural logarithm |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMLN($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->ln(); |
||||
} |
||||
|
||||
/** |
||||
* IMLOG10. |
||||
* |
||||
* Returns the common logarithm (base 10) of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMLOG10(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the common logarithm |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMLOG10($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->log10(); |
||||
} |
||||
|
||||
/** |
||||
* IMLOG2. |
||||
* |
||||
* Returns the base-2 logarithm of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMLOG2(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the base-2 logarithm |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMLOG2($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->log2(); |
||||
} |
||||
|
||||
/** |
||||
* IMEXP. |
||||
* |
||||
* Returns the exponential of a complex number in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMEXP(complexNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number for which you want the exponential |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMEXP($complexNumber) |
||||
{ |
||||
if (is_array($complexNumber)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $complex->exp(); |
||||
} |
||||
|
||||
/** |
||||
* IMPOWER. |
||||
* |
||||
* Returns a complex number in x + yi or x + yj text format raised to a power. |
||||
* |
||||
* Excel Function: |
||||
* IMPOWER(complexNumber,realNumber) |
||||
* |
||||
* @param array|string $complexNumber the complex number you want to raise to a power |
||||
* Or can be an array of values |
||||
* @param array|float|int|string $realNumber the power to which you want to raise the complex number |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMPOWER($complexNumber, $realNumber) |
||||
{ |
||||
if (is_array($complexNumber) || is_array($realNumber)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber, $realNumber); |
||||
} |
||||
|
||||
try { |
||||
$complex = new ComplexObject($complexNumber); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
if (!is_numeric($realNumber)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
return (string) $complex->pow((float) $realNumber); |
||||
} |
||||
} |
@ -0,0 +1,134 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use Complex\Complex as ComplexObject; |
||||
use Complex\Exception as ComplexException; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ComplexOperations |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* IMDIV. |
||||
* |
||||
* Returns the quotient of two complex numbers in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMDIV(complexDividend,complexDivisor) |
||||
* |
||||
* @param array|string $complexDividend the complex numerator or dividend |
||||
* Or can be an array of values |
||||
* @param array|string $complexDivisor the complex denominator or divisor |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMDIV($complexDividend, $complexDivisor) |
||||
{ |
||||
if (is_array($complexDividend) || is_array($complexDivisor)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexDividend, $complexDivisor); |
||||
} |
||||
|
||||
try { |
||||
return (string) (new ComplexObject($complexDividend))->divideby(new ComplexObject($complexDivisor)); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* IMSUB. |
||||
* |
||||
* Returns the difference of two complex numbers in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSUB(complexNumber1,complexNumber2) |
||||
* |
||||
* @param array|string $complexNumber1 the complex number from which to subtract complexNumber2 |
||||
* Or can be an array of values |
||||
* @param array|string $complexNumber2 the complex number to subtract from complexNumber1 |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function IMSUB($complexNumber1, $complexNumber2) |
||||
{ |
||||
if (is_array($complexNumber1) || is_array($complexNumber2)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber1, $complexNumber2); |
||||
} |
||||
|
||||
try { |
||||
return (string) (new ComplexObject($complexNumber1))->subtract(new ComplexObject($complexNumber2)); |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* IMSUM. |
||||
* |
||||
* Returns the sum of two or more complex numbers in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMSUM(complexNumber[,complexNumber[,...]]) |
||||
* |
||||
* @param string ...$complexNumbers Series of complex numbers to add |
||||
* |
||||
* @return string |
||||
*/ |
||||
public static function IMSUM(...$complexNumbers) |
||||
{ |
||||
// Return value |
||||
$returnValue = new ComplexObject(0.0); |
||||
$aArgs = Functions::flattenArray($complexNumbers); |
||||
|
||||
try { |
||||
// Loop through the arguments |
||||
foreach ($aArgs as $complex) { |
||||
$returnValue = $returnValue->add(new ComplexObject($complex)); |
||||
} |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $returnValue; |
||||
} |
||||
|
||||
/** |
||||
* IMPRODUCT. |
||||
* |
||||
* Returns the product of two or more complex numbers in x + yi or x + yj text format. |
||||
* |
||||
* Excel Function: |
||||
* IMPRODUCT(complexNumber[,complexNumber[,...]]) |
||||
* |
||||
* @param string ...$complexNumbers Series of complex numbers to multiply |
||||
* |
||||
* @return string |
||||
*/ |
||||
public static function IMPRODUCT(...$complexNumbers) |
||||
{ |
||||
// Return value |
||||
$returnValue = new ComplexObject(1.0); |
||||
$aArgs = Functions::flattenArray($complexNumbers); |
||||
|
||||
try { |
||||
// Loop through the arguments |
||||
foreach ($aArgs as $complex) { |
||||
$returnValue = $returnValue->multiply(new ComplexObject($complex)); |
||||
} |
||||
} catch (ComplexException $e) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (string) $returnValue; |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
class Constants |
||||
{ |
||||
/** |
||||
* EULER. |
||||
*/ |
||||
public const EULER = 2.71828182845904523536; |
||||
} |
@ -0,0 +1,71 @@
|
||||
<?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; |
||||
|
||||
/** @param mixed $value */ |
||||
protected static function validateValue($value): string |
||||
{ |
||||
if (is_bool($value)) { |
||||
if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
$value = (int) $value; |
||||
} |
||||
|
||||
if (is_numeric($value)) { |
||||
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { |
||||
$value = floor((float) $value); |
||||
} |
||||
} |
||||
|
||||
return strtoupper((string) $value); |
||||
} |
||||
|
||||
/** @param mixed $places */ |
||||
protected static function validatePlaces($places = null): ?int |
||||
{ |
||||
if ($places === null) { |
||||
return $places; |
||||
} |
||||
|
||||
if (is_numeric($places)) { |
||||
if ($places < 0 || $places > 10) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return (int) $places; |
||||
} |
||||
|
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
/** |
||||
* Formats a number base string value with leading zeroes. |
||||
* |
||||
* @param string $value The "number" to pad |
||||
* @param ?int $places The length that we want to pad this value |
||||
* |
||||
* @return string The padded "number" |
||||
*/ |
||||
protected static function nbrConversionFormat(string $value, ?int $places): string |
||||
{ |
||||
if ($places !== null) { |
||||
if (strlen($value) <= $places) { |
||||
return substr(str_pad($value, $places, '0', STR_PAD_LEFT), -10); |
||||
} |
||||
|
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return substr($value, -10); |
||||
} |
||||
} |
@ -0,0 +1,163 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ConvertBinary extends ConvertBase |
||||
{ |
||||
/** |
||||
* toDecimal. |
||||
* |
||||
* Return a binary value as decimal. |
||||
* |
||||
* Excel Function: |
||||
* BIN2DEC(x) |
||||
* |
||||
* @param array|string $value The binary number (as a string) that you want to convert. The number |
||||
* cannot contain more than 10 characters (10 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 9 bits are magnitude bits. |
||||
* Negative numbers are represented using two's-complement notation. |
||||
* If number is not a valid binary number, or if number contains more than |
||||
* 10 characters (10 bits), BIN2DEC returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toDecimal($value) |
||||
{ |
||||
if (is_array($value)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateBinary($value); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (strlen($value) == 10 && $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|string $value The binary number (as a string) that you want to convert. The number |
||||
* cannot contain more than 10 characters (10 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 9 bits are magnitude bits. |
||||
* Negative numbers are represented using two's-complement notation. |
||||
* If number is not a valid binary number, or if number contains more than |
||||
* 10 characters (10 bits), BIN2HEX returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, BIN2HEX uses the |
||||
* minimum number of characters necessary. Places is useful for padding the |
||||
* return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, BIN2HEX returns the #VALUE! error value. |
||||
* If places is negative, BIN2HEX returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toHex($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateBinary($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (strlen($value) == 10 && $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|string $value The binary number (as a string) that you want to convert. The number |
||||
* cannot contain more than 10 characters (10 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 9 bits are magnitude bits. |
||||
* Negative numbers are represented using two's-complement notation. |
||||
* If number is not a valid binary number, or if number contains more than |
||||
* 10 characters (10 bits), BIN2OCT returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, BIN2OCT uses the |
||||
* minimum number of characters necessary. Places is useful for padding the |
||||
* return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, BIN2OCT returns the #VALUE! error value. |
||||
* If places is negative, BIN2OCT returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toOctal($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateBinary($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (strlen($value) == 10 && $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; |
||||
} |
||||
} |
@ -0,0 +1,213 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ConvertDecimal extends ConvertBase |
||||
{ |
||||
const LARGEST_OCTAL_IN_DECIMAL = 536870911; |
||||
const SMALLEST_OCTAL_IN_DECIMAL = -536870912; |
||||
const LARGEST_BINARY_IN_DECIMAL = 511; |
||||
const SMALLEST_BINARY_IN_DECIMAL = -512; |
||||
const LARGEST_HEX_IN_DECIMAL = 549755813887; |
||||
const SMALLEST_HEX_IN_DECIMAL = -549755813888; |
||||
|
||||
/** |
||||
* toBinary. |
||||
* |
||||
* Return a decimal value as binary. |
||||
* |
||||
* Excel Function: |
||||
* DEC2BIN(x[,places]) |
||||
* |
||||
* @param array|string $value The decimal integer you want to convert. If number is negative, |
||||
* valid place values are ignored and DEC2BIN returns a 10-character |
||||
* (10-bit) binary number in which the most significant bit is the sign |
||||
* bit. The remaining 9 bits are magnitude bits. Negative numbers are |
||||
* represented using two's-complement notation. |
||||
* If number < -512 or if number > 511, DEC2BIN returns the #NUM! error |
||||
* value. |
||||
* If number is nonnumeric, DEC2BIN returns the #VALUE! error value. |
||||
* If DEC2BIN requires more than places characters, it returns the #NUM! |
||||
* error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, DEC2BIN uses |
||||
* the minimum number of characters necessary. Places is useful for |
||||
* padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, DEC2BIN returns the #VALUE! error value. |
||||
* If places is zero or negative, DEC2BIN returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toBinary($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateDecimal($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$value = (int) floor((float) $value); |
||||
if ($value > self::LARGEST_BINARY_IN_DECIMAL || $value < self::SMALLEST_BINARY_IN_DECIMAL) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$r = decbin($value); |
||||
// Two's Complement |
||||
$r = substr($r, -10); |
||||
|
||||
return self::nbrConversionFormat($r, $places); |
||||
} |
||||
|
||||
/** |
||||
* toHex. |
||||
* |
||||
* Return a decimal value as hex. |
||||
* |
||||
* Excel Function: |
||||
* DEC2HEX(x[,places]) |
||||
* |
||||
* @param array|string $value The decimal integer you want to convert. If number is negative, |
||||
* places is ignored and DEC2HEX returns a 10-character (40-bit) |
||||
* hexadecimal number in which the most significant bit is the sign |
||||
* bit. The remaining 39 bits are magnitude bits. Negative numbers |
||||
* are represented using two's-complement notation. |
||||
* If number < -549,755,813,888 or if number > 549,755,813,887, |
||||
* DEC2HEX returns the #NUM! error value. |
||||
* If number is nonnumeric, DEC2HEX returns the #VALUE! error value. |
||||
* If DEC2HEX requires more than places characters, it returns the |
||||
* #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, DEC2HEX uses |
||||
* the minimum number of characters necessary. Places is useful for |
||||
* padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, DEC2HEX returns the #VALUE! error value. |
||||
* If places is zero or negative, DEC2HEX returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toHex($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateDecimal($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$value = floor((float) $value); |
||||
if ($value > self::LARGEST_HEX_IN_DECIMAL || $value < self::SMALLEST_HEX_IN_DECIMAL) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
$r = strtoupper(dechex((int) $value)); |
||||
$r = self::hex32bit($value, $r); |
||||
|
||||
return self::nbrConversionFormat($r, $places); |
||||
} |
||||
|
||||
public static function hex32bit(float $value, string $hexstr, bool $force = false): string |
||||
{ |
||||
if (PHP_INT_SIZE === 4 || $force) { |
||||
if ($value >= 2 ** 32) { |
||||
$quotient = (int) ($value / (2 ** 32)); |
||||
|
||||
return strtoupper(substr('0' . dechex($quotient), -2) . $hexstr); |
||||
} |
||||
if ($value < -(2 ** 32)) { |
||||
$quotient = 256 - (int) ceil((-$value) / (2 ** 32)); |
||||
|
||||
return strtoupper(substr('0' . dechex($quotient), -2) . substr("00000000$hexstr", -8)); |
||||
} |
||||
if ($value < 0) { |
||||
return "FF$hexstr"; |
||||
} |
||||
} |
||||
|
||||
return $hexstr; |
||||
} |
||||
|
||||
/** |
||||
* toOctal. |
||||
* |
||||
* Return an decimal value as octal. |
||||
* |
||||
* Excel Function: |
||||
* DEC2OCT(x[,places]) |
||||
* |
||||
* @param array|string $value The decimal integer you want to convert. If number is negative, |
||||
* places is ignored and DEC2OCT returns a 10-character (30-bit) |
||||
* octal number in which the most significant bit is the sign bit. |
||||
* The remaining 29 bits are magnitude bits. Negative numbers are |
||||
* represented using two's-complement notation. |
||||
* If number < -536,870,912 or if number > 536,870,911, DEC2OCT |
||||
* returns the #NUM! error value. |
||||
* If number is nonnumeric, DEC2OCT returns the #VALUE! error value. |
||||
* If DEC2OCT requires more than places characters, it returns the |
||||
* #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, DEC2OCT uses |
||||
* the minimum number of characters necessary. Places is useful for |
||||
* padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, DEC2OCT returns the #VALUE! error value. |
||||
* If places is zero or negative, DEC2OCT returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toOctal($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateDecimal($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$value = (int) floor((float) $value); |
||||
if ($value > self::LARGEST_OCTAL_IN_DECIMAL || $value < self::SMALLEST_OCTAL_IN_DECIMAL) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
$r = decoct($value); |
||||
$r = substr($r, -10); |
||||
|
||||
return self::nbrConversionFormat($r, $places); |
||||
} |
||||
|
||||
protected static function validateDecimal(string $value): string |
||||
{ |
||||
if (strlen($value) > preg_match_all('/[-0123456789.]/', $value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return $value; |
||||
} |
||||
} |
@ -0,0 +1,175 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ConvertHex extends ConvertBase |
||||
{ |
||||
/** |
||||
* toBinary. |
||||
* |
||||
* Return a hex value as binary. |
||||
* |
||||
* Excel Function: |
||||
* HEX2BIN(x[,places]) |
||||
* |
||||
* @param array|string $value The hexadecimal number you want to convert. |
||||
* Number cannot contain more than 10 characters. |
||||
* The most significant bit of number is the sign bit (40th bit from the right). |
||||
* The remaining 9 bits are magnitude bits. |
||||
* Negative numbers are represented using two's-complement notation. |
||||
* If number is negative, HEX2BIN ignores places and returns a 10-character binary number. |
||||
* If number is negative, it cannot be less than FFFFFFFE00, |
||||
* and if number is positive, it cannot be greater than 1FF. |
||||
* If number is not a valid hexadecimal number, HEX2BIN returns the #NUM! error value. |
||||
* If HEX2BIN requires more than places characters, it returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, |
||||
* HEX2BIN uses the minimum number of characters necessary. Places |
||||
* is useful for padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, HEX2BIN returns the #VALUE! error value. |
||||
* If places is negative, HEX2BIN returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toBinary($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateHex($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$dec = self::toDecimal($value); |
||||
|
||||
return ConvertDecimal::toBinary($dec, $places); |
||||
} |
||||
|
||||
/** |
||||
* toDecimal. |
||||
* |
||||
* Return a hex value as decimal. |
||||
* |
||||
* Excel Function: |
||||
* HEX2DEC(x) |
||||
* |
||||
* @param array|string $value The hexadecimal number you want to convert. This number cannot |
||||
* contain more than 10 characters (40 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 39 bits are magnitude |
||||
* bits. Negative numbers are represented using two's-complement |
||||
* notation. |
||||
* If number is not a valid hexadecimal number, HEX2DEC returns the |
||||
* #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toDecimal($value) |
||||
{ |
||||
if (is_array($value)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateHex($value); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if (strlen($value) > 10) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$binX = ''; |
||||
foreach (str_split($value) as $char) { |
||||
$binX .= str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT); |
||||
} |
||||
if (strlen($binX) == 40 && $binX[0] == '1') { |
||||
for ($i = 0; $i < 40; ++$i) { |
||||
$binX[$i] = ($binX[$i] == '1' ? '0' : '1'); |
||||
} |
||||
|
||||
return (string) ((bindec($binX) + 1) * -1); |
||||
} |
||||
|
||||
return (string) bindec($binX); |
||||
} |
||||
|
||||
/** |
||||
* toOctal. |
||||
* |
||||
* Return a hex value as octal. |
||||
* |
||||
* Excel Function: |
||||
* HEX2OCT(x[,places]) |
||||
* |
||||
* @param array|string $value The hexadecimal number you want to convert. Number cannot |
||||
* contain more than 10 characters. The most significant bit of |
||||
* number is the sign bit. The remaining 39 bits are magnitude |
||||
* bits. Negative numbers are represented using two's-complement |
||||
* notation. |
||||
* If number is negative, HEX2OCT ignores places and returns a |
||||
* 10-character octal number. |
||||
* If number is negative, it cannot be less than FFE0000000, and |
||||
* if number is positive, it cannot be greater than 1FFFFFFF. |
||||
* If number is not a valid hexadecimal number, HEX2OCT returns |
||||
* the #NUM! error value. |
||||
* If HEX2OCT requires more than places characters, it returns |
||||
* the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, HEX2OCT |
||||
* uses the minimum number of characters necessary. Places is |
||||
* useful for padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, HEX2OCT returns the #VALUE! error |
||||
* value. |
||||
* If places is negative, HEX2OCT returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toOctal($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateHex($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$decimal = self::toDecimal($value); |
||||
|
||||
return ConvertDecimal::toOctal($decimal, $places); |
||||
} |
||||
|
||||
protected static function validateHex(string $value): string |
||||
{ |
||||
if (strlen($value) > preg_match_all('/[0123456789ABCDEF]/', $value)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $value; |
||||
} |
||||
} |
@ -0,0 +1,174 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ConvertOctal extends ConvertBase |
||||
{ |
||||
/** |
||||
* toBinary. |
||||
* |
||||
* Return an octal value as binary. |
||||
* |
||||
* Excel Function: |
||||
* OCT2BIN(x[,places]) |
||||
* |
||||
* @param array|string $value The octal number you want to convert. Number may not |
||||
* contain more than 10 characters. The most significant |
||||
* bit of number is the sign bit. The remaining 29 bits |
||||
* are magnitude bits. Negative numbers are represented |
||||
* using two's-complement notation. |
||||
* If number is negative, OCT2BIN ignores places and returns |
||||
* a 10-character binary number. |
||||
* If number is negative, it cannot be less than 7777777000, |
||||
* and if number is positive, it cannot be greater than 777. |
||||
* If number is not a valid octal number, OCT2BIN returns |
||||
* the #NUM! error value. |
||||
* If OCT2BIN requires more than places characters, it |
||||
* returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, |
||||
* OCT2BIN uses the minimum number of characters necessary. |
||||
* Places is useful for padding the return value with |
||||
* leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, OCT2BIN returns the #VALUE! |
||||
* error value. |
||||
* If places is negative, OCT2BIN returns the #NUM! error |
||||
* value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toBinary($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateOctal($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return ConvertDecimal::toBinary(self::toDecimal($value), $places); |
||||
} |
||||
|
||||
/** |
||||
* toDecimal. |
||||
* |
||||
* Return an octal value as decimal. |
||||
* |
||||
* Excel Function: |
||||
* OCT2DEC(x) |
||||
* |
||||
* @param array|string $value The octal number you want to convert. Number may not contain |
||||
* more than 10 octal characters (30 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 29 bits are |
||||
* magnitude bits. Negative numbers are represented using |
||||
* two's-complement notation. |
||||
* If number is not a valid octal number, OCT2DEC returns the |
||||
* #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toDecimal($value) |
||||
{ |
||||
if (is_array($value)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateOctal($value); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$binX = ''; |
||||
foreach (str_split($value) as $char) { |
||||
$binX .= str_pad(decbin((int) $char), 3, '0', STR_PAD_LEFT); |
||||
} |
||||
if (strlen($binX) == 30 && $binX[0] == '1') { |
||||
for ($i = 0; $i < 30; ++$i) { |
||||
$binX[$i] = ($binX[$i] == '1' ? '0' : '1'); |
||||
} |
||||
|
||||
return (string) ((bindec($binX) + 1) * -1); |
||||
} |
||||
|
||||
return (string) bindec($binX); |
||||
} |
||||
|
||||
/** |
||||
* toHex. |
||||
* |
||||
* Return an octal value as hex. |
||||
* |
||||
* Excel Function: |
||||
* OCT2HEX(x[,places]) |
||||
* |
||||
* @param array|string $value The octal number you want to convert. Number may not contain |
||||
* more than 10 octal characters (30 bits). The most significant |
||||
* bit of number is the sign bit. The remaining 29 bits are |
||||
* magnitude bits. Negative numbers are represented using |
||||
* two's-complement notation. |
||||
* If number is negative, OCT2HEX ignores places and returns a |
||||
* 10-character hexadecimal number. |
||||
* If number is not a valid octal number, OCT2HEX returns the |
||||
* #NUM! error value. |
||||
* If OCT2HEX requires more than places characters, it returns |
||||
* the #NUM! error value. |
||||
* Or can be an array of values |
||||
* @param array|int $places The number of characters to use. If places is omitted, OCT2HEX |
||||
* uses the minimum number of characters necessary. Places is useful |
||||
* for padding the return value with leading 0s (zeros). |
||||
* If places is not an integer, it is truncated. |
||||
* If places is nonnumeric, OCT2HEX returns the #VALUE! error value. |
||||
* If places is negative, OCT2HEX returns the #NUM! error value. |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|string Result, or an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function toHex($value, $places = null) |
||||
{ |
||||
if (is_array($value) || is_array($places)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); |
||||
} |
||||
|
||||
try { |
||||
$value = self::validateValue($value); |
||||
$value = self::validateOctal($value); |
||||
$places = self::validatePlaces($places); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$hexVal = strtoupper(dechex((int) self::toDecimal($value))); |
||||
$hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF{$hexVal}" : $hexVal; |
||||
|
||||
return self::nbrConversionFormat($hexVal, $places); |
||||
} |
||||
|
||||
protected static function validateOctal(string $value): string |
||||
{ |
||||
$numDigits = (int) preg_match_all('/[01234567]/', $value); |
||||
if (strlen($value) > $numDigits || $numDigits > 10) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $value; |
||||
} |
||||
} |
@ -0,0 +1,694 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ConvertUOM |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
public const CATEGORY_WEIGHT_AND_MASS = 'Weight and Mass'; |
||||
public const CATEGORY_DISTANCE = 'Distance'; |
||||
public const CATEGORY_TIME = 'Time'; |
||||
public const CATEGORY_PRESSURE = 'Pressure'; |
||||
public const CATEGORY_FORCE = 'Force'; |
||||
public const CATEGORY_ENERGY = 'Energy'; |
||||
public const CATEGORY_POWER = 'Power'; |
||||
public const CATEGORY_MAGNETISM = 'Magnetism'; |
||||
public const CATEGORY_TEMPERATURE = 'Temperature'; |
||||
public const CATEGORY_VOLUME = 'Volume and Liquid Measure'; |
||||
public const CATEGORY_AREA = 'Area'; |
||||
public const CATEGORY_INFORMATION = 'Information'; |
||||
public const CATEGORY_SPEED = 'Speed'; |
||||
|
||||
/** |
||||
* Details of the Units of measure that can be used in CONVERTUOM(). |
||||
* |
||||
* @var mixed[] |
||||
*/ |
||||
private static $conversionUnits = [ |
||||
// Weight and Mass |
||||
'g' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Gram', 'AllowPrefix' => true], |
||||
'sg' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Slug', 'AllowPrefix' => false], |
||||
'lbm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Pound mass (avoirdupois)', 'AllowPrefix' => false], |
||||
'u' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U (atomic mass unit)', 'AllowPrefix' => true], |
||||
'ozm' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ounce mass (avoirdupois)', 'AllowPrefix' => false], |
||||
'grain' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Grain', 'AllowPrefix' => false], |
||||
'cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], |
||||
'shweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'U.S. (short) hundredweight', 'AllowPrefix' => false], |
||||
'uk_cwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], |
||||
'lcwt' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], |
||||
'hweight' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial hundredweight', 'AllowPrefix' => false], |
||||
'stone' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Stone', 'AllowPrefix' => false], |
||||
'ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Ton', 'AllowPrefix' => false], |
||||
'uk_ton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], |
||||
'LTON' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], |
||||
'brton' => ['Group' => self::CATEGORY_WEIGHT_AND_MASS, 'Unit Name' => 'Imperial ton', 'AllowPrefix' => false], |
||||
// Distance |
||||
'm' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Meter', 'AllowPrefix' => true], |
||||
'mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Statute mile', 'AllowPrefix' => false], |
||||
'Nmi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Nautical mile', 'AllowPrefix' => false], |
||||
'in' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Inch', 'AllowPrefix' => false], |
||||
'ft' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Foot', 'AllowPrefix' => false], |
||||
'yd' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Yard', 'AllowPrefix' => false], |
||||
'ang' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Angstrom', 'AllowPrefix' => true], |
||||
'ell' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Ell', 'AllowPrefix' => false], |
||||
'ly' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Light Year', 'AllowPrefix' => false], |
||||
'parsec' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false], |
||||
'pc' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Parsec', 'AllowPrefix' => false], |
||||
'Pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false], |
||||
'Picapt' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/72 in)', 'AllowPrefix' => false], |
||||
'pica' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'Pica (1/6 in)', 'AllowPrefix' => false], |
||||
'survey_mi' => ['Group' => self::CATEGORY_DISTANCE, 'Unit Name' => 'U.S survey mile (statute mile)', 'AllowPrefix' => false], |
||||
// Time |
||||
'yr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Year', 'AllowPrefix' => false], |
||||
'day' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false], |
||||
'd' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Day', 'AllowPrefix' => false], |
||||
'hr' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Hour', 'AllowPrefix' => false], |
||||
'mn' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false], |
||||
'min' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Minute', 'AllowPrefix' => false], |
||||
'sec' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true], |
||||
's' => ['Group' => self::CATEGORY_TIME, 'Unit Name' => 'Second', 'AllowPrefix' => true], |
||||
// Pressure |
||||
'Pa' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true], |
||||
'p' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Pascal', 'AllowPrefix' => true], |
||||
'atm' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], |
||||
'at' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Atmosphere', 'AllowPrefix' => true], |
||||
'mmHg' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'mm of Mercury', 'AllowPrefix' => true], |
||||
'psi' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'PSI', 'AllowPrefix' => true], |
||||
'Torr' => ['Group' => self::CATEGORY_PRESSURE, 'Unit Name' => 'Torr', 'AllowPrefix' => true], |
||||
// Force |
||||
'N' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Newton', 'AllowPrefix' => true], |
||||
'dyn' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true], |
||||
'dy' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Dyne', 'AllowPrefix' => true], |
||||
'lbf' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pound force', 'AllowPrefix' => false], |
||||
'pond' => ['Group' => self::CATEGORY_FORCE, 'Unit Name' => 'Pond', 'AllowPrefix' => true], |
||||
// Energy |
||||
'J' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Joule', 'AllowPrefix' => true], |
||||
'e' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Erg', 'AllowPrefix' => true], |
||||
'c' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Thermodynamic calorie', 'AllowPrefix' => true], |
||||
'cal' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'IT calorie', 'AllowPrefix' => true], |
||||
'eV' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], |
||||
'ev' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Electron volt', 'AllowPrefix' => true], |
||||
'HPh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], |
||||
'hh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Horsepower-hour', 'AllowPrefix' => false], |
||||
'Wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], |
||||
'wh' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Watt-hour', 'AllowPrefix' => true], |
||||
'flb' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'Foot-pound', 'AllowPrefix' => false], |
||||
'BTU' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false], |
||||
'btu' => ['Group' => self::CATEGORY_ENERGY, 'Unit Name' => 'BTU', 'AllowPrefix' => false], |
||||
// Power |
||||
'HP' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], |
||||
'h' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Horsepower', 'AllowPrefix' => false], |
||||
'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], |
||||
'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], |
||||
'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], |
||||
// Magnetism |
||||
'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], |
||||
'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], |
||||
// Temperature |
||||
'C' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false], |
||||
'cel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Celsius', 'AllowPrefix' => false], |
||||
'F' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false], |
||||
'fah' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Fahrenheit', 'AllowPrefix' => false], |
||||
'K' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], |
||||
'kel' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Kelvin', 'AllowPrefix' => false], |
||||
'Rank' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Rankine', 'AllowPrefix' => false], |
||||
'Reau' => ['Group' => self::CATEGORY_TEMPERATURE, 'Unit Name' => 'Degrees Réaumur', 'AllowPrefix' => false], |
||||
// Volume |
||||
'l' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], |
||||
'L' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], |
||||
'lt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Litre', 'AllowPrefix' => true], |
||||
'tsp' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Teaspoon', 'AllowPrefix' => false], |
||||
'tspm' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Modern Teaspoon', 'AllowPrefix' => false], |
||||
'tbs' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Tablespoon', 'AllowPrefix' => false], |
||||
'oz' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Fluid Ounce', 'AllowPrefix' => false], |
||||
'cup' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cup', 'AllowPrefix' => false], |
||||
'pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], |
||||
'us_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.S. Pint', 'AllowPrefix' => false], |
||||
'uk_pt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'U.K. Pint', 'AllowPrefix' => false], |
||||
'qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Quart', 'AllowPrefix' => false], |
||||
'uk_qt' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Quart (UK)', 'AllowPrefix' => false], |
||||
'gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gallon', 'AllowPrefix' => false], |
||||
'uk_gal' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Imperial Gallon (UK)', 'AllowPrefix' => false], |
||||
'ang3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true], |
||||
'ang^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Angstrom', 'AllowPrefix' => true], |
||||
'barrel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Oil Barrel', 'AllowPrefix' => false], |
||||
'bushel' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'US Bushel', 'AllowPrefix' => false], |
||||
'in3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false], |
||||
'in^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Inch', 'AllowPrefix' => false], |
||||
'ft3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false], |
||||
'ft^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Foot', 'AllowPrefix' => false], |
||||
'ly3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false], |
||||
'ly^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Light Year', 'AllowPrefix' => false], |
||||
'm3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true], |
||||
'm^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Meter', 'AllowPrefix' => true], |
||||
'mi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false], |
||||
'mi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Mile', 'AllowPrefix' => false], |
||||
'yd3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false], |
||||
'yd^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Yard', 'AllowPrefix' => false], |
||||
'Nmi3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false], |
||||
'Nmi^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Nautical Mile', 'AllowPrefix' => false], |
||||
'Pica3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], |
||||
'Pica^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], |
||||
'Picapt3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], |
||||
'Picapt^3' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Cubic Pica', 'AllowPrefix' => false], |
||||
'GRT' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false], |
||||
'regton' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Gross Registered Ton', 'AllowPrefix' => false], |
||||
'MTON' => ['Group' => self::CATEGORY_VOLUME, 'Unit Name' => 'Measurement Ton (Freight Ton)', 'AllowPrefix' => false], |
||||
// Area |
||||
'ha' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Hectare', 'AllowPrefix' => true], |
||||
'uk_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'International Acre', 'AllowPrefix' => false], |
||||
'us_acre' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'US Survey/Statute Acre', 'AllowPrefix' => false], |
||||
'ang2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true], |
||||
'ang^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Angstrom', 'AllowPrefix' => true], |
||||
'ar' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Are', 'AllowPrefix' => true], |
||||
'ft2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false], |
||||
'ft^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Feet', 'AllowPrefix' => false], |
||||
'in2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false], |
||||
'in^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Inches', 'AllowPrefix' => false], |
||||
'ly2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false], |
||||
'ly^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Light Years', 'AllowPrefix' => false], |
||||
'm2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true], |
||||
'm^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Meters', 'AllowPrefix' => true], |
||||
'Morgen' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Morgen', 'AllowPrefix' => false], |
||||
'mi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false], |
||||
'mi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Miles', 'AllowPrefix' => false], |
||||
'Nmi2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false], |
||||
'Nmi^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Nautical Miles', 'AllowPrefix' => false], |
||||
'Pica2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], |
||||
'Pica^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], |
||||
'Picapt2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], |
||||
'Picapt^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Pica', 'AllowPrefix' => false], |
||||
'yd2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false], |
||||
'yd^2' => ['Group' => self::CATEGORY_AREA, 'Unit Name' => 'Square Yards', 'AllowPrefix' => false], |
||||
// Information |
||||
'byte' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Byte', 'AllowPrefix' => true], |
||||
'bit' => ['Group' => self::CATEGORY_INFORMATION, 'Unit Name' => 'Bit', 'AllowPrefix' => true], |
||||
// Speed |
||||
'm/s' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true], |
||||
'm/sec' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per second', 'AllowPrefix' => true], |
||||
'm/h' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true], |
||||
'm/hr' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Meters per hour', 'AllowPrefix' => true], |
||||
'mph' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Miles per hour', 'AllowPrefix' => false], |
||||
'admkn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Admiralty Knot', 'AllowPrefix' => false], |
||||
'kn' => ['Group' => self::CATEGORY_SPEED, 'Unit Name' => 'Knot', 'AllowPrefix' => false], |
||||
]; |
||||
|
||||
/** |
||||
* Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). |
||||
* |
||||
* @var mixed[] |
||||
*/ |
||||
private static $conversionMultipliers = [ |
||||
'Y' => ['multiplier' => 1E24, 'name' => 'yotta'], |
||||
'Z' => ['multiplier' => 1E21, 'name' => 'zetta'], |
||||
'E' => ['multiplier' => 1E18, 'name' => 'exa'], |
||||
'P' => ['multiplier' => 1E15, 'name' => 'peta'], |
||||
'T' => ['multiplier' => 1E12, 'name' => 'tera'], |
||||
'G' => ['multiplier' => 1E9, 'name' => 'giga'], |
||||
'M' => ['multiplier' => 1E6, 'name' => 'mega'], |
||||
'k' => ['multiplier' => 1E3, 'name' => 'kilo'], |
||||
'h' => ['multiplier' => 1E2, 'name' => 'hecto'], |
||||
'e' => ['multiplier' => 1E1, 'name' => 'dekao'], |
||||
'da' => ['multiplier' => 1E1, 'name' => 'dekao'], |
||||
'd' => ['multiplier' => 1E-1, 'name' => 'deci'], |
||||
'c' => ['multiplier' => 1E-2, 'name' => 'centi'], |
||||
'm' => ['multiplier' => 1E-3, 'name' => 'milli'], |
||||
'u' => ['multiplier' => 1E-6, 'name' => 'micro'], |
||||
'n' => ['multiplier' => 1E-9, 'name' => 'nano'], |
||||
'p' => ['multiplier' => 1E-12, 'name' => 'pico'], |
||||
'f' => ['multiplier' => 1E-15, 'name' => 'femto'], |
||||
'a' => ['multiplier' => 1E-18, 'name' => 'atto'], |
||||
'z' => ['multiplier' => 1E-21, 'name' => 'zepto'], |
||||
'y' => ['multiplier' => 1E-24, 'name' => 'yocto'], |
||||
]; |
||||
|
||||
/** |
||||
* Details of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). |
||||
* |
||||
* @var mixed[] |
||||
*/ |
||||
private static $binaryConversionMultipliers = [ |
||||
'Yi' => ['multiplier' => 2 ** 80, 'name' => 'yobi'], |
||||
'Zi' => ['multiplier' => 2 ** 70, 'name' => 'zebi'], |
||||
'Ei' => ['multiplier' => 2 ** 60, 'name' => 'exbi'], |
||||
'Pi' => ['multiplier' => 2 ** 50, 'name' => 'pebi'], |
||||
'Ti' => ['multiplier' => 2 ** 40, 'name' => 'tebi'], |
||||
'Gi' => ['multiplier' => 2 ** 30, 'name' => 'gibi'], |
||||
'Mi' => ['multiplier' => 2 ** 20, 'name' => 'mebi'], |
||||
'ki' => ['multiplier' => 2 ** 10, 'name' => 'kibi'], |
||||
]; |
||||
|
||||
/** |
||||
* Details of the Units of measure conversion factors, organised by group. |
||||
* |
||||
* @var mixed[] |
||||
*/ |
||||
private static $unitConversions = [ |
||||
// Conversion uses gram (g) as an intermediate unit |
||||
self::CATEGORY_WEIGHT_AND_MASS => [ |
||||
'g' => 1.0, |
||||
'sg' => 6.85217658567918E-05, |
||||
'lbm' => 2.20462262184878E-03, |
||||
'u' => 6.02214179421676E+23, |
||||
'ozm' => 3.52739619495804E-02, |
||||
'grain' => 1.54323583529414E+01, |
||||
'cwt' => 2.20462262184878E-05, |
||||
'shweight' => 2.20462262184878E-05, |
||||
'uk_cwt' => 1.96841305522212E-05, |
||||
'lcwt' => 1.96841305522212E-05, |
||||
'hweight' => 1.96841305522212E-05, |
||||
'stone' => 1.57473044417770E-04, |
||||
'ton' => 1.10231131092439E-06, |
||||
'uk_ton' => 9.84206527611061E-07, |
||||
'LTON' => 9.84206527611061E-07, |
||||
'brton' => 9.84206527611061E-07, |
||||
], |
||||
// Conversion uses meter (m) as an intermediate unit |
||||
self::CATEGORY_DISTANCE => [ |
||||
'm' => 1.0, |
||||
'mi' => 6.21371192237334E-04, |
||||
'Nmi' => 5.39956803455724E-04, |
||||
'in' => 3.93700787401575E+01, |
||||
'ft' => 3.28083989501312E+00, |
||||
'yd' => 1.09361329833771E+00, |
||||
'ang' => 1.0E+10, |
||||
'ell' => 8.74890638670166E-01, |
||||
'ly' => 1.05700083402462E-16, |
||||
'parsec' => 3.24077928966473E-17, |
||||
'pc' => 3.24077928966473E-17, |
||||
'Pica' => 2.83464566929134E+03, |
||||
'Picapt' => 2.83464566929134E+03, |
||||
'pica' => 2.36220472440945E+02, |
||||
'survey_mi' => 6.21369949494950E-04, |
||||
], |
||||
// Conversion uses second (s) as an intermediate unit |
||||
self::CATEGORY_TIME => [ |
||||
'yr' => 3.16880878140289E-08, |
||||
'day' => 1.15740740740741E-05, |
||||
'd' => 1.15740740740741E-05, |
||||
'hr' => 2.77777777777778E-04, |
||||
'mn' => 1.66666666666667E-02, |
||||
'min' => 1.66666666666667E-02, |
||||
'sec' => 1.0, |
||||
's' => 1.0, |
||||
], |
||||
// Conversion uses Pascal (Pa) as an intermediate unit |
||||
self::CATEGORY_PRESSURE => [ |
||||
'Pa' => 1.0, |
||||
'p' => 1.0, |
||||
'atm' => 9.86923266716013E-06, |
||||
'at' => 9.86923266716013E-06, |
||||
'mmHg' => 7.50063755419211E-03, |
||||
'psi' => 1.45037737730209E-04, |
||||
'Torr' => 7.50061682704170E-03, |
||||
], |
||||
// Conversion uses Newton (N) as an intermediate unit |
||||
self::CATEGORY_FORCE => [ |
||||
'N' => 1.0, |
||||
'dyn' => 1.0E+5, |
||||
'dy' => 1.0E+5, |
||||
'lbf' => 2.24808923655339E-01, |
||||
'pond' => 1.01971621297793E+02, |
||||
], |
||||
// Conversion uses Joule (J) as an intermediate unit |
||||
self::CATEGORY_ENERGY => [ |
||||
'J' => 1.0, |
||||
'e' => 9.99999519343231E+06, |
||||
'c' => 2.39006249473467E-01, |
||||
'cal' => 2.38846190642017E-01, |
||||
'eV' => 6.24145700000000E+18, |
||||
'ev' => 6.24145700000000E+18, |
||||
'HPh' => 3.72506430801000E-07, |
||||
'hh' => 3.72506430801000E-07, |
||||
'Wh' => 2.77777916238711E-04, |
||||
'wh' => 2.77777916238711E-04, |
||||
'flb' => 2.37304222192651E+01, |
||||
'BTU' => 9.47815067349015E-04, |
||||
'btu' => 9.47815067349015E-04, |
||||
], |
||||
// Conversion uses Horsepower (HP) as an intermediate unit |
||||
self::CATEGORY_POWER => [ |
||||
'HP' => 1.0, |
||||
'h' => 1.0, |
||||
'W' => 7.45699871582270E+02, |
||||
'w' => 7.45699871582270E+02, |
||||
'PS' => 1.01386966542400E+00, |
||||
], |
||||
// Conversion uses Tesla (T) as an intermediate unit |
||||
self::CATEGORY_MAGNETISM => [ |
||||
'T' => 1.0, |
||||
'ga' => 10000.0, |
||||
], |
||||
// Conversion uses litre (l) as an intermediate unit |
||||
self::CATEGORY_VOLUME => [ |
||||
'l' => 1.0, |
||||
'L' => 1.0, |
||||
'lt' => 1.0, |
||||
'tsp' => 2.02884136211058E+02, |
||||
'tspm' => 2.0E+02, |
||||
'tbs' => 6.76280454036860E+01, |
||||
'oz' => 3.38140227018430E+01, |
||||
'cup' => 4.22675283773038E+00, |
||||
'pt' => 2.11337641886519E+00, |
||||
'us_pt' => 2.11337641886519E+00, |
||||
'uk_pt' => 1.75975398639270E+00, |
||||
'qt' => 1.05668820943259E+00, |
||||
'uk_qt' => 8.79876993196351E-01, |
||||
'gal' => 2.64172052358148E-01, |
||||
'uk_gal' => 2.19969248299088E-01, |
||||
'ang3' => 1.0E+27, |
||||
'ang^3' => 1.0E+27, |
||||
'barrel' => 6.28981077043211E-03, |
||||
'bushel' => 2.83775932584017E-02, |
||||
'in3' => 6.10237440947323E+01, |
||||
'in^3' => 6.10237440947323E+01, |
||||
'ft3' => 3.53146667214886E-02, |
||||
'ft^3' => 3.53146667214886E-02, |
||||
'ly3' => 1.18093498844171E-51, |
||||
'ly^3' => 1.18093498844171E-51, |
||||
'm3' => 1.0E-03, |
||||
'm^3' => 1.0E-03, |
||||
'mi3' => 2.39912758578928E-13, |
||||
'mi^3' => 2.39912758578928E-13, |
||||
'yd3' => 1.30795061931439E-03, |
||||
'yd^3' => 1.30795061931439E-03, |
||||
'Nmi3' => 1.57426214685811E-13, |
||||
'Nmi^3' => 1.57426214685811E-13, |
||||
'Pica3' => 2.27769904358706E+07, |
||||
'Pica^3' => 2.27769904358706E+07, |
||||
'Picapt3' => 2.27769904358706E+07, |
||||
'Picapt^3' => 2.27769904358706E+07, |
||||
'GRT' => 3.53146667214886E-04, |
||||
'regton' => 3.53146667214886E-04, |
||||
'MTON' => 8.82866668037215E-04, |
||||
], |
||||
// Conversion uses hectare (ha) as an intermediate unit |
||||
self::CATEGORY_AREA => [ |
||||
'ha' => 1.0, |
||||
'uk_acre' => 2.47105381467165E+00, |
||||
'us_acre' => 2.47104393046628E+00, |
||||
'ang2' => 1.0E+24, |
||||
'ang^2' => 1.0E+24, |
||||
'ar' => 1.0E+02, |
||||
'ft2' => 1.07639104167097E+05, |
||||
'ft^2' => 1.07639104167097E+05, |
||||
'in2' => 1.55000310000620E+07, |
||||
'in^2' => 1.55000310000620E+07, |
||||
'ly2' => 1.11725076312873E-28, |
||||
'ly^2' => 1.11725076312873E-28, |
||||
'm2' => 1.0E+04, |
||||
'm^2' => 1.0E+04, |
||||
'Morgen' => 4.0E+00, |
||||
'mi2' => 3.86102158542446E-03, |
||||
'mi^2' => 3.86102158542446E-03, |
||||
'Nmi2' => 2.91553349598123E-03, |
||||
'Nmi^2' => 2.91553349598123E-03, |
||||
'Pica2' => 8.03521607043214E+10, |
||||
'Pica^2' => 8.03521607043214E+10, |
||||
'Picapt2' => 8.03521607043214E+10, |
||||
'Picapt^2' => 8.03521607043214E+10, |
||||
'yd2' => 1.19599004630108E+04, |
||||
'yd^2' => 1.19599004630108E+04, |
||||
], |
||||
// Conversion uses bit (bit) as an intermediate unit |
||||
self::CATEGORY_INFORMATION => [ |
||||
'bit' => 1.0, |
||||
'byte' => 0.125, |
||||
], |
||||
// Conversion uses Meters per Second (m/s) as an intermediate unit |
||||
self::CATEGORY_SPEED => [ |
||||
'm/s' => 1.0, |
||||
'm/sec' => 1.0, |
||||
'm/h' => 3.60E+03, |
||||
'm/hr' => 3.60E+03, |
||||
'mph' => 2.23693629205440E+00, |
||||
'admkn' => 1.94260256941567E+00, |
||||
'kn' => 1.94384449244060E+00, |
||||
], |
||||
]; |
||||
|
||||
/** |
||||
* getConversionGroups |
||||
* Returns a list of the different conversion groups for UOM conversions. |
||||
* |
||||
* @return array |
||||
*/ |
||||
public static function getConversionCategories() |
||||
{ |
||||
$conversionGroups = []; |
||||
foreach (self::$conversionUnits as $conversionUnit) { |
||||
$conversionGroups[] = $conversionUnit['Group']; |
||||
} |
||||
|
||||
return array_merge(array_unique($conversionGroups)); |
||||
} |
||||
|
||||
/** |
||||
* getConversionGroupUnits |
||||
* Returns an array of units of measure, for a specified conversion group, or for all groups. |
||||
* |
||||
* @param string $category The group whose units of measure you want to retrieve |
||||
* |
||||
* @return array |
||||
*/ |
||||
public static function getConversionCategoryUnits($category = null) |
||||
{ |
||||
$conversionGroups = []; |
||||
foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { |
||||
if (($category === null) || ($conversionGroup['Group'] == $category)) { |
||||
$conversionGroups[$conversionGroup['Group']][] = $conversionUnit; |
||||
} |
||||
} |
||||
|
||||
return $conversionGroups; |
||||
} |
||||
|
||||
/** |
||||
* getConversionGroupUnitDetails. |
||||
* |
||||
* @param string $category The group whose units of measure you want to retrieve |
||||
* |
||||
* @return array |
||||
*/ |
||||
public static function getConversionCategoryUnitDetails($category = null) |
||||
{ |
||||
$conversionGroups = []; |
||||
foreach (self::$conversionUnits as $conversionUnit => $conversionGroup) { |
||||
if (($category === null) || ($conversionGroup['Group'] == $category)) { |
||||
$conversionGroups[$conversionGroup['Group']][] = [ |
||||
'unit' => $conversionUnit, |
||||
'description' => $conversionGroup['Unit Name'], |
||||
]; |
||||
} |
||||
} |
||||
|
||||
return $conversionGroups; |
||||
} |
||||
|
||||
/** |
||||
* getConversionMultipliers |
||||
* Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM(). |
||||
* |
||||
* @return mixed[] |
||||
*/ |
||||
public static function getConversionMultipliers() |
||||
{ |
||||
return self::$conversionMultipliers; |
||||
} |
||||
|
||||
/** |
||||
* getBinaryConversionMultipliers |
||||
* Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM(). |
||||
* |
||||
* @return mixed[] |
||||
*/ |
||||
public static function getBinaryConversionMultipliers() |
||||
{ |
||||
return self::$binaryConversionMultipliers; |
||||
} |
||||
|
||||
/** |
||||
* CONVERT. |
||||
* |
||||
* Converts a number from one measurement system to another. |
||||
* For example, CONVERT can translate a table of distances in miles to a table of distances |
||||
* in kilometers. |
||||
* |
||||
* Excel Function: |
||||
* CONVERT(value,fromUOM,toUOM) |
||||
* |
||||
* @param array|float|int|string $value the value in fromUOM to convert |
||||
* Or can be an array of values |
||||
* @param array|string $fromUOM the units for value |
||||
* Or can be an array of values |
||||
* @param array|string $toUOM the units for the result |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string Result, or a string containing an error |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function CONVERT($value, $fromUOM, $toUOM) |
||||
{ |
||||
if (is_array($value) || is_array($fromUOM) || is_array($toUOM)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $fromUOM, $toUOM); |
||||
} |
||||
|
||||
if (!is_numeric($value)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
try { |
||||
[$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM); |
||||
[$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM); |
||||
} catch (Exception $e) { |
||||
return ExcelError::NA(); |
||||
} |
||||
|
||||
if ($fromCategory !== $toCategory) { |
||||
return ExcelError::NA(); |
||||
} |
||||
|
||||
// @var float $value |
||||
$value *= $fromMultiplier; |
||||
|
||||
if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) { |
||||
// We've already factored $fromMultiplier into the value, so we need |
||||
// to reverse it again |
||||
return $value / $fromMultiplier; |
||||
} elseif ($fromUOM === $toUOM) { |
||||
return $value / $toMultiplier; |
||||
} elseif ($fromCategory === self::CATEGORY_TEMPERATURE) { |
||||
return self::convertTemperature($fromUOM, $toUOM, /** @scrutinizer ignore-type */ $value); |
||||
} |
||||
|
||||
$baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]); |
||||
|
||||
return ($baseValue * self::$unitConversions[$fromCategory][$toUOM]) / $toMultiplier; |
||||
} |
||||
|
||||
private static function getUOMDetails(string $uom): 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'); |
||||
} |
||||
|
||||
/** |
||||
* @param float|int $value |
||||
* |
||||
* @return float|int |
||||
*/ |
||||
protected static function convertTemperature(string $fromUOM, string $toUOM, $value) |
||||
{ |
||||
$fromUOM = self::resolveTemperatureSynonyms($fromUOM); |
||||
$toUOM = self::resolveTemperatureSynonyms($toUOM); |
||||
|
||||
if ($fromUOM === $toUOM) { |
||||
return $value; |
||||
} |
||||
|
||||
// Convert to Kelvin |
||||
switch ($fromUOM) { |
||||
case 'F': |
||||
$value = ($value - 32) / 1.8 + 273.15; |
||||
|
||||
break; |
||||
case 'C': |
||||
$value += 273.15; |
||||
|
||||
break; |
||||
case 'Rank': |
||||
$value /= 1.8; |
||||
|
||||
break; |
||||
case 'Reau': |
||||
$value = $value * 1.25 + 273.15; |
||||
|
||||
break; |
||||
} |
||||
|
||||
// Convert from Kelvin |
||||
switch ($toUOM) { |
||||
case 'F': |
||||
$value = ($value - 273.15) * 1.8 + 32.00; |
||||
|
||||
break; |
||||
case 'C': |
||||
$value -= 273.15; |
||||
|
||||
break; |
||||
case 'Rank': |
||||
$value *= 1.8; |
||||
|
||||
break; |
||||
case 'Reau': |
||||
$value = ($value - 273.15) * 0.80000; |
||||
|
||||
break; |
||||
} |
||||
|
||||
return $value; |
||||
} |
||||
|
||||
private static function resolveTemperatureSynonyms(string $uom): string |
||||
{ |
||||
switch ($uom) { |
||||
case 'fah': |
||||
return 'F'; |
||||
case 'cel': |
||||
return 'C'; |
||||
case 'kel': |
||||
return 'K'; |
||||
} |
||||
|
||||
return $uom; |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class EngineeringValidations |
||||
{ |
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
public static function validateFloat($value): float |
||||
{ |
||||
if (!is_numeric($value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (float) $value; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
public static function validateInt($value): int |
||||
{ |
||||
if (!is_numeric($value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (int) floor((float) $value); |
||||
} |
||||
} |
@ -0,0 +1,110 @@
|
||||
<?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($lower, $upper = null) |
||||
{ |
||||
if (is_array($lower) || is_array($upper)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $lower, $upper); |
||||
} |
||||
|
||||
if (is_numeric($lower)) { |
||||
if ($upper === null) { |
||||
return self::erfValue($lower); |
||||
} |
||||
if (is_numeric($upper)) { |
||||
return self::erfValue($upper) - self::erfValue($lower); |
||||
} |
||||
} |
||||
|
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
/** |
||||
* ERFPRECISE. |
||||
* |
||||
* Returns the error function integrated between the lower and upper bound arguments. |
||||
* |
||||
* Excel Function: |
||||
* ERF.PRECISE(limit) |
||||
* |
||||
* @param mixed $limit Float bound for integrating ERF, other bound is zero |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function ERFPRECISE($limit) |
||||
{ |
||||
if (is_array($limit)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $limit); |
||||
} |
||||
|
||||
return self::ERF($limit); |
||||
} |
||||
|
||||
/** |
||||
* Method to calculate the erf value. |
||||
* |
||||
* @param float|int|string $value |
||||
* |
||||
* @return float |
||||
*/ |
||||
public static function erfValue($value) |
||||
{ |
||||
$value = (float) $value; |
||||
if (abs($value) > 2.2) { |
||||
return 1 - ErfC::ERFC($value); |
||||
} |
||||
$sum = $term = $value; |
||||
$xsqr = ($value * $value); |
||||
$j = 1; |
||||
do { |
||||
$term *= $xsqr / $j; |
||||
$sum -= $term / (2 * $j + 1); |
||||
++$j; |
||||
$term *= $xsqr / $j; |
||||
$sum += $term / (2 * $j + 1); |
||||
++$j; |
||||
if ($sum == 0.0) { |
||||
break; |
||||
} |
||||
} while (abs($term / $sum) > Functions::PRECISION); |
||||
|
||||
return self::TWO_SQRT_PI * $sum; |
||||
} |
||||
} |
@ -0,0 +1,82 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class ErfC |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* ERFC. |
||||
* |
||||
* Returns the complementary ERF function integrated between x and infinity |
||||
* |
||||
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument, |
||||
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was |
||||
* improved, so that it can now calculate the function for both positive and negative x values. |
||||
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments. |
||||
* |
||||
* Excel Function: |
||||
* ERFC(x) |
||||
* |
||||
* @param mixed $value The float lower bound for integrating ERFC |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
* with the same dimensions |
||||
*/ |
||||
public static function ERFC($value) |
||||
{ |
||||
if (is_array($value)) { |
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
} |
||||
|
||||
if (is_numeric($value)) { |
||||
return self::erfcValue($value); |
||||
} |
||||
|
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
private const ONE_SQRT_PI = 0.564189583547756287; |
||||
|
||||
/** |
||||
* Method to calculate the erfc value. |
||||
* |
||||
* @param float|int|string $value |
||||
* |
||||
* @return float |
||||
*/ |
||||
private static function erfcValue($value) |
||||
{ |
||||
$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; |
||||
} |
||||
} |
@ -0,0 +1,28 @@
|
||||
<?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. |
||||
* |
||||
* @param mixed $code |
||||
* @param mixed $string |
||||
* @param mixed $file |
||||
* @param mixed $line |
||||
* @param mixed $context |
||||
*/ |
||||
public static function errorHandlerCallback($code, $string, $file, $line, /** @scrutinizer ignore-unused */ $context): void |
||||
{ |
||||
$e = new self($string, $code); |
||||
$e->line = $line; |
||||
$e->file = $file; |
||||
|
||||
throw $e; |
||||
} |
||||
} |
@ -0,0 +1,24 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
||||
class ExceptionHandler |
||||
{ |
||||
/** |
||||
* Register errorhandler. |
||||
*/ |
||||
public function __construct() |
||||
{ |
||||
/** @var callable */ |
||||
$callable = [Exception::class, 'errorHandlerCallback']; |
||||
set_error_handler($callable, E_ALL); |
||||
} |
||||
|
||||
/** |
||||
* Unregister errorhandler. |
||||
*/ |
||||
public function __destruct() |
||||
{ |
||||
restore_error_handler(); |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,215 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
||||
class Amortization |
||||
{ |
||||
/** |
||||
* AMORDEGRC. |
||||
* |
||||
* Returns the depreciation for each accounting period. |
||||
* This function is provided for the French accounting system. If an asset is purchased in |
||||
* the middle of the accounting period, the prorated depreciation is taken into account. |
||||
* The function is similar to AMORLINC, except that a depreciation coefficient is applied in |
||||
* the calculation depending on the life of the assets. |
||||
* This function will return the depreciation until the last period of the life of the assets |
||||
* or until the cumulated value of depreciation is greater than the cost of the assets minus |
||||
* the salvage value. |
||||
* |
||||
* Excel Function: |
||||
* AMORDEGRC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) |
||||
* |
||||
* @param mixed $cost The float cost of the asset |
||||
* @param mixed $purchased Date of the purchase of the asset |
||||
* @param mixed $firstPeriod Date of the end of the first period |
||||
* @param mixed $salvage The salvage value at the end of the life of the asset |
||||
* @param mixed $period the period (float) |
||||
* @param mixed $rate rate of depreciation (float) |
||||
* @param mixed $basis The type of day count to use (int). |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string (string containing the error type if there is an error) |
||||
*/ |
||||
public static function AMORDEGRC( |
||||
$cost, |
||||
$purchased, |
||||
$firstPeriod, |
||||
$salvage, |
||||
$period, |
||||
$rate, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$purchased = Functions::flattenSingleValue($purchased); |
||||
$firstPeriod = Functions::flattenSingleValue($firstPeriod); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$cost = FinancialValidations::validateFloat($cost); |
||||
$purchased = FinancialValidations::validateDate($purchased); |
||||
$firstPeriod = FinancialValidations::validateDate($firstPeriod); |
||||
$salvage = FinancialValidations::validateFloat($salvage); |
||||
$period = FinancialValidations::validateInt($period); |
||||
$rate = FinancialValidations::validateFloat($rate); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); |
||||
if (is_string($yearFracx)) { |
||||
return $yearFracx; |
||||
} |
||||
/** @var float */ |
||||
$yearFrac = $yearFracx; |
||||
|
||||
$amortiseCoeff = self::getAmortizationCoefficient($rate); |
||||
|
||||
$rate *= $amortiseCoeff; |
||||
$fNRate = round($yearFrac * $rate * $cost, 0); |
||||
$cost -= $fNRate; |
||||
$fRest = $cost - $salvage; |
||||
|
||||
for ($n = 0; $n < $period; ++$n) { |
||||
$fNRate = round($rate * $cost, 0); |
||||
$fRest -= $fNRate; |
||||
|
||||
if ($fRest < 0.0) { |
||||
switch ($period - $n) { |
||||
case 0: |
||||
case 1: |
||||
return round($cost * 0.5, 0); |
||||
default: |
||||
return 0.0; |
||||
} |
||||
} |
||||
$cost -= $fNRate; |
||||
} |
||||
|
||||
return $fNRate; |
||||
} |
||||
|
||||
/** |
||||
* AMORLINC. |
||||
* |
||||
* Returns the depreciation for each accounting period. |
||||
* This function is provided for the French accounting system. If an asset is purchased in |
||||
* the middle of the accounting period, the prorated depreciation is taken into account. |
||||
* |
||||
* Excel Function: |
||||
* AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) |
||||
* |
||||
* @param mixed $cost The cost of the asset as a float |
||||
* @param mixed $purchased Date of the purchase of the asset |
||||
* @param mixed $firstPeriod Date of the end of the first period |
||||
* @param mixed $salvage The salvage value at the end of the life of the asset |
||||
* @param mixed $period The period as a float |
||||
* @param mixed $rate Rate of depreciation as float |
||||
* @param mixed $basis Integer indicating the type of day count to use. |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string (string containing the error type if there is an error) |
||||
*/ |
||||
public static function AMORLINC( |
||||
$cost, |
||||
$purchased, |
||||
$firstPeriod, |
||||
$salvage, |
||||
$period, |
||||
$rate, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$purchased = Functions::flattenSingleValue($purchased); |
||||
$firstPeriod = Functions::flattenSingleValue($firstPeriod); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$cost = FinancialValidations::validateFloat($cost); |
||||
$purchased = FinancialValidations::validateDate($purchased); |
||||
$firstPeriod = FinancialValidations::validateDate($firstPeriod); |
||||
$salvage = FinancialValidations::validateFloat($salvage); |
||||
$period = FinancialValidations::validateFloat($period); |
||||
$rate = FinancialValidations::validateFloat($rate); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$fOneRate = $cost * $rate; |
||||
$fCostDelta = $cost - $salvage; |
||||
// Note, quirky variation for leap years on the YEARFRAC for this function |
||||
$purchasedYear = DateTimeExcel\DateParts::year($purchased); |
||||
$yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); |
||||
if (is_string($yearFracx)) { |
||||
return $yearFracx; |
||||
} |
||||
/** @var float */ |
||||
$yearFrac = $yearFracx; |
||||
|
||||
if ( |
||||
$basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL |
||||
&& $yearFrac < 1 |
||||
&& 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; |
||||
} |
||||
} |
@ -0,0 +1,53 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class CashFlowValidations extends FinancialValidations |
||||
{ |
||||
/** |
||||
* @param mixed $rate |
||||
*/ |
||||
public static function validateRate($rate): float |
||||
{ |
||||
$rate = self::validateFloat($rate); |
||||
|
||||
return $rate; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $type |
||||
*/ |
||||
public static function validatePeriodType($type): int |
||||
{ |
||||
$rate = self::validateInt($type); |
||||
if ( |
||||
$type !== FinancialConstants::PAYMENT_END_OF_PERIOD && |
||||
$type !== FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD |
||||
) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $rate; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $presentValue |
||||
*/ |
||||
public static function validatePresentValue($presentValue): float |
||||
{ |
||||
return self::validateFloat($presentValue); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $futureValue |
||||
*/ |
||||
public static function validateFutureValue($futureValue): float |
||||
{ |
||||
return self::validateFloat($futureValue); |
||||
} |
||||
} |
@ -0,0 +1,200 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Periodic |
||||
{ |
||||
/** |
||||
* FV. |
||||
* |
||||
* Returns the Future Value of a cash flow with constant payments and interest rate (annuities). |
||||
* |
||||
* Excel Function: |
||||
* FV(rate,nper,pmt[,pv[,type]]) |
||||
* |
||||
* @param mixed $rate The interest rate per period |
||||
* @param mixed $numberOfPeriods Total number of payment periods in an annuity as an integer |
||||
* @param mixed $payment The payment made each period: it cannot change over the |
||||
* life of the annuity. Typically, pmt contains principal |
||||
* and interest but no other fees or taxes. |
||||
* @param mixed $presentValue present Value, or the lump-sum amount that a series of |
||||
* future payments is worth right now |
||||
* @param mixed $type A number 0 or 1 and indicates when payments are due: |
||||
* 0 or omitted At the end of the period. |
||||
* 1 At the beginning of the period. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function futureValue( |
||||
$rate, |
||||
$numberOfPeriods, |
||||
$payment = 0.0, |
||||
$presentValue = 0.0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment); |
||||
$presentValue = ($presentValue === null) ? 0.0 : Functions::flattenSingleValue($presentValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$payment = CashFlowValidations::validateFloat($payment); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return self::calculateFutureValue($rate, $numberOfPeriods, $payment, $presentValue, $type); |
||||
} |
||||
|
||||
/** |
||||
* PV. |
||||
* |
||||
* Returns the Present Value of a cash flow with constant payments and interest rate (annuities). |
||||
* |
||||
* @param mixed $rate Interest rate per period |
||||
* @param mixed $numberOfPeriods Number of periods as an integer |
||||
* @param mixed $payment Periodic payment (annuity) |
||||
* @param mixed $futureValue Future Value |
||||
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function presentValue( |
||||
$rate, |
||||
$numberOfPeriods, |
||||
$payment = 0.0, |
||||
$futureValue = 0.0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$payment = CashFlowValidations::validateFloat($payment); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($numberOfPeriods < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type); |
||||
} |
||||
|
||||
/** |
||||
* NPER. |
||||
* |
||||
* Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. |
||||
* |
||||
* @param mixed $rate Interest rate per period |
||||
* @param mixed $payment Periodic payment (annuity) |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $futureValue Future Value |
||||
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function periods( |
||||
$rate, |
||||
$payment, |
||||
$presentValue, |
||||
$futureValue = 0.0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$payment = Functions::flattenSingleValue($payment); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$payment = CashFlowValidations::validateFloat($payment); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($payment == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type); |
||||
} |
||||
|
||||
private static function calculateFutureValue( |
||||
float $rate, |
||||
int $numberOfPeriods, |
||||
float $payment, |
||||
float $presentValue, |
||||
int $type |
||||
): float { |
||||
if ($rate !== null && $rate != 0) { |
||||
return -$presentValue * |
||||
(1 + $rate) ** $numberOfPeriods - $payment * (1 + $rate * $type) * ((1 + $rate) ** $numberOfPeriods - 1) |
||||
/ $rate; |
||||
} |
||||
|
||||
return -$presentValue - $payment * $numberOfPeriods; |
||||
} |
||||
|
||||
private static function calculatePresentValue( |
||||
float $rate, |
||||
int $numberOfPeriods, |
||||
float $payment, |
||||
float $futureValue, |
||||
int $type |
||||
): float { |
||||
if ($rate != 0.0) { |
||||
return (-$payment * (1 + $rate * $type) |
||||
* (((1 + $rate) ** $numberOfPeriods - 1) / $rate) - $futureValue) / (1 + $rate) ** $numberOfPeriods; |
||||
} |
||||
|
||||
return -$futureValue - $payment * $numberOfPeriods; |
||||
} |
||||
|
||||
/** |
||||
* @return float|string |
||||
*/ |
||||
private static function calculatePeriods( |
||||
float $rate, |
||||
float $payment, |
||||
float $presentValue, |
||||
float $futureValue, |
||||
int $type |
||||
) { |
||||
if ($rate != 0.0) { |
||||
if ($presentValue == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return log(($payment * (1 + $rate * $type) / $rate - $futureValue) / |
||||
($presentValue + $payment * (1 + $rate * $type) / $rate)) / log(1 + $rate); |
||||
} |
||||
|
||||
return (-$presentValue - $futureValue) / $payment; |
||||
} |
||||
} |
@ -0,0 +1,142 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Cumulative |
||||
{ |
||||
/** |
||||
* CUMIPMT. |
||||
* |
||||
* Returns the cumulative interest paid on a loan between the start and end periods. |
||||
* |
||||
* Excel Function: |
||||
* CUMIPMT(rate,nper,pv,start,end[,type]) |
||||
* |
||||
* @param mixed $rate The Interest rate |
||||
* @param mixed $periods The total number of payment periods |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $start The first period in the calculation. |
||||
* Payment periods are numbered beginning with 1. |
||||
* @param mixed $end the last period in the calculation |
||||
* @param mixed $type A number 0 or 1 and indicates when payments are due: |
||||
* 0 or omitted At the end of the period. |
||||
* 1 At the beginning of the period. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function interest( |
||||
$rate, |
||||
$periods, |
||||
$presentValue, |
||||
$start, |
||||
$end, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$periods = Functions::flattenSingleValue($periods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$start = Functions::flattenSingleValue($start); |
||||
$end = Functions::flattenSingleValue($end); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$periods = CashFlowValidations::validateInt($periods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$start = CashFlowValidations::validateInt($start); |
||||
$end = CashFlowValidations::validateInt($end); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($start < 1 || $start > $end) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Calculate |
||||
$interest = 0; |
||||
for ($per = $start; $per <= $end; ++$per) { |
||||
$ipmt = Interest::payment($rate, $per, $periods, $presentValue, 0, $type); |
||||
if (is_string($ipmt)) { |
||||
return $ipmt; |
||||
} |
||||
|
||||
$interest += $ipmt; |
||||
} |
||||
|
||||
return $interest; |
||||
} |
||||
|
||||
/** |
||||
* CUMPRINC. |
||||
* |
||||
* Returns the cumulative principal paid on a loan between the start and end periods. |
||||
* |
||||
* Excel Function: |
||||
* CUMPRINC(rate,nper,pv,start,end[,type]) |
||||
* |
||||
* @param mixed $rate The Interest rate |
||||
* @param mixed $periods The total number of payment periods as an integer |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $start The first period in the calculation. |
||||
* Payment periods are numbered beginning with 1. |
||||
* @param mixed $end the last period in the calculation |
||||
* @param mixed $type A number 0 or 1 and indicates when payments are due: |
||||
* 0 or omitted At the end of the period. |
||||
* 1 At the beginning of the period. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function principal( |
||||
$rate, |
||||
$periods, |
||||
$presentValue, |
||||
$start, |
||||
$end, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$periods = Functions::flattenSingleValue($periods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$start = Functions::flattenSingleValue($start); |
||||
$end = Functions::flattenSingleValue($end); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$periods = CashFlowValidations::validateInt($periods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$start = CashFlowValidations::validateInt($start); |
||||
$end = CashFlowValidations::validateInt($end); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($start < 1 || $start > $end) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
// Calculate |
||||
$principal = 0; |
||||
for ($per = $start; $per <= $end; ++$per) { |
||||
$ppmt = Payments::interestPayment($rate, $per, $periods, $presentValue, 0, $type); |
||||
if (is_string($ppmt)) { |
||||
return $ppmt; |
||||
} |
||||
|
||||
$principal += $ppmt; |
||||
} |
||||
|
||||
return $principal; |
||||
} |
||||
} |
@ -0,0 +1,220 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Interest |
||||
{ |
||||
private const FINANCIAL_MAX_ITERATIONS = 128; |
||||
|
||||
private const FINANCIAL_PRECISION = 1.0e-08; |
||||
|
||||
/** |
||||
* IPMT. |
||||
* |
||||
* Returns the interest payment for a given period for an investment based on periodic, constant payments |
||||
* and a constant interest rate. |
||||
* |
||||
* Excel Function: |
||||
* IPMT(rate,per,nper,pv[,fv][,type]) |
||||
* |
||||
* @param mixed $interestRate Interest rate per period |
||||
* @param mixed $period Period for which we want to find the interest |
||||
* @param mixed $numberOfPeriods Number of periods |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $futureValue Future Value |
||||
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function payment( |
||||
$interestRate, |
||||
$period, |
||||
$numberOfPeriods, |
||||
$presentValue, |
||||
$futureValue = 0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$interestRate = Functions::flattenSingleValue($interestRate); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$interestRate = CashFlowValidations::validateRate($interestRate); |
||||
$period = CashFlowValidations::validateInt($period); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($period <= 0 || $period > $numberOfPeriods) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Calculate |
||||
$interestAndPrincipal = new InterestAndPrincipal( |
||||
$interestRate, |
||||
$period, |
||||
$numberOfPeriods, |
||||
$presentValue, |
||||
$futureValue, |
||||
$type |
||||
); |
||||
|
||||
return $interestAndPrincipal->interest(); |
||||
} |
||||
|
||||
/** |
||||
* ISPMT. |
||||
* |
||||
* Returns the interest payment for an investment based on an interest rate and a constant payment schedule. |
||||
* |
||||
* Excel Function: |
||||
* =ISPMT(interest_rate, period, number_payments, pv) |
||||
* |
||||
* @param mixed $interestRate is the interest rate for the investment |
||||
* @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments. |
||||
* @param mixed $numberOfPeriods is the number of payments for the annuity |
||||
* @param mixed $principleRemaining is the loan amount or present value of the payments |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function schedulePayment($interestRate, $period, $numberOfPeriods, $principleRemaining) |
||||
{ |
||||
$interestRate = Functions::flattenSingleValue($interestRate); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$principleRemaining = Functions::flattenSingleValue($principleRemaining); |
||||
|
||||
try { |
||||
$interestRate = CashFlowValidations::validateRate($interestRate); |
||||
$period = CashFlowValidations::validateInt($period); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$principleRemaining = CashFlowValidations::validateFloat($principleRemaining); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($period <= 0 || $period > $numberOfPeriods) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Return value |
||||
$returnValue = 0; |
||||
|
||||
// Calculate |
||||
$principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0); |
||||
for ($i = 0; $i <= $period; ++$i) { |
||||
$returnValue = $interestRate * $principleRemaining * -1; |
||||
$principleRemaining -= $principlePayment; |
||||
// principle needs to be 0 after the last payment, don't let floating point screw it up |
||||
if ($i == $numberOfPeriods) { |
||||
$returnValue = 0.0; |
||||
} |
||||
} |
||||
|
||||
return $returnValue; |
||||
} |
||||
|
||||
/** |
||||
* RATE. |
||||
* |
||||
* Returns the interest rate per period of an annuity. |
||||
* RATE is calculated by iteration and can have zero or more solutions. |
||||
* If the successive results of RATE do not converge to within 0.0000001 after 20 iterations, |
||||
* RATE returns the #NUM! error value. |
||||
* |
||||
* Excel Function: |
||||
* RATE(nper,pmt,pv[,fv[,type[,guess]]]) |
||||
* |
||||
* @param mixed $numberOfPeriods The total number of payment periods in an annuity |
||||
* @param mixed $payment The payment made each period and cannot change over the life of the annuity. |
||||
* Typically, pmt includes principal and interest but no other fees or taxes. |
||||
* @param mixed $presentValue The present value - the total amount that a series of future payments is worth now |
||||
* @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made. |
||||
* If fv is omitted, it is assumed to be 0 (the future value of a loan, |
||||
* for example, is 0). |
||||
* @param mixed $type A number 0 or 1 and indicates when payments are due: |
||||
* 0 or omitted At the end of the period. |
||||
* 1 At the beginning of the period. |
||||
* @param mixed $guess Your guess for what the rate will be. |
||||
* If you omit guess, it is assumed to be 10 percent. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function rate( |
||||
$numberOfPeriods, |
||||
$payment, |
||||
$presentValue, |
||||
$futureValue = 0.0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD, |
||||
$guess = 0.1 |
||||
) { |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$payment = Functions::flattenSingleValue($payment); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
$guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); |
||||
|
||||
try { |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$payment = CashFlowValidations::validateFloat($payment); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
$guess = CashFlowValidations::validateFloat($guess); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$rate = $guess; |
||||
// rest of code adapted from python/numpy |
||||
$close = false; |
||||
$iter = 0; |
||||
while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { |
||||
$nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type); |
||||
if (!is_numeric($nextdiff)) { |
||||
break; |
||||
} |
||||
$rate1 = $rate - $nextdiff; |
||||
$close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; |
||||
++$iter; |
||||
$rate = $rate1; |
||||
} |
||||
|
||||
return $close ? $rate : ExcelError::NAN(); |
||||
} |
||||
|
||||
/** @return float|string */ |
||||
private static function rateNextGuess(float $rate, int $numberOfPeriods, float $payment, float $presentValue, float $futureValue, int $type) |
||||
{ |
||||
if ($rate == 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
$tt1 = ($rate + 1) ** $numberOfPeriods; |
||||
$tt2 = ($rate + 1) ** ($numberOfPeriods - 1); |
||||
$numerator = $futureValue + $tt1 * $presentValue + $payment * ($tt1 - 1) * ($rate * $type + 1) / $rate; |
||||
$denominator = $numberOfPeriods * $tt2 * $presentValue - $payment * ($tt1 - 1) |
||||
* ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods |
||||
* $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate; |
||||
if ($denominator == 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return $numerator / $denominator; |
||||
} |
||||
} |
@ -0,0 +1,46 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
|
||||
class InterestAndPrincipal |
||||
{ |
||||
/** @var float */ |
||||
protected $interest; |
||||
|
||||
/** @var float */ |
||||
protected $principal; |
||||
|
||||
public function __construct( |
||||
float $rate = 0.0, |
||||
int $period = 0, |
||||
int $numberOfPeriods = 0, |
||||
float $presentValue = 0, |
||||
float $futureValue = 0, |
||||
int $type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$payment = Payments::annuity($rate, $numberOfPeriods, $presentValue, $futureValue, $type); |
||||
$capital = $presentValue; |
||||
$interest = 0.0; |
||||
$principal = 0.0; |
||||
for ($i = 1; $i <= $period; ++$i) { |
||||
$interest = ($type === FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD && $i == 1) ? 0 : -$capital * $rate; |
||||
$principal = (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; |
||||
} |
||||
} |
@ -0,0 +1,116 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Payments |
||||
{ |
||||
/** |
||||
* PMT. |
||||
* |
||||
* Returns the constant payment (annuity) for a cash flow with a constant interest rate. |
||||
* |
||||
* @param mixed $interestRate Interest rate per period |
||||
* @param mixed $numberOfPeriods Number of periods |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $futureValue Future Value |
||||
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function annuity( |
||||
$interestRate, |
||||
$numberOfPeriods, |
||||
$presentValue, |
||||
$futureValue = 0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$interestRate = Functions::flattenSingleValue($interestRate); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$interestRate = CashFlowValidations::validateRate($interestRate); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Calculate |
||||
if ($interestRate != 0.0) { |
||||
return (-$futureValue - $presentValue * (1 + $interestRate) ** $numberOfPeriods) / |
||||
(1 + $interestRate * $type) / (((1 + $interestRate) ** $numberOfPeriods - 1) / $interestRate); |
||||
} |
||||
|
||||
return (-$presentValue - $futureValue) / $numberOfPeriods; |
||||
} |
||||
|
||||
/** |
||||
* PPMT. |
||||
* |
||||
* Returns the interest payment for a given period for an investment based on periodic, constant payments |
||||
* and a constant interest rate. |
||||
* |
||||
* @param mixed $interestRate Interest rate per period |
||||
* @param mixed $period Period for which we want to find the interest |
||||
* @param mixed $numberOfPeriods Number of periods |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $futureValue Future Value |
||||
* @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function interestPayment( |
||||
$interestRate, |
||||
$period, |
||||
$numberOfPeriods, |
||||
$presentValue, |
||||
$futureValue = 0, |
||||
$type = FinancialConstants::PAYMENT_END_OF_PERIOD |
||||
) { |
||||
$interestRate = Functions::flattenSingleValue($interestRate); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); |
||||
$type = ($type === null) ? FinancialConstants::PAYMENT_END_OF_PERIOD : Functions::flattenSingleValue($type); |
||||
|
||||
try { |
||||
$interestRate = CashFlowValidations::validateRate($interestRate); |
||||
$period = CashFlowValidations::validateInt($period); |
||||
$numberOfPeriods = CashFlowValidations::validateInt($numberOfPeriods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
$type = CashFlowValidations::validatePeriodType($type); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($period <= 0 || $period > $numberOfPeriods) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Calculate |
||||
$interestAndPrincipal = new InterestAndPrincipal( |
||||
$interestRate, |
||||
$period, |
||||
$numberOfPeriods, |
||||
$presentValue, |
||||
$futureValue, |
||||
$type |
||||
); |
||||
|
||||
return $interestAndPrincipal->principal(); |
||||
} |
||||
} |
@ -0,0 +1,109 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Single |
||||
{ |
||||
/** |
||||
* FVSCHEDULE. |
||||
* |
||||
* Returns the future value of an initial principal after applying a series of compound interest rates. |
||||
* Use FVSCHEDULE to calculate the future value of an investment with a variable or adjustable rate. |
||||
* |
||||
* Excel Function: |
||||
* FVSCHEDULE(principal,schedule) |
||||
* |
||||
* @param mixed $principal the present value |
||||
* @param float[] $schedule an array of interest rates to apply |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function futureValue($principal, $schedule) |
||||
{ |
||||
$principal = Functions::flattenSingleValue($principal); |
||||
$schedule = Functions::flattenArray($schedule); |
||||
|
||||
try { |
||||
$principal = CashFlowValidations::validateFloat($principal); |
||||
|
||||
foreach ($schedule as $rate) { |
||||
$rate = CashFlowValidations::validateFloat($rate); |
||||
$principal *= 1 + $rate; |
||||
} |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return $principal; |
||||
} |
||||
|
||||
/** |
||||
* PDURATION. |
||||
* |
||||
* Calculates the number of periods required for an investment to reach a specified value. |
||||
* |
||||
* @param mixed $rate Interest rate per period |
||||
* @param mixed $presentValue Present Value |
||||
* @param mixed $futureValue Future Value |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function periods($rate, $presentValue, $futureValue) |
||||
{ |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = Functions::flattenSingleValue($futureValue); |
||||
|
||||
try { |
||||
$rate = CashFlowValidations::validateRate($rate); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return (log($futureValue) - log($presentValue)) / log(1 + $rate); |
||||
} |
||||
|
||||
/** |
||||
* RRI. |
||||
* |
||||
* Calculates the interest rate required for an investment to grow to a specified future value . |
||||
* |
||||
* @param float $periods The number of periods over which the investment is made |
||||
* @param float $presentValue Present Value |
||||
* @param float $futureValue Future Value |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function interestRate($periods = 0.0, $presentValue = 0.0, $futureValue = 0.0) |
||||
{ |
||||
$periods = Functions::flattenSingleValue($periods); |
||||
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
$futureValue = Functions::flattenSingleValue($futureValue); |
||||
|
||||
try { |
||||
$periods = CashFlowValidations::validateFloat($periods); |
||||
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Validate parameters |
||||
if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return ($futureValue / $presentValue) ** (1 / $periods) - 1; |
||||
} |
||||
} |
@ -0,0 +1,325 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class NonPeriodic |
||||
{ |
||||
const FINANCIAL_MAX_ITERATIONS = 128; |
||||
|
||||
const FINANCIAL_PRECISION = 1.0e-08; |
||||
|
||||
const DEFAULT_GUESS = 0.1; |
||||
|
||||
/** |
||||
* XIRR. |
||||
* |
||||
* Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic. |
||||
* |
||||
* Excel Function: |
||||
* =XIRR(values,dates,guess) |
||||
* |
||||
* @param float[] $values A series of cash flow payments |
||||
* The series of values must contain at least one positive value & one negative value |
||||
* @param mixed[] $dates A series of payment dates |
||||
* The first payment date indicates the beginning of the schedule of payments |
||||
* All other dates must be later than this date, but they may occur in any order |
||||
* @param mixed $guess An optional guess at the expected answer |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function rate($values, $dates, $guess = self::DEFAULT_GUESS) |
||||
{ |
||||
$rslt = self::xirrPart1($values, $dates); |
||||
if ($rslt !== '') { |
||||
return $rslt; |
||||
} |
||||
|
||||
// create an initial range, with a root somewhere between 0 and guess |
||||
$guess = Functions::flattenSingleValue($guess) ?? self::DEFAULT_GUESS; |
||||
if (!is_numeric($guess)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
$guess = ($guess + 0.0) ?: self::DEFAULT_GUESS; |
||||
$x1 = 0.0; |
||||
$x2 = $guess + 0.0; |
||||
$f1 = self::xnpvOrdered($x1, $values, $dates, false); |
||||
$f2 = self::xnpvOrdered($x2, $values, $dates, false); |
||||
$found = false; |
||||
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
if (!is_numeric($f1)) { |
||||
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 float $rate the discount rate to apply to the cash flows |
||||
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates. |
||||
* The first payment is optional and corresponds to a cost or payment that occurs |
||||
* at the beginning of the investment. |
||||
* If the first value is a cost or payment, it must be a negative value. |
||||
* All succeeding payments are discounted based on a 365-day year. |
||||
* The series of values must contain at least one positive value and one negative value. |
||||
* @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments. |
||||
* The first payment date indicates the beginning of the schedule of payments. |
||||
* All other dates must be later than this date, but they may occur in any order. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function presentValue($rate, $values, $dates) |
||||
{ |
||||
return self::xnpvOrdered($rate, $values, $dates, true); |
||||
} |
||||
|
||||
private static function bothNegAndPos(bool $neg, bool $pos): bool |
||||
{ |
||||
return $neg && $pos; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $values |
||||
* @param mixed $dates |
||||
*/ |
||||
private static function xirrPart1(&$values, &$dates): string |
||||
{ |
||||
$values = Functions::flattenArray($values); |
||||
$dates = Functions::flattenArray($dates); |
||||
$valuesIsArray = count($values) > 1; |
||||
$datesIsArray = count($dates) > 1; |
||||
if (!$valuesIsArray && !$datesIsArray) { |
||||
return ExcelError::NA(); |
||||
} |
||||
if (count($values) != count($dates)) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$datesCount = count($dates); |
||||
for ($i = 0; $i < $datesCount; ++$i) { |
||||
try { |
||||
$dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
} |
||||
|
||||
return self::xirrPart2($values); |
||||
} |
||||
|
||||
private static function xirrPart2(array &$values): string |
||||
{ |
||||
$valCount = count($values); |
||||
$foundpos = false; |
||||
$foundneg = false; |
||||
for ($i = 0; $i < $valCount; ++$i) { |
||||
$fld = $values[$i]; |
||||
if (!is_numeric($fld)) { |
||||
return ExcelError::VALUE(); |
||||
} elseif ($fld > 0) { |
||||
$foundpos = true; |
||||
} elseif ($fld < 0) { |
||||
$foundneg = true; |
||||
} |
||||
} |
||||
if (!self::bothNegAndPos($foundneg, $foundpos)) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return ''; |
||||
} |
||||
|
||||
/** |
||||
* @return float|string |
||||
*/ |
||||
private static function xirrPart3(array $values, array $dates, float $x1, float $x2) |
||||
{ |
||||
$f = self::xnpvOrdered($x1, $values, $dates, false); |
||||
if ($f < 0.0) { |
||||
$rtb = $x1; |
||||
$dx = $x2 - $x1; |
||||
} else { |
||||
$rtb = $x2; |
||||
$dx = $x1 - $x2; |
||||
} |
||||
|
||||
$rslt = ExcelError::VALUE(); |
||||
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
$dx *= 0.5; |
||||
$x_mid = $rtb + $dx; |
||||
$f_mid = (float) self::xnpvOrdered($x_mid, $values, $dates, false); |
||||
if ($f_mid <= 0.0) { |
||||
$rtb = $x_mid; |
||||
} |
||||
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { |
||||
$rslt = $x_mid; |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
return $rslt; |
||||
} |
||||
|
||||
/** |
||||
* @return float|string |
||||
*/ |
||||
private static function xirrBisection(array $values, array $dates, float $x1, float $x2) |
||||
{ |
||||
$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; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $rate |
||||
* @param mixed $values |
||||
* @param mixed $dates |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
private static function xnpvOrdered($rate, $values, $dates, bool $ordered = true, bool $capAtNegative1 = false) |
||||
{ |
||||
$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(); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $rate |
||||
*/ |
||||
private static function validateXnpv($rate, array $values, array $dates): void |
||||
{ |
||||
if (!is_numeric($rate)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
$valCount = count($values); |
||||
if ($valCount != count($dates)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,169 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Periodic |
||||
{ |
||||
const FINANCIAL_MAX_ITERATIONS = 128; |
||||
|
||||
const FINANCIAL_PRECISION = 1.0e-08; |
||||
|
||||
/** |
||||
* IRR. |
||||
* |
||||
* Returns the internal rate of return for a series of cash flows represented by the numbers in values. |
||||
* These cash flows do not have to be even, as they would be for an annuity. However, the cash flows must occur |
||||
* at regular intervals, such as monthly or annually. The internal rate of return is the interest rate received |
||||
* for an investment consisting of payments (negative values) and income (positive values) that occur at regular |
||||
* periods. |
||||
* |
||||
* Excel Function: |
||||
* IRR(values[,guess]) |
||||
* |
||||
* @param mixed $values An array or a reference to cells that contain numbers for which you want |
||||
* to calculate the internal rate of return. |
||||
* Values must contain at least one positive value and one negative value to |
||||
* calculate the internal rate of return. |
||||
* @param mixed $guess A number that you guess is close to the result of IRR |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function rate($values, $guess = 0.1) |
||||
{ |
||||
if (!is_array($values)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
$values = Functions::flattenArray($values); |
||||
$guess = Functions::flattenSingleValue($guess); |
||||
|
||||
// create an initial range, with a root somewhere between 0 and guess |
||||
$x1 = 0.0; |
||||
$x2 = $guess; |
||||
$f1 = self::presentValue($x1, $values); |
||||
$f2 = self::presentValue($x2, $values); |
||||
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
if (($f1 * $f2) < 0.0) { |
||||
break; |
||||
} |
||||
if (abs($f1) < abs($f2)) { |
||||
$f1 = self::presentValue($x1 += 1.6 * ($x1 - $x2), $values); |
||||
} else { |
||||
$f2 = self::presentValue($x2 += 1.6 * ($x2 - $x1), $values); |
||||
} |
||||
} |
||||
if (($f1 * $f2) > 0.0) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
$f = self::presentValue($x1, $values); |
||||
if ($f < 0.0) { |
||||
$rtb = $x1; |
||||
$dx = $x2 - $x1; |
||||
} else { |
||||
$rtb = $x2; |
||||
$dx = $x1 - $x2; |
||||
} |
||||
|
||||
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
$dx *= 0.5; |
||||
$x_mid = $rtb + $dx; |
||||
$f_mid = self::presentValue($x_mid, $values); |
||||
if ($f_mid <= 0.0) { |
||||
$rtb = $x_mid; |
||||
} |
||||
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { |
||||
return $x_mid; |
||||
} |
||||
} |
||||
|
||||
return ExcelError::VALUE(); |
||||
} |
||||
|
||||
/** |
||||
* MIRR. |
||||
* |
||||
* Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both |
||||
* the cost of the investment and the interest received on reinvestment of cash. |
||||
* |
||||
* Excel Function: |
||||
* MIRR(values,finance_rate, reinvestment_rate) |
||||
* |
||||
* @param mixed $values An array or a reference to cells that contain a series of payments and |
||||
* income occurring at regular intervals. |
||||
* Payments are negative value, income is positive values. |
||||
* @param mixed $financeRate The interest rate you pay on the money used in the cash flows |
||||
* @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function modifiedRate($values, $financeRate, $reinvestmentRate) |
||||
{ |
||||
if (!is_array($values)) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
$values = Functions::flattenArray($values); |
||||
$financeRate = Functions::flattenSingleValue($financeRate); |
||||
$reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate); |
||||
$n = count($values); |
||||
|
||||
$rr = 1.0 + $reinvestmentRate; |
||||
$fr = 1.0 + $financeRate; |
||||
|
||||
$npvPos = $npvNeg = self::$zeroPointZero; |
||||
foreach ($values as $i => $v) { |
||||
if ($v >= 0) { |
||||
$npvPos += $v / $rr ** $i; |
||||
} else { |
||||
$npvNeg += $v / $fr ** $i; |
||||
} |
||||
} |
||||
|
||||
if ($npvNeg === self::$zeroPointZero || $npvPos === self::$zeroPointZero) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
|
||||
$mirr = ((-$npvPos * $rr ** $n) |
||||
/ ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; |
||||
|
||||
return is_finite($mirr) ? $mirr : ExcelError::NAN(); |
||||
} |
||||
|
||||
/** |
||||
* Sop to Scrutinizer. |
||||
* |
||||
* @var float |
||||
*/ |
||||
private static $zeroPointZero = 0.0; |
||||
|
||||
/** |
||||
* NPV. |
||||
* |
||||
* Returns the Net Present Value of a cash flow series given a discount rate. |
||||
* |
||||
* @param mixed $rate |
||||
* @param array $args |
||||
* |
||||
* @return float |
||||
*/ |
||||
public static function presentValue($rate, ...$args) |
||||
{ |
||||
$returnValue = 0; |
||||
|
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$aArgs = Functions::flattenArray($args); |
||||
|
||||
// Calculate |
||||
$countArgs = count($aArgs); |
||||
for ($i = 1; $i <= $countArgs; ++$i) { |
||||
// Is it a numeric value? |
||||
if (is_numeric($aArgs[$i - 1])) { |
||||
$returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i; |
||||
} |
||||
} |
||||
|
||||
return $returnValue; |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -0,0 +1,426 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use DateTime; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Shared\Date; |
||||
|
||||
class Coupons |
||||
{ |
||||
private const PERIOD_DATE_PREVIOUS = false; |
||||
private const PERIOD_DATE_NEXT = true; |
||||
|
||||
/** |
||||
* COUPDAYBS. |
||||
* |
||||
* Returns the number of days from the beginning of the coupon period to the settlement date. |
||||
* |
||||
* Excel Function: |
||||
* COUPDAYBS(settlement,maturity,frequency[,basis]) |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue |
||||
* date when the security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $frequency The number of coupon payments per year (int). |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use (int). |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function COUPDAYBS( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); |
||||
if (is_string($daysPerYear)) { |
||||
return ExcelError::VALUE(); |
||||
} |
||||
$prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); |
||||
|
||||
if ($basis === FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) { |
||||
return abs((float) DateTimeExcel\Days::between($prev, $settlement)); |
||||
} |
||||
|
||||
return (float) DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear; |
||||
} |
||||
|
||||
/** |
||||
* COUPDAYS. |
||||
* |
||||
* Returns the number of days in the coupon period that contains the settlement date. |
||||
* |
||||
* Excel Function: |
||||
* COUPDAYS(settlement,maturity,frequency[,basis]) |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue |
||||
* date when the security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $frequency The number of coupon payments per year. |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use (int). |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function COUPDAYS( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
switch ($basis) { |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_365: |
||||
// Actual/365 |
||||
return 365 / $frequency; |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL: |
||||
// Actual/actual |
||||
if ($frequency == FinancialConstants::FREQUENCY_ANNUAL) { |
||||
$daysPerYear = (int) Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); |
||||
|
||||
return $daysPerYear / $frequency; |
||||
} |
||||
$prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); |
||||
$next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); |
||||
|
||||
return $next - $prev; |
||||
default: |
||||
// US (NASD) 30/360, Actual/360 or European 30/360 |
||||
return 360 / $frequency; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* COUPDAYSNC. |
||||
* |
||||
* Returns the number of days from the settlement date to the next coupon date. |
||||
* |
||||
* Excel Function: |
||||
* COUPDAYSNC(settlement,maturity,frequency[,basis]) |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue |
||||
* date when the security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $frequency The number of coupon payments per year. |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use (int) . |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function COUPDAYSNC( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
/** @var int */ |
||||
$daysPerYear = Helpers::daysPerYear(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 mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function COUPNCD( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
self::doNothing($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); |
||||
} |
||||
|
||||
/** |
||||
* COUPNUM. |
||||
* |
||||
* Returns the number of coupons payable between the settlement date and maturity date, |
||||
* rounded up to the nearest whole coupon. |
||||
* |
||||
* Excel Function: |
||||
* COUPNUM(settlement,maturity,frequency[,basis]) |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue |
||||
* date when the security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $frequency The number of coupon payments per year. |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use (int). |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return int|string |
||||
*/ |
||||
public static function COUPNUM( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
self::doNothing($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$yearsBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction( |
||||
$settlement, |
||||
$maturity, |
||||
FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
); |
||||
|
||||
return (int) ceil((float) $yearsBetweenSettlementAndMaturity * $frequency); |
||||
} |
||||
|
||||
/** |
||||
* COUPPCD. |
||||
* |
||||
* Returns the previous coupon date before the settlement date. |
||||
* |
||||
* Excel Function: |
||||
* COUPPCD(settlement,maturity,frequency[,basis]) |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue |
||||
* date when the security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $frequency The number of coupon payments per year. |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use (int). |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
* depending on the value of the ReturnDateType flag |
||||
*/ |
||||
public static function COUPPCD( |
||||
$settlement, |
||||
$maturity, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
self::validateCouponPeriod($settlement, $maturity); |
||||
$frequency = FinancialValidations::validateFrequency($frequency); |
||||
$basis = FinancialValidations::validateBasis($basis); |
||||
self::doNothing($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); |
||||
} |
||||
|
||||
private static function monthsDiff(DateTime $result, int $months, string $plusOrMinus, int $day, bool $lastDayFlag): void |
||||
{ |
||||
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), 1); |
||||
$result->modify("$plusOrMinus $months months"); |
||||
$daysInMonth = (int) $result->format('t'); |
||||
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), $lastDayFlag ? $daysInMonth : min($day, $daysInMonth)); |
||||
} |
||||
|
||||
private static function couponFirstPeriodDate(float $settlement, float $maturity, int $frequency, bool $next): float |
||||
{ |
||||
$months = 12 / $frequency; |
||||
|
||||
$result = Date::excelToDateTimeObject($maturity); |
||||
$day = (int) $result->format('d'); |
||||
$lastDayFlag = Helpers::isLastDayOfMonth($result); |
||||
|
||||
while ($settlement < Date::PHPToExcel($result)) { |
||||
self::monthsDiff($result, $months, '-', $day, $lastDayFlag); |
||||
} |
||||
if ($next === true) { |
||||
self::monthsDiff($result, $months, '+', $day, $lastDayFlag); |
||||
} |
||||
|
||||
return (float) Date::PHPToExcel($result); |
||||
} |
||||
|
||||
private static function validateCouponPeriod(float $settlement, float $maturity): void |
||||
{ |
||||
if ($settlement >= $maturity) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
} |
||||
|
||||
/** @param mixed $basis */ |
||||
private static function doNothing($basis): bool |
||||
{ |
||||
return $basis; |
||||
} |
||||
} |
@ -0,0 +1,276 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Depreciation |
||||
{ |
||||
/** @var float */ |
||||
private static $zeroPointZero = 0.0; |
||||
|
||||
/** |
||||
* DB. |
||||
* |
||||
* Returns the depreciation of an asset for a specified period using the |
||||
* fixed-declining balance method. |
||||
* This form of depreciation is used if you want to get a higher depreciation value |
||||
* at the beginning of the depreciation (as opposed to linear depreciation). The |
||||
* depreciation value is reduced with every depreciation period by the depreciation |
||||
* already deducted from the initial cost. |
||||
* |
||||
* Excel Function: |
||||
* DB(cost,salvage,life,period[,month]) |
||||
* |
||||
* @param mixed $cost Initial cost of the asset |
||||
* @param mixed $salvage Value at the end of the depreciation. |
||||
* (Sometimes called the salvage value of the asset) |
||||
* @param mixed $life Number of periods over which the asset is depreciated. |
||||
* (Sometimes called the useful life of the asset) |
||||
* @param mixed $period The period for which you want to calculate the |
||||
* depreciation. Period must use the same units as life. |
||||
* @param mixed $month Number of months in the first year. If month is omitted, |
||||
* it defaults to 12. |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DB($cost, $salvage, $life, $period, $month = 12) |
||||
{ |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$life = Functions::flattenSingleValue($life); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$month = Functions::flattenSingleValue($month); |
||||
|
||||
try { |
||||
$cost = self::validateCost($cost); |
||||
$salvage = self::validateSalvage($salvage); |
||||
$life = self::validateLife($life); |
||||
$period = self::validatePeriod($period); |
||||
$month = self::validateMonth($month); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($cost === self::$zeroPointZero) { |
||||
return 0.0; |
||||
} |
||||
|
||||
// Set Fixed Depreciation Rate |
||||
$fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life); |
||||
$fixedDepreciationRate = round($fixedDepreciationRate, 3); |
||||
|
||||
// Loop through each period calculating the depreciation |
||||
// TODO Handle period value between 0 and 1 (e.g. 0.5) |
||||
$previousDepreciation = 0; |
||||
$depreciation = 0; |
||||
for ($per = 1; $per <= $period; ++$per) { |
||||
if ($per == 1) { |
||||
$depreciation = $cost * $fixedDepreciationRate * $month / 12; |
||||
} elseif ($per == ($life + 1)) { |
||||
$depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate * (12 - $month) / 12; |
||||
} else { |
||||
$depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate; |
||||
} |
||||
$previousDepreciation += $depreciation; |
||||
} |
||||
|
||||
return $depreciation; |
||||
} |
||||
|
||||
/** |
||||
* DDB. |
||||
* |
||||
* Returns the depreciation of an asset for a specified period using the |
||||
* double-declining balance method or some other method you specify. |
||||
* |
||||
* Excel Function: |
||||
* DDB(cost,salvage,life,period[,factor]) |
||||
* |
||||
* @param mixed $cost Initial cost of the asset |
||||
* @param mixed $salvage Value at the end of the depreciation. |
||||
* (Sometimes called the salvage value of the asset) |
||||
* @param mixed $life Number of periods over which the asset is depreciated. |
||||
* (Sometimes called the useful life of the asset) |
||||
* @param mixed $period The period for which you want to calculate the |
||||
* depreciation. Period must use the same units as life. |
||||
* @param mixed $factor The rate at which the balance declines. |
||||
* If factor is omitted, it is assumed to be 2 (the |
||||
* double-declining balance method). |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function DDB($cost, $salvage, $life, $period, $factor = 2.0) |
||||
{ |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$life = Functions::flattenSingleValue($life); |
||||
$period = Functions::flattenSingleValue($period); |
||||
$factor = Functions::flattenSingleValue($factor); |
||||
|
||||
try { |
||||
$cost = self::validateCost($cost); |
||||
$salvage = self::validateSalvage($salvage); |
||||
$life = self::validateLife($life); |
||||
$period = self::validatePeriod($period); |
||||
$factor = self::validateFactor($factor); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($period > $life) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Loop through each period calculating the depreciation |
||||
// TODO Handling for fractional $period values |
||||
$previousDepreciation = 0; |
||||
$depreciation = 0; |
||||
for ($per = 1; $per <= $period; ++$per) { |
||||
$depreciation = min( |
||||
($cost - $previousDepreciation) * ($factor / $life), |
||||
($cost - $salvage - $previousDepreciation) |
||||
); |
||||
$previousDepreciation += $depreciation; |
||||
} |
||||
|
||||
return $depreciation; |
||||
} |
||||
|
||||
/** |
||||
* SLN. |
||||
* |
||||
* Returns the straight-line depreciation of an asset for one period |
||||
* |
||||
* @param mixed $cost Initial cost of the asset |
||||
* @param mixed $salvage Value at the end of the depreciation |
||||
* @param mixed $life Number of periods over which the asset is depreciated |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function SLN($cost, $salvage, $life) |
||||
{ |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$life = Functions::flattenSingleValue($life); |
||||
|
||||
try { |
||||
$cost = self::validateCost($cost, true); |
||||
$salvage = self::validateSalvage($salvage, true); |
||||
$life = self::validateLife($life, true); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($life === self::$zeroPointZero) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
|
||||
return ($cost - $salvage) / $life; |
||||
} |
||||
|
||||
/** |
||||
* SYD. |
||||
* |
||||
* Returns the sum-of-years' digits depreciation of an asset for a specified period. |
||||
* |
||||
* @param mixed $cost Initial cost of the asset |
||||
* @param mixed $salvage Value at the end of the depreciation |
||||
* @param mixed $life Number of periods over which the asset is depreciated |
||||
* @param mixed $period Period |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function SYD($cost, $salvage, $life, $period) |
||||
{ |
||||
$cost = Functions::flattenSingleValue($cost); |
||||
$salvage = Functions::flattenSingleValue($salvage); |
||||
$life = Functions::flattenSingleValue($life); |
||||
$period = Functions::flattenSingleValue($period); |
||||
|
||||
try { |
||||
$cost = self::validateCost($cost, true); |
||||
$salvage = self::validateSalvage($salvage); |
||||
$life = self::validateLife($life); |
||||
$period = self::validatePeriod($period); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($period > $life) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
$syd = (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1)); |
||||
|
||||
return $syd; |
||||
} |
||||
|
||||
/** @param mixed $cost */ |
||||
private static function validateCost($cost, bool $negativeValueAllowed = false): float |
||||
{ |
||||
$cost = FinancialValidations::validateFloat($cost); |
||||
if ($cost < 0.0 && $negativeValueAllowed === false) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $cost; |
||||
} |
||||
|
||||
/** @param mixed $salvage */ |
||||
private static function validateSalvage($salvage, bool $negativeValueAllowed = false): float |
||||
{ |
||||
$salvage = FinancialValidations::validateFloat($salvage); |
||||
if ($salvage < 0.0 && $negativeValueAllowed === false) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $salvage; |
||||
} |
||||
|
||||
/** @param mixed $life */ |
||||
private static function validateLife($life, bool $negativeValueAllowed = false): float |
||||
{ |
||||
$life = FinancialValidations::validateFloat($life); |
||||
if ($life < 0.0 && $negativeValueAllowed === false) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $life; |
||||
} |
||||
|
||||
/** @param mixed $period */ |
||||
private static function validatePeriod($period, bool $negativeValueAllowed = false): float |
||||
{ |
||||
$period = FinancialValidations::validateFloat($period); |
||||
if ($period <= 0.0 && $negativeValueAllowed === false) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $period; |
||||
} |
||||
|
||||
/** @param mixed $month */ |
||||
private static function validateMonth($month): int |
||||
{ |
||||
$month = FinancialValidations::validateInt($month); |
||||
if ($month < 1) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $month; |
||||
} |
||||
|
||||
/** @param mixed $factor */ |
||||
private static function validateFactor($factor): float |
||||
{ |
||||
$factor = FinancialValidations::validateFloat($factor); |
||||
if ($factor <= 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $factor; |
||||
} |
||||
} |
@ -0,0 +1,132 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\TextData\Format; |
||||
|
||||
class Dollar |
||||
{ |
||||
use ArrayEnabled; |
||||
|
||||
/** |
||||
* DOLLAR. |
||||
* |
||||
* This function converts a number to text using currency format, with the decimals rounded to the specified place. |
||||
* The format used is $#,##0.00_);($#,##0.00).. |
||||
* |
||||
* @param mixed $number The value to format, or can be an array of numbers |
||||
* Or can be an array of values |
||||
* @param mixed $precision The number of digits to display to the right of the decimal point (as an integer). |
||||
* If precision is negative, number is rounded to the left of the decimal point. |
||||
* If you omit precision, it is assumed to be 2 |
||||
* Or can be an array of precision values |
||||
* |
||||
* @return array|string |
||||
* If an array of values is passed for either of the arguments, then the returned result |
||||
* will also be an array with matching dimensions |
||||
*/ |
||||
public static function format($number, $precision = 2) |
||||
{ |
||||
return Format::DOLLAR($number, $precision); |
||||
} |
||||
|
||||
/** |
||||
* DOLLARDE. |
||||
* |
||||
* Converts a dollar price expressed as an integer part and a fraction |
||||
* part into a dollar price expressed as a decimal number. |
||||
* Fractional dollar numbers are sometimes used for security prices. |
||||
* |
||||
* Excel Function: |
||||
* DOLLARDE(fractional_dollar,fraction) |
||||
* |
||||
* @param mixed $fractionalDollar Fractional Dollar |
||||
* Or can be an array of values |
||||
* @param mixed $fraction Fraction |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
*/ |
||||
public static function decimal($fractionalDollar = null, $fraction = 0) |
||||
{ |
||||
if (is_array($fractionalDollar) || is_array($fraction)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $fractionalDollar, $fraction); |
||||
} |
||||
|
||||
try { |
||||
$fractionalDollar = FinancialValidations::validateFloat( |
||||
Functions::flattenSingleValue($fractionalDollar) ?? 0.0 |
||||
); |
||||
$fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Additional parameter validations |
||||
if ($fraction < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
if ($fraction == 0) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
|
||||
$dollars = ($fractionalDollar < 0) ? ceil($fractionalDollar) : floor($fractionalDollar); |
||||
$cents = fmod($fractionalDollar, 1.0); |
||||
$cents /= $fraction; |
||||
$cents *= 10 ** ceil(log10($fraction)); |
||||
|
||||
return $dollars + $cents; |
||||
} |
||||
|
||||
/** |
||||
* DOLLARFR. |
||||
* |
||||
* Converts a dollar price expressed as a decimal number into a dollar price |
||||
* expressed as a fraction. |
||||
* Fractional dollar numbers are sometimes used for security prices. |
||||
* |
||||
* Excel Function: |
||||
* DOLLARFR(decimal_dollar,fraction) |
||||
* |
||||
* @param mixed $decimalDollar Decimal Dollar |
||||
* Or can be an array of values |
||||
* @param mixed $fraction Fraction |
||||
* Or can be an array of values |
||||
* |
||||
* @return array|float|string |
||||
*/ |
||||
public static function fractional($decimalDollar = null, $fraction = 0) |
||||
{ |
||||
if (is_array($decimalDollar) || is_array($fraction)) { |
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $decimalDollar, $fraction); |
||||
} |
||||
|
||||
try { |
||||
$decimalDollar = FinancialValidations::validateFloat( |
||||
Functions::flattenSingleValue($decimalDollar) ?? 0.0 |
||||
); |
||||
$fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
// Additional parameter validations |
||||
if ($fraction < 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
if ($fraction == 0) { |
||||
return ExcelError::DIV0(); |
||||
} |
||||
|
||||
$dollars = ($decimalDollar < 0.0) ? ceil($decimalDollar) : floor($decimalDollar); |
||||
$cents = fmod($decimalDollar, 1); |
||||
$cents *= $fraction; |
||||
$cents *= 10 ** (-ceil(log10($fraction))); |
||||
|
||||
return $dollars + $cents; |
||||
} |
||||
} |
@ -0,0 +1,158 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class FinancialValidations |
||||
{ |
||||
/** |
||||
* @param mixed $date |
||||
*/ |
||||
public static function validateDate($date): float |
||||
{ |
||||
return DateTimeExcel\Helpers::getDateValue($date); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $settlement |
||||
*/ |
||||
public static function validateSettlementDate($settlement): float |
||||
{ |
||||
return self::validateDate($settlement); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $maturity |
||||
*/ |
||||
public static function validateMaturityDate($maturity): float |
||||
{ |
||||
return self::validateDate($maturity); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
public static function validateFloat($value): float |
||||
{ |
||||
if (!is_numeric($value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (float) $value; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $value |
||||
*/ |
||||
public static function validateInt($value): int |
||||
{ |
||||
if (!is_numeric($value)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
return (int) floor((float) $value); |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $rate |
||||
*/ |
||||
public static function validateRate($rate): float |
||||
{ |
||||
$rate = self::validateFloat($rate); |
||||
if ($rate < 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $rate; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $frequency |
||||
*/ |
||||
public static function validateFrequency($frequency): int |
||||
{ |
||||
$frequency = self::validateInt($frequency); |
||||
if ( |
||||
($frequency !== FinancialConstants::FREQUENCY_ANNUAL) && |
||||
($frequency !== FinancialConstants::FREQUENCY_SEMI_ANNUAL) && |
||||
($frequency !== FinancialConstants::FREQUENCY_QUARTERLY) |
||||
) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $frequency; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $basis |
||||
*/ |
||||
public static function validateBasis($basis): int |
||||
{ |
||||
if (!is_numeric($basis)) { |
||||
throw new Exception(ExcelError::VALUE()); |
||||
} |
||||
|
||||
$basis = (int) $basis; |
||||
if (($basis < 0) || ($basis > 4)) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $basis; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $price |
||||
*/ |
||||
public static function validatePrice($price): float |
||||
{ |
||||
$price = self::validateFloat($price); |
||||
if ($price < 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $price; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $parValue |
||||
*/ |
||||
public static function validateParValue($parValue): float |
||||
{ |
||||
$parValue = self::validateFloat($parValue); |
||||
if ($parValue < 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $parValue; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $yield |
||||
*/ |
||||
public static function validateYield($yield): float |
||||
{ |
||||
$yield = self::validateFloat($yield); |
||||
if ($yield < 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $yield; |
||||
} |
||||
|
||||
/** |
||||
* @param mixed $discount |
||||
*/ |
||||
public static function validateDiscount($discount): float |
||||
{ |
||||
$discount = self::validateFloat($discount); |
||||
if ($discount <= 0.0) { |
||||
throw new Exception(ExcelError::NAN()); |
||||
} |
||||
|
||||
return $discount; |
||||
} |
||||
} |
@ -0,0 +1,58 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use DateTimeInterface; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Helpers |
||||
{ |
||||
/** |
||||
* daysPerYear. |
||||
* |
||||
* Returns the number of days in a specified year, as defined by the "basis" value |
||||
* |
||||
* @param int|string $year The year against which we're testing |
||||
* @param int|string $basis The type of day count: |
||||
* 0 or omitted US (NASD) 360 |
||||
* 1 Actual (365 or 366 in a leap year) |
||||
* 2 360 |
||||
* 3 365 |
||||
* 4 European 360 |
||||
* |
||||
* @return int|string Result, or a string containing an error |
||||
*/ |
||||
public static function daysPerYear($year, $basis = 0) |
||||
{ |
||||
if (!is_numeric($basis)) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
switch ($basis) { |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_NASD: |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_360: |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_360_EUROPEAN: |
||||
return 360; |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_365: |
||||
return 365; |
||||
case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL: |
||||
return (DateTimeExcel\Helpers::isLeapYear($year)) ? 366 : 365; |
||||
} |
||||
|
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
/** |
||||
* isLastDayOfMonth. |
||||
* |
||||
* Returns a boolean TRUE/FALSE indicating if this date is the last date of the month |
||||
* |
||||
* @param DateTimeInterface $date The date for testing |
||||
*/ |
||||
public static function isLastDayOfMonth(DateTimeInterface $date): bool |
||||
{ |
||||
return $date->format('d') === $date->format('t'); |
||||
} |
||||
} |
@ -0,0 +1,73 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class InterestRate |
||||
{ |
||||
/** |
||||
* EFFECT. |
||||
* |
||||
* Returns the effective interest rate given the nominal rate and the number of |
||||
* compounding payments per year. |
||||
* |
||||
* Excel Function: |
||||
* EFFECT(nominal_rate,npery) |
||||
* |
||||
* @param mixed $nominalRate Nominal interest rate as a float |
||||
* @param mixed $periodsPerYear Integer number of compounding payments per year |
||||
* |
||||
* @return float|string |
||||
*/ |
||||
public static function effective($nominalRate = 0, $periodsPerYear = 0) |
||||
{ |
||||
$nominalRate = Functions::flattenSingleValue($nominalRate); |
||||
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear); |
||||
|
||||
try { |
||||
$nominalRate = FinancialValidations::validateFloat($nominalRate); |
||||
$periodsPerYear = FinancialValidations::validateInt($periodsPerYear); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($nominalRate <= 0 || $periodsPerYear < 1) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1; |
||||
} |
||||
|
||||
/** |
||||
* NOMINAL. |
||||
* |
||||
* Returns the nominal interest rate given the effective rate and the number of compounding payments per year. |
||||
* |
||||
* @param mixed $effectiveRate Effective interest rate as a float |
||||
* @param mixed $periodsPerYear Integer number of compounding payments per year |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function nominal($effectiveRate = 0, $periodsPerYear = 0) |
||||
{ |
||||
$effectiveRate = Functions::flattenSingleValue($effectiveRate); |
||||
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear); |
||||
|
||||
try { |
||||
$effectiveRate = FinancialValidations::validateFloat($effectiveRate); |
||||
$periodsPerYear = FinancialValidations::validateInt($periodsPerYear); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($effectiveRate <= 0 || $periodsPerYear < 1) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
|
||||
// Calculate |
||||
return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1); |
||||
} |
||||
} |
@ -0,0 +1,159 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\YearFrac; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
||||
class AccruedInterest |
||||
{ |
||||
public const ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT = true; |
||||
|
||||
public const ACCRINT_CALCMODE_FIRST_INTEREST_TO_SETTLEMENT = false; |
||||
|
||||
/** |
||||
* ACCRINT. |
||||
* |
||||
* Returns the accrued interest for a security that pays periodic interest. |
||||
* |
||||
* Excel Function: |
||||
* ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis][,calc_method]) |
||||
* |
||||
* @param mixed $issue the security's issue date |
||||
* @param mixed $firstInterest the security's first interest date |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue date |
||||
* when the security is traded to the buyer. |
||||
* @param mixed $rate The security's annual coupon rate |
||||
* @param mixed $parValue The security's par value. |
||||
* If you omit par, ACCRINT uses $1,000. |
||||
* @param mixed $frequency The number of coupon payments per year. |
||||
* Valid frequency values are: |
||||
* 1 Annual |
||||
* 2 Semi-Annual |
||||
* 4 Quarterly |
||||
* @param mixed $basis The type of day count to use. |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* @param mixed $calcMethod |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function periodic( |
||||
$issue, |
||||
$firstInterest, |
||||
$settlement, |
||||
$rate, |
||||
$parValue = 1000, |
||||
$frequency = FinancialConstants::FREQUENCY_ANNUAL, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD, |
||||
$calcMethod = self::ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT |
||||
) { |
||||
self::doNothing($calcMethod); |
||||
$issue = Functions::flattenSingleValue($issue); |
||||
$firstInterest = Functions::flattenSingleValue($firstInterest); |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue); |
||||
$frequency = ($frequency === null) |
||||
? FinancialConstants::FREQUENCY_ANNUAL |
||||
: Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$issue = SecurityValidations::validateIssueDate($issue); |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
SecurityValidations::validateSecurityPeriod($issue, $settlement); |
||||
$rate = SecurityValidations::validateRate($rate); |
||||
$parValue = SecurityValidations::validateParValue($parValue); |
||||
$frequency = SecurityValidations::validateFrequency($frequency); |
||||
self::doNothing($frequency); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); |
||||
if (!is_numeric($daysBetweenIssueAndSettlement)) { |
||||
// return date error |
||||
return $daysBetweenIssueAndSettlement; |
||||
} |
||||
$daysBetweenFirstInterestAndSettlement = Functions::scalar(YearFrac::fraction($firstInterest, $settlement, $basis)); |
||||
if (!is_numeric($daysBetweenFirstInterestAndSettlement)) { |
||||
// return date error |
||||
return $daysBetweenFirstInterestAndSettlement; |
||||
} |
||||
|
||||
return $parValue * $rate * $daysBetweenIssueAndSettlement; |
||||
} |
||||
|
||||
/** |
||||
* ACCRINTM. |
||||
* |
||||
* Returns the accrued interest for a security that pays interest at maturity. |
||||
* |
||||
* Excel Function: |
||||
* ACCRINTM(issue,settlement,rate[,par[,basis]]) |
||||
* |
||||
* @param mixed $issue The security's issue date |
||||
* @param mixed $settlement The security's settlement (or maturity) date |
||||
* @param mixed $rate The security's annual coupon rate |
||||
* @param mixed $parValue The security's par value. |
||||
* If you omit parValue, ACCRINT uses $1,000. |
||||
* @param mixed $basis The type of day count to use. |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function atMaturity( |
||||
$issue, |
||||
$settlement, |
||||
$rate, |
||||
$parValue = 1000, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$issue = Functions::flattenSingleValue($issue); |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$issue = SecurityValidations::validateIssueDate($issue); |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
SecurityValidations::validateSecurityPeriod($issue, $settlement); |
||||
$rate = SecurityValidations::validateRate($rate); |
||||
$parValue = SecurityValidations::validateParValue($parValue); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); |
||||
if (!is_numeric($daysBetweenIssueAndSettlement)) { |
||||
// return date error |
||||
return $daysBetweenIssueAndSettlement; |
||||
} |
||||
|
||||
return $parValue * $rate * $daysBetweenIssueAndSettlement; |
||||
} |
||||
|
||||
/** @param mixed $arg */ |
||||
private static function doNothing($arg): bool |
||||
{ |
||||
return (bool) $arg; |
||||
} |
||||
} |
@ -0,0 +1,284 @@
|
||||
<?php |
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; |
||||
|
||||
class Price |
||||
{ |
||||
/** |
||||
* PRICE. |
||||
* |
||||
* Returns the price per $100 face value of a security that pays periodic interest. |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security settlement date is the date after the issue date when the security |
||||
* is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $rate the security's annual coupon rate |
||||
* @param mixed $yield the security's annual yield |
||||
* @param mixed $redemption The number of coupon payments per year. |
||||
* For annual payments, frequency = 1; |
||||
* for semiannual, frequency = 2; |
||||
* for quarterly, frequency = 4. |
||||
* @param mixed $frequency |
||||
* @param mixed $basis The type of day count to use. |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function price( |
||||
$settlement, |
||||
$maturity, |
||||
$rate, |
||||
$yield, |
||||
$redemption, |
||||
$frequency, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$yield = Functions::flattenSingleValue($yield); |
||||
$redemption = Functions::flattenSingleValue($redemption); |
||||
$frequency = Functions::flattenSingleValue($frequency); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
$rate = SecurityValidations::validateRate($rate); |
||||
$yield = SecurityValidations::validateYield($yield); |
||||
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
$frequency = SecurityValidations::validateFrequency($frequency); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$dsc = (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( |
||||
$settlement, |
||||
$maturity, |
||||
$discount, |
||||
$redemption, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$discount = Functions::flattenSingleValue($discount); |
||||
$redemption = Functions::flattenSingleValue($redemption); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
$discount = SecurityValidations::validateDiscount($discount); |
||||
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
// return date error |
||||
return $daysBetweenSettlementAndMaturity; |
||||
} |
||||
|
||||
return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); |
||||
} |
||||
|
||||
/** |
||||
* PRICEMAT. |
||||
* |
||||
* Returns the price per $100 face value of a security that pays interest at maturity. |
||||
* |
||||
* @param mixed $settlement The security's settlement date. |
||||
* The security's settlement date is the date after the issue date when the |
||||
* security is traded to the buyer. |
||||
* @param mixed $maturity The security's maturity date. |
||||
* The maturity date is the date when the security expires. |
||||
* @param mixed $issue The security's issue date |
||||
* @param mixed $rate The security's interest rate at date of issue |
||||
* @param mixed $yield The security's annual yield |
||||
* @param mixed $basis The type of day count to use. |
||||
* 0 or omitted US (NASD) 30/360 |
||||
* 1 Actual/actual |
||||
* 2 Actual/360 |
||||
* 3 Actual/365 |
||||
* 4 European 30/360 |
||||
* |
||||
* @return float|string Result, or a string containing an error |
||||
*/ |
||||
public static function priceAtMaturity( |
||||
$settlement, |
||||
$maturity, |
||||
$issue, |
||||
$rate, |
||||
$yield, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$issue = Functions::flattenSingleValue($issue); |
||||
$rate = Functions::flattenSingleValue($rate); |
||||
$yield = Functions::flattenSingleValue($yield); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
$issue = SecurityValidations::validateIssueDate($issue); |
||||
$rate = SecurityValidations::validateRate($rate); |
||||
$yield = SecurityValidations::validateYield($yield); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
$daysPerYear = 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( |
||||
$settlement, |
||||
$maturity, |
||||
$investment, |
||||
$discount, |
||||
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
) { |
||||
$settlement = Functions::flattenSingleValue($settlement); |
||||
$maturity = Functions::flattenSingleValue($maturity); |
||||
$investment = Functions::flattenSingleValue($investment); |
||||
$discount = Functions::flattenSingleValue($discount); |
||||
$basis = ($basis === null) |
||||
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
: Functions::flattenSingleValue($basis); |
||||
|
||||
try { |
||||
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
$investment = SecurityValidations::validateFloat($investment); |
||||
$discount = SecurityValidations::validateDiscount($discount); |
||||
$basis = SecurityValidations::validateBasis($basis); |
||||
} catch (Exception $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
|
||||
if ($investment <= 0) { |
||||
return ExcelError::NAN(); |
||||
} |
||||
$daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); |
||||
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
// return date error |
||||
return Functions::scalar($daysBetweenSettlementAndMaturity); |
||||
} |
||||
|
||||
return $investment / (1 - ($discount * $daysBetweenSettlementAndMaturity)); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue