From 66da98ebb8a4d76acc5b70701bd70c1fa8ca39df Mon Sep 17 00:00:00 2001 From: "fjohnston@avatarasoftware.com" Date: Fri, 18 Nov 2022 14:54:48 -0500 Subject: [PATCH 1/2] Handle Currencies Stored as Strings in Formulas Added a function to the new `FormattedNumber` helper that will use the currency code pulled from `localeconv` by `StringHelper::getCurrencyCode()`. The currency code is `preg_quoted` and then dropped into a regexp that is modelled on the expression developed for `convertToNumberIfPercent`. This will allow locale independent operation. Unfortunately `localeconv` only provides information about standard currency formats and not accounting formats. This means that we can't say for sure whether or not a locale has an accounting format where the currency symbol shows up in a non-standard position (eg. left justified for some countries instead of appearing directly in front of the value). The regexp should handle these cases. The primary issue with this approach is that the regexp will incorrectly match invalid currency formats for some locals as it checks for the symbol before and after the value. A possible improvement would be to pull `p_cs_precedes` and `n_cs_precedes` from `localeconv` and using them to determine if the currency symbol should appear before or after the value for the current locale. Since white-space is ignored by the regexp, accounting formats should still work, as long as the accounting format doesn't involve moving the symbol to the opposite side of the value. --- .../Calculation/Engine/FormattedNumber.php | 26 ++++++++++++ .../Calculation/CalculationTest.php | 15 +++++++ .../Engine/FormattedNumberTest.php | 41 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php index 70a2317195..6853578d74 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php +++ b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engine; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class FormattedNumber { @@ -17,6 +18,7 @@ class FormattedNumber [self::class, 'convertToNumberIfNumeric'], [self::class, 'convertToNumberIfFraction'], [self::class, 'convertToNumberIfPercent'], + [self::class, 'convertToNumberIfCurrency'], ]; /** @@ -92,4 +94,28 @@ public static function convertToNumberIfPercent(string &$operand): bool return false; } + + /** + * Identify whether a string contains a currency value, and if so, + * convert it to a numeric. + * + * @param string $operand string value to test + */ + public static function convertToNumberIfCurrency(string &$operand): bool + { + $quotedCurrencyCode = preg_quote(StringHelper::getCurrencyCode()); + $regExp = '~^(?:(?: *(?[-+])? *' . $quotedCurrencyCode . ' *(?[-+])? *(?[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *' . $quotedCurrencyCode . ' *))$~i'; + + $match = []; + if (preg_match($regExp, $operand, $match, PREG_UNMATCHED_AS_NULL)) { + //Determine the sign + $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? ''; + //Cast to a float + $operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue'])); + + return true; + } + + return false; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 0b94c19ecd..5aaf7a615e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -216,6 +217,20 @@ public function testCellWithStringPercentage(): void self::assertSame(2.0, $cell2->getCalculatedValue()); } + public function testCellWithStringCurrency(): void + { + $currencyCode = StringHelper::getCurrencyCode(); + + $spreadsheet = new Spreadsheet(); + $workSheet = $spreadsheet->getActiveSheet(); + $cell1 = $workSheet->getCell('A1'); + $cell1->setValue($currencyCode . '2'); + $cell2 = $workSheet->getCell('B1'); + $cell2->setValue('=100*A1'); + + self::assertSame(200.0, $cell2->getCalculatedValue()); + } + public function testBranchPruningFormulaParsingSimpleCase(): void { $calculation = Calculation::getInstance(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php index 40c16cecbe..375db36c86 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine; use PhpOffice\PhpSpreadsheet\Calculation\Engine\FormattedNumber; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PHPUnit\Framework\TestCase; class FormattedNumberTest extends TestCase @@ -183,4 +184,44 @@ public function providerPercentages(): array 'permutation_88' => [' - % 2.50e - 06 ', ' - % 2.50e - 06 '], ]; } + + /** + * @dataProvider providerCurrencies + */ + public function testCurrencies(string $expected, string $value): void + { + $originalValue = $value; + $result = FormattedNumber::convertToNumberIfCurrency($value); + if ($result === false) { + self::assertSame($expected, $originalValue); + self::assertSame($expected, $value); + } else { + self::assertSame($expected, (string) $value); + self::assertNotEquals($value, $originalValue); + } + } + + public function providerCurrencies(): array + { + $currencyCode = StringHelper::getCurrencyCode(); + return [ + 'basic_prefix_currency' => ['2.75', "{$currencyCode}2.75"], + 'basic_postfix_currency' => ['2.75', "2.75{$currencyCode}"], + + 'basic_prefix_currency_with_spaces' => ['2.75', "{$currencyCode} 2.75"], + 'basic_postfix_currency_with_spaces' => ['2.75', "2.75 {$currencyCode}"], + + 'negative_basic_prefix_currency' => ['-2.75', "-{$currencyCode}2.75"], + 'negative_basic_postfix_currency' => ['-2.75', "-2.75{$currencyCode}"], + + 'negative_basic_prefix_currency_with_spaces' => ['-2.75', "-{$currencyCode} 2.75"], + 'negative_basic_postfix_currency_with_spaces' => ['-2.75', "-2.75 {$currencyCode}"], + + 'basic_prefix_scientific_currency' => ['2000000', "{$currencyCode}2E6"], + 'basic_postfix_scientific_currency' => ['2000000', "2E6{$currencyCode}"], + + 'basic_prefix_scientific_currency_with_spaces' => ['2000000', "{$currencyCode} 2E6"], + 'basic_postfix_scientific_currency_with_spaces' => ['2000000', "2E6 {$currencyCode}"], + ]; + } } From 171d7684d672185dc3a189cc431faebaa272347b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 25 Nov 2022 13:01:13 +0100 Subject: [PATCH 2/2] Allow currency numeric strings with thousands separator --- .../Calculation/Engine/FormattedNumber.php | 6 ++-- .../Engine/FormattedNumberTest.php | 31 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php index 6853578d74..f46a1d67a3 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php +++ b/src/PhpSpreadsheet/Calculation/Engine/FormattedNumber.php @@ -104,10 +104,12 @@ public static function convertToNumberIfPercent(string &$operand): bool public static function convertToNumberIfCurrency(string &$operand): bool { $quotedCurrencyCode = preg_quote(StringHelper::getCurrencyCode()); - $regExp = '~^(?:(?: *(?[-+])? *' . $quotedCurrencyCode . ' *(?[-+])? *(?[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *' . $quotedCurrencyCode . ' *))$~i'; + + $value = preg_replace('/(\d),(\d)/u', '$1$2', $operand); + $regExp = '~^(?:(?: *(?[-+])? *' . $quotedCurrencyCode . ' *(?[-+])? *(?[0-9]+\.?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?[-+])? *(?[0-9]+\.?[0-9]*(?:E[-+]?[0-9]*)?) *' . $quotedCurrencyCode . ' *))$~ui'; $match = []; - if (preg_match($regExp, $operand, $match, PREG_UNMATCHED_AS_NULL)) { + if ($value !== null && preg_match($regExp, $value, $match, PREG_UNMATCHED_AS_NULL)) { //Determine the sign $sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? ''; //Cast to a float diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php index 375db36c86..77fc9d5a7f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberTest.php @@ -204,24 +204,31 @@ public function testCurrencies(string $expected, string $value): void public function providerCurrencies(): array { $currencyCode = StringHelper::getCurrencyCode(); + return [ - 'basic_prefix_currency' => ['2.75', "{$currencyCode}2.75"], - 'basic_postfix_currency' => ['2.75', "2.75{$currencyCode}"], + 'basic_prefix_currency' => ['2.75', "{$currencyCode}2.75"], + 'basic_postfix_currency' => ['2.75', "2.75{$currencyCode}"], + + 'basic_prefix_currency_with_spaces' => ['2.75', "{$currencyCode} 2.75"], + 'basic_postfix_currency_with_spaces' => ['2.75', "2.75 {$currencyCode}"], + + 'negative_basic_prefix_currency' => ['-2.75', "-{$currencyCode}2.75"], + 'negative_basic_postfix_currency' => ['-2.75', "-2.75{$currencyCode}"], - 'basic_prefix_currency_with_spaces' => ['2.75', "{$currencyCode} 2.75"], - 'basic_postfix_currency_with_spaces' => ['2.75', "2.75 {$currencyCode}"], + 'negative_basic_prefix_currency_with_spaces' => ['-2.75', "-{$currencyCode} 2.75"], + 'negative_basic_postfix_currency_with_spaces' => ['-2.75', "-2.75 {$currencyCode}"], - 'negative_basic_prefix_currency' => ['-2.75', "-{$currencyCode}2.75"], - 'negative_basic_postfix_currency' => ['-2.75', "-2.75{$currencyCode}"], + 'positive_signed_prefix_currency_with_spaces' => ['2.75', "+{$currencyCode} 2.75"], + 'positive_signed_prefix_currency_with_spaces-2' => ['2.75', "{$currencyCode} +2.75"], + 'positive_signed_postfix_currency_with_spaces' => ['2.75', "+2.75 {$currencyCode}"], - 'negative_basic_prefix_currency_with_spaces' => ['-2.75', "-{$currencyCode} 2.75"], - 'negative_basic_postfix_currency_with_spaces' => ['-2.75', "-2.75 {$currencyCode}"], + 'basic_prefix_scientific_currency' => ['2000000', "{$currencyCode}2E6"], + 'basic_postfix_scientific_currency' => ['2000000', "2E6{$currencyCode}"], - 'basic_prefix_scientific_currency' => ['2000000', "{$currencyCode}2E6"], - 'basic_postfix_scientific_currency' => ['2000000', "2E6{$currencyCode}"], + 'basic_prefix_scientific_currency_with_spaces' => ['2000000', "{$currencyCode} 2E6"], + 'basic_postfix_scientific_currency_with_spaces' => ['2000000', "2E6 {$currencyCode}"], - 'basic_prefix_scientific_currency_with_spaces' => ['2000000', "{$currencyCode} 2E6"], - 'basic_postfix_scientific_currency_with_spaces' => ['2000000', "2E6 {$currencyCode}"], + 'high_value_currency_with_thousands_separator' => ['2750000', "+{$currencyCode} 2,750,000"], ]; } }