Skip to content

Commit f3d5028

Browse files
author
MarkBaker
committed
Work on setting up locale-aware formatted number conversion for the Csv Reader
Unit tests for locale-aware boolean conversion for Csv Reader
1 parent 5579712 commit f3d5028

File tree

8 files changed

+427
-3
lines changed

8 files changed

+427
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12-
- Implementation of the ISREF() information function
12+
- Implementation of the ISREF() information function.
13+
- Allow Boolean Conversion in Csv Reader to be locale-aware when using the String Value Binder.
14+
15+
(i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.)
1316

1417
### Changed
1518

@@ -27,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2730

2831
### Fixed
2932

30-
- Fixed behaviour of XLSX font style vertical align settings
33+
- Fixed behaviour of XLSX font style vertical align settings.
3134
- Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels.
3235

3336
Note that this method is used when translating Excel functions between en and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel).

src/PhpSpreadsheet/Reader/Csv.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
1010
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1111
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12+
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
1213

1314
class Csv extends BaseReader
1415
{
@@ -85,6 +86,16 @@ class Csv extends BaseReader
8586
*/
8687
private static $constructorCallback;
8788

89+
/**
90+
* @var bool
91+
*/
92+
protected $castFormattedNumberToNumeric = false;
93+
94+
/**
95+
* @var bool
96+
*/
97+
protected $preserveNumericFormatting = false;
98+
8899
/**
89100
* Create a new CSV Reader instance.
90101
*/
@@ -283,6 +294,14 @@ private static function setAutoDetect(?string $value): ?string
283294
return $retVal;
284295
}
285296

297+
public function castFormattedNumberToNumeric(
298+
bool $castFormattedNumberToNumeric,
299+
bool $preserveNumericFormatting = false
300+
): void {
301+
$this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric;
302+
$this->preserveNumericFormatting = $preserveNumericFormatting;
303+
}
304+
286305
/**
287306
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
288307
*/
@@ -319,6 +338,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
319338
$columnLetter = 'A';
320339
foreach ($rowData as $rowDatum) {
321340
$this->convertBoolean($rowDatum, $preserveBooleanString);
341+
$numberFormatMask = $this->convertFormattedNumber($rowDatum);
322342
if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) {
323343
if ($this->contiguous) {
324344
if ($noOutputYet) {
@@ -328,6 +348,10 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
328348
} else {
329349
$outRow = $currentRow;
330350
}
351+
// Set basic styling for the value (Note that this could be overloaded by styling in a value binder)
352+
$sheet->getCell($columnLetter . $outRow)->getStyle()
353+
->getNumberFormat()
354+
->setFormatCode($numberFormatMask);
331355
// Set cell value
332356
$sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
333357
}
@@ -364,6 +388,39 @@ private function convertBoolean(&$rowDatum, bool $preserveBooleanString): void
364388
}
365389
}
366390

391+
/**
392+
* Convert numeric strings to int or float values.
393+
*
394+
* @param mixed $rowDatum
395+
*/
396+
private function convertFormattedNumber(&$rowDatum): string
397+
{
398+
$numberFormatMask = NumberFormat::FORMAT_GENERAL;
399+
if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
400+
$numeric = str_replace(
401+
[StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()],
402+
['', '.'],
403+
$rowDatum
404+
);
405+
406+
if (is_numeric($numeric)) {
407+
$decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator());
408+
if ($this->preserveNumericFormatting === true) {
409+
$numberFormatMask = (strpos($rowDatum, StringHelper::getThousandsSeparator()) !== false)
410+
? '#,##0' : '0';
411+
if ($decimalPos !== false) {
412+
$decimals = strlen($rowDatum) - $decimalPos - 1;
413+
$numberFormatMask .= '.' . str_repeat('0', min($decimals, 6));
414+
}
415+
}
416+
417+
$rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric;
418+
}
419+
}
420+
421+
return $numberFormatMask;
422+
}
423+
367424
public function getDelimiter(): ?string
368425
{
369426
return $this->delimiter;

tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
56
use PhpOffice\PhpSpreadsheet\Cell\Cell;
67
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
78
use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
@@ -31,7 +32,7 @@ protected function tearDown(): void
3132
* @param mixed $b2Value
3233
* @param mixed $b3Value
3334
*/
34-
public function testEncodings(bool $useStringBinder, ?bool $preserveBoolString, $b2Value, $b3Value): void
35+
public function testBooleanConversions(bool $useStringBinder, ?bool $preserveBoolString, $b2Value, $b3Value): void
3536
{
3637
if ($useStringBinder) {
3738
$binder = new StringValueBinder();
@@ -60,4 +61,41 @@ public function providerIssue2232(): array
6061
[true, true, 'FaLSe', 'tRUE'],
6162
];
6263
}
64+
65+
/**
66+
* @dataProvider providerIssue2232locale
67+
*
68+
* @param mixed $b4Value
69+
* @param mixed $b5Value
70+
*/
71+
public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, $b4Value, $b5Value): void
72+
{
73+
if ($useStringBinder) {
74+
$binder = new StringValueBinder();
75+
if (is_bool($preserveBoolString)) {
76+
$binder->setBooleanConversion($preserveBoolString);
77+
}
78+
Cell::setValueBinder($binder);
79+
}
80+
81+
Calculation::getInstance()->setLocale('fr');
82+
83+
$reader = new Csv();
84+
$filename = 'tests/data/Reader/CSV/issue.2232.csv';
85+
$spreadsheet = $reader->load($filename);
86+
$sheet = $spreadsheet->getActiveSheet();
87+
self::assertSame($b4Value, $sheet->getCell('B4')->getValue());
88+
self::assertSame($b5Value, $sheet->getCell('B5')->getValue());
89+
$spreadsheet->disconnectWorksheets();
90+
}
91+
92+
public function providerIssue2232locale(): array
93+
{
94+
return [
95+
[true, true, 'Faux', 'Vrai'],
96+
[true, true, 'Faux', 'Vrai'],
97+
[false, false, false, true],
98+
[false, false, false, true],
99+
];
100+
}
63101
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
4+
5+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
6+
use PhpOffice\PhpSpreadsheet\Reader\Csv;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class CsvNumberFormatLocaleTest extends TestCase
10+
{
11+
/**
12+
* @var bool
13+
*/
14+
private $localeAdjusted;
15+
16+
/**
17+
* @var false|string
18+
*/
19+
private $currentLocale;
20+
21+
/**
22+
* @var string
23+
*/
24+
protected $filename;
25+
26+
/**
27+
* @var Csv
28+
*/
29+
protected $csvReader;
30+
31+
protected function setUp(): void
32+
{
33+
$this->currentLocale = setlocale(LC_ALL, '0');
34+
35+
if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu')) {
36+
$this->localeAdjusted = false;
37+
38+
return;
39+
}
40+
41+
$this->localeAdjusted = true;
42+
43+
$this->filename = 'tests/data/Reader/CSV/NumberFormatTest.de.csv';
44+
$this->csvReader = new Csv();
45+
}
46+
47+
protected function tearDown(): void
48+
{
49+
if ($this->localeAdjusted && is_string($this->currentLocale)) {
50+
setlocale(LC_ALL, $this->currentLocale);
51+
}
52+
}
53+
54+
/**
55+
* @dataProvider providerNumberFormatNoConversionTest
56+
*
57+
* @param mixed $expectedValue
58+
*/
59+
public function testNumberFormatNoConversion($expectedValue, string $expectedFormat, string $cellAddress): void
60+
{
61+
if (!$this->localeAdjusted) {
62+
self::markTestSkipped('Unable to set locale for testing.');
63+
}
64+
65+
$spreadsheet = $this->csvReader->load($this->filename);
66+
$worksheet = $spreadsheet->getActiveSheet();
67+
68+
$cell = $worksheet->getCell($cellAddress);
69+
70+
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
71+
self::assertSame($expectedFormat, $cell->getFormattedValue(), 'Format mask check');
72+
}
73+
74+
public function providerNumberFormatNoConversionTest(): array
75+
{
76+
return [
77+
[
78+
-123,
79+
'-123',
80+
'A1',
81+
],
82+
[
83+
'12.345,67',
84+
'12.345,67',
85+
'C1',
86+
],
87+
[
88+
'-1.234,567',
89+
'-1.234,567',
90+
'A3',
91+
],
92+
];
93+
}
94+
95+
/**
96+
* @dataProvider providerNumberValueConversionTest
97+
*
98+
* @param mixed $expectedValue
99+
*/
100+
public function testNumberValueConversion($expectedValue, string $cellAddress): void
101+
{
102+
if (!$this->localeAdjusted) {
103+
self::markTestSkipped('Unable to set locale for testing.');
104+
}
105+
106+
$this->csvReader->castFormattedNumberToNumeric(true);
107+
$spreadsheet = $this->csvReader->load($this->filename);
108+
$worksheet = $spreadsheet->getActiveSheet();
109+
110+
$cell = $worksheet->getCell($cellAddress);
111+
112+
self::assertSame(DataType::TYPE_NUMERIC, $cell->getDataType(), 'Datatype check');
113+
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
114+
}
115+
116+
public function providerNumberValueConversionTest(): array
117+
{
118+
return [
119+
'A1' => [
120+
-123,
121+
'A1',
122+
],
123+
'B1' => [
124+
1234,
125+
'B1',
126+
],
127+
'C1' => [
128+
12345.67,
129+
'C1',
130+
],
131+
'A2' => [
132+
123.4567,
133+
'A2',
134+
],
135+
'B2' => [
136+
123.456789012,
137+
'B2',
138+
],
139+
'A3' => [
140+
-1234.567,
141+
'A3',
142+
],
143+
];
144+
}
145+
}

0 commit comments

Comments
 (0)