diff --git a/bbnurse b/bbnurse index 303cd1b..3da8cff 100755 --- a/bbnurse +++ b/bbnurse @@ -123,8 +123,8 @@ args=( -u[n|m] package|Class [DEST] Il y a deux types d'évolutions possibles: -* mettre en nurserie -* arriver à maturation +* mettre en nurserie (-n) +* arriver à maturation (-m) Pour chaque type d'évolution, il y a deux actions possibles: * copier (-c) diff --git a/composer.json b/composer.json index b55b2c5..0d7bea4 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ } ], "require": { - "nulib/php": "dev-wip", "symfony/yaml": "^5.0", "php": ">=7.4" }, diff --git a/src/AccessException.php b/src/AccessException.php new file mode 100644 index 0000000..22d36a2 --- /dev/null +++ b/src/AccessException.php @@ -0,0 +1,36 @@ + $file, + "line" => $line, + "class" => $class, + "object" => null, + "type" => $type, + "function" => $function, + "args" => [], + ]; + } + return $frames; + } + + function __construct(Throwable $exception) { + $this->class = get_class($exception); + $this->message = $exception->getMessage(); + $this->code = $exception->getCode(); + $this->file = $exception->getFile(); + $this->line = $exception->getLine(); + $this->trace = self::extract_trace($exception->getTrace()); + $previous = $exception->getPrevious(); + if ($previous !== null) $this->previous = new static($previous); + } + + /** @var string */ + protected $class; + + function getClass(): string { + return $this->class; + } + + /** @var string */ + protected $message; + + function getMessage(): string { + return $this->message; + } + + /** @var mixed */ + protected $code; + + function getCode() { + return $this->code; + } + + /** @var string */ + protected $file; + + function getFile(): string { + return $this->file; + } + + /** @var int */ + protected $line; + + function getLine(): int { + return $this->line; + } + + /** @var array */ + protected $trace; + + function getTrace(): array { + return $this->trace; + } + + function getTraceAsString(): string { + $lines = []; + foreach ($this->trace as $index => $frame) { + $lines[] = "#$index $frame[file]($frame[line]): $frame[class]$frame[type]$frame[function]()"; + } + $index++; + $lines[] = "#$index {main}"; + return implode("\n", $lines); + } + + /** @var ExceptionShadow */ + protected $previous; + + function getPrevious(): ?ExceptionShadow { + return $this->previous; + } +} diff --git a/src/ExitException.php b/src/ExitException.php new file mode 100644 index 0000000..327d417 --- /dev/null +++ b/src/ExitException.php @@ -0,0 +1,22 @@ +getCode() !== 0; + } + + function haveMessage(): bool { + return $this->getUserMessage() !== null; + } +} diff --git a/src/StateException.php b/src/StateException.php new file mode 100644 index 0000000..1561943 --- /dev/null +++ b/src/StateException.php @@ -0,0 +1,23 @@ +getUserMessage(); + else return null; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_user_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_user_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + $parts[] = get_class($e) . ": " . $message; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static function get_message($e): ?string { + $message = $e->getMessage(); + if (!$message && $e instanceof self) $message = $e->getUserMessage(); + return $message; + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_summary($e): string { + $parts = []; + $first = true; + while ($e !== null) { + $message = self::get_message($e); + if (!$message) $message = "(no message)"; + if ($first) $first = false; + else $parts[] = "caused by "; + if ($e instanceof ExceptionShadow) $class = $e->getClass(); + else $class = get_class($e); + $parts[] = "$class: $message"; + $e = $e->getPrevious(); + } + return implode(", ", $parts); + } + + /** @param Throwable|ExceptionShadow $e */ + static final function get_traceback($e): string { + $tbs = []; + $previous = false; + while ($e !== null) { + if (!$previous) { + $efile = $e->getFile(); + $eline = $e->getLine(); + $tbs[] = "at $efile($eline)"; + } else { + $tbs[] = "~~ caused by: " . self::get_summary($e); + } + $tbs[] = $e->getTraceAsString(); + $e = $e->getPrevious(); + $previous = true; + #XXX il faudrait ne pas réinclure les lignes communes aux exceptions qui + # ont déjà été affichées + } + return implode("\n", $tbs); + } + + function __construct($user_message, $tech_message=null, $code=0, ?Throwable $previous=null) { + $this->userMessage = $user_message; + if ($tech_message === null) $tech_message = $user_message; + parent::__construct($tech_message, $code, $previous); + } + + protected $userMessage; + + function getUserMessage(): ?string { + return $this->userMessage; + } +} diff --git a/src/ValueException.php b/src/ValueException.php new file mode 100644 index 0000000..66a345a --- /dev/null +++ b/src/ValueException.php @@ -0,0 +1,65 @@ +"; + } elseif (is_array($value)) { + $values = $value; + $parts = []; + foreach ($values as $value) { + $parts[] = self::value($value); + } + return "[" . implode(", ", $parts) . "]"; + } elseif (is_string($value)) { + return $value; + } else { + return var_export($value, true); + } + } + + private static function message($value, ?string $message, ?string $kind, ?string $prefix, ?string $suffix): string { + if ($kind === null) $kind = "value"; + if ($message === null) $message = "$kind$suffix"; + if ($value !== null) { + $value = self::value($value); + if ($prefix) $prefix = "$prefix: $value"; + else $prefix = $value; + } + if ($prefix) $prefix = "$prefix: "; + return $prefix.$message; + } + + static final function null(?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message(null, $message, $kind, $prefix, " should not be null")); + } + + static final function invalid_kind($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is invalid")); + } + + static final function invalid_key($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "key", $prefix, $message); + } + + static final function invalid_value($value, ?string $prefix=null, ?string $message=null): self { + return self::invalid_kind($value, "value", $prefix, $message); + } + + static final function invalid_type($value, string $expected_type): self { + return new static(self::message($value, null, "type", null, " is invalid, expected $expected_type")); + } + + static final function invalid_class($class, string $expected_class): self { + if (is_object($class)) $class = get_class($class); + return new static(self::message($class, null, "class", null, " is invalid, expected $expected_class")); + } + + static final function forbidden($value=null, ?string $kind=null, ?string $prefix=null, ?string $message=null): self { + return new static(self::message($value, $message, $kind, $prefix, " is forbidden")); + } +} diff --git a/src/cl.php b/src/cl.php new file mode 100644 index 0000000..6d57b73 --- /dev/null +++ b/src/cl.php @@ -0,0 +1,509 @@ + $value) { + if (is_int($key)) return true; + } + return false; + } + + /** + * tester si $array est une liste, c'est à dire un tableau non null avec + * uniquement des clés numériques séquentielles commençant à zéro + * + * NB: is_list(null) === false + * et is_list([]) === true + */ + static final function is_list(?array $array): bool { + if ($array === null) return false; + $index = -1; + foreach ($array as $key => $value) { + ++$index; + if ($key !== $index) return false; + } + return true; + } + + /** + * tester si $array contient la clé $key + * + * @param array|ArrayAccess $array + */ + static final function has($array, $key): bool { + if (is_array($array)) { + return array_key_exists($key, $array); + } elseif ($array instanceof ArrayAccess) { + return $array->offsetExists($key); + } + return false; + } + + /** + * retourner $array[$key] ou $default si la clé n'existe pas + * + * @param array|ArrayAccess $array + */ + static final function get($array, $key, $default=null) { + if (is_array($array)) { + if (array_key_exists($key, $array)) return $array[$key]; + } elseif ($array instanceof ArrayAccess) { + if ($array->offsetExists($key)) return $array->offsetGet($key); + } + return $default; + } + + /** + * si $array est un array ou une instance de ArrayAccess, créer ou modifier + * l'élément dont la clé est $key + * + * @param array|ArrayAccess $array + */ + static final function set(&$array, $key, $value): void { + if (is_array($array) || $array === null) { + if ($key === null) $array[] = $value; + else $array[$key] = $value; + } elseif ($array instanceof ArrayAccess) { + $array->offsetSet($key, $value); + } + } + + /** + * si $array est un array ou une instance de ArrayAccess, supprimer l'élément + * dont la clé est $key + * + * @param array|ArrayAccess $array + */ + static final function del(&$array, $key): void { + if (is_array($array)) { + unset($array[$key]); + } elseif ($array instanceof ArrayAccess) { + $array->offsetUnset($key); + } + } + + /** retourner le nombre d'éléments de $array */ + static final function count(?array $array): int { + return $array !== null? count($array): 0; + } + + /** retourner la liste des clés de $array */ + static final function keys(?array $array): array { + return $array !== null? array_keys($array): []; + } + + ############################################################################# + + /** + * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées. + * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null. + */ + static final function merge(...$arrays): ?array { + $merges = []; + foreach ($arrays as $array) { + self::ensure_narray($array); + if ($array !== null) $merges[] = $array; + } + return $merges? array_merge(...$merges): null; + } + + ############################################################################# + + /** + * vérifier que le chemin $keys existe dans le tableau $array + * + * si $keys est vide ou null, retourner true + */ + static final function phas($array, $pkey): bool { + # optimisations + if ($pkey === null || $pkey === []) { + return true; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::has($array, $pkey); + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # phas + $first = true; + foreach($pkey as $key) { + if ($key === "" && $first) { + # une chaine vide en première position est ignorée + continue; + } elseif (is_array($array)) { + if (!array_key_exists($key, $array)) return false; + $array = $array[$key]; + } elseif ($array instanceof ArrayAccess) { + if (!$array->offsetExists($key)) return false; + $array = $array->offsetGet($key); + } else { + return false; + } + $first = false; + } + return true; + } + + static final function each_phas($array, ?array $pkeys): array { + $result = []; + if ($pkeys !== null) { + foreach ($pkeys as $pkey) { + $result[] = self::phas($array, $pkey); + } + } + return $result; + } + + /** + * obtenir la valeur correspondant au chemin $keys dans $array + * + * si $keys est vide ou null, retourner $default + */ + static final function pget($array, $pkey, $default=null) { + # optimisations + if ($pkey === null || $pkey === []) return $default; + elseif ($pkey === "") return $array; + elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + return self::get($array, $pkey, $default); + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pget + $value = $array; + $first = true; + foreach($pkey as $key) { + if ($key === "" && $first) { + # une chaine vide en première position est ignorée + continue; + } elseif (is_array($value)) { + if (!array_key_exists($key, $value)) return $default; + $value = $value[$key]; + } elseif ($value instanceof ArrayAccess) { + if (!$value->offsetExists($key)) return $default; + $value = $value->offsetGet($key); + } else { + return $default; + } + $first = false; + } + return $value; + } + + static final function each_pget($array, ?array $pkeys): array { + $result = []; + if ($pkeys !== null) { + foreach ($pkeys as $key => $pkey) { + $result[$key] = self::pget($array, $pkey); + } + } + return $result; + } + + /** + * modifier la valeur au chemin de clé $keys dans le tableau $array + * + * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g + * - pset($array, [""], $value) est équivalent à $array[] = $value + * - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value + * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position + * + * si $keys est vide ou null, $array est remplacé par $value + */ + static final function pset(&$array, $pkey, $value): void { + # optimisations + if ($pkey === null || $pkey === []) { + $array = $value; + return; + } elseif ($pkey === "") { + $array[] = $value; + return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::set($array, $pkey, $value); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pset + self::ensure_array($array); + $current =& $array; + $key = null; + $last = count($pkey) - 1; + $i = 0; + foreach ($pkey as $key) { + if ($i == $last) break; + if ($current instanceof ArrayAccess) { + if (!$current->offsetExists($key)) $current->offsetSet($key, []); + $current =& $current->offsetGet($key); + if ($current === null) { + $current = []; + } elseif (!is_array($current) && !($current instanceof ArrayAccess)) { + $current = [$current]; + } + } else { + self::ensure_array($current[$key]); + $current =& $current[$key]; + } + $i++; + } + if ($key === "") $current[] = $value; + else $current[$key] = $value; + } + + static final function each_pset(&$array, ?array $values): void { + if ($values !== null) { + foreach ($values as $pkey => $value) { + self::pset($array, $pkey, $value); + } + } + } + + /** + * supprimer la valeur au chemin de clé $keys dans $array + * + * si $array vaut null ou false, sa valeur est inchangée. + * si $keys est vide ou null, $array devient null + */ + static final function pdel(&$array, $pkey): void { + # optimisations + if ($array === null || $array === false) { + return; + } elseif ($pkey === null || $pkey === []) { + $array = null; + return; + } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) { + self::del($array, $pkey); + return; + } elseif (!is_array($pkey)) { + $pkey = explode(".", strval($pkey)); + } + # pdel + self::ensure_array($array); + $current =& $array; + $key = null; + $last = count($pkey) - 1; + $i = 0; + foreach ($pkey as $key) { + if ($i == $last) break; + if ($current instanceof ArrayAccess) { + if (!$current->offsetExists($key)) break; + } elseif (is_array($current)) { + if (!array_key_exists($key, $current)) break; + } else { + break; + } + $current =& $current[$key]; + $i++; + } + if ($i == $last) { + if ($current instanceof ArrayAccess) { + $current->offsetUnset($key); + } elseif (is_array($current)) { + unset($current[$key]); + } + } + } + + static final function each_pdel(&$array, ?array $pkeys): void { + if ($pkeys !== null) { + foreach ($pkeys as $pkey) { + self::pdel($array, $pkey); + } + } + } + + ############################################################################# + + /** + * retourner le tableau $array en "renommant" les clés selon le tableau + * $mappings qui contient des associations de la forme [$from => $to] + */ + static function rekey(?array $array, ?array $mappings): ?array { + if ($array === null || $mappings === null) return $array; + $mapped = []; + foreach ($array as $key => $value) { + if (array_key_exists($key, $mappings)) $key = $mappings[$key]; + $mapped[$key] = $value; + } + return $mapped; + } + + ############################################################################# + + /** tester si tous les éléments du tableau satisfont la condition */ + static final function all_if(?array $array, callable $cond): bool { + if ($array !== null) { + foreach ($array as $value) { + if (!$cond($value)) return false; + } + } + return true; + } + + static final function all_z(?array $array): bool { return self::all_if($array, [cv::class, "z"]);} + static final function all_nz(?array $array): bool { return self::all_if($array, [cv::class, "nz"]);} + static final function all_n(?array $array): bool { return self::all_if($array, [cv::class, "n"]);} + static final function all_nn(?array $array): bool { return self::all_if($array, [cv::class, "nn"]);} + static final function all_t(?array $array): bool { return self::all_if($array, [cv::class, "t"]);} + static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);} + static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);} + static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);} + static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); } + static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); } + static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); } + static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($value)); } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /** tester si au moins un élément du tableau satisfait la condition */ + static final function any_if(?array $array, callable $cond): bool { + if ($array !== null) { + foreach ($array as $value) { + if ($cond($value)) return true; + } + } + return false; + } + + static final function any_z(?array $array): bool { return self::any_if($array, [cv::class, "z"]);} + static final function any_nz(?array $array): bool { return self::any_if($array, [cv::class, "nz"]);} + static final function any_n(?array $array): bool { return self::any_if($array, [cv::class, "n"]);} + static final function any_nn(?array $array): bool { return self::any_if($array, [cv::class, "nn"]);} + static final function any_t(?array $array): bool { return self::any_if($array, [cv::class, "t"]);} + static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);} + static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);} + static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);} + static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); } + static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); } + static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); } + static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($value)); } + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + static final function filter_if(?array $array, callable $cond): ?array { + if ($array === null) return null; + $filtered = []; + $index = 0; + foreach ($array as $key => $value) { + if (!$cond($value)) { + if ($key === $index) { + $index++; + $filtered[] = $value; + } else { + $filtered[$key] = $value; + } + } elseif ($key === $index) { + $index++; + } + } + return $filtered; + } + + static final function filter_z(?array $array): ?array { return self::filter_if($array, [cv::class, "z"]);} + static final function filter_nz(?array $array): ?array { return self::filter_if($array, [cv::class, "nz"]);} + static final function filter_n(?array $array): ?array { return self::filter_if($array, [cv::class, "n"]);} + static final function filter_nn(?array $array): ?array { return self::filter_if($array, [cv::class, "nn"]);} + static final function filter_t(?array $array): ?array { return self::filter_if($array, [cv::class, "t"]);} + static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);} + static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);} + static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);} + static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); } + static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); } + static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); } + static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($value)); } + + ############################################################################# + + static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array { + if ($array === null) return null; + if ($assoc) asort($array, $flags); + else sort($array, $flags); + return $array; + } + + static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array { + if ($array === null) return null; + ksort($array, $flags); + return $array; + } + + /** + * retourner une fonction permettant de trier un tableau sur les clés + * spécifiées. + * + * - les clés ayant le préfixe '+' ou le suffixe '|asc' indiquent un tri + * ascendant + * - les clés ayant le préfixe '-' ou le suffixe '|desc' indiquent un tri + * descendant + * - sinon, par défaut, le tri est ascendant + */ + static final function compare(array $keys): callable { + return function ($a, $b) use ($keys) { + foreach ($keys as $key) { + if (str::del_prefix($key, "+")) $w = 1; + elseif (str::del_prefix($key, "-")) $w = -1; + elseif (str::del_suffix($key, "|asc")) $w = 1; + elseif (str::del_suffix($key, "|desc")) $w = -1; + else $w = 1; + if ($c = $w * cv::compare(cl::get($a, $key), cl::get($b, $key))) { + return $c; + } + } + return 0; + }; + } + + static final function usorted(?array $array, array $keys, bool $assoc=false): ?array { + if ($array === null) return null; + if ($assoc) uasort($array, self::compare($keys)); + else usort($array, self::compare($keys)); + return $array; + } +} diff --git a/src/cv.php b/src/cv.php new file mode 100644 index 0000000..5e6812e --- /dev/null +++ b/src/cv.php @@ -0,0 +1,216 @@ + $b) return 1; + return 0; + } +} diff --git a/src/os/README.md b/src/os/README.md index d8675cf..70983ad 100644 --- a/src/os/README.md +++ b/src/os/README.md @@ -1,4 +1,4 @@ -# nulib\os +# nur\sery\os Ce package contient des services permettant d'interagir avec le système d'exploitation diff --git a/src/os/file/SharedFile.php b/src/os/file/SharedFile.php index a45ac5a..3d93b99 100644 --- a/src/os/file/SharedFile.php +++ b/src/os/file/SharedFile.php @@ -1,7 +1,7 @@ $length) { + if ($ellips && $length > 3) $s = substr($s, 0, $length - 3)."..."; + else $s = substr($s, 0, $length); + } + if ($suffix !== null) $s .= $suffix; + return $s; + } + + /** trimmer $s */ + static final function trim(?string $s): ?string { + if ($s === null) return null; + else return trim($s); + } + + /** trimmer $s à gauche */ + static final function ltrim(?string $s): ?string { + if ($s === null) return null; + else return ltrim($s); + } + + /** trimmer $s à droite */ + static final function rtrim(?string $s): ?string { + if ($s === null) return null; + else return rtrim($s); + } + + static final function left(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size); + } + + static final function right(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, " ", STR_PAD_LEFT); + } + + static final function center(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, " ", STR_PAD_BOTH); + } + + static final function pad0(?string $s, int $size): ?string { + if ($s === null) return null; + else return str_pad($s, $size, "0", STR_PAD_LEFT); + } + + static final function lower(?string $s): ?string { + if ($s === null) return null; + else return strtolower($s); + } + + static final function lower1(?string $s): ?string { + if ($s === null) return null; + else return lcfirst($s); + } + + static final function upper(?string $s): ?string { + if ($s === null) return null; + else return strtoupper($s); + } + + static final function upper1(?string $s): ?string { + if ($s === null) return null; + else return ucfirst($s); + } + + static final function upperw(?string $s, ?string $delimiters=null): ?string { + if ($s === null) return null; + if ($delimiters !== null) return ucwords($s, $delimiters); + else return ucwords($s, " _-\t\r\n\f\v"); + } + + protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool { + if ($prefix === $s) return true; + $len = strlen($prefix); + if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false; + return $len == 0 || $prefix === substr($s, 0, $len); + } + + /** + * tester si $s commence par $prefix + * par exemple: + * - starts_with("", "whatever") est true + * - starts_with("fi", "first") est true + * - starts_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - starts_with("a", "abc", 2) est false + * - starts_with("a", "a", 2) est true + */ + static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $prefix === null) return false; + else return self::_starts_with($prefix, $s, $min_len); + } + + /** Retourner $s sans le préfixe $prefix s'il existe */ + static final function without_prefix(?string $prefix, ?string $s): ?string { + if ($s === null || $prefix === null) return $s; + if (self::_starts_with($prefix, $s)) $s = substr($s, strlen($prefix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le préfixe $prefix s'il existe + * + * retourner true si le préfixe a été enlevé. + */ + static final function del_prefix(?string &$s, ?string $prefix): bool { + if ($s === null || !self::_starts_with($prefix, $s)) return false; + $s = self::without_prefix($prefix, $s); + return true; + } + + /** + * Retourner $s avec le préfixe $prefix + * + * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà + */ + static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $prefix === null) return $s; + if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s; + return $s; + } + + /** + * modifier $s en place pour ajouter le préfixe $prefix + * + * retourner true si le préfixe a été ajouté. + */ + static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool { + if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false; + $s = self::with_prefix($prefix, $s, null, $unless_exists); + return true; + } + + protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool { + if ($suffix === $s) return true; + $len = strlen($suffix); + if ($min_len !== null && ($len < $min_len || $len > strlen($s))) return false; + return $len == 0 || $suffix === substr($s, -$len); + } + + /** + * tester si $string se termine par $suffix + * par exemple: + * - ends_with("", "whatever") est true + * - ends_with("st", "first") est true + * - ends_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - ends_with("c", "abc", 2) est false + * - ends_with("c", "c", 2) est true + */ + static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $suffix === null) return false; + else return self::_ends_with($suffix, $s, $min_len); + } + + /** Retourner $s sans le suffixe $suffix s'il existe */ + static final function without_suffix(?string $suffix, ?string $s): ?string { + if ($s === null || $suffix === null) return $s; + if (self::_ends_with($suffix, $s)) $s = substr($s, 0, -strlen($suffix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le suffixe $suffix s'il existe + * + * retourner true si le suffixe a été enlevé. + */ + static final function del_suffix(?string &$s, ?string $suffix): bool { + if ($s === null || !self::_ends_with($suffix, $s)) return false; + $s = self::without_suffix($suffix, $s); + return true; + } + + /** + * Retourner $s avec le suffixe $suffix + * + * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà + */ + static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $suffix === null) return $s; + if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix; + return $s; + } + + /** + * modifier $s en place pour ajouter le suffixe $suffix + * + * retourner true si le suffixe a été ajouté. + */ + static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool { + if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false; + $s = self::with_suffix($suffix, $s, null, $unless_exists); + return true; + } + + /** splitter $s en deux chaines séparées par $sep */ + static final function split_pair(?string $s, string $sep=":"): array { + if ($s === null) return [null, null]; + $parts = explode($sep, $s, 2); + if ($parts === false) return [null, null]; + if (count($parts) < 2) $parts[] = null; + return $parts; + } + + /** retourner $line sans son caractère de fin de ligne */ + static final function strip_nl(?string $line): ?string { + if ($line === null) return null; + if (substr($line, -2) == "\r\n") { + $line = substr($line, 0, -2); + } elseif (substr($line, -1) == "\n") { + $line = substr($line, 0, -1); + } elseif (substr($line, -1) == "\r") { + $line = substr($line, 0, -1); + } + return $line; + } + + /** + * normaliser le caractère de fin de ligne: tous les occurrences de [CR]LF et CR sont remplacées par LF + */ + static final function norm_nl(?string $s): ?string { + if ($s === null) return null; + $s = str_replace("\r\n", "\n", $s); + $s = str_replace("\r", "\n", $s); + return $s; + } + + /** + * joindre les éléments de $parts comme avec implode(), mais en ignorant les + * valeurs fausses (cela n'inclue pas la chaine "0") + * + * pour chaque valeur du tableau avec une clé associative, c'est la clé qui + * est utilisée mais uniquement si la valeur est vraie + */ + static final function join(string $glue, ?iterable $values): ?string { + if ($values === null) return null; + $pieces = []; + $index = 0; + foreach ($values as $key => $value) { + if (is_array($value)) $value = self::join($glue, $value); + if ($key === $index) { + $index++; + if (cv::t($value)) $pieces[] = $value; + } elseif (cv::t($value)) { + $pieces[] = $key; + } + } + return implode($glue, $pieces); + } + + /** + * comme {@link join()} mais en ignorant les valeurs fausses selon les règles + * de PHP + */ + static final function pjoin(string $glue, ?iterable $values): ?string { + if ($values === null) return null; + $pieces = []; + $index = 0; + foreach ($values as $key => $value) { + if (is_array($value)) $value = self::join($glue, $value); + if ($key === $index) { + $index++; + if ($value) $pieces[] = $value; + } elseif ($value) { + $pieces[] = $key; + } + } + return implode($glue, $pieces); + } + + const CAMEL_PATTERN0 = '/([A-Z0-9]+)$/A'; + const CAMEL_PATTERN1 = '/([A-Z0-9]+)[A-Z]/A'; + const CAMEL_PATTERN2 = '/([^A-Z]+)/A'; + const CAMEL_PATTERN3 = '/([A-Z][^A-Z]*)/A'; + + /** + * convertir une chaine de la forme "camelCase" en "under_score". le premier + * ensemble de caractères en majuscule est considéré comme étant en minuscule + * + * par exemple: + * - 'myCamelCase' devient 'my_camel_case' + * - 'AValue' devient 'a_value' + * - 'UPPERValue' devient 'upper_value' + * - 'UPPER' devient 'upper' + * - 'aXYZ' devient 'a_x_y_z' + * + * $delimiter est le séparateur en sortie ('_' par défaut) + * $upper indique s'il faut transformer le résultat en majuscule + */ + static final function camel2us(?string $camel, bool $upper=false, string $delimiter="_"): ?string { + if ($camel === null || $camel === "") return $camel; + $parts = []; + if (preg_match(self::CAMEL_PATTERN0, $camel, $vs, PREG_OFFSET_CAPTURE)) { + # que des majuscules + } elseif (preg_match(self::CAMEL_PATTERN1, $camel, $vs, PREG_OFFSET_CAPTURE)) { + # préfixe en majuscule + } elseif (preg_match(self::CAMEL_PATTERN2, $camel, $vs, PREG_OFFSET_CAPTURE)) { + # préfixe en minuscule + } else { + throw ValueException::invalid_kind($camel, "camel string"); + } + $parts[] = strtolower($vs[1][0]); + $index = intval($vs[1][1]) + strlen($vs[1][0]); + while (preg_match(self::CAMEL_PATTERN3, $camel, $vs, PREG_OFFSET_CAPTURE, $index)) { + $parts[] = strtolower($vs[1][0]); + $index = intval($vs[1][1]) + strlen($vs[1][0]); + } + $us = implode($delimiter, $parts); + if ($upper) $us = strtoupper($us); + return $us; + } + + const US_PATTERN = '/([ _\-\t\r\n\f\v])/'; + + /** + * convertir une chaine de la forme "under_score" en "camelCase" + * + * par exemple, 'my_camel_case' devient 'myCamelCalse' + * et 'UPPER_VALUE' devient 'upperValue' + * + * si la chaine de départ ne contient pas de delimiter, e.g 'myValue', elle + * est retournée inchangée + */ + static final function us2camel(?string $us, ?string $delimiters=null): ?string { + if ($us === null || $us === "") return $us; + if ($delimiters === null) $pattern = self::US_PATTERN; + else $pattern = '/(['.preg_quote($delimiters).'])/'; + $parts = preg_split($pattern, $us); + $count = count($parts); + if ($count == 1) return $us; + for ($i = 0; $i < $count; $i++) { + $part = strtolower($parts[$i]); + if ($i > 0) $part = ucfirst($part); + $parts[$i] = $part; + } + return implode("", $parts); + } +} diff --git a/src/values/akey.php b/src/values/akey.php index 325f141..27518a7 100644 --- a/src/values/akey.php +++ b/src/values/akey.php @@ -2,7 +2,7 @@ namespace nur\sery\values; use ArrayAccess; -use nulib\cl; +use nur\sery\cl; /** * Class akey: accéder aux valeurs d'un tableau diff --git a/src/values/mprop.php b/src/values/mprop.php index 1114e05..09e9d4d 100644 --- a/src/values/mprop.php +++ b/src/values/mprop.php @@ -1,8 +1,8 @@