Skip to content

Extract some methods from the Calculation Engine into dedicated classes #2537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 25 additions & 35 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$str1 of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\BinaryComparison\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
count: 3
path: src/PhpSpreadsheet/Calculation/BinaryComparison.php

-
message: "#^Parameter \\#1 \\$str1 of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\BinaryComparison\\:\\:strcmpLowercaseFirst\\(\\) expects string\\|null, mixed given\\.$#"
count: 2
path: src/PhpSpreadsheet/Calculation/BinaryComparison.php

-
message: "#^Parameter \\#2 \\$str2 of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\BinaryComparison\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
count: 3
path: src/PhpSpreadsheet/Calculation/BinaryComparison.php

-
message: "#^Parameter \\#2 \\$str2 of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\BinaryComparison\\:\\:strcmpLowercaseFirst\\(\\) expects string\\|null, mixed given\\.$#"
count: 2
path: src/PhpSpreadsheet/Calculation/BinaryComparison.php

-
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 3
Expand Down Expand Up @@ -100,6 +120,11 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Cannot cast mixed to string\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Cannot use array destructuring on mixed\\.$#"
count: 6
Expand Down Expand Up @@ -235,36 +260,16 @@ parameters:
count: 2
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#1 \\$str1 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
count: 4
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#1 \\$str1 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpLowercaseFirst\\(\\) expects string\\|null, mixed given\\.$#"
count: 2
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#1 \\$textValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:strCaseReverse\\(\\) expects string, string\\|null given\\.$#"
count: 2
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#1 \\$worksheetName of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getSheetByName\\(\\) expects string, mixed given\\.$#"
count: 3
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#2 \\$str2 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
count: 4
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
count: 4
Expand All @@ -285,11 +290,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#4 \\$operation of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:executeBinaryComparisonOperation\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Parameter \\#4 \\$storeKey of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Token\\\\Stack\\:\\:push\\(\\) expects string\\|null, mixed given\\.$#"
count: 2
Expand Down Expand Up @@ -405,16 +405,6 @@ parameters:
count: 2
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Variable \\$inversedStr1 on left side of \\?\\? always exists and is not nullable\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Variable \\$inversedStr2 on left side of \\?\\? always exists and is not nullable\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/Calculation.php

-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Database\\:\\:DMAX\\(\\) should return float but returns float\\|string\\|null\\.$#"
count: 1
Expand Down
181 changes: 181 additions & 0 deletions src/PhpSpreadsheet/Calculation/BinaryComparison.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading