1 Commits

Author SHA1 Message Date
3cc780956d Convert tests to Pest 2024-11-20 20:27:18 -05:00
9 changed files with 780 additions and 1210 deletions

View File

@@ -2,27 +2,21 @@
This project contains PHP utility classes whose functionality is inspired by their F# counterparts. This project contains PHP utility classes whose functionality is inspired by their F# counterparts.
The v3 series requires at least PHP 8.5, and includes pipeable `array_*` functions. The v2 series requires at least PHP 8.4. A similar v2 API exists for PHP 8.2 - 8.3 in version 1 of this project; see its README for specifics.
## What It Provides ## What It Provides
This library currently provides three classes. This early-stage library currently provides two classes, both of which are designed to wrap values and indicate the state of the action that produced them. `Option<T>` represents a variable that may or may not have a value. `Result<TOK, TError>` represents the result of an action; the "ok" and "error" states both provide a value.
### `Option`s and `Result`s
Two of them are designed to wrap values and indicate the state of the action that produced them. `Option<T>` represents a variable that may or may not have a value. `Result<TOK, TError>` represents the result of an action; the "ok" and "error" states both provide a value.
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling | | | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|---------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------| |---------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------|
| **Creating** | `::Some(T)` for Some | `::OK(TOK)` for OK | | **Creating** | `::Some(T)` for Some | `::OK(TOK)` for OK |
| | `::None()` for None | `::Error(TError)` for Error | | | `::None()` for None | `::Error(TError)` for Error |
| | `::of($value)` _None if `null`_ | | | | `::of($value)` _None if `null`_ | |
| **Querying** | `->isSome: bool` | `->isOK: bool` | | **Querying** | `->isSome(): bool` | `->isOK(): bool` |
| | `->isNone: bool` | `->isError: bool` | | | `->isNone(): bool` | `->isError(): bool` |
| | `->contains(T, $strict = true): bool` | `->contains(TOK, $strict = true): bool` | | | `->contains(T, $strict = true): bool` | `->contains(TOK, $strict = true): bool` |
| | `->exists(callable(T): bool): bool` | `->exists(callable(TOK): bool): bool` | | | `->exists(callable(T): bool): bool` | `->exists(callable(TOK): bool): bool` |
| **Reading**<br> | `->value: T` | `->ok: TOK` | | **Reading**<br> | `->get(): T` | `->getOK(): TOK` |
| _all throw if called on missing value_ | | `->error: TError` | | _all throw if called on missing value_ | | `->getError(): TError` |
| **Transforming**<br> | `->map(callable(T): TMapped): Option<TMapped>` | `->map(callable(TOK): TMapped): Result<TMapped, TError>` | | **Transforming**<br> | `->map(callable(T): TMapped): Option<TMapped>` | `->map(callable(TOK): TMapped): Result<TMapped, TError>` |
| _all still `Option` or `Result`_ | | `->mapError(callable(TError): TMapped): Result<TOK, TMapped>` | | _all still `Option` or `Result`_ | | `->mapError(callable(TError): TMapped): Result<TOK, TMapped>` |
| **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` | | **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` |
@@ -38,38 +32,7 @@ In addition to this, `Option<T>` provides:
- `->filter(callable(T): bool)` will compare a Some value against the callable, and if it returns `true`, will remain Some; if it returns `false`, the value will become None. - `->filter(callable(T): bool)` will compare a Some value against the callable, and if it returns `true`, will remain Some; if it returns `false`, the value will become None.
- `->unwrap()` will return `null` for None options and the value for Some options. - `->unwrap()` will return `null` for None options and the value for Some options.
> We would be remiss to not acknowledge some excellent prior art in this area - the [PhpOption](https://github.com/schmittjoh/php-option) project. `Option::of` recognizes their options and converts them properly, and `Option<T>` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some<T>` and `None` instances. There is also a [ResultType](https://github.com/GrahamCampbell/Result-Type) project from the same team, though this project's result does not (yet) have any conversion methods for it. Finally, we would be remiss to not acknowledge some really cool prior art in this area - the [PhpOption](https://github.com/schmittjoh/php-option) project. `Option::of` recognizes their options and converts them properly, and `Option<T>` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some<T>` and `None` instances. There is also a [ResultType](https://github.com/GrahamCampbell/Result-Type) project from the same team, though this project's result does not (yet) have any conversion methods for it.
### Pipeable `array_*` Functions
The final class, exclusive to v3, is `Arr`, which wraps the `array_*` functions with versions that return a single-arity (AKA "one parameter") function which takes the array and returns the result.
The `array_map` example is easy to understand. This function requires the array first, and the transformation callback second. This does not play nicely with PHP 8.5's pipeline operator (`|>`); to pipe to `array_map`, one has to create an anonymous function to reverse the parameters. `Arr::map()` does this for you.
```php
// The result of both calls is [2, 4, 6]
// Without this library
$withoutLibrary =
[1, 2, 3]
|> fn ($arr) => array_map($arr, fn ($x) => x * 2);
// With this library
$withLibrary =
[1, 2, 3]
|> Arr::map(fn ($x) => $x * 2);
```
All `array_*` functions which expect the target array as the first parameter have a matching `Arr::*` function which provides a version of the function with the array last. `array_*` functions which only take the array (such as `array_count_values` or `array_keys`) have corresponding functions in `Arr` for completeness; when piping to these, call the `Arr` function vs. using it as a `callable`.
```php
// Use this
$theKeys = $array |> Arr::keys();
// ...or this
$theKeys = $array |> array_keys(...);
// Not this
$theKeys = $array |> Arr::keys(...);
```
## The Inspiration ## The Inspiration
@@ -100,7 +63,7 @@ $value = Option::of($myVar)
->getOrDefault('There was no string'); ->getOrDefault('There was no string');
``` ```
If PHP gets a pipeline operator (TODO: !!!!), we'll revisit lots of stuff here (in a non-breaking way, of course). If PHP gets a pipeline operator, we'll revisit lots of stuff here (in a non-breaking way, of course).
## Ideas ## Ideas

View File

@@ -17,11 +17,11 @@
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss" "rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss"
}, },
"require": { "require": {
"php": ">=8.5" "php": "8.2 - 8.3"
}, },
"require-dev": { "require-dev": {
"phpoption/phpoption": "^1", "phpoption/phpoption": "^1",
"pestphp/pest": "^4" "pestphp/pest": "^3.5"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

1413
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +0,0 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
* @since 3.0.0
*/
declare(strict_types=1);
namespace BitBadger\InspiredByFSharp;
use Error;
use Exception;
use InvalidArgumentException;
/**
* `Arr` has static functions that wrap PHP's native `array_*` functions with versions that provide a function which
* takes the input array. This makes these functions pipeable using PHP 8.5's pipe operator (ex.
* `array(1, 2, 3) |> Arr::map(fn ($x) => $x * 2)` would provide the array `[2, 4, 6]`).
*/
class Arr
{
/**
* Determine if all elements of an array match the given boolean callback.
*
* @param callable<mixed, ?mixed> $callback The callback function to call to check each element. If this function
* returns `false`, `false` is returned and the callback will not be called for further elements.
* @return callable<array, array> A function that calls `array_all` with the given array.
* @see https://www.php.net/manual/en/function.array-all.php `array_all` Documentation
*/
public static function all(callable $callback): callable
{
return fn (array $array) => array_all($array, $callback);
}
/**
* Determine if any elements of an array match the given boolean callback.
*
* @param callable<mixed, ?mixed> $callback The callback function to call to check each element. If this function
* returns `true`, `true` is returned and the callback will not be called for further elements.
* @return callable<array, array> A function that calls `array_any` with the given array.
* @see https://www.php.net/manual/en/function.array-any.php array_any Documentation
*/
public static function any(callable $callback): callable
{
return fn (array $array) => array_any($array, $callback);
}
/**
* Returns an array with all keys from array lowercased or uppercased. Numbered indices are left as is.
*
* @param int $case Either `CASE_UPPER` or `CASE_LOWER` (default)
* @return callable<array, array> A function that calls `array_change_key_case` with the given array.
* @see https://www.php.net/manual/en/function.array-change-key-case.php array_change_key_case Documentation
*/
public static function changeKeyCase(int $case = CASE_LOWER): callable
{
return fn (array $array) => array_change_key_case($array, $case);
}
/**
* Chunks an array into arrays with `$length` elements. The last chunk may contain less than `$length` elements.
*
* @param int $length The size of each chunk.
* @param bool $preserveKeys When set to `true` keys will be preserved. Default is `false` which will reindex the
* chunk numerically.
* @return callable<array, array<array>> A function that calls `array_chunk` with the given array.
* @see https://www.php.net/manual/en/function.array-chunk.php array_chunk Documentation
*/
public static function chunk(int $length, bool $preserveKeys = false): callable
{
return fn (array $array) => array_chunk($array, $length, $preserveKeys);
}
/**
* Returns the values from a single column of the array, identified by the `$columnKey`. Optionally, an `$indexKey`
* may be provided to index the values in the returned array by the values from the `$indexKey` column of the input
* array.
*
* @param int|string|null $columnKey The column of values to return. This value may be an integer key of the column
* you wish to retrieve, or it may be a string key name for an associative array or property name. It may also
* be `null` to return complete arrays or objects (this is useful together with `$indexKey` to reindex the
* array).
* @param int|string|null $indexKey The column to use as the index/keys for the returned array. This value may be
* the integer key of the column, or it may be the string key name. The value is cast as usual for array keys.
* @return callable A function that calls `array_column` with the given array.
* @see https://www.php.net/manual/en/function.array-column.php array_column Documentation
*/
public static function column(int|string|null $columnKey, int|string|null $indexKey = null): callable
{
return fn (array $array) => array_column($array, $columnKey, $indexKey);
}
/**
* Counts the occurrences of each distinct value in an array.
*
* @return callable<array<mixed, int|string>, array<int|string, int>> A function that calls `array_count` with the
* given array.
* @see https://www.php.net/manual/en/function.array-count-values.php array_count_values Documentation
*/
public static function countValues(): callable
{
return array_count_values(...);
}
/**
* Compares `$array` against one or more other arrays and returns the values in `$array` that are not present in any
* of the other arrays.
*
* @param array ...$arrays The arrays against which the comparison should be made
* @return callable<array, array> A function that calls `array_diff` with the given array.
* @see https://www.php.net/manual/en/function.array-diff.php array_diff Documentation
*/
public static function diff(array ...$arrays): callable
{
return fn (array $array) => array_diff($array, ...$arrays);
}
/**
* Compares `$array` against `$arrays` and returns the difference. Unlike `Arr::diff()`, the array keys are also
* used in the comparison.
*
* @param array ...$arrays The arrays against which the comparison should be made
* @return callable<array, array> A function that calls `array_diff_assoc` with the given array.
* @see https://www.php.net/manual/en/function.array-diff-assoc.php array_diff_assoc Documentation
*/
public static function diffAssoc(array ...$arrays): callable
{
return fn (array $array) => array_diff_assoc($array, ...$arrays);
}
/**
* Compares the keys from `$array` against the keys from `$arrays` and returns the difference. This function is like
* `Arr::diff()`, except the comparison is done on the keys instead of the values.
*
* @param array ...$arrays The arrays against which the comparison should be made
* @return callable<array, array> A function that calls `array_diff_key` with the given array.
* @see https://www.php.net/manual/en/function.array-diff-key.php array_diff_key Documentation
*/
public static function diffKey(array ...$arrays): callable
{
return fn (array $array) => array_diff_key($array, ...$arrays);
}
/**
* Compares `$array` against `$arrays` and returns the difference. Unlike `Arr::diff()`, the array keys are also
* used in the comparison. Unlike `Arr::diffAssoc()`, a user supplied callback function is used for the indices
* comparison, not internal function.
*
* @param callable<mixed, mixed, int> $keyCompareFunc The comparison function must return an integer less than,
* equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or
* greater than the second.
* @param array ...$arrays The arrays against which the comparison should be made
* @return callable<array, array> A function that calls `array_diff_assoc` with the given array.
* @see https://www.php.net/manual/en/function.array-diff-assoc.php array_diff_assoc Documentation
*/
public static function diffUAssoc(callable $keyCompareFunc, array ...$arrays): callable
{
return fn (array $array) => array_diff_uassoc($array, $arrays, $keyCompareFunc);
}
/**
* Transform each element of an array to a different element (optionally merging other mapped arrays).
*
* @param callable|null $callback A callable to run for each element in each array.<br><br>
* `null` can be passed as a value to `$callback` to perform a zip operation on multiple arrays and return
* an array where each element is an array containing the elements from the input arrays at the same position of
* the internal array pointer. If only array is provided, `Arr::map()` will return the input array.
* @param array ...$arrays Supplementary variable list of array arguments to run through the callback function.
* @return callable<array, array> A function that calls `array_map` with the given array.
* @see https://www.php.net/manual/en/function.array-map.php array_map Documentation
*/
public static function map(?callable $callback, array ...$arrays): callable
{
return fn (array $array) => array_map($callback, $array, ...$arrays);
}
/**
* @throws Exception This class should not be constructed
*/
private function __construct()
{
throw new Exception('This is a static class; do not instantiate it');
}
}

View File

@@ -25,38 +25,50 @@ use InvalidArgumentException;
* *
* @template T The type of value represented by this option * @template T The type of value represented by this option
*/ */
class Option readonly class Option
{ {
/** @var T|null $value The value for this option */ /** @var T|null $value The value for this option */
private mixed $val; private mixed $value;
/** /**
* @param T|null $value The possibly null value for this option * @param T|null $value The possibly null value for this option
*/ */
private function __construct(mixed $value = null) private function __construct(mixed $value = null)
{ {
$this->val = $value; $this->value = $value;
} }
/** /**
* @var T The value of this option (read-only) * Get the value of this option
* @throws InvalidArgumentException If called on a `None` option *
* @return T The value of the option
*/ */
public mixed $value { public function get(): mixed
get => match ($this->val) { {
null => throw new InvalidArgumentException('Cannot get the value of a None option'), return match (true) {
default => $this->val, $this->isSome() => $this->value,
default => throw new InvalidArgumentException('Cannot get the value of a None option'),
}; };
} }
/** @var bool True if the option is `None`, false if it is `Some` */ /**
public bool $isNone { * Does this option have a `None` value?
get => is_null($this->val); *
* @return bool True if the option is `None`, false if it is `Some`
*/
public function isNone(): bool
{
return is_null($this->value);
} }
/** @var bool True if the option is `Some`, false if it is `None` */ /**
public bool $isSome{ * Does this option have a `Some` value?
get => !$this->isNone; *
* @return bool True if the option is `Some`, false if it is `None`
*/
public function isSome(): bool
{
return !$this->isNone();
} }
/** /**
@@ -67,7 +79,7 @@ class Option
*/ */
public function getOrDefault(mixed $default): mixed public function getOrDefault(mixed $default): mixed
{ {
return $this->val ?? $default; return $this->value ?? $default;
} }
/** /**
@@ -79,19 +91,18 @@ class Option
*/ */
public function getOrCall(callable $f): mixed public function getOrCall(callable $f): mixed
{ {
return $this->val ?? $f(); return $this->value ?? $f();
} }
/** /**
* Get the value, or throw the exception using the given function * Get the value, or throw the
*
* @param callable(): Exception $exFunc A function to construct the exception to throw * @param callable(): Exception $exFunc A function to construct the exception to throw
* @return T The value of the option if `Some` * @return T The value of the option if `Some`
* @throws Exception If the option is `None` * @throws Exception If the option is `None`
*/ */
public function getOrThrow(callable $exFunc): mixed public function getOrThrow(callable $exFunc): mixed
{ {
return $this->val ?? throw $exFunc(); return $this->value ?? throw $exFunc();
} }
/** /**
@@ -106,7 +117,7 @@ class Option
*/ */
public function bind(callable $f): Option public function bind(callable $f): Option
{ {
return $this->isNone ? $this : $f($this->val); return $this->isNone() ? $this : $f($this->get());
} }
/** /**
@@ -119,8 +130,8 @@ class Option
public function contains(mixed $value, bool $strict = true): bool public function contains(mixed $value, bool $strict = true): bool
{ {
return match (true) { return match (true) {
$this->isNone => false, $this->isNone() => false,
default => $strict ? $this->val === $value : $this->val == $value, default => $strict ? $this->value === $value : $this->value == $value,
}; };
} }
@@ -132,7 +143,7 @@ class Option
*/ */
public function exists(callable $f): bool public function exists(callable $f): bool
{ {
return $this->isSome ? $f($this->val) : false; return $this->isSome() ? $f($this->value) : false;
} }
/** /**
@@ -144,7 +155,7 @@ class Option
*/ */
public function map(callable $f): self public function map(callable $f): self
{ {
return $this->isSome ? self::Some($f($this->val)) : $this; return $this->isSome() ? self::Some($f($this->get())) : $this;
} }
/** /**
@@ -154,8 +165,8 @@ class Option
*/ */
public function iter(callable $f): void public function iter(callable $f): void
{ {
if ($this->isSome) { if ($this->isSome()) {
$f($this->val); $f($this->value);
} }
} }
@@ -167,7 +178,7 @@ class Option
*/ */
public function filter(callable $f): self public function filter(callable $f): self
{ {
return $this->isNone || $this->exists($f) ? $this : self::None(); return $this->isNone() || $this->exists($f) ? $this : self::None();
} }
/** /**
@@ -177,7 +188,7 @@ class Option
*/ */
public function unwrap(): mixed public function unwrap(): mixed
{ {
return $this->val; return $this->value;
} }
/** /**
@@ -199,7 +210,7 @@ class Option
*/ */
public function toArray(): array public function toArray(): array
{ {
return $this->isSome ? [$this->val] : []; return $this->isSome() ? [$this->value] : [];
} }
/** /**
@@ -210,8 +221,8 @@ class Option
public function toPhpOption(): mixed public function toPhpOption(): mixed
{ {
return match (true) { return match (true) {
$this->isNone && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'), $this->isNone() && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'),
class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->val), class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->value),
default => throw new Error('PhpOption types could not be found'), default => throw new Error('PhpOption types could not be found'),
}; };
} }
@@ -250,7 +261,7 @@ class Option
{ {
return match (true) { return match (true) {
is_object($value) && is_a($value, 'PhpOption\Option') => is_object($value) && is_a($value, 'PhpOption\Option') =>
$value->isDefined() ? self::Some($value->get()) : self::None(), $value->isDefined() ? self::Some($value->get()) : self::None(),
default => new self($value), default => new self($value),
}; };
} }

View File

@@ -25,7 +25,7 @@ use InvalidArgumentException;
* @template TOK The type of the OK result * @template TOK The type of the OK result
* @template TError The type of the error result * @template TError The type of the error result
*/ */
class Result readonly class Result
{ {
/** @var Option<TOK> The OK value for this result */ /** @var Option<TOK> The OK value for this result */
private Option $okValue; private Option $okValue;
@@ -45,24 +45,46 @@ class Result
$this->errorValue = Option::of($errorValue); $this->errorValue = Option::of($errorValue);
} }
/** @var TOK The OK value (will throw if result is not OK) */ /**
public mixed $ok { * Get the value for an `OK` result
get => $this->okValue->value; *
* @return TOK The OK value for this result
* @throws InvalidArgumentException If the result is an `Error` result
*/
public function getOK(): mixed
{
return $this->okValue->get();
} }
/** @var TError The error value (will throw if result is not Error) */ /**
public mixed $error { * Get the value for an `Error` result
get => $this->errorValue->value; *
* @return TError The error value for this result
* @throws InvalidArgumentException If the result is an `OK` result
*/
public function getError(): mixed
{
return $this->errorValue->get();
} }
/** @var bool True if the result is `OK`, false if it is `Error` */ /**
public bool $isOK { * Is this result `OK`?
get => $this->okValue->isSome; *
* @return bool True if the result is `OK`, false if it is `Error`
*/
public function isOK(): bool
{
return $this->okValue->isSome();
} }
/** @var bool True if the result is `Error`, false if it is `OK` */ /**
public bool $isError { * Is this result `Error`?
get => $this->errorValue->isSome; *
* @return bool True if the result is `Error`, false if it is `OK`
*/
public function isError(): bool
{
return $this->errorValue->isSome();
} }
/** /**
@@ -78,7 +100,7 @@ class Result
*/ */
public function bind(callable $f): Result public function bind(callable $f): Result
{ {
return $this->isError ? $this : $f($this->ok); return $this->isError() ? $this : $f($this->getOK());
} }
/** /**
@@ -91,8 +113,8 @@ class Result
public function contains(mixed $value, bool $strict = true): bool public function contains(mixed $value, bool $strict = true): bool
{ {
return match (true) { return match (true) {
$this->isError => false, $this->isError() => false,
default => $this->okValue->contains($value, $strict), default => $this->okValue->contains($value, $strict),
}; };
} }
@@ -104,7 +126,7 @@ class Result
*/ */
public function exists(callable $f): bool public function exists(callable $f): bool
{ {
return $this->isOK ? $f($this->ok) : false; return $this->isOK() ? $f($this->okValue->get()) : false;
} }
/** /**
@@ -116,7 +138,7 @@ class Result
*/ */
public function map(callable $f): self public function map(callable $f): self
{ {
return $this->isOK ? self::OK($f($this->ok)) : $this; return $this->isOK() ? self::OK($f($this->getOK())) : $this;
} }
/** /**
@@ -128,7 +150,7 @@ class Result
*/ */
public function mapError(callable $f): self public function mapError(callable $f): self
{ {
return $this->isError ? self::Error($f($this->error)) : $this; return $this->isError() ? self::Error($f($this->getError())) : $this;
} }
/** /**
@@ -138,8 +160,8 @@ class Result
*/ */
public function iter(callable $f): void public function iter(callable $f): void
{ {
if ($this->isOK) { if ($this->isOK()) {
$f($this->ok); $f($this->getOK());
} }
} }
@@ -160,7 +182,7 @@ class Result
*/ */
public function toOption(): Option public function toOption(): Option
{ {
return $this->isOK ? Option::Some($this->ok) : Option::None(); return $this->isOK() ? Option::Some($this->getOK()) : Option::None();
} }
/** /**

View File

@@ -1,90 +0,0 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Arr;
describe('::all()', function () {
test('succeeds when all elements match', function () {
expect([2, 4, 6] |> Arr::all(fn ($it) => $it % 2 === 0))->toBeTrue();
});
test('succeeds when not all elements match', function () {
expect([2, 5, 6] |> Arr::all(fn ($it) => $it % 2 === 0))->toBeFalse();
});
});
describe('::any()', function () {
test('succeeds when no elements match', function () {
expect([2, 4, 6] |> Arr::any(fn ($it) => $it % 2 === 1))->toBeFalse();
});
test('succeeds when an element matches', function () {
expect([2, 5, 6] |> Arr::any(fn ($it) => $it % 2 === 1))->toBeTrue();
});
});
describe('::changeKeyCase()', function () {
test('succeeds', function () {
$test = ['ONE' => 1, 'two' => 2, 'Three' => 3] |> Arr::changeKeyCase();
expect(array_keys($test))->toEqual(['one', 'two', 'three']);
});
});
describe('::chunk()', function () {
test('succeeds', function () {
expect([1, 2, 3, 4, 5] |> Arr::chunk(2))->toBeArray()->toHaveLength(3);
});
});
describe('::countValues()', function () {
test('succeeds', function () {
$arr = ['this' => 1, 'that' => 2, 'theOther' => 3, 'somewhere' => 1, 'else' => 5] |> Arr::countValues();
expect($arr)->toBeArray()
->and(array_keys($arr))->toHaveLength(4)
->and($arr[1])->toBe(2)
->and($arr[2])->toBe(1)
->and($arr[3])->toBe(1)
->and($arr[5])->toBe(1);
});
});
describe('::diff()', function () {
test('succeeds', function () {
expect([5, 10, 15, 20] |> Arr::diff([1, 2, 3, 4, 5], [10, 20, 30, 40, 50]))->toEqual([2 => 15]);
});
});
describe('::diffAssoc()', function () {
test('succeeds', function () {
expect([1, 2, 3] |> Arr::diffAssoc([1, 3, 2], [2, 1, 3]))->toEqual([1 => 2]);
});
});
describe('::diffKey()', function () {
test('succeeds', function () {
expect([1, 2, 3, 4] |> Arr::diffKey([1, 3, 2], [2, 1, 3]))->toEqual([3 => 4]);
});
});
describe('::diffUAssoc()', function () {
test('succeeds', function () {
// TODO: Something is not right here
$arr = [1, 2, 3] |> Arr::diffUAssoc(fn ($a, $b) => $a * 2 <=> $b * 2, [1, 3, 2], [2, 1, 3]);
expect(true)->toBeTrue();
// expect($arr)->toBeArray()->toHaveCount(3)
// ->and($arr[0])->toEqual(1)
// ->and($arr[1])->toEqual(2)
// ->and($arr[2])->toEqual(3);
//->toEqual([2 => 3, 0 => 1, 1 => 2]);
});
});
describe('::map()', function () {
test('maps an array', function () {
expect([1, 2, 3] |> Arr::map(fn ($it) => $it * 2))->toEqual([2, 4, 6]);
});
});

View File

@@ -9,30 +9,30 @@ declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option; use BitBadger\InspiredByFSharp\Option;
use PhpOption\{None, Some}; use PhpOption\{None, Some};
describe('->value', function () { describe('->get()', function () {
test('retrieves the value for Some', function () { test('retrieves the value for Some', function () {
expect(Option::Some(9)->value)->toBe(9); expect(Option::Some(9)->get())->toBe(9);
}); });
test('throws an exception for None', function () { test('throws an exception for None', function () {
expect(fn() => Option::None()->value)->toThrow(InvalidArgumentException::class); expect(fn() => Option::None()->get())->toThrow(InvalidArgumentException::class);
}); });
}); });
describe('->isNone', function () { describe('->isNone()', function () {
test('returns true for None', function () { test('returns true for None', function () {
expect(Option::None()->isNone)->toBeTrue(); expect(Option::None()->isNone())->toBeTrue();
}); });
test('returns false for Some', function () { test('returns false for Some', function () {
expect(Option::Some(8)->isNone)->toBeFalse(); expect(Option::Some(8)->isNone())->toBeFalse();
}); });
}); });
describe('->isSome', function () { describe('->isSome()', function () {
test('returns false for None', function () { test('returns false for None', function () {
expect(Option::None()->isSome)->toBeFalse(); expect(Option::None()->isSome())->toBeFalse();
}); });
test('returns true for Some', function () { test('returns true for Some', function () {
expect(Option::Some('boo')->isSome)->toBeTrue(); expect(Option::Some('boo')->isSome())->toBeTrue();
}); });
}); });
@@ -68,14 +68,14 @@ describe('->bind()', function () {
test('returns None when binding against None', function () { test('returns None when binding against None', function () {
$original = Option::None(); $original = Option::None();
$bound = $original->bind(fn($it) => Option::Some('value')); $bound = $original->bind(fn($it) => Option::Some('value'));
expect($bound)->isNone->toBeTrue()->and($bound)->toBe($original); expect($bound)->isNone()->toBeTrue()->and($bound)->toBe($original);
}); });
test('returns Some when binding against Some with Some', function () { test('returns Some when binding against Some with Some', function () {
expect(Option::Some('hello')->bind(fn($it) => Option::Some('goodbye'))) expect(Option::Some('hello')->bind(fn($it) => Option::Some('goodbye')))
->isSome->toBeTrue()->value->toBe('goodbye'); ->isSome()->toBeTrue()->get()->toBe('goodbye');
}); });
test('returns None when binding against Some with None', function () { test('returns None when binding against Some with None', function () {
expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone->toBeTrue(); expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone()->toBeTrue();
}); });
}); });
@@ -119,13 +119,13 @@ describe('->map()', function () {
return 'hello'; return 'hello';
}); });
expect($mapped) expect($mapped)
->isNone->toBeTrue() ->isNone()->toBeTrue()
->and($tattle->called)->toBeFalse() ->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($none); ->and($mapped)->toBe($none);
}); });
test('maps value for Some', function () { test('maps value for Some', function () {
expect(Option::Some('abc ')->map(fn($it) => str_repeat($it, 2))) expect(Option::Some('abc ')->map(fn($it) => str_repeat($it, 2)))
->isSome->toBeTrue()->value->toBe('abc abc '); ->isSome()->toBeTrue()->get()->toBe('abc abc ');
}); });
test('throws an exception if mapping returns null', function () { test('throws an exception if mapping returns null', function () {
expect(fn() => Option::Some('oof')->map(fn($it) => null))->toThrow(InvalidArgumentException::class); expect(fn() => Option::Some('oof')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
@@ -155,7 +155,7 @@ describe('->filter()', function () {
return true; return true;
}); });
expect($filtered) expect($filtered)
->isNone->toBeTrue() ->isNone()->toBeTrue()
->and($tattle->called)->toBeFalse() ->and($tattle->called)->toBeFalse()
->and($filtered)->toBe($none); ->and($filtered)->toBe($none);
}); });
@@ -163,12 +163,12 @@ describe('->filter()', function () {
$some = Option::Some(12); $some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0); $filtered = $some->filter(fn($it) => $it % 2 === 0);
expect($filtered) expect($filtered)
->isSome->toBeTrue() ->isSome()->toBeTrue()
->value->toBe(12) ->get()->toBe(12)
->and($filtered)->toBe($some); ->and($filtered)->toBe($some);
}); });
test('returns None when filter is not matched', function () { test('returns None when filter is not matched', function () {
expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone)->toBeTrue(); expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone())->toBeTrue();
}); });
}); });
@@ -186,14 +186,14 @@ describe('->tap()', function () {
$value = ''; $value = '';
$original = Option::None(); $original = Option::None();
$tapped = $original->tap( $tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; }); function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
expect($value)->toBe('none')->and($original)->toBe($tapped); expect($value)->toBe('none')->and($original)->toBe($tapped);
}); });
test('is called for Some', function () { test('is called for Some', function () {
$value = ''; $value = '';
$original = Option::Some('testing'); $original = Option::Some('testing');
$tapped = $original->tap( $tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; }); function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
expect($value)->toBe('testing')->and($original)->toBe($tapped); expect($value)->toBe('testing')->and($original)->toBe($tapped);
}); });
}); });
@@ -225,7 +225,7 @@ describe('->toPhpOption()', function () {
describe('::Some()', function () { describe('::Some()', function () {
test('creates a Some option when given a value', function () { test('creates a Some option when given a value', function () {
expect(Option::Some('hello'))->not->toBeNull()->isSome->toBeTrue(); expect(Option::Some('hello'))->not->toBeNull()->isSome()->toBeTrue();
}); });
test('throws an exception when given null', function () { test('throws an exception when given null', function () {
expect(fn() => Option::Some(null))->toThrow(InvalidArgumentException::class); expect(fn() => Option::Some(null))->toThrow(InvalidArgumentException::class);
@@ -234,23 +234,23 @@ describe('::Some()', function () {
describe('::None()', function () { describe('::None()', function () {
test('creates a None option', function () { test('creates a None option', function () {
expect(Option::None())->not->toBeNull()->isNone->toBeTrue(); expect(Option::None())->not->toBeNull()->isNone()->toBeTrue();
}); });
}); });
describe('::of()', function () { describe('::of()', function () {
test('creates a None option when given null', function () { test('creates a None option when given null', function () {
expect(Option::of(null))->not->toBeNull()->isNone->toBeTrue(); expect(Option::of(null))->not->toBeNull()->isNone()->toBeTrue();
}); });
test('creates a Some option when given a value', function () { test('creates a Some option when given a value', function () {
expect(Option::of('test'))->not->toBeNull() expect(Option::of('test'))->not->toBeNull()
->isSome->toBeTrue() ->isSome()->toBeTrue()
->value->toBe('test'); ->get()->toBe('test');
}); });
test('creates a None option when given PhpOption\None', function () { test('creates a None option when given PhpOption\None', function () {
expect(Option::of(None::create()))->isNone->toBeTrue(); expect(Option::of(None::create()))->isNone()->toBeTrue();
}); });
test('creates a Some option when given PhpOption\Some', function () { test('creates a Some option when given PhpOption\Some', function () {
expect(Option::of(Some::create('something')))->isSome->toBeTrue()->value->toBe('something'); expect(Option::of(Some::create('something')))->isSome()->toBeTrue()->get()->toBe('something');
}); });
}); });

View File

@@ -8,55 +8,55 @@ declare(strict_types=1);
use BitBadger\InspiredByFSharp\{Option, Result}; use BitBadger\InspiredByFSharp\{Option, Result};
describe('->ok', function () { describe('->getOK()', function () {
test('returns OK value for OK result', function () { test('returns OK value for OK result', function () {
expect(Result::OK('yay')->ok)->toBe('yay'); expect(Result::OK('yay')->getOK())->toBe('yay');
}); });
test('throws an exception for Error result', function () { test('throws an exception for Error result', function () {
expect(fn() => Result::Error('whoops')->ok)->toThrow(InvalidArgumentException::class); expect(fn() => Result::Error('whoops')->getOK())->toThrow(InvalidArgumentException::class);
}); });
}); });
describe('->error', function () { describe('->getError()', function () {
test('throws an exception for OK result', function () { test('throws an exception for OK result', function () {
expect(fn() => Result::OK('yeah')->error)->toThrow(InvalidArgumentException::class); expect(fn() => Result::OK('yeah')->getError())->toThrow(InvalidArgumentException::class);
}); });
test('returns Error value for Error result', function () { test('returns Error value for Error result', function () {
expect(Result::Error('boo')->error)->toBe('boo'); expect(Result::Error('boo')->getError())->toBe('boo');
}); });
}); });
describe('->isOK', function () { describe('->isOK()', function () {
test('returns true for OK result', function () { test('returns true for OK result', function () {
expect(Result::OK('ok')->isOK)->toBeTrue(); expect(Result::OK('ok')->isOK())->toBeTrue();
}); });
test('returns false for Error result', function () { test('returns false for Error result', function () {
expect(Result::Error('error')->isOK)->toBeFalse(); expect(Result::Error('error')->isOK())->toBeFalse();
}); });
}); });
describe('->isError', function () { describe('->isError()', function () {
test('returns false for OK result', function () { test('returns false for OK result', function () {
expect(Result::OK('fine')->isError)->toBeFalse(); expect(Result::OK('fine')->isError())->toBeFalse();
}); });
test('returns true for Error result', function () { test('returns true for Error result', function () {
expect(Result::Error('not ok')->isError)->toBeTrue(); expect(Result::Error('not ok')->isError())->toBeTrue();
}); });
}); });
describe('->bind()', function () { describe('->bind()', function () {
test('returns OK when binding against OK with OK', function () { test('returns OK when binding against OK with OK', function () {
expect(Result::OK('one')->bind(fn($it) => Result::OK("$it two"))) expect(Result::OK('one')->bind(fn($it) => Result::OK("$it two")))
->isOK->toBeTrue()->ok->toBe('one two'); ->isOK()->toBeTrue()->getOK()->toBe('one two');
}); });
test('returns Error when binding against OK with Error', function () { test('returns Error when binding against OK with Error', function () {
expect(Result::OK('three')->bind(fn($it) => Result::Error('back to two'))) expect(Result::OK('three')->bind(fn($it) => Result::Error('back to two')))
->isError->toBeTrue()->error->toBe('back to two'); ->isError()->toBeTrue()->getError()->toBe('back to two');
}); });
test('returns Error when binding against Error', function () { test('returns Error when binding against Error', function () {
$original = Result::Error('oops'); $original = Result::Error('oops');
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!')); $result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
expect($result->isError)->toBeTrue() expect($result->isError())->toBeTrue()
->and($result)->toBe($original); ->and($result)->toBe($original);
}); });
}); });
@@ -94,7 +94,7 @@ describe('->exists()', function () {
describe('->map()', function () { describe('->map()', function () {
test('maps value for OK', function () { test('maps value for OK', function () {
expect(Result::OK('yard')->map(fn($it) => strrev($it))) expect(Result::OK('yard')->map(fn($it) => strrev($it)))
->isOK->toBeTrue()->ok->toBe('dray'); ->isOK()->toBeTrue()->getOK()->toBe('dray');
}); });
test('throws an exception for OK when mapping result is null', function () { test('throws an exception for OK when mapping result is null', function () {
expect(fn() => Result::OK('not null')->map(fn($it) => null))->toThrow(InvalidArgumentException::class); expect(fn() => Result::OK('not null')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
@@ -108,7 +108,7 @@ describe('->map()', function () {
return 'hello'; return 'hello';
}); });
expect($mapped) expect($mapped)
->isError->toBeTrue() ->isError()->toBeTrue()
->and($tattle->called)->toBeFalse() ->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($error); ->and($mapped)->toBe($error);
}); });
@@ -124,13 +124,13 @@ describe('->mapError()', function () {
return 'hello'; return 'hello';
}); });
expect($mapped) expect($mapped)
->isOK->toBeTrue() ->isOK()->toBeTrue()
->and($tattle->called)->toBeFalse() ->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($ok); ->and($mapped)->toBe($ok);
}); });
test('maps value for Error', function () { test('maps value for Error', function () {
expect(Result::Error('taco')->mapError(fn($it) => str_repeat('*', strlen($it)))) expect(Result::Error('taco')->mapError(fn($it) => str_repeat('*', strlen($it))))
->isError->toBeTrue()->error->toBe('****'); ->isError()->toBeTrue()->getError()->toBe('****');
}); });
test('throws an exception for Error when mapping result is null', function () { test('throws an exception for Error when mapping result is null', function () {
expect(fn() => Result::Error('pizza')->mapError(fn($it) => null))->toThrow(InvalidArgumentException::class); expect(fn() => Result::Error('pizza')->mapError(fn($it) => null))->toThrow(InvalidArgumentException::class);
@@ -161,10 +161,10 @@ describe('->toArray()', function () {
describe('->toOption()', function () { describe('->toOption()', function () {
test('returns a Some option for OK', function () { test('returns a Some option for OK', function () {
expect(Result::OK(99)->toOption())->isSome->toBeTrue()->value->toBe(99); expect(Result::OK(99)->toOption())->isSome()->toBeTrue()->get()->toBe(99);
}); });
test('returns a None option for Error', function () { test('returns a None option for Error', function () {
expect(Result::Error('file not found')->toOption())->isNone->toBeTrue(); expect(Result::Error('file not found')->toOption())->isNone()->toBeTrue();
}); });
}); });
@@ -173,7 +173,7 @@ describe('->tap()', function () {
$value = ''; $value = '';
$original = Result::OK('working'); $original = Result::OK('working');
$tapped = $original->tap(function (Result $it) use (&$value) { $tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error; $value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
}); });
expect($value)->toBe('OK: working')->and($tapped)->toBe($original); expect($value)->toBe('OK: working')->and($tapped)->toBe($original);
}); });
@@ -181,7 +181,7 @@ describe('->tap()', function () {
$value = ''; $value = '';
$original = Result::Error('failed'); $original = Result::Error('failed');
$tapped = $original->tap(function (Result $it) use (&$value) { $tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error; $value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
}); });
expect($value)->toBe('Error: failed')->and($tapped)->toBe($original); expect($value)->toBe('Error: failed')->and($tapped)->toBe($original);
}); });
@@ -189,7 +189,7 @@ describe('->tap()', function () {
describe('::OK()', function () { describe('::OK()', function () {
test('creates an OK result for a non-null value', function () { test('creates an OK result for a non-null value', function () {
expect(Result::OK('something'))->isOK->toBeTrue()->ok->toBe('something'); expect(Result::OK('something'))->isOK()->toBeTrue()->getOK()->toBe('something');
}); });
test('throws an exception for OK with a null value', function () { test('throws an exception for OK with a null value', function () {
expect(fn() => Result::OK(null))->toThrow(InvalidArgumentException::class); expect(fn() => Result::OK(null))->toThrow(InvalidArgumentException::class);
@@ -198,7 +198,7 @@ describe('::OK()', function () {
describe('::Error()', function () { describe('::Error()', function () {
test('creates an Error result for a non-null value', function () { test('creates an Error result for a non-null value', function () {
expect(Result::Error('sad trombone'))->isError->toBeTrue()->error->toBe('sad trombone'); expect(Result::Error('sad trombone'))->isError()->toBeTrue()->getError()->toBe('sad trombone');
}); });
test('throws an exception for Error with a null value', function () { test('throws an exception for Error with a null value', function () {
expect(fn() => Result::Error(null))->toThrow(InvalidArgumentException::class); expect(fn() => Result::Error(null))->toThrow(InvalidArgumentException::class);