Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3769489
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
50 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 25, 14:54 (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260430
Default Alt Text
(50 KB)
Attached To
Mode
rKERUALD Keruald libraries development repository
Attached
Detach File
Event Timeline
Log In to Comment