Work on setting up locale-aware formatted number conversion for the Csv Reader

Unit tests for locale-aware boolean conversion for Csv Reader
This commit is contained in:
MarkBaker 2022-02-27 23:24:20 +01:00
parent 5579712688
commit f3d5028518
8 changed files with 427 additions and 3 deletions

View File

@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### Added
- Implementation of the ISREF() information function - Implementation of the ISREF() information function.
- Allow Boolean Conversion in Csv Reader to be locale-aware when using the String Value Binder.
(i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.)
### Changed ### Changed
@ -27,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed ### Fixed
- Fixed behaviour of XLSX font style vertical align settings - Fixed behaviour of XLSX font style vertical align settings.
- Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. - Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels.
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). 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).

View File

@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class Csv extends BaseReader class Csv extends BaseReader
{ {
@ -85,6 +86,16 @@ class Csv extends BaseReader
*/ */
private static $constructorCallback; private static $constructorCallback;
/**
* @var bool
*/
protected $castFormattedNumberToNumeric = false;
/**
* @var bool
*/
protected $preserveNumericFormatting = false;
/** /**
* Create a new CSV Reader instance. * Create a new CSV Reader instance.
*/ */
@ -283,6 +294,14 @@ class Csv extends BaseReader
return $retVal; return $retVal;
} }
public function castFormattedNumberToNumeric(
bool $castFormattedNumberToNumeric,
bool $preserveNumericFormatting = false
): void {
$this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric;
$this->preserveNumericFormatting = $preserveNumericFormatting;
}
/** /**
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
*/ */
@ -319,6 +338,7 @@ class Csv extends BaseReader
$columnLetter = 'A'; $columnLetter = 'A';
foreach ($rowData as $rowDatum) { foreach ($rowData as $rowDatum) {
$this->convertBoolean($rowDatum, $preserveBooleanString); $this->convertBoolean($rowDatum, $preserveBooleanString);
$numberFormatMask = $this->convertFormattedNumber($rowDatum);
if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) {
if ($this->contiguous) { if ($this->contiguous) {
if ($noOutputYet) { if ($noOutputYet) {
@ -328,6 +348,10 @@ class Csv extends BaseReader
} else { } else {
$outRow = $currentRow; $outRow = $currentRow;
} }
// Set basic styling for the value (Note that this could be overloaded by styling in a value binder)
$sheet->getCell($columnLetter . $outRow)->getStyle()
->getNumberFormat()
->setFormatCode($numberFormatMask);
// Set cell value // Set cell value
$sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
} }
@ -364,6 +388,39 @@ class Csv extends BaseReader
} }
} }
/**
* Convert numeric strings to int or float values.
*
* @param mixed $rowDatum
*/
private function convertFormattedNumber(&$rowDatum): string
{
$numberFormatMask = NumberFormat::FORMAT_GENERAL;
if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
$numeric = str_replace(
[StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()],
['', '.'],
$rowDatum
);
if (is_numeric($numeric)) {
$decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator());
if ($this->preserveNumericFormatting === true) {
$numberFormatMask = (strpos($rowDatum, StringHelper::getThousandsSeparator()) !== false)
? '#,##0' : '0';
if ($decimalPos !== false) {
$decimals = strlen($rowDatum) - $decimalPos - 1;
$numberFormatMask .= '.' . str_repeat('0', min($decimals, 6));
}
}
$rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric;
}
}
return $numberFormatMask;
}
public function getDelimiter(): ?string public function getDelimiter(): ?string
{ {
return $this->delimiter; return $this->delimiter;

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv; namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder;
@ -31,7 +32,7 @@ class CsvIssue2232Test extends TestCase
* @param mixed $b2Value * @param mixed $b2Value
* @param mixed $b3Value * @param mixed $b3Value
*/ */
public function testEncodings(bool $useStringBinder, ?bool $preserveBoolString, $b2Value, $b3Value): void public function testBooleanConversions(bool $useStringBinder, ?bool $preserveBoolString, $b2Value, $b3Value): void
{ {
if ($useStringBinder) { if ($useStringBinder) {
$binder = new StringValueBinder(); $binder = new StringValueBinder();
@ -60,4 +61,41 @@ class CsvIssue2232Test extends TestCase
[true, true, 'FaLSe', 'tRUE'], [true, true, 'FaLSe', 'tRUE'],
]; ];
} }
/**
* @dataProvider providerIssue2232locale
*
* @param mixed $b4Value
* @param mixed $b5Value
*/
public function testBooleanConversionsLocaleAware(bool $useStringBinder, ?bool $preserveBoolString, $b4Value, $b5Value): void
{
if ($useStringBinder) {
$binder = new StringValueBinder();
if (is_bool($preserveBoolString)) {
$binder->setBooleanConversion($preserveBoolString);
}
Cell::setValueBinder($binder);
}
Calculation::getInstance()->setLocale('fr');
$reader = new Csv();
$filename = 'tests/data/Reader/CSV/issue.2232.csv';
$spreadsheet = $reader->load($filename);
$sheet = $spreadsheet->getActiveSheet();
self::assertSame($b4Value, $sheet->getCell('B4')->getValue());
self::assertSame($b5Value, $sheet->getCell('B5')->getValue());
$spreadsheet->disconnectWorksheets();
}
public function providerIssue2232locale(): array
{
return [
[true, true, 'Faux', 'Vrai'],
[true, true, 'Faux', 'Vrai'],
[false, false, false, true],
[false, false, false, true],
];
}
} }

View File

@ -0,0 +1,145 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
use PHPUnit\Framework\TestCase;
class CsvNumberFormatLocaleTest extends TestCase
{
/**
* @var bool
*/
private $localeAdjusted;
/**
* @var false|string
*/
private $currentLocale;
/**
* @var string
*/
protected $filename;
/**
* @var Csv
*/
protected $csvReader;
protected function setUp(): void
{
$this->currentLocale = setlocale(LC_ALL, '0');
if (!setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu')) {
$this->localeAdjusted = false;
return;
}
$this->localeAdjusted = true;
$this->filename = 'tests/data/Reader/CSV/NumberFormatTest.de.csv';
$this->csvReader = new Csv();
}
protected function tearDown(): void
{
if ($this->localeAdjusted && is_string($this->currentLocale)) {
setlocale(LC_ALL, $this->currentLocale);
}
}
/**
* @dataProvider providerNumberFormatNoConversionTest
*
* @param mixed $expectedValue
*/
public function testNumberFormatNoConversion($expectedValue, string $expectedFormat, string $cellAddress): void
{
if (!$this->localeAdjusted) {
self::markTestSkipped('Unable to set locale for testing.');
}
$spreadsheet = $this->csvReader->load($this->filename);
$worksheet = $spreadsheet->getActiveSheet();
$cell = $worksheet->getCell($cellAddress);
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
self::assertSame($expectedFormat, $cell->getFormattedValue(), 'Format mask check');
}
public function providerNumberFormatNoConversionTest(): array
{
return [
[
-123,
'-123',
'A1',
],
[
'12.345,67',
'12.345,67',
'C1',
],
[
'-1.234,567',
'-1.234,567',
'A3',
],
];
}
/**
* @dataProvider providerNumberValueConversionTest
*
* @param mixed $expectedValue
*/
public function testNumberValueConversion($expectedValue, string $cellAddress): void
{
if (!$this->localeAdjusted) {
self::markTestSkipped('Unable to set locale for testing.');
}
$this->csvReader->castFormattedNumberToNumeric(true);
$spreadsheet = $this->csvReader->load($this->filename);
$worksheet = $spreadsheet->getActiveSheet();
$cell = $worksheet->getCell($cellAddress);
self::assertSame(DataType::TYPE_NUMERIC, $cell->getDataType(), 'Datatype check');
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
}
public function providerNumberValueConversionTest(): array
{
return [
'A1' => [
-123,
'A1',
],
'B1' => [
1234,
'B1',
],
'C1' => [
12345.67,
'C1',
],
'A2' => [
123.4567,
'A2',
],
'B2' => [
123.456789012,
'B2',
],
'A3' => [
-1234.567,
'A3',
],
];
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
use PHPUnit\Framework\TestCase;
class CsvNumberFormatTest extends TestCase
{
/**
* @var string
*/
protected $filename;
/**
* @var Csv
*/
protected $csvReader;
protected function setUp(): void
{
$this->filename = 'tests/data/Reader/CSV/NumberFormatTest.csv';
$this->csvReader = new Csv();
}
/**
* @dataProvider providerNumberFormatNoConversionTest
*
* @param mixed $expectedValue
*/
public function testNumberFormatNoConversion($expectedValue, string $expectedFormat, string $cellAddress): void
{
$spreadsheet = $this->csvReader->load($this->filename);
$worksheet = $spreadsheet->getActiveSheet();
$cell = $worksheet->getCell($cellAddress);
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
self::assertSame($expectedFormat, $cell->getFormattedValue(), 'Format mask check');
}
public function providerNumberFormatNoConversionTest(): array
{
return [
[
-123,
'-123',
'A1',
],
[
'12,345.67',
'12,345.67',
'C1',
],
[
'-1,234.567',
'-1,234.567',
'A3',
],
];
}
/**
* @dataProvider providerNumberValueConversionTest
*
* @param mixed $expectedValue
*/
public function testNumberValueConversion($expectedValue, string $cellAddress): void
{
$this->csvReader->castFormattedNumberToNumeric(true);
$spreadsheet = $this->csvReader->load($this->filename);
$worksheet = $spreadsheet->getActiveSheet();
$cell = $worksheet->getCell($cellAddress);
self::assertSame(DataType::TYPE_NUMERIC, $cell->getDataType(), 'Datatype check');
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
}
public function providerNumberValueConversionTest(): array
{
return [
'A1' => [
-123,
'A1',
],
'B1' => [
1234,
'B1',
],
'C1' => [
12345.67,
'C1',
],
'A2' => [
123.4567,
'A2',
],
'B2' => [
123.456789012,
'B2',
],
'A3' => [
-1234.567,
'A3',
],
'B3' => [
1234.567,
'B3',
],
];
}
/**
* @dataProvider providerNumberFormatConversionTest
*
* @param mixed $expectedValue
*/
public function testNumberFormatConversion($expectedValue, string $expectedFormat, string $cellAddress): void
{
$this->csvReader->castFormattedNumberToNumeric(true, true);
$spreadsheet = $this->csvReader->load($this->filename);
$worksheet = $spreadsheet->getActiveSheet();
$cell = $worksheet->getCell($cellAddress);
self::assertSame(DataType::TYPE_NUMERIC, $cell->getDataType(), 'Datatype check');
self::assertSame($expectedValue, $cell->getValue(), 'Expected value check');
self::assertSame($expectedFormat, $cell->getFormattedValue(), 'Format mask check');
}
public function providerNumberFormatConversionTest(): array
{
return [
'A1' => [
-123,
'-123',
'A1',
],
'B1' => [
1234,
'1,234',
'B1',
],
'C1' => [
12345.67,
'12,345.67',
'C1',
],
'A2' => [
123.4567,
'123.4567',
'A2',
],
'B2' => [
123.456789012,
'123.456789',
'B2',
],
'A3' => [
-1234.567,
'-1,234.567',
'A3',
],
'B3' => [
1234.567,
'1234.567',
'B3',
],
];
}
}

View File

@ -0,0 +1,3 @@
"-123","1,234","12,345.67"
"123.4567","123.456789012"
"-1,234.567",1234.567
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,3 @@
"-123","1.234","12.345,67"
"123,4567","123,456789012"
"-1.234,567"
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -1,3 +1,5 @@
1,2,3 1,2,3
a,FaLSe,b a,FaLSe,b
cc,tRUE,cc cc,tRUE,cc
dd,Faux,ee
ff,Vrai,gg

1 1 2 3
2 a FaLSe b
3 cc tRUE cc
4 dd Faux ee
5 ff Vrai gg