diff --git a/src/Spout/Common/Helper/StringHelper.php b/src/Spout/Common/Helper/StringHelper.php index 0906132..6256b1e 100644 --- a/src/Spout/Common/Helper/StringHelper.php +++ b/src/Spout/Common/Helper/StringHelper.php @@ -13,12 +13,20 @@ class StringHelper /** @var bool Whether the mbstring extension is loaded */ protected $hasMbstringSupport; + /** @var bool Whether the code is running with PHP7 or older versions */ + private $isRunningPhp7OrOlder; + + /** @var array Locale info, used for number formatting */ + private $localeInfo; + /** * */ public function __construct() { $this->hasMbstringSupport = \extension_loaded('mbstring'); + $this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0; + $this->localeInfo = \localeconv(); } /** @@ -68,4 +76,30 @@ class StringHelper return ($position !== false) ? $position : -1; } + + /** + * Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format. + * + * Formatting of float values is locale dependent in PHP < 8. + * Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34). + * However, float values must be formatted with no thousands separator and a "." as decimal point + * to work properly. This method can be used to convert the value to the correct format before storing it. + * + * @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8. + * + * @param int|float $numericValue + * @return string + */ + public function formatNumericValue($numericValue) + { + if ($this->isRunningPhp7OrOlder && is_float($numericValue)) { + return str_replace( + [$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']], + ['', '.'], + $numericValue + ); + } + + return $numericValue; + } } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index e0366d1..7d7cb0e 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -231,8 +231,9 @@ class WorksheetManager implements WorksheetManagerInterface $data .= '' . $cell->getValue() . ''; $data .= ''; } elseif ($cell->isNumeric()) { - $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">'; - $data .= '' . $cell->getValue() . ''; + $cellValue = $this->stringHelper->formatNumericValue($cell->getValue()); + $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; + $data .= '' . $cellValue . ''; $data .= ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 028bdef..61b93a1 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -240,7 +240,7 @@ EOD; } elseif ($cell->isBoolean()) { $cellXML .= ' t="b">' . (int) ($cell->getValue()) . ''; } elseif ($cell->isNumeric()) { - $cellXML .= '>' . $cell->getValue() . ''; + $cellXML .= '>' . $this->stringHelper->formatNumericValue($cell->getValue()) . ''; } elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) { // only writes the error value if it's a string $cellXML .= ' t="e">' . $cell->getValueEvenIfError() . ''; diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index 31dcccf..61c1b9e 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -279,6 +279,52 @@ class WriterTest extends TestCase $this->assertValueWasWrittenToSheet($fileName, 1, 10.2); } + /** + * @return void + */ + public function testAddRowShouldSupportFloatValuesInDifferentLocale() + { + $previousLocale = \setlocale(LC_ALL, 0); + + try { + // Pick a supported locale whose decimal point is a comma. + // Installed locales differ from one system to another, so we can't pick + // a given locale. + $supportedLocales = explode("\n", shell_exec('locale -a')); + foreach ($supportedLocales as $supportedLocale) { + \setlocale(LC_ALL, $supportedLocale); + if (\localeconv()['decimal_point'] === ',') { + break; + } + } + $this->assertEquals(',', \localeconv()['decimal_point']); + + $cellValue = 1234.5; + var_dump("Cell value before: " . $cellValue); + $cellValue = str_replace( + [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], + ['', '.'], + $cellValue + ); + var_dump("Cell value after: " . $cellValue); + var_dump("Thousands sep: " . \localeconv()['thousands_sep']); + var_dump("Decimal point: " . \localeconv()['decimal_point']); + + $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; + $dataRows = $this->createRowsFromValues([ + [1234.5], + ]); + + $this->writeToODSFile($dataRows, $fileName); + + $this->assertValueWasNotWrittenToSheet($fileName, 1, "1234,5"); + $this->assertValueWasWrittenToSheet($fileName, 1, "1234.5"); + } finally { + // reset locale + \setlocale(LC_ALL, $previousLocale); + } + } + /** * @return array */ diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index 9cc157a..e2a8e4e 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -393,6 +393,52 @@ class WriterTest extends TestCase $this->assertInlineDataWasWrittenToSheet($fileName, 1, 't="e">#DIV/0'); } + /** + * @return void + */ + public function testAddRowShouldSupportFloatValuesInDifferentLocale() + { + $previousLocale = \setlocale(LC_ALL, 0); + $valueToWrite = 1234.5; // needs to be defined before changing the locale as PHP8 would expect 1234,5 + + try { + // Pick a supported locale whose decimal point is a comma. + // Installed locales differ from one system to another, so we can't pick + // a given locale. + $supportedLocales = explode("\n", shell_exec('locale -a')); + foreach ($supportedLocales as $supportedLocale) { + \setlocale(LC_ALL, $supportedLocale); + if (\localeconv()['decimal_point'] === ',') { + break; + } + } + $this->assertEquals(',', \localeconv()['decimal_point']); + + var_dump("Cell value before: " . $valueToWrite); + $cellValue = str_replace( + [\localeconv()['thousands_sep'], \localeconv()['decimal_point']], + ['', '.'], + $valueToWrite + ); + var_dump("Cell value after: " . $cellValue); + var_dump("Thousands sep: " . \localeconv()['thousands_sep']); + var_dump("Decimal point: " . \localeconv()['decimal_point']); + + $fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx'; + $dataRows = $this->createRowsFromValues([ + [$valueToWrite], + ]); + + $this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false); + + $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, "1234,5"); + $this->assertInlineDataWasWrittenToSheet($fileName, 1, "1234.5"); + } finally { + // reset locale + \setlocale(LC_ALL, $previousLocale); + } + } + /** * @return void */