From 9bef9c90cec6935a9ac0b804d799088a1d605ddb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:49:43 -0800 Subject: [PATCH] Tests Involving Decimal and Currency Separators This was suggested by the investigation of issue #3811. No fix is necessary for the issue. However, two possible code solutions (Php setlocale, which comes with certain design flaws, and StringHelper set(Decimal/Thousands)Separator were suggested, and neither is adequately tested. This PR adds such tests. Unusually, getting StringHelper Decimal Separator, Thousands Separator, and Currency Code can result in a change to those properties. So, the existing design in several tests where those properties are captured in Setup and restored in Teardown do not work quite as designed. Instead, the ability to set those properties to their default value (null) is added, and the tests re-done to restore the default in Teardown. The two methods yield the same results when parsing input. However, they diverge when examining output fields through `getFormattedValue`. Such output is currently correct (usually) when using setlocale, but not when using StringHelper. The former works through the 'trick' of using `sprintf(%f)`, which generates a locale-aware string. However, using non-locale-aware `sprintf(%F)` followed by `str_replace` will produce the correct result for both setlocale and StringHelper. One place in the code uses a cast to string, which is incorrect for both methods. Following that up with the same str_replace makes it correct for both. These changes permit, but do not require, the user to avoid setlocale altogether. It remains an open question whether Settings/Calculation::setLocale should set DecimalSeparator, CurrencySeparator, and CurrencyCode. That makes logical sense, but it would be a breaking change, and having to explicitly set those values when using setLocale does not seem especially burdensome. For now, such a change will not be made. --- src/PhpSpreadsheet/Shared/StringHelper.php | 14 +-- .../Style/NumberFormat/BaseFormatter.php | 13 +++ .../Style/NumberFormat/Formatter.php | 4 +- .../Style/NumberFormat/NumberFormatter.php | 6 +- .../NumberFormat/PercentageFormatter.php | 4 +- .../Engine/FormattedNumberSlashTest.php | 19 +--- .../Functions/TextData/ValueTest.php | 20 +--- .../Cell/AdvancedValueBinderTest.php | 15 +-- .../Reader/Csv/CsvNumberFormatLocaleTest.php | 4 +- .../Shared/StringHelperTest.php | 20 +--- .../Style/NumberFormatTest.php | 15 +-- .../Writer/Html/HtmlNumberFormatTest.php | 15 +-- .../Writer/Xlsx/LocaleFloatsTest.php | 102 ++++++++++++++---- tests/data/Writer/XLSX/issue.3811b.xlsx | Bin 0 -> 6594 bytes 14 files changed, 131 insertions(+), 120 deletions(-) create mode 100644 tests/data/Writer/XLSX/issue.3811b.xlsx diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 9a8fa86551..66800f179c 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -35,7 +35,7 @@ class StringHelper /** * Currency code. * - * @var string + * @var ?string */ private static $currencyCode; @@ -551,9 +551,9 @@ public static function getDecimalSeparator(): string * Set the decimal separator. Only used by NumberFormat::toFormattedString() * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. * - * @param string $separator Character for decimal separator + * @param ?string $separator Character for decimal separator */ - public static function setDecimalSeparator(string $separator): void + public static function setDecimalSeparator(?string $separator): void { self::$decimalSeparator = $separator; } @@ -582,9 +582,9 @@ public static function getThousandsSeparator(): string * Set the thousands separator. Only used by NumberFormat::toFormattedString() * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. * - * @param string $separator Character for thousands separator + * @param ?string $separator Character for thousands separator */ - public static function setThousandsSeparator(string $separator): void + public static function setThousandsSeparator(?string $separator): void { self::$thousandsSeparator = $separator; } @@ -618,9 +618,9 @@ public static function getCurrencyCode(): string * Set the currency code. Only used by NumberFormat::toFormattedString() * to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. * - * @param string $currencyCode Character for currency code + * @param ?string $currencyCode Character for currency code */ - public static function setCurrencyCode(string $currencyCode): void + public static function setCurrencyCode(?string $currencyCode): void { self::$currencyCode = $currencyCode; } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php index 7988143c51..d6f373d079 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; + abstract class BaseFormatter { protected static function stripQuotes(string $format): string @@ -9,4 +11,15 @@ protected static function stripQuotes(string $format): string // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols return str_replace(['"', '*'], '', $format); } + + protected static function adjustSeparators(string $value): string + { + $thousandsSeparator = StringHelper::getThousandsSeparator(); + $decimalSeparator = StringHelper::getDecimalSeparator(); + if ($thousandsSeparator !== ',' || $decimalSeparator !== '.') { + $value = str_replace(['.', ',', "\u{fffd}"], ["\u{fffd}", '.', ','], $value); + } + + return $value; + } } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 1115467de4..f7969a449a 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -8,7 +8,7 @@ use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -class Formatter +class Formatter extends BaseFormatter { /** * Matches any @ symbol that isn't enclosed in quotes. @@ -133,7 +133,7 @@ public static function toFormattedString($value, $format, $callBack = null) // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, // it seems to round numbers to a total of 10 digits. if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) { - return (string) $value; + return self::adjustSeparators((string) $value); } // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc diff --git a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index f927964d37..6a6934524c 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -5,7 +5,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -class NumberFormatter +class NumberFormatter extends BaseFormatter { private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/'; @@ -176,11 +176,11 @@ private static function formatStraightNumericValue(mixed $value, string $format, return $result; } - $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'F'; /** @var float */ $valueFloat = $value; - $value = sprintf($sprintf_pattern, round($valueFloat, strlen($right))); + $value = self::adjustSeparators(sprintf($sprintf_pattern, round($valueFloat, strlen($right)))); return self::pregReplace(self::NUMBER_REGEX, $value, $format); } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index 55b5971aa9..6d23f9ddfa 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -38,11 +38,11 @@ public static function format($value, string $format): string $wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0); $replacement = "0{$wholePartSize}.{$decimalPartSize}"; - $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); + $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}F{$placeHolders}", $format); /** @var float */ $valueFloat = $value; - return sprintf($mask, round($valueFloat, $decimalPartSize)); + return self::adjustSeparators(sprintf($mask, round($valueFloat, $decimalPartSize))); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php index 9196bcc32a..c23f738709 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php @@ -10,24 +10,11 @@ class FormattedNumberSlashTest extends TestCase { - private string $originalCurrencyCode; - - private string $originalDecimalSeparator; - - private string $originalThousandsSeparator; - - protected function setUp(): void - { - $this->originalCurrencyCode = StringHelper::getCurrencyCode(); - $this->originalDecimalSeparator = StringHelper::getDecimalSeparator(); - $this->originalThousandsSeparator = StringHelper::getThousandsSeparator(); - } - protected function tearDown(): void { - StringHelper::setCurrencyCode($this->originalCurrencyCode); - StringHelper::setDecimalSeparator($this->originalDecimalSeparator); - StringHelper::setThousandsSeparator($this->originalThousandsSeparator); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php index c04dfe8798..2df5db47bd 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php @@ -9,26 +9,12 @@ class ValueTest extends AllSetupTeardown { - private string $currencyCode; - - private string $decimalSeparator; - - private string $thousandsSeparator; - - protected function setUp(): void - { - parent::setUp(); - $this->currencyCode = StringHelper::getCurrencyCode(); - $this->decimalSeparator = StringHelper::getDecimalSeparator(); - $this->thousandsSeparator = StringHelper::getThousandsSeparator(); - } - protected function tearDown(): void { parent::tearDown(); - StringHelper::setCurrencyCode($this->currencyCode); - StringHelper::setDecimalSeparator($this->decimalSeparator); - StringHelper::setThousandsSeparator($this->thousandsSeparator); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } /** diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 9963f01f6c..3ab89878bf 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -18,20 +18,11 @@ class AdvancedValueBinderTest extends TestCase private string $originalLocale; - private string $originalCurrencyCode; - - private string $originalDecimalSeparator; - - private string $originalThousandsSeparator; - private IValueBinder $valueBinder; protected function setUp(): void { $this->originalLocale = Settings::getLocale(); - $this->originalCurrencyCode = StringHelper::getCurrencyCode(); - $this->originalDecimalSeparator = StringHelper::getDecimalSeparator(); - $this->originalThousandsSeparator = StringHelper::getThousandsSeparator(); $this->valueBinder = Cell::getValueBinder(); Cell::setValueBinder(new AdvancedValueBinder()); @@ -39,9 +30,9 @@ protected function setUp(): void protected function tearDown(): void { - StringHelper::setCurrencyCode($this->originalCurrencyCode); - StringHelper::setDecimalSeparator($this->originalDecimalSeparator); - StringHelper::setThousandsSeparator($this->originalThousandsSeparator); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); Settings::setLocale($this->originalLocale); Cell::setValueBinder($this->valueBinder); } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php index 9550636b3c..d6d65329e9 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php @@ -31,7 +31,7 @@ protected function setUp(): void { $this->currentLocale = setlocale(LC_ALL, '0'); - if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu')) { + if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) { $this->localeAdjusted = false; return; @@ -52,6 +52,8 @@ protected function tearDown(): void /** * @dataProvider providerNumberFormatNoConversionTest + * + * @runInSeparateProcess */ public function testNumberFormatNoConversion(mixed $expectedValue, string $expectedFormat, string $cellAddress): void { diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php index e77e02a424..03a4403d1a 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php @@ -9,25 +9,11 @@ class StringHelperTest extends TestCase { - private string $currencyCode; - - private string $decimalSeparator; - - private string $thousandsSeparator; - - protected function setUp(): void - { - parent::setUp(); - $this->currencyCode = StringHelper::getCurrencyCode(); - $this->decimalSeparator = StringHelper::getDecimalSeparator(); - $this->thousandsSeparator = StringHelper::getThousandsSeparator(); - } - protected function tearDown(): void { - StringHelper::setCurrencyCode($this->currencyCode); - StringHelper::setDecimalSeparator($this->decimalSeparator); - StringHelper::setThousandsSeparator($this->thousandsSeparator); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } public function testGetIsIconvEnabled(): void diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php index 5cd09574c8..0a1aaf2920 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php @@ -11,26 +11,17 @@ class NumberFormatTest extends TestCase { - private string $currencyCode; - - private string $decimalSeparator; - - private string $thousandsSeparator; - protected function setUp(): void { - $this->currencyCode = StringHelper::getCurrencyCode(); - $this->decimalSeparator = StringHelper::getDecimalSeparator(); - $this->thousandsSeparator = StringHelper::getThousandsSeparator(); StringHelper::setDecimalSeparator('.'); StringHelper::setThousandsSeparator(','); } protected function tearDown(): void { - StringHelper::setCurrencyCode($this->currencyCode); - StringHelper::setDecimalSeparator($this->decimalSeparator); - StringHelper::setThousandsSeparator($this->thousandsSeparator); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } /** diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php index 1594a6fa34..f792f92523 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php @@ -13,27 +13,18 @@ class HtmlNumberFormatTest extends Functional\AbstractFunctional { - private string $currency; - - private string $decsep; - - private string $thosep; - protected function setUp(): void { - $this->currency = StringHelper::getCurrencyCode(); StringHelper::setCurrencyCode('$'); - $this->decsep = StringHelper::getDecimalSeparator(); StringHelper::setDecimalSeparator('.'); - $this->thosep = StringHelper::getThousandsSeparator(); StringHelper::setThousandsSeparator(','); } protected function tearDown(): void { - StringHelper::setCurrencyCode($this->currency); - StringHelper::setDecimalSeparator($this->decsep); - StringHelper::setThousandsSeparator($this->thosep); + StringHelper::setCurrencyCode(null); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } public function testColorNumberFormat(): void diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php index 185154e1bd..5902896e7c 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php @@ -4,17 +4,18 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Settings; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class LocaleFloatsTest extends AbstractFunctional { - private bool $localeAdjusted; + private false|string $currentPhpLocale; - /** - * @var false|string - */ - private $currentLocale; + private string $originalLocale; /** @var ?Spreadsheet */ private $spreadsheet; @@ -24,21 +25,19 @@ class LocaleFloatsTest extends AbstractFunctional protected function setUp(): void { - $this->currentLocale = setlocale(LC_ALL, '0'); - - if (!setlocale(LC_ALL, 'fr_FR.UTF-8', 'fra_fra')) { - $this->localeAdjusted = false; - - return; - } - - $this->localeAdjusted = true; + $this->currentPhpLocale = setlocale(LC_ALL, '0'); + $this->originalLocale = Settings::getLocale(); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); } protected function tearDown(): void { - if ($this->localeAdjusted && is_string($this->currentLocale)) { - setlocale(LC_ALL, $this->currentLocale); + StringHelper::setDecimalSeparator(null); + StringHelper::setThousandsSeparator(null); + Settings::setLocale($this->originalLocale); + if ($this->currentPhpLocale !== false) { + setlocale(LC_ALL, $this->currentPhpLocale); } if ($this->spreadsheet !== null) { $this->spreadsheet->disconnectWorksheets(); @@ -50,12 +49,17 @@ protected function tearDown(): void } } + /** + * Use separate process because this calls native Php setlocale. + * + * @runInSeparateProcess + */ public function testLocaleFloatsCorrectlyConvertedByWriter(): void { - if (!$this->localeAdjusted) { + if (!setlocale(LC_ALL, 'fr_FR.UTF-8', 'fra_fra.utf8')) { + $this->currentPhpLocale = false; self::markTestSkipped('Unable to set locale for testing.'); } - $this->spreadsheet = $spreadsheet = new Spreadsheet(); $properties = $spreadsheet->getProperties(); $properties->setCustomProperty('Version', 1.2); @@ -68,7 +72,67 @@ public function testLocaleFloatsCorrectlyConvertedByWriter(): void $prop = $reloadedSpreadsheet->getProperties()->getCustomPropertyValue('Version'); self::assertEqualsWithDelta(1.2, $prop, 1.0E-8); - $actual = sprintf('%f', $result); + $actual = $reloadedSpreadsheet->getActiveSheet()->getCell('A1')->getFormattedValue(); self::assertStringContainsString('1,1', $actual); } + + public function testPercentageStoredAsString(): void + { + Settings::setLocale('fr_FR'); + StringHelper::setDecimalSeparator(','); + StringHelper::setThousandsSeparator('.'); + $reader = new XlsxReader(); + $this->spreadsheet = $spreadsheet = $reader->load('tests/data/Writer/Xlsx/issue.3811b.xlsx'); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('48,34%', $sheet->getCell('L2')->getValue()); + self::assertIsString($sheet->getCell('L2')->getValue()); + self::assertSame('=(10%+L2)/2', $sheet->getCell('L1')->getValue()); + self::assertEqualsWithDelta(0.2917, $sheet->getCell('L1')->getCalculatedValue(), 1E-8); + self::assertIsFloat($sheet->getCell('L1')->getCalculatedValue()); + self::assertEquals('29,17%', $sheet->getCell('L1')->getFormattedValue()); + + $sheet->getCell('A10')->setValue(3.2); + self::assertSame(NumberFormat::FORMAT_GENERAL, $sheet->getStyle('A10')->getNumberFormat()->getFormatCode()); + self::assertSame('3,2', $sheet->getCell('A10')->getFormattedValue()); + $sheet->getCell('A11')->setValue(1002.5); + $sheet->getStyle('A11')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); + self::assertSame('1.002,50', $sheet->getCell('A11')->getFormattedValue()); + $sheet->getCell('A12')->setValue(2.5); + $sheet->getStyle('A12')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_00); + self::assertSame('2,50', $sheet->getCell('A12')->getFormattedValue()); + } + + /** + * Use separate process because this calls native Php setlocale. + * + * @runInSeparateProcess + */ + public function testPercentageStoredAsString2(): void + { + if (!setlocale(LC_ALL, 'fr_FR.UTF-8', 'fra_fra.utf8')) { + $this->currentPhpLocale = false; + self::markTestSkipped('Unable to set locale for testing.'); + } + $reader = new XlsxReader(); + $this->spreadsheet = $spreadsheet = $reader->load('tests/data/Writer/Xlsx/issue.3811b.xlsx'); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('48,34%', $sheet->getCell('L2')->getValue()); + self::assertIsString($sheet->getCell('L2')->getValue()); + self::assertSame('=(10%+L2)/2', $sheet->getCell('L1')->getValue()); + self::assertEqualsWithDelta(0.2917, $sheet->getCell('L1')->getCalculatedValue(), 1E-8); + self::assertIsFloat($sheet->getCell('L1')->getCalculatedValue()); + self::assertEquals('29,17%', $sheet->getCell('L1')->getFormattedValue()); + + $sheet->getCell('A10')->setValue(3.2); + self::assertSame(NumberFormat::FORMAT_GENERAL, $sheet->getStyle('A10')->getNumberFormat()->getFormatCode()); + self::assertSame('3,2', $sheet->getCell('A10')->getFormattedValue()); + $narrowNonBreakSpace = "\u{202f}"; + self::assertSame($narrowNonBreakSpace, localeconv()['thousands_sep']); + $sheet->getCell('A11')->setValue(1002.5); + $sheet->getStyle('A11')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); + self::assertSame('1' . $narrowNonBreakSpace . '002,50', $sheet->getCell('A11')->getFormattedValue()); + $sheet->getCell('A12')->setValue(2.5); + $sheet->getStyle('A12')->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_00); + self::assertSame('2,50', $sheet->getCell('A12')->getFormattedValue()); + } } diff --git a/tests/data/Writer/XLSX/issue.3811b.xlsx b/tests/data/Writer/XLSX/issue.3811b.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..43dbf4bd8d81d90297c2923ee730718fd846ddde GIT binary patch literal 6594 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 literal 0 HcmV?d00001