Page MenuHomeDevCentral

No OneTemporary

diff --git a/src/Collections/BaseCollection.php b/src/Collections/BaseCollection.php
new file mode 100644
index 0000000..bbb4913
--- /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
index 0000000..a588c71
--- /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
index 0000000..980f1de
--- /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
index 5ed44ff..0000000
--- 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
index 0000000..0938aa1
--- /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
index 48e212e..52364ef 100644
--- a/src/Collections/TraversableUtilities.php
+++ b/src/Collections/TraversableUtilities.php
@@ -1,38 +1,62 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Collections;
use Countable;
+use InvalidArgumentException;
use ResourceBundle;
use SimpleXMLElement;
use TypeError;
class TraversableUtilities {
public static function count ($countable) : int {
- if (self::isCountable($countable)) {
+ if (is_countable($countable)) {
return count($countable);
}
if ($countable === null || $countable === false) {
return 0;
}
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
return is_countable($countable);
}
// https://github.com/Ayesh/is_countable-polyfill/blob/master/src/is_countable.php
return is_array($countable)
|| $countable instanceof Countable
|| $countable instanceof SimpleXMLElement
|| $countable instanceof ResourceBundle;
}
}
diff --git a/src/Collections/Vector.php b/src/Collections/Vector.php
new file mode 100644
index 0000000..dad21fb
--- /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
index 30e38ee..637e892 100644
--- a/src/DateTime/DateStamp.php
+++ b/src/DateTime/DateStamp.php
@@ -1,89 +1,89 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\DateTime;
-use Keruald\OmniTools\Collections\OmniArray;
+use Keruald\OmniTools\Collections\Vector;
use DateTime;
use InvalidArgumentException;
class DateStamp {
///
/// Private members
///
/**
* @var int
*/
private $year;
/**
* @var int
*/
private $month;
/**
* @var int
*/
private $day;
///
/// Constructors
///
public function __construct (int $year, int $month, int $day) {
$this->year = $year;
$this->month = $month;
$this->day = $day;
}
public static function fromUnixTime (?int $unixtime = null) : self {
$dateStamp = date('Y-m-d', $unixtime ?? time());
return self::parse($dateStamp);
}
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();
return new DateStamp(...$args);
}
if (preg_match("/^[0-9]{4}[0-1][0-9][0-3][0-9]$/", $date)) {
// YYYYMMDD
return new DateStamp(
(int)substr($date, 0, 4), // YYYY
(int)substr($date, 4, 2), // MM
(int)substr($date, 6, 2) // DD
);
}
throw new InvalidArgumentException("YYYYMMDD or YYYY-MM-DD format expected, $date received.");
}
///
/// Convert methods
///
public function toUnixTime () : int {
return mktime(0, 0, 0, $this->month, $this->day, $this->year);
}
public function toDateTime () : DateTime {
return new DateTime($this->__toString());
}
public function toShortString () : string {
return date('Ymd', $this->toUnixTime());
}
public function __toString () : string {
return date('Y-m-d', $this->toUnixTime());
}
}
diff --git a/src/HTTP/URL.php b/src/HTTP/URL.php
index 80a8992..b992df9 100644
--- a/src/HTTP/URL.php
+++ b/src/HTTP/URL.php
@@ -1,208 +1,208 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\HTTP;
use Keruald\OmniTools\Strings\Multibyte\OmniString;
class URL {
///
/// Constants
///
/**
* Encode the query using RFC 3986, but keep / intact as a separators.
* As such, everything will be encoded excepted ~ - _ . / characters.
*/
const ENCODE_RFC3986_SLASH_EXCEPTED = 1;
/**
* Encode the query using RFC 3986, including the /.
* As such, everything will be encoded excepted ~ - _ . characters.
*/
const ENCODE_RFC3986_PURE = 2;
/**
* Consider the query already encoded.
*/
const ENCODE_AS_IS = 3;
///
/// Private members
///
/**
* @var string
*/
private $url;
/**
* @var int
*/
private $queryEncoding;
///
/// Constructors
///
public function __construct ($url,
$queryEncoding = self::ENCODE_RFC3986_SLASH_EXCEPTED) {
$this->url = $url;
$this->queryEncoding = $queryEncoding;
}
public static function compose (string $protocol, string $domain,
string $query,
$queryEncoding = self::ENCODE_RFC3986_SLASH_EXCEPTED
) : self {
return (new URL("", $queryEncoding))
->update($protocol, $domain, $query);
}
///
/// Getters and setters
///
public function getProtocol () : string {
if (preg_match("@(.*?)://.*@", $this->url, $matches)) {
return $matches[1];
}
return "";
}
private function getUrlParts() : array {
preg_match("@://(.*)@", $this->url, $matches);
return explode("/", $matches[1], 2);
}
public function getDomain () : string {
if (strpos($this->url, "://") === false) {
return "";
}
$domain = $this->getUrlParts()[0];
if ($domain === "") {
return "";
}
return self::beautifyDomain($domain);
}
public function getQuery () : string {
if (strpos($this->url, "://") === false) {
return $this->url;
}
$parts = $this->getUrlParts();
if (count($parts) < 2 || $parts[1] === "" || $parts[1] === "/") {
return "/";
}
return "/" . $this->beautifyQuery($parts[1]);
}
public function setProtocol ($protocol) : self {
$this->update($protocol, $this->getDomain(), $this->getQuery());
return $this;
}
public function setDomain ($domain) : self {
$this->update($this->getProtocol(), $domain, $this->getQuery());
return $this;
}
public function setQuery ($query,
$encodeMode = self::ENCODE_RFC3986_SLASH_EXCEPTED
) : self {
$this->queryEncoding = $encodeMode;
$this->update($this->getProtocol(), $this->getDomain(), $query);
return $this;
}
private function isRootQuery($query) : bool {
return $this->queryEncoding !== self::ENCODE_RFC3986_PURE
&& $query !== ""
&& $query[0] === '/';
}
private function update (string $protocol, string $domain, string $query) : self {
$url = "";
if ($domain !== "") {
if ($protocol !== "") {
$url = $protocol;
}
$url .= "://" . self::normalizeDomain($domain);
if (!$this->isRootQuery($query)) {
$url .= "/";
}
}
$url .= $this->normalizeQuery($query);
$this->url = $url;
return $this;
}
public function normalizeQuery (string $query) : string {
switch ($this->queryEncoding) {
case self::ENCODE_RFC3986_SLASH_EXCEPTED:
return (new OmniString($query))
->explode("/")
->map("rawurlencode")
->implode("/")
->__toString();
case self::ENCODE_AS_IS:
return $query;
case self::ENCODE_RFC3986_PURE:
return rawurlencode($query);
}
throw new \Exception('Unexpected encoding value');
}
public function beautifyQuery (string $query) : string {
switch ($this->queryEncoding) {
case self::ENCODE_RFC3986_SLASH_EXCEPTED:
return (new OmniString($query))
->explode("/")
->map("rawurldecode")
->implode("/")
->__toString();
case self::ENCODE_AS_IS:
return $query;
case self::ENCODE_RFC3986_PURE:
return rawurldecode($query);
}
throw new \Exception('Unexpected encoding value');
}
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 () {
return $this->url;
}
}
diff --git a/src/Reflection/CallableElement.php b/src/Reflection/CallableElement.php
new file mode 100644
index 0000000..3a99449
--- /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
index d18c780..5e94aa3 100644
--- a/src/Strings/Multibyte/OmniString.php
+++ b/src/Strings/Multibyte/OmniString.php
@@ -1,125 +1,125 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Strings\Multibyte;
-use Keruald\OmniTools\Collections\OmniArray;
+use Keruald\OmniTools\Collections\Vector;
class OmniString {
use WithEncoding;
///
/// Private members
///
/**
* @var string
*/
private $value;
///
/// Constructor
///
public function __construct (string $value = '', string $encoding = '') {
$this->value = $value;
$this->setEncoding($encoding ?: "UTF-8");
}
///
/// Magic methods
///
public function __toString() : string {
return $this->value;
}
///
/// Helper methods
///
public function pad(
int $padLength = 0,
string $padString = ' ',
int $padType = STR_PAD_RIGHT
) : string {
return (new StringPad)
->setInput($this->value)
->setEncoding($this->encoding)
->setPadLength($padLength)
->setPadString($padString)
->setPadType($padType)
->pad();
}
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 {
return mb_strlen($this->value, $this->encoding);
}
public function getChars () : array {
$chars = [];
$len = $this->len();
for ($i = 0 ; $i < $len ; $i++) {
$chars[] = mb_substr($this->value, $i, 1, $this->encoding);
}
return $chars;
}
- public function getBigrams () {
+ public function getBigrams () : array {
$bigrams = [];
$len = $this->len();
for ($i = 0 ; $i < $len - 1 ; $i++) {
$bigrams[] = mb_substr($this->value, $i, 2, $this->encoding);
}
return $bigrams;
}
///
/// Transformation methods
///
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));
}
///
/// Getters and setters
///
/**
* @return string
*/
public function getValue () : string {
return $this->value;
}
/**
* @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
index dd57d92..7702467 100644
--- a/src/Strings/Multibyte/StringUtilities.php
+++ b/src/Strings/Multibyte/StringUtilities.php
@@ -1,88 +1,97 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Strings\Multibyte;
class StringUtilities {
/**
* Pads a multibyte string to a certain length with another string
*
* @param string $input the input string
* @param int $padLength the target string size
* @param string $padString the padding characters (optional, default is space)
* @param int $padType STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH (optional, default is STR_PAD_RIGHT)
* @param string $encoding the character encoding (optional)
*
* @return string the padded string
*
*/
public static function pad (
string $input,
int $padLength,
string $padString = ' ',
int $padType = STR_PAD_RIGHT,
string $encoding = ''
) : string {
return (new StringPad)
->setInput($input)
->setPadLength($padLength)
->setPadString($padString)
->setPadType($padType)
->setEncoding($encoding ?: mb_internal_encoding())
->pad();
}
public static function isSupportedEncoding (string $encoding) : bool {
foreach (mb_list_encodings() as $supportedEncoding) {
if ($encoding === $supportedEncoding) {
return true;
}
}
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);
}
/**
* Encode a string using a variant of the MIME base64 compatible with URLs.
*
* The '+' and '/' characters used in base64 are replaced by '-' and '_'.
* The '=' padding is removed.
*
* @param string $string The string to encode
* @return string The encoded string
*/
public static function encodeInBase64 (string $string) : string {
return str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode($string)
);
}
/**
* Decode a string encoded with StringUtilities::encodeInBase64
*
* @param string $string The string to decode
* @return string The decoded string
*/
public static function decodeFromBase64 (string $string) : string {
$toDecode = str_replace(['-', '_'], ['+', '/'], $string);
return base64_decode($toDecode);
}
}
diff --git a/tests/Collections/ArrayUtilitiesTest.php b/tests/Collections/ArrayUtilitiesTest.php
new file mode 100644
index 0000000..737ea38
--- /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
index 0000000..e33178f
--- /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
index 9b587f1..0000000
--- 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
index d3c1073..e2056fa 100644
--- a/tests/Collections/TraversableUtilitiesTest.php
+++ b/tests/Collections/TraversableUtilitiesTest.php
@@ -1,55 +1,116 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Tests\Collections;
use Keruald\OmniTools\Collections\TraversableUtilities;
+
use PHPUnit\Framework\TestCase;
use Countable;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Traversable;
class TraversableUtilitiesTest extends TestCase {
/**
* @dataProvider provideCountables
*/
public function testCount ($expectedCount, $countable) {
$this->assertEquals(
$expectedCount, TraversableUtilities::count($countable)
);
}
/**
* @dataProvider provideNotCountables
*/
public function testCountWithNotCountables ($notCountable) {
$this->expectException("TypeError");
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
///
public function provideCountables () : iterable {
yield [0, null];
yield [0, false];
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;
+ }
+ }
];
}
public function provideNotCountables () : iterable {
yield [true];
yield [new \stdClass];
yield [0];
yield [""];
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
index 0000000..f3bc7ce
--- /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/x-diff
Expires
Mon, Nov 25, 14:54 (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260430
Default Alt Text
(50 KB)

Event Timeline