Skip to content

Commit 9bef9c9

Browse files
committed
Tests Involving Decimal and Currency Separators
This was suggested by the investigation of issue PHPOffice#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.
1 parent 9fcfa4b commit 9bef9c9

File tree

14 files changed

+131
-120
lines changed

14 files changed

+131
-120
lines changed

src/PhpSpreadsheet/Shared/StringHelper.php

+7-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class StringHelper
3535
/**
3636
* Currency code.
3737
*
38-
* @var string
38+
* @var ?string
3939
*/
4040
private static $currencyCode;
4141

@@ -551,9 +551,9 @@ public static function getDecimalSeparator(): string
551551
* Set the decimal separator. Only used by NumberFormat::toFormattedString()
552552
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
553553
*
554-
* @param string $separator Character for decimal separator
554+
* @param ?string $separator Character for decimal separator
555555
*/
556-
public static function setDecimalSeparator(string $separator): void
556+
public static function setDecimalSeparator(?string $separator): void
557557
{
558558
self::$decimalSeparator = $separator;
559559
}
@@ -582,9 +582,9 @@ public static function getThousandsSeparator(): string
582582
* Set the thousands separator. Only used by NumberFormat::toFormattedString()
583583
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
584584
*
585-
* @param string $separator Character for thousands separator
585+
* @param ?string $separator Character for thousands separator
586586
*/
587-
public static function setThousandsSeparator(string $separator): void
587+
public static function setThousandsSeparator(?string $separator): void
588588
{
589589
self::$thousandsSeparator = $separator;
590590
}
@@ -618,9 +618,9 @@ public static function getCurrencyCode(): string
618618
* Set the currency code. Only used by NumberFormat::toFormattedString()
619619
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
620620
*
621-
* @param string $currencyCode Character for currency code
621+
* @param ?string $currencyCode Character for currency code
622622
*/
623-
public static function setCurrencyCode(string $currencyCode): void
623+
public static function setCurrencyCode(?string $currencyCode): void
624624
{
625625
self::$currencyCode = $currencyCode;
626626
}

src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php

+13
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat;
44

5+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
6+
57
abstract class BaseFormatter
68
{
79
protected static function stripQuotes(string $format): string
810
{
911
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
1012
return str_replace(['"', '*'], '', $format);
1113
}
14+
15+
protected static function adjustSeparators(string $value): string
16+
{
17+
$thousandsSeparator = StringHelper::getThousandsSeparator();
18+
$decimalSeparator = StringHelper::getDecimalSeparator();
19+
if ($thousandsSeparator !== ',' || $decimalSeparator !== '.') {
20+
$value = str_replace(['.', ',', "\u{fffd}"], ["\u{fffd}", '.', ','], $value);
21+
}
22+
23+
return $value;
24+
}
1225
}

src/PhpSpreadsheet/Style/NumberFormat/Formatter.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PhpOffice\PhpSpreadsheet\Style\Color;
99
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
1010

11-
class Formatter
11+
class Formatter extends BaseFormatter
1212
{
1313
/**
1414
* Matches any @ symbol that isn't enclosed in quotes.
@@ -133,7 +133,7 @@ public static function toFormattedString($value, $format, $callBack = null)
133133
// For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
134134
// it seems to round numbers to a total of 10 digits.
135135
if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) {
136-
return (string) $value;
136+
return self::adjustSeparators((string) $value);
137137
}
138138

139139
// Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc

src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
66
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
77

8-
class NumberFormatter
8+
class NumberFormatter extends BaseFormatter
99
{
1010
private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/';
1111

@@ -176,11 +176,11 @@ private static function formatStraightNumericValue(mixed $value, string $format,
176176
return $result;
177177
}
178178

179-
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
179+
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'F';
180180

181181
/** @var float */
182182
$valueFloat = $value;
183-
$value = sprintf($sprintf_pattern, round($valueFloat, strlen($right)));
183+
$value = self::adjustSeparators(sprintf($sprintf_pattern, round($valueFloat, strlen($right))));
184184

185185
return self::pregReplace(self::NUMBER_REGEX, $value, $format);
186186
}

src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ public static function format($value, string $format): string
3838

3939
$wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0);
4040
$replacement = "0{$wholePartSize}.{$decimalPartSize}";
41-
$mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format);
41+
$mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}F{$placeHolders}", $format);
4242

4343
/** @var float */
4444
$valueFloat = $value;
4545

46-
return sprintf($mask, round($valueFloat, $decimalPartSize));
46+
return self::adjustSeparators(sprintf($mask, round($valueFloat, $decimalPartSize)));
4747
}
4848
}

tests/PhpSpreadsheetTests/Calculation/Engine/FormattedNumberSlashTest.php

+3-16
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,11 @@
1010

1111
class FormattedNumberSlashTest extends TestCase
1212
{
13-
private string $originalCurrencyCode;
14-
15-
private string $originalDecimalSeparator;
16-
17-
private string $originalThousandsSeparator;
18-
19-
protected function setUp(): void
20-
{
21-
$this->originalCurrencyCode = StringHelper::getCurrencyCode();
22-
$this->originalDecimalSeparator = StringHelper::getDecimalSeparator();
23-
$this->originalThousandsSeparator = StringHelper::getThousandsSeparator();
24-
}
25-
2613
protected function tearDown(): void
2714
{
28-
StringHelper::setCurrencyCode($this->originalCurrencyCode);
29-
StringHelper::setDecimalSeparator($this->originalDecimalSeparator);
30-
StringHelper::setThousandsSeparator($this->originalThousandsSeparator);
15+
StringHelper::setCurrencyCode(null);
16+
StringHelper::setDecimalSeparator(null);
17+
StringHelper::setThousandsSeparator(null);
3118
}
3219

3320
/**

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php

+3-17
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,12 @@
99

1010
class ValueTest extends AllSetupTeardown
1111
{
12-
private string $currencyCode;
13-
14-
private string $decimalSeparator;
15-
16-
private string $thousandsSeparator;
17-
18-
protected function setUp(): void
19-
{
20-
parent::setUp();
21-
$this->currencyCode = StringHelper::getCurrencyCode();
22-
$this->decimalSeparator = StringHelper::getDecimalSeparator();
23-
$this->thousandsSeparator = StringHelper::getThousandsSeparator();
24-
}
25-
2612
protected function tearDown(): void
2713
{
2814
parent::tearDown();
29-
StringHelper::setCurrencyCode($this->currencyCode);
30-
StringHelper::setDecimalSeparator($this->decimalSeparator);
31-
StringHelper::setThousandsSeparator($this->thousandsSeparator);
15+
StringHelper::setCurrencyCode(null);
16+
StringHelper::setDecimalSeparator(null);
17+
StringHelper::setThousandsSeparator(null);
3218
}
3319

3420
/**

tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php

+3-12
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,21 @@ class AdvancedValueBinderTest extends TestCase
1818

1919
private string $originalLocale;
2020

21-
private string $originalCurrencyCode;
22-
23-
private string $originalDecimalSeparator;
24-
25-
private string $originalThousandsSeparator;
26-
2721
private IValueBinder $valueBinder;
2822

2923
protected function setUp(): void
3024
{
3125
$this->originalLocale = Settings::getLocale();
32-
$this->originalCurrencyCode = StringHelper::getCurrencyCode();
33-
$this->originalDecimalSeparator = StringHelper::getDecimalSeparator();
34-
$this->originalThousandsSeparator = StringHelper::getThousandsSeparator();
3526

3627
$this->valueBinder = Cell::getValueBinder();
3728
Cell::setValueBinder(new AdvancedValueBinder());
3829
}
3930

4031
protected function tearDown(): void
4132
{
42-
StringHelper::setCurrencyCode($this->originalCurrencyCode);
43-
StringHelper::setDecimalSeparator($this->originalDecimalSeparator);
44-
StringHelper::setThousandsSeparator($this->originalThousandsSeparator);
33+
StringHelper::setCurrencyCode(null);
34+
StringHelper::setDecimalSeparator(null);
35+
StringHelper::setThousandsSeparator(null);
4536
Settings::setLocale($this->originalLocale);
4637
Cell::setValueBinder($this->valueBinder);
4738
}

tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected function setUp(): void
3131
{
3232
$this->currentLocale = setlocale(LC_ALL, '0');
3333

34-
if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu')) {
34+
if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) {
3535
$this->localeAdjusted = false;
3636

3737
return;
@@ -52,6 +52,8 @@ protected function tearDown(): void
5252

5353
/**
5454
* @dataProvider providerNumberFormatNoConversionTest
55+
*
56+
* @runInSeparateProcess
5557
*/
5658
public function testNumberFormatNoConversion(mixed $expectedValue, string $expectedFormat, string $cellAddress): void
5759
{

tests/PhpSpreadsheetTests/Shared/StringHelperTest.php

+3-17
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,11 @@
99

1010
class StringHelperTest extends TestCase
1111
{
12-
private string $currencyCode;
13-
14-
private string $decimalSeparator;
15-
16-
private string $thousandsSeparator;
17-
18-
protected function setUp(): void
19-
{
20-
parent::setUp();
21-
$this->currencyCode = StringHelper::getCurrencyCode();
22-
$this->decimalSeparator = StringHelper::getDecimalSeparator();
23-
$this->thousandsSeparator = StringHelper::getThousandsSeparator();
24-
}
25-
2612
protected function tearDown(): void
2713
{
28-
StringHelper::setCurrencyCode($this->currencyCode);
29-
StringHelper::setDecimalSeparator($this->decimalSeparator);
30-
StringHelper::setThousandsSeparator($this->thousandsSeparator);
14+
StringHelper::setCurrencyCode(null);
15+
StringHelper::setDecimalSeparator(null);
16+
StringHelper::setThousandsSeparator(null);
3117
}
3218

3319
public function testGetIsIconvEnabled(): void

tests/PhpSpreadsheetTests/Style/NumberFormatTest.php

+3-12
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,17 @@
1111

1212
class NumberFormatTest extends TestCase
1313
{
14-
private string $currencyCode;
15-
16-
private string $decimalSeparator;
17-
18-
private string $thousandsSeparator;
19-
2014
protected function setUp(): void
2115
{
22-
$this->currencyCode = StringHelper::getCurrencyCode();
23-
$this->decimalSeparator = StringHelper::getDecimalSeparator();
24-
$this->thousandsSeparator = StringHelper::getThousandsSeparator();
2516
StringHelper::setDecimalSeparator('.');
2617
StringHelper::setThousandsSeparator(',');
2718
}
2819

2920
protected function tearDown(): void
3021
{
31-
StringHelper::setCurrencyCode($this->currencyCode);
32-
StringHelper::setDecimalSeparator($this->decimalSeparator);
33-
StringHelper::setThousandsSeparator($this->thousandsSeparator);
22+
StringHelper::setCurrencyCode(null);
23+
StringHelper::setDecimalSeparator(null);
24+
StringHelper::setThousandsSeparator(null);
3425
}
3526

3627
/**

tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php

+3-12
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,18 @@
1313

1414
class HtmlNumberFormatTest extends Functional\AbstractFunctional
1515
{
16-
private string $currency;
17-
18-
private string $decsep;
19-
20-
private string $thosep;
21-
2216
protected function setUp(): void
2317
{
24-
$this->currency = StringHelper::getCurrencyCode();
2518
StringHelper::setCurrencyCode('$');
26-
$this->decsep = StringHelper::getDecimalSeparator();
2719
StringHelper::setDecimalSeparator('.');
28-
$this->thosep = StringHelper::getThousandsSeparator();
2920
StringHelper::setThousandsSeparator(',');
3021
}
3122

3223
protected function tearDown(): void
3324
{
34-
StringHelper::setCurrencyCode($this->currency);
35-
StringHelper::setDecimalSeparator($this->decsep);
36-
StringHelper::setThousandsSeparator($this->thosep);
25+
StringHelper::setCurrencyCode(null);
26+
StringHelper::setDecimalSeparator(null);
27+
StringHelper::setThousandsSeparator(null);
3728
}
3829

3930
public function testColorNumberFormat(): void

0 commit comments

Comments
 (0)