diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 02ebacc7..e43ab9a0 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -16,6 +16,10 @@ [ '@PSR12' => true, 'linebreak_after_opening_tag' => true, + 'trailing_comma_in_multiline' => [ + 'after_heredoc' => false, + 'elements' => [], + ], 'binary_operator_spaces' => [ 'operators' => [ '=>' => 'align', diff --git a/changes.md b/changes.md index f63fc5ba..877b21a9 100644 --- a/changes.md +++ b/changes.md @@ -1,5 +1,15 @@ # bingo-functional changes +## v2.4.0 + +- Added `jsonDecode` and `extract` functions +- Reworked internals of `partition`, `slugify`, and `partitionBy` functions +- Infused `startsWith`, `endsWith`, `contains`, and `truncate` functions with multi-byte string processing capabilities +- Added iteration modes to `filter` and `reject` functions +- Added [ext-eio](https://pecl.php.net/eio)-powered non-blocking file operations for IO monad +- Replaced pattern matching parser in `functional-php/pattern-matching` with custom parser +- Replaced cons matching function calls in `cmatch` with custom parser + ## v2.3.0 - Added `keys`, `values`, and `size` functions diff --git a/composer.json b/composer.json index db0797e7..ec5ab706 100644 --- a/composer.json +++ b/composer.json @@ -68,8 +68,7 @@ } ], "require": { - "php": "^7 || ^8", - "functional-php/pattern-matching": "^1" + "php": "^7 || ^8" }, "require-dev": { "ergebnis/composer-normalize": "^2", @@ -79,6 +78,7 @@ }, "suggest": { "ext-apcu": "In-memory key-value PHP userland store", + "ext-eio": "An interface to the libeio library", "ext-mbstring": "PHP extension for accurately determining byte-length of strings", "ext-readline": "An interface to the GNU readline library", "chemem/bingo-functional-repl": "A simple REPL for the bingo-functional library" @@ -111,7 +111,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.x-dev" } }, "scripts": { diff --git a/src/Functional/Contains.php b/src/Functional/Contains.php index 69d5e7b8..7a98548d 100644 --- a/src/Functional/Contains.php +++ b/src/Functional/Contains.php @@ -28,5 +28,12 @@ */ function contains(string $haystack, string $needle): bool { - return !equals(\strpos($haystack, $needle), false); + return !equals( + ( + \extension_loaded('mbstring') ? + '\mb_stripos' : + '\stripos' + )($haystack, $needle), + false + ); } diff --git a/src/Functional/EndsWith.php b/src/Functional/EndsWith.php index 0b678e5d..e1fc04df 100644 --- a/src/Functional/EndsWith.php +++ b/src/Functional/EndsWith.php @@ -28,13 +28,21 @@ */ function endsWith(string $haystack, string $needle): bool { - $strlen = \function_exists('mb_strlen') ? - \mb_strlen($needle, 'utf-8') : - \strlen($needle); + $lmbstr = \extension_loaded('mbstring'); + $strlen = ( + $lmbstr ? + '\mb_strlen' : + '\strlen' + )($needle); - if (equals($strlen, 0)) { - return false; - } - - return equals(\substr($haystack, -$strlen), $needle); + return $strlen > 0 ? + equals( + ( + $lmbstr ? + '\mb_substr' : + '\substr' + )($haystack, -$strlen), + $needle + ) : + false; } diff --git a/src/Functional/Filter.php b/src/Functional/Filter.php index 1ea2c589..d3a6dc4f 100644 --- a/src/Functional/Filter.php +++ b/src/Functional/Filter.php @@ -16,21 +16,30 @@ * filter * selects list values that conform to a boolean predicate * - * filter :: (a -> Bool) -> [a] -> [a] + * filter :: (a -> Bool) -> [a] -> Int -> [a] * * @param callable $func * @param array|object $list + * @param int $mode * @return array|object * @example * * filter(fn ($x) => $x % 2 === 0, range(4, 8)) * => [4, 6, 8] */ -function filter(callable $func, $list) +function filter(callable $func, $list, int $mode = 0) { return fold( - function ($acc, $val, $idx) use ($func) { - if (!$func($val)) { + function ($acc, $val, $idx) use ($func, $mode) { + $filter = equals($mode, ARRAY_FILTER_USE_KEY) ? + $func($idx) : + ( + equals($mode, ARRAY_FILTER_USE_BOTH) ? + $func($val, $idx) : + $func($val) + ); + + if (!$filter) { if (\is_object($acc)) { unset($acc->{$idx}); } elseif (\is_array($acc)) { diff --git a/src/Functional/JsonDecode.php b/src/Functional/JsonDecode.php new file mode 100644 index 00000000..05d8af3e --- /dev/null +++ b/src/Functional/JsonDecode.php @@ -0,0 +1,57 @@ +{$key} = jsonDecode($value, ...$options); + } + } else { + if (\is_iterable($acc)) { + $acc[$key] = $decoder( + \sprintf('%s', $value), + ...$options + ) ?? $value; + } elseif (\is_object($acc)) { + $acc->{$key} = $decoder( + \sprintf('%s', $value), + ...$options + ) ?? $value; + } + } + + return $acc; + }, + $initial, + \is_iterable($initial) ? [] : new \stdClass() + ); +} diff --git a/src/Functional/Partition.php b/src/Functional/Partition.php index 87ac8b1c..64470ce7 100644 --- a/src/Functional/Partition.php +++ b/src/Functional/Partition.php @@ -32,17 +32,10 @@ */ function partition(int $number, $list) { - $list = _props($list); - $count = size($list); + $list = _props($list); - if ($number < 2 || $count < 2) { - return [$list]; - } - - $psize = \ceil($count / $number); - - return extend( - [\array_slice($list, 0, $psize, true)], - partition($number - 1, dropLeft($list, $psize)) + return partitionBy( + (int) \ceil(size($list) / $number), + $list ); } diff --git a/src/Functional/PartitionBy.php b/src/Functional/PartitionBy.php index 4d81da53..5461f216 100644 --- a/src/Functional/PartitionBy.php +++ b/src/Functional/PartitionBy.php @@ -32,13 +32,27 @@ */ function partitionBy(int $partitionSize, $list) { - $list = _props($list); - - if (equals($partitionSize, 0)) { - return $list; - } - - $number = (int) \ceil(size($list) / $partitionSize); - - return partition($number, $list); + $list = _props($list); + $counter = 0; + $idx = 0; + + return fold( + function (iterable $acc, $value, $key) use ( + &$counter, + &$idx, + $partitionSize + ) { + $idx++; + + $acc[$counter][$key] = $value; + + if ($idx > 0 && ($idx % $partitionSize === 0)) { + $counter++; + } + + return $acc; + }, + $list, + [] + ); } diff --git a/src/Functional/Pluck.php b/src/Functional/Pluck.php index 6dbe5278..d2208977 100644 --- a/src/Functional/Pluck.php +++ b/src/Functional/Pluck.php @@ -28,9 +28,11 @@ */ function pluck($values, $search, $default = null) { + $search = \sprintf('%s', $search); + return fold( function ($acc, $val, $idx) use ($search) { - if ($search == $idx) { + if (equals($search, \sprintf('%s', $idx))) { $acc = $val; } diff --git a/src/Functional/Reject.php b/src/Functional/Reject.php index 3900f195..d6e46994 100644 --- a/src/Functional/Reject.php +++ b/src/Functional/Reject.php @@ -16,21 +16,30 @@ * reject * selects list values that do not conform to a boolean predicate * - * reject :: (a -> Bool) -> [a] -> [a] + * reject :: (a -> Bool) -> [a] -> Int -> [a] * * @param callable $func * @param array|object $list + * @param int $mode * @return array|object * @example * * reject(fn ($x) => $x % 2 === 0, range(4, 8)) * => [5, 7] */ -function reject(callable $func, $list) +function reject(callable $func, $list, int $mode = 0) { return fold( - function ($acc, $val, $idx) use ($func) { - if ($func($val)) { + function ($acc, $val, $idx) use ($func, $mode) { + $filter = equals($mode, ARRAY_FILTER_USE_KEY) ? + $func($idx) : + ( + equals($mode, ARRAY_FILTER_USE_BOTH) ? + $func($val, $idx) : + $func($val) + ); + + if ($filter) { if (\is_object($acc)) { unset($acc->{$idx}); } elseif (\is_array($acc)) { diff --git a/src/Functional/Slugify.php b/src/Functional/Slugify.php index de392e57..891158d6 100644 --- a/src/Functional/Slugify.php +++ b/src/Functional/Slugify.php @@ -27,12 +27,9 @@ */ function slugify(string $string): string { - $slugify = compose( - partial('explode', ' '), - function (array $words) { - return concat('-', ...$words); - } + return preg_replace( + '/(\s){1,}/ix', + '-', + $string ); - - return $slugify($string); } diff --git a/src/Functional/StartsWith.php b/src/Functional/StartsWith.php index 9a4954cb..8c613ef5 100644 --- a/src/Functional/StartsWith.php +++ b/src/Functional/StartsWith.php @@ -28,13 +28,25 @@ */ function startsWith(string $haystack, string $needle): bool { - $strlen = \function_exists('mb_strlen') ? - \mb_strlen($needle, 'utf-8') : - \strlen($needle); + $lmbstr = \extension_loaded('mbstring'); + $strlen = ( + $lmbstr ? + '\mb_strlen' : + '\strlen' + )($needle); - if (equals($strlen, 0)) { - return false; - } - - return equals(\substr($haystack, 0, $strlen), $needle); + return $strlen > 0 ? + equals( + ( + $lmbstr ? + '\mb_substr' : + '\substr' + )( + $haystack, + 0, + $strlen + ), + $needle + ) : + false; } diff --git a/src/Functional/Truncate.php b/src/Functional/Truncate.php index 450aec6a..a20717ac 100644 --- a/src/Functional/Truncate.php +++ b/src/Functional/Truncate.php @@ -29,15 +29,28 @@ */ function truncate(string $string, int $limit): string { - $strlen = 0; - $strlen += !\function_exists('mb_strlen') ? - \strlen($string) : - \mb_strlen($string, 'utf-8'); - + $lmbstr = \extension_loaded('mbstring'); $truncate = compose( - partialRight('substr', $limit, 0), - partialRight(partial(concat, '..'), '.') + partialRight( + ( + $lmbstr ? + '\mb_substr' : + '\substr' + ), + $limit, + 0 + ), + partial('\sprintf', '%s...') ); - return $limit > $strlen ? $string : $truncate($string); + return ( + $limit > + ( + $lmbstr ? + '\mb_strlen' : + '\strlen' + )($string) + ) ? + $string : + $truncate($string); } diff --git a/src/Functional/index.php b/src/Functional/index.php index 857856ed..7018798f 100644 --- a/src/Functional/index.php +++ b/src/Functional/index.php @@ -43,6 +43,7 @@ require_once __DIR__ . '/Intersects.php'; require_once __DIR__ . '/Intersperse.php'; require_once __DIR__ . '/IsArrayOf.php'; +require_once __DIR__ . '/JsonDecode.php'; require_once __DIR__ . '/K.php'; require_once __DIR__ . '/Keys.php'; require_once __DIR__ . '/Last.php'; diff --git a/src/Functors/Monads/IO/AppendFile.php b/src/Functors/Monads/IO/AppendFile.php index 370af7b8..de8cb182 100644 --- a/src/Functors/Monads/IO/AppendFile.php +++ b/src/Functors/Monads/IO/AppendFile.php @@ -11,8 +11,11 @@ namespace Chemem\Bingo\Functional\Functors\Monads\IO; +require_once __DIR__ . '/Internal/_Eio.php'; + use Chemem\Bingo\Functional\Functors\Monads\Monad; +use function Chemem\Bingo\Functional\Functors\Monads\IO\Internal\_eio; use function Chemem\Bingo\Functional\toException; const appendFile = __NAMESPACE__ . '\\appendFile'; @@ -32,7 +35,20 @@ function appendFile(string $file, string $contents): Monad return IO( toException( function () use ($contents, $file) { - return \file_put_contents($file, $contents, FILE_APPEND); + if (\extension_loaded('eio')) { + return _eio() + ->write($file, $contents, true) + ->exec(); + } + + $rbytes = @\file_put_contents($file, $contents, FILE_APPEND); + $error = \error_get_last(); + + if (!\is_null($error)) { + throw new \Exception($error['message']); + } + + return $rbytes; }, function (\Throwable $err) { return IOException($err->getMessage())->exec(); diff --git a/src/Functors/Monads/IO/Internal/_Eio.php b/src/Functors/Monads/IO/Internal/_Eio.php new file mode 100644 index 00000000..b0272297 --- /dev/null +++ b/src/Functors/Monads/IO/Internal/_Eio.php @@ -0,0 +1,168 @@ + IO + * + * @param string $file + * @return Monad + */ + public function read(string $file): Monad + { + \eio_open( + $file, + EIO_O_RDONLY | EIO_O_NONBLOCK, + EIO_S_IRUSR, + EIO_PRI_DEFAULT, + function (...$args) { + [$file, $fd, $req] = $args; + + if ($fd > 0) { + \eio_stat( + $file, + EIO_PRI_DEFAULT, + function (...$args) { + [$fd, $result] = $args; + + if (\key_exists('size', $result)) { + ['size' => $size] = $result; + + // make subsequent file reads more performant + \eio_readahead( + $fd, + 0, + $size, + EIO_PRI_DEFAULT, + function (...$args) use ($fd, $size) { + \eio_read( + $fd, + $size, + 0, + EIO_PRI_DEFAULT, + function (...$args) use ($fd) { + [, $data] = $args; + $this->fcontents = IO\IO($data); + + \eio_close($fd); + } + ); + } + ); + } + }, + $fd + ); + } else { + $this->fcontents = IO\IOException( + \eio_get_last_error($req) + ); + } + }, + $file + ); + \eio_event_loop(); + + return $this->fcontents; + } + + /** + * write + * performs the action of either writing or appending arbitrary data to a specified file in a non-blocking fashion + * + * write :: String -> String -> Bool -> IO + * + * @param string $file + * @param string $data + * @param bool $append + * @return Monad + */ + public function write( + string $file, + string $data, + bool $append = false + ): Monad { + \eio_open( + $file, + ( + $append ? + ( + EIO_O_NONBLOCK | + EIO_O_WRONLY | + EIO_O_APPEND | + EIO_O_CREAT + ) : + ( + EIO_O_NONBLOCK | + EIO_O_WRONLY | + EIO_O_CREAT + ) + ), + EIO_S_IRUSR | EIO_S_IWUSR, + EIO_PRI_DEFAULT, + function (...$args) { + [$params, $fd, $req] = $args; + [$contents, $append] = $params; + + if ($fd > 0) { + \eio_write( + $fd, + $contents, + ( + \extension_loaded('mbstring') ? + 'mb_strlen' : + 'strlen' + )($contents), + 0, + EIO_PRI_DEFAULT, + function (...$args) use ($fd) { + [, $result] = $args; + + $this->fcontents = IO\IO($result); + + \eio_close($fd); + } + ); + } else { + $this->fcontents = IO\IOException( + \eio_get_last_error($req) + ); + } + }, + [$data, $append] + ); + \eio_event_loop(); + + return $this->fcontents; + } + }; +} diff --git a/src/Functors/Monads/IO/ReadFile.php b/src/Functors/Monads/IO/ReadFile.php index 74be3928..d4ab16e2 100644 --- a/src/Functors/Monads/IO/ReadFile.php +++ b/src/Functors/Monads/IO/ReadFile.php @@ -11,8 +11,11 @@ namespace Chemem\Bingo\Functional\Functors\Monads\IO; +require_once __DIR__ . '/Internal/_Eio.php'; + use Chemem\Bingo\Functional\Functors\Monads\Monad; +use function Chemem\Bingo\Functional\Functors\Monads\IO\Internal\_eio; use function Chemem\Bingo\Functional\toException; const readFile = __NAMESPACE__ . '\\readFile'; @@ -31,7 +34,21 @@ function readFile(string $file): Monad return IO( toException( function () use ($file) { - return \file_get_contents($file); + // use ext-eio bindings + if (\extension_loaded('eio')) { + return _eio() + ->read($file) + ->exec(); + } + + $contents = @\file_get_contents($file); + $error = \error_get_last(); + + if (!\is_null($error)) { + throw new \Exception($error['message']); + } + + return $contents; }, function ($err) { return IOException($err->getMessage())->exec(); diff --git a/src/Functors/Monads/IO/WriteFile.php b/src/Functors/Monads/IO/WriteFile.php index f4eb221f..33032f7f 100644 --- a/src/Functors/Monads/IO/WriteFile.php +++ b/src/Functors/Monads/IO/WriteFile.php @@ -11,8 +11,11 @@ namespace Chemem\Bingo\Functional\Functors\Monads\IO; +require_once __DIR__ . '/Internal/_Eio.php'; + use Chemem\Bingo\Functional\Functors\Monads\Monad; +use function Chemem\Bingo\Functional\Functors\Monads\IO\Internal\_eio; use function Chemem\Bingo\Functional\toException; const writeFile = __NAMESPACE__ . '\\writeFile'; @@ -32,7 +35,20 @@ function writeFile(string $file, string $contents): Monad return IO( toException( function () use ($contents, $file) { - return \file_put_contents($file, $contents); + if (\extension_loaded('eio')) { + return _eio() + ->write($file, $contents) + ->exec(); + } + + $rbytes = @\file_put_contents($file, $contents); + $error = \error_get_last(); + + if (!\is_null($error)) { + throw new \Exception($error['message']); + } + + return $rbytes; }, function (\Throwable $err) { return IOException($err->getMessage())->exec(); diff --git a/src/PatternMatching/Cmatch.php b/src/PatternMatching/Cmatch.php index f3d7b348..180c7646 100644 --- a/src/PatternMatching/Cmatch.php +++ b/src/PatternMatching/Cmatch.php @@ -1,7 +1,7 @@ 12 */ -function cmatch(array $patterns): callable +function cmatch(iterable $patterns): callable { - // extract cons pattern counts - $cons = _filterMatch($patterns); - - return function (array $values) use ($patterns, $cons) { - // return false on invalid cons data - // perform match otherwise - return empty($cons) ? - false : - _execConsMatch($patterns, $cons, $values); + return function (iterable $values) use ($patterns) { + $size = size($values); + + foreach ($patterns as $pattern => $action) { + if (\preg_match('/^(\(){1}(.*)(\:\_)?(\)){1}$/ix', $pattern, $matches)) { + $ptt = isset($matches[2]) ? $matches[2] : []; + + if (!empty($ptt)) { + $next = _tokenize($ptt, PM_RULE_CONS); + $final = $next['tokens'][$next['token_count'] - 1]; + + // non-strict cons match + if (equals($final[0], PM_WILDCARD)) { + if (equals($size, ($next['token_count'] - 1))) { + return $action(...$values); + } + } elseif (equals($size, $next['token_count'])) { + return $action(...$values); + } + } + } + } + + if (!isset($patterns['_'])) { + throw new \Exception('Could not find match for provided input'); + } + + return $patterns['_'](); }; } diff --git a/src/PatternMatching/Extract.php b/src/PatternMatching/Extract.php new file mode 100644 index 00000000..8d6f4525 --- /dev/null +++ b/src/PatternMatching/Extract.php @@ -0,0 +1,145 @@ + a -> Array + * + * @param string $pattern + * @param mixed $values + * @return iterable + * + * extract( + * '["foo", _, (x:xs:_)]', + * [ + * 'foo', + * null, + * \range(1, 3), + * ], + * ); + * => ['x' => 1, 'xs' => [2, 3]] + */ +function extract(string $pattern, $values): iterable +{ + $size = null; + $tokens = _type($pattern); + $xargs = []; + + if ($iterable = \is_iterable($values)) { + $size = size($values); + } + + if (equals($tokens['type'], PM_RULE_CONS) && $iterable) { + $limit = $tokens['token_count'] - 1; + $final = $tokens['tokens'][$limit]; + $idx = -1; + + if (equals($final[0], PM_WILDCARD)) { + while ($idx++ < ($limit - 1)) { + $xargs[$tokens['tokens'][$idx][1]] = $values[$idx]; + } + } else { + while ($idx++ < $size) { + if ($idx < $limit) { + $xargs[$tokens['tokens'][$idx][1]] = $values[$idx]; + } else { + $xargs[$tokens['tokens'][$idx][1]] = dropLeft($values, $limit); + + return $xargs; + } + } + } + + return $xargs; + } + + if (equals($tokens['type'], PM_RULE_ARRAY) && $iterable) { + if ($size > 0) { + $valid = 0; + $elements = $tokens['tokens']; + $idx = -1; + + while ($idx++ < $tokens['token_count'] - 1) { + $bloc = $elements[$idx]; + + if (equals($bloc[0], PM_IDENTIFIER) && isset($values[$idx])) { + $valid++; + + $xargs[$bloc[1]] = $values[$idx]; + } + + if (equals($bloc[1], $values[$idx] ?? null)) { + $valid++; + } + + if (equals($bloc[0], PM_WILDCARD) && ($values[$idx] ?? true)) { + $valid++; + } + + if (equals($bloc[0], PM_CONS) && \is_iterable($values[$idx] ?? null)) { + $pttn = ''; + $consc = size($bloc[1]); + $inputc = size($values[$idx]); + + foreach ($bloc[1] as $count => $item) { + if ($count > 0 && ($count < ($consc - 1))) { + $pttn .= \sprintf('%s:', $item[1]); + } elseif (equals($count, 0)) { + $pttn .= \sprintf('(%s:', $item[1]); + } else { + $pttn .= \sprintf('%s)', $item[1]); + } + } + + if (!empty($pttn)) { + $xargs = \array_merge($xargs, extract($pttn, $values[$idx])); + } + + if ( + equals($bloc[1][$consc - 1][0], PM_WILDCARD) || + equals($inputc, $consc) + ) { + $valid++; + } + } + } + + if (equals($valid, $size) && equals($valid, $tokens['token_count'])) { + return $xargs; + } + } + } + + if (equals($tokens['type'], PM_RULE_IDENTIFIER)) { + $xargs[$tokens['tokens'][0][1]] = $values; + } + + return $xargs; +} diff --git a/src/PatternMatching/Internal/_BaseFilter.php b/src/PatternMatching/Internal/_BaseFilter.php deleted file mode 100644 index 81ab769b..00000000 --- a/src/PatternMatching/Internal/_BaseFilter.php +++ /dev/null @@ -1,44 +0,0 @@ - b)] -> ([(a -> b)] -> [c]) -> [c] - * - * @internal - * @param array $patterns - * @param callable $transformer - * @return mixed - */ -function _baseFilter( - $default, - array $patterns, - callable $success, - callable ...$filters -) { - return _filterMaybeN( - $default, - $patterns, - $success, - ...extend( - [ - partial('key_exists', '_'), - partialRight(every, 'is_callable'), - ], - $filters - ) - ); -} diff --git a/src/PatternMatching/Internal/_ConsEachCount.php b/src/PatternMatching/Internal/_ConsEachCount.php deleted file mode 100644 index eb7d4e83..00000000 --- a/src/PatternMatching/Internal/_ConsEachCount.php +++ /dev/null @@ -1,39 +0,0 @@ - [Int] - * - * @internal - * @param array $cons - * @return array - * @example - * - * _consEachCount(['(x:xs:_)', '(x:_)']) - * => ['(x:xs:_)' => 2, '(x:_)' => 1] - */ -function _consEachCount(array $cons): array -{ - return compose( - // get cons count for each cons pattern - partial(map, _consPatternCount), - // recursively merge each pattern - function (array $cons) { - return extend(...$cons); - } - )($cons); -} diff --git a/src/PatternMatching/Internal/_ConsPatternCount.php b/src/PatternMatching/Internal/_ConsPatternCount.php deleted file mode 100644 index e39d6c2e..00000000 --- a/src/PatternMatching/Internal/_ConsPatternCount.php +++ /dev/null @@ -1,51 +0,0 @@ - [Int] - * - * @internal - * @param string $pattern - * @return array - * @example - * - * _consPatternCount('(x:xs:_)') - * => ['(x:xs:_)' => 2] - */ -function _consPatternCount(string $pattern): array -{ - return [ - $pattern => compose( - // check if underscore is present in pattern; propagate _ if absent - function ($pttn) { - return \preg_match('/([_])+/', $pttn) ? $pttn : '_'; - }, - // remove brackets from pattern - partial('preg_replace', '/([(\)])+/', ''), - // explode the pattern by colon (:) - partial('explode', ':'), - // take out non-underscore patterns - partial( - filter, - function ($pttn) { - return !equals($pttn, '_'); - } - ), - // count - size - )($pattern), - ]; -} diff --git a/src/PatternMatching/Internal/_ExecConsMatch.php b/src/PatternMatching/Internal/_ExecConsMatch.php deleted file mode 100644 index 3dbdffd7..00000000 --- a/src/PatternMatching/Internal/_ExecConsMatch.php +++ /dev/null @@ -1,61 +0,0 @@ - b)] -> [Int] -> [a] -> b - * - * @internal - * @param array $patterns - * @param array $cons - * @param array $values - * @return mixed - * @example - * - * _execConsMatch( - * [ - * '(x:_)' => fn ($x) => $x + 10, - * '_' => fn () => 0 - * ], - * ['(x:_)' => 1, '_' => 0], - * [3] - * ) - * => 13 - */ -function _execConsMatch( - array $patterns, - array $cons, - array $values -) { - return compose( - // get pattern whose cons count matches value count - partial( - filter, - function ($count) use ($values) { - return equals($count, size($values)); - } - ), - // extract first element from list - 'array_keys', - // extract key that matches pattern - function ($keys) { - return empty($keys) ? '_' : head($keys); - }, - // extract executable function that matches apt pattern in pattern list - partial(pluck, $patterns) - )($cons)(...$values); -} diff --git a/src/PatternMatching/Internal/_ExecTypeMatch.php b/src/PatternMatching/Internal/_ExecTypeMatch.php deleted file mode 100644 index 56c52a19..00000000 --- a/src/PatternMatching/Internal/_ExecTypeMatch.php +++ /dev/null @@ -1,43 +0,0 @@ - b)] -> a -> (a -> c -> Bool) -> b - * - * @internal - * @param array $patterns - * @param mixed $input - * @param callable $comparator - * @return mixed - */ -function _execTypeMatch( - array $patterns, - $input, - callable $comparator -) { - // memoize pattern list key extraction - $pluck = partial(pluck, $patterns); - - return compose( - 'array_keys', - // strip down the pattern & apply comparator to check for exactness - partial(filter, partial($comparator, $input)), - head, - // extract function and call it; call wildcard case otherwise - partialRight($pluck, $pluck('_')) - )($patterns)(); -} diff --git a/src/PatternMatching/Internal/_FilterMatch.php b/src/PatternMatching/Internal/_FilterMatch.php deleted file mode 100644 index cf2d5c71..00000000 --- a/src/PatternMatching/Internal/_FilterMatch.php +++ /dev/null @@ -1,33 +0,0 @@ - [Int] - * - * @internal - * @param array $patterns - * @return array - * @example - * - * _filterMatch() - * => - */ -function _filterMatch(array $patterns): array -{ - return _baseFilter( - [], - $patterns, - compose('array_keys', _consEachCount) - ); -} diff --git a/src/PatternMatching/Internal/_FilterMaybeN.php b/src/PatternMatching/Internal/_FilterMaybeN.php deleted file mode 100644 index d042a98c..00000000 --- a/src/PatternMatching/Internal/_FilterMaybeN.php +++ /dev/null @@ -1,44 +0,0 @@ - a -> (a -> b) -> [(a -> Bool)] -> b - * - * @internal - * @param mixed $default - * @param mixed $value - * @param callable $success - * @param callable ...$filters - * @return mixed - */ -function _filterMaybeN( - $default, - $value, - callable $success, - callable ...$filters -) { - return maybe( - $default, - $success, - // apply multiple filters to Maybe type - fold( - function (Monad $maybe, callable $filter) { - return $maybe->filter($filter); - }, - $filters, - Maybe::just($value) - ) - ); -} diff --git a/src/PatternMatching/Internal/_MatchObject.php b/src/PatternMatching/Internal/_MatchObject.php deleted file mode 100644 index 23c53f94..00000000 --- a/src/PatternMatching/Internal/_MatchObject.php +++ /dev/null @@ -1,20 +0,0 @@ - b)] -> a -> b - * - * @internal - * @param array $patterns - * @param mixed $input - * @return mixed - * @example - * - * _matchString([ - * '"12"' => fn () => 'exact', - * '_' => fn () => 'undef' - * ], 12) - * => 'exact' - */ -function _matchString(array $patterns, $input) -{ - return _execTypeMatch( - $patterns, - $input, - function ($input, $pattern) { - $compare = \preg_replace('/[\'\"]+/', '', $pattern); - - // handle comparisons between string, float, and integer types - return \is_string($input) ? - equals($input, $compare) : - ( - \is_int($input) ? - equals((int) $compare, $input) : - ( - \is_float($input) ? - equals((float) $compare, $input) : - false - ) - ); - } - ); -} diff --git a/src/PatternMatching/LetIn.php b/src/PatternMatching/LetIn.php index a266435a..6019c94b 100644 --- a/src/PatternMatching/LetIn.php +++ b/src/PatternMatching/LetIn.php @@ -11,7 +11,6 @@ namespace Chemem\Bingo\Functional\PatternMatching; use function Chemem\Bingo\Functional\fold; -use function FunctionalPHP\PatternMatching\extract; const letIn = __NAMESPACE__ . '\\letIn'; @@ -31,7 +30,6 @@ */ function letIn(string $pattern, array $items): callable { - // extract the tokens from the list $tokens = extract($pattern, $items); return function (array $keys, callable $func) use ($tokens) { diff --git a/src/PatternMatching/Parser/_Tag.php b/src/PatternMatching/Parser/_Tag.php new file mode 100644 index 00000000..7cdb553a --- /dev/null +++ b/src/PatternMatching/Parser/_Tag.php @@ -0,0 +1,61 @@ + Int -> Array + * + * @param string $input + * @param int &$token + * @return iterable + */ +function _tag(string $input, int &$token = 0): iterable +{ + $token++; + + if (\preg_match('/^(true)$/ix', $input)) { + return [PM_TRUE, true]; + } + + if (\preg_match('/^(false)$/ix', $input)) { + return [PM_FALSE, false]; + } + + if (\preg_match('/^(\-?\d+)$/', $input)) { + return [PM_INTEGER, (int) $input]; + } + + if (\preg_match('/^(\-?\d+(\.\d+)?)$/', $input)) { + return [PM_FLOAT, (float) $input]; + } + + if (\preg_match('/(\"(\\\\.|[^\"])*\")/ix', $input)) { + return [ + PM_STRING, + ( + \extension_loaded('mbstring') ? + '\mb_substr' : + '\substr' + )($input, 1, -1) + ]; + } + + if (\preg_match('/^(\_)$/ix', $input)) { + return [PM_WILDCARD, $input]; + } + + return [PM_IDENTIFIER, $input]; +} diff --git a/src/PatternMatching/Parser/_Tokenize.php b/src/PatternMatching/Parser/_Tokenize.php new file mode 100644 index 00000000..e4c8da5f --- /dev/null +++ b/src/PatternMatching/Parser/_Tokenize.php @@ -0,0 +1,82 @@ + Int -> Array + * + * @param string $input + * @param int $type + * @return iterable + */ +function _tokenize(string $input, int $type): iterable +{ + if ( + !\in_array( + $type, + [ + PM_RULE_CONS, + PM_RULE_WILDCARD, + PM_RULE_ARRAY, + PM_RULE_UNKNOWN, + PM_RULE_IDENTIFIER, + PM_RULE_STRING, + ] + ) + ) { + return []; + } + + $acc = []; + $tc = 0; + + if (equals($type, PM_RULE_ARRAY)) { + foreach (\preg_split('/(\s)?\,(\s)?/ix', $input) as $idx => $token) { + if (!empty($token)) { + $result = _tag($token, $tc); + + if (\preg_match('/^(\(){1}(.*)(\:\_)?(\)){1}$/ix', $result[1], $matches)) { + $patterns = isset($matches[2]) ? + \str_replace(['(', ')'], '', $matches[2]) : + []; + + $acc[$idx] = \array_merge( + $acc[$idx] ?? [], + [PM_CONS, _tokenize($patterns, PM_RULE_CONS)['tokens']] + ); + } else { + $acc[$idx] = $result; + } + } + } + } elseif (equals($type, PM_RULE_CONS)) { + foreach (\preg_split('/(\s)?\:(\s)?/ix', $input) as $token) { + if (!empty($token)) { + $acc[] = _tag($token, $tc); + } + } + } else { + $acc[] = _tag($input, $tc); + } + + return [ + 'tokens' => $acc, + 'token_count' => $tc, + 'type' => $type, + ]; +} diff --git a/src/PatternMatching/Parser/_Tokens.php b/src/PatternMatching/Parser/_Tokens.php new file mode 100644 index 00000000..624733a7 --- /dev/null +++ b/src/PatternMatching/Parser/_Tokens.php @@ -0,0 +1,81 @@ + + */ +const PM_TRUE = 1; + +/** + * @var int PM_FALSE Boolean false value + */ +const PM_FALSE = 2; + +/** + * @var int PM_INTEGER Integer value <(\-?\d+)> + */ +const PM_INTEGER = 3; + +/** + * @var int PM_FLOAT Floating point value <(\-?\d+(.\d+)?)> + */ +const PM_FLOAT = 4; + +/** + * @var int PM_STRING String value <\"(\\\\.|[^\"])*\"> + */ +const PM_STRING = 5; + +/** + * @var int PM_WILDCARD Boolean true value <(\_)> + */ +const PM_WILDCARD = 6; + +/** + * @var int PM_IDENTIFIER Placeholder value <.*> + */ +const PM_IDENTIFIER = 7; + +/** + * @var int PM_TRUE Boolean true value + */ +const PM_CONS = 8; + +/** + * @var int PM_RULE_ARRAY Array parser + */ +const PM_RULE_ARRAY = 100; + +/** + * @var int PM_RULE_CONS Cons parser + */ +const PM_RULE_CONS = 101; + +/** + * @var int PM_RULE_WILDCARD Wildcard value parser + */ +const PM_RULE_WILDCARD = 102; + +/** + * @var int PM_RULE_UNKNOWN Unknown value parser + */ +const PM_RULE_UNKNOWN = 103; + +/** + * @var int PM_RULE_IDENTIFIER Identifier parser + */ +const PM_RULE_IDENTIFIER = 104; + +/** + * @var int PM_RULE_STRING String value parser + */ +const PM_RULE_STRING = 105; diff --git a/src/PatternMatching/Parser/_Type.php b/src/PatternMatching/Parser/_Type.php new file mode 100644 index 00000000..0585c168 --- /dev/null +++ b/src/PatternMatching/Parser/_Type.php @@ -0,0 +1,47 @@ + Array + * + * @param string $input + * @return iterable + */ +function _type(string $input): iterable +{ + if (\preg_match('/^(\[){1}(.*)(\]){1}$/ix', $input, $matches)) { + $patterns = isset($matches[2]) ? $matches[2] : '_'; + + return _tokenize($patterns, PM_RULE_ARRAY); + } + + if (\preg_match('/^(\(){1}(.*)(\:\_)?(\)){1}$/ix', $input, $matches)) { + $patterns = isset($matches[2]) ? $matches[2] : '_'; + + return _tokenize($patterns, PM_RULE_CONS); + } + + if (\preg_match('/^(\_)$/ix', $input)) { + return _tokenize($input, PM_RULE_WILDCARD); + } + + if (\preg_match('/(\"(\\\\.|[^\"])*\")/ix', $input)) { + return _tokenize($input, PM_RULE_STRING); + } + + return _tokenize($input, PM_RULE_IDENTIFIER); +} diff --git a/src/PatternMatching/Parser/index.php b/src/PatternMatching/Parser/index.php new file mode 100644 index 00000000..6b712524 --- /dev/null +++ b/src/PatternMatching/Parser/index.php @@ -0,0 +1,14 @@ + 'FOO' */ -function patternMatch(array $patterns, $value) +function patternMatch(iterable $patterns, $input) { - // operationalize base pattern filter for subsequent matches - $match = partialRight( - partial(_baseFilter, false, $patterns), - // apply value filter to Maybe filter context - function ($_) use ($value) { - return !empty($value); + $input = \is_object($input) ? \get_class($input) : $input; + $size = null; + $tokens = map(_type, $keys = keys($patterns)); + + if ($iterable = \is_iterable($input)) { + $size = size($input); + } + + foreach ($tokens as $idx => $next) { + // perform cons match + if (equals($next['type'], PM_RULE_CONS) && $iterable) { + $final = $next['tokens'][$next['token_count'] - 1]; + + if (equals($final[0], PM_WILDCARD)) { + if (equals($size, ($next['token_count'] - 1))) { + return $patterns[($keys[$idx])](...$input); + } + } elseif (equals($size, $next['token_count'])) { + return $patterns[($keys[$idx])](...$input); + } } - ); - - return \is_array($value) ? - // use FunctionalPHP\PatternMatching primitives for array matches - pmatch($patterns, $value) : - ( - // perform object match on detection of object input - // perform string-based search otherwise - \is_object($value) ? - $match(partialRight(_matchObject, $value)) : - $match(partialRight(_matchString, $value)) - ); + + // perform array match + if (equals($next['type'], PM_RULE_ARRAY) && $iterable) { + if ($size > 0) { + $valid = 0; + $args = []; + $lexemes = $next['tokens']; + + foreach ($lexemes as $count => $bloc) { + if (equals($bloc[0], PM_IDENTIFIER) && isset($input[$count])) { + $valid++; + $args[] = $input[$count]; + } + + if (equals($bloc[1], $input[$count] ?? null)) { + $valid++; + } + + if (equals($bloc[0], PM_WILDCARD) && ($input[$count] ?? true)) { + $valid++; + } + + if (equals($bloc[0], PM_CONS) && \is_iterable($input[$count] ?? null)) { + $consc = size($bloc[1]); + $inputc = size($input[$count]); + $final = $bloc[1][$consc - 1]; + + if ( + (equals($final[0], PM_WILDCARD) && equals($inputc, ($consc - 1))) || + equals($inputc, $consc) + ) { + $valid++; + + $args[] = $input[$count]; + } + } + } + + if (equals($valid, $size) && equals($valid, $next['token_count'])) { + return $patterns[($keys[$idx])](...$args); + } + } + } + + // perform string match + if ( + (equals($next['type'], PM_RULE_STRING) && !$iterable) && + equals($input, $next['tokens'][0][1]) + ) { + return $patterns[($keys[$idx])](); + } + + // perform identifier/placeholder match + if (equals($next['type'], PM_RULE_IDENTIFIER) && !$iterable) { + return $patterns[($keys[$idx])]($input); + } + } + + if (!isset($patterns['_'])) { + throw new \Exception('Could not find match for provided input'); + } + + return $patterns['_'](); } diff --git a/src/PatternMatching/index.php b/src/PatternMatching/index.php index 40c01619..30ef7601 100644 --- a/src/PatternMatching/index.php +++ b/src/PatternMatching/index.php @@ -9,5 +9,6 @@ */ require_once __DIR__ . '/Cmatch.php'; +require_once __DIR__ . '/Extract.php'; require_once __DIR__ . '/LetIn.php'; require_once __DIR__ . '/PatternMatch.php'; diff --git a/tests/Functional/EqualsTest.php b/tests/Functional/EqualsTest.php index 5507cfe0..0cb1d2b3 100644 --- a/tests/Functional/EqualsTest.php +++ b/tests/Functional/EqualsTest.php @@ -95,7 +95,10 @@ function ($x, $y) { ], [ [ - \fopen(__DIR__ . '/../../io.test.txt', 'r'), + \fopen( + \sprintf('%s/../../README.md', __DIR__), + 'r' + ), \curl_init(), ], false @@ -149,5 +152,17 @@ public function testequalsPerformsArtifactComparisonsToAscertainEquivalence($arg $this->assertIsBool($comp); $this->assertEquals($result, $comp); + + foreach ($args as $arg) { + if (\is_resource($arg)) { + $type = \get_resource_type($arg); + + if (f\equals($type, 'stream')) { + \fclose($arg); + } elseif (f\equals($type, 'curl')) { + \curl_close($arg); + } + } + } } } diff --git a/tests/Functional/FilterTest.php b/tests/Functional/FilterTest.php index ae4e3cc0..5746cc1e 100644 --- a/tests/Functional/FilterTest.php +++ b/tests/Functional/FilterTest.php @@ -14,6 +14,7 @@ function ($val) { return $val % 2 == 0; }, \range(1, 5), + 0, [1 => 2, 3 => 4], ], [ @@ -25,17 +26,90 @@ function ($val) { ) > 3; }, (object) ['foo', 'bar', 'foo-bar'], + 0, (object) [2 => 'foo-bar'], ], + [ + function ($val, $key) { + return ( + ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3 + ) && f\equals($val % 2, 0); + }, + [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_BOTH, + ['fooz' => 4], + ], + [ + function ($val, $key) { + return ( + ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3 + ) && f\equals($val % 2, 0); + }, + (object) [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_BOTH, + (object) ['fooz' => 4], + ], + [ + function (string $key) { + return ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3; + }, + (object) [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_KEY, + (object) ['fooz' => 4], + ], + [ + function (string $key) { + return ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3; + }, + [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_KEY, + ['fooz' => 4], + ], ]; } /** * @dataProvider contextProvider */ - public function testfilterRemovesItemsThatDoNotConformToBooleanPredicate($func, $list, $res) + public function testfilterRemovesItemsThatDoNotConformToBooleanPredicate($func, $list, $mode, $res) { - $filter = f\filter($func, $list); + $filter = f\filter($func, $list, $mode); $this->assertEquals($res, $filter); } diff --git a/tests/Functional/JsonDecodeTest.php b/tests/Functional/JsonDecodeTest.php new file mode 100644 index 00000000..65bf8923 --- /dev/null +++ b/tests/Functional/JsonDecodeTest.php @@ -0,0 +1,76 @@ + \json_encode(['foo' => 'bar']), + 'baz' => \json_encode(\range(1, 5)), + 'bar' => 2, + ] + ), + true, + ], + [ + 'foo' => ['foo' => 'bar'], + 'baz' => \range(1, 5), + 'bar' => 2, + ], + ], + [ + [ + \json_encode( + [ + 'foo' => \json_encode(['foo' => 'bar']), + 'baz' => \json_encode(\range(1, 5)), + 'bar' => 2, + ] + ), + false, + 5000, + ], + (object) [ + 'foo' => (object) ['foo' => 'bar'], + 'baz' => \range(1, 5), + 'bar' => 2, + ], + ], + [ + [ + \json_encode( + [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ] + ), + true, + ], + [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ], + ], + ]; + } + + /** + * @dataProvider contextProvider + */ + public function testjsonDecodeRecursivelyDecodesJSON($args, $result) + { + $data = f\jsonDecode(...$args); + + $this->assertEquals($result, $data); + } +} diff --git a/tests/Functional/MapTest.php b/tests/Functional/MapTest.php index b801cd5d..9114b459 100644 --- a/tests/Functional/MapTest.php +++ b/tests/Functional/MapTest.php @@ -17,7 +17,7 @@ function ($val) { ['foo' => 4, 'bar' => 16], ], [ - f\partial(f\concat, '', 'foo-'), + f\partial('\sprintf', 'foo-%s'), (object) ['bar' => 'bar', 'baz'], (object) ['bar' => 'foo-bar', 'foo-baz'], ], diff --git a/tests/Functional/RejectTest.php b/tests/Functional/RejectTest.php index fbd7160e..fa9d4469 100644 --- a/tests/Functional/RejectTest.php +++ b/tests/Functional/RejectTest.php @@ -14,6 +14,7 @@ function ($val) { return $val % 2 == 0; }, \range(1, 5), + 0, [0 => 1, 2 => 3, 4 => 5], ], [ @@ -25,17 +26,106 @@ function ($val) { ) > 3; }, (object) ['foo', 'bar', 'foo-bar'], + 0, (object) [0 => 'foo', 1 => 'bar'], ], + [ + function ($val, $key) { + return ( + ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3 + ) && f\equals($val % 2, 0); + }, + [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_BOTH, + [ + 'foo' => 12, + 'bar' => 223, + 'baz' => 25, + ], + ], + [ + function ($val, $key) { + return ( + ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3 + ) && f\equals($val % 2, 0); + }, + (object) [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_BOTH, + (object) [ + 'foo' => 12, + 'bar' => 223, + 'baz' => 25, + ], + ], + [ + function (string $key) { + return ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3; + }, + (object) [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_KEY, + (object) [ + 'foo' => 12, + 'bar' => 223, + 'baz' => 25, + ], + ], + [ + function (string $key) { + return ( + \extension_loaded('mbstring') ? + '\mb_strlen' : + '\strlen' + )($key) > 3; + }, + [ + 'foo' => 12, + 'fooz' => 4, + 'bar' => 223, + 'baz' => 25, + ], + ARRAY_FILTER_USE_KEY, + [ + 'foo' => 12, + 'bar' => 223, + 'baz' => 25, + ], + ], ]; } /** * @dataProvider contextProvider */ - public function testRejectRemovesItemsThatConformToBooleanPredicate($func, $list, $res) + public function testRejectRemovesItemsThatConformToBooleanPredicate($func, $list, $mode, $res) { - $filter = f\reject($func, $list); + $filter = f\reject($func, $list, $mode); $this->assertEquals($res, $filter); } diff --git a/tests/Functors/Monads/IOTest.php b/tests/Functors/Monads/IOTest.php index 9d5671af..85f9ae1c 100644 --- a/tests/Functors/Monads/IOTest.php +++ b/tests/Functors/Monads/IOTest.php @@ -24,7 +24,7 @@ public static function setUpBeforeClass(): void public static function tearDownAfterClass(): void { - unlink(self::$file); + @unlink(self::$file); } /** @@ -35,7 +35,7 @@ public function IOMonadObeysFunctorLaws() $this ->forAll( Generator\map( - IO\readFile, + f\compose(IO\readFile, IO\catchIO), Generator\constant(self::$file) ) ) @@ -89,30 +89,42 @@ public function IOObeysMonadLaws() public function testreadFileSafelyReadsFileContents() { - $impure = IO\readFile(self::$file); + $impure = IO\catchIO( + IO\readFile(self::$file) + ); + $result = $impure->exec(); $this->assertInstanceOf(IO::class, $impure); // $this->assertIsString($impure->exec()); - $this->assertTrue($impure->map('is_string')->exec()); + $this->assertTrue( + \is_string($result) + ); } public function testappendFileSafelyAppendsDataToFile() { - $append = IO\appendFile(self::$file, 'fooz'); + $append = IO\catchIO( + IO\appendFile(self::$file, \sprintf("%s\n", 'fooz')) + ); + $rbytes = $append->exec(); $this->assertInstanceOf(IO::class, $append); $this->assertTrue( - \is_bool($append->exec()) || \is_int($append->exec()) + \is_string($rbytes) || \is_int($rbytes) ); } public function testwriteFileSafelyWritesContentsToFile() { - $write = IO\writeFile(self::$file, 'bar'); + $write = IO\catchIO( + IO\writeFile(self::$file, \sprintf("%s\n", 'bar')) + ); + $rbytes = $write->exec(); $this->assertInstanceOf(IO::class, $write); $this->assertTrue( - \is_bool($write->exec()) || \is_int($write->exec()) + \is_string($rbytes) || \is_int($rbytes) + // \is_bool($write->exec()) || \is_int($write->exec()) ); } diff --git a/tests/Immutable/CollectionTest.php b/tests/Immutable/CollectionTest.php index 2bcf0079..db48083d 100644 --- a/tests/Immutable/CollectionTest.php +++ b/tests/Immutable/CollectionTest.php @@ -5,7 +5,6 @@ \error_reporting(0); use Eris\Generator; - use Chemem\Bingo\Functional as f; use Chemem\Bingo\Functional\Immutable\Collection; diff --git a/tests/PatternMatching/MatchTest.php b/tests/PatternMatching/MatchTest.php index fe720509..c05738a1 100644 --- a/tests/PatternMatching/MatchTest.php +++ b/tests/PatternMatching/MatchTest.php @@ -46,13 +46,14 @@ public function cmatchProvider() */ public function testcmatchComputesExactMatches($patterns, $entries, $res) { - $eval = p\cmatch($patterns); - [$fst, $snd, $thd] = $entries; - [$rfst, $rsnd, $rthd] = $res; + $eval = p\cmatch($patterns); + $result = []; - $this->assertEquals($rfst, $eval($fst)); - $this->assertEquals($rsnd, $eval($snd)); - $this->assertEquals($rthd, $eval($thd)); + foreach ($entries as $entry) { + $result[] = $eval($entry); + } + + $this->assertEquals($res, $result); } public function patternMatchProvider() @@ -60,13 +61,13 @@ public function patternMatchProvider() return [ [ [ - '["hello", name]' => function ($name) { + '["hello", name]' => function ($name) { return f\concat(' ', 'Hello', $name); }, - IO::class => function () { + \sprintf('"%s"', IO::class) => function () { return 'i/o'; }, - '_' => function () { + '_' => function () { return 'undefined'; }, ], @@ -90,13 +91,13 @@ public function patternMatchProvider() ], [ [ - \stdClass::class => function () { + \sprintf('"%s"', \stdClass::class) => function () { return 'std'; }, - IO::class => function () { + \sprintf('"%s"', IO::class) => function () { return 'i/o'; }, - '_' => function () { + '_' => function () { return 'undefined'; }, ], @@ -105,28 +106,54 @@ public function patternMatchProvider() ], [ [ - '[_, "bar"]' => function () { + '[_, "bar"]' => function () { return 2; }, - '["get", name]' => function ($name) { + '["get", name]' => function ($name) { return f\concat(' ', 'Hello', $name); }, - '["foo", "baz"]' => function () { + '["foo", 12]' => function () { return 'foo-bar'; }, - '[a, (x:xs), b]' => function () { - return 12; + '[a, (x:xs), b]' => function (...$args) { + return f\mean(f\flatten($args)); }, - '_' => function () { + '[12.224, _]' => function () { + return 'float'; + }, + '[_, name, "foo"]' => function ($name) { + return \sprintf('Hello, %s', $name); + }, + '(a:b:c:d:e:_)' => function (...$args) { + return f\mean($args); + }, + 'a' => function ($value) { + return \strtoupper($value); + }, + '_' => function () { return 'undefined'; }, ], [ ['get', 'World'], - [3, ['foo', 'bar'], 'baz'], + [3, [5, 8], 12], ['xyz'], + [12, 'Loki', 'foo'], + [12.224, 'baz'], + ['foo', 12], + 'string', + \range(1, 5), + ], + [ + 'Hello World', + 7, + 'undefined', + 'Hello, Loki', + 'float', + 'foo-bar', + 'STRING', + 3, ], - ['Hello World', 12, 'undefined'], ], ]; } @@ -136,13 +163,14 @@ public function patternMatchProvider() */ public function testpatternMatchPerformsExhaustiveMatch($patterns, $entries, $res) { - $eval = f\partial(p\patternMatch, $patterns); - [$fst, $snd, $thd] = $entries; - [$rfst, $rsnd, $rthd] = $res; + $eval = f\partial(p\patternMatch, $patterns); + $result = null; - $this->assertEquals($rfst, $eval($fst)); - $this->assertEquals($rsnd, $eval($snd)); - $this->assertEquals($rthd, $eval($thd)); + foreach ($entries as $entry) { + $result[] = $eval($entry); + } + + $this->assertEquals($res, $result); } public function letInProvider() @@ -183,4 +211,53 @@ public function testletInPerformsDestructuringByPatternMatching( $this->assertEquals($res, $let($arg, $func)); } + + public static function extractProvider(): iterable + { + return [ + [ + '["foo", _, (x:xs)]', + ['foo', null, \range(1, 3)], + [ + 'x' => 1, + 'xs' => [1 => 2, 2 => 3], + ], + ], + [ + '(x:xs:_)', + \range(1, 5), + [ + 'x' => 1, + 'xs' => 2, + ], + ], + [ + '[12.23, 2, _, a]', + [12.23, 2, 'bar'], + [], + ], + [ + 'x', + 12, + ['x' => 12], + ], + [ + 'x', + [24.2, ['foo', 'bar']], + [ + 'x' => [24.2, ['foo', 'bar']], + ], + ], + ]; + } + + /** + * @dataProvider extractProvider + */ + public function testextractEffectsPatternBasedDestructuring(string $pattern, $input, $result) + { + $extracts = p\extract($pattern, $input); + + $this->assertEquals($result, $extracts); + } }