Skip to content

Commit c928bfd

Browse files
authored
Merge pull request #3164 from PHPOffice/CalcEngine-Refactor_Formatted-Numbers
Refactoring for checks on strings containing formatted numeric values
2 parents 6f71efc + 6a50973 commit c928bfd

File tree

8 files changed

+311
-211
lines changed

8 files changed

+311
-211
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3030

3131
### Fixed
3232

33+
- 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)
3334
- 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)
3435
- 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)
3536
- 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)

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5110,7 +5110,7 @@ private function validateBinaryOperand(&$operand, &$stack)
51105110
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand));
51115111

51125112
return false;
5113-
} elseif (!Shared\StringHelper::convertToNumberIfFraction($operand) && !Shared\StringHelper::convertToNumberIfPercent($operand)) {
5113+
} elseif (Engine\FormattedNumber::convertToNumberIfFormatted($operand) === false) {
51145114
// If not a numeric, a fraction or a percentage, then it's a text string, and so can't be used in mathematical binary operations
51155115
$stack->push('Error', '#VALUE!');
51165116
$this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!'));
@@ -5120,7 +5120,7 @@ private function validateBinaryOperand(&$operand, &$stack)
51205120
}
51215121
}
51225122

5123-
// return a true if the value of the operand is one that we can use in normal binary operations
5123+
// return a true if the value of the operand is one that we can use in normal binary mathematical operations
51245124
return true;
51255125
}
51265126

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
7+
class FormattedNumber
8+
{
9+
/** Constants */
10+
/** Regular Expressions */
11+
// Fraction
12+
private const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
13+
14+
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';
15+
16+
private const STRING_CONVERSION_LIST = [
17+
[self::class, 'convertToNumberIfNumeric'],
18+
[self::class, 'convertToNumberIfFraction'],
19+
[self::class, 'convertToNumberIfPercent'],
20+
];
21+
22+
/**
23+
* Identify whether a string contains a formatted numeric value,
24+
* and convert it to a numeric if it is.
25+
*
26+
* @param string $operand string value to test
27+
*/
28+
public static function convertToNumberIfFormatted(string &$operand): bool
29+
{
30+
foreach (self::STRING_CONVERSION_LIST as $conversionMethod) {
31+
if ($conversionMethod($operand) === true) {
32+
return true;
33+
}
34+
}
35+
36+
return false;
37+
}
38+
39+
/**
40+
* Identify whether a string contains a numeric value,
41+
* and convert it to a numeric if it is.
42+
*
43+
* @param string $operand string value to test
44+
*/
45+
public static function convertToNumberIfNumeric(string &$operand): bool
46+
{
47+
if (is_numeric($operand)) {
48+
$operand = (float) $operand;
49+
50+
return true;
51+
}
52+
53+
return false;
54+
}
55+
56+
/**
57+
* Identify whether a string contains a fractional numeric value,
58+
* and convert it to a numeric if it is.
59+
*
60+
* @param string $operand string value to test
61+
*/
62+
public static function convertToNumberIfFraction(string &$operand): bool
63+
{
64+
if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) {
65+
$sign = ($match[1] === '-') ? '-' : '+';
66+
$wholePart = ($match[3] === '') ? '' : ($sign . $match[3]);
67+
$fractionFormula = '=' . $wholePart . $sign . $match[4];
68+
$operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula);
69+
70+
return true;
71+
}
72+
73+
return false;
74+
}
75+
76+
/**
77+
* Identify whether a string contains a percentage, and if so,
78+
* convert it to a numeric.
79+
*
80+
* @param string $operand string value to test
81+
*/
82+
public static function convertToNumberIfPercent(string &$operand): bool
83+
{
84+
$match = [];
85+
if (preg_match(self::STRING_REGEXP_PERCENT, $operand, $match, PREG_UNMATCHED_AS_NULL)) {
86+
//Calculate the percentage
87+
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
88+
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;
89+
90+
return true;
91+
}
92+
93+
return false;
94+
}
95+
}

src/PhpSpreadsheet/Shared/JAMA/Matrix.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\Engine\FormattedNumber;
56
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
67
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
78
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
8-
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
99

1010
/**
1111
* Matrix class.
@@ -1169,7 +1169,7 @@ private function validateExtractedValue($value, bool $validValues): array
11691169
}
11701170
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
11711171
$value = trim($value, '"');
1172-
$validValues &= StringHelper::convertToNumberIfFraction($value);
1172+
$validValues &= FormattedNumber::convertToNumberIfFormatted($value);
11731173
}
11741174

11751175
return [$value, $validValues];

src/PhpSpreadsheet/Shared/StringHelper.php

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,8 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Shared;
44

5-
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6-
75
class StringHelper
86
{
9-
/** Constants */
10-
/** Regular Expressions */
11-
// Fraction
12-
const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
13-
14-
const STRING_REGEXP_PERCENT = '~^(?:(?: *(?<PrefixedSign>[-+])? *\% *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i';
15-
167
/**
178
* Control characters array.
189
*
@@ -540,46 +531,6 @@ public static function strCaseReverse(string $textValue): string
540531
return implode('', $characters);
541532
}
542533

543-
/**
544-
* Identify whether a string contains a fractional numeric value,
545-
* and convert it to a numeric if it is.
546-
*
547-
* @param string $operand string value to test
548-
*/
549-
public static function convertToNumberIfFraction(string &$operand): bool
550-
{
551-
if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) {
552-
$sign = ($match[1] == '-') ? '-' : '+';
553-
$wholePart = ($match[3] === '') ? '' : ($sign . $match[3]);
554-
$fractionFormula = '=' . $wholePart . $sign . $match[4];
555-
$operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula);
556-
557-
return true;
558-
}
559-
560-
return false;
561-
}
562-
563-
/**
564-
* Identify whether a string contains a percentage, and if so,
565-
* convert it to a numeric.
566-
*
567-
* @param string $operand string value to test
568-
*/
569-
public static function convertToNumberIfPercent(string &$operand): bool
570-
{
571-
$match = [];
572-
if (preg_match(self::STRING_REGEXP_PERCENT, $operand, $match, PREG_UNMATCHED_AS_NULL)) {
573-
//Calculate the percentage
574-
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
575-
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;
576-
577-
return true;
578-
}
579-
580-
return false;
581-
}
582-
583534
/**
584535
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number
585536
* formatting information from locale.

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,30 @@ public function testCellWithFormulaTwoIndirect(): void
180180
self::assertEquals('9', $cell3->getCalculatedValue());
181181
}
182182

183+
public function testCellWithStringNumeric(): void
184+
{
185+
$spreadsheet = new Spreadsheet();
186+
$workSheet = $spreadsheet->getActiveSheet();
187+
$cell1 = $workSheet->getCell('A1');
188+
$cell1->setValue('+2.5');
189+
$cell2 = $workSheet->getCell('B1');
190+
$cell2->setValue('=100*A1');
191+
192+
self::assertSame(250.0, $cell2->getCalculatedValue());
193+
}
194+
195+
public function testCellWithStringFraction(): void
196+
{
197+
$spreadsheet = new Spreadsheet();
198+
$workSheet = $spreadsheet->getActiveSheet();
199+
$cell1 = $workSheet->getCell('A1');
200+
$cell1->setValue('3/4');
201+
$cell2 = $workSheet->getCell('B1');
202+
$cell2->setValue('=100*A1');
203+
204+
self::assertSame(75.0, $cell2->getCalculatedValue());
205+
}
206+
183207
public function testCellWithStringPercentage(): void
184208
{
185209
$spreadsheet = new Spreadsheet();
@@ -189,7 +213,7 @@ public function testCellWithStringPercentage(): void
189213
$cell2 = $workSheet->getCell('B1');
190214
$cell2->setValue('=100*A1');
191215

192-
self::assertEquals('2', $cell2->getCalculatedValue());
216+
self::assertSame(2.0, $cell2->getCalculatedValue());
193217
}
194218

195219
public function testBranchPruningFormulaParsingSimpleCase(): void

0 commit comments

Comments
 (0)