mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-08-30 12:07:56 +00:00
Merge pull request #4827 from kemo/perf/csv-streaming-encoding
Experimental Stream encoding conversion in CSV reader to reduce peak memory
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetBenchmarks;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class CsvChunk extends Csv
|
||||
{
|
||||
/**
|
||||
* Size of each chunk when streaming encoding conversion.
|
||||
* Aligned to a multiple of 4 so UTF-16/UTF-32 character
|
||||
* boundaries are never split.
|
||||
*/
|
||||
private const CHUNK_SIZE = 65536;
|
||||
|
||||
protected int $chunkSize = self::CHUNK_SIZE;
|
||||
|
||||
public function setChunkSize(int $chunkSize): void
|
||||
{
|
||||
$this->chunkSize = $chunkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert file encoding to UTF-8 using chunked streaming to avoid
|
||||
* loading the entire file into memory at once.
|
||||
*/
|
||||
protected function convertNonUtf8(string $filename): void
|
||||
{
|
||||
$sourceHandle = null;
|
||||
$encoding = strtoupper($this->inputEncoding);
|
||||
if ($encoding === 'UTF-16' || $encoding === 'UCS-2') {
|
||||
$sourceHandle = fopen($filename, 'rb');
|
||||
if ($sourceHandle === false) {
|
||||
$sourceHandle = null;
|
||||
} else {
|
||||
$first2 = (string) fread($sourceHandle, self::UTF16BE_BOM_LEN);
|
||||
if ($first2 === self::UTF16BE_BOM) {
|
||||
$encoding .= 'BE';
|
||||
} elseif ($first2 === self::UTF16LE_BOM) {
|
||||
$encoding .= 'LE';
|
||||
} else {
|
||||
$encoding .= 'BE';
|
||||
fclose($sourceHandle);
|
||||
$sourceHandle = null;
|
||||
}
|
||||
}
|
||||
} elseif ($encoding === 'UTF-32' || $encoding === 'UCS-4') {
|
||||
$sourceHandle = fopen($filename, 'rb');
|
||||
if ($sourceHandle === false) {
|
||||
$sourceHandle = null;
|
||||
} else {
|
||||
$first2 = (string) fread($sourceHandle, self::UTF32BE_BOM_LEN);
|
||||
if ($first2 === self::UTF32BE_BOM) {
|
||||
$encoding .= 'BE';
|
||||
} elseif ($first2 === self::UTF32LE_BOM) {
|
||||
$encoding .= 'LE';
|
||||
} else {
|
||||
$encoding .= 'BE';
|
||||
fclose($sourceHandle);
|
||||
$sourceHandle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (str_starts_with($encoding, 'UTF-7')) {
|
||||
parent::convertNonUtf8($filename);
|
||||
|
||||
return;
|
||||
}
|
||||
fclose($this->fileHandle);
|
||||
if ($sourceHandle === null) {
|
||||
$sourceHandle = fopen($filename, 'rb');
|
||||
}
|
||||
// Using php://temp instead of php://memory: spills to disk when data
|
||||
// exceeds 2MB, reducing peak memory for large files.
|
||||
$outputHandle = fopen('php://temp', 'r+b');
|
||||
if ($sourceHandle === false || $outputHandle === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($sourceHandle !== false) {
|
||||
fclose($sourceHandle);
|
||||
}
|
||||
if ($outputHandle !== false) {
|
||||
fclose($outputHandle);
|
||||
}
|
||||
|
||||
throw new ReaderException("Failed to open file for encoding conversion: {$filename}");
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($encoding === 'UTF-16BE') {
|
||||
$checkdigit = -2;
|
||||
} elseif ($encoding === 'UTF-16LE') {
|
||||
$checkdigit = -1;
|
||||
} else {
|
||||
$checkdigit = 0;
|
||||
}
|
||||
$charWidth = $this->encodingCharWidth($encoding);
|
||||
// Ensure chunk size is aligned to character width
|
||||
$chunkSize = $this->chunkSize - ($this->chunkSize % $charWidth);
|
||||
|
||||
$leftover = '';
|
||||
while (!feof($sourceHandle)) {
|
||||
$rawChunk = fread($sourceHandle, max(1, $chunkSize));
|
||||
if ($rawChunk === false || $rawChunk === '') {
|
||||
break; // @codeCoverageIgnore
|
||||
}
|
||||
if ($checkdigit !== 0) {
|
||||
$last1 = substr($rawChunk, $checkdigit, 1);
|
||||
if (in_array($last1, ["\xd8", "\xd9", "\xda", "\xdb"], true)) {
|
||||
$newChunk = fread($sourceHandle, 2);
|
||||
if ($newChunk === false) {
|
||||
break; // @codeCoverageIgnore
|
||||
}
|
||||
$rawChunk .= $newChunk;
|
||||
}
|
||||
}
|
||||
|
||||
$chunk = $leftover . $rawChunk;
|
||||
$leftover = '';
|
||||
|
||||
if ($charWidth > 1) {
|
||||
// For fixed-width multi-byte encodings (UTF-16, UTF-32),
|
||||
// ensure we don't split in the middle of a character
|
||||
$remainder = strlen($chunk) % $charWidth;
|
||||
if ($remainder !== 0) {
|
||||
$leftover = substr($chunk, -$remainder);
|
||||
$chunk = substr($chunk, 0, -$remainder);
|
||||
}
|
||||
}
|
||||
// For variable-width encodings (e.g. UTF-8 source, though
|
||||
// this path is for non-UTF-8), and single-byte encodings
|
||||
// (ISO-8859-*, CP1252), no boundary adjustment needed.
|
||||
// Single-byte encodings have 1:1 byte-to-character mapping.
|
||||
|
||||
if ($chunk !== '') {
|
||||
$converted = StringHelper::convertEncoding($chunk, 'UTF-8', $encoding);
|
||||
fwrite($outputHandle, $converted);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining bytes (incomplete multi-byte chars will throw)
|
||||
if ($leftover !== '') {
|
||||
$converted = StringHelper::convertEncoding($leftover, 'UTF-8', $encoding);
|
||||
fwrite($outputHandle, $converted); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
fclose($sourceHandle);
|
||||
$this->fileHandle = $outputHandle;
|
||||
$this->skipBOM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the byte width of a single character in the given encoding.
|
||||
* Returns 1 for variable-width or single-byte encodings.
|
||||
*/
|
||||
private function encodingCharWidth(string $encoding): int
|
||||
{
|
||||
return match ($encoding) {
|
||||
'UTF-32BE', 'UTF-32LE', 'UCS-4BE', 'UCS-4LE' => 4, // UTF-32 and UCS-4 are given BE/LE suffix above
|
||||
'UTF-16BE', 'UTF-16LE', 'UCS-2BE', 'UCS-2LE' => 2, // UTF-16 and UCS-2 are given BE/LE suffix above
|
||||
default => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetBenchmarks;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||
use PhpOffice\PhpSpreadsheetTests\Reader\Csv\CsvIconv2;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\Group('benchmark')]
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class CsvStreamingEncodingBenchmarkTest extends TestCase
|
||||
{
|
||||
private string $whichCsv = 'CsvChunk';
|
||||
//private string $whichCsv = 'CsvIconv2';
|
||||
//private string $whichCsv = 'Csv';
|
||||
|
||||
/** @var string[] */
|
||||
private array $tempFiles = [];
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ($this->tempFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
$this->tempFiles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSV file in ISO-8859-1 encoding with accented characters.
|
||||
*/
|
||||
private function generateIso88591Csv(int $rows, int $cols = 10): string
|
||||
{
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'csv_bench_iso_') . '.csv';
|
||||
$this->tempFiles[] = $tempFile;
|
||||
|
||||
$handle = fopen($tempFile, 'wb');
|
||||
self::assertNotFalse($handle);
|
||||
|
||||
// Accented characters in ISO-8859-1: é(0xE9), ö(0xF6), ü(0xFC), ñ(0xF1), à(0xE0), ç(0xE7)
|
||||
$specialChars = ["\xE9", "\xF6", "\xFC", "\xF1", "\xE0", "\xE7"];
|
||||
|
||||
for ($r = 0; $r < $rows; ++$r) {
|
||||
$fields = [];
|
||||
for ($c = 0; $c < $cols; ++$c) {
|
||||
$accent = $specialChars[($r + $c) % count($specialChars)];
|
||||
$fields[] = sprintf('Cell_%d_%d_%s_data_with_special_chars_%s', $r, $c, $accent, $accent);
|
||||
}
|
||||
fwrite($handle, implode(',', $fields) . "\n");
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSV file in UTF-16LE encoding with BOM and accented characters.
|
||||
*/
|
||||
private function generateUtf16LeCsv(int $rows, int $cols = 10): string
|
||||
{
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'csv_bench_u16_') . '.csv';
|
||||
$this->tempFiles[] = $tempFile;
|
||||
|
||||
$handle = fopen($tempFile, 'wb');
|
||||
self::assertNotFalse($handle);
|
||||
|
||||
// Write UTF-16LE BOM
|
||||
fwrite($handle, "\xFF\xFE");
|
||||
|
||||
$specialChars = ['é', 'ö', 'ü', 'ñ', 'à', 'ç'];
|
||||
|
||||
for ($r = 0; $r < $rows; ++$r) {
|
||||
$fields = [];
|
||||
for ($c = 0; $c < $cols; ++$c) {
|
||||
$accent = $specialChars[($r + $c) % count($specialChars)];
|
||||
$fields[] = sprintf('Cell_%d_%d_%s_data_with_special_chars_%s', $r, $c, $accent, $accent);
|
||||
}
|
||||
$line = implode(',', $fields) . "\n";
|
||||
// Convert UTF-8 line to UTF-16LE
|
||||
$converted = mb_convert_encoding($line, 'UTF-16LE', 'UTF-8');
|
||||
fwrite($handle, $converted);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a CSV file with the given encoding and return timing/memory stats.
|
||||
*
|
||||
* @return array{time_ms: float, memory_used_mb: float, rows: int}
|
||||
*/
|
||||
private function benchmarkRead(string $filename, string $encoding): array
|
||||
{
|
||||
// Force garbage collection before measurement
|
||||
gc_collect_cycles();
|
||||
$memoryBefore = memory_get_usage(true);
|
||||
|
||||
switch ($this->whichCsv) {
|
||||
case 'CsvChunk':
|
||||
fwrite(STDERR, "Using CsvChunk\n");
|
||||
$reader = new CsvChunk();
|
||||
|
||||
break;
|
||||
case 'CsvIconv2':
|
||||
fwrite(STDERR, "Using CsvIconv2\n");
|
||||
$reader = new CsvIconv2();
|
||||
|
||||
break;
|
||||
default:
|
||||
fwrite(STDERR, "Using Csv\n");
|
||||
$reader = new Csv();
|
||||
}
|
||||
$reader->setInputEncoding($encoding);
|
||||
|
||||
$startTime = hrtime(true);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$endTime = hrtime(true);
|
||||
|
||||
$memoryAfter = memory_get_usage(true);
|
||||
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$rows = $sheet->getHighestRow();
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
return [
|
||||
'time_ms' => ($endTime - $startTime) / 1_000_000,
|
||||
'memory_used_mb' => ($memoryAfter - $memoryBefore) / 1024 / 1024,
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
public function testStreamingEncodingIso88591(): void
|
||||
{
|
||||
$filename = $this->generateIso88591Csv(5000);
|
||||
$fileSize = filesize($filename);
|
||||
|
||||
$result = $this->benchmarkRead($filename, 'ISO-8859-1');
|
||||
|
||||
fwrite(STDERR, "\n");
|
||||
fwrite(STDERR, "=== ISO-8859-1 Streaming Encoding Benchmark ===\n");
|
||||
fwrite(STDERR, sprintf("File size: %.2f MB\n", $fileSize / 1024 / 1024));
|
||||
fwrite(STDERR, sprintf("Rows read: %d\n", $result['rows']));
|
||||
fwrite(STDERR, sprintf("Time: %.2f ms\n", $result['time_ms']));
|
||||
fwrite(STDERR, sprintf("Memory used: %.2f MB\n", $result['memory_used_mb']));
|
||||
|
||||
self::assertSame(5000, $result['rows']);
|
||||
}
|
||||
|
||||
public function testStreamingEncodingUtf16Le(): void
|
||||
{
|
||||
$filename = $this->generateUtf16LeCsv(5000);
|
||||
$fileSize = filesize($filename);
|
||||
|
||||
$result = $this->benchmarkRead($filename, 'UTF-16LE');
|
||||
|
||||
fwrite(STDERR, "\n");
|
||||
fwrite(STDERR, "=== UTF-16LE Streaming Encoding Benchmark ===\n");
|
||||
fwrite(STDERR, sprintf("File size: %.2f MB\n", $fileSize / 1024 / 1024));
|
||||
fwrite(STDERR, sprintf("Rows read: %d\n", $result['rows']));
|
||||
fwrite(STDERR, sprintf("Time: %.2f ms\n", $result['time_ms']));
|
||||
fwrite(STDERR, sprintf("Memory used: %.2f MB\n", $result['memory_used_mb']));
|
||||
|
||||
self::assertSame(5000, $result['rows']);
|
||||
}
|
||||
|
||||
public function testMemoryScalingWithFileSize(): void
|
||||
{
|
||||
// Generate small and large ISO-8859-1 CSV files
|
||||
$smallFile = $this->generateIso88591Csv(1000);
|
||||
$largeFile = $this->generateIso88591Csv(5000);
|
||||
|
||||
$smallFileSize = filesize($smallFile);
|
||||
$largeFileSize = filesize($largeFile);
|
||||
|
||||
$smallResult = $this->benchmarkRead($smallFile, 'ISO-8859-1');
|
||||
$largeResult = $this->benchmarkRead($largeFile, 'ISO-8859-1');
|
||||
|
||||
$fileSizeRatio = $largeFileSize / $smallFileSize;
|
||||
$smallMemory = $smallResult['memory_used_mb'];
|
||||
$memoryRatio = 0;
|
||||
if ($smallMemory > 0) {
|
||||
$memoryRatio = $largeResult['memory_used_mb'] / $smallMemory;
|
||||
}
|
||||
|
||||
fwrite(STDERR, "\n");
|
||||
fwrite(STDERR, "=== Memory Scaling Comparison (ISO-8859-1) ===\n");
|
||||
fwrite(STDERR, sprintf("Small file (1000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $smallFileSize / 1024 / 1024, $smallResult['memory_used_mb'], $smallResult['time_ms']));
|
||||
fwrite(STDERR, sprintf("Large file (5000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $largeFileSize / 1024 / 1024, $largeResult['memory_used_mb'], $largeResult['time_ms']));
|
||||
fwrite(STDERR, sprintf("File size ratio (large/small): %.2fx\n", $fileSizeRatio));
|
||||
fwrite(STDERR, sprintf("Memory ratio (large/small): %.2fx\n", $memoryRatio));
|
||||
fwrite(STDERR, sprintf("Streaming keeps memory sub-linear: %s\n", $memoryRatio < $fileSizeRatio ? 'YES' : 'NO'));
|
||||
|
||||
// With streaming, memory should not scale linearly with file size.
|
||||
// The encoding conversion itself uses constant memory (CHUNK_SIZE),
|
||||
// though the spreadsheet object will still grow with row count.
|
||||
// We verify memory ratio is less than file size ratio.
|
||||
self::assertLessThan(
|
||||
$fileSizeRatio,
|
||||
$memoryRatio,
|
||||
'Peak memory should grow sub-linearly relative to file size due to streaming encoding conversion'
|
||||
);
|
||||
|
||||
// Also generate small and large UTF-16LE files
|
||||
$smallUtf16 = $this->generateUtf16LeCsv(1000);
|
||||
$largeUtf16 = $this->generateUtf16LeCsv(5000);
|
||||
|
||||
$smallUtf16Size = filesize($smallUtf16);
|
||||
$largeUtf16Size = filesize($largeUtf16);
|
||||
|
||||
$smallUtf16Result = $this->benchmarkRead($smallUtf16, 'UTF-16LE');
|
||||
$largeUtf16Result = $this->benchmarkRead($largeUtf16, 'UTF-16LE');
|
||||
|
||||
$utf16FileSizeRatio = $largeUtf16Size / $smallUtf16Size;
|
||||
$smallMemory = $smallUtf16Result['memory_used_mb'];
|
||||
$utf16MemoryRatio = 0;
|
||||
if ($smallMemory > 0) {
|
||||
$utf16MemoryRatio = $largeUtf16Result['memory_used_mb'] / $smallMemory;
|
||||
}
|
||||
|
||||
fwrite(STDERR, "\n");
|
||||
fwrite(STDERR, "=== Memory Scaling Comparison (UTF-16LE) ===\n");
|
||||
fwrite(STDERR, sprintf("Small file (1000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $smallUtf16Size / 1024 / 1024, $smallUtf16Result['memory_used_mb'], $smallUtf16Result['time_ms']));
|
||||
fwrite(STDERR, sprintf("Large file (5000 rows): %.2f MB file, %.2f MB memory used, %.2f ms\n", $largeUtf16Size / 1024 / 1024, $largeUtf16Result['memory_used_mb'], $largeUtf16Result['time_ms']));
|
||||
fwrite(STDERR, sprintf("File size ratio (large/small): %.2fx\n", $utf16FileSizeRatio));
|
||||
fwrite(STDERR, sprintf("Memory ratio (large/small): %.2fx\n", $utf16MemoryRatio));
|
||||
fwrite(STDERR, sprintf("Streaming keeps memory sub-linear: %s\n", $utf16MemoryRatio < $utf16FileSizeRatio ? 'YES' : 'NO'));
|
||||
|
||||
self::assertLessThan(
|
||||
$utf16FileSizeRatio,
|
||||
$utf16MemoryRatio,
|
||||
'Peak memory should grow sub-linearly relative to file size due to streaming encoding conversion (UTF-16LE)'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetBenchmarks;
|
||||
|
||||
use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Csv;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
//use PhpOffice\PhpSpreadsheetBenchmarks\CsvChunk;
|
||||
use PhpOffice\PhpSpreadsheetTests\Reader\Csv\CsvIconv2;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for the streaming/chunked encoding conversion in the CSV reader.
|
||||
*
|
||||
* Verifies that encoding conversion produces correct results when
|
||||
* processing files in chunks rather than loading them entirely into memory.
|
||||
*/
|
||||
class CsvStreamingEncodingTest extends TestCase
|
||||
{
|
||||
private string $whichCsv = 'CsvChunk';
|
||||
//private string $whichCsv = 'CsvIconv2';
|
||||
//private string $whichCsv = 'Csv';
|
||||
|
||||
private string $tempFile = '';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->tempFile !== '') {
|
||||
unlink($this->tempFile);
|
||||
$this->tempFile = '';
|
||||
}
|
||||
}
|
||||
|
||||
private function newCsv(): Csv
|
||||
{
|
||||
if ($this->whichCsv === 'CsvChunk') {
|
||||
return new CsvChunk();
|
||||
}
|
||||
|
||||
if ($this->whichCsv === 'CsvIconv2') {
|
||||
return new CsvIconv2();
|
||||
}
|
||||
|
||||
return new Csv();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that existing non-UTF-8 CSV files are still read correctly
|
||||
* with the streaming approach.
|
||||
*/
|
||||
#[DataProvider('providerExistingEncodings')]
|
||||
public function testExistingNonUtf8FilesReadCorrectly(string $filename, string $encoding): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding($encoding);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertEquals("\u{00C5}", $sheet->getCell('A1')->getValue(), 'Å character should be preserved after streaming encoding conversion');
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public static function providerExistingEncodings(): array
|
||||
{
|
||||
return [
|
||||
'ISO-8859-1' => ['tests/data/Reader/CSV/encoding.iso88591.csv', 'ISO-8859-1'],
|
||||
'UTF-16BE' => ['tests/data/Reader/CSV/encoding.utf16be.csv', 'UTF-16BE'],
|
||||
'UTF-16LE' => ['tests/data/Reader/CSV/encoding.utf16le.csv', 'UTF-16LE'],
|
||||
'UTF-32BE' => ['tests/data/Reader/CSV/encoding.utf32be.csv', 'UTF-32BE'],
|
||||
'UTF-32LE' => ['tests/data/Reader/CSV/encoding.utf32le.csv', 'UTF-32LE'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that UTF-8 files (no conversion needed) are unaffected by the changes.
|
||||
*/
|
||||
public function testUtf8FileUnaffected(): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-8');
|
||||
$spreadsheet = $reader->load('tests/data/Reader/CSV/encoding.utf8.csv');
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertEquals("\u{00C5}", $sheet->getCell('A1')->getValue());
|
||||
$val = $sheet->getCell('B1')->getValue();
|
||||
self::assertIsScalar($val);
|
||||
self::assertEquals(1, (int) $val);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that UTF-8 BOM files work correctly without conversion.
|
||||
*/
|
||||
public function testUtf8BomFileUnaffected(): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-8');
|
||||
$spreadsheet = $reader->load('tests/data/Reader/CSV/encoding.utf8bom.csv');
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertEquals("\u{00C5}", $sheet->getCell('A1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testChunkBrokenUtf16BE(): void
|
||||
{
|
||||
$array = [
|
||||
"\xfe\xff", // bom
|
||||
"\xd8\x01\xdc\x00", // osmanya 𐐀
|
||||
"\x00\x20", // blank
|
||||
"\x00\x68", // h
|
||||
"\x00\x65", // e
|
||||
"\x00\x6c", // l
|
||||
"\x00\x6c", // l
|
||||
"\x00\x6f", // o
|
||||
"\x00\x20", // blank
|
||||
"\xd8\x01\xdc\x01", // osmanya 𐐁
|
||||
"\x00\x0a", // newline
|
||||
];
|
||||
$chars = implode('', $array);
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
$fho = fopen($filename, 'wb');
|
||||
self::assertNotFalse($fho);
|
||||
fwrite($fho, $chars);
|
||||
fclose($fho);
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-16');
|
||||
$reader->setDelimiter(',');
|
||||
if (method_exists($reader, 'setChunkSize')) {
|
||||
$reader->setChunkSize(2);
|
||||
}
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame('𐐀 hello 𐐁', $sheet->getCell('A1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testChunkBrokenUtf16LE(): void
|
||||
{
|
||||
$array = [
|
||||
"\xff\xfe", // bom
|
||||
"\x01\xd8\x00\xdc", // osmanya 𐐀
|
||||
"\x20\x00", // blank
|
||||
"\x68\x00", // h
|
||||
"\x65\x00", // e
|
||||
"\x6c\x00", // l
|
||||
"\x6c\x00", // l
|
||||
"\x6f\x00", // o
|
||||
"\x20\x00", // blank
|
||||
"\x01\xd8\x01\xdc", // osmanya 𐐁
|
||||
"\x0a\x00", // newline
|
||||
];
|
||||
$chars = implode('', $array);
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
$fho = fopen($filename, 'wb');
|
||||
self::assertNotFalse($fho);
|
||||
fwrite($fho, $chars);
|
||||
fclose($fho);
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-16');
|
||||
$reader->setDelimiter(',');
|
||||
if (method_exists($reader, 'setChunkSize')) {
|
||||
$reader->setChunkSize(2);
|
||||
}
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame('𐐀 hello 𐐁', $sheet->getCell('A1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testUnexpectedNoBom(): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-16');
|
||||
$reader->setDelimiter(',');
|
||||
$array = [
|
||||
"\xd8\x01\xdc\x00", // osmanya 𐐀
|
||||
"\x00\x20", // blank
|
||||
"\x00\x68", // h
|
||||
"\x00\x65", // e
|
||||
"\x00\x6c", // l
|
||||
"\x00\x6c", // l
|
||||
"\x00\x6f", // o
|
||||
"\x00\x20", // blank
|
||||
"\xd8\x01\xdc\x01", // osmanya 𐐁
|
||||
"\x00\x0a", // newline
|
||||
];
|
||||
$chars = implode('', $array);
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
$fho = fopen($filename, 'wb');
|
||||
self::assertNotFalse($fho);
|
||||
fwrite($fho, $chars);
|
||||
fclose($fho);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame('𐐀 hello 𐐁', $sheet->getCell('A1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testNoChunkedUtf7(): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-7');
|
||||
$reader->setDelimiter(',');
|
||||
$chars = 'Hello,World+ACE-';
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
$fho = fopen($filename, 'wb');
|
||||
self::assertNotFalse($fho);
|
||||
fwrite($fho, $chars);
|
||||
fclose($fho);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame('Hello', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('World!', $sheet->getCell('B1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Windows-1252 encoded file with various special characters
|
||||
* is correctly converted via streaming.
|
||||
*/
|
||||
public function testWindows1252SpecialCharacters(): void
|
||||
{
|
||||
// Build a CSV in Windows-1252 with various special characters
|
||||
$utf8Csv = "Name,City,Note\n"
|
||||
. "Müller,Zürich,Straße\n"
|
||||
. "Café,Père,£100\n"
|
||||
. "Smörgås,Göteborg,©2024\n";
|
||||
|
||||
$win1252Csv = mb_convert_encoding($utf8Csv, 'Windows-1252', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, $win1252Csv);
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('CP1252');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Row 1: headers
|
||||
self::assertSame('Name', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('City', $sheet->getCell('B1')->getValue());
|
||||
self::assertSame('Note', $sheet->getCell('C1')->getValue());
|
||||
|
||||
// Row 2: German characters
|
||||
self::assertSame('Müller', $sheet->getCell('A2')->getValue());
|
||||
self::assertSame('Zürich', $sheet->getCell('B2')->getValue());
|
||||
self::assertSame('Straße', $sheet->getCell('C2')->getValue());
|
||||
|
||||
// Row 3: French + symbol
|
||||
self::assertSame('Café', $sheet->getCell('A3')->getValue());
|
||||
self::assertSame('Père', $sheet->getCell('B3')->getValue());
|
||||
self::assertSame('£100', $sheet->getCell('C3')->getValue());
|
||||
|
||||
// Row 4: Swedish + symbol
|
||||
self::assertSame('Smörgås', $sheet->getCell('A4')->getValue());
|
||||
self::assertSame('Göteborg', $sheet->getCell('B4')->getValue());
|
||||
self::assertSame('©2024', $sheet->getCell('C4')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ISO-8859-1 encoded file with accented characters.
|
||||
*/
|
||||
public function testIso88591AccentedCharacters(): void
|
||||
{
|
||||
$utf8Csv = "première,deuxième,troisième\n"
|
||||
. "quatrième,cinquième,sixième\n";
|
||||
|
||||
$iso88591Csv = mb_convert_encoding($utf8Csv, 'ISO-8859-1', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, $iso88591Csv);
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('ISO-8859-1');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertSame('première', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('sixième', $sheet->getCell('C2')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a large non-UTF-8 file produces identical results to
|
||||
* what the old (non-streaming) approach would produce.
|
||||
* This verifies that chunk boundaries don't corrupt data.
|
||||
*/
|
||||
public function testLargeFileProducesCorrectResults(): void
|
||||
{
|
||||
// Generate a file large enough to span multiple chunks (>64KB).
|
||||
// Each row has accented characters to verify encoding conversion.
|
||||
$utf8Rows = [];
|
||||
$utf8Rows[] = 'id,name,description';
|
||||
for ($i = 1; $i <= 2000; ++$i) {
|
||||
$utf8Rows[] = sprintf(
|
||||
'%d,"Ñoño %d","Descripción número %d con carácteres especiales: äöüß"',
|
||||
$i,
|
||||
$i,
|
||||
$i
|
||||
);
|
||||
}
|
||||
$utf8Csv = implode("\n", $utf8Rows) . "\n";
|
||||
|
||||
// Convert to ISO-8859-1
|
||||
$isoCsv = mb_convert_encoding($utf8Csv, 'ISO-8859-1', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, $isoCsv);
|
||||
|
||||
// Verify file is large enough to trigger multiple chunks
|
||||
self::assertGreaterThan(65536, strlen($isoCsv), 'Test file should be larger than one chunk');
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('ISO-8859-1');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Check first row (header)
|
||||
self::assertSame('id', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('name', $sheet->getCell('B1')->getValue());
|
||||
self::assertSame('description', $sheet->getCell('C1')->getValue());
|
||||
|
||||
// Check several data rows including ones near chunk boundaries
|
||||
self::assertSame('Ñoño 1', $sheet->getCell('B2')->getValue());
|
||||
self::assertSame('Descripción número 1 con carácteres especiales: äöüß', $sheet->getCell('C2')->getValue());
|
||||
|
||||
// Check a row in the middle
|
||||
self::assertSame('Ñoño 1000', $sheet->getCell('B1001')->getValue());
|
||||
self::assertSame('Descripción número 1000 con carácteres especiales: äöüß', $sheet->getCell('C1001')->getValue());
|
||||
|
||||
// Check the last row
|
||||
self::assertSame('Ñoño 2000', $sheet->getCell('B2001')->getValue());
|
||||
self::assertSame('Descripción número 2000 con carácteres especiales: äöüß', $sheet->getCell('C2001')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a large UTF-16LE file to verify multi-byte character boundary
|
||||
* handling across chunks.
|
||||
*/
|
||||
public function testLargeUtf16leFile(): void
|
||||
{
|
||||
$utf8Rows = [];
|
||||
$utf8Rows[] = 'id,value';
|
||||
for ($i = 1; $i <= 1500; ++$i) {
|
||||
// Include characters that use multi-byte UTF-16 sequences
|
||||
$utf8Rows[] = sprintf('%d,"Ströëm café résumé #%d"', $i, $i);
|
||||
}
|
||||
$utf8Csv = implode("\n", $utf8Rows) . "\n";
|
||||
|
||||
// Convert to UTF-16LE (2 bytes per character)
|
||||
$utf16leCsv = mb_convert_encoding($utf8Csv, 'UTF-16LE', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
file_put_contents($filename, $utf16leCsv);
|
||||
|
||||
self::assertGreaterThan(65536, strlen($utf16leCsv), 'UTF-16LE file should be larger than one chunk');
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-16LE');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertSame('id', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('value', $sheet->getCell('B1')->getValue());
|
||||
self::assertSame('Ströëm café résumé #1', $sheet->getCell('B2')->getValue());
|
||||
self::assertSame('Ströëm café résumé #750', $sheet->getCell('B751')->getValue());
|
||||
self::assertSame('Ströëm café résumé #1500', $sheet->getCell('B1501')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a large UTF-32BE file to verify 4-byte character boundary
|
||||
* handling across chunks.
|
||||
*/
|
||||
public function testLargeUtf32beFile(): void
|
||||
{
|
||||
$utf8Rows = [];
|
||||
$utf8Rows[] = 'col1,col2';
|
||||
for ($i = 1; $i <= 1000; ++$i) {
|
||||
$utf8Rows[] = sprintf('%d,"Ünïcödé têst %d"', $i, $i);
|
||||
}
|
||||
$utf8Csv = implode("\n", $utf8Rows) . "\n";
|
||||
|
||||
$utf32beCsv = mb_convert_encoding($utf8Csv, 'UTF-32BE', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
file_put_contents($filename, $utf32beCsv);
|
||||
|
||||
self::assertGreaterThan(65536, strlen($utf32beCsv), 'UTF-32BE file should be larger than one chunk');
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-32BE');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertSame('col1', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('col2', $sheet->getCell('B1')->getValue());
|
||||
self::assertSame('Ünïcödé têst 1', $sheet->getCell('B2')->getValue());
|
||||
self::assertSame('Ünïcödé têst 1000', $sheet->getCell('B1001')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that listWorksheetInfo also works with the streaming approach
|
||||
* for non-UTF-8 files.
|
||||
*/
|
||||
public function testListWorksheetInfoWithNonUtf8(): void
|
||||
{
|
||||
$utf8Csv = "a,b,c\n1,2,3\n4,5,6\n";
|
||||
$isoCsv = mb_convert_encoding($utf8Csv, 'ISO-8859-1', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, $isoCsv);
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('ISO-8859-1');
|
||||
$info = $reader->listWorksheetInfo($filename);
|
||||
|
||||
self::assertCount(1, $info);
|
||||
self::assertSame(3, $info[0]['totalRows']);
|
||||
self::assertSame(3, $info[0]['totalColumns']);
|
||||
self::assertSame('C', $info[0]['lastColumnLetter']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that listWorksheetNames works with non-UTF-8 streaming path.
|
||||
*/
|
||||
public function testListWorksheetNamesWithNonUtf8(): void
|
||||
{
|
||||
$utf8Csv = "a,b,c\n1,2,3\n";
|
||||
$isoCsv = mb_convert_encoding($utf8Csv, 'ISO-8859-1', 'UTF-8');
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, $isoCsv);
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('ISO-8859-1');
|
||||
$names = $reader->listWorksheetNames($filename);
|
||||
|
||||
self::assertCount(1, $names);
|
||||
self::assertSame('Worksheet', $names[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an empty non-UTF-8 file is handled gracefully.
|
||||
*/
|
||||
public function testEmptyNonUtf8File(): void
|
||||
{
|
||||
$filename = $this->tempFile = File::temporaryFileName();
|
||||
file_put_contents($filename, '');
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('ISO-8859-1');
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($sheet->getCell('A1')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the leftover flush path: a UTF-16LE file whose total byte
|
||||
* count is odd triggers the leftover flush at lines 373-377 in Csv.php.
|
||||
*
|
||||
* The leftover contains an incomplete multi-byte character, so iconv
|
||||
* raises a warning (converted to an exception by the test error handler).
|
||||
*
|
||||
* Note that, if iconv is not available for some reason,
|
||||
* we will try mb_convert_encoding, which converts the leftover
|
||||
* to a question mark with no message of any kind.
|
||||
*/
|
||||
public function testLeftoverFlushWithUnalignedUtf16le(): void
|
||||
{
|
||||
$utf8Csv = "a,b\n1,2\n";
|
||||
$utf16leCsv = mb_convert_encoding($utf8Csv, 'UTF-16LE', 'UTF-8');
|
||||
// Append a single extra byte to make total length odd (not aligned to charWidth=2)
|
||||
$utf16leCsv .= "\x00";
|
||||
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
file_put_contents($filename, $utf16leCsv);
|
||||
|
||||
self::assertSame(1, strlen($utf16leCsv) % 2, 'File byte count should be odd to trigger leftover path');
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
if ($this->whichCsv === 'CsvIconv2') {
|
||||
$this->expectExceptionMessage('invalid multibyte sequence');
|
||||
} else {
|
||||
$this->expectExceptionMessage('Detected an incomplete multibyte character');
|
||||
}
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-16LE');
|
||||
$reader->load($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the leftover flush path with UTF-32BE: file byte count not
|
||||
* divisible by 4 triggers leftover handling with incomplete characters.
|
||||
*
|
||||
* Note that, if iconv is not available for some reason,
|
||||
* we will try mb_convert_encoding, which converts the leftover
|
||||
* to a question mark with no message of any kind.
|
||||
*/
|
||||
public function testLeftoverFlushWithUnalignedUtf32be(): void
|
||||
{
|
||||
$utf8Csv = "x,y\n3,4\n";
|
||||
$utf32beCsv = mb_convert_encoding($utf8Csv, 'UTF-32BE', 'UTF-8');
|
||||
// Append 2 extra bytes so length % 4 != 0
|
||||
$utf32beCsv .= "\x00\x00";
|
||||
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
file_put_contents($filename, $utf32beCsv);
|
||||
|
||||
self::assertNotSame(0, strlen($utf32beCsv) % 4, 'File byte count should not be aligned to 4 to trigger leftover path');
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
if ($this->whichCsv === 'CsvIconv2') {
|
||||
$this->expectExceptionMessage('invalid multibyte sequence');
|
||||
} else {
|
||||
$this->expectExceptionMessage('Detected an incomplete multibyte character');
|
||||
}
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding('UTF-32BE');
|
||||
$reader->load($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test encodingCharWidth returns 2 for UCS-2 variants by loading
|
||||
* a file with UCS-2BE input encoding.
|
||||
*/
|
||||
#[DataProvider('providerUcsEncodings')]
|
||||
public function testUcsEncodingVariants(string $inputEncoding, string $mbEncoding, int $charWidth): void
|
||||
{
|
||||
$utf8Csv = "a,b\n1,2\n";
|
||||
$encoded = mb_convert_encoding($utf8Csv, $mbEncoding, 'UTF-8');
|
||||
|
||||
$filename = $this->tempFile = File::temporaryFileName() . '.csv';
|
||||
file_put_contents($filename, $encoded);
|
||||
|
||||
// Verify encoding alignment: byte count should be divisible by char width
|
||||
self::assertSame(0, strlen($encoded) % $charWidth);
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding($inputEncoding);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertSame('a', $sheet->getCell('A1')->getValue());
|
||||
self::assertSame('b', $sheet->getCell('B1')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public static function providerUcsEncodings(): array
|
||||
{
|
||||
return [
|
||||
'UCS-2BE' => ['UCS-2BE', 'UCS-2BE', 2],
|
||||
'UCS-2LE' => ['UCS-2LE', 'UCS-2LE', 2],
|
||||
'UCS-4BE' => ['UCS-4BE', 'UCS-4BE', 4],
|
||||
'UCS-4LE' => ['UCS-4LE', 'UCS-4LE', 4],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loadSpreadsheetFromString with non-UTF-8 content pre-converted
|
||||
* to UTF-8. This verifies the string loading path works for content
|
||||
* that was originally in a different encoding.
|
||||
*/
|
||||
public function testLoadFromStringWithConvertedEncoding(): void
|
||||
{
|
||||
$utf8Csv = "Name,City\nMüller,Zürich\nCafé,Père\n";
|
||||
|
||||
$reader = $this->newCsv();
|
||||
$spreadsheet = $reader->loadSpreadsheetFromString($utf8Csv);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertSame('Müller', $sheet->getCell('A2')->getValue());
|
||||
self::assertSame('Zürich', $sheet->getCell('B2')->getValue());
|
||||
self::assertSame('Café', $sheet->getCell('A3')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test guess encoding still works with the streaming path.
|
||||
*/
|
||||
#[DataProvider('providerGuessEncodingStreaming')]
|
||||
public function testGuessEncodingWithStreaming(string $filename): void
|
||||
{
|
||||
$reader = $this->newCsv();
|
||||
$reader->setInputEncoding(Csv::GUESS_ENCODING);
|
||||
$spreadsheet = $reader->load($filename);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertEquals('première', $sheet->getCell('A1')->getValue());
|
||||
self::assertEquals('sixième', $sheet->getCell('C2')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public static function providerGuessEncodingStreaming(): array
|
||||
{
|
||||
return [
|
||||
'UTF-16BE' => ['tests/data/Reader/CSV/premiere.utf16be.csv'],
|
||||
'UTF-16LE' => ['tests/data/Reader/CSV/premiere.utf16le.csv'],
|
||||
'UTF-32BE' => ['tests/data/Reader/CSV/premiere.utf32be.csv'],
|
||||
'UTF-32LE' => ['tests/data/Reader/CSV/premiere.utf32le.csv'],
|
||||
'Win-1252' => ['tests/data/Reader/CSV/premiere.win1252.csv'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user