From 2f3b17094b4066199cbdb5e5138695dabd3a2bec Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 30 Jan 2025 18:47:55 +0400 Subject: [PATCH] ajout iter et Cursor --- composer.lock | 8 +- src/php/coll/Cursor.php | 236 ++++++++++++++++++++++++++++++ src/php/iter.php | 169 +++++++++++++++++++++ tests/wip/php/coll/CursorTest.php | 112 ++++++++++++++ 4 files changed, 521 insertions(+), 4 deletions(-) create mode 100644 src/php/coll/Cursor.php create mode 100644 src/php/iter.php create mode 100644 tests/wip/php/coll/CursorTest.php diff --git a/composer.lock b/composer.lock index 241c9cc..f8a19ee 100644 --- a/composer.lock +++ b/composer.lock @@ -589,7 +589,7 @@ "dist": { "type": "path", "url": "../nulib", - "reference": "fd27e2ee3709aaa1af114c698b625317631fce97" + "reference": "de3b84441d2b588475b4d6d74bffdc487e333034" }, "require": { "ext-json": "*", @@ -600,7 +600,7 @@ "ext-curl": "*", "ext-pcntl": "*", "ext-posix": "*", - "nulib/tests": "7.4" + "nulib/tests": "^7.4" }, "type": "library", "extra": { @@ -736,7 +736,7 @@ "source": { "type": "git", "url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git", - "reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca" + "reference": "a8541304eeddf696040e65792f45308d1d292aed" }, "require": { "php": ">=7.3", @@ -760,7 +760,7 @@ } ], "description": "fonctions et classes pour les tests", - "time": "2024-03-26T10:56:17+00:00" + "time": "2025-01-30T14:12:43+00:00" }, { "name": "phar-io/manifest", diff --git a/src/php/coll/Cursor.php b/src/php/coll/Cursor.php new file mode 100644 index 0000000..884f449 --- /dev/null +++ b/src/php/coll/Cursor.php @@ -0,0 +1,236 @@ + "x", "c" => function() { return "y"; }, "d" => null] + * alors retourner le tableau + * ["a" => $row["a"], "b" => $row["x"], "c" => "y", "d" => null] + */ + private static function map_row(array $row, ?array $map): array { + if ($map === null) return $row; + $index = 0; + $mapped = []; + foreach ($map as $key => $value) { + if ($key === $index) { + $index++; + if ($value === null) $mapped[] = null; + else $mapped[$value] = cl::get($row, $value); + } elseif (is_callable($value)) { + $func = func::with($value); + $value = cl::get($row, $key); + $mapped[$key] = $func->invoke([$value, $key, $row]); + } else { + if ($value === null) $mapped[$key] = null; + else $mapped[$key] = cl::get($row, $key); + } + } + return $mapped; + } + + /** + * tester si $row satisfait les conditions de $filter + * - $filter est un scalaire, le transformer en [$filter] + * - sinon $filter doit être un tableau de scalaires + * + * les règles des conditions sont les suivantes: + * - une valeur séquentielle $key est équivalente à la valeur associative + * $key => true + * - une valeur associative $key => bool indique que la clé correspondante ne + * doit pas (resp. doit) exister selon que bool vaut false (resp. true) + * - une valeur associative $key => $value indique que la clé correspondante + * doit exiter avec la valeur spécifiée + */ + private static function filter_row(array $row, $filter): bool { + if ($filter === null) return false; + if (!is_array($filter)) $filter = [$filter]; + if (!$filter) return false; + + $index = 0; + foreach ($filter as $key => $value) { + if ($key === $index) { + $index++; + if (!array_key_exists($value, $row)) return false; + } elseif (is_bool($value)) { + if ($value) { + if (!array_key_exists($value, $row)) return false; + } else { + if (array_key_exists($value, $row)) return false; + } + } else { + if (!array_key_exists($value, $row)) return false; + if ($row[$key] !== $value) return false; + } + } + return true; + } + + function __construct(?iterable $rows=null, ?array $params=null) { + if ($rows !== null) $params["rows"] = $rows; + + $rows = $params["rows"] ?? null; + $rowsGenerator = null; + $rowsFunc = $params["rows_func"] ?? null; + if ($rowsFunc !== null) { + if ($rowsFunc instanceof Traversable) { + $rowsGenerator = $rowsFunc; + $rowsFunc = null; + } else { + $rowsFunc = func::with($rowsFunc, [$rows]); + } + } elseif ($rows instanceof Traversable) { + $rowsGenerator = $rows; + } else { + $rowsFunc = func::with(function() use ($rows) { + return $rows; + }); + } + $this->rowsGenerator = $rowsGenerator; + $this->rowsFunc = $rowsFunc; + + $map = $params["map"] ?? null; + $mapFunc = $params["map_func"] ?? null; + if ($mapFunc !== null) { + $mapFunc = func::with($mapFunc); + } elseif ($map !== null) { + $mapFunc = func::with(function(array $row) use ($map) { + return self::map_row($row, $map); + }); + } + $this->mapFunc = $mapFunc; + + $filter = $params["filter"] ?? null; + $filterFunc = $params["filter_func"] ?? null; + if ($filterFunc !== null) { + $filterFunc = func::with($filterFunc); + } elseif ($filter !== null) { + $filterFunc = func::with(function(array $row) use ($filter) { + return self::filter_row($row, $filter); + }); + } + $this->filterFunc = $filterFunc; + } + + /** un générateur de lignes */ + private ?Traversable $rowsGenerator; + + /** une fonction qui retourne une instance de {@link iterable} */ + private ?func $rowsFunc; + + private ?func $mapFunc; + + private ?func $filterFunc; + + protected ?iterable $rows; + + public int $index; + + public int $origIndex; + + public $key; + + public $raw; + + public ?array $row; + + function __get($name) { + if ($name === "value") return $this->row; + elseif ($name == "rows") return $this->rows; + trigger_error("Undefined property $name"); + return null; + } + + protected function filter(): bool { + return false; + } + + protected function map(): ?array { + return $this->row; + } + + ############################################################################# + # Iterator + + function rewind() { + $this->index = 0; + $this->origIndex = 0; + $this->key = null; + $this->raw = null; + $this->row = null; + if ($this->rowsGenerator !== null) { + $this->rows = $this->rowsGenerator; + $this->rows->rewind(); + } else { + $this->rows = $this->rowsFunc->invoke(); + } + } + + function valid() { + $filter = $this->filterFunc; + $map = $this->mapFunc; + while ($valid = iter::valid($this->rows)) { + $this->raw = iter::current($this->rows, $this->key); + $this->row = cl::withn($this->raw); + if ($filter === null) $filtered = $this->filter(); + else $filtered = $filter->invoke([$this]); + if (!$filtered) { + if ($map === null) $this->row = $this->map(); + else $this->row = $map->invoke([$this]); + break; + } else { + iter::next($this->rows); + $this->origIndex++; + } + } + if (!$valid) { + iter::close($this->rows); + $this->rows = null; + $this->index = -1; + $this->origIndex = -1; + $this->key = null; + $this->raw = null; + $this->row = null; + } + return $valid; + } + + function current() { + return $this->row; + } + + function key() { + return $this->key; + } + + function next() { + iter::next($this->rows); + $this->index++; + $this->origIndex++; + } + + ############################################################################# + + function each(callable $func): void { + $func = func::with($func); + $this->rewind(); + while ($this->valid()) { + $func->invoke([$this]); + $this->next(); + } + } +} diff --git a/src/php/iter.php b/src/php/iter.php new file mode 100644 index 0000000..2501047 --- /dev/null +++ b/src/php/iter.php @@ -0,0 +1,169 @@ +close(); + return true; + } elseif ($it instanceof Generator) { + try { + $it->throw(new StopException()); + return true; + } catch (StopException $e) { + } + } + return false; + } + + /** + * retourner la première valeur du tableau, de l'itérateur ou de l'instance + * de Traversable, ou $default si aucun élément n'est trouvé. + */ + static final function first($values, $default=null) { + if ($values instanceof IteratorAggregate) $values = $values->getIterator(); + if ($values instanceof Iterator) { + try { + $values->rewind(); + $value = $values->valid()? $values->current(): $default; + } finally { + self::close($values); + } + } elseif (is_array($values) || $values instanceof Traversable) { + $value = $default; + foreach ($values as $value) { + break; + } + } else { + throw self::unexpected_type($values); + } + return $value; + } + + /** + * retourner la première clé du tableau, de l'itérateur ou de l'instance + * de Traversable, ou $default si aucun élément n'est trouvé. + */ + static final function first_key($values, $default=null) { + if ($values instanceof IteratorAggregate) $values = $values->getIterator(); + if ($values instanceof Iterator) { + try { + $values->rewind(); + $key = $values->valid()? $values->key(): $default; + } finally { + self::close($values); + } + } elseif (is_array($values) || $values instanceof Traversable) { + $key = $default; + foreach ($values as $key => $ignored) { + break; + } + } else { + throw self::unexpected_type($values); + } + return $key; + } + + ############################################################################# + # outils pour gérer de façon générique des instances de {@link Iterator} ou + # des arrays + + /** + * @param $it ?iterable|array + * @return bool true si l'itérateur ou le tableau ont pu être réinitialisés + */ + static function rewind(&$it, ?Exception &$exception=null): bool { + if ($it instanceof Iterator) { + try { + $exception = null; + $it->rewind(); + return true; + } catch (Exception $e) { + $exception = $e; + } + } elseif ($it !== null) { + reset($it); + return true; + } + return false; + } + + /** + * @param $it ?iterable|array + */ + static function valid($it): bool { + if ($it instanceof Iterator) { + return $it->valid(); + } elseif ($it !== null) { + return key($it) !== null; + } else { + return false; + } + } + + /** + * @param $it ?iterable|array + */ + static function current($it, &$key=null) { + if ($it instanceof Iterator) { + $key = $it->key(); + return $it->current(); + } elseif ($it !== null) { + $key = key($it); + return current($it); + } else { + $key = null; + return null; + } + } + + /** + * @param $it ?iterable|array + */ + static function next(&$it, ?Exception &$exception=null): void { + if ($it instanceof Iterator) { + try { + $exception = null; + $it->next(); + } catch (Exception $e) { + $exception = $e; + } + } elseif ($it !== null) { + next($it); + } + } + + /** + * obtenir la valeur de retour si $it est un générateur terminé, ou null sinon + */ + static function get_return($it) { + if ($it instanceof Generator) { + try { + return $it->getReturn(); + } catch (Exception $e) { + } + } + return null; + } +} diff --git a/tests/wip/php/coll/CursorTest.php b/tests/wip/php/coll/CursorTest.php new file mode 100644 index 0000000..ec820d8 --- /dev/null +++ b/tests/wip/php/coll/CursorTest.php @@ -0,0 +1,112 @@ +generator()); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + self::assertException(Exception::class, function() use ($c) { + // pas possible de rewind un générateur + return cl::all($c); + }); + + $c = new Cursor(null, [ + "rows" => function() { + return self::SCALARS; + }, + ]); + self::assertError(TypeError::class, function() use ($c) { + // rows doit être un iterable, pas une fonction + return cl::all($c); + }); + + $c = new Cursor(null, [ + "rows_func" => function() { + return self::SCALARS; + }, + ]); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + + $c = new Cursor(null, [ + "rows_func" => $this->generator(), + ]); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + self::assertException(Exception::class, function() use ($c) { + // pas possible de rewind un générateur + return cl::all($c); + }); + + $c = new Cursor(null, [ + "rows_func" => function() { + yield from self::SCALARS; + }, + ]); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + self::assertSame([[0], [1], [2], [3], [4]], cl::all($c)); + } + + function testMap() { + $c = new Cursor(self::SCALARS, [ + "map_func" => function(Cursor $c) { + return [$c->raw + 1]; + }, + ]); + self::assertSame([[1], [2], [3], [4], [5]], cl::all($c)); + } + + function testFilter() { + $c = new Cursor(self::SCALARS, [ + "filter_func" => function(Cursor $c) { + return $c->raw % 2 == 0; + }, + ]); + self::assertSame([[1], [3]], cl::all($c)); + } + + function testEach() { + $c = new Cursor(self::SCALARS, [ + "filter_func" => function(Cursor $c) { + return $c->raw % 2 == 0; + }, + "map_func" => function(Cursor $c) { + return [$c->raw + 1]; + }, + ]); + $xs = []; + $xitems = []; + $oxs = []; + $kitems = []; + $c->each(function(Cursor $c) use (&$xs, &$xitems, &$oxs, &$kitems) { + $xs[] = $c->index; + $oxs[] = $c->origIndex; + $xitems[$c->index] = $c->row[0]; + $kitems[$c->key] = $c->row[0]; + }); + self::assertSame([0, 1], $xs); + self::assertSame([2, 4], $xitems); + self::assertSame([1, 3], $oxs); + self::assertSame([1 => 2, 3 => 4], $kitems); + } +}