ajout iter et Cursor

This commit is contained in:
Jephté Clain 2025-01-30 18:47:55 +04:00
parent bf1037d3b9
commit 2f3b17094b
4 changed files with 521 additions and 4 deletions

8
composer.lock generated
View File

@ -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",

236
src/php/coll/Cursor.php Normal file
View File

@ -0,0 +1,236 @@
<?php
namespace nur\sery\wip\php\coll;
use Iterator;
use nulib\cl;
use nulib\php\func;
use nur\sery\wip\php\iter;
use Traversable;
/**
* Class Cursor: parcours des lignes itérable
*
* @property-read array|null $value alias pour $row
* @property-read iterable|null $rows
*/
class Cursor implements Iterator {
/**
* mapper le tableau source $row selon les règles suivantes illustrées dans
* l'exemple suivant:
* si
* $map = ["a", "b" => "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();
}
}
}

169
src/php/iter.php Normal file
View File

@ -0,0 +1,169 @@
<?php # -*- coding: utf-8 mode: php -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
namespace nur\sery\wip\php;
use Exception;
use Generator;
use Iterator;
use IteratorAggregate;
use nulib\php\ICloseable;
use nulib\StopException;
use nulib\ValueException;
use Traversable;
/**
* Class iter: gestion des itérateurs
*/
class iter {
private static function unexpected_type($object): ValueException {
return ValueException::invalid_type($object, "iterable");
}
/**
* fermer "proprement" un itérateur ou un générateur. retourner true en cas de
* succès, ou false si c'est un générateur et qu'il ne supporte pas l'arrêt
* avec StopException (la valeur de retour n'est alors pas disponible)
*/
static function close($it): bool {
if ($it instanceof ICloseable) {
$it->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;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace nur\sery\wip\php\coll;
use Exception;
use nulib\cl;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
use nulib\tests\TestCase;
use TypeError;
class CursorTest extends TestCase {
protected function setUp(): void {
msg::set_messenger(new StdMessenger());
}
const SCALARS = [0, 1, 2, 3, 4];
function generator() {
yield from self::SCALARS;
}
function testVanilla() {
$c = new Cursor(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($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" => 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);
}
}