package and depencies

This commit is contained in:
RafficMohammed
2023-01-08 02:57:24 +05:30
parent d5332eb421
commit 1d54b8bc7f
4309 changed files with 193331 additions and 172289 deletions

View File

@@ -0,0 +1,226 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('vendor')
->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;

View File

@@ -5,11 +5,469 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com)
and this project adheres to [Semantic Versioning](https://semver.org).
## Unreleased - TBD
## 1.26.0 - 2022-12-21
### Added
- Nothing.
- Extended flag options for the Reader `load()` and Writer `save()` methods
- Apply Row/Column limits (1048576 and XFD) in ReferenceHelper [PR #3213](https://github.com/PHPOffice/PhpSpreadsheet/pull/3213)
- Allow the creation of In-Memory Drawings from a string of binary image data, or from a stream. [PR #3157](https://github.com/PHPOffice/PhpSpreadsheet/pull/3157)
- Xlsx Reader support for Pivot Tables [PR #2829](https://github.com/PHPOffice/PhpSpreadsheet/pull/2829)
- Permit Date/Time Entered on Spreadsheet to be calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121)
### Changed
- Nothing
### Deprecated
- Direct update of Calculation::suppressFormulaErrors is replaced with setter.
- Font public static variable defaultColumnWidths replaced with constant DEFAULT_COLUMN_WIDTHS.
- ExcelError public static variable errorCodes replaced with constant ERROR_CODES.
- NumberFormat constant FORMAT_DATE_YYYYMMDD2 replaced with existing identical FORMAT_DATE_YYYYMMDD.
### Removed
- Nothing
### Fixed
- Fixed handling for `_xlws` prefixed functions from Office365 [Issue #3245](https://github.com/PHPOffice/PhpSpreadsheet/issues/3245) [PR #3247](https://github.com/PHPOffice/PhpSpreadsheet/pull/3247)
- Conditionals formatting rules applied to rows/columns are removed [Issue #3184](https://github.com/PHPOffice/PhpSpreadsheet/issues/3184) [PR #3213](https://github.com/PHPOffice/PhpSpreadsheet/pull/3213)
- Treat strings containing currency or accounting values as floats in Calculation Engine operations [Issue #3165](https://github.com/PHPOffice/PhpSpreadsheet/issues/3165) [PR #3189](https://github.com/PHPOffice/PhpSpreadsheet/pull/3189)
- Treat strings containing percentage values as floats in Calculation Engine operations [Issue #3155](https://github.com/PHPOffice/PhpSpreadsheet/issues/3155) [PR #3156](https://github.com/PHPOffice/PhpSpreadsheet/pull/3156) and [PR #3164](https://github.com/PHPOffice/PhpSpreadsheet/pull/3164)
- Xlsx Reader Accept Palette of Fewer than 64 Colors [Issue #3093](https://github.com/PHPOffice/PhpSpreadsheet/issues/3093) [PR #3096](https://github.com/PHPOffice/PhpSpreadsheet/pull/3096)
- Use Locale-Independent Float Conversion for Xlsx Writer Custom Property [Issue #3095](https://github.com/PHPOffice/PhpSpreadsheet/issues/3095) [PR #3099](https://github.com/PHPOffice/PhpSpreadsheet/pull/3099)
- Allow setting AutoFilter range on a single cell or row [Issue #3102](https://github.com/PHPOffice/PhpSpreadsheet/issues/3102) [PR #3111](https://github.com/PHPOffice/PhpSpreadsheet/pull/3111)
- Xlsx Reader External Data Validations Flag Missing [Issue #2677](https://github.com/PHPOffice/PhpSpreadsheet/issues/2677) [PR #3078](https://github.com/PHPOffice/PhpSpreadsheet/pull/3078)
- Reduces extra memory usage on `__destruct()` calls [PR #3092](https://github.com/PHPOffice/PhpSpreadsheet/pull/3092)
- Additional properties for Trendlines [Issue #3011](https://github.com/PHPOffice/PhpSpreadsheet/issues/3011) [PR #3028](https://github.com/PHPOffice/PhpSpreadsheet/pull/3028)
- Calculation suppressFormulaErrors fix [Issue #1531](https://github.com/PHPOffice/PhpSpreadsheet/issues/1531) [PR #3092](https://github.com/PHPOffice/PhpSpreadsheet/pull/3092)
- Permit Date/Time Entered on Spreadsheet to be Calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121)
- Incorrect Handling of Data Validation Formula Containing Ampersand [Issue #3145](https://github.com/PHPOffice/PhpSpreadsheet/issues/3145) [PR #3146](https://github.com/PHPOffice/PhpSpreadsheet/pull/3146)
- Xlsx Namespace Handling of Drawings, RowAndColumnAttributes, MergeCells [Issue #3138](https://github.com/PHPOffice/PhpSpreadsheet/issues/3138) [PR #3136](https://github.com/PHPOffice/PhpSpreadsheet/pull/3137)
- Generation3 Copy With Image in Footer [Issue #3126](https://github.com/PHPOffice/PhpSpreadsheet/issues/3126) [PR #3140](https://github.com/PHPOffice/PhpSpreadsheet/pull/3140)
- MATCH Function Problems with Int/Float Compare and Wildcards [Issue #3141](https://github.com/PHPOffice/PhpSpreadsheet/issues/3141) [PR #3142](https://github.com/PHPOffice/PhpSpreadsheet/pull/3142)
- Fix ODS Read Filter on number-columns-repeated cell [Issue #3148](https://github.com/PHPOffice/PhpSpreadsheet/issues/3148) [PR #3149](https://github.com/PHPOffice/PhpSpreadsheet/pull/3149)
- Problems Formatting Very Small and Very Large Numbers [Issue #3128](https://github.com/PHPOffice/PhpSpreadsheet/issues/3128) [PR #3152](https://github.com/PHPOffice/PhpSpreadsheet/pull/3152)
- XlsxWrite preserve line styles for y-axis, not just x-axis [PR #3163](https://github.com/PHPOffice/PhpSpreadsheet/pull/3163)
- Xlsx Namespace Handling of Drawings, RowAndColumnAttributes, MergeCells [Issue #3138](https://github.com/PHPOffice/PhpSpreadsheet/issues/3138) [PR #3137](https://github.com/PHPOffice/PhpSpreadsheet/pull/3137)
- More Detail for Cyclic Error Messages [Issue #3169](https://github.com/PHPOffice/PhpSpreadsheet/issues/3169) [PR #3170](https://github.com/PHPOffice/PhpSpreadsheet/pull/3170)
- Improved Documentation for Deprecations - many PRs [Issue #3162](https://github.com/PHPOffice/PhpSpreadsheet/issues/3162)
## 1.25.2 - 2022-09-25
### Added
- Nothing
### Changed
- Nothing
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Composer dependency clash with ezyang/htmlpurifier
## 1.25.0 - 2022-09-25
### Added
- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions
- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions
- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of
JpGraph library to render charts added.
- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes.
### Changed
- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065)
- Merge methods now allow an additional `$behaviour` argument. Permitted values are:
- Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour)
- Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
- Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
### Deprecated
- Axis getLineProperty deprecated in favor of getLineColorProperty.
- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated.
- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated.
- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed).
- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT.
- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead.
### Removed
- Nothing
### Fixed
- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077)
- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074)
- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013)
- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956)
- cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988)
- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951)
- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960)
- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971)
- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950)
- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976)
- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001)
- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994)
- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006)
- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016)
- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018)
- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048)
- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052)
- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053)
## 1.24.1 - 2022-07-18
### Added
- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0
- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940)
### Changed
- Nothing
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943)
- Additional Chart fixes
- Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928)
- Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930)
- Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933)
- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939)
## 1.24.0 - 2022-07-09
Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available.
### Added
- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files)
- Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801)
- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830)
Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet.
- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853)
- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850)
- Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792)
- Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842)
- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty":
- Treat rows/columns containing no cell records as empty (default)
- Treat cells containing a null value as empty
- Treat cells containing an empty string as empty
### Changed
- Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926)
- Better enforcement of value modification to match specified datatype when using `setValueExplicit()`
- Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776)
- Memory and speed improvements, particularly for the Cell Collection, and the Writers.
See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions
- Improved performance for removing rows/columns from a worksheet
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827)
- Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834)
- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849)
- Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772)
- Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788)
- Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813)
- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923)
- Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909)
- Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921)
- Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910)
- Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902)
- Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899)
## 1.23.0 - 2022-04-24
### Added
- Ods Writer support for Freeze Pane [Issue #2013](https://github.com/PHPOffice/PhpSpreadsheet/issues/2013) [PR #2755](https://github.com/PHPOffice/PhpSpreadsheet/pull/2755)
- Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753)
- Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts.
- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions.
- Implementation of the ISREF() Information function.
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
(i.e a value of "12,345.67" can be read as numeric `12345.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled.
This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators.
- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) [#2674](https://github.com/PHPOffice/PhpSpreadsheet/pull/2674)
- Limited support for Xls Reader to handle Conditional Formatting:
Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color.
- Add ability to suppress Mac line ending check for CSV [#2623](https://github.com/PHPOffice/PhpSpreadsheet/pull/2623)
- Initial support for creating and writing Tables (Xlsx Writer only) [PR #2671](https://github.com/PHPOffice/PhpSpreadsheet/pull/2671)
See `/samples/Table` for examples of use.
Note that PreCalculateFormulas needs to be disabled when saving spreadsheets containing tables with formulae (totals or column formulae).
### Changed
- Gnumeric Reader now loads number formatting for cells.
- Gnumeric Reader now correctly identifies selected worksheet and selected cells in a worksheet.
- Some Refactoring of the Ods Reader, moving all formula and address translation from Ods to Excel into a separate class to eliminate code duplication and ensure consistency.
- Make Boolean Conversion in Csv Reader locale-aware when using the String Value Binder.
This is determined by the Calculation Engine locale setting.
(i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.)
- Allow `psr/simple-cache` 2.x
### Deprecated
- All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
- Worksheet methods that reference cells "byColumnandRow". All such methods have an equivalent that references the cell by its address (e.g. '`E3'` rather than `5, 3`).
These functions now accept either a cell address string (`'E3')` or an array with columnId and rowId (`[5, 3]`) or a new `CellAddress` object as their `cellAddress`/`coordinate` argument.
This includes the methods:
- `setCellValueByColumnAndRow()` use the equivalent `setCellValue()`
- `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()`
- `getCellByColumnAndRow()` use the equivalent `getCell()`
- `cellExistsByColumnAndRow()` use the equivalent `cellExists()`
- `getStyleByColumnAndRow()` use the equivalent `getStyle()`
- `setBreakByColumnAndRow()` use the equivalent `setBreak()`
- `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()`
- `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()`
- `protectCellsByColumnAndRow()` use the equivalent `protectCells()`
- `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()`
- `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()`
- `freezePaneByColumnAndRow()` use the equivalent `freezePane()`
- `getCommentByColumnAndRow()` use the equivalent `getComment()`
- `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()`
This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method);
and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument.
### Removed
- Nothing
### Fixed
- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754)
- Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746)
- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689)
- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687)
- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address.
- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet.
- Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae.
- Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669)
- Fixed behaviour of XLSX font style vertical align settings [PR #2619](https://github.com/PHPOffice/PhpSpreadsheet/pull/2619)
- Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels.
Note that this method is used when translating Excel functions between `en_us` and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel).
Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic.
- Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651)
- Change open mode for output from `wb+` to `wb` [Issue #2372](https://github.com/PHPOffice/PhpSpreadsheet/issues/2372) [PR #2657](https://github.com/PHPOffice/PhpSpreadsheet/pull/2657)
- Use color palette if supplied [Issue #2499](https://github.com/PHPOffice/PhpSpreadsheet/issues/2499) [PR #2595](https://github.com/PHPOffice/PhpSpreadsheet/pull/2595)
- Xls reader treat drawing offsets as int rather than float [PR #2648](https://github.com/PHPOffice/PhpSpreadsheet/pull/2648)
- Handle booleans in conditional styles properly [PR #2654](https://github.com/PHPOffice/PhpSpreadsheet/pull/2654)
- Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default.
- Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739)
- Time Interval Formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772)
## 1.22.0 - 2022-02-18
### Added
- Namespacing phase 2 - styles.
[PR #2471](https://github.com/PHPOffice/PhpSpreadsheet/pull/2471)
- Improved support for passing of array arguments to Excel function implementations to return array results (where appropriate). [Issue #2551](https://github.com/PHPOffice/PhpSpreadsheet/issues/2551)
This is the first stage in an ongoing process of adding array support to all appropriate function implementations,
- Support for the Excel365 Math/Trig SEQUENCE() function [PR #2536](https://github.com/PHPOffice/PhpSpreadsheet/pull/2536)
- Support for the Excel365 Math/Trig RANDARRAY() function [PR #2540](https://github.com/PHPOffice/PhpSpreadsheet/pull/2540)
Note that the Spill Operator is not yet supported in the Calculation Engine; but this can still be useful for defining array constants.
- Improved support for Conditional Formatting Rules [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491)
- Provide support for a wider range of Conditional Formatting Rules for Xlsx Reader/Writer:
- Cells Containing (cellIs)
- Specific Text (containing, notContaining, beginsWith, endsWith)
- Dates Occurring (all supported timePeriods)
- Blanks/NoBlanks
- Errors/NoErrors
- Duplicates/Unique
- Expression
- Provision of CF Wizards (for all the above listed rule types) to help create/modify CF Rules without having to manage all the combinations of types/operators, and the complexities of formula expressions, or the text/timePeriod attributes.
See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/conditional-formatting/) for details
- Full support of the above CF Rules for the Xlsx Reader and Writer; even when the file being loaded has CF rules listed in the `<extLst><ext><ConditionalFormattings>` element for the worksheet rather than the `<ConditionalFormatting>` element.
- Provision of a CellMatcher to identify if rules are matched for a cell, and which matching style will be applied.
- Improved documentation and examples, covering all supported CF rule types.
- Add support for one digit decimals (FORMAT_NUMBER_0, FORMAT_PERCENTAGE_0). [PR #2525](https://github.com/PHPOffice/PhpSpreadsheet/pull/2525)
- Initial work enabling Excel function implementations for handling arrays as arguments when used in "array formulae" [#2562](https://github.com/PHPOffice/PhpSpreadsheet/issues/2562)
- Enable most of the Date/Time functions to accept array arguments [#2573](https://github.com/PHPOffice/PhpSpreadsheet/issues/2573)
- Array ready functions - Text, Math/Trig, Statistical, Engineering and Logical [#2580](https://github.com/PHPOffice/PhpSpreadsheet/issues/2580)
### Changed
- Additional Russian translations for Excel Functions (courtesy of aleks-samurai).
- Improved code coverage for NumberFormat. [PR #2556](https://github.com/PHPOffice/PhpSpreadsheet/pull/2556)
- Extract some methods from the Calculation Engine into dedicated classes [#2537](https://github.com/PHPOffice/PhpSpreadsheet/issues/2537)
- Eliminate calls to `flattenSingleValue()` that are no longer required when we're checking for array values as arguments [#2590](https://github.com/PHPOffice/PhpSpreadsheet/issues/2590)
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Fixed `ReferenceHelper@insertNewBefore` behavior when removing column before last column with null value [PR #2541](https://github.com/PHPOffice/PhpSpreadsheet/pull/2541)
- Fix bug with `DOLLARDE()` and `DOLLARFR()` functions when the dollar value is negative [Issue #2578](https://github.com/PHPOffice/PhpSpreadsheet/issues/2578) [PR #2579](https://github.com/PHPOffice/PhpSpreadsheet/pull/2579)
- Fix partial function name matching when translating formulae from Russian to English [Issue #2533](https://github.com/PHPOffice/PhpSpreadsheet/issues/2533) [PR #2534](https://github.com/PHPOffice/PhpSpreadsheet/pull/2534)
- Various bugs related to Conditional Formatting Rules, and errors in the Xlsx Writer for Conditional Formatting [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491)
- Xlsx Reader merge range fixes. [Issue #2501](https://github.com/PHPOffice/PhpSpreadsheet/issues/2501) [PR #2504](https://github.com/PHPOffice/PhpSpreadsheet/pull/2504)
- Handle explicit "date" type for Cell in Xlsx Reader. [Issue #2373](https://github.com/PHPOffice/PhpSpreadsheet/issues/2373) [PR #2485](https://github.com/PHPOffice/PhpSpreadsheet/pull/2485)
- Recalibrate Row/Column Dimensions after removeRow/Column. [Issue #2442](https://github.com/PHPOffice/PhpSpreadsheet/issues/2442) [PR #2486](https://github.com/PHPOffice/PhpSpreadsheet/pull/2486)
- Refinement for XIRR. [Issue #2469](https://github.com/PHPOffice/PhpSpreadsheet/issues/2469) [PR #2487](https://github.com/PHPOffice/PhpSpreadsheet/pull/2487)
- Xlsx Reader handle cell with non-null explicit type but null value. [Issue #2488](https://github.com/PHPOffice/PhpSpreadsheet/issues/2488) [PR #2489](https://github.com/PHPOffice/PhpSpreadsheet/pull/2489)
- Xlsx Reader fix height and width for oneCellAnchorDrawings. [PR #2492](https://github.com/PHPOffice/PhpSpreadsheet/pull/2492)
- Fix rounding error in NumberFormat::NUMBER_PERCENTAGE, NumberFormat::NUMBER_PERCENTAGE_00. [PR #2555](https://github.com/PHPOffice/PhpSpreadsheet/pull/2555)
- Don't treat thumbnail file as xml. [Issue #2516](https://github.com/PHPOffice/PhpSpreadsheet/issues/2516) [PR #2517](https://github.com/PHPOffice/PhpSpreadsheet/pull/2517)
- Eliminating Xlsx Reader warning when no sz tag for RichText. [Issue #2542](https://github.com/PHPOffice/PhpSpreadsheet/issues/2542) [PR #2550](https://github.com/PHPOffice/PhpSpreadsheet/pull/2550)
- Fix Xlsx/Xls Writer handling of inline strings. [Issue #353](https://github.com/PHPOffice/PhpSpreadsheet/issues/353) [PR #2569](https://github.com/PHPOffice/PhpSpreadsheet/pull/2569)
- Richtext colors were not being read correctly after namespace change [#2458](https://github.com/PHPOffice/PhpSpreadsheet/issues/2458)
- Fix discrepancy between the way markdown tables are rendered in ReadTheDocs and in PHPStorm [#2520](https://github.com/PHPOffice/PhpSpreadsheet/issues/2520)
- Update Russian Functions Text File [#2557](https://github.com/PHPOffice/PhpSpreadsheet/issues/2557)
- Fix documentation, instantiation example [#2564](https://github.com/PHPOffice/PhpSpreadsheet/issues/2564)
## 1.21.0 - 2022-01-06
### Added
- Ability to add a picture to the background of the comment. Supports four image formats: png, jpeg, gif, bmp. New `Comment::setSizeAsBackgroundImage()` to change the size of a comment to the size of a background image. [Issue #1547](https://github.com/PHPOffice/PhpSpreadsheet/issues/1547) [PR #2422](https://github.com/PHPOffice/PhpSpreadsheet/pull/2422)
- Ability to set default paper size and orientation [PR #2410](https://github.com/PHPOffice/PhpSpreadsheet/pull/2410)
- Ability to extend AutoFilter to Maximum Row [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414)
### Changed
- Xlsx Writer will evaluate AutoFilter only if it is as yet unevaluated, or has changed since it was last evaluated [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414)
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Rounding in `NumberFormatter` [Issue #2385](https://github.com/PHPOffice/PhpSpreadsheet/issues/2385) [PR #2399](https://github.com/PHPOffice/PhpSpreadsheet/pull/2399)
- Support for themes [Issue #2075](https://github.com/PHPOffice/PhpSpreadsheet/issues/2075) [Issue #2387](https://github.com/PHPOffice/PhpSpreadsheet/issues/2387) [PR #2403](https://github.com/PHPOffice/PhpSpreadsheet/pull/2403)
- Read spreadsheet with `#` in name [Issue #2405](https://github.com/PHPOffice/PhpSpreadsheet/issues/2405) [PR #2409](https://github.com/PHPOffice/PhpSpreadsheet/pull/2409)
- Improve PDF support for page size and orientation [Issue #1691](https://github.com/PHPOffice/PhpSpreadsheet/issues/1691) [PR #2410](https://github.com/PHPOffice/PhpSpreadsheet/pull/2410)
- Wildcard handling issues in text match [Issue #2430](https://github.com/PHPOffice/PhpSpreadsheet/issues/2430) [PR #2431](https://github.com/PHPOffice/PhpSpreadsheet/pull/2431)
- Respect DataType in `insertNewBefore` [PR #2433](https://github.com/PHPOffice/PhpSpreadsheet/pull/2433)
- Handle rows explicitly hidden after AutoFilter [Issue #1641](https://github.com/PHPOffice/PhpSpreadsheet/issues/1641) [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414)
- Special characters in image file name [Issue #1470](https://github.com/PHPOffice/PhpSpreadsheet/issues/1470) [Issue #2415](https://github.com/PHPOffice/PhpSpreadsheet/issues/2415) [PR #2416](https://github.com/PHPOffice/PhpSpreadsheet/pull/2416)
- Mpdf with very many styles [Issue #2432](https://github.com/PHPOffice/PhpSpreadsheet/issues/2432) [PR #2434](https://github.com/PHPOffice/PhpSpreadsheet/pull/2434)
- Name clashes between parsed and unparsed drawings [Issue #1767](https://github.com/PHPOffice/PhpSpreadsheet/issues/1767) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [PR #2423](https://github.com/PHPOffice/PhpSpreadsheet/pull/2423)
- Fill pattern start and end colors [Issue #2441](https://github.com/PHPOffice/PhpSpreadsheet/issues/2441) [PR #2444](https://github.com/PHPOffice/PhpSpreadsheet/pull/2444)
- General style specified in wrong case [Issue #2450](https://github.com/PHPOffice/PhpSpreadsheet/issues/2450) [PR #2451](https://github.com/PHPOffice/PhpSpreadsheet/pull/2451)
- Null passed to `AutoFilter::setRange()` [Issue #2281](https://github.com/PHPOffice/PhpSpreadsheet/issues/2281) [PR #2454](https://github.com/PHPOffice/PhpSpreadsheet/pull/2454)
- Another undefined index in Xls reader (#2470) [Issue #2463](https://github.com/PHPOffice/PhpSpreadsheet/issues/2463) [PR #2470](https://github.com/PHPOffice/PhpSpreadsheet/pull/2470)
- Allow single-cell checks on conditional styles, even when the style is configured for a range of cells (#) [PR #2483](https://github.com/PHPOffice/PhpSpreadsheet/pull/2483)
## 1.20.0 - 2021-11-23
### Added
- Xlsx Writer Support for WMF Files [#2339](https://github.com/PHPOffice/PhpSpreadsheet/issues/2339)
- Use standard temporary file for internal use of HTMLPurifier [#2383](https://github.com/PHPOffice/PhpSpreadsheet/issues/2383)
### Changed
- Drop support for PHP 7.2, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support
- Use native typing for objects that were already documented as such
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Fixed null conversation for strToUpper [#2292](https://github.com/PHPOffice/PhpSpreadsheet/issues/2292)
- Fixed Trying to access array offset on value of type null (Xls Reader) [#2315](https://github.com/PHPOffice/PhpSpreadsheet/issues/2315)
- Don't corrupt XLSX files containing data validation [#2377](https://github.com/PHPOffice/PhpSpreadsheet/issues/2377)
- Non-fixed cells were not updated if shared formula has a fixed cell [#2354](https://github.com/PHPOffice/PhpSpreadsheet/issues/2354)
- Declare key of generic ArrayObject
- CSV reader better support for boolean values [#2374](https://github.com/PHPOffice/PhpSpreadsheet/pull/2374)
- Some ZIP file could not be read [#2376](https://github.com/PHPOffice/PhpSpreadsheet/pull/2376)
- Fix regression were hyperlinks could not be read [#2391](https://github.com/PHPOffice/PhpSpreadsheet/pull/2391)
- AutoFilter Improvements [#2393](https://github.com/PHPOffice/PhpSpreadsheet/pull/2393)
- Don't corrupt file when using chart with fill color [#589](https://github.com/PHPOffice/PhpSpreadsheet/pull/589)
- Restore imperfect array formula values in xlsx writer [#2343](https://github.com/PHPOffice/PhpSpreadsheet/pull/2343)
- Restore explicit list of changes to PHPExcel migration document [#1546](https://github.com/PHPOffice/PhpSpreadsheet/issues/1546)
## 1.19.0 - 2021-10-31
### Added
- Ability to set style on named range, and validate input to setSelectedCells [Issue #2279](https://github.com/PHPOffice/PhpSpreadsheet/issues/2279) [PR #2280](https://github.com/PHPOffice/PhpSpreadsheet/pull/2280)
- Process comments in Sylk file [Issue #2276](https://github.com/PHPOffice/PhpSpreadsheet/issues/2276) [PR #2277](https://github.com/PHPOffice/PhpSpreadsheet/pull/2277)
- Addition of Custom Properties to Ods Writer, and 32-bit-safe timestamps for Document Properties [PR #2113](https://github.com/PHPOffice/PhpSpreadsheet/pull/2113)
- Added callback to CSV reader to set user-specified defaults for various properties (especially for escape which has a poor PHP-inherited default of backslash which does not correspond with Excel) [PR #2103](https://github.com/PHPOffice/PhpSpreadsheet/pull/2103)
- Phase 1 of better namespace handling for Xlsx, resolving many open issues [PR #2173](https://github.com/PHPOffice/PhpSpreadsheet/pull/2173) [PR #2204](https://github.com/PHPOffice/PhpSpreadsheet/pull/2204) [PR #2303](https://github.com/PHPOffice/PhpSpreadsheet/pull/2303)
- Add ability to extract images if source is a URL [Issue #1997](https://github.com/PHPOffice/PhpSpreadsheet/issues/1997) [PR #2072](https://github.com/PHPOffice/PhpSpreadsheet/pull/2072)
- Support for passing flags in the Reader `load()` and Writer `save()`methods, and through the IOFactory, to set behaviours [PR #2136](https://github.com/PHPOffice/PhpSpreadsheet/pull/2136)
- See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#readerwriter-flags) for details
- More flexibility in the StringValueBinder to determine what datatypes should be treated as strings [PR #2138](https://github.com/PHPOffice/PhpSpreadsheet/pull/2138)
- Helper class for conversion between css size Units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`) [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145)
- Allow Row height and Column Width to be set using different units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`), rather than only in points or MS Excel column width units [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145)
- Ability to stream to an Amazon S3 bucket [Issue #2249](https://github.com/PHPOffice/PhpSpreadsheet/issues/2249)
- Provided a Size Helper class to validate size values (pt, px, em) [PR #1694](https://github.com/PHPOffice/PhpSpreadsheet/pull/1694)
### Changed
@@ -17,7 +475,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Deprecated
- Nothing.
- PHP 8.1 will deprecate auto_detect_line_endings. As a result of this change, Csv Reader using some release after PHP8.1 will no longer be able to handle a Csv with Mac line endings.
### Removed
@@ -25,8 +483,145 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed
- Unexpected format in Xlsx Timestamp [Issue #2331](https://github.com/PHPOffice/PhpSpreadsheet/issues/2331) [PR #2332](https://github.com/PHPOffice/PhpSpreadsheet/pull/2332)
- Corrections for HLOOKUP [Issue #2123](https://github.com/PHPOffice/PhpSpreadsheet/issues/2123) [PR #2330](https://github.com/PHPOffice/PhpSpreadsheet/pull/2330)
- Corrections for Xlsx Read Comments [Issue #2316](https://github.com/PHPOffice/PhpSpreadsheet/issues/2316) [PR #2329](https://github.com/PHPOffice/PhpSpreadsheet/pull/2329)
- Lowercase Calibri font names [Issue #2273](https://github.com/PHPOffice/PhpSpreadsheet/issues/2273) [PR #2325](https://github.com/PHPOffice/PhpSpreadsheet/pull/2325)
- isFormula Referencing Sheet with Space in Title [Issue #2304](https://github.com/PHPOffice/PhpSpreadsheet/issues/2304) [PR #2306](https://github.com/PHPOffice/PhpSpreadsheet/pull/2306)
- Xls Reader Fatal Error due to Undefined Offset [Issue #1114](https://github.com/PHPOffice/PhpSpreadsheet/issues/1114) [PR #2308](https://github.com/PHPOffice/PhpSpreadsheet/pull/2308)
- Permit Csv Reader delimiter to be set to null [Issue #2287](https://github.com/PHPOffice/PhpSpreadsheet/issues/2287) [PR #2288](https://github.com/PHPOffice/PhpSpreadsheet/pull/2288)
- Csv Reader did not handle booleans correctly [PR #2232](https://github.com/PHPOffice/PhpSpreadsheet/pull/2232)
- Problems when deleting sheet with local defined name [Issue #2266](https://github.com/PHPOffice/PhpSpreadsheet/issues/2266) [PR #2284](https://github.com/PHPOffice/PhpSpreadsheet/pull/2284)
- Worksheet passwords were not always handled correctly [Issue #1897](https://github.com/PHPOffice/PhpSpreadsheet/issues/1897) [PR #2197](https://github.com/PHPOffice/PhpSpreadsheet/pull/2197)
- Gnumeric Reader will now distinguish between Created and Modified timestamp [PR #2133](https://github.com/PHPOffice/PhpSpreadsheet/pull/2133)
- Xls Reader will now handle MACCENTRALEUROPE with or without hyphen [Issue #549](https://github.com/PHPOffice/PhpSpreadsheet/issues/549) [PR #2213](https://github.com/PHPOffice/PhpSpreadsheet/pull/2213)
- Tweaks to input file validation [Issue #1718](https://github.com/PHPOffice/PhpSpreadsheet/issues/1718) [PR #2217](https://github.com/PHPOffice/PhpSpreadsheet/pull/2217)
- Html Reader did not handle comments correctly [Issue #2234](https://github.com/PHPOffice/PhpSpreadsheet/issues/2234) [PR #2235](https://github.com/PHPOffice/PhpSpreadsheet/pull/2235)
- Apache OpenOffice Uses Unexpected Case for General format [Issue #2239](https://github.com/PHPOffice/PhpSpreadsheet/issues/2239) [PR #2242](https://github.com/PHPOffice/PhpSpreadsheet/pull/2242)
- Problems with fraction formatting [Issue #2253](https://github.com/PHPOffice/PhpSpreadsheet/issues/2253) [PR #2254](https://github.com/PHPOffice/PhpSpreadsheet/pull/2254)
- Xlsx Reader had problems reading file with no styles.xml or empty styles.xml [Issue #2246](https://github.com/PHPOffice/PhpSpreadsheet/issues/2246) [PR #2247](https://github.com/PHPOffice/PhpSpreadsheet/pull/2247)
- Xlsx Reader did not read Data Validation flags correctly [Issue #2224](https://github.com/PHPOffice/PhpSpreadsheet/issues/2224) [PR #2225](https://github.com/PHPOffice/PhpSpreadsheet/pull/2225)
- Better handling of empty arguments in Calculation engine [PR #2143](https://github.com/PHPOffice/PhpSpreadsheet/pull/2143)
- Many fixes for Autofilter [Issue #2216](https://github.com/PHPOffice/PhpSpreadsheet/issues/2216) [PR #2141](https://github.com/PHPOffice/PhpSpreadsheet/pull/2141) [PR #2162](https://github.com/PHPOffice/PhpSpreadsheet/pull/2162) [PR #2218](https://github.com/PHPOffice/PhpSpreadsheet/pull/2218)
- Locale generator will now use Unix line endings even on Windows [Issue #2172](https://github.com/PHPOffice/PhpSpreadsheet/issues/2172) [PR #2174](https://github.com/PHPOffice/PhpSpreadsheet/pull/2174)
- Support differences in implementation of Text functions between Excel/Ods/Gnumeric [PR #2151](https://github.com/PHPOffice/PhpSpreadsheet/pull/2151)
- Fixes to places where PHP8.1 enforces new or previously unenforced restrictions [PR #2137](https://github.com/PHPOffice/PhpSpreadsheet/pull/2137) [PR #2191](https://github.com/PHPOffice/PhpSpreadsheet/pull/2191) [PR #2231](https://github.com/PHPOffice/PhpSpreadsheet/pull/2231)
- Clone for HashTable was incorrect [PR #2130](https://github.com/PHPOffice/PhpSpreadsheet/pull/2130)
- Xlsx Reader was not evaluating Document Security Lock correctly [PR #2128](https://github.com/PHPOffice/PhpSpreadsheet/pull/2128)
- Error in COUPNCD handling end of month [Issue #2116](https://github.com/PHPOffice/PhpSpreadsheet/issues/2116) [PR #2119](https://github.com/PHPOffice/PhpSpreadsheet/pull/2119)
- Xls Writer Parser did not handle concatenation operator correctly [PR #2080](https://github.com/PHPOffice/PhpSpreadsheet/pull/2080)
- Xlsx Writer did not handle boolean false correctly [Issue #2082](https://github.com/PHPOffice/PhpSpreadsheet/issues/2082) [PR #2087](https://github.com/PHPOffice/PhpSpreadsheet/pull/2087)
- SUM needs to treat invalid strings differently depending on whether they come from a cell or are used as literals [Issue #2042](https://github.com/PHPOffice/PhpSpreadsheet/issues/2042) [PR #2045](https://github.com/PHPOffice/PhpSpreadsheet/pull/2045)
- Html reader could have set illegal coordinates when dealing with embedded tables [Issue #2029](https://github.com/PHPOffice/PhpSpreadsheet/issues/2029) [PR #2032](https://github.com/PHPOffice/PhpSpreadsheet/pull/2032)
- Documentation for printing gridlines was wrong [PR #2188](https://github.com/PHPOffice/PhpSpreadsheet/pull/2188)
- Return Value Error - DatabaseAbstruct::buildQuery() return null but must be string [Issue #2158](https://github.com/PHPOffice/PhpSpreadsheet/issues/2158) [PR #2160](https://github.com/PHPOffice/PhpSpreadsheet/pull/2160)
- Xlsx reader not recognize data validations that references another sheet [Issue #1432](https://github.com/PHPOffice/PhpSpreadsheet/issues/1432) [Issue #2149](https://github.com/PHPOffice/PhpSpreadsheet/issues/2149) [PR #2150](https://github.com/PHPOffice/PhpSpreadsheet/pull/2150) [PR #2265](https://github.com/PHPOffice/PhpSpreadsheet/pull/2265)
- Don't calculate cell width for autosize columns if a cell contains a null or empty string value [Issue #2165](https://github.com/PHPOffice/PhpSpreadsheet/issues/2165) [PR #2167](https://github.com/PHPOffice/PhpSpreadsheet/pull/2167)
- Allow negative interest rate values in a number of the Financial functions (`PPMT()`, `PMT()`, `FV()`, `PV()`, `NPER()`, etc) [Issue #2163](https://github.com/PHPOffice/PhpSpreadsheet/issues/2163) [PR #2164](https://github.com/PHPOffice/PhpSpreadsheet/pull/2164)
- Xls Reader changing grey background to black in Excel template [Issue #2147](https://github.com/PHPOffice/PhpSpreadsheet/issues/2147) [PR #2156](https://github.com/PHPOffice/PhpSpreadsheet/pull/2156)
- Column width and Row height styles in the Html Reader when the value includes a unit of measure [Issue #2145](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145).
- Data Validation flags not set correctly when reading XLSX files [Issue #2224](https://github.com/PHPOffice/PhpSpreadsheet/issues/2224) [PR #2225](https://github.com/PHPOffice/PhpSpreadsheet/pull/2225)
- Reading XLSX files without styles.xml throws an exception [Issue #2246](https://github.com/PHPOffice/PhpSpreadsheet/issues/2246)
- Improved performance of `Style::applyFromArray()` when applied to several cells [PR #1785](https://github.com/PHPOffice/PhpSpreadsheet/issues/1785).
- Improve XLSX parsing speed if no readFilter is applied (again) - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772)
## 1.18.0 - 2021-05-31
### Added
- Enhancements to CSV Reader, allowing options to be set when using `IOFactory::load()` with a callback to set delimiter, enclosure, charset etc [PR #2103](https://github.com/PHPOffice/PhpSpreadsheet/pull/2103) - See [documentation](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/docs/topics/reading-and-writing-to-file.md#csv-comma-separated-values) for details.
- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053)
- Implemented basic AutoFiltering for Gnumeric Reader [PR #2055](https://github.com/PHPOffice/PhpSpreadsheet/pull/2055)
- Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028)
- Implemented URLENCODE() Web Function
- Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions.
- Support for ActiveSheet and SelectedCells in the ODS Reader and Writer [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908)
- Support for notContainsText Conditional Style in xlsx [Issue #984](https://github.com/PHPOffice/PhpSpreadsheet/issues/984)
### Changed
- Use of `nb` rather than `no` as the locale code for Norsk Bokmål.
### Deprecated
- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
### Removed
- Use of `nb` rather than `no` as the locale language code for Norsk Bokmål.
### Fixed
- Fixed error in COUPNCD() calculation for end of month [Issue #2116](https://github.com/PHPOffice/PhpSpreadsheet/issues/2116) - [PR #2119](https://github.com/PHPOffice/PhpSpreadsheet/pull/2119)
- Resolve default values when a null argument is passed for HLOOKUP(), VLOOKUP() and ADDRESS() functions [Issue #2120](https://github.com/PHPOffice/PhpSpreadsheet/issues/2120) - [PR #2121](https://github.com/PHPOffice/PhpSpreadsheet/pull/2121)
- Fixed incorrect R1C1 to A1 subtraction formula conversion (`R[-2]C-R[2]C`) [Issue #2076](https://github.com/PHPOffice/PhpSpreadsheet/pull/2076) [PR #2086](https://github.com/PHPOffice/PhpSpreadsheet/pull/2086)
- Correctly handle absolute A1 references when converting to R1C1 format [PR #2060](https://github.com/PHPOffice/PhpSpreadsheet/pull/2060)
- Correct default fill style for conditional without a pattern defined [Issue #2035](https://github.com/PHPOffice/PhpSpreadsheet/issues/2035) [PR #2050](https://github.com/PHPOffice/PhpSpreadsheet/pull/2050)
- Fixed issue where array key check for existince before accessing arrays in Xlsx.php [PR #1970](https://github.com/PHPOffice/PhpSpreadsheet/pull/1970)
- Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978)
- Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928)
- Fixed issue with _ spacing character in number format mask corrupting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927)
- Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save
- Fixed issue with Xlsx@listWorksheetInfo not returning any data
- Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640)
- Fix for [Issue #1916](https://github.com/PHPOffice/PhpSpreadsheet/issues/1916) - Invalid signature check for XML files
- Fix change in `Font::setSize()` behavior for PHP8 [PR #2100](https://github.com/PHPOffice/PhpSpreadsheet/pull/2100)
## 1.17.1 - 2021-03-01
### Added
- Implementation of the Excel `AVERAGEIFS()` functions as part of a restructuring of Database functions and Conditional Statistical functions.
- Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875)
- Support for booleans, and for wildcard text search in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876)
- Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754)
- Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796)
- Basic implementation of the PERMUTATIONA() Statistical Function
### Changed
- Formula functions that previously called PHP functions directly are now processed through the Excel Functions classes; resolving issues with PHP8 stricter typing. [#1789](https://github.com/PHPOffice/PhpSpreadsheet/issues/1789)
The following MathTrig functions are affected:
`ABS()`, `ACOS()`, `ACOSH()`, `ASIN()`, `ASINH()`, `ATAN()`, `ATANH()`,
`COS()`, `COSH()`, `DEGREES()` (rad2deg), `EXP()`, `LN()` (log), `LOG10()`,
`RADIANS()` (deg2rad), `SIN()`, `SINH()`, `SQRT()`, `TAN()`, `TANH()`.
One TextData function is also affected: `REPT()` (str_repeat).
- `formatAsDate` correctly matches language metadata, reverting c55272e
- Formulae that previously crashed on sub function call returning excel error value now return said value.
The following functions are affected `CUMPRINC()`, `CUMIPMT()`, `AMORLINC()`,
`AMORDEGRC()`.
- Adapt some function error return value to match excel's error.
The following functions are affected `PPMT()`, `IPMT()`.
### Deprecated
- Calling many of the Excel formula functions directly rather than through the Calculation Engine.
The logic for these Functions is now being moved out of the categorised `Database`, `DateTime`, `Engineering`, `Financial`, `Logical`, `LookupRef`, `MathTrig`, `Statistical`, `TextData` and `Web` classes into small, dedicated classes for individual functions or related groups of functions.
This makes the logic in these classes easier to maintain; and will reduce the memory footprint required to execute formulae when calling these functions.
### Removed
- Nothing.
### Fixed
- Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829)
- Fixed issue with Worksheet's `getCell()` method when trying to get a cell by defined name. [#1858](https://github.com/PHPOffice/PhpSpreadsheet/issues/1858)
- Fix possible endless loop in NumberFormat Masks [#1792](https://github.com/PHPOffice/PhpSpreadsheet/issues/1792)
- Fix problem resulting from literal dot inside quotes in number format masks [PR #1830](https://github.com/PHPOffice/PhpSpreadsheet/pull/1830)
- Resolve Google Sheets Xlsx charts issue. Google Sheets uses oneCellAnchor positioning and does not include *Cache values in the exported Xlsx [PR #1761](https://github.com/PHPOffice/PhpSpreadsheet/pull/1761)
- Fix for Xlsx Chart axis titles mapping to correct X or Y axis label when only one is present [PR #1760](https://github.com/PHPOffice/PhpSpreadsheet/pull/1760)
- Fix For Null Exception on ODS Read of Page Settings. [#1772](https://github.com/PHPOffice/PhpSpreadsheet/issues/1772)
- Fix Xlsx reader overriding manually set number format with builtin number format [PR #1805](https://github.com/PHPOffice/PhpSpreadsheet/pull/1805)
- Fix Xlsx reader cell alignment [PR #1710](https://github.com/PHPOffice/PhpSpreadsheet/pull/1710)
- Fix for not yet implemented data-types in Open Document writer [Issue #1674](https://github.com/PHPOffice/PhpSpreadsheet/issues/1674)
- Fix XLSX reader when having a corrupt numeric cell data type [PR #1664](https://github.com/phpoffice/phpspreadsheet/pull/1664)
- Fix on `CUMPRINC()`, `CUMIPMT()`, `AMORLINC()`, `AMORDEGRC()` usage. When those functions called one of `YEARFRAC()`, `PPMT()`, `IPMT()` and they would get back an error value (represented as a string), trying to use numeral operands (`+`, `/`, `-`, `*`) on said return value and a number (`float or `int`) would fail.
## 1.16.0 - 2020-12-31
### Added
@@ -39,7 +634,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Deprecated
- Nothing.
- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
### Removed
@@ -47,6 +642,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed
- Fixed issue with absolute path in worksheets' Target [PR #1769](https://github.com/PHPOffice/PhpSpreadsheet/pull/1769)
- Fix for Xls Reader when SST has a bad length [#1592](https://github.com/PHPOffice/PhpSpreadsheet/issues/1592)
- Resolve Xlsx loader issue whe hyperlinks don't have a destination
- Resolve issues when printer settings resources IDs clash with drawing IDs

View File

@@ -9,3 +9,12 @@ If you would like to contribute, here are some notes and guidelines:
- All code changes must be validated by `composer check`
- [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")
## 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 tag with `git push --tags`, GitHub Actions will create a GitHub release automatically

View File

@@ -11,6 +11,80 @@
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.23"
},
"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) (fork with php 8.1 support)
and then configure PhpSpreadsheet using:
```php
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph
//or
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph
```
One or the other of these libraries is necessary if you want to generate HTML or PDF files that include 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).

View File

@@ -1,7 +1,22 @@
{
"name": "phpoffice/phpspreadsheet",
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"keywords": ["PHP", "OpenXML", "Excel", "xlsx", "xls", "ods", "gnumeric", "spreadsheet"],
"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",
@@ -29,53 +44,58 @@
"check": [
"php-cs-fixer fix --ansi --dry-run --diff",
"phpcs",
"phpunit --color=always"
"phpunit --color=always",
"phpstan analyse --ansi"
],
"fix": [
"php-cs-fixer fix --ansi"
],
"versions": [
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.2- -n"
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.4- -n"
]
},
"require": {
"php": "^7.2||^8.0",
"php": "^7.4 || ^8.0",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-fileinfo": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-SimpleXML": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1",
"markbaker/complex": "^1.5||^2.0",
"markbaker/matrix": "^1.2||^2.0",
"psr/simple-cache": "^1.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"ezyang/htmlpurifier": "^4.13"
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dompdf/dompdf": "^0.8.5",
"friendsofphp/php-cs-fixer": "^2.16",
"jpgraph/jpgraph": "^4.0",
"mpdf/mpdf": "^8.0",
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.2.4",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^8.5||^9.3",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.3"
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.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 (doesn't yet support PHP8)",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)",
"jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers"
"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": {
@@ -84,7 +104,8 @@
},
"autoload-dev": {
"psr-4": {
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests"
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests",
"PhpOffice\\PhpSpreadsheetInfra\\": "infra"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
<?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,
];
$config['parameters']['ignoreErrors'][] = [
'message' => '#^Property PhpOffice\\PhpSpreadsheet\\Shared\\JAMA\\Matrix::$A (array) does not accept array<int, array<int, int>|false>|false.$#',
'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/JAMA/Matrix.php',
'count' => 2,
];
} 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' => 2,
];
}
return $config;

View File

@@ -0,0 +1,23 @@
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
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\.$~'

View File

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

View File

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

View File

@@ -17,4 +17,5 @@ abstract class Category
const CATEGORY_STATISTICAL = 'Statistical';
const CATEGORY_TEXT_AND_DATA = 'Text and Data';
const CATEGORY_WEB = 'Web';
const CATEGORY_UNCATEGORISED = 'Uncategorised';
}

View File

@@ -2,126 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
/**
* @deprecated 1.17.0
*
* @codeCoverageIgnore
*/
class Database
{
/**
* 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.
*
* @return null|string
*/
private static function fieldExtract($database, $field)
{
$field = strtoupper(Functions::flattenSingleValue($field));
$fieldNames = array_map('strtoupper', array_shift($database));
if (is_numeric($field)) {
$keys = array_keys($fieldNames);
return $keys[$field - 1];
}
$key = array_search($field, $fieldNames);
return ($key) ? $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 array of mixed
*/
private static function filter($database, $criteria)
{
$fieldNames = array_shift($database);
$criteriaNames = array_shift($criteria);
// Convert the criteria into a set of AND/OR conditions with [:placeholders]
$testConditions = $testValues = [];
$testConditionsCount = 0;
foreach ($criteriaNames as $key => $criteriaName) {
$testCondition = [];
$testConditionCount = 0;
foreach ($criteria as $row => $criterion) {
if ($criterion[$key] > '') {
$testCondition[] = '[:' . $criteriaName . ']' . Functions::ifCondition($criterion[$key]);
++$testConditionCount;
}
}
if ($testConditionCount > 1) {
$testConditions[] = 'OR(' . implode(',', $testCondition) . ')';
++$testConditionsCount;
} elseif ($testConditionCount == 1) {
$testConditions[] = $testCondition[0];
++$testConditionsCount;
}
}
if ($testConditionsCount > 1) {
$testConditionSet = 'AND(' . implode(',', $testConditions) . ')';
} elseif ($testConditionsCount == 1) {
$testConditionSet = $testConditions[0];
}
// Loop through each row of the database
foreach ($database as $dataRow => $dataValues) {
// Substitute actual values from the database row for our [:placeholders]
$testConditionList = $testConditionSet;
foreach ($criteriaNames as $key => $criteriaName) {
$k = array_search($criteriaName, $fieldNames);
if (isset($dataValues[$k])) {
$dataValue = $dataValues[$k];
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
$testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList);
}
}
// evaluate the criteria against the row data
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList);
// If the row failed to meet the criteria, remove it from the database
if (!$result) {
unset($database[$dataRow]);
}
}
return $database;
}
private static function getFilteredColumn($database, $field, $criteria)
{
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
// extract an array of values for the requested column
$colData = [];
foreach ($database as $row) {
$colData[] = $row[$field];
}
return $colData;
}
/**
* DAVERAGE.
*
@@ -130,6 +17,10 @@ class Database
* 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
@@ -149,15 +40,7 @@ class Database
*/
public static function DAVERAGE($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::AVERAGE(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DAverage::evaluate($database, $field, $criteria);
}
/**
@@ -169,14 +52,15 @@ class Database
* Excel Function:
* DCOUNT(database,[field],criteria)
*
* Excel Function:
* DAVERAGE(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 int|string $field Indicates which column is used in the function. Enter the
* @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
@@ -187,22 +71,14 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return int
* @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)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::COUNT(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DCount::evaluate($database, $field, $criteria);
}
/**
@@ -213,6 +89,10 @@ class Database
* 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
@@ -228,30 +108,11 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return int
*
* @TODO The field argument is optional. If field is omitted, DCOUNTA counts all records in the
* database that match the criteria.
* @return int|string
*/
public static function DCOUNTA($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
// extract an array of values for the requested column
$colData = [];
foreach ($database as $row) {
$colData[] = $row[$field];
}
// Return
return Statistical::COUNTA(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DCountA::evaluate($database, $field, $criteria);
}
/**
@@ -263,6 +124,10 @@ class Database
* 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
@@ -282,18 +147,7 @@ class Database
*/
public static function DGET($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
$colData = self::getFilteredColumn($database, $field, $criteria);
if (count($colData) > 1) {
return Functions::NAN();
}
return $colData[0];
return Database\DGet::evaluate($database, $field, $criteria);
}
/**
@@ -305,6 +159,10 @@ class Database
* 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
@@ -320,19 +178,11 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return float
* @return null|float|string
*/
public static function DMAX($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::MAX(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DMax::evaluate($database, $field, $criteria);
}
/**
@@ -344,6 +194,10 @@ class Database
* 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
@@ -359,19 +213,11 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return float
* @return null|float|string
*/
public static function DMIN($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::MIN(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DMin::evaluate($database, $field, $criteria);
}
/**
@@ -382,6 +228,10 @@ class Database
* 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
@@ -397,19 +247,11 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return float
* @return float|string
*/
public static function DPRODUCT($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return MathTrig::PRODUCT(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DProduct::evaluate($database, $field, $criteria);
}
/**
@@ -421,6 +263,10 @@ class Database
* 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
@@ -440,15 +286,7 @@ class Database
*/
public static function DSTDEV($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::STDEV(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DStDev::evaluate($database, $field, $criteria);
}
/**
@@ -460,6 +298,10 @@ class Database
* 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
@@ -479,15 +321,7 @@ class Database
*/
public static function DSTDEVP($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::STDEVP(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DStDevP::evaluate($database, $field, $criteria);
}
/**
@@ -498,6 +332,10 @@ class Database
* 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
@@ -513,19 +351,11 @@ class Database
* the column label in which you specify a condition for the
* column.
*
* @return float
* @return null|float|string
*/
public static function DSUM($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return MathTrig::SUM(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DSum::evaluate($database, $field, $criteria);
}
/**
@@ -537,6 +367,10 @@ class Database
* 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
@@ -556,15 +390,7 @@ class Database
*/
public static function DVAR($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::VARFunc(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DVar::evaluate($database, $field, $criteria);
}
/**
@@ -576,6 +402,10 @@ class Database
* 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
@@ -595,14 +425,6 @@ class Database
*/
public static function DVARP($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
// Return
return Statistical::VARP(
self::getFilteredColumn($database, $field, $criteria)
);
return Database\DVarP::evaluate($database, $field, $criteria);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,11 +48,11 @@ class Logger
/**
* Enable/Disable Calculation engine logging.
*
* @param bool $pValue
* @param bool $writeDebugLog
*/
public function setWriteDebugLog($pValue): void
public function setWriteDebugLog($writeDebugLog): void
{
$this->writeDebugLog = $pValue;
$this->writeDebugLog = $writeDebugLog;
}
/**
@@ -68,11 +68,11 @@ class Logger
/**
* Enable/Disable echoing of debug log information.
*
* @param bool $pValue
* @param bool $echoDebugLog
*/
public function setEchoDebugLog($pValue): void
public function setEchoDebugLog($echoDebugLog): void
{
$this->echoDebugLog = $pValue;
$this->echoDebugLog = $echoDebugLog;
}
/**
@@ -87,18 +87,20 @@ class Logger
/**
* Write an entry to the calculation engine debug log.
*
* @param mixed $args
*/
public function writeDebugLog(...$args): void
public function writeDebugLog(string $message, ...$args): void
{
// Only write the debug log if logging is enabled
if ($this->writeDebugLog) {
$message = implode('', $args);
$message = sprintf($message, ...$args);
$cellReference = implode(' -> ', $this->cellStack->showStack());
if ($this->echoDebugLog) {
echo $cellReference,
($this->cellStack->count() > 0 ? ' => ' : ''),
$message,
PHP_EOL;
($this->cellStack->count() > 0 ? ' => ' : ''),
$message,
PHP_EOL;
}
$this->debugLog[] = $cellReference .
($this->cellStack->count() > 0 ? ' => ' : '') .

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,14 @@
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 ConvertUOM
{
use ArrayEnabled;
public const CATEGORY_WEIGHT_AND_MASS = 'Weight and Mass';
public const CATEGORY_DISTANCE = 'Distance';
public const CATEGORY_TIME = 'Time';
@@ -103,6 +106,7 @@ class ConvertUOM
'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
@@ -490,7 +494,7 @@ class ConvertUOM
* getConversionMultipliers
* Returns an array of the Multiplier prefixes that can be used with Units of Measure in CONVERTUOM().
*
* @return array of mixed
* @return mixed[]
*/
public static function getConversionMultipliers()
{
@@ -501,7 +505,7 @@ class ConvertUOM
* getBinaryConversionMultipliers
* Returns an array of the additional Multiplier prefixes that can be used with Information Units of Measure in CONVERTUOM().
*
* @return array of mixed
* @return mixed[]
*/
public static function getBinaryConversionMultipliers()
{
@@ -518,33 +522,39 @@ class ConvertUOM
* Excel Function:
* CONVERT(value,fromUOM,toUOM)
*
* @param float|int $value the value in fromUOM to convert
* @param string $fromUOM the units for value
* @param string $toUOM the units for the result
* @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 float|string
* @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)
{
$value = Functions::flattenSingleValue($value);
$fromUOM = Functions::flattenSingleValue($fromUOM);
$toUOM = Functions::flattenSingleValue($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 Functions::VALUE();
return ExcelError::VALUE();
}
try {
[$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM);
[$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM);
} catch (Exception $e) {
return Functions::NA();
return ExcelError::NA();
}
if ($fromCategory !== $toCategory) {
return Functions::NA();
return ExcelError::NA();
}
// @var float $value
$value *= $fromMultiplier;
if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) {
@@ -554,7 +564,7 @@ class ConvertUOM
} elseif ($fromUOM === $toUOM) {
return $value / $toMultiplier;
} elseif ($fromCategory === self::CATEGORY_TEMPERATURE) {
return self::convertTemperature($fromUOM, $toUOM, $value);
return self::convertTemperature($fromUOM, $toUOM, /** @scrutinizer ignore-type */ $value);
}
$baseValue = $value * (1.0 / self::$unitConversions[$fromCategory][$fromUOM]);

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ class Exception extends PhpSpreadsheetException
* @param mixed $line
* @param mixed $context
*/
public static function errorHandlerCallback($code, $string, $file, $line, $context): void
public static function errorHandlerCallback($code, $string, $file, $line, /** @scrutinizer ignore-unused */ $context): void
{
$e = new self($string, $code);
$e->line = $line;

View File

@@ -9,7 +9,9 @@ class ExceptionHandler
*/
public function __construct()
{
set_error_handler([Exception::class, 'errorHandlerCallback'], E_ALL);
/** @var callable */
$callable = [Exception::class, 'errorHandlerCallback'];
set_error_handler($callable, E_ALL);
}
/**

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,17 +61,17 @@ class FormulaParser
/**
* Create a new FormulaParser.
*
* @param string $pFormula Formula to parse
* @param ?string $formula Formula to parse
*/
public function __construct($pFormula = '')
public function __construct($formula = '')
{
// Check parameters
if ($pFormula === null) {
if ($formula === null) {
throw new Exception('Invalid parameter passed: formula');
}
// Initialise values
$this->formula = trim($pFormula);
$this->formula = trim($formula);
// Parse!
$this->parseToTokens();
}
@@ -89,17 +89,15 @@ class FormulaParser
/**
* Get Token.
*
* @param int $pId Token id
*
* @return string
* @param int $id Token id
*/
public function getToken($pId = 0)
public function getToken(int $id = 0): FormulaToken
{
if (isset($this->tokens[$pId])) {
return $this->tokens[$pId];
if (isset($this->tokens[$id])) {
return $this->tokens[$id];
}
throw new Exception("Token with id $pId does not exist.");
throw new Exception("Token with id $id does not exist.");
}
/**
@@ -139,7 +137,8 @@ class FormulaParser
// Helper variables
$tokens1 = $tokens2 = $stack = [];
$inString = $inPath = $inRange = $inError = false;
$token = $previousToken = $nextToken = null;
$nextToken = null;
//$token = $previousToken = null;
$index = 1;
$value = '';
@@ -492,7 +491,7 @@ class FormulaParser
if (
!(
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
(($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
)
@@ -506,7 +505,7 @@ class FormulaParser
if (
!(
(($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
(($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
(($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
)
@@ -529,11 +528,11 @@ class FormulaParser
} else {
$previousToken = null;
}
if (isset($tokens2[$i + 1])) {
$nextToken = $tokens2[$i + 1];
} else {
$nextToken = null;
}
//if (isset($tokens2[$i + 1])) {
// $nextToken = $tokens2[$i + 1];
//} else {
// $nextToken = null;
//}
if ($token === null) {
continue;

View File

@@ -76,16 +76,16 @@ class FormulaToken
/**
* Create a new FormulaToken.
*
* @param string $pValue
* @param string $pTokenType Token type (represented by TOKEN_TYPE_*)
* @param string $pTokenSubType Token Subtype (represented by TOKEN_SUBTYPE_*)
* @param string $value
* @param string $tokenType Token type (represented by TOKEN_TYPE_*)
* @param string $tokenSubType Token Subtype (represented by TOKEN_SUBTYPE_*)
*/
public function __construct($pValue, $pTokenType = self::TOKEN_TYPE_UNKNOWN, $pTokenSubType = self::TOKEN_SUBTYPE_NOTHING)
public function __construct($value, $tokenType = self::TOKEN_TYPE_UNKNOWN, $tokenSubType = self::TOKEN_SUBTYPE_NOTHING)
{
// Initialise values
$this->value = $pValue;
$this->tokenType = $pTokenType;
$this->tokenSubType = $pTokenSubType;
$this->value = $value;
$this->tokenType = $tokenType;
$this->tokenSubType = $tokenSubType;
}
/**

View File

@@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Shared\Date;
class Functions
{
@@ -13,12 +14,13 @@ class Functions
*/
const M_2DIVPI = 0.63661977236758134307553505349006;
/** constants */
const COMPATIBILITY_EXCEL = 'Excel';
const COMPATIBILITY_GNUMERIC = 'Gnumeric';
const COMPATIBILITY_OPENOFFICE = 'OpenOfficeCalc';
/** Use of RETURNDATE_PHP_NUMERIC is discouraged - not 32-bit Y2038-safe, no timezone. */
const RETURNDATE_PHP_NUMERIC = 'P';
/** Use of RETURNDATE_UNIX_TIMESTAMP is discouraged - not 32-bit Y2038-safe, no timezone. */
const RETURNDATE_UNIX_TIMESTAMP = 'P';
const RETURNDATE_PHP_OBJECT = 'O';
const RETURNDATE_PHP_DATETIME_OBJECT = 'O';
@@ -38,30 +40,14 @@ class Functions
*/
protected static $returnDateType = self::RETURNDATE_EXCEL;
/**
* List of error codes.
*
* @var array
*/
protected static $errorCodes = [
'null' => '#NULL!',
'divisionbyzero' => '#DIV/0!',
'value' => '#VALUE!',
'reference' => '#REF!',
'name' => '#NAME?',
'num' => '#NUM!',
'na' => '#N/A',
'gettingdata' => '#GETTING_DATA',
];
/**
* Set the Compatibility Mode.
*
* @param string $compatibilityMode Compatibility Mode
* Permitted values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
* Permitted values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
*
* @return bool (Success or Failure)
*/
@@ -84,10 +70,10 @@ class Functions
* Return the current Compatibility Mode.
*
* @return string Compatibility Mode
* Possible Return values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
* Possible Return values are:
* Functions::COMPATIBILITY_EXCEL 'Excel'
* Functions::COMPATIBILITY_GNUMERIC 'Gnumeric'
* Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc'
*/
public static function getCompatibilityMode()
{
@@ -95,13 +81,13 @@ class Functions
}
/**
* Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object).
* Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP DateTime Object).
*
* @param string $returnDateType Return Date Format
* Permitted values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL 'E'
* Permitted values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL 'E'
*
* @return bool Success or failure
*/
@@ -124,10 +110,10 @@ class Functions
* Return the current Return Date Format for functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object).
*
* @return string Return Date Format
* Possible Return values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL 'E'
* Possible Return values are:
* Functions::RETURNDATE_UNIX_TIMESTAMP 'P'
* Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O'
* Functions::RETURNDATE_EXCEL ' 'E'
*/
public static function getReturnDateType()
{
@@ -144,92 +130,6 @@ class Functions
return '#Not Yet Implemented';
}
/**
* DIV0.
*
* @return string #Not Yet Implemented
*/
public static function DIV0()
{
return self::$errorCodes['divisionbyzero'];
}
/**
* NA.
*
* Excel Function:
* =NA()
*
* Returns the error value #N/A
* #N/A is the error value that means "no value is available."
*
* @return string #N/A!
*/
public static function NA()
{
return self::$errorCodes['na'];
}
/**
* NaN.
*
* Returns the error value #NUM!
*
* @return string #NUM!
*/
public static function NAN()
{
return self::$errorCodes['num'];
}
/**
* NAME.
*
* Returns the error value #NAME?
*
* @return string #NAME?
*/
public static function NAME()
{
return self::$errorCodes['name'];
}
/**
* REF.
*
* Returns the error value #REF!
*
* @return string #REF!
*/
public static function REF()
{
return self::$errorCodes['reference'];
}
/**
* NULL.
*
* Returns the error value #NULL!
*
* @return string #NULL!
*/
public static function null()
{
return self::$errorCodes['null'];
}
/**
* VALUE.
*
* Returns the error value #VALUE!
*
* @return string #VALUE!
*/
public static function VALUE()
{
return self::$errorCodes['value'];
}
public static function isMatrixValue($idx)
{
return (substr_count($idx, '.') <= 1) || (preg_match('/\.[A-Z]/', $idx) > 0);
@@ -237,7 +137,7 @@ class Functions
public static function isValue($idx)
{
return substr_count($idx, '.') == 0;
return substr_count($idx, '.') === 0;
}
public static function isCellValue($idx)
@@ -250,11 +150,17 @@ class Functions
$condition = self::flattenSingleValue($condition);
if ($condition === '') {
$condition = '=""';
return '=""';
}
if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) {
if (!is_numeric($condition)) {
if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) {
$condition = self::operandSpecialHandling($condition);
if (is_bool($condition)) {
return '=' . ($condition ? 'TRUE' : 'FALSE');
} elseif (!is_numeric($condition)) {
if ($condition !== '""') { // Not an empty string
// Escape any quotes in the string value
$condition = (string) preg_replace('/"/ui', '""', $condition);
}
$condition = Calculation::wrapResult(strtoupper($condition));
}
@@ -263,9 +169,10 @@ class Functions
preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches);
[, $operator, $operand] = $matches;
$operand = self::operandSpecialHandling($operand);
if (is_numeric(trim($operand, '"'))) {
$operand = trim($operand, '"');
} elseif (!is_numeric($operand)) {
} elseif (!is_numeric($operand) && $operand !== 'FALSE' && $operand !== 'TRUE') {
$operand = str_replace('"', '""', $operand);
$operand = Calculation::wrapResult(strtoupper($operand));
}
@@ -273,26 +180,147 @@ class Functions
return str_replace('""""', '""', $operator . $operand);
}
private static function operandSpecialHandling($operand)
{
if (is_numeric($operand) || is_bool($operand)) {
return $operand;
} elseif (strtoupper($operand) === Calculation::getTRUE() || strtoupper($operand) === Calculation::getFALSE()) {
return strtoupper($operand);
}
// Check for percentage
if (preg_match('/^\-?\d*\.?\d*\s?\%$/', $operand)) {
return ((float) rtrim($operand, '%')) / 100;
}
// Check for dates
if (($dateValueOperand = Date::stringToExcel($operand)) !== false) {
return $dateValueOperand;
}
return $operand;
}
/**
* NULL.
*
* Returns the error value #NULL!
*
* @deprecated 1.23.0 Use the null() method in the Information\ExcelError class instead
* @see Information\ExcelError::null()
*
* @return string #NULL!
*/
public static function null()
{
return Information\ExcelError::null();
}
/**
* NaN.
*
* Returns the error value #NUM!
*
* @deprecated 1.23.0 Use the NAN() method in the Information\Error class instead
* @see Information\ExcelError::NAN()
*
* @return string #NUM!
*/
public static function NAN()
{
return Information\ExcelError::NAN();
}
/**
* REF.
*
* Returns the error value #REF!
*
* @deprecated 1.23.0 Use the REF() method in the Information\ExcelError class instead
* @see Information\ExcelError::REF()
*
* @return string #REF!
*/
public static function REF()
{
return Information\ExcelError::REF();
}
/**
* NA.
*
* Excel Function:
* =NA()
*
* Returns the error value #N/A
* #N/A is the error value that means "no value is available."
*
* @deprecated 1.23.0 Use the NA() method in the Information\ExcelError class instead
* @see Information\ExcelError::NA()
*
* @return string #N/A!
*/
public static function NA()
{
return Information\ExcelError::NA();
}
/**
* VALUE.
*
* Returns the error value #VALUE!
*
* @deprecated 1.23.0 Use the VALUE() method in the Information\ExcelError class instead
* @see Information\ExcelError::VALUE()
*
* @return string #VALUE!
*/
public static function VALUE()
{
return Information\ExcelError::VALUE();
}
/**
* NAME.
*
* Returns the error value #NAME?
*
* @deprecated 1.23.0 Use the NAME() method in the Information\ExcelError class instead
* @see Information\ExcelError::NAME()
*
* @return string #NAME?
*/
public static function NAME()
{
return Information\ExcelError::NAME();
}
/**
* DIV0.
*
* @deprecated 1.23.0 Use the DIV0() method in the Information\ExcelError class instead
* @see Information\ExcelError::DIV0()
*
* @return string #Not Yet Implemented
*/
public static function DIV0()
{
return Information\ExcelError::DIV0();
}
/**
* ERROR_TYPE.
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the type() method in the Information\ExcelError class instead
* @see Information\ExcelError::type()
*
* @return array|int|string
*/
public static function errorType($value = '')
{
$value = self::flattenSingleValue($value);
$i = 1;
foreach (self::$errorCodes as $errorCode) {
if ($value === $errorCode) {
return $i;
}
++$i;
}
return self::NA();
return Information\ExcelError::type($value);
}
/**
@@ -300,15 +328,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isBlank() method in the Information\Value class instead
* @see Information\Value::isBlank()
*
* @return array|bool
*/
public static function isBlank($value = null)
{
if ($value !== null) {
$value = self::flattenSingleValue($value);
}
return $value === null;
return Information\Value::isBlank($value);
}
/**
@@ -316,13 +343,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isErr() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isErr()
*
* @return array|bool
*/
public static function isErr($value = '')
{
$value = self::flattenSingleValue($value);
return self::isError($value) && (!self::isNa(($value)));
return Information\ErrorValue::isErr($value);
}
/**
@@ -330,17 +358,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isError() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isError()
*
* @return array|bool
*/
public static function isError($value = '')
{
$value = self::flattenSingleValue($value);
if (!is_string($value)) {
return false;
}
return in_array($value, self::$errorCodes);
return Information\ErrorValue::isError($value);
}
/**
@@ -348,13 +373,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isNa() method in the Information\ErrorValue class instead
* @see Information\ErrorValue::isNa()
*
* @return array|bool
*/
public static function isNa($value = '')
{
$value = self::flattenSingleValue($value);
return $value === self::NA();
return Information\ErrorValue::isNa($value);
}
/**
@@ -362,19 +388,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool|string
* @deprecated 1.23.0 Use the isEven() method in the Information\Value class instead
* @see Information\Value::isEven()
*
* @return array|bool|string
*/
public static function isEven($value = null)
{
$value = self::flattenSingleValue($value);
if ($value === null) {
return self::NAME();
} elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) {
return self::VALUE();
}
return $value % 2 == 0;
return Information\Value::isEven($value);
}
/**
@@ -382,19 +403,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool|string
* @deprecated 1.23.0 Use the isOdd() method in the Information\Value class instead
* @see Information\Value::isOdd()
*
* @return array|bool|string
*/
public static function isOdd($value = null)
{
$value = self::flattenSingleValue($value);
if ($value === null) {
return self::NAME();
} elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) {
return self::VALUE();
}
return abs($value) % 2 == 1;
return Information\Value::isOdd($value);
}
/**
@@ -402,17 +418,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isNumber() method in the Information\Value class instead
* @see Information\Value::isNumber()
*
* @return array|bool
*/
public static function isNumber($value = null)
{
$value = self::flattenSingleValue($value);
if (is_string($value)) {
return false;
}
return is_numeric($value);
return Information\Value::isNumber($value);
}
/**
@@ -420,13 +433,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isLogical() method in the Information\Value class instead
* @see Information\Value::isLogical()
*
* @return array|bool
*/
public static function isLogical($value = null)
{
$value = self::flattenSingleValue($value);
return is_bool($value);
return Information\Value::isLogical($value);
}
/**
@@ -434,13 +448,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isText() method in the Information\Value class instead
* @see Information\Value::isText()
*
* @return array|bool
*/
public static function isText($value = null)
{
$value = self::flattenSingleValue($value);
return is_string($value) && !self::isError($value);
return Information\Value::isText($value);
}
/**
@@ -448,11 +463,14 @@ class Functions
*
* @param mixed $value Value to check
*
* @return bool
* @deprecated 1.23.0 Use the isNonText() method in the Information\Value class instead
* @see Information\Value::isNonText()
*
* @return array|bool
*/
public static function isNonText($value = null)
{
return !self::isText($value);
return Information\Value::isNonText($value);
}
/**
@@ -460,9 +478,12 @@ class Functions
*
* Returns a value converted to a number
*
* @deprecated 1.23.0 Use the asNumber() method in the Information\Value class instead
* @see Information\Value::asNumber()
*
* @param null|mixed $value The value you want converted
*
* @return number N converts values listed in the following table
* @return number|string N converts values listed in the following table
* If value is or refers to N returns
* A number That number
* A date The serial number of that date
@@ -473,27 +494,7 @@ class Functions
*/
public static function n($value = null)
{
while (is_array($value)) {
$value = array_shift($value);
}
switch (gettype($value)) {
case 'double':
case 'float':
case 'integer':
return $value;
case 'boolean':
return (int) $value;
case 'string':
// Errors
if ((strlen($value) > 0) && ($value[0] == '#')) {
return $value;
}
break;
}
return 0;
return Information\Value::asNumber($value);
}
/**
@@ -501,6 +502,9 @@ class Functions
*
* Returns a number that identifies the type of a value
*
* @deprecated 1.23.0 Use the type() method in the Information\Value class instead
* @see Information\Value::type()
*
* @param null|mixed $value The value you want tested
*
* @return number N converts values listed in the following table
@@ -513,45 +517,13 @@ class Functions
*/
public static function TYPE($value = null)
{
$value = self::flattenArrayIndexed($value);
if (is_array($value) && (count($value) > 1)) {
end($value);
$a = key($value);
// Range of cells is an error
if (self::isCellValue($a)) {
return 16;
// Test for Matrix
} elseif (self::isMatrixValue($a)) {
return 64;
}
} elseif (empty($value)) {
// Empty Cell
return 1;
}
$value = self::flattenSingleValue($value);
if (($value === null) || (is_float($value)) || (is_int($value))) {
return 1;
} elseif (is_bool($value)) {
return 4;
} elseif (is_array($value)) {
return 64;
} elseif (is_string($value)) {
// Errors
if ((strlen($value) > 0) && ($value[0] == '#')) {
return 16;
}
return 2;
}
return 0;
return Information\Value::type($value);
}
/**
* Convert a multi-dimensional array to a simple 1-dimensional array.
*
* @param array $array Array to be flattened
* @param array|mixed $array Array to be flattened
*
* @return array Flattened array
*/
@@ -561,30 +533,44 @@ class Functions
return (array) $array;
}
$arrayValues = [];
foreach ($array as $value) {
$flattened = [];
$stack = array_values($array);
while (!empty($stack)) {
$value = array_shift($stack);
if (is_array($value)) {
foreach ($value as $val) {
if (is_array($val)) {
foreach ($val as $v) {
$arrayValues[] = $v;
}
} else {
$arrayValues[] = $val;
}
}
array_unshift($stack, ...array_values($value));
} else {
$arrayValues[] = $value;
$flattened[] = $value;
}
}
return $arrayValues;
return $flattened;
}
/**
* @param mixed $value
*
* @return null|mixed
*/
public static function scalar($value)
{
if (!is_array($value)) {
return $value;
}
do {
$value = array_pop($value);
} while (is_array($value));
return $value;
}
/**
* Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing.
*
* @param array $array Array to be flattened
* @param array|mixed $array Array to be flattened
*
* @return array Flattened array
*/
@@ -633,26 +619,50 @@ class Functions
/**
* ISFORMULA.
*
* @param mixed $cellReference The cell to check
* @param Cell $pCell The current cell (containing this formula)
* @deprecated 1.23.0 Use the isFormula() method in the Information\Value class instead
* @see Information\Value::isFormula()
*
* @return bool|string
* @param mixed $cellReference The cell to check
* @param ?Cell $cell The current cell (containing this formula)
*
* @return array|bool|string
*/
public static function isFormula($cellReference = '', ?Cell $pCell = null)
public static function isFormula($cellReference = '', ?Cell $cell = null)
{
if ($pCell === null) {
return self::REF();
return Information\Value::isFormula($cellReference, $cell);
}
public static function expandDefinedName(string $coordinate, Cell $cell): string
{
$worksheet = $cell->getWorksheet();
$spreadsheet = $worksheet->getParent();
// Uppercase coordinate
$pCoordinatex = strtoupper($coordinate);
// Eliminate leading equal sign
$pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex);
$defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet);
if ($defined !== null) {
$worksheet2 = $defined->getWorkSheet();
if (!$defined->isFormula() && $worksheet2 !== null) {
$coordinate = "'" . $worksheet2->getTitle() . "'!" .
(string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue()));
}
}
preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches);
return $coordinate;
}
$cellReference = $matches[6] . $matches[7];
$worksheetName = str_replace("''", "'", trim($matches[2], "'"));
public static function trimTrailingRange(string $coordinate): string
{
return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate);
}
$worksheet = (!empty($worksheetName))
? $pCell->getWorksheet()->getParent()->getSheetByName($worksheetName)
: $pCell->getWorksheet();
public static function trimSheetFromCellReference(string $coordinate): string
{
if (strpos($coordinate, '!') !== false) {
$coordinate = substr($coordinate, strrpos($coordinate, '!') + 1);
}
return $worksheet->getCell($cellReference)->isFormula();
return $coordinate;
}
}

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