ajout iter et Cursor
This commit is contained in:
parent
bf1037d3b9
commit
2f3b17094b
|
@ -589,7 +589,7 @@
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"url": "../nulib",
|
"url": "../nulib",
|
||||||
"reference": "fd27e2ee3709aaa1af114c698b625317631fce97"
|
"reference": "de3b84441d2b588475b4d6d74bffdc487e333034"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
|
@ -600,7 +600,7 @@
|
||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"ext-pcntl": "*",
|
"ext-pcntl": "*",
|
||||||
"ext-posix": "*",
|
"ext-posix": "*",
|
||||||
"nulib/tests": "7.4"
|
"nulib/tests": "^7.4"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
|
@ -736,7 +736,7 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
|
"url": "https://git.univ-reunion.fr/sda-php/nulib-tests.git",
|
||||||
"reference": "6ce8257560b42e8fb3eea03eba84d3877c9648ca"
|
"reference": "a8541304eeddf696040e65792f45308d1d292aed"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.3",
|
"php": ">=7.3",
|
||||||
|
@ -760,7 +760,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "fonctions et classes pour les tests",
|
"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",
|
"name": "phar-io/manifest",
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue