Skip to content

Commit

Permalink
Merge pull request #3156 from fdjohnston/master
Browse files Browse the repository at this point in the history
Convert percentages stored as strings to numerics in formula calculations
  • Loading branch information
MarkBaker authored Nov 10, 2022
2 parents 8a06e1b + 04fb3bb commit 6f71efc
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -5110,8 +5110,8 @@ private function validateBinaryOperand(&$operand, &$stack)
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand));

return false;
} elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) {
// If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations
} elseif (!Shared\StringHelper::convertToNumberIfFraction($operand) && !Shared\StringHelper::convertToNumberIfPercent($operand)) {
// 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
$stack->push('Error', '#VALUE!');
$this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!'));

Expand Down
22 changes: 21 additions & 1 deletion src/PhpSpreadsheet/Shared/StringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class StringHelper
// Fraction
const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~';

const STRING_REGEXP_PERCENT = '~^(?:(?: *(?<PrefixedSign>[-+])? *\% *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *\% *))$~i';

/**
* Control characters array.
*
Expand Down Expand Up @@ -558,7 +560,25 @@ public static function convertToNumberIfFraction(string &$operand): bool
return false;
}

// function convertToNumberIfFraction()
/**
* Identify whether a string contains a percentage, and if so,
* convert it to a numeric.
*
* @param string $operand string value to test
*/
public static function convertToNumberIfPercent(string &$operand): bool
{
$match = [];
if (preg_match(self::STRING_REGEXP_PERCENT, $operand, $match, PREG_UNMATCHED_AS_NULL)) {
//Calculate the percentage
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])) / 100;

return true;
}

return false;
}

/**
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number
Expand Down
12 changes: 12 additions & 0 deletions tests/PhpSpreadsheetTests/Calculation/CalculationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ public function testCellWithFormulaTwoIndirect(): void
self::assertEquals('9', $cell3->getCalculatedValue());
}

public function testCellWithStringPercentage(): void
{
$spreadsheet = new Spreadsheet();
$workSheet = $spreadsheet->getActiveSheet();
$cell1 = $workSheet->getCell('A1');
$cell1->setValue('2%');
$cell2 = $workSheet->getCell('B1');
$cell2->setValue('=100*A1');

self::assertEquals('2', $cell2->getCalculatedValue());
}

public function testBranchPruningFormulaParsingSimpleCase(): void
{
$calculation = Calculation::getInstance();
Expand Down
128 changes: 128 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/StringHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,132 @@ public function providerFractions(): array
'improper fraction' => ['1.75', '7/4'],
];
}

/**
* @dataProvider providerPercentages
*/
public function testPercentage(string $expected, string $value): void
{
$originalValue = $value;
$result = StringHelper::convertToNumberIfPercent($value);
if ($result === false) {
self::assertSame($expected, $originalValue);
self::assertSame($expected, $value);
} else {
self::assertSame($expected, (string) $value);
self::assertNotEquals($value, $originalValue);
}
}

public function providerPercentages(): array
{
return [
'non-percentage' => ['10', '10'],
'single digit percentage' => ['0.02', '2%'],
'two digit percentage' => ['0.13', '13%'],
'negative single digit percentage' => ['-0.07', '-7%'],
'negative two digit percentage' => ['-0.75', '-75%'],
'large percentage' => ['98.45', '9845%'],
'small percentage' => ['0.0005', '0.05%'],
'percentage with decimals' => ['0.025', '2.5%'],
'trailing percent with space' => ['0.02', '2 %'],
'trailing percent with leading and trailing space' => ['0.02', ' 2 % '],
'leading percent with decimals' => ['0.025', ' % 2.5'],

//These should all fail
'percent only' => ['%', '%'],
'nonsense percent' => ['2%2', '2%2'],
'negative leading percent' => ['-0.02', '-%2'],

//Percent position permutations
'permutation_1' => ['0.02', '2%'],
'permutation_2' => ['0.02', ' 2%'],
'permutation_3' => ['0.02', '2% '],
'permutation_4' => ['0.02', ' 2 % '],
'permutation_5' => ['0.0275', '2.75% '],
'permutation_6' => ['0.0275', ' 2.75% '],
'permutation_7' => ['0.0275', ' 2.75 % '],
'permutation_8' => [' 2 . 75 %', ' 2 . 75 %'],
'permutation_9' => [' 2.7 5 % ', ' 2.7 5 % '],
'permutation_10' => ['-0.02', '-2%'],
'permutation_11' => ['-0.02', ' -2% '],
'permutation_12' => ['-0.02', '- 2% '],
'permutation_13' => ['-0.02', '-2 % '],
'permutation_14' => ['-0.0275', '-2.75% '],
'permutation_15' => ['-0.0275', ' -2.75% '],
'permutation_16' => ['-0.0275', '-2.75 % '],
'permutation_17' => ['-0.0275', ' - 2.75 % '],
'permutation_18' => ['0.02', '2%'],
'permutation_19' => ['0.02', '% 2 '],
'permutation_20' => ['0.02', ' %2 '],
'permutation_21' => ['0.02', ' % 2 '],
'permutation_22' => ['0.0275', '%2.75 '],
'permutation_23' => ['0.0275', ' %2.75 '],
'permutation_24' => ['0.0275', ' % 2.75 '],
'permutation_25' => [' %2 . 75 ', ' %2 . 75 '],
'permutation_26' => [' %2.7 5 ', ' %2.7 5 '],
'permutation_27' => [' % 2 . 75 ', ' % 2 . 75 '],
'permutation_28' => [' % 2.7 5 ', ' % 2.7 5 '],
'permutation_29' => ['-0.0275', '-%2.75 '],
'permutation_30' => ['-0.0275', ' - %2.75 '],
'permutation_31' => ['-0.0275', '- % 2.75 '],
'permutation_32' => ['-0.0275', ' - % 2.75 '],
'permutation_33' => ['0.02', '2%'],
'permutation_34' => ['0.02', '2 %'],
'permutation_35' => ['0.02', ' 2%'],
'permutation_36' => ['0.02', ' 2 % '],
'permutation_37' => ['0.0275', '2.75%'],
'permutation_38' => ['0.0275', ' 2.75 % '],
'permutation_39' => ['2 . 75 % ', '2 . 75 % '],
'permutation_40' => ['-0.0275', '-2.75% '],
'permutation_41' => ['-0.0275', '- 2.75% '],
'permutation_42' => ['-0.0275', ' - 2.75% '],
'permutation_43' => ['-0.0275', ' -2.75 % '],
'permutation_44' => ['-2. 75 % ', '-2. 75 % '],
'permutation_45' => ['%', '%'],
'permutation_46' => ['0.02', '%2 '],
'permutation_47' => ['0.02', '% 2 '],
'permutation_48' => ['0.02', ' %2 '],
'permutation_49' => ['0.02', '% 2 '],
'permutation_50' => ['0.02', ' % 2 '],
'permutation_51' => ['0.02', ' 2 % '],
'permutation_52' => ['-0.02', '-2%'],
'permutation_53' => ['-0.02', '- %2'],
'permutation_54' => ['-0.02', ' -%2 '],
'permutation_55' => ['2%2', '2%2'],
'permutation_56' => [' 2% %', ' 2% %'],
'permutation_57' => [' % 2 -', ' % 2 -'],
'permutation_58' => ['-0.02', '%-2'],
'permutation_59' => ['-0.02', ' % - 2'],
'permutation_60' => ['-0.0275', '%-2.75 '],
'permutation_61' => ['-0.0275', ' % - 2.75 '],
'permutation_62' => ['-0.0275', ' % - 2.75 '],
'permutation_63' => ['-0.0275', ' % - 2.75 '],
'permutation_64' => ['0.0275', ' % + 2.75 '],
'permutation_65' => ['0.0275', ' % + 2.75 '],
'permutation_66' => ['0.0275', ' % + 2.75 '],
'permutation_67' => ['0.02', '+2%'],
'permutation_68' => ['0.02', ' +2% '],
'permutation_69' => ['0.02', '+ 2% '],
'permutation_70' => ['0.02', '+2 % '],
'permutation_71' => ['0.0275', '+2.75% '],
'permutation_72' => ['0.0275', ' +2.75% '],
'permutation_73' => ['0.0275', '+2.75 % '],
'permutation_74' => ['0.0275', ' + 2.75 % '],
'permutation_75' => ['-2.5E-6', '-2.5E-4%'],
'permutation_76' => ['200', '2E4%'],
'permutation_77' => ['-2.5E-8', '-%2.50E-06'],
'permutation_78' => [' - % 2.50 E -06 ', ' - % 2.50 E -06 '],
'permutation_79' => ['-2.5E-8', ' - % 2.50E-06 '],
'permutation_80' => [' - % 2.50E- 06 ', ' - % 2.50E- 06 '],
'permutation_81' => [' - % 2.50E - 06 ', ' - % 2.50E - 06 '],
'permutation_82' => ['-2.5E-6', '-2.5e-4%'],
'permutation_83' => ['200', '2e4%'],
'permutation_84' => ['-2.5E-8', '-%2.50e-06'],
'permutation_85' => [' - % 2.50 e -06 ', ' - % 2.50 e -06 '],
'permutation_86' => ['-2.5E-8', ' - % 2.50e-06 '],
'permutation_87' => [' - % 2.50e- 06 ', ' - % 2.50e- 06 '],
'permutation_88' => [' - % 2.50e - 06 ', ' - % 2.50e - 06 '],
];
}
}

0 comments on commit 6f71efc

Please sign in to comment.