diff --git a/composer.json b/composer.json index 544d425..4ff2b3f 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,8 @@ }, "require-dev": { "nulib/tests": "7.4", + "ext-posix": "*", + "ext-pcntl": "*", "ext-curl": "*" }, "autoload": { @@ -20,6 +22,9 @@ "nulib\\": "php/src_base", "nulib\\ref\\": "php/src_ref", "nulib\\php\\": "php/src_php", + "nulib\\os\\": "php/src_os", + "nulib\\file\\": "php/src_file", + "nulib\\values\\": "php/src_values", "nulib\\output\\": "php/src_output", "nulib\\web\\": "php/src_web" } diff --git a/composer.lock b/composer.lock index 628a628..609426a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d0cd1ea89a82b87aa4a078dcea524293", + "content-hash": "0f0743123abf677caf20c5e71ece9616", "packages": [], "packages-dev": [ { @@ -665,16 +665,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.18", + "version": "9.6.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04" + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04", - "reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", "shasum": "" }, "require": { @@ -748,7 +748,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.18" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" }, "funding": [ { @@ -764,7 +764,7 @@ "type": "tidelift" } ], - "time": "2024-03-21T12:07:32+00:00" + "time": "2024-04-05T04:35:58+00:00" }, { "name": "sebastian/cli-parser", @@ -1789,6 +1789,8 @@ "php": ">=7.4" }, "platform-dev": { + "ext-posix": "*", + "ext-pcntl": "*", "ext-curl": "*" }, "plugin-api-version": "2.2.0" diff --git a/php/src_base/AccessException.php b/php/src_base/AccessException.php index 13a4d4a..6996667 100644 --- a/php/src_base/AccessException.php +++ b/php/src_base/AccessException.php @@ -1,8 +1,6 @@ $value) { + if ($key === $index) { + $index++; + $parts[] = self::value($value); + } else { + $parts[] = "$key=>".self::value($value); + } } return "[" . implode(", ", $parts) . "]"; } elseif (is_string($value)) { diff --git a/php/src_base/cl.php b/php/src_base/cl.php index dff62ee..21c3202 100644 --- a/php/src_base/cl.php +++ b/php/src_base/cl.php @@ -141,6 +141,15 @@ class cl { return $array !== null? array_keys($array): []; } + /** + * retourner la première valeur de $array ou $default si le tableau est null + * ou vide + */ + static final function first($array, $default=null) { + if (is_array($array)) return $array[array_key_first($array)]; + return $default; + } + ############################################################################# /** diff --git a/php/src_base/cv.php b/php/src_base/cv.php index 796a6da..8fdcc4b 100644 --- a/php/src_base/cv.php +++ b/php/src_base/cv.php @@ -79,6 +79,15 @@ class cv { ############################################################################# + /** échanger les deux valeurs */ + static final function swap(&$a, &$b): void { + $tmp = $a; + $a = $b; + $b = $tmp; + } + + ############################################################################# + /** mettre à jour $dest avec $value si $cond($value) est vrai */ static final function set_if(&$dest, $value, callable $cond) { if ($cond($value)) $dest = $value; diff --git a/php/src_base/file.php b/php/src_base/file.php new file mode 100644 index 0000000..360f35f --- /dev/null +++ b/php/src_base/file.php @@ -0,0 +1,86 @@ +close(); + } + } + return $file; + } + + static function writer($output, ?string $mode=null, ?callable $func=null): FileWriter { + $file = new FileWriter($output, $mode); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function shared($file, ?callable $func=null): SharedFile { + $file = new SharedFile($file); + if ($func !== null) { + try { + $func($file); + } finally { + $file ->close(); + } + } + return $file; + } + + static function tmpwriter($destdir=null, ?callable $func=null): TmpfileWriter { + $file = new TmpfileWriter($destdir); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function memory(?callable $func=null): MemoryStream { + $file = new MemoryStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } + + static function temp(?callable $func=null): TempStream { + $file = new TempStream(); + if ($func !== null) { + try { + $func($file); + } finally { + $file->close(); + } + } + return $file; + } +} diff --git a/php/src_file/IReader.php b/php/src_file/IReader.php new file mode 100644 index 0000000..980a34f --- /dev/null +++ b/php/src_file/IReader.php @@ -0,0 +1,52 @@ +ignoreBom = $ignoreBom; + if ($input === null) { + $fd = STDIN; + $close = false; + } elseif (is_resource($input)) { + $fd = $input; + $close = false; + } else { + $file = $input; + if ($mode === null) $mode = static::DEFAULT_MODE; + $this->file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } + + /** @return resource */ + protected function open() { + $fd = parent::open(); + $this->haveBom = false; + if ($this->ignoreBom) { + $bom = fread($fd, 3); + if ($bom === "\xEF\xBB\xBF") $this->seekOffset = 3; + else rewind($fd); + } + return $fd; + } + + function haveBom(): bool { + return $this->seekOffset !== 0; + } +} diff --git a/php/src_file/base/FileWriter.php b/php/src_file/base/FileWriter.php new file mode 100644 index 0000000..fab7404 --- /dev/null +++ b/php/src_file/base/FileWriter.php @@ -0,0 +1,31 @@ +file = $file; + $this->mode = $mode; + $fd = $this->open(); + $close = true; + } + parent::__construct($fd, $close, $throwOnError, $allowLocking); + } +} diff --git a/php/src_file/base/MemoryStream.php b/php/src_file/base/MemoryStream.php new file mode 100644 index 0000000..0b36a59 --- /dev/null +++ b/php/src_file/base/MemoryStream.php @@ -0,0 +1,21 @@ +fd === null) $this->fd = self::memory_fd(); + return parent::getResource(); + } +} diff --git a/php/src_file/base/SharedFile.php b/php/src_file/base/SharedFile.php new file mode 100644 index 0000000..f706533 --- /dev/null +++ b/php/src_file/base/SharedFile.php @@ -0,0 +1,15 @@ +fd = $fd; + $this->close = $close; + $this->throwOnError = $throwOnError; + if ($useLocking === null) $useLocking = static::USE_LOCKING; + $this->useLocking = $useLocking; + } + + ############################################################################# + # File + + /** + * @return resource|null retourner la resource associée à ce fichier si cela + * a du sens + */ + function getResource() { + IOException::ensure_open($this->fd === null); + $this->_streamAppendFilters($this->fd); + return $this->fd; + } + + protected function lock(int $operation, ?int &$wouldBlock=null): bool { + $locked = flock($this->getResource(), $operation, $wouldBlock); + if ($locked) return true; + if ($operation & LOCK_NB) return false; + else throw IOException::error(); + } + + protected function unlock(bool $close=false): void { + if ($this->fd !== null) { + flock($this->fd, LOCK_UN); + if ($close) $this->close(); + } + } + + function isatty(): bool { + return stream_isatty($this->getResource()); + } + + /** obtenir des informations sur le fichier */ + function fstat(bool $reload=false): array { + if ($this->stat === null || $reload) { + $fd = $this->getResource(); + $this->stat = IOException::ensure_valid(fstat($fd), $this->throwOnError); + } + return $this->stat; + } + + function getSize(?int $seekOffset=null): int { + if ($seekOffset === null) $seekOffset = $this->seekOffset; + return $this->fstat()["size"] - $seekOffset; + } + + /** @throws IOException */ + function ftell(?int $seekOffset=null): int { + $fd = $this->getResource(); + if ($seekOffset === null) $seekOffset = $this->seekOffset; + return IOException::ensure_valid(ftell($fd), $this->throwOnError) - $seekOffset; + } + + /** + * @return int la position après avoir déplacé le pointeur + * @throws IOException + */ + function fseek(int $offset, int $whence=SEEK_SET, ?int $seekOffset=null): int { + $fd = $this->getResource(); + if ($seekOffset === null) $seekOffset = $this->seekOffset; + if ($whence === SEEK_SET) $offset += $seekOffset; + IOException::ensure_valid(fseek($fd, $offset, $whence), $this->throwOnError, -1); + return $this->ftell($seekOffset); + } + + function seek(int $offset, int $whence=SEEK_SET): self { + $this->fseek($offset, $whence); + return $this; + } + + /** fermer le fichier si c'est nécessaire */ + function close(bool $close=true, ?int $ifSerial=null): void { + AbstractIterator::rewind(); + if ($this->fd !== null && $close && $this->close && ($ifSerial === null || $this->serial === $ifSerial)) { + fclose($this->fd); + $this->fd = null; + } + } + + function copyTo(IWriter $dest, bool $closeWriter=false, bool $closeReader=true): void { + $srcr = $this->getResource(); + $destr = $dest->getResource(); + if ($srcr !== null && $destr !== null) { + while (!feof($srcr)) { + fwrite($destr, fread($srcr, 8192)); + } + } else { + $dest->fwrite($this->getContents(false)); + } + if ($closeWriter) $dest->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 + + /** @throws IOException */ + function fread(int $length): string { + $fd = $this->getResource(); + return IOException::ensure_valid(fread($fd, $length), $this->throwOnError); + } + + /** + * lire la prochaine ligne. la ligne est retournée avec le caractère de fin + * de ligne[\r]\n + * + * @throws EOFException si plus aucune ligne n'est disponible + * @throws IOException si une erreur se produit + */ + function fgets(?int $length=null): string { + $fd = $this->getResource(); + if ($length === null) $r = fgets($fd); + else $r = fgets($fd, $length); + return EOFException::ensure_not_eof($r, $this->throwOnError); + } + + /** @throws IOException */ + function fpassthru(): int { + $fd = $this->getResource(); + 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 + * + * @throws EOFException si plus aucune ligne n'est disponible + * @throws IOException si une erreur se produit + */ + function readLine(): ?string { + return str::strip_nl($this->fgets()); + } + + /** lire et retourner toutes les lignes */ + function readLines(): array { + return iterator_to_array($this); + } + + /** + * essayer de verrouiller le fichier en lecture. retourner true si l'opération + * réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument + * true + */ + function canRead(): bool { + if ($this->useLocking) return $this->lock(LOCK_SH + LOCK_NB); + else return true; + } + + /** + * verrouiller en mode partagé puis retourner un objet permettant de lire le + * fichier. + */ + function getReader(bool $lockedByCanRead=false): IReader { + if ($this->useLocking && !$lockedByCanRead) $this->lock(LOCK_SH); + return new class($this->fd, ++$this->serial, $this) extends Stream { + function __construct($fd, int $serial, Stream $parent) { + $this->parent = $parent; + parent::__construct($fd); + } + + /** @var Stream */ + private $parent; + + function close(bool $close=true): void { + if ($this->parent !== null && $close) { + $this->parent->close(true, $this->serial); + $this->fd = null; + $this->parent = null; + } + } + }; + } + + /** retourner le contenu du fichier sous forme de chaine */ + function getContents(bool $close=true, bool $lockedByCanRead=false): string { + $useLocking = $this->useLocking; + if ($useLocking && !$lockedByCanRead) $this->lock(LOCK_SH); + try { + return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false) { + $args = [$this->getContents($lockedByCanRead)]; + if ($options !== null) $args[] = $options; + return unserialize(...$args); + } + + ############################################################################# + # Iterator + + protected function _setup(): void { + } + + protected function _next(&$key) { + try { + return $this->fgets(); + } catch (EOFException $e) { + throw new NoMoreDataException(); + } + } + + protected function _teardown(): void { + $md = stream_get_meta_data($this->fd); + if ($md["seekable"]) $this->fseek(0); + } + + ############################################################################# + # Writer + + /** @throws IOException */ + function fwrite(string $data, ?int $length=null): int { + $fd = $this->getResource(); + if ($length === null) $r = fwrite($fd, $data); + else $r = fwrite($fd, $data, $length); + 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(); + IOException::ensure_valid(fflush($fd), $this->throwOnError); + return $this; + } + + /** @throws IOException */ + function ftruncate(int $size): self { + $fd = $this->getResource(); + IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError); + return $this; + } + + function writeLines(?iterable $lines): IWriter { + if ($lines !== null) { + foreach ($lines as $line) { + $this->fwrite($line); + $this->fwrite("\n"); + } + } + return $this; + } + + /** + * essayer de verrouiller le fichier en écriture. retourner true si l'opération + * réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument + * true + */ + function canWrite(): bool { + if ($this->useLocking) return $this->lock(LOCK_EX + LOCK_NB); + else return true; + } + + /** + * verrouiller en mode exclusif puis retourner un objet permettant d'écrire + * dans le fichier + */ + function getWriter(bool $lockedByCanWrite=false): IWriter { + if ($this->useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX); + return new class($this->fd, ++$this->serial, $this) extends Stream { + function __construct($fd, int $serial, Stream $parent) { + $this->parent = $parent; + $this->serial = $serial; + parent::__construct($fd); + } + + /** @var Stream */ + private $parent; + + function close(bool $close=true): void { + if ($this->parent !== null && $close) { + $this->parent->close(true, $this->serial); + $this->fd = null; + $this->parent = null; + } + } + }; + } + + function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void { + $useLocking = $this->useLocking; + if ($useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX); + try { + $this->fwrite($contents); + } finally { + if ($useLocking) $this->unlock($close); + elseif ($close) $this->close(); + } + } + + function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void { + $this->putContents(serialize($object), $lockedByCanWrite); + } +} diff --git a/php/src_file/base/TStreamFilter.php b/php/src_file/base/TStreamFilter.php new file mode 100644 index 0000000..5f15ef2 --- /dev/null +++ b/php/src_file/base/TStreamFilter.php @@ -0,0 +1,50 @@ +filters[] = [$filterName, $readWrite, $params]; + } + + function prependFilter(string $filterName, ?int $readWrite=null, $params=null): void { + if ($this->filters === null) $this->filters = []; + array_unshift($this->filters, [$filterName, $readWrite, $params]); + } + + function setEncodingFilter(string $from, string $to): void { + if ($to !== $from) { + $this->appendFilter("convert.iconv.$from.$to"); + } + } + + /** + * @param $fd resource + * @throws IOException + */ + protected function _streamAppendFilters($fd): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + if (stream_filter_append($fd, $filterName, $readWrite, $params) === false) { + throw new IOException("unable to add filter $filterName"); + } + } + $this->filters = null; + } + } + + /** + * @param $file _IFile + */ + protected function _appendFilters($file): void { + if ($this->filters !== null) { + foreach ($this->filters as [$filterName, $readWrite, $params]) { + $file->appendFilter($filterName, $readWrite, $params); + } + } + } +} diff --git a/php/src_file/base/TempStream.php b/php/src_file/base/TempStream.php new file mode 100644 index 0000000..a6715fe --- /dev/null +++ b/php/src_file/base/TempStream.php @@ -0,0 +1,29 @@ +maxMemory = $maxMemory; + parent::__construct($this->tempFd(), true, $throwOnError); + } + + /** @var int */ + protected $maxMemory; + + protected function tempFd() { + return fopen("php://temp/maxmemory:$this->maxMemory", "w+b"); + } + + function getResource() { + if ($this->fd === null) $this->fd = $this->tempFd(); + return parent::getResource(); + } +} diff --git a/php/src_file/base/TmpfileWriter.php b/php/src_file/base/TmpfileWriter.php new file mode 100644 index 0000000..071f1c2 --- /dev/null +++ b/php/src_file/base/TmpfileWriter.php @@ -0,0 +1,93 @@ +delete = true; + } elseif (is_file($destdir)) { + # si on spécifie un fichier qui existe le prendre comme "fichier + # temporaire" mais ne pas le supprimer automatiquement + $file = $destdir; + if (!path::is_qualified($file)) $file = path::join($tmpDir, $file); + $this->delete = false; + } else { + # un chemin qui n'existe pas: ne le sélectionner que si le répertoire + # existe. dans ce cas, le fichier sera créé automatiquement, mais pas + # supprimé + if (!is_dir(dirname($destdir))) { + throw new IOException("$destdir: no such file or directory"); + } + $file = $destdir; + $this->delete = false; + } + parent::__construct($file, $mode, $throwOnError, $allowLocking); + } + + /** @var bool */ + protected $delete; + + function __destruct() { + $this->close(); + if ($this->delete) $this->delete(); + } + + /** supprimer le fichier. NB: le flux **n'est pas** fermé au préalable */ + function delete(): self { + if (file_exists($this->file)) unlink($this->file); + return $this; + } + + /** + * renommer le fichier. le flux est fermé d'abord + * + * @param int|null $defaultMode mode par défaut si le fichier destination + * n'existe pas. sinon, changer le mode du fichier temporaire à la valeur du + * fichier destination après renommage + * @param bool $setOwner si le propriétaire et/ou le groupe du fichier + * temporaire ne sont pas les mêmes que le fichier destination, tenter de + * changer le propriétaire et le groupe du fichier temporaire à la valeur + * du fichier destination après le renommage (nécessite les droits de root) + * @throws IOException + */ + function rename(string $dest, ?int $defaultMode=0644, bool $setOwner=true): void { + $this->close(); + $file = $this->file; + if (file_exists($dest)) { + $mode = fileperms($dest); + if ($setOwner) { + $tmpowner = fileowner($file); + $owner = fileowner($dest); + $tmpgroup = filegroup($file); + $group = filegroup($dest); + } else { + $owner = $group = null; + } + } else { + $mode = $defaultMode; + $owner = $group = null; + } + if (!rename($file, $dest)) { + throw new IOException("$file: unable to rename to $dest"); + } + $this->file = $dest; + if ($mode !== null) chmod($dest, $mode); + if ($owner !== null) { + if ($owner !== $tmpowner) chown($dest, $owner); + if ($group !== $tmpgroup) chgrp($dest, $group); + } + if ($mode !== null || $owner !== null) clearstatcache(true, $file); + } +} diff --git a/php/src_file/base/_File.php b/php/src_file/base/_File.php new file mode 100644 index 0000000..bb04797 --- /dev/null +++ b/php/src_file/base/_File.php @@ -0,0 +1,35 @@ +file, $this->mode)); + } + + /** @return resource */ + function getResource() { + if ($this->fd === null && $this->file !== null) $this->fd = $this->open(); + return parent::getResource(); + } + + /** streamer le contenu du fichier en sortie */ + function readfile(?string $contentType=null, ?string $charset=null, ?string $filename=null, string $disposition=null): bool { + if ($contentType !== null) http::content_type($contentType, $charset); + if ($filename !== null) http::download_as($filename, $disposition); + return readfile($this->file) !== false; + } +} diff --git a/php/src_file/csv/csv_flavours.php b/php/src_file/csv/csv_flavours.php new file mode 100644 index 0000000..334c0f6 --- /dev/null +++ b/php/src_file/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 cl::get(self::ENCODINGS, $flavour); + } +} diff --git a/php/src_os/EOFException.php b/php/src_os/EOFException.php new file mode 100644 index 0000000..c7af63d --- /dev/null +++ b/php/src_os/EOFException.php @@ -0,0 +1,14 @@ + $part) { + $key = self::_quote($key); + $val = self::_quote($part); + $parts[] = "[$key]=$val"; + } + } + return "(".implode(" ", $parts).")"; + } else { + return self::_quote(strval($value)); + } + } + + /** + * obtenir une commande shell à partir du tableau des arguments. + * à utiliser avec exec() + */ + static final function join(array $parts): string { + $count = count($parts); + for($i = 0; $i < $count; $i++) { + $parts[$i] = self::_quote(strval($parts[$i])); + } + return implode(" ", $parts); + } + + private static final function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void { + if ($redir !== null) { + switch ($redir) { + case "outonly": + case "noerr": + $redir = "2>/dev/null"; + break; + case "erronly": + case "noout": + $redir = "2>&1 >/dev/null"; + break; + case "both": + case "err2out": + $redir = "2>&1"; + break; + case "none": + case "null": + $redir = ">/dev/null 2>&1"; + break; + case "default": + $redir = null; + break; + } + } + if ($input !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= "<".escapeshellarg($input); + } + if ($output !== null) { + $redir = $redir !== null? "$redir ": ""; + $redir .= ">".escapeshellarg($output); + } + if ($redir !== null) $cmd .= " $redir"; + } + + /** + * Corriger la commande $cmd: + * - si c'est tableau, joindre les arguments avec {@link join()} + * - sinon, mettre les caractères en échappement avec {@link escapeshellarg()} + */ + static final function verifix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void { + if (is_array($cmd)) $cmd = self::join($cmd); + else $cmd = escapeshellcmd(strval($cmd)); + self::add_redir($cmd, $redir, $input, $output); + } + + /** + * Lancer la commande spécifiée avec passthru() et retourner le code de retour + * dans la variable $retcode. $cmd doit déjà être formaté comme il convient + * + * voici la différence entre system(), passthru() et exec() + * +----------------+-----------------+----------------+----------------+ + * | Command | Displays Output | Can Get Output | Gets Exit Code | + * +----------------+-----------------+----------------+----------------+ + * | passthru() | Yes (raw) | No | Yes | + * | system() | Yes (as text) | Last line only | Yes | + * | exec() | No | Yes (array) | Yes | + * +----------------+-----------------+----------------+----------------+ + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function _passthru(string $cmd, int &$retcode=null): bool { + passthru($cmd, $retcode); + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec system(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool { + $last_line = system($cmd, $retcode); + if ($last_line !== false) $output = $last_line; + return $retcode == 0; + } + + /** + * Comme {@link _passthru()} mais lancer la commande spécifiée avec exec(). + * cf la doc de {@link _passthru()} pour les autres détails + */ + static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool { + exec($cmd, $output, $retcode); + return $retcode == 0; + } + + /** + * Lancer la commande $cmd dans un processus fils via un shell et attendre la + * fin de son exécution. + * + * $cmd doit déjà être formaté comme il convient + */ + static final function _fork_exec(string $cmd, int &$retcode=null): bool { + $pid = pcntl_fork(); + if ($pid == -1) { + // parent, impossible de forker + throw new RuntimeException("unable to fork"); + } elseif ($pid) { + // parent, fork ok + pcntl_waitpid($pid, $status); + if (pcntl_wifexited($status)) { + $retcode = pcntl_wexitstatus($status); + } else { + $retcode = 127; + } + return $retcode == 0; + } + // child, fork ok + pcntl_exec("/bin/sh", ["-c", $cmd]); + return false; + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()} puis la lancer + * avec passthru() et retourner le code de retour dans la variable $retcode + * + * $redir spécifie le type de redirection demandée: + * - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé + * - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu + * - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu + * - "both" | "err2out": $output reçoit STDOUT et STDERR + * - "none" | "null": STDOUT et STDERR sont perdus + * - sinon c'est une redirection spécifique, et la valeur est rajoutée telle + * quelle à la ligne de commande + * + * @return bool true si la commande s'est lancée sans erreur, false sinon + */ + static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_passthru($cmd, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec system(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_system($cmd, $output, $retcode); + } + + /** + * Comme {@link passthru()} mais lancer la commande spécifiée avec exec(). + * Cf la doc de {@link passthru()} pour les autres détails + */ + static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_exec($cmd, $output, $retcode); + } + + /** + * Corriger la commande spécifiée avec {@link verifix_cmd()}, la préfixer de + * "exec" puis la lancer avec {@link _fork_exec()} + */ + static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool { + self::verifix_cmd($cmd, $redir); + return self::_fork_exec("exec $cmd", $retcode); + } + + ############################################################################# + + /** retourner le répertoire $HOME */ + static final function homedir(): string { + $homedir = getenv("HOME"); + if ($homedir === false) { + $homedir = posix_getpwuid(posix_getuid())["dir"]; + } + return path::abspath($homedir); + } + + /** s'assurer que le répertoire $dir existe */ + static final function mkdirp(string $dir): bool { + if (is_dir($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** créer le répertoire qui va contenir le fichier $file */ + static final function mkdirof(string $file): bool { + if (file_exists($file)) return true; + $dir = path::dirname($file); + if (file_exists($dir)) return true; + return mkdir($dir, 0777, true); + } + + /** + * créer un répertoire avec un nom unique. ce répertoire doit être supprimé + * manuellement quand il n'est plus utilisé. + * + * @return string le chemin du répertoire + * @throws IOException si une erreur se produit (impossible de créer un + * répertoire unique après 2560 essais) + */ + static final function mktempdir(?string $prefix=null, ?string $basedir=null): string { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + $max = 2560; + do { + $dir = "$basedir/$prefix".uniqid(); + $r = @mkdir($dir); + $max--; + } while ($r === false && $max > 0); + if ($r === false) { + throw IOException::last_error("$dir: unable to create directory"); + } + return $dir; + } + + /** + * Supprimer un répertoire créé avec mktempdir + * + * un minimum de vérification est effectué qu'il s'agit bien d'un répertoire + * généré par mktempdir + */ + static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + if ($prefix !== null) $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) { + self::exec(["rm", "-rf", $tmpdir]); + } else { + throw new IOException("$tmpdir: n'est pas un répertoire temporaire"); + } + } + + /** + * supprimer tous les répertoires temporaires qui ont été créés avec le + * suffixe spécifié dans le répertoire $basedir + */ + static final function cleantempdirs(string $prefix, ?string $basedir=null): void { + if ($basedir === null) $basedir = sys_get_temp_dir(); + $prefix .= "-"; + // 13 '?' parce que c'est la taille d'une chaine générée par uniqid() + $tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR); + if ($tmpdirs) { + self::exec(["rm", "-rf", ...$tmpdirs]); + } + } + + static final function is_diff_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return true; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode !== 0; + } + + static final function is_same_file(string $f, string $g): bool { + if (!is_file($f) || !is_file($g)) return false; + self::exec(array("diff", "-q", $f, $g), $output, $retcode); + return $retcode === 0; + } + + static final function is_diff_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return true; + return @readlink($f) !== @readlink($g); + } + + static final function is_same_link(string $f, string $g): bool { + if (!is_link($f) || !is_link($g)) return false; + return @readlink($f) === @readlink($g); + } +} diff --git a/php/src_output/TODO.md b/php/src_output/TODO.md index 87c6b0e..3a4e60a 100644 --- a/php/src_output/TODO.md +++ b/php/src_output/TODO.md @@ -4,5 +4,8 @@ rotation des logs * [ ] lors de la rotation, si l'ouverture du nouveau fichier échoue, continuer à écrire dans l'ancien fichier +* [ ] dans `StdMessenger::resetParams()`, `[output]` peut être une instance de + StdOutput pour mettre à jour $out ET $err, ou un tableau de deux éléments pour + mettre à jour séparément $out et $err -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src_output/log.php b/php/src_output/log.php index 0542587..b315f16 100644 --- a/php/src_output/log.php +++ b/php/src_output/log.php @@ -6,6 +6,9 @@ use nulib\output\std\ProxyMessenger; /** * Class log: inscrire un message dans les logs uniquement + * + * Cette classe (ou la classe parallèle {@link msg} DOIT être initialisée avant + * d'être utilisée */ class log extends _messenger { static function set_messenger(IMessenger $log=null) { diff --git a/php/src_output/msg.php b/php/src_output/msg.php index cf09a11..2846d38 100644 --- a/php/src_output/msg.php +++ b/php/src_output/msg.php @@ -6,6 +6,10 @@ use nulib\output\std\ProxyMessenger; /** * Class msg: inscrire un message dans les logs ET l'afficher sur la console + * + * Cette classe DOIT être initialisée avec {@link set_messenger()} ou + * {@link set_messenger_class()} avant d'être utilisée. Une fois initialisée, + * les classes {@link say} et {@link log} sont utilisables aussi */ class msg extends _messenger { static function set_messenger(IMessenger $say, ?IMessenger $log=null) { diff --git a/php/src_output/out.php b/php/src_output/out.php new file mode 100644 index 0000000..4966bb6 --- /dev/null +++ b/php/src_output/out.php @@ -0,0 +1,34 @@ +resetParams($params); + return self::$out; + } + + static function write(...$values): void { self::$out->write(...$values); } + static function print(...$values): void { self::$out->print(...$values); } + + static function iwrite(int $indentLevel, ...$values): void { self::$out->iwrite($indentLevel, ...$values); } + static function iprint(int $indentLevel, ...$values): void { self::$out->iprint($indentLevel, ...$values); } +} +out::reset(); diff --git a/php/src_output/say.php b/php/src_output/say.php index 9a3e847..fd0fe82 100644 --- a/php/src_output/say.php +++ b/php/src_output/say.php @@ -6,6 +6,9 @@ use nulib\output\std\ProxyMessenger; /** * Class say: afficher un message sur la console uniquement + * + * Cette classe (ou la classe parallèle {@link msg} DOIT être initialisée avant + * d'être utilisée */ class say extends _messenger { static function set_messenger(IMessenger $say) { diff --git a/php/src_output/std/StdMessenger.php b/php/src_output/std/StdMessenger.php index b7ba5ca..953dc0c 100644 --- a/php/src_output/std/StdMessenger.php +++ b/php/src_output/std/StdMessenger.php @@ -150,9 +150,14 @@ class StdMessenger implements _IMessenger { "color" => $color, "indent" => $indent, ]; - if ($output !== null) { + if ($this->out === $this->err) { $this->out->resetParams($params); } else { + # NB: si initialement [output] était null, et qu'on spécifie une valeur + # [output], alors les deux instances $out et $err sont mis à jour + # séparément avec la même valeur de output + # de plus, on ne peut plus revenir à la situation initiale avec une + # destination différente pour $out et $err $this->out->resetParams($params); $this->err->resetParams($params); } diff --git a/php/src_output/std/StdOutput.php b/php/src_output/std/StdOutput.php index a95468b..9cc90c3 100644 --- a/php/src_output/std/StdOutput.php +++ b/php/src_output/std/StdOutput.php @@ -3,8 +3,10 @@ namespace nulib\output\std; use Exception; use nulib\cl; -use nulib\output\IContent; -use nulib\output\IPrintable; +use nulib\os\file\Stream; +use nulib\php\content\content; +use nulib\php\content\IContent; +use nulib\php\content\IPrintable; /** * Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque @@ -86,6 +88,7 @@ class StdOutput { $indent = cl::get($params, "indent"); $flush = cl::get($params, "flush"); + if ($output instanceof Stream) $output = $output->getResource(); if ($output !== null) { if ($output === "php://stdout") { $outf = STDOUT; @@ -181,37 +184,12 @@ class StdOutput { return preg_replace('/\x1B\[.*?m/', "", $text); } - static function flatten($values, ?array &$dest=null): array { - if ($dest === null) $dest = []; - if ($values === null) return $dest; - if (is_string($values)) { - $dest[] = $values; - return $dest; - } elseif (!is_array($values)) { - if ($values instanceof IContent) { - $values = $values->getContent(); - } elseif ($values instanceof IPrintable) { - ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE); - $values->print(); - $dest[] = ob_get_clean(); - return $dest; - } elseif (!is_iterable($values)) { - $dest[] = strval($values); - return $dest; - } - } - foreach ($values as $value) { - self::flatten($value, $dest); - } - return $dest; - } - function getIndent(int $indentLevel): string { return str_repeat($this->indent, $indentLevel); } function getLines(bool $withNl, ...$values): array { - $values = self::flatten($values); + $values = content::flatten($values); if (!$values) return []; $text = implode("", $values); if ($text === "") return [""]; diff --git a/php/src_php/coll/AutoArray.php b/php/src_php/coll/AutoArray.php new file mode 100644 index 0000000..82d1904 --- /dev/null +++ b/php/src_php/coll/AutoArray.php @@ -0,0 +1,44 @@ +has($name)) return true; + $properties = self::_AUTO_PROPERTIES(); + if ($properties === null) return false; + return array_key_exists($name, $properties); + } + function __get($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) return $this->get($name); + $pkey = cl::get($properties, $name, $name); + return cl::pget($this->data, $pkey); + } + function __set($name, $value) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->set($name, $value); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pset($this->data, $pkey, $value); + } + } + function __unset($name) { + $properties = self::_AUTO_PROPERTIES(); + if ($this->has($name)) { + $this->del($name); + } else { + $pkey = cl::get($properties, $name, $name); + cl::pdel($this->data, $pkey); + } + } +} diff --git a/php/src_php/coll/BaseArray.php b/php/src_php/coll/BaseArray.php new file mode 100644 index 0000000..96f88fa --- /dev/null +++ b/php/src_php/coll/BaseArray.php @@ -0,0 +1,115 @@ +reset($data); + } + + /** @var array */ + protected $data; + + function __toString(): string { return var_export($this->data, true); } + #function __debugInfo() { return $this->data; } + function reset(?array &$data): void { $this->data =& $data; } + function &array(): ?array { return $this->data; } + function count(): int { return $this->data !== null? count($this->data): 0; } + function keys(): array { return $this->data !== null? array_keys($this->data): []; } + + ############################################################################# + # base + + function has($key): bool { + return $this->data !== null && array_key_exists($key, $this->data); + } + function &get($key, $default=null) { + if ($this->data !== null && array_key_exists($key, $this->data)) { + return $this->data[$key]; + } else return $default; + } + function set($key, $value): void { + if ($key === null) $this->data[] = $value; + else $this->data[$key] = $value; + } + function del($key): void { + unset($this->data[$key]); + } + + function offsetExists($offset): bool { return $this->has($offset); } + function &offsetGet($offset) { return $this->get($offset); } + function offsetSet($offset, $value) { $this->set($offset, $value); } + function offsetUnset($offset) { $this->del($offset); } + + function __isset($name) { return $this->has($name); } + function &__get($name) { return $this->get($name); } + function __set($name, $value) { $this->set($name, $value); } + function __unset($name) { $this->del($name); } + + ############################################################################# + # iterator + + /** @var bool */ + private $valid = false; + + function rewind() { + if ($this->data !== null) { + $first = reset($this->data); + $this->valid = $first !== false || key($this->data) !== null; + } else { + $this->valid = false; + } + } + function valid(): bool { return $this->valid; } + function key() { return key($this->data); } + function current() { return current($this->data); } + function next() { + $next = next($this->data); + $this->valid = $next !== false || key($this->data) !== null; + } + + ############################################################################# + # divers + + function phas($pkey): bool { return cl::phas($this->data, $pkey); } + function pget($pkey, $default=null): bool { return cl::pget($this->data, $pkey, $default); } + function pset($pkey, $value): void { cl::pset($this->data, $pkey, $value); } + function pdel($pkey): void { cl::pdel($this->data, $pkey); } + + function contains($value, bool $strict=false): bool { + if ($value === null || $this->data === null) return false; + return in_array($value, $this->data, $strict); + } + + function add($value, bool $unique=true, bool $strict=false): bool { + if ($unique && $this->contains($value, $strict)) return false; + $this->set(null, $value); + return true; + } + + function addAll(?array $values, bool $unique=true, bool $strict=false): void { + if ($values === null) return; + $index = 0; + foreach ($values as $key => $value) { + if ($key === $index) { + $this->add($value, $unique, $strict); + $index++; + } else { + $this->set($key, $value); + } + } + } + + function resetAll(?array $values): void { + $this->data = null; + $this->addAll($values); + } +} diff --git a/php/src_php/content/IContent.php b/php/src_php/content/IContent.php index 4d2de30..7fd4386 100644 --- a/php/src_php/content/IContent.php +++ b/php/src_php/content/IContent.php @@ -3,7 +3,7 @@ namespace nulib\php\content; /** * Interface IContent: un objet capable de produire du contenu à afficher. le - * contenu retourné doit être pris tel quel,sans plus d'analyse + * contenu retourné doit être pris tel quel, sans plus d'analyse */ interface IContent { /** retourner le contenu à afficher */ diff --git a/php/src_php/func.php b/php/src_php/func.php index 2747273..b100956 100644 --- a/php/src_php/func.php +++ b/php/src_php/func.php @@ -4,7 +4,7 @@ namespace nulib\php; use Closure; use nulib\cl; use nulib\ValueException; -use nulib\ref\sys\ref_func; +use nulib\ref\php\ref_func; use nulib\schema\Schema; use ReflectionClass; use ReflectionFunction; @@ -300,12 +300,16 @@ class func { return true; } + /** @var Schema */ + private static $call_all_params_schema; + /** * retourner la liste des méthodes de $class_or_object qui correspondent au * filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA} */ static function get_all($class_or_object, $params=null): array { - Schema::nv($paramsv, $params, null, $schema, ref_func::CALL_ALL_PARAMS_SCHEMA); + Schema::nv($paramsv, $params, null + , self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA); if (is_callable($class_or_object, true) && is_array($class_or_object)) { # callable sous forme de tableau $class_or_object = $class_or_object[0]; diff --git a/php/src_php/iter/AbstractIterator.php b/php/src_php/iter/AbstractIterator.php index 9da33e5..3e882ef 100644 --- a/php/src_php/iter/AbstractIterator.php +++ b/php/src_php/iter/AbstractIterator.php @@ -3,7 +3,7 @@ namespace nulib\php\iter; use Exception; use Iterator; -use nulib\DataException; +use nulib\NoMoreDataException; use nulib\php\ICloseable; /** @@ -29,12 +29,13 @@ abstract class AbstractIterator implements Iterator, ICloseable { protected function beforeIter() {} /** - * retourner le prochain élément. lancer l'exception {@link DataException} - * pour indiquer que plus aucun élément n'est disponible + * retourner le prochain élément. + * lancer l'exception {@link NoMoreDataException} pour indiquer que plus aucun + * élément n'est disponible * * le cas échéant, initialiser $key * - * @throws DataException + * @throws NoMoreDataException */ abstract protected function _next(&$key); @@ -94,7 +95,7 @@ abstract class AbstractIterator implements Iterator, ICloseable { $this->valid = false; try { $item = $this->_next($key); - } catch (DataException $e) { + } catch (NoMoreDataException $e) { $this->beforeClose(); try { $this->_teardown(); diff --git a/php/src_php/time/Date.php b/php/src_php/time/Date.php new file mode 100644 index 0000000..ac00e00 --- /dev/null +++ b/php/src_php/time/Date.php @@ -0,0 +1,20 @@ +setTime(0, 0, 0, 0); + } + + function format($format=self::DEFAULT_FORMAT): string { + return \DateTime::format($format); + } +} diff --git a/php/src_php/time/DateInterval.php b/php/src_php/time/DateInterval.php new file mode 100644 index 0000000..d593d58 --- /dev/null +++ b/php/src_php/time/DateInterval.php @@ -0,0 +1,53 @@ +y; + $m = $interval->m; + $d = $interval->d; + if ($y > 0) $string .= "${y}Y"; + if ($m > 0) $string .= "${m}M"; + if ($d > 0) $string .= "${d}D"; + $string .= "T"; + $h = $interval->h; + $i = $interval->i; + $s = $interval->s; + if ($h > 0) $string .= "${h}H"; + if ($i > 0) $string .= "${i}M"; + if ($s > 0 || $string == "PT") $string .= "${s}S"; + if ($interval->invert == 1) $string = "-$string"; + return $string; + } + + function __construct($duration) { + if ($duration instanceof \DateInterval) { + $this->y = $duration->y; + $this->m = $duration->m; + $this->d = $duration->d; + $this->h = $duration->h; + $this->i = $duration->i; + $this->s = $duration->s; + $this->invert = $duration->invert; + $this->days = $duration->days; + } elseif (!is_string($duration)) { + throw new InvalidArgumentException("duration must be a string"); + } else { + if (substr($duration, 0, 1) == "-") { + $duration = substr($duration, 1); + $invert = true; + } else { + $invert = false; + } + parent::__construct($duration); + if ($invert) $this->invert = 1; + } + } + + function __toString(): string { + return self::to_string($this); + } +} diff --git a/php/src_php/time/DateTime.php b/php/src_php/time/DateTime.php new file mode 100644 index 0000000..016287c --- /dev/null +++ b/php/src_php/time/DateTime.php @@ -0,0 +1,188 @@ +format("Ymd\\THis"); + $Z = $datetime->format("P"); + if ($Z === "+00:00") $Z = "Z"; + return "$YmdHMS$Z"; + } + + const DEFAULT_FORMAT = "d/m/Y H:i:s"; + const INT_FORMATS = [ + "year" => "Y", + "month" => "m", + "day" => "d", + "hour" => "H", + "minute" => "i", + "second" => "s", + "wday" => "N", + "wnum" => "W", + ]; + const STRING_FORMATS = [ + "timezone" => "P", + "datetime" => "d/m/Y H:i:s", + "date" => "d/m/Y", + "Ymd" => "Ymd", + "YmdHMS" => "Ymd\\THis", + "YmdHMSZ" => [self::class, "_YmdHMSZ_format"], + ]; + + static function clone(DateTimeInterface $dateTime): self { + if ($dateTime instanceof static) return clone $dateTime; + $clone = new static(); + $clone->setTimestamp($dateTime->getTimestamp()); + $clone->setTimezone($dateTime->getTimezone()); + return $clone; + } + + /** + * corriger une année à deux chiffres qui est située dans le passé et + * retourner l'année à 4 chiffres. + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '1919' + * - fix_past_year('20') === '1920' + */ + static function fix_past_year(int $year): int { + if ($year < 100) { + $y = getdate(); $y = $y["year"]; + $r = $y % 100; + $c = $y - $r; + if ($year >= $r) $year += $c - 100; + else $year += $c; + } + return $year; + } + + /** + * corriger une année à deux chiffres et retourner l'année à 4 chiffres. + * l'année charnière entre année passée et année future est 70 + * + * par exemple, si l'année courante est 2019, alors: + * - fix_past_year('18') === '2018' + * - fix_past_year('19') === '2019' + * - fix_past_year('20') === '2020' + * - fix_past_year('69') === '2069' + * - fix_past_year('70') === '1970' + * - fix_past_year('71') === '1971' + */ + static function fix_any_year(int $year): int { + if ($year < 100) { + $y = intval(date("Y")); + $r = $y % 100; + $c = $y - $r; + if ($year >= 70) $year += $c - 100; + else $year += $c; + } + return $year; + } + + function __construct($datetime="now", DateTimeZone $timezone=null) { + if ($datetime instanceof \DateTimeInterface) { + if ($timezone === null) $timezone = $datetime->getTimezone(); + parent::__construct(); + $this->setTimestamp($datetime->getTimestamp()); + $this->setTimezone($timezone); + } elseif (is_int($datetime)) { + parent::__construct("now", $timezone); + $this->setTimestamp($datetime); + } elseif (!is_string($datetime)) { + throw new InvalidArgumentException("datetime must be a string or an instance of DateTimeInterface"); + } else { + $Y = $H = $Z = null; + if (preg_match(self::DMY_PATTERN, $datetime, $ms)) { + $Y = $ms[3] ?? null; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + } elseif (preg_match(self::YMD_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + } elseif (preg_match(self::DMYHIS_PATTERN, $datetime, $ms)) { + $Y = $ms[3]; + if ($Y !== null) $Y = self::fix_any_year(intval($Y)); + else $Y = intval(date("Y")); + $m = intval($ms[2]); + $d = intval($ms[1]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + } elseif (preg_match(self::YMDHISZ_PATTERN, $datetime, $ms)) { + $Y = $ms[1]; + if (strlen($Y) == 2) $Y = self::fix_any_year(intval($ms[1])); + else $Y = intval($Y); + $m = intval($ms[2]); + $d = intval($ms[3]); + $H = intval($ms[4]); + $M = intval($ms[5]); + $S = intval($ms[6] ?? 0); + $Z = $ms[7] ?? null; + } + if ($Y !== null) { + if ($H === null) $datetime = sprintf("%04d-%02d-%02d", $Y, $m, $d); + else $datetime = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y, $m, $d, $H, $M, $S); + if ($Z !== null) $timezone = new DateTimeZone("UTC"); + } + parent::__construct($datetime, $timezone); + } + } + + function diff($target, $absolute=false): DateInterval { + return new DateInterval(parent::diff($target, $absolute)); + } + + function format($format=self::DEFAULT_FORMAT): string { + return \DateTime::format($format); + } + + function __toString(): string { + return $this->format(); + } + + function __get($name) { + if (array_key_exists($name, self::INT_FORMATS)) { + $format = self::INT_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return intval($this->format($format)); + } elseif (array_key_exists($name, self::STRING_FORMATS)) { + $format = self::STRING_FORMATS[$name]; + if (is_callable($format)) return $format($this); + else return $this->format($format); + } + throw new InvalidArgumentException("Unknown property $name"); + } +} diff --git a/php/src_php/time/Delay.php b/php/src_php/time/Delay.php new file mode 100644 index 0000000..8161351 --- /dev/null +++ b/php/src_php/time/Delay.php @@ -0,0 +1,139 @@ + [0, 5], + "d" => [1, 5], + "h" => [1, 0], + "m" => [1, 0], + "s" => [1, 0], + ]; + + static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array { + $dest = DateTime::clone($from); + switch ($u) { + case "w": + if ($x > 0) { + $x *= 7; + $dest->add(new \DateInterval("P${x}D")); + } + $w = 7 - intval($dest->format("w")); + $dest->add(new \DateInterval("P${w}D")); + $u = "h"; + break; + case "d": + $dest->add(new \DateInterval("P${x}D")); + $u = "h"; + break; + case "h": + $dest->add(new \DateInterval("PT${x}H")); + $u = "m"; + break; + case "m": + $dest->add(new \DateInterval("PT${x}M")); + $u = "s"; + break; + case "s": + $dest->add(new \DateInterval("PT${x}S")); + $u = null; + break; + } + if ($y !== null && $u !== null) { + $h = intval($dest->format("H")); + $m = intval($dest->format("i")); + switch ($u) { + case "h": + $dest->setTime($y, 0, 0, 0); + break; + case "m": + $dest->setTime($h, $y, 0, 0); + break; + case "s": + $dest->setTime($h, $m, $y, 0); + break; + } + } + $repr = $y !== null? "$x$y$y": "$x"; + return [$dest, $repr]; + } + + function __construct($delay, ?DateTimeInterface $from=null) { + if ($from === null) $from = new DateTime(); + if (is_int($delay)) { + [$dest, $repr] = self::compute_dest($delay, "s", null, $from); + } elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) { + $x = intval($delay); + [$dest, $repr] = self::compute_dest($x, "s", null, $from); + } elseif (is_string($delay) && preg_match('/^(\d*)([wdhms])(\d*)$/i', $delay, $ms)) { + [$x, $u, $y] = [$ms[1], $ms[2], $ms[3]]; + $u = strtolower($u); + $default = self::DEFAULTS[$u]; + if ($x === "") $x = $default[0]; + else $x = intval($x); + if ($y === "") $y = $default[1]; + else $y = intval($y); + [$dest, $repr] = self::compute_dest($x, $u, $y, $from); + } else { + throw new InvalidArgumentException("invalid delay"); + } + $this->dest = $dest; + $this->repr = $repr; + } + + /** @var DateTime */ + protected $dest; + + function getDest(): DateTime { + return $this->dest; + } + + /** @var string */ + protected $repr; + + function __toString(): string { + return $this->repr; + } + + protected function _getDiff(?DateTimeInterface $now=null): \DateInterval { + if ($now === null) $now = new DateTime(); + return $this->dest->diff($now); + } + + /** retourner true si le délai imparti est écoulé */ + function isElapsed(?DateTimeInterface $now=null): bool { + return $this->_getDiff($now)->invert == 0; + } + + /** + * retourner l'intervalle entre le moment courant et la destination. + * + * l'intervalle est négatif si le délai n'est pas écoulé, positif sinon + */ + function getDiff(?DateTimeInterface $now=null): DateInterval { + return new DateInterval($this->_getDiff($now)); + } +} diff --git a/php/src_ref/cli/ref_args.php b/php/src_ref/cli/ref_args.php new file mode 100644 index 0000000..98f5515 --- /dev/null +++ b/php/src_ref/cli/ref_args.php @@ -0,0 +1,85 @@ + [null, null, "tableau contenant des paramètres et des options par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci avant de calculer la liste effective des options"], + "merge" => [null, null, "tableau à merger à celui-ci avant de calculer la liste effective des options", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "name" => [null, null, "nom du programme, utilisé pour l'affichage de l'aide"], + "purpose" => [null, null, "courte description de l'objet de ce programme"], + "usage" => [null, null, "exposé textuel des arguments valides du programme", + # ce peut être une chaine e.g '[options] SRC DESC' + # ou un tableau auquel cas autant de lignes que nécessaire sont affichées + ], + "description" => [null, null, "description longue de l'objet du programme, affiché après usage"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + "dynamic_command" => [null, null, "fonction indiquant si une commande est valide", + # la signature de la fonction est function(string $command):?array + # elle doit retourner un tableau au format DEFS_SCHEMA qui définit la + # commande spécifiée, ou null si ce n'est pas une commande valide + ], + "sections" => [null, null, "liste de sections permettant de grouper les arguments"], + "commandname" => [null, null, "propriété ou clé qui obtient la commande courante", + # la valeur par défaut est "command" si ni commandproperty ni commandkey ne sont définis + ], + "commandproperty" => [null, null, "comme commandname mais force l'utilisation d'une propriété"], + "commandkey" => [null, null, "comme commandname mais force l'utilisation d'une clé"], + "argsname" => [null, null, "propriété ou clé qui obtient les arguments restants", + # la valeur par défaut est "args" si ni argsproperty ni argskey ne sont définis + ], + "argsproperty" => [null, null, "comme argsname mais force l'utilisation d'une propriété"], + "argskey" => [null, null, "comme argsname mais force l'utilisation d'une clé"], + "autohelp" => ["?bool", null, "faut-il ajouter automatiquement le support de l'option --help"], + "autoremains" => ["?bool", null, "faut-il ajouter automatiquement la prise en compte des arguments restants"], + ]; + + const SECTION_SCHEMA = [ + "show" => ["bool", true, "faut-il afficher cette section?"], + "title" => [null, null, "titre de la section"], + "prefix" => [null, null, "texte à afficher avant l'aide générée automatiquement"], + "suffix" => [null, null, "texte à afficher après l'aide générée automatiquement"], + + # ces valeurs sont calculées + "defs" => [null, null, "(interne) liste des définitions de cette section"], + ]; + + const DEF_SCHEMA = [ + "set_defaults" => [null, null, "tableau contenant des paramètres par défaut"], + "merge_arrays" => [null, null, "liste de tableaux à merger à celui-ci"], + "merge" => [null, null, "tableau à merger à celui-ci", + # si merge_arrays et merge sont spécifiés tous les deux, "merge" est mergé après "merge_arrays" + ], + "kind" => [null, null, "type de définition: 'option' ou 'command'"], + "arg" => [null, null, "type de l'argument attendu par l'option"], + "args" => [null, null, "type des arguments attendus par l'option", + # si args est spécifié, arg est ignoré + ], + "argsdesc" => [null, null, "description textuelle des arguments, utilisé pour l'affichage de l'aide"], + "type" => [null, null, "types dans lesquels convertir les arguments avant de les fournir à l'utilisateur"], + "action" => [null, null, "fonction à appeler quand cette option est utilisée", + # la signature de la fonction est ($value, $name, $arg, $dest, $def) + ], + "name" => [null, null, "propriété ou clé à initialiser en réponse à l'utilisation de cette option", + # le nom à spécifier est au format under_score, qui est transformée en camelCase si la destination est un objet + ], + "property" => [null, null, "comme name mais force l'utilisation d'une propriété"], + "key" => [null, null, "comme name mais force l'utilisation d'une clé"], + "inverse" => ["bool", false, "décrémenter la destination au lieu de l'incrémenter pour une option sans argument"], + "value" => ["mixed", null, "valeur à forcer au lieu d'incrémenter la destination"], + "ensure_array" => [null, null, "forcer la destination à être un tableau"], + "help" => [null, null, "description de cette option, utilisé pour l'affichage de l'aide"], + "cmd_args" => [null, null, "définition des sous-options pour une commande"], + + # ces valeurs sont calculées + "cmd_defs" => [null, null, "(interne) liste des définitions correspondant au paramètre options"], + ]; + + const ARGS_ALLOWED_VALUES = ["value", "path", "dir", "file", "host"]; +} diff --git a/php/src_ref/os/csv/ref_csv.php b/php/src_ref/os/csv/ref_csv.php new file mode 100644 index 0000000..591c354 --- /dev/null +++ b/php/src_ref/os/csv/ref_csv.php @@ -0,0 +1,20 @@ + ["string", null, "nature du schéma", + "pkey" => 0, + "allowed_values" => ["assoc", "list", "scalar"], + ], + "title" => ["?string", null, "libellé de la valeur"], + "required" => ["bool", false, "la valeur est-elle requise?"], + "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"], + "desc" => ["?content", null, "description de la valeur"], + "name" => ["?key", null, "identifiant de la valeur"], + "schema" => ["?array", null, "définition du schéma"], + ]; + + /** @var array meta-schema d'un schéma de nature scalaire */ + const SCALAR_METASCHEMA = [ + "type" => ["array", null, "types possibles de la valeur", "required" => true], + "default" => [null, null, "valeur par défaut si la valeur n'existe pas"], + "title" => ["?string", null, "libellé de la valeur"], + "required" => ["bool", false, "la valeur est-elle requise?"], + "nullable" => ["?bool", null, "la valeur peut-elle être nulle?"], + "desc" => ["?content", null, "description de la valeur"], + "analyzer_func" => ["?callable", null, "fonction qui analyse une valeur entrante et indique comment la traiter"], + "extractor_func" => ["?callable", null, "fonction qui extrait la valeur à analyser dans une chaine de caractère"], + "parser_func" => ["?callable", null, "fonction qui analyse une chaine de caractères pour produire la valeur"], + "normalizer_func" => ["?callable", null, "fonction qui normalise la valeur"], + "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"], + "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"], + "format" => [null, null, "format à utiliser pour l'affichage"], + "" => ["array", "scalar", "nature du schéma", + "" => ["assoc", "schema" => self::NATURE_METASCHEMA], + ], + "name" => ["?string", null, "identifiant de la valeur"], + "pkey" => ["?pkey", null, "chemin de clé de la valeur dans un tableau associatif"], + "header" => ["?string", null, "nom de l'en-tête s'il faut présenter cette donnée dans un tableau"], + "composite" => ["?bool", null, "ce champ fait-il partie d'une valeur composite?"], + ]; + + const MESSAGES = [ + "missing" => "{key}: Vous devez spécifier cette valeur", + "unavailable" => "{key}: Vous devez spécifier cette valeur", + "null" => "{key}: cette valeur ne doit pas être nulle", + "empty" => "{key}: cette valeur ne doit pas être vide", + "invalid" => "{key}: {orig}: cette valeur est invalide", + ]; + + /** @var array meta-schema d'un schéma de nature associative */ + const ASSOC_METASCHEMA = [ + ]; + + /** @var array meta-schema d'un schéma de nature liste */ + const LIST_METASCHEMA = [ + ]; +} diff --git a/php/src_ref/schema/ref_types.php b/php/src_ref/schema/ref_types.php new file mode 100644 index 0000000..24973d5 --- /dev/null +++ b/php/src_ref/schema/ref_types.php @@ -0,0 +1,10 @@ + "bool", + "integer" => "int", + "flt" => "float", "double" => "float", "dbl" => "float", + ]; +} diff --git a/php/src_values/akey.php b/php/src_values/akey.php new file mode 100644 index 0000000..403bf1d --- /dev/null +++ b/php/src_values/akey.php @@ -0,0 +1,98 @@ +offsetExists($key)) return $array->offsetGet($key); + else return $default; + } else { + if (!is_array($array)) $array = cl::with($array); + return cl::get($array, $key, $default); + } + } + + /** spécifier la valeur d'une clé */ + static final function set(&$array, $key, $value) { + if ($array instanceof ArrayAccess) { + $array->offsetSet($key, $value); + } else { + cl::set($array, $key, $value); + } + return $value; + } + + /** initialiser $dest avec les valeurs de $values */ + static final function set_values(&$array, ?array $values): void { + if ($values === null) return; + foreach ($values as $key => $value) { + self::set($array, $key, $value); + } + } + + /** incrémenter la valeur de la clé */ + static final function inc(&$array, $key): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + $array->offsetSet($key, ++$value); + return $value; + } else { + cl::ensure_array($array); + $value = (int)cl::get($array, $key); + return $array[$key] = ++$value; + } + } + + /** décrémenter la valeur de la clé */ + static final function dec(&$array, $key, bool $allow_negative=false): int { + if ($array instanceof ArrayAccess) { + $value = (int)$array->offsetGet($key); + if ($allow_negative || $value > 0) $array->offsetSet($key, --$value); + return $value; + } else { + cl::ensure_array($array); + $value = (int)cl::get($array, $key); + if ($allow_negative || $value > 0) $array[$key] = --$value; + return $value; + } + } + + /** + * fusionner $merge dans la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function merge(&$array, $key, $merge): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + $value = cl::merge($value, $merge); + $array->offsetSet($key, $value); + } else { + cl::ensure_array($array); + $array[$key] = cl::merge($array[$key], $merge); + } + } + + /** + * ajouter $value à la valeur de la clé, qui est d'abord transformé en + * tableau si nécessaire + */ + static final function append(&$array, $key, $value): void { + if ($array instanceof ArrayAccess) { + $value = $array->offsetGet($key); + cl::set($value, null, $value); + $array->offsetSet($key, $value); + } else { + cl::ensure_array($array); + cl::set($array[$key], null, $value); + } + } +} diff --git a/php/src_values/mprop.php b/php/src_values/mprop.php new file mode 100644 index 0000000..5236582 --- /dev/null +++ b/php/src_values/mprop.php @@ -0,0 +1,123 @@ +getMethod($method); + } catch (ReflectionException $e) { + return oprop::get($object, $property, $default); + } + return func::call([$object, $m], $default); + } + + /** spécifier la valeur d'une propriété */ + static final function set(object $object, string $property, $value, ?string $method=null) { + $c = new ReflectionClass($object); + return self::_set($c, $object, $property, $value, $method); + } + + private static final function _set(ReflectionClass $c, object $object, string $property, $value, ?string $method) { + if ($method === null) $method = self::get_setter_name($property); + try { + $m = $c->getMethod($method); + } catch (ReflectionException $e) { + return oprop::_set($c, $object, $property, $value); + } + func::call([$object, $m], $value); + return $value; + } + + /** + * initialiser $dest avec les valeurs de $values + * + * les noms des clés de $values sont transformées en camelCase pour avoir les + * noms des propriétés correspondantes + */ + static final function set_values(object $object, ?array $values, ?array $keys=null): void { + if ($values === null) return; + if ($keys === null) $keys = array_keys($values); + $c = new ReflectionClass($object); + foreach ($keys as $key) { + if (array_key_exists($key, $values)) { + $property = str::us2camel($key); + self::_set($c, $object, $property, $values[$key], null); + } + } + } + + /** incrémenter la valeur d'une propriété */ + static final function inc(object $object, string $property): int { + $value = intval(self::get($object, $property, 0)); + $value++; + self::set($object, $property, $value); + return $value; + } + + /** décrémenter la valeur d'une propriété */ + static final function dec(object $object, string $property, bool $allow_negative=false): int { + $value = intval(self::get($object, $property, 0)); + if ($allow_negative || $value > 0) { + $value--; + self::set($object, $property, $value); + } + return $value; + } + + /** + * Fusionner la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function merge(object $object, string $property, $array): void { + $values = cl::with(self::get($object, $property)); + $values = cl::merge($values, cl::with($array)); + self::set($object, $property, $values); + } + + /** + * Ajouter la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function append(object $object, string $property, $value): void { + $values = cl::with(self::get($object, $property)); + $values[] = $value; + self::set($object, $property, $values); + } +} diff --git a/php/src_values/oprop.php b/php/src_values/oprop.php new file mode 100644 index 0000000..f496a7d --- /dev/null +++ b/php/src_values/oprop.php @@ -0,0 +1,151 @@ +getProperty($property); + $p->setAccessible(true); + return $p->getValue($object); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) return $object->$property; + else return $default; + } + } + + static final function _set(ReflectionClass $c, object $object, string $property, $value) { + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $p->setValue($object, $value); + } catch (ReflectionException $e) { + $object->$property = $value; + } + return $value; + } + + /** spécifier la valeur d'une propriété */ + static final function set(object $object, string $property, $value) { + $c = new ReflectionClass($object); + return self::_set($c, $object, $property, $value); + } + + /** + * initialiser $dest avec les valeurs de $values + * + * les noms des clés de $values sont transformées en camelCase pour avoir les + * noms des propriétés correspondantes + */ + static final function set_values(object $object, ?array $values, ?array $keys=null): void { + if ($values === null) return; + if ($keys === null) $keys = array_keys($values); + $c = new ReflectionClass($object); + foreach ($keys as $key) { + if (array_key_exists($key, $values)) { + $property = str::us2camel($key); + self::_set($c, $object, $property, $values[$key]); + } + } + } + + /** incrémenter la valeur d'une propriété */ + static final function inc(object $object, string $property): int { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $value = (int)$p->getValue($object); + $value++; + $p->setValue($object, $value); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $value = (int)$object->$property; + $value++; + } else { + $value = 1; + } + $object->$property = $value; + } + return $value; + } + + /** décrémenter la valeur d'une propriété */ + static final function dec(object $object, string $property, bool $allow_negative=false): int { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $value = (int)$p->getValue($object); + if ($allow_negative || $value > 0) { + $value --; + $p->setValue($object, $value); + } + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $value = (int)$object->$property; + } else { + $value = 0; + } + if ($allow_negative || $value > 0) $value--; + $object->$property = $value; + } + return $value; + } + + /** + * Fusionner la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function merge(object $object, string $property, $array): void { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $values = cl::with($p->getValue($object)); + $values = cl::merge($values, cl::with($array)); + $p->setValue($object, $values); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $values = cl::with($object->$property); + } else { + $values = []; + } + $values = cl::merge($values, cl::with($array)); + $object->$property = $values; + } + } + + /** + * Ajouter la valeur à la propriété qui est transformée en tableau si + * nécessaire + */ + static final function append(object $object, string $property, $value): void { + $c = new ReflectionClass($object); + try { + $p = $c->getProperty($property); + $p->setAccessible(true); + $values = cl::with($p->getValue($object)); + $values[] = $value; + $p->setValue($object, $values); + } catch (ReflectionException $e) { + if (property_exists($object, $property)) { + $values = cl::with($object->$property); + } else { + $values = []; + } + $values[] = $value; + $object->$property = $values; + } + } +} diff --git a/php/src_values/valm.php b/php/src_values/valm.php new file mode 100644 index 0000000..dc4c100 --- /dev/null +++ b/php/src_values/valm.php @@ -0,0 +1,84 @@ +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/php/tests/file/base/impl/avec_bom.csv b/php/tests/file/base/impl/avec_bom.csv new file mode 100644 index 0000000..d1512a2 --- /dev/null +++ b/php/tests/file/base/impl/avec_bom.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/php/tests/file/base/impl/avec_bom.txt b/php/tests/file/base/impl/avec_bom.txt new file mode 100644 index 0000000..9e55899 --- /dev/null +++ b/php/tests/file/base/impl/avec_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/php/tests/file/base/impl/msexcel.csv b/php/tests/file/base/impl/msexcel.csv new file mode 100644 index 0000000..b2d95c4 --- /dev/null +++ b/php/tests/file/base/impl/msexcel.csv @@ -0,0 +1,2 @@ +nom;prenom;age +clain;jephte;50 diff --git a/php/tests/file/base/impl/ooffice.csv b/php/tests/file/base/impl/ooffice.csv new file mode 100644 index 0000000..f00d4ff --- /dev/null +++ b/php/tests/file/base/impl/ooffice.csv @@ -0,0 +1,2 @@ +nom,prenom,age +clain,jephte,50 diff --git a/php/tests/file/base/impl/sans_bom.txt b/php/tests/file/base/impl/sans_bom.txt new file mode 100644 index 0000000..f16e49f --- /dev/null +++ b/php/tests/file/base/impl/sans_bom.txt @@ -0,0 +1 @@ +0123456789ABCDEFGHIJ0123456789abcdefghij0123456789 diff --git a/php/tests/file/base/impl/weird.tsv b/php/tests/file/base/impl/weird.tsv new file mode 100644 index 0000000..cd8bf3a --- /dev/null +++ b/php/tests/file/base/impl/weird.tsv @@ -0,0 +1,2 @@ +nom prenom age +clain jephte 50 diff --git a/php/tests/php/time/DateTest.php b/php/tests/php/time/DateTest.php new file mode 100644 index 0000000..a045154 --- /dev/null +++ b/php/tests/php/time/DateTest.php @@ -0,0 +1,55 @@ +format()); + self::assertSame("05/04/2024", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(0, $date->hour); + self::assertSame(0, $date->minute); + self::assertSame(0, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertSame("+04:00", $date->timezone); + self::assertSame("05/04/2024 00:00:00", $date->datetime); + self::assertSame("05/04/2024", $date->date); + } + + function testClone() { + $date = self::dt("now"); + $clone = Date::clone($date); + self::assertInstanceOf(DateTime::class, $clone); + } + + function testConstruct() { + $y = date("Y"); + self::assertSame("05/04/$y", strval(new Date("5/4"))); + self::assertSame("05/04/2024", strval(new Date("5/4/24"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024"))); + self::assertSame("05/04/2024", strval(new Date("05/04/2024"))); + self::assertSame("05/04/2024", strval(new Date("20240405"))); + self::assertSame("05/04/2024", strval(new Date("240405"))); + self::assertSame("05/04/2024", strval(new Date("20240405T091523"))); + self::assertSame("05/04/2024", strval(new Date("20240405T091523Z"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9:15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9.15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 9h15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09:15"))); + self::assertSame("05/04/2024", strval(new Date("5/4/2024 09h15"))); + } +} diff --git a/php/tests/php/time/DateTimeTest.php b/php/tests/php/time/DateTimeTest.php new file mode 100644 index 0000000..83d111b --- /dev/null +++ b/php/tests/php/time/DateTimeTest.php @@ -0,0 +1,64 @@ +format()); + self::assertEquals("05/04/2024 09:15:23", strval($date)); + self::assertSame(2024, $date->year); + self::assertSame(4, $date->month); + self::assertSame(5, $date->day); + self::assertSame(9, $date->hour); + self::assertSame(15, $date->minute); + self::assertSame(23, $date->second); + self::assertSame(5, $date->wday); + self::assertSame(14, $date->wnum); + self::assertEquals("+04:00", $date->timezone); + self::assertSame("05/04/2024 09:15:23", $date->datetime); + self::assertSame("05/04/2024", $date->date); + self::assertSame("20240405", $date->Ymd); + self::assertSame("20240405T091523", $date->YmdHMS); + self::assertSame("20240405T091523+04:00", $date->YmdHMSZ); + } + + function testDateTimeZ() { + $date = new DateTime("20240405T091523Z"); + self::assertSame("20240405T091523", $date->YmdHMS); + self::assertSame("20240405T091523Z", $date->YmdHMSZ); + } + + function testClone() { + $date = self::dt("now"); + $clone = DateTime::clone($date); + self::assertInstanceOf(DateTime::class, $clone); + } + + function testConstruct() { + $y = date("Y"); + self::assertSame("05/04/$y 00:00:00", strval(new DateTime("5/4"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/24"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("5/4/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("05/04/2024"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("20240405"))); + self::assertSame("05/04/2024 00:00:00", strval(new DateTime("240405"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("20240405T091523Z"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9:15:23"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 9.15.23"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9.15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 9h15"))); + self::assertSame("05/04/2024 09:15:23", strval(new DateTime("5/4/2024 09:15:23"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09:15"))); + self::assertSame("05/04/2024 09:15:00", strval(new DateTime("5/4/2024 09h15"))); + } +} diff --git a/php/tests/php/time/DelayTest.php b/php/tests/php/time/DelayTest.php new file mode 100644 index 0000000..132bc4d --- /dev/null +++ b/php/tests/php/time/DelayTest.php @@ -0,0 +1,83 @@ +getDest()); + + $delay = new Delay("10", $from); + self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + + $delay = new Delay("10s", $from); + self::assertEquals(self::dt("2024-04-05 09:15:33"), $delay->getDest()); + + $delay = new Delay("s", $from); + self::assertEquals(self::dt("2024-04-05 09:15:24"), $delay->getDest()); + + $delay = new Delay("5m", $from); + self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + + $delay = new Delay("5m0", $from); + self::assertEquals(self::dt("2024-04-05 09:20:00"), $delay->getDest()); + + $delay = new Delay("5m2", $from); + self::assertEquals(self::dt("2024-04-05 09:20:02"), $delay->getDest()); + + $delay = new Delay("m", $from); + self::assertEquals(self::dt("2024-04-05 09:16:00"), $delay->getDest()); + + $delay = new Delay("5h", $from); + self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + + $delay = new Delay("5h0", $from); + self::assertEquals(self::dt("2024-04-05 14:00:00"), $delay->getDest()); + + $delay = new Delay("5h2", $from); + self::assertEquals(self::dt("2024-04-05 14:02:00"), $delay->getDest()); + + $delay = new Delay("h", $from); + self::assertEquals(self::dt("2024-04-05 10:00:00"), $delay->getDest()); + + $delay = new Delay("5d", $from); + self::assertEquals(self::dt("2024-04-10 05:00:00"), $delay->getDest()); + + $delay = new Delay("5d2", $from); + self::assertEquals(self::dt("2024-04-10 02:00:00"), $delay->getDest()); + + $delay = new Delay("5d0", $from); + self::assertEquals(self::dt("2024-04-10 00:00:00"), $delay->getDest()); + + $delay = new Delay("d", $from); + self::assertEquals(self::dt("2024-04-06 05:00:00"), $delay->getDest()); + + $delay = new Delay("2w", $from); + self::assertEquals(self::dt("2024-04-21 05:00:00"), $delay->getDest()); + + $delay = new Delay("2w2", $from); + self::assertEquals(self::dt("2024-04-21 02:00:00"), $delay->getDest()); + + $delay = new Delay("2w0", $from); + self::assertEquals(self::dt("2024-04-21 00:00:00"), $delay->getDest()); + + $delay = new Delay("w", $from); + self::assertEquals(self::dt("2024-04-07 05:00:00"), $delay->getDest()); + } + + function testElapsed() { + $delay = new Delay(5); + sleep(2); + self::assertFalse($delay->isElapsed()); + sleep(5); + self::assertTrue($delay->isElapsed()); + } +}