Skip to content

Commit 6f71efc

Browse files
authored
Merge pull request #3156 from fdjohnston/master
Convert percentages stored as strings to numerics in formula calculations
2 parents 8a06e1b + 04fb3bb commit 6f71efc

File tree

4 files changed

+163
-3
lines changed

4 files changed

+163
-3
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5110,8 +5110,8 @@ 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)) {
5114-
// If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
5113+
} elseif (!Shared\StringHelper::convertToNumberIfFraction($operand) && !Shared\StringHelper::convertToNumberIfPercent($operand)) {
5114+
// 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!'));
51175117

src/PhpSpreadsheet/Shared/StringHelper.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class StringHelper
1111
// Fraction
1212
const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';
1313

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+
1416
/**
1517
* Control characters array.
1618
*
@@ -558,7 +560,25 @@ public static function convertToNumberIfFraction(string &$operand): bool
558560
return false;
559561
}
560562

561-
// function convertToNumberIfFraction()
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+
}
562582

563583
/**
564584
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

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

183+
public function testCellWithStringPercentage(): void
184+
{
185+
$spreadsheet = new Spreadsheet();
186+
$workSheet = $spreadsheet->getActiveSheet();
187+
$cell1 = $workSheet->getCell('A1');
188+
$cell1->setValue('2%');
189+
$cell2 = $workSheet->getCell('B1');
190+
$cell2->setValue('=100*A1');
191+
192+
self::assertEquals('2', $cell2->getCalculatedValue());
193+
}
194+
183195
public function testBranchPruningFormulaParsingSimpleCase(): void
184196
{
185197
$calculation = Calculation::getInstance();

tests/PhpSpreadsheetTests/Shared/StringHelperTest.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,132 @@ public function providerFractions(): array
148148
'improper fraction' => ['1.75', '7/4'],
149149
];
150150
}
151+
152+
/**
153+
* @dataProvider providerPercentages
154+
*/
155+
public function testPercentage(string $expected, string $value): void
156+
{
157+
$originalValue = $value;
158+
$result = StringHelper::convertToNumberIfPercent($value);
159+
if ($result === false) {
160+
self::assertSame($expected, $originalValue);
161+
self::assertSame($expected, $value);
162+
} else {
163+
self::assertSame($expected, (string) $value);
164+
self::assertNotEquals($value, $originalValue);
165+
}
166+
}
167+
168+
public function providerPercentages(): array
169+
{
170+
return [
171+
'non-percentage' => ['10', '10'],
172+
'single digit percentage' => ['0.02', '2%'],
173+
'two digit percentage' => ['0.13', '13%'],
174+
'negative single digit percentage' => ['-0.07', '-7%'],
175+
'negative two digit percentage' => ['-0.75', '-75%'],
176+
'large percentage' => ['98.45', '9845%'],
177+
'small percentage' => ['0.0005', '0.05%'],
178+
'percentage with decimals' => ['0.025', '2.5%'],
179+
'trailing percent with space' => ['0.02', '2 %'],
180+
'trailing percent with leading and trailing space' => ['0.02', ' 2 % '],
181+
'leading percent with decimals' => ['0.025', ' % 2.5'],
182+
183+
//These should all fail
184+
'percent only' => ['%', '%'],
185+
'nonsense percent' => ['2%2', '2%2'],
186+
'negative leading percent' => ['-0.02', '-%2'],
187+
188+
//Percent position permutations
189+
'permutation_1' => ['0.02', '2%'],
190+
'permutation_2' => ['0.02', ' 2%'],
191+
'permutation_3' => ['0.02', '2% '],
192+
'permutation_4' => ['0.02', ' 2 % '],
193+
'permutation_5' => ['0.0275', '2.75% '],
194+
'permutation_6' => ['0.0275', ' 2.75% '],
195+
'permutation_7' => ['0.0275', ' 2.75 % '],
196+
'permutation_8' => [' 2 . 75 %', ' 2 . 75 %'],
197+
'permutation_9' => [' 2.7 5 % ', ' 2.7 5 % '],
198+
'permutation_10' => ['-0.02', '-2%'],
199+
'permutation_11' => ['-0.02', ' -2% '],
200+
'permutation_12' => ['-0.02', '- 2% '],
201+
'permutation_13' => ['-0.02', '-2 % '],
202+
'permutation_14' => ['-0.0275', '-2.75% '],
203+
'permutation_15' => ['-0.0275', ' -2.75% '],
204+
'permutation_16' => ['-0.0275', '-2.75 % '],
205+
'permutation_17' => ['-0.0275', ' - 2.75 % '],
206+
'permutation_18' => ['0.02', '2%'],
207+
'permutation_19' => ['0.02', '% 2 '],
208+
'permutation_20' => ['0.02', ' %2 '],
209+
'permutation_21' => ['0.02', ' % 2 '],
210+
'permutation_22' => ['0.0275', '%2.75 '],
211+
'permutation_23' => ['0.0275', ' %2.75 '],
212+
'permutation_24' => ['0.0275', ' % 2.75 '],
213+
'permutation_25' => [' %2 . 75 ', ' %2 . 75 '],
214+
'permutation_26' => [' %2.7 5 ', ' %2.7 5 '],
215+
'permutation_27' => [' % 2 . 75 ', ' % 2 . 75 '],
216+
'permutation_28' => [' % 2.7 5 ', ' % 2.7 5 '],
217+
'permutation_29' => ['-0.0275', '-%2.75 '],
218+
'permutation_30' => ['-0.0275', ' - %2.75 '],
219+
'permutation_31' => ['-0.0275', '- % 2.75 '],
220+
'permutation_32' => ['-0.0275', ' - % 2.75 '],
221+
'permutation_33' => ['0.02', '2%'],
222+
'permutation_34' => ['0.02', '2 %'],
223+
'permutation_35' => ['0.02', ' 2%'],
224+
'permutation_36' => ['0.02', ' 2 % '],
225+
'permutation_37' => ['0.0275', '2.75%'],
226+
'permutation_38' => ['0.0275', ' 2.75 % '],
227+
'permutation_39' => ['2 . 75 % ', '2 . 75 % '],
228+
'permutation_40' => ['-0.0275', '-2.75% '],
229+
'permutation_41' => ['-0.0275', '- 2.75% '],
230+
'permutation_42' => ['-0.0275', ' - 2.75% '],
231+
'permutation_43' => ['-0.0275', ' -2.75 % '],
232+
'permutation_44' => ['-2. 75 % ', '-2. 75 % '],
233+
'permutation_45' => ['%', '%'],
234+
'permutation_46' => ['0.02', '%2 '],
235+
'permutation_47' => ['0.02', '% 2 '],
236+
'permutation_48' => ['0.02', ' %2 '],
237+
'permutation_49' => ['0.02', '% 2 '],
238+
'permutation_50' => ['0.02', ' % 2 '],
239+
'permutation_51' => ['0.02', ' 2 % '],
240+
'permutation_52' => ['-0.02', '-2%'],
241+
'permutation_53' => ['-0.02', '- %2'],
242+
'permutation_54' => ['-0.02', ' -%2 '],
243+
'permutation_55' => ['2%2', '2%2'],
244+
'permutation_56' => [' 2% %', ' 2% %'],
245+
'permutation_57' => [' % 2 -', ' % 2 -'],
246+
'permutation_58' => ['-0.02', '%-2'],
247+
'permutation_59' => ['-0.02', ' % - 2'],
248+
'permutation_60' => ['-0.0275', '%-2.75 '],
249+
'permutation_61' => ['-0.0275', ' % - 2.75 '],
250+
'permutation_62' => ['-0.0275', ' % - 2.75 '],
251+
'permutation_63' => ['-0.0275', ' % - 2.75 '],
252+
'permutation_64' => ['0.0275', ' % + 2.75 '],
253+
'permutation_65' => ['0.0275', ' % + 2.75 '],
254+
'permutation_66' => ['0.0275', ' % + 2.75 '],
255+
'permutation_67' => ['0.02', '+2%'],
256+
'permutation_68' => ['0.02', ' +2% '],
257+
'permutation_69' => ['0.02', '+ 2% '],
258+
'permutation_70' => ['0.02', '+2 % '],
259+
'permutation_71' => ['0.0275', '+2.75% '],
260+
'permutation_72' => ['0.0275', ' +2.75% '],
261+
'permutation_73' => ['0.0275', '+2.75 % '],
262+
'permutation_74' => ['0.0275', ' + 2.75 % '],
263+
'permutation_75' => ['-2.5E-6', '-2.5E-4%'],
264+
'permutation_76' => ['200', '2E4%'],
265+
'permutation_77' => ['-2.5E-8', '-%2.50E-06'],
266+
'permutation_78' => [' - % 2.50 E -06 ', ' - % 2.50 E -06 '],
267+
'permutation_79' => ['-2.5E-8', ' - % 2.50E-06 '],
268+
'permutation_80' => [' - % 2.50E- 06 ', ' - % 2.50E- 06 '],
269+
'permutation_81' => [' - % 2.50E - 06 ', ' - % 2.50E - 06 '],
270+
'permutation_82' => ['-2.5E-6', '-2.5e-4%'],
271+
'permutation_83' => ['200', '2e4%'],
272+
'permutation_84' => ['-2.5E-8', '-%2.50e-06'],
273+
'permutation_85' => [' - % 2.50 e -06 ', ' - % 2.50 e -06 '],
274+
'permutation_86' => ['-2.5E-8', ' - % 2.50e-06 '],
275+
'permutation_87' => [' - % 2.50e- 06 ', ' - % 2.50e- 06 '],
276+
'permutation_88' => [' - % 2.50e - 06 ', ' - % 2.50e - 06 '],
277+
];
278+
}
151279
}

0 commit comments

Comments
 (0)