Skip to content

Commit 2a0ee7e

Browse files
author
MarkBaker
committed
Move binary comparisons out into a dedicated class
1 parent 2607917 commit 2a0ee7e

File tree

6 files changed

+288
-166
lines changed

6 files changed

+288
-166
lines changed

phpstan-baseline.neon

-35
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,16 @@ parameters:
235235
count: 2
236236
path: src/PhpSpreadsheet/Calculation/Calculation.php
237237

238-
-
239-
message: "#^Parameter \\#1 \\$str1 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
240-
count: 4
241-
path: src/PhpSpreadsheet/Calculation/Calculation.php
242-
243-
-
244-
message: "#^Parameter \\#1 \\$str1 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpLowercaseFirst\\(\\) expects string\\|null, mixed given\\.$#"
245-
count: 2
246-
path: src/PhpSpreadsheet/Calculation/Calculation.php
247-
248238
-
249239
message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
250240
count: 1
251241
path: src/PhpSpreadsheet/Calculation/Calculation.php
252242

253-
-
254-
message: "#^Parameter \\#1 \\$textValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:strCaseReverse\\(\\) expects string, string\\|null given\\.$#"
255-
count: 2
256-
path: src/PhpSpreadsheet/Calculation/Calculation.php
257-
258243
-
259244
message: "#^Parameter \\#1 \\$worksheetName of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getSheetByName\\(\\) expects string, mixed given\\.$#"
260245
count: 3
261246
path: src/PhpSpreadsheet/Calculation/Calculation.php
262247

263-
-
264-
message: "#^Parameter \\#2 \\$str2 of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:strcmpAllowNull\\(\\) expects string\\|null, mixed given\\.$#"
265-
count: 4
266-
path: src/PhpSpreadsheet/Calculation/Calculation.php
267-
268248
-
269249
message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
270250
count: 4
@@ -285,11 +265,6 @@ parameters:
285265
count: 1
286266
path: src/PhpSpreadsheet/Calculation/Calculation.php
287267

288-
-
289-
message: "#^Parameter \\#4 \\$operation of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:executeBinaryComparisonOperation\\(\\) expects string, mixed given\\.$#"
290-
count: 1
291-
path: src/PhpSpreadsheet/Calculation/Calculation.php
292-
293268
-
294269
message: "#^Parameter \\#4 \\$storeKey of method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Token\\\\Stack\\:\\:push\\(\\) expects string\\|null, mixed given\\.$#"
295270
count: 2
@@ -405,16 +380,6 @@ parameters:
405380
count: 2
406381
path: src/PhpSpreadsheet/Calculation/Calculation.php
407382

408-
-
409-
message: "#^Variable \\$inversedStr1 on left side of \\?\\? always exists and is not nullable\\.$#"
410-
count: 1
411-
path: src/PhpSpreadsheet/Calculation/Calculation.php
412-
413-
-
414-
message: "#^Variable \\$inversedStr2 on left side of \\?\\? always exists and is not nullable\\.$#"
415-
count: 1
416-
path: src/PhpSpreadsheet/Calculation/Calculation.php
417-
418383
-
419384
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Database\\:\\:DMAX\\(\\) should return float but returns float\\|string\\|null\\.$#"
420385
count: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
6+
7+
class BinaryComparison
8+
{
9+
/**
10+
* Epsilon Precision used for comparisons in calculations.
11+
*/
12+
private const DELTA = 0.1e-12;
13+
14+
/**
15+
* Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters.
16+
*
17+
* @param null|string $str1 First string value for the comparison
18+
* @param null|string $str2 Second string value for the comparison
19+
*/
20+
private static function strcmpLowercaseFirst($str1, $str2): int
21+
{
22+
$inversedStr1 = StringHelper::strCaseReverse($str1 ?? '');
23+
$inversedStr2 = StringHelper::strCaseReverse($str2 ?? '');
24+
25+
return strcmp($inversedStr1, $inversedStr2);
26+
}
27+
28+
/**
29+
* PHP8.1 deprecates passing null to strcmp.
30+
*
31+
* @param null|string $str1 First string value for the comparison
32+
* @param null|string $str2 Second string value for the comparison
33+
*/
34+
private static function strcmpAllowNull($str1, $str2): int
35+
{
36+
return strcmp($str1 ?? '', $str2 ?? '');
37+
}
38+
39+
/**
40+
* @param mixed $operand1
41+
* @param mixed $operand2
42+
*/
43+
public static function compare($operand1, $operand2, string $operator): bool
44+
{
45+
// Simple validate the two operands if they are string values
46+
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) {
47+
$operand1 = Calculation::unwrapResult($operand1);
48+
}
49+
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) {
50+
$operand2 = Calculation::unwrapResult($operand2);
51+
}
52+
53+
// Use case insensitive comparaison if not OpenOffice mode
54+
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
55+
if (is_string($operand1)) {
56+
$operand1 = StringHelper::strToUpper($operand1);
57+
}
58+
if (is_string($operand2)) {
59+
$operand2 = StringHelper::strToUpper($operand2);
60+
}
61+
}
62+
63+
$useLowercaseFirstComparison = is_string($operand1) &&
64+
is_string($operand2) &&
65+
Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE;
66+
67+
return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison);
68+
}
69+
70+
/**
71+
* @param mixed $operand1
72+
* @param mixed $operand2
73+
*/
74+
private static function evaluateComparison($operand1, $operand2, string $operator, bool $useLowercaseFirstComparison): bool
75+
{
76+
switch ($operator) {
77+
// Equality
78+
case '=':
79+
return self::equal($operand1, $operand2);
80+
// Greater than
81+
case '>':
82+
return self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison);
83+
// Less than
84+
case '<':
85+
return self::lessThan($operand1, $operand2, $useLowercaseFirstComparison);
86+
// Greater than or equal
87+
case '>=':
88+
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison);
89+
// Less than or equal
90+
case '<=':
91+
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison);
92+
// Inequality
93+
case '<>':
94+
return self::notEqual($operand1, $operand2);
95+
default:
96+
throw new Exception('Unsupported binary comparison operator');
97+
}
98+
}
99+
100+
/**
101+
* @param mixed $operand1
102+
* @param mixed $operand2
103+
*/
104+
private static function equal($operand1, $operand2): bool
105+
{
106+
if (is_numeric($operand1) && is_numeric($operand2)) {
107+
$result = (abs($operand1 - $operand2) < self::DELTA);
108+
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
109+
$result = $operand1 == $operand2;
110+
} else {
111+
$result = self::strcmpAllowNull($operand1, $operand2) == 0;
112+
}
113+
114+
return $result;
115+
}
116+
117+
/**
118+
* @param mixed $operand1
119+
* @param mixed $operand2
120+
*/
121+
private static function greaterThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool
122+
{
123+
if (is_numeric($operand1) && is_numeric($operand2)) {
124+
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 > $operand2));
125+
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
126+
$result = $operand1 >= $operand2;
127+
} elseif ($useLowercaseFirstComparison) {
128+
$result = self::strcmpLowercaseFirst($operand1, $operand2) >= 0;
129+
} else {
130+
$result = self::strcmpAllowNull($operand1, $operand2) >= 0;
131+
}
132+
133+
return $result;
134+
}
135+
136+
/**
137+
* @param mixed $operand1
138+
* @param mixed $operand2
139+
*/
140+
private static function lessThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool
141+
{
142+
if (is_numeric($operand1) && is_numeric($operand2)) {
143+
$result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 < $operand2));
144+
} elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) {
145+
$result = $operand1 <= $operand2;
146+
} elseif ($useLowercaseFirstComparison) {
147+
$result = self::strcmpLowercaseFirst($operand1, $operand2) <= 0;
148+
} else {
149+
$result = self::strcmpAllowNull($operand1, $operand2) <= 0;
150+
}
151+
152+
return $result;
153+
}
154+
155+
/**
156+
* @param mixed $operand1
157+
* @param mixed $operand2
158+
*/
159+
private static function greaterThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool
160+
{
161+
return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
162+
}
163+
164+
/**
165+
* @param mixed $operand1
166+
* @param mixed $operand2
167+
*/
168+
private static function lessThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool
169+
{
170+
return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true;
171+
}
172+
173+
/**
174+
* @param mixed $operand1
175+
* @param mixed $operand2
176+
*/
177+
private static function notEqual($operand1, $operand2): bool
178+
{
179+
return self::equal($operand1, $operand2) !== true;
180+
}
181+
}

0 commit comments

Comments
 (0)