support lecture et écriture CSV
This commit is contained in:
parent
6a837fbcb3
commit
e3e3fc91f4
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
namespace nur\sery\os\file;
|
||||
|
||||
use nur\sery\NoMoreDataException;
|
||||
use nur\sery\os\csv\csv_flavours;
|
||||
use nur\sery\os\EOFException;
|
||||
use nur\sery\os\IOException;
|
||||
use nur\sery\php\iter\AbstractIterator;
|
||||
use nur\sery\ref\os\csv\ref_csv;
|
||||
use nur\sery\str;
|
||||
use nur\sery\ValueException;
|
||||
|
||||
|
@ -139,6 +141,40 @@ class Stream extends AbstractIterator implements IReader, IWriter {
|
|||
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
|
||||
|
||||
|
@ -168,6 +204,18 @@ class Stream extends AbstractIterator implements IReader, IWriter {
|
|||
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
|
||||
* de ligne [\r]\n
|
||||
|
@ -267,6 +315,12 @@ class Stream extends AbstractIterator implements IReader, IWriter {
|
|||
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 */
|
||||
function fflush(): self {
|
||||
$fd = $this->getResource();
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
nom,prenom,age
|
||||
clain,jephte,50
|
|
|
@ -0,0 +1 @@
|
|||
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789
|
|
@ -0,0 +1,2 @@
|
|||
nom;prenom;age
|
||||
clain;jephte;50
|
|
|
@ -0,0 +1,2 @@
|
|||
nom,prenom,age
|
||||
clain,jephte,50
|
|
|
@ -0,0 +1 @@
|
|||
0123456789ABCDEFGHIJ0123456789abcdefghij0123456789
|
|
@ -0,0 +1,2 @@
|
|||
nom prenom age
|
||||
clain jephte 50
|
|
Loading…
Reference in New Issue