diff --git a/src/os/csv/csv_flavours.php b/src/os/csv/csv_flavours.php new file mode 100644 index 0000000..c240249 --- /dev/null +++ b/src/os/csv/csv_flavours.php @@ -0,0 +1,46 @@ + 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); + } +} diff --git a/src/os/file/Stream.php b/src/os/file/Stream.php index b9767f5..4ed0296 100644 --- a/src/os/file/Stream.php +++ b/src/os/file/Stream.php @@ -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(); diff --git a/src/ref/os/csv/ref_csv.php b/src/ref/os/csv/ref_csv.php new file mode 100644 index 0000000..dceb0a8 --- /dev/null +++ b/src/ref/os/csv/ref_csv.php @@ -0,0 +1,20 @@ +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(); + } +} diff --git a/tests/os/file/impl/avec_bom.csv b/tests/os/file/impl/avec_bom.csv new file mode 100644 index 0000000..d1512a2 --- /dev/null +++ b/tests/os/file/impl/avec_bom.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/tests/os/file/impl/avec_bom.txt b/tests/os/file/impl/avec_bom.txt new file mode 100644 index 0000000..9e55899 --- /dev/null +++ b/tests/os/file/impl/avec_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/tests/os/file/impl/msexcel.csv b/tests/os/file/impl/msexcel.csv new file mode 100644 index 0000000..b2d95c4 --- /dev/null +++ b/tests/os/file/impl/msexcel.csv @@ -0,0 +1,2 @@ +nom;prenom;age +clain;jephte;50 diff --git a/tests/os/file/impl/ooffice.csv b/tests/os/file/impl/ooffice.csv new file mode 100644 index 0000000..f00d4ff --- /dev/null +++ b/tests/os/file/impl/ooffice.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/tests/os/file/impl/sans_bom.txt b/tests/os/file/impl/sans_bom.txt new file mode 100644 index 0000000..f16e49f --- /dev/null +++ b/tests/os/file/impl/sans_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/tests/os/file/impl/weird.tsv b/tests/os/file/impl/weird.tsv new file mode 100644 index 0000000..cd8bf3a --- /dev/null +++ b/tests/os/file/impl/weird.tsv @@ -0,0 +1,2 @@ +nom prenom age +clain jephte 50