Page MenuHomeDevCentral

D2493.id6319.diff
No OneTemporary

D2493.id6319.diff

diff --git a/src/Collections/BaseCollection.php b/src/Collections/BaseCollection.php
new file mode 100644
--- /dev/null
+++ b/src/Collections/BaseCollection.php
@@ -0,0 +1,26 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Collections;
+
+interface BaseCollection {
+
+ ///
+ /// Constructors
+ ///
+
+ public static function from (iterable $items) : static;
+
+ ///
+ /// Getters
+ ///
+
+ public function toArray () : array;
+
+ ///
+ /// Properties
+ ///
+
+ public function count () : int;
+
+}
diff --git a/src/Collections/BaseMap.php b/src/Collections/BaseMap.php
new file mode 100644
--- /dev/null
+++ b/src/Collections/BaseMap.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Keruald\OmniTools\Collections;
+
+interface BaseMap {
+
+ public function get (mixed $key) : mixed;
+
+ public function getOr (mixed $key, mixed $defaultValue): mixed;
+
+ public function set (mixed $key, mixed $value) : static;
+
+ public function has (mixed $key) : bool;
+
+ public function contains (mixed $value) : bool;
+
+}
diff --git a/src/Collections/HashMap.php b/src/Collections/HashMap.php
new file mode 100644
--- /dev/null
+++ b/src/Collections/HashMap.php
@@ -0,0 +1,167 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Collections;
+
+use Keruald\OmniTools\Reflection\CallableElement;
+
+use InvalidArgumentException;
+
+/**
+ * An associative array allowing the use of chained
+ *
+ *
+ * This class can be used as a service container,
+ * an application context, to store configuration.
+ */
+class HashMap implements BaseCollection, BaseMap {
+
+ ///
+ /// Properties
+ ///
+
+ private array $map;
+
+ ///
+ /// Constructor
+ ///
+
+ public function __construct (iterable $iterable = []) {
+ if (is_array($iterable)) {
+ $this->map = (array)$iterable;
+ return;
+ }
+
+ foreach ($iterable as $key => $value) {
+ $this->map[$key] = $value;
+ }
+ }
+
+ public static function from (iterable $items) : static {
+ return new self($items);
+ }
+
+ ///
+ /// Interact with map content at key level
+ ///
+
+ public function get (mixed $key) : mixed {
+ if (!array_key_exists($key, $this->map)) {
+ throw new InvalidArgumentException("Key not found.");
+ }
+
+ return $this->map[$key];
+ }
+
+ public function getOr (mixed $key, mixed $defaultValue) : mixed {
+ return $this->map[$key] ?? $defaultValue;
+ }
+
+ public function set (mixed $key, mixed $value) : static {
+ $this->map[$key] = $value;
+
+ return $this;
+ }
+
+ public function has (mixed $key) : bool {
+ return array_key_exists($key, $this->map);
+ }
+
+ public function contains (mixed $value) : bool {
+ return in_array($value, $this->map);
+ }
+
+ ///
+ /// Interact with collection content at collection level
+ ///
+
+ public function count () : int {
+ return count($this->map);
+ }
+
+ public function clear () : self {
+ $this->map = [];
+
+ return $this;
+ }
+
+ /**
+ * Merge the specified map with the current map.
+ *
+ * If a key already exists, the value already set is kept.
+ *
+ * @see update() when you need to update with the new value.
+ */
+ public function merge (iterable $iterable) : self {
+ foreach ($iterable as $key => $value) {
+ $this->map[$key] ??= $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Merge the specified map with the current bag.
+ *
+ * If a key already exists, the value is updated with the new one.
+ *
+ * @see merge() when you need to keep old value.
+ */
+ public function update (iterable $iterable) : self {
+ foreach ($iterable as $key => $value) {
+ $this->map[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets a copy of the internal map.
+ *
+ * Scalar values (int, strings) are cloned.
+ * Objects are references to a specific objet, not a clone.
+ *
+ * @return array<string, mixed>
+ */
+ public function toArray () : array {
+ return $this->map;
+ }
+
+ ///
+ /// HOF
+ ///
+
+ public function map (callable $callable) : self {
+ return new self(array_map($callable, $this->map));
+ }
+
+ public function filter (callable $callable) : self {
+ $argc = (new CallableElement($callable))->countArguments();
+ if ($argc === 0) {
+ throw new InvalidArgumentException(
+ "Callback should have at least one argument"
+ );
+ }
+ $mode = (int)($argc > 1);
+
+ return new self(
+ array_filter($this->map, $callable, $mode)
+ );
+ }
+
+ public function mapKeys (callable $callable) : self {
+ $mappedMap = [];
+ foreach ($this->map as $key => $value) {
+ $mappedMap[$callable($key)] = $value;
+ }
+
+ return new self($mappedMap);
+ }
+
+ public function filterKeys (callable $callable) : self {
+ return new self(
+ array_filter($this->map, $callable, ARRAY_FILTER_USE_KEY)
+ );
+ }
+
+}
diff --git a/src/Collections/OmniArray.php b/src/Collections/OmniArray.php
deleted file mode 100644
--- a/src/Collections/OmniArray.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Keruald\OmniTools\Collections;
-
-use Keruald\OmniTools\Strings\Multibyte\OmniString;
-
-class OmniArray {
-
- /**
- * @var array
- */
- private $items = [];
-
- ///
- /// Constructors
- ///
-
- public function __construct (?iterable $items = null) {
- if ($items === null) {
- return;
- }
-
- if (is_array($items)) {
- $this->items = $items;
-
- return;
- }
-
- foreach ($items as $item) {
- $this->items[] = $item;
- }
- }
-
- public static function explode (string $delimiter, string $string,
- int $limit = PHP_INT_MAX) : self {
- return (new OmniString($string))
- ->explode($delimiter, $limit);
- }
-
- ///
- /// Transformation methods
- ///
-
- public function toIntegers () : self {
- array_walk($this->items, ArrayUtilities::toIntegerCallback());
-
- return $this;
- }
-
- public function map (callable $callable) : self {
- $items = array_map($callable, $this->items);
-
- return new self($items);
- }
-
- public function implode(string $delimiter) : OmniString {
- return new OmniString(implode($delimiter, $this->items));
- }
-
- ///
- /// Getters methods
- ///
-
- public function toArray () : array {
- return $this->items;
- }
-
-}
diff --git a/src/Collections/SharedBag.php b/src/Collections/SharedBag.php
new file mode 100644
--- /dev/null
+++ b/src/Collections/SharedBag.php
@@ -0,0 +1,32 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Collections;
+
+/**
+ * A shared bag is a collection of key and values, which implements
+ * a monostate pattern, i.e. there is only one bag, which can be accessed
+ * though an arbitrary amount of SharedBag instances.
+ *
+ * The SharedBag class can be used as:
+ * — shared context, to contain the application configuration
+ * — service locator, to contain application dependencies
+ * — a migration path to store global variables of a legacy application
+ * pending the migration to a collection sharing the same interface
+ *
+ * Such patterns can be discouraged and as such used with architectural care,
+ * as they mainly use SharedBag as global variables, or as an antipattern.
+ */
+class SharedBag {
+
+ private static ?HashMap $bag = null;
+
+ public function getBag() : HashMap {
+ if (self::$bag === null) {
+ self::$bag = new HashMap;
+ }
+
+ return self::$bag;
+ }
+
+}
diff --git a/src/Collections/TraversableUtilities.php b/src/Collections/TraversableUtilities.php
--- a/src/Collections/TraversableUtilities.php
+++ b/src/Collections/TraversableUtilities.php
@@ -4,6 +4,7 @@
namespace Keruald\OmniTools\Collections;
use Countable;
+use InvalidArgumentException;
use ResourceBundle;
use SimpleXMLElement;
use TypeError;
@@ -11,7 +12,7 @@
class TraversableUtilities {
public static function count ($countable) : int {
- if (self::isCountable($countable)) {
+ if (is_countable($countable)) {
return count($countable);
}
@@ -22,6 +23,29 @@
throw new TypeError;
}
+ public static function first (iterable $iterable) : mixed {
+ foreach ($iterable as $value) {
+ return $value;
+ }
+
+ throw new InvalidArgumentException(
+ "Can't call first() on an empty iterable."
+ );
+ }
+
+ public static function firstOr (
+ iterable $iterable, mixed $defaultValue = null
+ ) : mixed {
+ foreach ($iterable as $value) {
+ return $value;
+ }
+
+ return $defaultValue;
+ }
+
+ /**
+ * @deprecated Use \is_countable
+ */
public static function isCountable ($countable) : bool {
if (function_exists('is_countable')) {
// PHP 7.3 has is_countable
diff --git a/src/Collections/Vector.php b/src/Collections/Vector.php
new file mode 100644
--- /dev/null
+++ b/src/Collections/Vector.php
@@ -0,0 +1,206 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Collections;
+
+use Keruald\OmniTools\Reflection\CallableElement;
+use Keruald\OmniTools\Strings\Multibyte\OmniString;
+
+use InvalidArgumentException;
+
+class Vector implements BaseCollection {
+
+ ///
+ /// Properties
+ ///
+
+ private array $items;
+
+ ///
+ /// Constructors
+ ///
+
+ public function __construct (iterable $items = []) {
+ if (is_array($items)) {
+ $this->items = $items;
+ return;
+ }
+
+ foreach ($items as $item) {
+ $this->items[] = $item;
+ }
+ }
+
+ public static function from (iterable $items) : static {
+ return new self($items);
+ }
+
+ ///
+ /// Specialized constructors
+ ///
+
+ /**
+ * Constructs a new instance of a vector by exploding a string
+ * according a specified delimiter.
+ *
+ * @param string $delimiter The substring to find for explosion
+ * @param string $string The string to explode
+ * @param int $limit If specified, the maximum count of vector elements
+ * @return static
+ */
+ public static function explode (string $delimiter, string $string,
+ int $limit = PHP_INT_MAX) : self {
+ // There is some discussion to know if this method belongs
+ // to Vector or OmniString.
+ //
+ // The advantage to keep it here is we can have constructs like:
+ // Vector::explode(",", "1,1,2,3,5,8,13")
+ // ->toIntegers()
+ // >map(function($n) { return $n * $n; })
+ // ->toArray();
+ //
+ // In this chaining, it is clear we manipulate Vector methods.
+
+ return (new OmniString($string))
+ ->explode($delimiter, $limit);
+ }
+
+ ///
+ /// Interact with collection content at key level
+ ///
+
+ public function get (int $key) : mixed {
+ if (!array_key_exists($key, $this->items)) {
+ throw new InvalidArgumentException("Key not found.");
+ }
+
+ return $this->items[$key];
+ }
+
+ public function getOr (int $key, mixed $defaultValue) : mixed {
+ return $this->items[$key] ?? $defaultValue;
+ }
+
+ public function set (int $key, mixed $value) : static {
+ $this->items[$key] = $value;
+
+ return $this;
+ }
+
+ public function contains (mixed $value) : bool {
+ return in_array($value, $this->items);
+ }
+
+
+ ///
+ /// Interact with collection content at collection level
+ ///
+
+ public function count () : int {
+ return count($this->items);
+ }
+
+ public function clear () : self {
+ $this->items = [];
+
+ return $this;
+ }
+
+ /**
+ * Append all elements of the specified iterable
+ * to the current vector.
+ *
+ * If a value already exists, the value is still added
+ * as a duplicate.
+ *
+ * @see update() when you need to only add unique values.
+ */
+ public function append (iterable $iterable) : self {
+ foreach ($iterable as $value) {
+ $this->items[] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Append all elements of the specified iterable
+ * to the current vector.
+ *
+ * If a value already exists, it is skipped.
+ *
+ * @see append() when you need to always add everything.
+ */
+ public function update (iterable $iterable) : self {
+ foreach ($iterable as $value) {
+ if (!$this->contains($value)) {
+ $this->items[] = $value;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets a copy of the internal vector.
+ *
+ * Scalar values (int, strings) are cloned.
+ * Objects are references to a specific objet, not a clone.
+ *
+ * @return array
+ */
+ public function toArray () : array {
+ return $this->items;
+ }
+
+ ///
+ /// HOF :: generic
+ ///
+
+ public function map (callable $callable) : self {
+ return new self(array_map($callable, $this->items));
+ }
+
+ public function filter (callable $callable) : self {
+ $argc = (new CallableElement($callable))->countArguments();
+
+ if ($argc === 0) {
+ throw new InvalidArgumentException(
+ "Callback should have at least one argument"
+ );
+ }
+
+ $mode = (int)($argc > 1);
+ return new self(array_filter($this->items, $callable, $mode));
+ }
+
+ public function mapKeys (callable $callable) : self {
+ $mappedVector = [];
+ foreach ($this->items as $key => $value) {
+ $mappedVector[$callable($key)] = $value;
+ }
+
+ return new self($mappedVector);
+ }
+
+ public function filterKeys (callable $callable) : self {
+ return new self(
+ array_filter($this->items, $callable, ARRAY_FILTER_USE_KEY)
+ );
+ }
+
+ ///
+ /// HOF :: specialized
+ ///
+
+ public function toIntegers () : self {
+ array_walk($this->items, ArrayUtilities::toIntegerCallback());
+
+ return $this;
+ }
+
+ public function implode(string $delimiter) : OmniString {
+ return new OmniString(implode($delimiter, $this->items));
+ }
+
+}
diff --git a/src/DateTime/DateStamp.php b/src/DateTime/DateStamp.php
--- a/src/DateTime/DateStamp.php
+++ b/src/DateTime/DateStamp.php
@@ -3,7 +3,7 @@
namespace Keruald\OmniTools\DateTime;
-use Keruald\OmniTools\Collections\OmniArray;
+use Keruald\OmniTools\Collections\Vector;
use DateTime;
use InvalidArgumentException;
@@ -47,7 +47,7 @@
public static function parse (string $date) : self {
if (preg_match("/^[0-9]{4}\-[0-1][0-9]\-[0-3][0-9]$/", $date)) {
// YYYY-MM-DD
- $args = OmniArray::explode("-", $date)
+ $args = Vector::explode("-", $date)
->toIntegers()
->toArray();
diff --git a/src/HTTP/URL.php b/src/HTTP/URL.php
--- a/src/HTTP/URL.php
+++ b/src/HTTP/URL.php
@@ -194,11 +194,11 @@
}
public static function normalizeDomain (string $domain) : string {
- return idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
+ return \idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
}
public static function beautifyDomain (string $domain) : string {
- return idn_to_utf8($domain, 0, INTL_IDNA_VARIANT_UTS46);
+ return \idn_to_utf8($domain, 0, INTL_IDNA_VARIANT_UTS46);
}
public function __toString () {
diff --git a/src/Reflection/CallableElement.php b/src/Reflection/CallableElement.php
new file mode 100644
--- /dev/null
+++ b/src/Reflection/CallableElement.php
@@ -0,0 +1,75 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Reflection;
+
+use Closure;
+use http\Exception\InvalidArgumentException;
+use ReflectionException;
+use ReflectionFunction;
+use ReflectionFunctionAbstract;
+use ReflectionMethod;
+
+class CallableElement {
+
+ private ReflectionFunctionAbstract $callable;
+
+ /**
+ * @throws ReflectionException
+ */
+ public function __construct (callable $callable) {
+ $this->callable = self::getReflectionFunction($callable);
+ }
+
+ /**
+ * @throws ReflectionException
+ */
+ private static function getReflectionFunction (callable $callable)
+ : ReflectionFunctionAbstract {
+
+ ///
+ /// Functions
+ ///
+
+ if ($callable instanceof Closure) {
+ return new ReflectionFunction($callable);
+ }
+
+ ///
+ /// Objets and methods
+ ///
+
+ if (is_array($callable)) {
+ return new ReflectionMethod($callable[0], $callable[1]);
+ }
+
+ if (is_object($callable)) {
+ // If __invoke() doesn't exist, the objet isn't a callable.
+ // Calling this method with such object would throw a TypeError
+ // before reaching this par of the code, so it is safe to assume
+ // we can correctly call it.
+ return new ReflectionMethod([$callable, '__invoke']);
+ }
+
+ ///
+ /// Hybrid cases
+ ///
+
+ if (is_string($callable)) {
+ if (!str_contains($callable, "::")) {
+ return new ReflectionFunction($callable);
+ }
+
+ return new ReflectionMethod($callable);
+ }
+
+ throw new InvalidArgumentException(
+ "Callable not recognized: " . gettype($callable)
+ );
+ }
+
+ public function countArguments () : int {
+ return $this->callable->getNumberOfParameters();
+ }
+
+}
diff --git a/src/Strings/Multibyte/OmniString.php b/src/Strings/Multibyte/OmniString.php
--- a/src/Strings/Multibyte/OmniString.php
+++ b/src/Strings/Multibyte/OmniString.php
@@ -3,7 +3,7 @@
namespace Keruald\OmniTools\Strings\Multibyte;
-use Keruald\OmniTools\Collections\OmniArray;
+use Keruald\OmniTools\Collections\Vector;
class OmniString {
@@ -54,11 +54,11 @@
}
public function startsWith (string $start) : bool {
- return StringUtilities::startsWith($this->value, $start);
+ return str_starts_with($this->value, $start);
}
public function endsWith (string $end) : bool {
- return StringUtilities::endsWith($this->value, $end);
+ return str_ends_with($this->value, $end);
}
public function len () : int {
@@ -76,7 +76,7 @@
return $chars;
}
- public function getBigrams () {
+ public function getBigrams () : array {
$bigrams = [];
$len = $this->len();
@@ -92,16 +92,16 @@
///
public function explode (string $delimiter,
- int $limit = PHP_INT_MAX) : OmniArray {
+ int $limit = PHP_INT_MAX) : Vector {
if ($delimiter === "") {
if ($limit < 0) {
- return new OmniArray;
+ return new Vector;
}
- return new OmniArray([$this->value]);
+ return new Vector([$this->value]);
}
- return new OmniArray(explode($delimiter, $this->value, $limit));
+ return new Vector(explode($delimiter, $this->value, $limit));
}
///
@@ -118,7 +118,7 @@
/**
* @param string $value
*/
- public function setValue (string $value) {
+ public function setValue (string $value) : void {
$this->value = $value;
}
diff --git a/src/Strings/Multibyte/StringUtilities.php b/src/Strings/Multibyte/StringUtilities.php
--- a/src/Strings/Multibyte/StringUtilities.php
+++ b/src/Strings/Multibyte/StringUtilities.php
@@ -43,18 +43,27 @@
return false;
}
- public static function startsWith (string $string, string $start) {
+ /**
+ * @deprecated Since PHP 8.0, we can replace by \str_starts_with
+ */
+ public static function startsWith (string $string, string $start) : bool {
$length = mb_strlen($start);
return mb_substr($string, 0, $length) === $start;
}
- public static function endsWith (string $string, string $end) {
+ /**
+ * @deprecated Since PHP 8.0, we can replace by \str_ends_with
+ */
+ public static function endsWith (string $string, string $end) : bool {
$length = mb_strlen($end);
return $length === 0 || mb_substr($string, -$length) === $end;
}
+ /**
+ * @deprecated Since PHP 8.0, we can replace by \str_contains
+ */
public static function contains (string $string, string $needle) : bool {
- return strpos($string, $needle) !== false;
+ return str_contains($string, $needle);
}
/**
diff --git a/tests/Collections/ArrayUtilitiesTest.php b/tests/Collections/ArrayUtilitiesTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Collections/ArrayUtilitiesTest.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Keruald\OmniTools\Tests\Collections;
+
+use Keruald\OmniTools\Collections\ArrayUtilities;
+
+use PHPUnit\Framework\TestCase;
+
+class ArrayUtilitiesTest extends TestCase {
+
+ /**
+ * @dataProvider provideIntegersArray
+ */
+ public function testToIntegers ($expected, $toConvert) {
+ $this->assertEquals($expected, ArrayUtilities::toIntegers($toConvert));
+ }
+
+ public function provideIntegersArray () : iterable {
+ yield [[1, 2, 3], ["1", "2", "3"]];
+
+ yield [[1, 2, 3], [1, 2, 3]];
+ yield [[], []];
+ }
+}
diff --git a/tests/Collections/HashMapTest.php b/tests/Collections/HashMapTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Collections/HashMapTest.php
@@ -0,0 +1,273 @@
+<?php
+
+namespace Keruald\OmniTools\Tests\Collections;
+
+use Keruald\OmniTools\Collections\HashMap;
+
+use PHPUnit\Framework\TestCase;
+
+use InvalidArgumentException;
+use IteratorAggregate;
+use Traversable;
+
+class HashMapTest extends TestCase {
+
+ ///
+ /// Test set up
+ ///
+
+ private HashMap $map;
+
+ const MAP_CONTENT = [
+ // Some sci-fi civilizations and author
+ "The Culture" => "Iain Banks",
+ "Radchaai Empire" => "Ann Leckie",
+ "Barrayar" => "Lois McMaster Bujold",
+ "Hainish" => "Ursula K. Le Guin",
+ ];
+
+ protected function setUp () : void {
+ $this->map = new HashMap(self::MAP_CONTENT);
+ }
+
+ ///
+ /// Constructors
+ ///
+
+ public function testConstructorWithArray () {
+ $this->assertSame(self::MAP_CONTENT, $this->map->toArray());
+ }
+
+ public function testConstructorWithTraversable () {
+ $expected = [
+ "color" => "blue",
+ "material" => "glass",
+ "shape" => "sphere",
+ ];
+
+ $iterable = new class implements IteratorAggregate {
+ function getIterator () : Traversable {
+ yield "color" => "blue";
+ yield "material" => "glass";
+ yield "shape" => "sphere";
+ }
+ };
+
+ $map = new HashMap($iterable);
+ $this->assertSame($expected, $map->toArray());
+ }
+
+ public function testFrom () {
+ $map = HashMap::from(self::MAP_CONTENT);
+ $this->assertSame(self::MAP_CONTENT, $map->toArray());
+ }
+
+ ///
+ /// Getters and setters
+ ///
+
+ public function testGet () {
+ $this->assertSame("Iain Banks", $this->map->get("The Culture"));
+ }
+
+ public function testGetWhenKeyIsNotFound () {
+ $this->expectException(InvalidArgumentException::class);
+
+ $this->map->get("Quuxians");
+ }
+
+ public function testGetOr () {
+ $actual = $this->map
+ ->getOr("The Culture", "Another author");
+
+ $this->assertSame("Iain Banks", $actual);
+ }
+
+ public function testGetOrWhenKeyIsNotFound () {
+ $actual = $this->map
+ ->getOr("Quuxians", "Another author");
+
+ $this->assertSame("Another author", $actual);
+ }
+
+ public function testSetWithNewKey () {
+ $this->map->set("Thélème", "François Rabelais");
+
+ $this->assertSame("François Rabelais",
+ $this->map->get("Thélème"));
+ }
+
+ public function testSetWithExistingKey () {
+ $this->map->set("The Culture", "Iain M. Banks");
+
+ $this->assertSame("Iain M. Banks",
+ $this->map->get("The Culture"));
+ }
+
+ public function testHas () {
+ $this->assertTrue($this->map->has("The Culture"));
+ $this->assertFalse($this->map->has("Not existing key"));
+ }
+
+ public function testContains () {
+ $this->assertTrue($this->map->contains("Iain Banks"));
+ $this->assertFalse($this->map->contains("Not existing value"));
+ }
+
+ ///
+ /// Collection method
+ ///
+
+ public function testCount () {
+ $this->assertSame(4, $this->map->count());
+ }
+
+ public function testClear () {
+ $this->map->clear();
+ $this->assertSame(0, $this->map->count());
+ }
+
+ public function testMerge () {
+ $iterable = [
+ "The Culture" => "Iain M. Banks", // existing key
+ "Thélème" => "François Rabelais", // new key
+ ];
+
+ $expected = [
+ // The original map
+ "The Culture" => "Iain Banks", // Old value should be kept
+ "Radchaai Empire" => "Ann Leckie",
+ "Barrayar" => "Lois McMaster Bujold",
+ "Hainish" => "Ursula K. Le Guin",
+
+ // The entries with a new key
+ "Thélème" => "François Rabelais",
+ ];
+
+ $this->map->merge($iterable);
+ $this->assertSame($expected, $this->map->toArray());
+ }
+
+ public function testUpdate () {
+ $iterable = [
+ "The Culture" => "Iain M. Banks", // existing key
+ "Thélème" => "François Rabelais", // new key
+ ];
+
+ $expected = [
+ // The original map
+ "The Culture" => "Iain M. Banks", // Old value should be updated
+ "Radchaai Empire" => "Ann Leckie",
+ "Barrayar" => "Lois McMaster Bujold",
+ "Hainish" => "Ursula K. Le Guin",
+
+ // The entries with a new key
+ "Thélème" => "François Rabelais",
+ ];
+
+ $this->map->update($iterable);
+ $this->assertSame($expected, $this->map->toArray());
+ }
+
+ public function testToArray () {
+ $this->assertEquals(self::MAP_CONTENT, $this->map->toArray());
+ }
+
+ ///
+ /// High order functions
+ ///
+
+ public function testMap () {
+ $callback = function ($value) {
+ return "author='" . $value . "'";
+ };
+
+ $expected = [
+ "The Culture" => "author='Iain Banks'",
+ "Radchaai Empire" => "author='Ann Leckie'",
+ "Barrayar" => "author='Lois McMaster Bujold'",
+ "Hainish" => "author='Ursula K. Le Guin'",
+ ];
+
+ $actual = $this->map->map($callback)->toArray();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testMapKeys () {
+ $callback = function ($key) {
+ return "civ::" . $key;
+ };
+
+ $expected = [
+ // Some sci-fi civilizations and author
+ "civ::The Culture" => "Iain Banks",
+ "civ::Radchaai Empire" => "Ann Leckie",
+ "civ::Barrayar" => "Lois McMaster Bujold",
+ "civ::Hainish" => "Ursula K. Le Guin",
+ ];
+
+ $actual = $this->map->mapKeys($callback)->toArray();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFilter () {
+ // Let's filter to keep names with 3 parts or more
+
+ $callback = function ($value) : bool {
+ return str_word_count($value) > 2;
+ };
+
+ $expected = [
+ // Some sci-fi civilizations and author
+ "Barrayar" => "Lois McMaster Bujold",
+ "Hainish" => "Ursula K. Le Guin",
+ ];
+
+ $actual = $this->map->filter($callback)->toArray();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFilterWithKeyValueCallback () {
+ // Let's find civilization AND author with e inside
+
+ $expected = [
+ // Some sci-fi civilizations and author
+ "Radchaai Empire" => "Ann Leckie",
+ ];
+
+ $callback = function ($key, $value) : bool {
+ return str_contains($key, "e") && str_contains($value, "e");
+ };
+
+ $actual = $this->map->filter($callback)->toArray();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFilterWithCallbackWithoutArgument() {
+ $this->expectException(InvalidArgumentException::class);
+
+ $callback = function () : bool { // No argument
+ return true;
+ };
+
+ $this->map->filter($callback);
+ }
+
+ public function testFilterKeys () {
+ // Let's filter to keep short civilization names
+
+ $callback = function ($key) : bool {
+ return str_word_count($key) == 1;
+ };
+
+ $expected = [
+ // Some sci-fi civilizations and author
+ "Barrayar" => "Lois McMaster Bujold",
+ "Hainish" => "Ursula K. Le Guin",
+ ];
+
+ $actual = $this->map->filterKeys($callback)->toArray();
+ $this->assertEquals($expected, $actual);
+ }
+
+}
diff --git a/tests/Collections/OmniArrayTest.php b/tests/Collections/OmniArrayTest.php
deleted file mode 100644
--- a/tests/Collections/OmniArrayTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Keruald\OmniTools\Tests\Collections;
-
-use Keruald\OmniTools\Collections\OmniArray;
-use PHPUnit\Framework\TestCase;
-
-class OmniArrayTest extends TestCase {
-
- public function testMap () : void {
- $actual = (new OmniArray([1, 2, 3, 4, 5]))
- ->map(function ($x) { return $x * $x; })
- ->toArray();
-
- $this->assertEquals([1, 4, 9, 16, 25], $actual);
- }
-
- public function testImplode() : void {
- $actual = (new OmniArray(["a", "b", "c"]))
- ->implode(".")
- ->__toString();
-
- $this->assertEquals("a.b.c", $actual);
- }
-
- public function testImplodeWithoutDelimiter() : void {
- $actual = (new OmniArray(["a", "b", "c"]))
- ->implode("")
- ->__toString();
-
- $this->assertEquals("abc", $actual);
- }
-
- public function testExplode() : void {
- $actual = OmniArray::explode(".", "a.b.c");
-
- $this->assertEquals(["a", "b", "c"], $actual->toArray());
- }
-
- public function testExplodeWithoutDelimiter() : void {
- $actual = OmniArray::explode("", "a.b.c");
-
- $this->assertEquals(["a.b.c"], $actual->toArray());
- }
-
-}
diff --git a/tests/Collections/TraversableUtilitiesTest.php b/tests/Collections/TraversableUtilitiesTest.php
--- a/tests/Collections/TraversableUtilitiesTest.php
+++ b/tests/Collections/TraversableUtilitiesTest.php
@@ -3,10 +3,15 @@
namespace Keruald\OmniTools\Tests\Collections;
+use Exception;
+use InvalidArgumentException;
+use IteratorAggregate;
+use JetBrains\PhpStorm\Internal\TentativeType;
use Keruald\OmniTools\Collections\TraversableUtilities;
use PHPUnit\Framework\TestCase;
use Countable;
+use Traversable;
class TraversableUtilitiesTest extends TestCase {
@@ -27,6 +32,39 @@
TraversableUtilities::count($notCountable);
}
+ /**
+ * @dataProvider providePureCountables
+ */
+ public function testIsCountable ($countable) {
+ $this->assertTrue(TraversableUtilities::isCountable($countable));
+ }
+
+ /**
+ * @dataProvider provideIterableAndFirst
+ */
+ public function testIsFirst($expected, $iterable) {
+ $this->assertEquals($expected, TraversableUtilities::first($iterable));
+ }
+
+ public function testIsFirstWithEmptyCollection() {
+ $this->expectException(InvalidArgumentException::class);
+
+ TraversableUtilities::first([]);
+ }
+
+ /**
+ * @dataProvider provideIterableAndFirst
+ */
+ public function testIsFirstOr($expected, $iterable) {
+ $actual = TraversableUtilities::firstOr($iterable, 666);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testIsFirstOrWithEmptyCollection() {
+ $actual = TraversableUtilities::firstOr([], 666);
+ $this->assertEquals(666, $actual);
+ }
+
///
/// Data providers
///
@@ -37,10 +75,21 @@
yield [0, []];
yield [3, ["a", "b", "c"]];
yield [42, new class implements Countable {
- public function count () : int {
- return 42;
- }
+ public function count () : int {
+ return 42;
}
+ }
+ ];
+ }
+
+ public function providePureCountables () : iterable {
+ yield [[]];
+ yield [["a", "b", "c"]];
+ yield [new class implements Countable {
+ public function count () : int {
+ return 42;
+ }
+ }
];
}
@@ -52,4 +101,17 @@
yield ["abc"];
}
+ public function provideIterableAndFirst() : iterable {
+ yield ["a", ["a", "b", "c"]];
+
+ yield ["apple", ["fruit" => "apple", "vegetable" => "leeks"]];
+
+ yield [42, new class implements IteratorAggregate {
+ public function getIterator () : Traversable {
+ yield 42;
+ yield 100;
+ }
+ }];
+ }
+
}
diff --git a/tests/Collections/VectorTest.php b/tests/Collections/VectorTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Collections/VectorTest.php
@@ -0,0 +1,171 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Tests\Collections;
+
+use Keruald\OmniTools\Collections\Vector;
+
+use PHPUnit\Framework\TestCase;
+
+use InvalidArgumentException;
+use IteratorAggregate;
+use Traversable;
+
+class VectorTest extends TestCase {
+
+ private Vector $vector;
+
+ protected function setUp () : void {
+ $this->vector = new Vector([1, 2, 3, 4, 5]);
+ }
+
+ public function testConstructorWithIterable () : void {
+ $iterable = new class implements IteratorAggregate {
+ public function getIterator () : Traversable {
+ yield 42;
+ yield 100;
+ }
+ };
+
+ $vector = new Vector($iterable);
+ $this->assertEquals([42, 100], $vector->toArray());
+ }
+
+ public function testFrom () : void {
+ $this->assertEquals([42, 100], Vector::from([42, 100])->toArray());
+ }
+
+ public function testGet () : void {
+ $vector = new Vector(["a", "b", "c"]);
+
+ $this->assertEquals("b", $vector->get(1));
+ }
+
+ public function testGetOverflow () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $this->vector->get(800);
+ }
+
+ public function testGetOr () : void {
+ $vector = new Vector(["a", "b", "c"]);
+
+ $this->assertEquals("X", $vector->getOr(800, "X"));
+ }
+
+ public function testSet () : void {
+ $vector = new Vector(["a", "b", "c"]);
+ $vector->set(1, "x"); // should replace "b"
+
+ $this->assertEquals(["a", "x", "c"], $vector->toArray());
+ }
+
+ public function testContains () : void {
+ $this->assertTrue($this->vector->contains(2));
+ $this->assertFalse($this->vector->contains(666));
+ }
+
+ public function testCount () : void {
+ $this->assertEquals(5, $this->vector->count());
+ $this->assertEquals(0, (new Vector)->count());
+ }
+
+ public function testClear () : void {
+ $this->vector->clear();
+
+ $this->assertEquals(0, $this->vector->count());
+ }
+
+ public function testAppend () : void {
+ $this->vector->append([6, 7, 8]);
+
+ $this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
+ }
+
+ public function testUpdate () : void {
+ $this->vector->update([5, 5, 5, 6, 7, 8]); // 5 already exists
+
+ $this->assertEquals([1, 2, 3, 4, 5, 6, 7 ,8], $this->vector->toArray());
+ }
+
+
+ public function testMap () : void {
+ $actual = $this->vector
+ ->map(function ($x) { return $x * $x; })
+ ->toArray();
+
+ $this->assertEquals([1, 4, 9, 16, 25], $actual);
+ }
+
+ public function testMapKeys () : void {
+ $vector = new Vector(["foo", "bar", "quux", "xizzy"]);
+
+ $filter = function ($key) {
+ return 0; // Let's collapse our array
+ };
+
+ $actual = $vector->mapKeys($filter)->toArray();
+ $this->assertEquals(["xizzy"], $actual);
+
+ }
+
+ public function testFilter () : void {
+ $vector = new Vector(["foo", "bar", "quux", "xizzy"]);
+
+ $filter = function ($item) {
+ return strlen($item) === 3; // Let's keep 3-letters words
+ };
+
+ $actual = $vector->filter($filter)->toArray();
+ $this->assertEquals(["foo", "bar"], $actual);
+ }
+
+ public function testFilterWithBadCallback () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $badFilter = function () {};
+
+ $this->vector->filter($badFilter);
+ }
+
+ public function testFilterKeys () : void {
+ $filter = function ($key) {
+ return $key % 2 === 0; // Let's keep even indices
+ };
+
+ $actual = $this->vector
+ ->filterKeys($filter)
+ ->toArray();
+
+ $this->assertEquals([0, 2, 4], array_keys($actual));
+ }
+
+ public function testImplode() : void {
+ $actual = (new Vector(["a", "b", "c"]))
+ ->implode(".")
+ ->__toString();
+
+ $this->assertEquals("a.b.c", $actual);
+ }
+
+ public function testImplodeWithoutDelimiter() : void {
+ $actual = (new Vector(["a", "b", "c"]))
+ ->implode("")
+ ->__toString();
+
+ $this->assertEquals("abc", $actual);
+ }
+
+ public function testExplode() : void {
+ $actual = Vector::explode(".", "a.b.c");
+
+ $this->assertEquals(["a", "b", "c"], $actual->toArray());
+ }
+
+ public function testExplodeWithoutDelimiter() : void {
+ $actual = Vector::explode("", "a.b.c");
+
+ $this->assertEquals(["a.b.c"], $actual->toArray());
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 18, 05:54 (21 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2250333
Default Alt Text
D2493.id6319.diff (38 KB)

Event Timeline