From 4134ff246ab9477997dbd29f3eb1b739554359e3 Mon Sep 17 00:00:00 2001 From: oleibman Date: Tue, 26 Jan 2021 13:55:06 -0800 Subject: [PATCH] Problems Using Builtin PHP Functions Directly As Excel Functions (#1799) * Problems Using Builtin PHP Functions Directly As Excel Functions This fixes issue #1789. As originally reported, stricter typing was causing PHP8 to throw an exception when a non-numeric value was passed to the Round function. Previous releases of PHP did not see this problem, however, on further analysis, they were also incorrect in returning 0 as the result in the erroneous situation, when they should have been returning a VALUE error. Yet more analysis showed that other functions would also have problems, and, in addition, might not handle invalid input (e.g. a negative length passed to REPT) or output (e.g. NAN in the case of ACOS(2)) correctly. The following MathTrig functions are affected: ABS, ACOS, ACOSH, ASIN, ASINH, ATAN, ATANH, COS, COSH, DEGREES (rad2deg), EXP, LN (log), LOG10, RADIANS (deg2rad), REPT (str_repeat), SIN, SINH, SQRT, TAN, TANH. One TextData function (REPT) is also affected. This change lets PhpSpreadsheet validate the input for each of these functions before passing control to the builtin, and handle the output afterwards. There were no explicit tests for any of these functions, a fact made easy to ignore by the fact that PhpSpreadsheet delegated the heavy lifting to PHP itself for these cases. A full suite of tests is now added for each of the affected functions. * Scrutinizer Recommendations Only in 3 modules which are part of this PR. * Improved Handling of Tan(PI/2) Return DIV0 error for TAN when COS is very small. * Additional Trig Tests Results which should be infinity, i.e. DIV/0 error. --- .../Calculation/Calculation.php | 42 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 437 +++++++++++++++++- src/PhpSpreadsheet/Calculation/TextData.php | 23 +- .../Functions/MathTrig/AbsTest.php | 36 ++ .../Functions/MathTrig/AcosTest.php | 36 ++ .../Functions/MathTrig/AcoshTest.php | 36 ++ .../Functions/MathTrig/AsinTest.php | 36 ++ .../Functions/MathTrig/AsinhTest.php | 36 ++ .../Functions/MathTrig/AtanTest.php | 36 ++ .../Functions/MathTrig/AtanhTest.php | 36 ++ .../Functions/MathTrig/CosTest.php | 36 ++ .../Functions/MathTrig/CoshTest.php | 36 ++ .../Functions/MathTrig/DegreesTest.php | 36 ++ .../Functions/MathTrig/ExpTest.php | 36 ++ .../Calculation/Functions/MathTrig/LnTest.php | 36 ++ .../Functions/MathTrig/Log10Test.php | 36 ++ .../Functions/MathTrig/RadiansTest.php | 36 ++ .../Functions/MathTrig/RoundTest.php | 40 ++ .../Functions/MathTrig/SinTest.php | 36 ++ .../Functions/MathTrig/SinhTest.php | 36 ++ .../Functions/MathTrig/SqrtTest.php | 36 ++ .../Functions/MathTrig/TanTest.php | 36 ++ .../Functions/MathTrig/TanhTest.php | 36 ++ .../Functions/TextData/ReptTest.php | 40 ++ tests/data/Calculation/MathTrig/ABS.php | 12 + tests/data/Calculation/MathTrig/ACOS.php | 10 + tests/data/Calculation/MathTrig/ACOSH.php | 10 + tests/data/Calculation/MathTrig/ASIN.php | 10 + tests/data/Calculation/MathTrig/ASINH.php | 12 + tests/data/Calculation/MathTrig/ATAN.php | 12 + tests/data/Calculation/MathTrig/ATANH.php | 11 + tests/data/Calculation/MathTrig/COS.php | 12 + tests/data/Calculation/MathTrig/COSH.php | 11 + tests/data/Calculation/MathTrig/COT.php | 8 + tests/data/Calculation/MathTrig/CSC.php | 8 + tests/data/Calculation/MathTrig/DEGREES.php | 7 + tests/data/Calculation/MathTrig/EXP.php | 7 + tests/data/Calculation/MathTrig/LN.php | 11 + tests/data/Calculation/MathTrig/LOG10.php | 12 + tests/data/Calculation/MathTrig/RADIANS.php | 7 + tests/data/Calculation/MathTrig/ROUND.php | 11 + tests/data/Calculation/MathTrig/SEC.php | 8 + tests/data/Calculation/MathTrig/SIN.php | 11 + tests/data/Calculation/MathTrig/SINH.php | 10 + tests/data/Calculation/MathTrig/SQRT.php | 10 + tests/data/Calculation/MathTrig/TAN.php | 13 + tests/data/Calculation/MathTrig/TANH.php | 10 + tests/data/Calculation/TextData/REPT.php | 11 + 48 files changed, 1482 insertions(+), 28 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php create mode 100644 tests/data/Calculation/MathTrig/ABS.php create mode 100644 tests/data/Calculation/MathTrig/ACOS.php create mode 100644 tests/data/Calculation/MathTrig/ACOSH.php create mode 100644 tests/data/Calculation/MathTrig/ASIN.php create mode 100644 tests/data/Calculation/MathTrig/ASINH.php create mode 100644 tests/data/Calculation/MathTrig/ATAN.php create mode 100644 tests/data/Calculation/MathTrig/ATANH.php create mode 100644 tests/data/Calculation/MathTrig/COS.php create mode 100644 tests/data/Calculation/MathTrig/COSH.php create mode 100644 tests/data/Calculation/MathTrig/DEGREES.php create mode 100644 tests/data/Calculation/MathTrig/EXP.php create mode 100644 tests/data/Calculation/MathTrig/LN.php create mode 100644 tests/data/Calculation/MathTrig/LOG10.php create mode 100644 tests/data/Calculation/MathTrig/RADIANS.php create mode 100644 tests/data/Calculation/MathTrig/ROUND.php create mode 100644 tests/data/Calculation/MathTrig/SIN.php create mode 100644 tests/data/Calculation/MathTrig/SINH.php create mode 100644 tests/data/Calculation/MathTrig/SQRT.php create mode 100644 tests/data/Calculation/MathTrig/TAN.php create mode 100644 tests/data/Calculation/MathTrig/TANH.php create mode 100644 tests/data/Calculation/TextData/REPT.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 99260e3bf1..204b75604a 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -228,7 +228,7 @@ class Calculation private static $phpSpreadsheetFunctions = [ 'ABS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'abs', + 'functionCall' => [MathTrig::class, 'builtinABS'], 'argumentCount' => '1', ], 'ACCRINT' => [ @@ -243,12 +243,12 @@ class Calculation ], 'ACOS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'acos', + 'functionCall' => [MathTrig::class, 'builtinACOS'], 'argumentCount' => '1', ], 'ACOSH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'acosh', + 'functionCall' => [MathTrig::class, 'builtinACOSH'], 'argumentCount' => '1', ], 'ACOT' => [ @@ -303,17 +303,17 @@ class Calculation ], 'ASIN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'asin', + 'functionCall' => [MathTrig::class, 'builtinASIN'], 'argumentCount' => '1', ], 'ASINH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'asinh', + 'functionCall' => [MathTrig::class, 'builtinASINH'], 'argumentCount' => '1', ], 'ATAN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'atan', + 'functionCall' => [MathTrig::class, 'builtinATAN'], 'argumentCount' => '1', ], 'ATAN2' => [ @@ -323,7 +323,7 @@ class Calculation ], 'ATANH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'atanh', + 'functionCall' => [MathTrig::class, 'builtinATANH'], 'argumentCount' => '1', ], 'AVEDEV' => [ @@ -604,12 +604,12 @@ class Calculation ], 'COS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'cos', + 'functionCall' => [MathTrig::class, 'builtinCOS'], 'argumentCount' => '1', ], 'COSH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'cosh', + 'functionCall' => [MathTrig::class, 'builtinCOSH'], 'argumentCount' => '1', ], 'COT' => [ @@ -834,7 +834,7 @@ class Calculation ], 'DEGREES' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'rad2deg', + 'functionCall' => [MathTrig::class, 'builtinDEGREES'], 'argumentCount' => '1', ], 'DELTA' => [ @@ -974,7 +974,7 @@ class Calculation ], 'EXP' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'exp', + 'functionCall' => [MathTrig::class, 'builtinEXP'], 'argumentCount' => '1', ], 'EXPONDIST' => [ @@ -1565,7 +1565,7 @@ class Calculation ], 'LN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'log', + 'functionCall' => [MathTrig::class, 'builtinLN'], 'argumentCount' => '1', ], 'LOG' => [ @@ -1575,7 +1575,7 @@ class Calculation ], 'LOG10' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'log10', + 'functionCall' => [MathTrig::class, 'builtinLOG10'], 'argumentCount' => '1', ], 'LOGEST' => [ @@ -2037,7 +2037,7 @@ class Calculation ], 'RADIANS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'deg2rad', + 'functionCall' => [MathTrig::class, 'builtinRADIANS'], 'argumentCount' => '1', ], 'RAND' => [ @@ -2092,7 +2092,7 @@ class Calculation ], 'REPT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => 'str_repeat', + 'functionCall' => [TextData::class, 'builtinREPT'], 'argumentCount' => '2', ], 'RIGHT' => [ @@ -2112,7 +2112,7 @@ class Calculation ], 'ROUND' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'round', + 'functionCall' => [MathTrig::class, 'builtinROUND'], 'argumentCount' => '2', ], 'ROUNDDOWN' => [ @@ -2203,12 +2203,12 @@ class Calculation ], 'SIN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'sin', + 'functionCall' => [MathTrig::class, 'builtinSIN'], 'argumentCount' => '1', ], 'SINH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'sinh', + 'functionCall' => [MathTrig::class, 'builtinSINH'], 'argumentCount' => '1', ], 'SKEW' => [ @@ -2248,7 +2248,7 @@ class Calculation ], 'SQRT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'sqrt', + 'functionCall' => [MathTrig::class, 'builtinSQRT'], 'argumentCount' => '1', ], 'SQRTPI' => [ @@ -2364,12 +2364,12 @@ class Calculation ], 'TAN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'tan', + 'functionCall' => [MathTrig::class, 'builtinTAN'], 'argumentCount' => '1', ], 'TANH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => 'tanh', + 'functionCall' => [MathTrig::class, 'builtinTANH'], 'argumentCount' => '1', ], 'TBILLEQ' => [ diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index b8449b55bf..9ce4a75244 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -39,6 +39,13 @@ private static function romanCut($num, $n) return ($num - ($num % $n)) / $n; } + private static function strSplit(string $roman): array + { + $rslt = str_split($roman); + + return is_array($rslt) ? $rslt : []; + } + /** * ARABIC. * @@ -66,7 +73,7 @@ public static function ARABIC($roman) } try { - $arabic = self::calculateArabic(str_split($roman)); + $arabic = self::calculateArabic(self::strSplit($roman)); } catch (Exception $e) { return Functions::VALUE(); // Invalid character detected } @@ -1666,7 +1673,7 @@ public static function SEC($angle) $result = cos($angle); - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return self::verySmallDivisor($result) ? Functions::DIV0() : (1 / $result); } /** @@ -1710,7 +1717,7 @@ public static function CSC($angle) $result = sin($angle); - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return self::verySmallDivisor($result) ? Functions::DIV0() : (1 / $result); } /** @@ -1752,9 +1759,9 @@ public static function COT($angle) return Functions::VALUE(); } - $result = tan($angle); + $result = sin($angle); - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return self::verySmallDivisor($result) ? Functions::DIV0() : (cos($angle) / $result); } /** @@ -1799,6 +1806,18 @@ public static function ACOT($number) return (M_PI / 2) - atan($number); } + /** + * Return NAN or value depending on argument. + * + * @param float $result Number + * + * @return float|string + */ + public static function numberOrNan($result) + { + return is_nan($result) ? Functions::NAN() : $result; + } + /** * ACOTH. * @@ -1818,6 +1837,412 @@ public static function ACOTH($number) $result = log(($number + 1) / ($number - 1)) / 2; - return is_nan($result) ? Functions::NAN() : $result; + return self::numberOrNan($result); + } + + /** + * ROUND. + * + * Returns the result of builtin function round after validating args. + * + * @param mixed $number Should be numeric + * @param mixed $precision Should be int + * + * @return float|string Rounded number + */ + public static function builtinROUND($number, $precision) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number) || !is_numeric($precision)) { + return Functions::VALUE(); + } + + return round($number, $precision); + } + + /** + * ABS. + * + * Returns the result of builtin function abs after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|int|string Rounded number + */ + public static function builtinABS($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return abs($number); + } + + /** + * ACOS. + * + * Returns the result of builtin function acos after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinACOS($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::numberOrNan(acos($number)); + } + + /** + * ACOSH. + * + * Returns the result of builtin function acosh after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinACOSH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::numberOrNan(acosh($number)); + } + + /** + * ASIN. + * + * Returns the result of builtin function asin after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinASIN($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::numberOrNan(asin($number)); + } + + /** + * ASINH. + * + * Returns the result of builtin function asinh after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinASINH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return asinh($number); + } + + /** + * ASIN. + * + * Returns the result of builtin function atan after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinATAN($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::numberOrNan(atan($number)); + } + + /** + * ATANH. + * + * Returns the result of builtin function atanh after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinATANH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return atanh($number); + } + + /** + * COS. + * + * Returns the result of builtin function cos after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinCOS($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return cos($number); + } + + /** + * COSH. + * + * Returns the result of builtin function cos after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinCOSH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return cosh($number); + } + + /** + * DEGREES. + * + * Returns the result of builtin function rad2deg after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinDEGREES($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return rad2deg($number); + } + + /** + * EXP. + * + * Returns the result of builtin function exp after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinEXP($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return exp($number); + } + + /** + * LN. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinLN($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return log($number); + } + + /** + * LOG10. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinLOG10($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return log10($number); + } + + /** + * RADIANS. + * + * Returns the result of builtin function deg2rad after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinRADIANS($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return deg2rad($number); + } + + /** + * SIN. + * + * Returns the result of builtin function sin after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinSIN($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return sin($number); + } + + /** + * SINH. + * + * Returns the result of builtin function sinh after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinSINH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return sinh($number); + } + + /** + * SQRT. + * + * Returns the result of builtin function sqrt after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinSQRT($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::numberOrNan(sqrt($number)); + } + + /** + * TAN. + * + * Returns the result of builtin function tan after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinTAN($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return self::verySmallDivisor(cos($number)) ? Functions::DIV0() : tan($number); + } + + /** + * TANH. + * + * Returns the result of builtin function sinh after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function builtinTANH($number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number)) { + return Functions::VALUE(); + } + + return tanh($number); + } + + private static function verySmallDivisor(float $number): bool + { + return abs($number) < 1.0E-12; } } diff --git a/src/PhpSpreadsheet/Calculation/TextData.php b/src/PhpSpreadsheet/Calculation/TextData.php index f89744029e..163756649f 100644 --- a/src/PhpSpreadsheet/Calculation/TextData.php +++ b/src/PhpSpreadsheet/Calculation/TextData.php @@ -162,7 +162,7 @@ public static function DOLLAR($value = 0, $decimals = 2) if (!is_numeric($value) || !is_numeric($decimals)) { return Functions::VALUE(); } - $decimals = floor($decimals); + $decimals = (int) $decimals; $mask = '$#,##0'; if ($decimals > 0) { @@ -673,4 +673,25 @@ public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args) return implode($delimiter, $aArgs); } + + /** + * REPT. + * + * Returns the result of builtin function round after validating args. + * + * @param string $str Should be numeric + * @param mixed $number Should be int + * + * @return string + */ + public static function builtinREPT($str, $number) + { + $number = Functions::flattenSingleValue($number); + + if (!is_numeric($number) || $number < 0) { + return Functions::VALUE(); + } + + return str_repeat($str, $number); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php new file mode 100644 index 0000000000..4981602411 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ABS()'; + } else { + $formula = "=ABS($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerAbs() + { + return require 'tests/data/Calculation/MathTrig/ABS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php new file mode 100644 index 0000000000..825626da2f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ACOS()'; + } else { + $formula = "=ACOS($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAcos() + { + return require 'tests/data/Calculation/MathTrig/ACOS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php new file mode 100644 index 0000000000..bda64d0398 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ACOSH()'; + } else { + $formula = "=ACOSH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAcosh() + { + return require 'tests/data/Calculation/MathTrig/ACOSH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php new file mode 100644 index 0000000000..1edc1c3396 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ASIN()'; + } else { + $formula = "=ASIN($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAsin() + { + return require 'tests/data/Calculation/MathTrig/ASIN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php new file mode 100644 index 0000000000..1621eb7913 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ASINH()'; + } else { + $formula = "=ASINH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAsinh() + { + return require 'tests/data/Calculation/MathTrig/ASINH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php new file mode 100644 index 0000000000..50d7696744 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ATAN()'; + } else { + $formula = "=ATAN($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAtan() + { + return require 'tests/data/Calculation/MathTrig/ATAN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php new file mode 100644 index 0000000000..2863a182a6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=ATANH()'; + } else { + $formula = "=ATANH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerAtanh() + { + return require 'tests/data/Calculation/MathTrig/ATANH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php new file mode 100644 index 0000000000..da7a9a158c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=COS()'; + } else { + $formula = "=COS($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerCos() + { + return require 'tests/data/Calculation/MathTrig/COS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php new file mode 100644 index 0000000000..2c452bd5c1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=COSH()'; + } else { + $formula = "=COSH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerCosh() + { + return require 'tests/data/Calculation/MathTrig/COSH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php new file mode 100644 index 0000000000..3f92703b14 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=DEGREES()'; + } else { + $formula = "=DEGREES($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerDegrees() + { + return require 'tests/data/Calculation/MathTrig/DEGREES.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php new file mode 100644 index 0000000000..89bc0097da --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=EXP()'; + } else { + $formula = "=EXP($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerEXP() + { + return require 'tests/data/Calculation/MathTrig/EXP.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php new file mode 100644 index 0000000000..1910ef0290 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=LN()'; + } else { + $formula = "=LN($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerLN() + { + return require 'tests/data/Calculation/MathTrig/LN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php new file mode 100644 index 0000000000..e537030cc2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=LOG10()'; + } else { + $formula = "=LOG10($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerLN() + { + return require 'tests/data/Calculation/MathTrig/LOG10.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php new file mode 100644 index 0000000000..b584954079 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=RADIANS()'; + } else { + $formula = "=RADIANS($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerRADIANS() + { + return require 'tests/data/Calculation/MathTrig/RADIANS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php new file mode 100644 index 0000000000..3cd353a492 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php @@ -0,0 +1,40 @@ +expectException(CalcExp::class); + $formula = '=ROUND()'; + } elseif ($precision === null) { + $this->expectException(CalcExp::class); + $formula = "=ROUND($val)"; + } else { + $formula = "=ROUND($val, $precision)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerRound() + { + return require 'tests/data/Calculation/MathTrig/ROUND.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php new file mode 100644 index 0000000000..7a144e0e32 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=SIN()'; + } else { + $formula = "=SIN($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerSin() + { + return require 'tests/data/Calculation/MathTrig/SIN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php new file mode 100644 index 0000000000..c24bb19283 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=SINH()'; + } else { + $formula = "=SINH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerCosh() + { + return require 'tests/data/Calculation/MathTrig/SINH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php new file mode 100644 index 0000000000..972035e70d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=SQRT()'; + } else { + $formula = "=SQRT($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerSqrt() + { + return require 'tests/data/Calculation/MathTrig/SQRT.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php new file mode 100644 index 0000000000..13093f6a79 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=TAN()'; + } else { + $formula = "=TAN($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerTan() + { + return require 'tests/data/Calculation/MathTrig/TAN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php new file mode 100644 index 0000000000..69f28e8a64 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php @@ -0,0 +1,36 @@ +expectException(CalcExp::class); + $formula = '=TANH()'; + } else { + $formula = "=TANH($val)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + } + + public function providerTanh() + { + return require 'tests/data/Calculation/MathTrig/TANH.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php new file mode 100644 index 0000000000..cb4f1586d6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php @@ -0,0 +1,40 @@ +expectException(CalcExp::class); + $formula = '=REPT()'; + } elseif ($rpt === null) { + $this->expectException(CalcExp::class); + $formula = "=REPT($val)"; + } else { + $formula = "=REPT($val, $rpt)"; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($formula); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerREPT() + { + return require 'tests/data/Calculation/TextData/REPT.php'; + } +} diff --git a/tests/data/Calculation/MathTrig/ABS.php b/tests/data/Calculation/MathTrig/ABS.php new file mode 100644 index 0000000000..2fc9631bf4 --- /dev/null +++ b/tests/data/Calculation/MathTrig/ABS.php @@ -0,0 +1,12 @@ +