support lecture et écriture CSV

This commit is contained in:
Jephté Clain 2024-04-23 05:40:56 +04:00
parent 6a837fbcb3
commit e3e3fc91f4
10 changed files with 192 additions and 0 deletions

View File

@ -0,0 +1,46 @@
<?php
namespace nur\sery\os\csv;
use nur\A;
use nur\ref\ref_csv;
class csv_flavours {
const MAP = [
"oo" => ref_csv::OO_FLAVOUR,
"ooffice" => ref_csv::OO_FLAVOUR,
ref_csv::OO_NAME => ref_csv::OO_FLAVOUR,
"xl" => ref_csv::XL_FLAVOUR,
"excel" => ref_csv::XL_FLAVOUR,
ref_csv::XL_NAME => ref_csv::XL_FLAVOUR,
];
const ENCODINGS = [
ref_csv::OO_FLAVOUR => ref_csv::OO_ENCODING,
ref_csv::XL_FLAVOUR => ref_csv::XL_ENCODING,
];
static final function verifix(string $flavour): string {
$lflavour = strtolower($flavour);
if (array_key_exists($lflavour, self::MAP)) {
$flavour = self::MAP[$lflavour];
}
if (strlen($flavour) < 1) $flavour .= ",";
if (strlen($flavour) < 2) $flavour .= "\"";
if (strlen($flavour) < 3) $flavour .= "\\";
return $flavour;
}
static final function get_name(string $flavour): string {
if ($flavour == ref_csv::OO_FLAVOUR) return ref_csv::OO_NAME;
elseif ($flavour == ref_csv::XL_FLAVOUR) return ref_csv::XL_NAME;
else return $flavour;
}
static final function get_params(string $flavour): array {
return [$flavour[0], $flavour[1], $flavour[2]];
}
static final function get_encoding(string $flavour): ?string {
return A::get(self::ENCODINGS, $flavour);
}
}

View File

@ -2,9 +2,11 @@
namespace nur\sery\os\file; namespace nur\sery\os\file;
use nur\sery\NoMoreDataException; use nur\sery\NoMoreDataException;
use nur\sery\os\csv\csv_flavours;
use nur\sery\os\EOFException; use nur\sery\os\EOFException;
use nur\sery\os\IOException; use nur\sery\os\IOException;
use nur\sery\php\iter\AbstractIterator; use nur\sery\php\iter\AbstractIterator;
use nur\sery\ref\os\csv\ref_csv;
use nur\sery\str; use nur\sery\str;
use nur\sery\ValueException; use nur\sery\ValueException;
@ -139,6 +141,40 @@ class Stream extends AbstractIterator implements IReader, IWriter {
if ($closeReader) $this->close(); if ($closeReader) $this->close();
} }
const DEFAULT_CSV_FLAVOUR = ref_csv::OO_FLAVOUR;
/** @var array paramètres pour la lecture et l'écriture de flux au format CSV */
protected $csvFlavour;
function setCsvFlavour(string $flavour): void {
$this->csvFlavour = csv_flavours::verifix($flavour);
}
protected function getCsvParams($fd): array {
$flavour = $this->csvFlavour;
if ($flavour === null) {
if ($fd === null) {
# utiliser la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR;
} else {
# il faut déterminer le type de fichier CSV en lisant la première ligne
$pos = IOException::ensure_valid(ftell($fd));
$line = IOException::ensure_valid(fgets($fd));
$line = strpbrk($line, ",;\t");
if ($line === false) {
# aucun séparateur trouvé, prender la valeur par défaut
$flavour = static::DEFAULT_CSV_FLAVOUR;
} else {
$flavour = substr($line, 0, 1);
$flavour = csv_flavours::verifix($flavour);
}
IOException::ensure_valid(fseek($fd, $pos), true, -1);
}
$this->csvFlavour = $flavour;
}
return csv_flavours::get_params($flavour);
}
############################################################################# #############################################################################
# Reader # Reader
@ -168,6 +204,18 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return IOException::ensure_valid(fpassthru($fd), $this->throwOnError); return IOException::ensure_valid(fpassthru($fd), $this->throwOnError);
} }
/**
* retourner la prochaine ligne au format CSV ou null si le fichier est arrivé
* à sa fin
*/
function fgetcsv(): ?array {
$fd = $this->getResource();
$params = $this->getCsvParams($fd);
$row = fgetcsv($fd, 0, $params[0], $params[1], $params[2]);
if ($row === false && feof($fd)) return null;
return IOException::ensure_valid($row, $this->throwOnError);
}
/** /**
* lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin * lire la prochaine ligne. la ligne est retournée *sans* le caractère de fin
* de ligne [\r]\n * de ligne [\r]\n
@ -267,6 +315,12 @@ class Stream extends AbstractIterator implements IReader, IWriter {
return IOException::ensure_valid($r, $this->throwOnError); return IOException::ensure_valid($r, $this->throwOnError);
} }
function fputcsv(array $row): void {
$fd = $this->getResource();
$params = $this->getCsvParams($fd);
IOException::ensure_valid(fputcsv($fd, $row, $params[0], $params[1], $params[2]));
}
/** @throws IOException */ /** @throws IOException */
function fflush(): self { function fflush(): self {
$fd = $this->getResource(); $fd = $this->getResource();

View File

@ -0,0 +1,20 @@
<?php
namespace nur\sery\ref\os\csv;
/**
* Class ref_csv: références des valeurs normalisées pour les fichiers CSV à
* destination de Microsoft Excel et de LibreOffice Calc
*/
class ref_csv {
const UTF8 = "utf-8";
const CP1252 = "cp1252";
const LATIN1 = "iso-8859-1";
const OO_NAME = "oocalc";
const OO_FLAVOUR = ",\"\\";
const OO_ENCODING = self::UTF8;
const XL_NAME = "msexcel";
const XL_FLAVOUR = ";\"\\";
const XL_ENCODING = self::CP1252;
}

View File

@ -0,0 +1,62 @@
<?php
namespace nur\sery\os\file;
use PHPUnit\Framework\TestCase;
class FileReaderTest extends TestCase {
function testIgnoreBom() {
# la lecture avec et sans BOM doit être identique
## sans BOM
$reader = new FileReader(__DIR__.'/impl/sans_bom.txt');
self::assertSame("0123456789", $reader->fread(10));
self::assertSame(10, $reader->ftell());
$reader->seek(30);
self::assertSame("abcdefghij", $reader->fread(10));
self::assertSame(40, $reader->ftell());
$reader->seek(10);
self::assertSame("ABCDEFGHIJ", $reader->fread(10));
self::assertSame(20, $reader->ftell());
$reader->seek(40);
self::assertSame("0123456789\n", $reader->getContents());
$reader->close();
## avec BOM
$reader = new FileReader(__DIR__.'/impl/avec_bom.txt');
self::assertSame("0123456789", $reader->fread(10));
self::assertSame(10, $reader->ftell());
$reader->seek(30);
self::assertSame("abcdefghij", $reader->fread(10));
self::assertSame(40, $reader->ftell());
$reader->seek(10);
self::assertSame("ABCDEFGHIJ", $reader->fread(10));
self::assertSame(20, $reader->ftell());
$reader->seek(40);
self::assertSame("0123456789\n", $reader->getContents());
$reader->close();
}
function testCsvAutoParams() {
$reader = new FileReader(__DIR__.'/impl/msexcel.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__.'/impl/ooffice.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__.'/impl/weird.tsv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
$reader = new FileReader(__DIR__.'/impl/avec_bom.csv');
self::assertSame(["nom", "prenom", "age"], $reader->fgetcsv());
self::assertSame(["clain", "jephte", "50"], $reader->fgetcsv());
self::assertNull($reader->fgetcsv());
$reader->close();
}
}

View File

@ -0,0 +1,2 @@
nom,prenom,age
clain,jephte,50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1 @@
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789

View File

@ -0,0 +1,2 @@
nom;prenom;age
clain;jephte;50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1,2 @@
nom,prenom,age
clain,jephte,50
1 nom prenom age
2 clain jephte 50

View File

@ -0,0 +1 @@
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789

View File

@ -0,0 +1,2 @@
nom prenom age
clain jephte 50
1 nom prenom age
2 clain jephte 50