diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0217bbb..6c93c71 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,43 +1,44 @@ name: deploy docs on: - release: - types: [ created ] + push: + branches: + - master workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + jobs: - test: + deploy: name: Deploy docs runs-on: ubuntu-latest - + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - coverage: xdebug - tools: composer:v2 - - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: PHP Version Check - run: php -v - - - name: Validate Composer JSON - run: composer validate + - name: Generate API docs + run: docker run --rm -v $(pwd):/data phpdoc/phpdoc:3 --directory ./src --visibility public --target ./phpdoc --template default -v - - name: Run Composer - run: composer install --no-interaction + - name: Setup Pages + uses: actions/configure-pages@v4 - - name: Generate Docs - run: | - composer phpdoc - - - name: Deploy - uses: JamesIves/github-pages-deploy-action@4.1.0 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 with: - branch: docs - folder: phpdoc + path: './phpdoc' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 7920560..49a6788 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ vendor .idea .phpdoc +phpdoc tests/_output tests/_support/_generated composer.lock composer.phar build +g diff --git a/README.md b/README.md index edb074d..c10cf0a 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,18 @@ ![Build and test](https://github.com/Smoren/array-view-php/actions/workflows/test.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**Array View** is a PHP library that provides a powerful set of utilities for working with arrays in -a versatile and efficient manner. These classes enable developers to create views of arrays, manipulate data with ease, -and select specific elements using index lists, masks, and slice parameters. +![Array View Logo](docs/images/logo.png) -Array View offers a Python-like slicing experience for efficient data manipulation and selection of array elements. +**Array View** is a PHP library that provides powerful abstractions and utilities for working with lists of data. +Create views of arrays, slice and index using Python-like notation, transform and select your data using chained and +fluent operations. ## Features -- Create array views for easy data manipulation. -- Select elements using [Python-like slice notation](https://www.geeksforgeeks.org/python-list-slicing/). -- Handle array slicing operations with ease. -- Enable efficient selection of elements using index lists and boolean masks. - +- Array views as an abstraction over an array +- Forward and backward array indexing +- Selecting and slicing using [Python-like slice notation](https://www.geeksforgeeks.org/python-list-slicing/) +- Filtering, mapping, matching and masking +- Chaining operations via pipes and fluent interfaces ## How to install to your project ```bash @@ -24,26 +24,49 @@ composer require smoren/array-view ``` ## Usage -## Quick examples -### Slicing +### Indexing + +Index into an array forward or backwards using positive or negative indexes. + +| Data | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| ---------------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| *Positive Index* | *0* | *1* | *2* | *3* | *4* | *5* | *6* | +| *Negative Index* | *-7* | *-6* | *-5* | *-4* | *-3* | *-2* | *-1* | +```php +use Smoren\ArrayView\Views\ArrayView; + +$view = ArrayView::toView([1, 2, 3, 4, 5, 6, 7]); + +$view[0]; // 1 +$view[1]; // 2 +$view[-1]; // 7 +$view[-2]; // 6 +``` + +### Slices + +Use [Python-like slice notation](https://www.geeksforgeeks.org/python-list-slicing/) to select a range of elements: `[start, stop, step]`. ```php use Smoren\ArrayView\Views\ArrayView; $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; $view = ArrayView::toView($originalArray); +$view['1:6']; // [2, 3, 4, 5, 6] $view['1:7:2']; // [2, 4, 6] -$view[':3']; // [1, 2, 3] -$view['::-1']; // [9, 8, 7, 6, 5, 4, 3, 2, 1] - -$view[2]; // 3 -$view[4]; // 5 +$view[':3']; // [1, 2, 3] +$view['::-1']; // [9, 8, 7, 6, 5, 4, 3, 2, 1] +``` +Insert into parts of the array. +```php $view['1:7:2'] = [22, 44, 66]; -print_r($view); // [1, 22, 3, 44, 5, 66, 7, 8, 9] +print_r($originalArray); // [1, 22, 3, 44, 5, 66, 7, 8, 9] ``` ### Subviews + +Create subviews of the original view using masks, indexes, and slices. ```php use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; @@ -53,16 +76,23 @@ use Smoren\ArrayView\Views\ArrayView; $originalArray = [1, 2, 3, 4, 5]; $view = ArrayView::toView($originalArray); -$view->subview(new MaskSelector([true, false, true, false, true])).toArray(); // [1, 3, 5] -$view->subview(new IndexListSelector([1, 2, 4])).toArray(); // [2, 3, 5] -$view->subview(new SliceSelector('::-1')).toArray(); // [5, 4, 3, 2, 1] -$view->subview('::-1').toArray(); // [5, 4, 3, 2, 1] +// Object-oriented style +$view->subview(new MaskSelector([true, false, true, false, true]))->toArray(); // [1, 3, 5] +$view->subview(new IndexListSelector([1, 2, 4]))->toArray(); // [2, 3, 5] +$view->subview(new SliceSelector('::-1'))->toArray(); // [5, 4, 3, 2, 1] + +// Scripting style +$view->subview([true, false, true, false, true])->toArray(); // [1, 3, 5] +$view->subview([1, 2, 4])->toArray(); // [2, 3, 5] +$view->subview('::-1')->toArray(); // [5, 4, 3, 2, 1] -$view->subview(new MaskSelector([true, false, true, false, true])).apply(fn ($x) => x * 10); +$view->subview(new MaskSelector([true, false, true, false, true]))->apply(fn ($x) => x * 10); print_r($originalArray); // [10, 2, 30, 4, 50] ``` -### Subarrays +### Subarray Multi-indexing + +Directly select multiple elements using an array-index multi-selection. ```php use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; @@ -72,16 +102,23 @@ use Smoren\ArrayView\Views\ArrayView; $originalArray = [1, 2, 3, 4, 5]; $view = ArrayView::toView($originalArray); +// Object-oriented style $view[new MaskSelector([true, false, true, false, true])]; // [1, 3, 5] -$view[new IndexListSelector([1, 2, 4])]; // [2, 3, 5] -$view[new SliceSelector('::-1')]; // [5, 4, 3, 2, 1] -$view['::-1']; // [5, 4, 3, 2, 1] +$view[new IndexListSelector([1, 2, 4])]; // [2, 3, 5] +$view[new SliceSelector('::-1')]; // [5, 4, 3, 2, 1] + +// Scripting style +$view[[true, false, true, false, true]]; // [1, 3, 5] +$view[[1, 2, 4]]; // [2, 3, 5] +$view['::-1']; // [5, 4, 3, 2, 1] $view[new MaskSelector([true, false, true, false, true])] = [10, 30, 50]; print_r($originalArray); // [10, 2, 30, 4, 50] ``` -### Combining subviews +### Combining Subviews + +Combine and chain subviews one after another in a fluent interface to perform multiple selection operations. ```php use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; @@ -90,16 +127,57 @@ use Smoren\ArrayView\Views\ArrayView; $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; -$subview = ArrayView::toView(originalArray) - ->subview('::2') // [1, 3, 5, 7, 9] +// Fluent object-oriented style +$subview = ArrayView::toView($originalArray) + ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] - ->subview('1:'); // [5, 7] + ->subview(new SliceSelector('1:')); // [5, 7] + +$subview[':'] = [55, 77]; +print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] + +// Fluent scripting style +$originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +$subview = ArrayView::toView($originalArray) + ->subview('::2') // [1, 3, 5, 7, 9] + ->subview([true, false, true, true, true]) // [1, 5, 7, 9] + ->subview([0, 1, 2]) // [1, 5, 7] + ->subview('1:'); // [5, 7] $subview[':'] = [55, 77]; print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] ``` +### Selectors Pipe + +Create pipelines of selections that can be saved and applied again and again to new array views. +```php +use Smoren\ArrayView\Selectors\IndexListSelector; +use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\SliceSelector; +use Smoren\ArrayView\Views\ArrayView; + +$originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +$selector = new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, true, true]), + new IndexListSelector([0, 1, 2]), + new SliceSelector('1:'), +]); + +$view = ArrayView::toView($originalArray); +$subview = $view->subview($selector); +print_r($subview[':']); // [5, 7] + +$subview[':'] = [55, 77]; +print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] +``` + +## Documentation +For detailed documentation and usage examples, please refer to the +[API documentation](https://smoren.github.io/array-view-php/packages/Application.html). + ## Unit testing ``` composer install diff --git a/composer.json b/composer.json index 6f500c8..ce846bd 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ "stan": ["./vendor/bin/phpstan analyse -l 9 src"], "phpdoc": [ "export COMPOSER_PROCESS_TIMEOUT=9000", - "vendor/bin/phpdoc --directory ./src --visibility public --target ./phpdoc -v" + "vendor/bin/phpdoc --directory ./src --visibility public --target ./phpdoc --template default -v" ] } } diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..db18b19 Binary files /dev/null and b/docs/images/logo.png differ diff --git a/src/Exceptions/NotSupportedError.php b/src/Exceptions/NotSupportedError.php index ca0cda0..1228f2e 100644 --- a/src/Exceptions/NotSupportedError.php +++ b/src/Exceptions/NotSupportedError.php @@ -7,6 +7,6 @@ /** * Error class for not supported errors. */ -class NotSupportedError extends \Exception +class NotSupportedError extends \RuntimeException { } diff --git a/src/Interfaces/ArrayViewInterface.php b/src/Interfaces/ArrayViewInterface.php index 11a85c3..b8c316d 100644 --- a/src/Interfaces/ArrayViewInterface.php +++ b/src/Interfaces/ArrayViewInterface.php @@ -17,7 +17,7 @@ * * @template T The type of elements in the array * - * @extends \ArrayAccess> + * @extends \ArrayAccess|ArrayViewInterface|ArraySelectorInterface, T|array> * @extends \IteratorAggregate */ interface ArrayViewInterface extends \ArrayAccess, \IteratorAggregate, \Countable @@ -65,10 +65,24 @@ public static function toUnlinkedView($source, ?bool $readonly = null): ArrayVie */ public function toArray(): array; + /** + * Returns a subview of this view based on a selector or string slice. + * + * @param string|array|ArrayViewInterface|ArraySelectorInterface $selector The selector or + * string to filter the subview. + * @param bool|null $readonly Flag indicating if the subview should be read-only. + * + * @return ArrayViewInterface A new view representing the subview of this view. + * + * @throws IndexError if the selector is IndexListSelector and some indexes are out of range. + * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view. + */ + public function subview($selector, bool $readonly = null): ArrayViewInterface; + /** * Filters the elements in the view based on a predicate function. * - * @param callable(T): bool $predicate Function that returns a boolean value for each element. + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. * * @return ArrayViewInterface A new view with elements that satisfy the predicate. */ @@ -77,21 +91,70 @@ public function filter(callable $predicate): ArrayViewInterface; /** * Checks if all elements in the view satisfy a given predicate function. * - * @param callable(T): bool $predicate Function that returns a boolean value for each element. + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. * * @return MaskSelectorInterface Boolean mask for selecting elements that satisfy the predicate. + * + * @see ArrayViewInterface::match() Full synonim. */ public function is(callable $predicate): MaskSelectorInterface; /** - * Returns a subview of this view based on a selector or string slice. + * Checks if all elements in the view satisfy a given predicate function. * - * @param ArraySelectorInterface|string $selector The selector or string to filter the subview. - * @param bool|null $readonly Flag indicating if the subview should be read-only. + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. * - * @return ArrayViewInterface A new view representing the subview of this view. + * @return MaskSelectorInterface Boolean mask for selecting elements that satisfy the predicate. + * + * @see ArrayViewInterface::is() Full synonim. */ - public function subview($selector, bool $readonly = null): ArrayViewInterface; + public function match(callable $predicate): MaskSelectorInterface; + + /** + * Compares the elements of the current ArrayView instance with another array or ArrayView + * using the provided comparator function. + * + * @template U The type of the elements in the array for comparison with. + * + * @param array|ArrayViewInterface|U $data The array or ArrayView to compare to. + * @param callable(T, U, int): bool $comparator Function that determines the comparison logic between the elements. + * + * @return MaskSelectorInterface A MaskSelector instance representing the results of the element comparisons. + * + * @throws ValueError if the $data is not sequential array. + * @throws SizeError if size of $data not equals to size of the view. + */ + public function matchWith($data, callable $comparator): MaskSelectorInterface; + + /** + * Transforms each element of the array using the given callback function. + * + * The callback function receives two parameters: the current element of the array and its index. + * + * @param callable(T, int): T $mapper Function to transform each element. + * + * @return array New array with transformed elements of this view. + */ + public function map(callable $mapper): array; + + /** + * Transforms each pair of elements from the current array view and the provided data array using the given + * callback function. + * + * The callback function receives three parameters: the current element of the current array view, + * the corresponding element of the data array, and the index. + * + * @template U The type rhs of a binary operation. + * + * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. + * @param callable(T, U, int): T $mapper Function to transform each pair of elements. + * + * @return array New array with transformed elements of this view. + * + * @throws ValueError if the $data is not sequential array. + * @throws SizeError if size of $data not equals to size of the view. + */ + public function mapWith($data, callable $mapper): array; /** * Applies a transformation function to each element in the view. diff --git a/src/Interfaces/PipeSelectorInterface.php b/src/Interfaces/PipeSelectorInterface.php new file mode 100644 index 0000000..b3b586a --- /dev/null +++ b/src/Interfaces/PipeSelectorInterface.php @@ -0,0 +1,18 @@ + + */ + public function getValue(): array; +} diff --git a/src/Selectors/IndexListSelector.php b/src/Selectors/IndexListSelector.php index da6d40c..e6060fd 100644 --- a/src/Selectors/IndexListSelector.php +++ b/src/Selectors/IndexListSelector.php @@ -11,6 +11,15 @@ /** * Represents an index list selector that selects elements based on the provided array of indexes. + * + * ```php + * $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $view = ArrayView::toView($originalArray); + * + * $selector = new IndexListSelector([0, 2, 4]); + * print_r($view[$selector]); // [1, 3, 5] + * print_r($view->subview($selector)->toArray()); // [1, 3, 5] + * ``` */ final class IndexListSelector implements IndexListSelectorInterface { diff --git a/src/Selectors/MaskSelector.php b/src/Selectors/MaskSelector.php index f861589..80f0dca 100644 --- a/src/Selectors/MaskSelector.php +++ b/src/Selectors/MaskSelector.php @@ -10,8 +10,17 @@ /** * Represents a mask selector that selects elements based on the provided array of boolean mask values. + * + * ```php + * $originalArray = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($originalArray); + * + * $selector = new MaskSelector([true, false, true, false, true]); + * print_r($view[$selector]); // [1, 3, 5] + * print_r($view->subview($selector)->toArray()); // [1, 3, 5] + * ``` */ -class MaskSelector implements MaskSelectorInterface +final class MaskSelector implements MaskSelectorInterface { /** * @var array The array of boolean mask values to select elements based on. diff --git a/src/Selectors/PipeSelector.php b/src/Selectors/PipeSelector.php new file mode 100644 index 0000000..3548a0d --- /dev/null +++ b/src/Selectors/PipeSelector.php @@ -0,0 +1,118 @@ +subview($selector); + * print_r($subview[':']); // [5, 7] + * + * $subview[':'] = [55, 77]; + * print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] + * ``` + * + * ##### Example with nested pipes + * ```php + * $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $selector = new PipeSelector([ + * new SliceSelector('::2'), + * new PipeSelector([ + * new MaskSelector([true, false, true, true, true]), + * new IndexListSelector([0, 1, 2]), + * ]) + * new SliceSelector('1:'), + * ]); + * + * $view = ArrayView::toView($originalArray); + * $subview = $view->subview($selector); + * print_r($subview[':']); // [5, 7] + * + * $subview[':'] = [55, 77]; + * print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] + * ``` + */ +final class PipeSelector implements PipeSelectorInterface +{ + /** + * @var array An array of selectors to be applied sequentially. + */ + private array $selectors; + + /** + * Creates a new PipeSelector instance with the provided selectors array. + * + * @param array $selectors An array of selectors to be assigned to the PipeSelector. + */ + public function __construct(array $selectors) + { + $this->selectors = $selectors; + } + + /** + * Applies the series of selectors to the given source array view. + * + * @template T The type of elements in the source array view. + * + * @param ArrayViewInterface $source The source array view to select from. + * @param bool|null $readonly Optional parameter to specify if the view should be read-only. + * + * @return ArrayViewInterface The resulting array view after applying all selectors. + */ + public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayViewInterface + { + $view = ArrayView::toView($source, $readonly); + foreach ($this->selectors as $selector) { + $view = $selector->select($view, $readonly); + } + /** @var ArrayViewInterface $view */ + return $view; + } + + /** + * Checks if the series of selectors are compatible with the given array view. + * + * @template T The type of elements in the source array view. + * + * @param ArrayViewInterface $view The array view to check compatibility with. + * + * @return bool True if all selectors are compatible with the array view, false otherwise. + */ + public function compatibleWith(ArrayViewInterface $view): bool + { + foreach ($this->selectors as $selector) { + if (!$selector->compatibleWith($view)) { + return false; + } + $view = $selector->select($view); + } + return true; + } + + /** + * Returns the array of selectors assigned to the PipeSelector. + * + * @return array The array of selectors. + */ + public function getValue(): array + { + return $this->selectors; + } +} diff --git a/src/Selectors/SliceSelector.php b/src/Selectors/SliceSelector.php index 1625121..d1d1152 100644 --- a/src/Selectors/SliceSelector.php +++ b/src/Selectors/SliceSelector.php @@ -11,8 +11,33 @@ /** * Represents a slice selector that selects elements based on the provided slice parameters. + * + * ```php + * $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $view = ArrayView::toView($originalArray); + * + * $selector = new SliceSelector('::2'); + * print_r($view[$selector]); // [1, 3, 5, 7, 9] + * print_r($view->subview($selector)->toArray()); // [1, 3, 5, 7, 9] + * + * $selector = new SliceSelector('1::2'); + * print_r($view[$selector]); // [2, 4, 6, 8, 10] + * print_r($view->subview($selector)->toArray()); // [2, 4, 6, 8, 10] + * + * $selector = new SliceSelector('-3::-2'); + * print_r($view[$selector]); // [8, 6, 4, 2] + * print_r($view->subview($selector)->toArray()); // [8, 6, 4, 2] + * + * $selector = new SliceSelector('1:4'); + * print_r($view[$selector]); // [2, 3, 4] + * print_r($view->subview($selector)->toArray()); // [2, 3, 4] + * + * $selector = new SliceSelector('-2:0:-1'); + * print_r($view[$selector]); // [9, 8, 7, 6, 5, 4, 3, 2] + * print_r($view->subview($selector)->toArray()); // [9, 8, 7, 6, 5, 4, 3, 2] + * ``` */ -class SliceSelector extends Slice implements ArraySelectorInterface +final class SliceSelector extends Slice implements ArraySelectorInterface { /** * Creates a new SliceSelector instance with the provided slice parameters. diff --git a/src/Structs/NormalizedSlice.php b/src/Structs/NormalizedSlice.php index e7044e3..05a507b 100644 --- a/src/Structs/NormalizedSlice.php +++ b/src/Structs/NormalizedSlice.php @@ -65,7 +65,7 @@ public function getStep(): int */ public function count(): int { - return intval(ceil(abs((($this->end - $this->start) / $this->step)))); + return \intval(\ceil(\abs((($this->end - $this->start) / $this->step)))); } /** diff --git a/src/Structs/Slice.php b/src/Structs/Slice.php index 56d056c..5fffc6c 100644 --- a/src/Structs/Slice.php +++ b/src/Structs/Slice.php @@ -47,7 +47,7 @@ public static function toSlice($s): Slice } if (!self::isSliceString($s)) { - $str = \is_scalar($s) ? "{$s}" : gettype($s); + $str = \is_scalar($s) ? "{$s}" : \gettype($s); throw new ValueError("Invalid slice: \"{$str}\"."); } diff --git a/src/Traits/ArrayViewAccessTrait.php b/src/Traits/ArrayViewAccessTrait.php index 9ff93c3..5d9d096 100644 --- a/src/Traits/ArrayViewAccessTrait.php +++ b/src/Traits/ArrayViewAccessTrait.php @@ -10,22 +10,48 @@ use Smoren\ArrayView\Exceptions\ReadonlyError; use Smoren\ArrayView\Interfaces\ArraySelectorInterface; use Smoren\ArrayView\Interfaces\ArrayViewInterface; +use Smoren\ArrayView\Selectors\IndexListSelector; +use Smoren\ArrayView\Selectors\MaskSelector; use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Structs\Slice; +use Smoren\ArrayView\Util; /** * Trait providing methods for accessing elements in ArrayView object. + * * The trait implements methods for accessing, retrieving, setting, * and unsetting elements in the ArrayView object. * * @template T Type of ArrayView values. + * @template S of string|array|ArrayViewInterface|ArraySelectorInterface Selector type. */ trait ArrayViewAccessTrait { /** * Check if the specified offset exists in the ArrayView object. * - * @param numeric|string|ArraySelectorInterface $offset The offset to check. + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * isset($view[0]); // true + * isset($view[-1]); // true + * isset($view[10]); // false + * + * isset($view[new SliceSelector('::2')]); // true + * isset($view[new IndexListSelector([0, 2, 4])]); // true + * isset($view[new IndexListSelector([0, 2, 10])]); // false + * isset($view[new MaskSelector([true, true, false, false, true])]); // true + * isset($view[new MaskSelector([true, true, false, false, true, true])]); // false + * + * isset($view['::2']); // true + * isset($view[[0, 2, 4]]); // true + * isset($view[[0, 2, 10]]); // false + * isset($view[[true, true, false, false, true]]); // true + * isset($view[[true, true, false, false, true, true]]); // false + * ``` + * + * @param numeric|S $offset The offset to check. * * @return bool * @@ -37,21 +63,33 @@ public function offsetExists($offset): bool return $this->numericOffsetExists($offset); } - if (\is_string($offset) && Slice::isSlice($offset)) { - return true; - } - - if ($offset instanceof ArraySelectorInterface) { - return $offset->compatibleWith($this); + try { + return $this->toSelector($offset)->compatibleWith($this); + } catch (KeyError $e) { + return false; } - - return false; } /** * Get the value at the specified offset in the ArrayView object. * - * @param numeric|string|ArraySelectorInterface $offset The offset to get the value from. + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * $view[0]; // 1 + * $view[-1]; // 5 + * + * $view[new SliceSelector('::2')]; // [1, 3, 5] + * $view[new IndexListSelector([0, 2, 4])]; // [1, 3, 5] + * $view[new MaskSelector([true, true, false, false, true])]; // [1, 2, 5] + * + * $view['::2']; // [1, 3, 5] + * $view[[0, 2, 4]]; // [1, 3, 5] + * $view[[true, true, false, false, true]]; // [1, 2, 5] + * ``` + * + * @param numeric|S $offset The offset to get the value at. * * @return T|array The value at the specified offset. * @@ -63,7 +101,6 @@ public function offsetExists($offset): bool #[\ReturnTypeWillChange] public function offsetGet($offset) { - /** @var mixed $offset */ if (\is_numeric($offset)) { if (!$this->numericOffsetExists($offset)) { throw new IndexError("Index {$offset} is out of range."); @@ -71,22 +108,47 @@ public function offsetGet($offset) return $this->source[$this->convertIndex(\intval($offset))]; } - if (\is_string($offset) && Slice::isSlice($offset)) { - return $this->subview(new SliceSelector($offset))->toArray(); - } - - if ($offset instanceof ArraySelectorInterface) { - return $this->subview($offset)->toArray(); - } - - $strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset); - throw new KeyError("Invalid key: \"{$strOffset}\"."); + return $this->subview($this->toSelector($offset))->toArray(); } /** * Set the value at the specified offset in the ArrayView object. * - * @param numeric|string|ArraySelectorInterface $offset The offset to set the value at. + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * $view[0] = 11; + * $view[-1] = 55; + * + * $source; // [11, 2, 3, 4, 55] + * + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * $view[new SliceSelector('::2')] = [11, 33, 55]; + * $source; // [11, 2, 33, 4, 55] + * + * $view[new IndexListSelector([1, 3])] = [22, 44]; + * $source; // [11, 22, 33, 44, 55] + * + * $view[new MaskSelector([true, false, false, false, true])] = [111, 555]; + * $source; // [111, 22, 33, 44, 555] + * + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * $view['::2'] = [11, 33, 55]; + * $source; // [11, 2, 33, 4, 55] + * + * $view[[1, 3]] = [22, 44]; + * $source; // [11, 22, 33, 44, 55] + * + * $view[[true, false, false, false, true]] = [111, 555]; + * $source; // [111, 22, 33, 44, 555] + * ``` + * + * @param numeric|S $offset The offset to set the value at. * @param T|array|ArrayViewInterface $value The value to set. * * @return void @@ -99,40 +161,27 @@ public function offsetGet($offset) */ public function offsetSet($offset, $value): void { - /** @var mixed $offset */ if ($this->isReadonly()) { throw new ReadonlyError("Cannot modify a readonly view."); } - if (\is_numeric($offset)) { - if (!$this->numericOffsetExists($offset)) { - throw new IndexError("Index {$offset} is out of range."); - } - - // @phpstan-ignore-next-line - $this->source[$this->convertIndex(\intval($offset))] = $value; + if (!\is_numeric($offset)) { + $this->subview($this->toSelector($offset))->set($value); return; } - if (\is_string($offset) && Slice::isSlice($offset)) { - /** @var array|ArrayViewInterface $value */ - $this->subview(new SliceSelector($offset))->set($value); - return; + if (!$this->numericOffsetExists($offset)) { + throw new IndexError("Index {$offset} is out of range."); } - if ($offset instanceof ArraySelectorInterface) { - $this->subview($offset)->set($value); - return; - } - - $strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset); - throw new KeyError("Invalid key: \"{$strOffset}\"."); + // @phpstan-ignore-next-line + $this->source[$this->convertIndex(\intval($offset))] = $value; } /** * Unset the value at the specified offset in the array-like object. * - * @param numeric|string|ArraySelectorInterface $offset The offset to unset the value at. + * @param numeric|S $offset The offset to unset the value at. * * @return void * @@ -144,4 +193,39 @@ public function offsetUnset($offset): void { throw new NotSupportedError(); } + + /** + * Converts array to selector. + * + * @param S $input value to convert. + * + * @return ArraySelectorInterface + */ + protected function toSelector($input): ArraySelectorInterface + { + if ($input instanceof ArraySelectorInterface) { + return $input; + } + + if (\is_string($input) && Slice::isSlice($input)) { + return new SliceSelector($input); + } + + if ($input instanceof ArrayViewInterface) { + $input = $input->toArray(); + } + + if (!\is_array($input) || !Util::isArraySequential($input)) { + $strOffset = \is_scalar($input) ? \strval($input) : \gettype($input); + throw new KeyError("Invalid key: \"{$strOffset}\"."); + } + + if (\count($input) > 0 && \is_bool($input[0])) { + /** @var array $input */ + return new MaskSelector($input); + } + + /** @var array $input */ + return new IndexListSelector($input); + } } diff --git a/src/Traits/ArrayViewOperationsTrait.php b/src/Traits/ArrayViewOperationsTrait.php new file mode 100644 index 0000000..f82ab2a --- /dev/null +++ b/src/Traits/ArrayViewOperationsTrait.php @@ -0,0 +1,341 @@ +|ArrayViewInterface|ArraySelectorInterface Selector type. + */ +trait ArrayViewOperationsTrait +{ + /** + * Filters the elements in the view based on a predicate function. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6]; + * $view = ArrayView::toView($source); + * + * $filtered = $view->filter(fn ($x) => $x % 2 === 0); + * $filtered->toArray(); // [2, 4, 6] + * + * $filtered[':'] = [20, 40, 60]; + * $filtered->toArray(); // [20, 40, 60] + * $source; // [1, 20, 3, 40, 5, 60] + * ``` + * + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. + * + * @return ArrayMaskView A new view with elements that satisfy the predicate. + */ + public function filter(callable $predicate): ArrayViewInterface + { + return $this->is($predicate)->select($this); + } + + /** + * Checks if all elements in the view satisfy a given predicate function. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6]; + * $view = ArrayView::toView($source); + * + * $mask = $view->is(fn ($x) => $x % 2 === 0); + * $mask->getValue(); // [false, true, false, true, false, true] + * + * $view->subview($mask)->toArray(); // [2, 4, 6] + * $view[$mask]; // [2, 4, 6] + * + * $view[$mask] = [20, 40, 60]; + * $source; // [1, 20, 3, 40, 5, 60] + * ``` + * + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. + * + * @return MaskSelector Boolean mask for selecting elements that satisfy the predicate. + * + * @see ArrayViewInterface::match() Full synonim. + */ + public function is(callable $predicate): MaskSelectorInterface + { + $data = $this->toArray(); + return new MaskSelector(array_map($predicate, $data, array_keys($data))); + } + + /** + * Checks if all elements in the view satisfy a given predicate function. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6]; + * $view = ArrayView::toView($source); + * + * $mask = $view->match(fn ($x) => $x % 2 === 0); + * $mask->getValue(); // [false, true, false, true, false, true] + * + * $view->subview($mask)->toArray(); // [2, 4, 6] + * $view[$mask]; // [2, 4, 6] + * + * $view[$mask] = [20, 40, 60]; + * $source; // [1, 20, 3, 40, 5, 60] + * ``` + * + * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. + * + * @return MaskSelector Boolean mask for selecting elements that satisfy the predicate. + * + * @see ArrayView::match() Full synonim. + */ + public function match(callable $predicate): MaskSelectorInterface + { + return $this->is($predicate); + } + + /** + * Compares the elements of the current ArrayView instance with another array or ArrayView + * using the provided comparator function. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6]; + * $view = ArrayView::toView($source); + * + * $data = [6, 5, 4, 3, 2, 1]; + * + * $mask = $view->matchWith($data, fn ($lhs, $rhs) => $lhs > $rhs); + * $mask->getValue(); // [false, false, false, true, true, true] + * + * $view->subview($mask)->toArray(); // [4, 5, 6] + * $view[$mask]; // [4, 5, 6] + * + * $view[$mask] = [40, 50, 60]; + * $source; // [1, 2, 3, 40, 50, 60] + * ``` + * + * @template U The type of the elements in the array for comparison with. + * + * @param array|ArrayViewInterface|U $data The array or ArrayView to compare to. + * @param callable(T, U, int): bool $comparator Function that determines the comparison logic between the elements. + * + * @return MaskSelectorInterface A MaskSelector instance representing the results of the element comparisons. + * + * @throws ValueError if the $data is not sequential array. + * @throws SizeError if size of $data not equals to size of the view. + * + * @see ArrayView::is() Full synonim. + */ + public function matchWith($data, callable $comparator): MaskSelectorInterface + { + $data = $this->checkAndConvertArgument($data); + return new MaskSelector(array_map($comparator, $this->toArray(), $data, array_keys($data))); + } + + /** + * Transforms each element of the array using the given callback function. + * + * The callback function receives two parameters: the current element of the array and its index. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] + * + * $subview->map(fn ($x) => $x * 10); // [10, 30, 50, 70, 90] + * ``` + * + * @param callable(T, int): T $mapper Function to transform each element. + * + * @return array New array with transformed elements of this view. + */ + public function map(callable $mapper): array + { + $result = []; + $size = \count($this); + for ($i = 0; $i < $size; $i++) { + /** @var T $item */ + $item = $this[$i]; + $result[$i] = $mapper($item, $i); + } + return $result; + } + + /** + * Transforms each pair of elements from the current array view and the provided data array using the given + * callback function. + * + * The callback function receives three parameters: the current element of the current array view, + * the corresponding element of the data array, and the index. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] + * + * $data = [9, 27, 45, 63, 81]; + * + * $subview->mapWith($data, fn ($lhs, $rhs) => $lhs + $rhs); // [10, 30, 50, 70, 90] + * ``` + * + * @template U The type rhs of a binary operation. + * + * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. + * @param callable(T, U, int): T $mapper Function to transform each pair of elements. + * + * @return array New array with transformed elements of this view. + * + * @throws ValueError if the $data is not sequential array. + * @throws SizeError if size of $data not equals to size of the view. + */ + public function mapWith($data, callable $mapper): array + { + $data = $this->checkAndConvertArgument($data); + $result = []; + + $size = \count($this); + for ($i = 0; $i < $size; $i++) { + /** @var T $lhs */ + $lhs = $this[$i]; + /** @var U $rhs */ + $rhs = $data[$i]; + $result[$i] = $mapper($lhs, $rhs, $i); + } + + return $result; + } + + /** + * Applies a transformation function to each element in the view. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] + * + * $subview->apply(fn ($x) => $x * 10); + * + * $subview->toArray(); // [10, 30, 50, 70, 90] + * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10] + * ``` + * + * @param callable(T, int): T $mapper Function to transform each element. + * + * @return ArrayView this view. + */ + public function apply(callable $mapper): self + { + $size = \count($this); + for ($i = 0; $i < $size; $i++) { + /** @var T $item */ + $item = $this[$i]; + $this[$i] = $mapper($item, $i); + } + return $this; + } + + /** + * Sets new values for the elements in the view. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] + * + * $data = [9, 27, 45, 63, 81]; + * + * $subview->applyWith($data, fn ($lhs, $rhs) => $lhs + $rhs); + * $subview->toArray(); // [10, 30, 50, 70, 90] + * + * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10] + * ``` + * + * @template U Type of $data items. + * + * @param array|ArrayViewInterface $data + * @param callable(T, U, int): T $mapper + * + * @return ArrayView this view. + * + * @throws ValueError if the $data is not sequential array. + * @throws SizeError if size of $data not equals to size of the view. + */ + public function applyWith($data, callable $mapper): self + { + $data = $this->checkAndConvertArgument($data); + + $size = \count($this); + for ($i = 0; $i < $size; $i++) { + /** @var T $lhs */ + $lhs = $this[$i]; + /** @var U $rhs */ + $rhs = $data[$i]; + $this[$i] = $mapper($lhs, $rhs, $i); + } + + return $this; + } + + /** + * Check if the given source array is sequential (indexed from 0 to n-1). + * + * If the array is not sequential, a ValueError is thrown indicating that + * a view cannot be created for a non-sequential array. + * + * @param mixed $source The source array to check for sequential indexing. + * + * @return void + * + * @throws ValueError if the source array is not sequential. + */ + protected function checkSequentialArgument($source): void + { + if ($source instanceof ArrayViewInterface) { + return; + } + + if (\is_array($source) && !Util::isArraySequential($source)) { + throw new ValueError('Argument is not sequential.'); + } + } + + /** + * Util function for checking and converting data argument. + * + * @template U Type of $data items. + * + * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. + * + * @return array converted data. + */ + protected function checkAndConvertArgument($data): array + { + $this->checkSequentialArgument($data); + + if ($data instanceof ArrayViewInterface) { + $data = $data->toArray(); + } elseif (!\is_array($data)) { + $data = \array_fill(0, \count($this), $data); + } + + [$dataSize, $thisSize] = [\count($data), \count($this)]; + if ($dataSize !== $thisSize) { + throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize})."); + } + + return $data; + } +} diff --git a/src/Util.php b/src/Util.php index 6eb12f3..7b51f9e 100644 --- a/src/Util.php +++ b/src/Util.php @@ -26,7 +26,7 @@ class Util */ public static function normalizeIndex(int $index, int $containerLength, bool $throwError = true): int { - $dist = $index >= 0 ? $index : abs($index) - 1; + $dist = $index >= 0 ? $index : \abs($index) - 1; if ($throwError && $dist >= $containerLength) { throw new IndexError("Index {$index} is out of range."); } @@ -43,9 +43,9 @@ public static function normalizeIndex(int $index, int $containerLength, bool $th */ public static function isArraySequential(array $source, bool $forceCustomImplementation = false): bool { - if (!function_exists('array_is_list') || $forceCustomImplementation) { - return \count($source) === 0 || array_keys($source) === range(0, count($source) - 1); + if (!\function_exists('array_is_list') || $forceCustomImplementation) { + return \count($source) === 0 || \array_keys($source) === \range(0, \count($source) - 1); } - return array_is_list($source); + return \array_is_list($source); } } diff --git a/src/Views/ArrayIndexListView.php b/src/Views/ArrayIndexListView.php index 4936b54..a6c5ec9 100644 --- a/src/Views/ArrayIndexListView.php +++ b/src/Views/ArrayIndexListView.php @@ -14,7 +14,13 @@ * * Each element in the view is based on the specified indexes. * - * @template T Type of array view elements. + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source)->subview(new IndexListSelector([0, 2, 4])); + * $view->toArray(); // [1, 3, 5] + * ``` + * + * @template T Type of array source elements. * * @extends ArrayView */ @@ -46,8 +52,7 @@ public function __construct(&$source, array $indexes, ?bool $readonly = null) */ public function toArray(): array { - /** @var Array */ - return array_map(fn(int $index) => $this[$index], array_keys($this->indexes)); + return array_map(fn (int $index) => $this->source[$index], $this->indexes); } /** diff --git a/src/Views/ArrayMaskView.php b/src/Views/ArrayMaskView.php index 1f1bb5b..c4b1581 100644 --- a/src/Views/ArrayMaskView.php +++ b/src/Views/ArrayMaskView.php @@ -14,7 +14,13 @@ * * Each element in the view is included or excluded based on the specified boolean mask. * - * @template T + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source)->subview(new MaskSelector([true, false, true, false, true])); + * $view->toArray(); // [1, 3, 5] + * ``` + * + * @template T Type of array source elements. * * @extends ArrayIndexListView */ diff --git a/src/Views/ArraySliceView.php b/src/Views/ArraySliceView.php index 83845e3..a973da2 100644 --- a/src/Views/ArraySliceView.php +++ b/src/Views/ArraySliceView.php @@ -14,7 +14,13 @@ * Class representing a slice-based view of an array or another ArrayView * for accessing elements within a specified slice range. * - * @template T + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source)->subview(new SliceSelector('::2')); + * $view->toArray(); // [1, 3, 5] + * ``` + * + * @template T Type of array source elements. * * @extends ArrayView */ diff --git a/src/Views/ArrayView.php b/src/Views/ArrayView.php index df039d0..5c1e692 100644 --- a/src/Views/ArrayView.php +++ b/src/Views/ArrayView.php @@ -5,30 +5,43 @@ namespace Smoren\ArrayView\Views; use Smoren\ArrayView\Exceptions\IndexError; +use Smoren\ArrayView\Exceptions\KeyError; use Smoren\ArrayView\Exceptions\SizeError; use Smoren\ArrayView\Exceptions\ReadonlyError; use Smoren\ArrayView\Exceptions\ValueError; +use Smoren\ArrayView\Interfaces\ArraySelectorInterface; use Smoren\ArrayView\Interfaces\ArrayViewInterface; -use Smoren\ArrayView\Interfaces\MaskSelectorInterface; -use Smoren\ArrayView\Selectors\MaskSelector; -use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Traits\ArrayViewAccessTrait; +use Smoren\ArrayView\Traits\ArrayViewOperationsTrait; use Smoren\ArrayView\Util; /** * Class representing a view of an array or another array view * with additional methods for filtering, mapping, and transforming the data. * - * @template T + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * ``` + * + * @template T Type of array source elements. * * @implements ArrayViewInterface */ class ArrayView implements ArrayViewInterface { /** - * @use ArrayViewAccessTrait for array access methods. + * @use ArrayViewAccessTrait|ArrayViewInterface|ArraySelectorInterface> + * + * for array access methods. */ use ArrayViewAccessTrait; + /** + * @use ArrayViewOperationsTrait|ArrayViewInterface|ArraySelectorInterface> + * + * for utils methods. + */ + use ArrayViewOperationsTrait; /** * @var array|ArrayViewInterface The source array or view. @@ -44,7 +57,43 @@ class ArrayView implements ArrayViewInterface protected ?ArrayViewInterface $parentView; /** - * {@inheritDoc} + * Creates an ArrayView instance from the given source array or ArrayView. + * + * * If the source is not an ArrayView, a new ArrayView is created with the provided source. + * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, + * a new readonly ArrayView is created. + * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * + * $view[0]; // 1 + * $view['1::2']; // [2, 4] + * $view['1::2'] = [22, 44]; + * + * $view->toArray(); // [1, 22, 3, 44, 5] + * $source; // [1, 22, 3, 44, 5] + * ``` + * + * ##### Readonly example + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source, true); + * + * $view['1::2']; // [2, 4] + * $view['1::2'] = [22, 44]; // throws ReadonlyError + * $view[0] = 11; // throws ReadonlyError + * ``` + * + * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. + * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. + * + * @return ArrayViewInterface An ArrayView instance based on the source array or ArrayView. + * + * @throws ValueError if the array is not sequential. + * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. */ public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface { @@ -61,6 +110,29 @@ public static function toView(&$source, ?bool $readonly = null): ArrayViewInterf /** * {@inheritDoc} + * + * ##### Example: + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toUnlinkedView($source); + * + * $view[0]; // 1 + * $view['1::2']; // [2, 4] + * $view['1::2'] = [22, 44]; + * + * $view->toArray(); // [1, 22, 3, 44, 5] + * $source; // [1, 2, 3, 4, 5] + * ``` + * + * ##### Readonly example: + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toUnlinkedView($source, true); + * + * $view['1::2']; // [2, 4] + * $view['1::2'] = [22, 44]; // throws ReadonlyError + * $view[0] = 11; // throws ReadonlyError + * ``` */ public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface { @@ -70,15 +142,22 @@ public static function toUnlinkedView($source, ?bool $readonly = null): ArrayVie /** * Constructor to create a new ArrayView. * + * * If the source is not an ArrayView, a new ArrayView is created with the provided source. + * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, + * a new readonly ArrayView is created. + * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. + * * @param array|ArrayViewInterface $source The source array or view. * @param bool|null $readonly Flag indicating if the view is readonly. * * @throws ValueError if the array is not sequential. * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. + * + * @see ArrayView::toView() for creating views. */ public function __construct(&$source, ?bool $readonly = null) { - $this->checkSequential($source); + $this->checkSequentialArgument($source); $this->source = &$source; $this->readonly = $readonly ?? (($source instanceof ArrayViewInterface) ? $source->isReadonly() : false); @@ -90,7 +169,16 @@ public function __construct(&$source, ?bool $readonly = null) } /** - * {@inheritDoc} + * Returns the array representation of the view. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * $view = ArrayView::toView($source); + * $view->toArray(); // [1, 2, 3, 4, 5] + * ``` + * + * @return array The array representation of the view. */ public function toArray(): array { @@ -98,91 +186,86 @@ public function toArray(): array } /** - * {@inheritDoc} - */ - public function filter(callable $predicate): ArrayViewInterface - { - return $this->is($predicate)->select($this); - } - - /** - * {@inheritDoc} - */ - public function is(callable $predicate): MaskSelectorInterface - { - $data = $this->toArray(); - return new MaskSelector(array_map($predicate, $data, array_keys($data))); - } - - /** - * {@inheritDoc} + * Returns a subview of this view based on a selector or string slice. + * + * ##### Example (using selector objects) + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * + * $subview = ArrayView::toView($source) + * ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] + * ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] + * ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] + * ->subview(new SliceSelector('1:')); // [5, 7] + * + * $subview[':'] = [55, 77]; + * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] + * ``` + * + * ##### Example (using short objects) + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * + * $subview = ArrayView::toView($source) + * ->subview('::2') // [1, 3, 5, 7, 9] + * ->subview([true, false, true, true, true]) // [1, 5, 7, 9] + * ->subview([0, 1, 2]) // [1, 5, 7] + * ->subview('1:'); // [5, 7] + * + * $subview[':'] = [55, 77]; + * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] + * ``` + * + * ##### Readonly example + * ```php + * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + * $subview = ArrayView::toView($source)->subview('::2'); + * + * $subview[':']; // [1, 3, 5, 7, 9] + * $subview[':'] = [11, 33, 55, 77, 99]; // throws ReadonlyError + * $subview[0] = [11]; // throws ReadonlyError + * ``` + * + * @template S of string|array|ArrayViewInterface|ArraySelectorInterface Selector type. + * + * @param S $selector The selector or string to filter the subview. + * @param bool|null $readonly Flag indicating if the subview should be read-only. + * + * @return ArrayViewInterface A new view representing the subview of this view. + * + * @throws IndexError if the selector is IndexListSelector and some indexes are out of range. + * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view. + * @throws KeyError if the selector is not valid (e.g. non-sequential array). */ public function subview($selector, bool $readonly = null): ArrayViewInterface { - return is_string($selector) - ? (new SliceSelector($selector))->select($this, $readonly) - : $selector->select($this, $readonly); + return $this->toSelector($selector)->select($this, $readonly); } /** - * @return ArrayView + * Sets new values for the elements in the view. * - * {@inheritDoc} - */ - public function apply(callable $mapper): self - { - $size = \count($this); - for ($i = 0; $i < $size; $i++) { - /** @var T $item */ - $item = $this[$i]; - $this[$i] = $mapper($item, $i); - } - return $this; - } - - /** - * @template U + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] * - * @param array|ArrayViewInterface $data - * @param callable(T, U, int): T $mapper + * $subview->set([11, 33, 55]); + * $subview->toArray(); // [11, 33, 55] * - * @return ArrayView + * $source; // [11, 2, 33, 4, 55] + * ``` * - * {@inheritDoc} - */ - public function applyWith($data, callable $mapper): self - { - $this->checkSequential($data); - - [$dataSize, $thisSize] = [\count($data), \count($this)]; - if ($dataSize !== $thisSize) { - throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize})."); - } - - $dataView = ArrayView::toView($data); - - $size = \count($this); - for ($i = 0; $i < $size; $i++) { - /** @var T $lhs */ - $lhs = $this[$i]; - /** @var U $rhs */ - $rhs = $dataView[$i]; - $this[$i] = $mapper($lhs, $rhs, $i); - } - - return $this; - } - - /** - * {@inheritDoc} + * @param array|ArrayViewInterface|T $newValues The new values to set. * * @return ArrayView this view. * - * @throws SizeError if the length of newValues array is not equal to the length of the view. + * @throws ValueError if the $newValues is not sequential array. + * @throws SizeError if size of $newValues not equals to size of the view. */ public function set($newValues): self { - $this->checkSequential($newValues); + $this->checkSequentialArgument($newValues); if (!\is_array($newValues) && !($newValues instanceof ArrayViewInterface)) { $size = \count($this); @@ -197,18 +280,31 @@ public function set($newValues): self throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize})."); } - $newValuesView = ArrayView::toView($newValues); - $size = \count($this); + for ($i = 0; $i < $size; $i++) { - $this[$i] = $newValuesView[$i]; + $this[$i] = $newValues[$i]; } return $this; } /** - * {@inheritDoc} + * Return iterator to iterate the view elements. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] + * + * foreach ($subview as $item) { + * // 1, 3, 5 + * } + * + * print_r([...$subview]); // [1, 3, 5] + * ``` + * + * @return \Generator */ public function getIterator(): \Generator { @@ -221,7 +317,26 @@ public function getIterator(): \Generator } /** - * {@inheritDoc} + * Return true if view is readonly, otherwise false. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * + * $readonlyView = ArrayView::toView($source, true); + * $readonlyView->isReadonly(); // true + * + * $readonlySubview = ArrayView::toView($source)->subview('::2', true); + * $readonlySubview->isReadonly(); // true + * + * $view = ArrayView::toView($source); + * $view->isReadonly(); // false + * + * $subview = ArrayView::toView($source)->subview('::2'); + * $subview->isReadonly(); // false + * ``` + * + * @return bool */ public function isReadonly(): bool { @@ -229,7 +344,17 @@ public function isReadonly(): bool } /** - * {@inheritDoc} + * Return size of the view. + * + * ##### Example + * ```php + * $source = [1, 2, 3, 4, 5]; + * + * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] + * count($subview); // 3 + * ``` + * + * @return int */ public function count(): int { @@ -248,25 +373,6 @@ protected function getParentSize(): int : \count($this->source); } - /** - * Check if the given source array is sequential (indexed from 0 to n-1). - * - * If the array is not sequential, a ValueError is thrown indicating that - * a view cannot be created for a non-sequential array. - * - * @param mixed $source The source array to check for sequential indexing. - * - * @return void - * - * @throws ValueError if the source array is not sequential. - */ - protected function checkSequential($source): void - { - if (is_array($source) && !Util::isArraySequential($source)) { - throw new ValueError('Cannot create view for non-sequential array.'); - } - } - /** * Convert the given index to a valid index within the source array. * @@ -290,11 +396,13 @@ protected function convertIndex(int $i): int */ private function numericOffsetExists($offset): bool { - if (!\is_string($offset) && \is_numeric($offset) && (\is_nan($offset) || \is_infinite($offset))) { + // Non-string must be integer + if (!\is_string($offset) && !\is_int($offset)) { return false; } - if (\is_numeric($offset) && !\is_integer($offset + 0)) { + // Numeric string must be integer + if (!\is_integer($offset + 0)) { return false; } @@ -303,6 +411,7 @@ private function numericOffsetExists($offset): bool } catch (IndexError $e) { return false; } + return \is_array($this->source) ? \array_key_exists($index, $this->source) : $this->source->offsetExists($index); diff --git a/tests/unit/ArrayIndexListView/IssetTest.php b/tests/unit/ArrayIndexListView/IssetTest.php index 430204e..51ba130 100644 --- a/tests/unit/ArrayIndexListView/IssetTest.php +++ b/tests/unit/ArrayIndexListView/IssetTest.php @@ -47,6 +47,7 @@ public function testIssetSelectorTrue(array $source, array $indexes) $view = ArrayView::toView($source); $this->assertTrue(isset($view[new IndexListSelector($indexes)])); + $this->assertTrue(isset($view[$indexes])); $subview = $view->subview(new IndexListSelector($indexes)); $this->assertSame(\count($indexes), \count($subview)); diff --git a/tests/unit/ArrayIndexListView/ReadTest.php b/tests/unit/ArrayIndexListView/ReadTest.php index 5711e7e..f010dc7 100644 --- a/tests/unit/ArrayIndexListView/ReadTest.php +++ b/tests/unit/ArrayIndexListView/ReadTest.php @@ -43,10 +43,58 @@ public function testReadByMethod(array $source, array $indexes, array $expected) /** * @dataProvider dataProviderForRead */ - public function testReadByIndex(array $source, array $mask, array $expected) + public function testReadByIndex(array $source, array $indexes, array $expected) { $view = ArrayView::toView($source); - $subArray = $view[new IndexListSelector($mask)]; + $subArray = $view[new IndexListSelector($indexes)]; + + $this->assertSame($expected, $subArray); + $this->assertSame(\count($expected), \count($subArray)); + + for ($i = 0; $i < \count($subArray); ++$i) { + $this->assertSame($expected[$i], $subArray[$i]); + } + + for ($i = 0; $i < \count($view); ++$i) { + $this->assertSame($source[$i], $view[$i]); + } + + $this->assertSame($source, $view->toArray()); + $this->assertSame($source, [...$view]); + $this->assertSame($expected, $subArray); + } + + /** + * @dataProvider dataProviderForRead + */ + public function testReadByArrayIndex(array $source, array $indexes, array $expected) + { + $view = ArrayView::toView($source); + $subArray = $view[$indexes]; + + $this->assertSame($expected, $subArray); + $this->assertSame(\count($expected), \count($subArray)); + + for ($i = 0; $i < \count($subArray); ++$i) { + $this->assertSame($expected[$i], $subArray[$i]); + } + + for ($i = 0; $i < \count($view); ++$i) { + $this->assertSame($source[$i], $view[$i]); + } + + $this->assertSame($source, $view->toArray()); + $this->assertSame($source, [...$view]); + $this->assertSame($expected, $subArray); + } + + /** + * @dataProvider dataProviderForRead + */ + public function testReadByArrayViewIndex(array $source, array $indexes, array $expected) + { + $view = ArrayView::toView($source); + $subArray = $view[ArrayView::toView($indexes)]; $this->assertSame($expected, $subArray); $this->assertSame(\count($expected), \count($subArray)); @@ -71,16 +119,21 @@ public function dataProviderForRead(): array [[1], [], []], [[1, 2, 3], [], []], [[1], [0], [1]], + [[1], [-1], [1]], [[1], [0, 0], [1, 1]], + [[1], [0, -1], [1, 1]], [[1], [0, 0, 0], [1, 1, 1]], [[1, 2], [0], [1]], [[1, 2], [1], [2]], + [[1, 2], [-1], [2]], + [[1, 2], [-2], [1]], [[1, 2], [0, 1], [1, 2]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, 7], [2, 4, 6, 8]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], [7, 5, 3, 1], [8, 6, 4, 2]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 3, 7], [2, 6, 4, 8]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 7, 8], [1, 2, 8, 9]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 5, 5, 3], [2, 2, 6, 6, 4]], + [[1, 2, 3, 4, 5, 6, 7, 8, 9], [-1, -1, 5, 5, 3], [9, 9, 6, 6, 4]], ]; } } diff --git a/tests/unit/ArrayIndexListView/WriteTest.php b/tests/unit/ArrayIndexListView/WriteTest.php index 11c3034..681132b 100644 --- a/tests/unit/ArrayIndexListView/WriteTest.php +++ b/tests/unit/ArrayIndexListView/WriteTest.php @@ -10,11 +10,37 @@ class WriteTest extends \Codeception\Test\Unit /** * @dataProvider dataProviderForMaskSubviewWrite */ - public function testWriteByIndex(array $source, array $config, array $toWrite, array $expected) + public function testWriteByIndex(array $source, array $indexes, array $toWrite, array $expected) { $view = ArrayView::toView($source); - $view[new IndexListSelector($config)] = $toWrite; + $view[new IndexListSelector($indexes)] = $toWrite; + + $this->assertSame($expected, [...$view]); + $this->assertSame($expected, $source); + } + + /** + * @dataProvider dataProviderForMaskSubviewWrite + */ + public function testWriteByArrayIndex(array $source, array $indexes, array $toWrite, array $expected) + { + $view = ArrayView::toView($source); + + $view[$indexes] = $toWrite; + + $this->assertSame($expected, [...$view]); + $this->assertSame($expected, $source); + } + + /** + * @dataProvider dataProviderForMaskSubviewWrite + */ + public function testWriteByArrayViewIndex(array $source, array $indexes, array $toWrite, array $expected) + { + $view = ArrayView::toView($source); + + $view[ArrayView::toView($indexes)] = $toWrite; $this->assertSame($expected, [...$view]); $this->assertSame($expected, $source); diff --git a/tests/unit/ArrayMaskView/IssetTest.php b/tests/unit/ArrayMaskView/IssetTest.php index 3e711c2..123a8d7 100644 --- a/tests/unit/ArrayMaskView/IssetTest.php +++ b/tests/unit/ArrayMaskView/IssetTest.php @@ -17,57 +17,159 @@ public function testIssetSelectorTrue(array $source, array $boolMask) $view = ArrayView::toView($source); $this->assertTrue(isset($view[new MaskSelector($boolMask)])); + $this->assertTrue(isset($view[$boolMask])); + $this->assertTrue(isset($view[ArrayView::toView($boolMask)])); } /** * @dataProvider dataProviderForIssetSelectorFalse */ - public function testIssetSelectorFalse(array $source, array $indexes) + public function testIssetSelectorFalse(array $source, array $boolMask) { $view = ArrayView::toView($source); - $this->assertFalse(isset($view[new MaskSelector($indexes)])); + $this->assertFalse(isset($view[new MaskSelector($boolMask)])); + $this->assertFalse(isset($view[$boolMask])); + $this->assertFalse(isset($view[ArrayView::toView($boolMask)])); $this->expectException(SizeError::class); - $_ = $view[new MaskSelector($indexes)]; + $_ = $view[new MaskSelector($boolMask)]; } public function dataProviderForIssetSelectorTrue(): array { return [ - [[], [], []], - [[1], [0], []], - [[1, 2, 3], [0, 0, 0], []], - [[1], [1], [1]], - [[1, 2], [1, 0], [1]], - [[1, 2], [0, 1], [2]], - [[1, 2], [1, 1], [1, 2]], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 0, 1, 0, 1, 0, 1, 0], [2, 4, 6, 8]], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 1, 0, 0, 0, 0, 0, 1], [1, 2, 3, 9]], + [ + [], + [], + [], + ], + [ + [1], + [false], + [], + ], + [ + [1, 2, 3], + [false, false, false], + [], + ], + [ + [1], + [true], + [1], + ], + [ + [1, 2], + [true, false], + [1], + ], + [ + [1, 2], + [false, true], + [2], + ], + [ + [1, 2], + [true, true], + [1, 2], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [false, true, false, true, false, true, false, true, false], + [2, 4, 6, 8], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [true, true, true, false, false, false, false, false, true], + [1, 2, 3, 9], + ], ]; } public function dataProviderForIssetSelectorFalse(): array { return [ - [[], [0], []], - [[], [1], []], - [[], [0, 1], []], - [[1], [], []], - [[1], [0, 0], []], - [[1], [1, 0], []], - [[1], [1, 1, 1], []], - [[1, 2, 3], [], []], - [[1, 2, 3], [0], []], - [[1, 2, 3], [0, 0], []], - [[1, 2, 3], [0, 0, 0, 0], []], - [[1, 2, 3], [0, 0, 0, 0, 0], []], - [[1, 2, 3], [1], []], - [[1, 2, 3], [1, 1], []], - [[1, 2, 3], [1, 1, 1, 1], []], - [[1, 2, 3], [1, 1, 1, 1, 1], []], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 0, 1, 0, 1, 0, 1], [2, 4, 6, 8]], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 1, 0, 0, 0, 0, 0, 1, 0], [1, 2, 3, 9]], + [ + [], + [false], + [], + ], + [ + [], + [true], + [], + ], + [ + [], + [false, true], + [], + ], + [ + [1], + [false, false], + [], + ], + [ + [1], + [true, false], + [], + ], + [ + [1], + [true, true, true], + [], + ], + [ + [1, 2, 3], + [false], + [], + ], + [ + [1, 2, 3], + [false, false], + [], + ], + [ + [1, 2, 3], + [false, false, false, false], + [], + ], + [ + [1, 2, 3], + [false, false, false, false, false], + [], + ], + [ + [1, 2, 3], + [true], + [], + ], + [ + [1, 2, 3], + [true, true], + [], + ], + [ + [1, 2, 3], + [true, true, true, true], + [], + ], + [ + [1, 2, 3], + [true, true, true, true, true], + [], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [false, true, false, true, false, true, false, true], + [2, 4, 6, 8], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [true, true, true, false, false, false, false, false, true, false], + [1, 2, 3, 9], + ], ]; } } diff --git a/tests/unit/ArrayMaskView/ReadTest.php b/tests/unit/ArrayMaskView/ReadTest.php index ee1e5ba..0f02708 100644 --- a/tests/unit/ArrayMaskView/ReadTest.php +++ b/tests/unit/ArrayMaskView/ReadTest.php @@ -63,18 +63,102 @@ public function testReadByIndex(array $source, array $mask, array $expected) $this->assertSame($expected, $subArray); } + /** + * @dataProvider dataProviderForRead + */ + public function testReadByArrayViewIndex(array $source, array $mask, array $expected) + { + $view = ArrayView::toView($source); + $subArray = $view[ArrayView::toView($mask)]; + + $this->assertSame($expected, $subArray); + $this->assertSame(\count($expected), \count($subArray)); + + for ($i = 0; $i < \count($subArray); ++$i) { + $this->assertSame($expected[$i], $subArray[$i]); + } + + for ($i = 0; $i < \count($view); ++$i) { + $this->assertSame($source[$i], $view[$i]); + } + + $this->assertSame($source, $view->toArray()); + $this->assertSame($source, [...$view]); + $this->assertSame($expected, $subArray); + } + + /** + * @dataProvider dataProviderForRead + */ + public function testReadByArrayIndex(array $source, array $mask, array $expected) + { + $view = ArrayView::toView($source); + $subArray = $view[$mask]; + + $this->assertSame($expected, $subArray); + $this->assertSame(\count($expected), \count($subArray)); + + for ($i = 0; $i < \count($subArray); ++$i) { + $this->assertSame($expected[$i], $subArray[$i]); + } + + for ($i = 0; $i < \count($view); ++$i) { + $this->assertSame($source[$i], $view[$i]); + } + + $this->assertSame($source, $view->toArray()); + $this->assertSame($source, [...$view]); + $this->assertSame($expected, $subArray); + } + public function dataProviderForRead(): array { return [ - [[], [], []], - [[1], [0], []], - [[1, 2, 3], [0, 0, 0], []], - [[1], [1], [1]], - [[1, 2], [1, 0], [1]], - [[1, 2], [0, 1], [2]], - [[1, 2], [1, 1], [1, 2]], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 0, 1, 0, 1, 0, 1, 0], [2, 4, 6, 8]], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 1, 0, 0, 0, 0, 0, 1], [1, 2, 3, 9]], + [ + [], + [], + [], + ], + [ + [1], + [false], + [], + ], + [ + [1, 2, 3], + [false, false, false], + [], + ], + [ + [1], + [true], + [1], + ], + [ + [1, 2], + [true, false], + [1], + ], + [ + [1, 2], + [false, true], + [2], + ], + [ + [1, 2], + [true, true], + [1, 2], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [false, true, false, true, false, true, false, true, false], + [2, 4, 6, 8], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [true, true, true, false, false, false, false, false, true], + [1, 2, 3, 9], + ], ]; } } diff --git a/tests/unit/ArrayMaskView/WriteTest.php b/tests/unit/ArrayMaskView/WriteTest.php index c6358cd..ede0f00 100644 --- a/tests/unit/ArrayMaskView/WriteTest.php +++ b/tests/unit/ArrayMaskView/WriteTest.php @@ -10,11 +10,11 @@ class WriteTest extends \Codeception\Test\Unit /** * @dataProvider dataProviderForMaskSubviewWrite */ - public function testWriteByIndex(array $source, array $config, array $toWrite, array $expected) + public function testWriteByIndex(array $source, array $mask, array $toWrite, array $expected) { $view = ArrayView::toView($source); - $view[new MaskSelector($config)] = $toWrite; + $view[new MaskSelector($mask)] = $toWrite; $this->assertSame($expected, [...$view]); $this->assertSame($expected, $source); @@ -23,11 +23,37 @@ public function testWriteByIndex(array $source, array $config, array $toWrite, a /** * @dataProvider dataProviderForMaskSubviewWrite */ - public function testWriteBySubview(array $source, $config, array $toWrite, array $expected) + public function testWriteByArrayIndex(array $source, array $mask, array $toWrite, array $expected) { $view = ArrayView::toView($source); - $view->subview(new MaskSelector($config))[':'] = $toWrite; + $view[$mask] = $toWrite; + + $this->assertSame($expected, [...$view]); + $this->assertSame($expected, $source); + } + + /** + * @dataProvider dataProviderForMaskSubviewWrite + */ + public function testWriteByArrayViewIndex(array $source, array $mask, array $toWrite, array $expected) + { + $view = ArrayView::toView($source); + + $view[ArrayView::toView($mask)] = $toWrite; + + $this->assertSame($expected, [...$view]); + $this->assertSame($expected, $source); + } + + /** + * @dataProvider dataProviderForMaskSubviewWrite + */ + public function testWriteBySubview(array $source, array $mask, array $toWrite, array $expected) + { + $view = ArrayView::toView($source); + + $view->subview(new MaskSelector($mask))[':'] = $toWrite; $this->assertSame($expected, [...$view]); $this->assertSame($expected, $source); @@ -44,49 +70,49 @@ public function dataProviderForMaskSubviewWrite(): array ], [ [1], - [0], + [false], [], [1], ], [ [1, 2, 3], - [0, 0, 0], + [false, false, false], [], [1, 2, 3], ], [ [1], - [1], + [true], [2], [2], ], [ [1, 2], - [1, 0], + [true, false], [2], [2, 2], ], [ [1, 2], - [0, 1], + [false, true], [3], [1, 3], ], [ [1, 2], - [1, 1], + [true, true], [2, 3], [2, 3], ], [ [1, 2, 3, 4, 5, 6, 7, 8, 9], - [0, 1, 0, 1, 0, 1, 0, 1, 0], + [false, true, false, true, false, true, false, true, false], [3, 5, 7, 9], [1, 3, 3, 5, 5, 7, 7, 9, 9], ], [ [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 1, 1, 0, 0, 0, 0, 0, 1], + [true, true, true, false, false, false, false, false, true], [2, 3, 4, 10], [2, 3, 4, 4, 5, 6, 7, 8, 10], ], diff --git a/tests/unit/ArraySliceView/IssetTest.php b/tests/unit/ArraySliceView/IssetTest.php index b4c60cd..cd78648 100644 --- a/tests/unit/ArraySliceView/IssetTest.php +++ b/tests/unit/ArraySliceView/IssetTest.php @@ -20,7 +20,7 @@ public function testIssetSelectorObjectTrue(array $source, $slice) /** * @dataProvider dataProviderForIssetSelectorStringTrue */ - public function testIssetSelectorStringTrue(array $source, $slice) + public function testIssetSelectorStringTrue(array $source, string $slice) { $view = ArrayView::toView($source); $this->assertTrue(isset($view[$slice])); diff --git a/tests/unit/ArrayView/ErrorsTest.php b/tests/unit/ArrayView/ErrorsTest.php index 0e954cd..20fd41d 100644 --- a/tests/unit/ArrayView/ErrorsTest.php +++ b/tests/unit/ArrayView/ErrorsTest.php @@ -156,22 +156,6 @@ public function testInvalidSliceWrite(array $source, string $slice) $view[new SliceSelector($slice)] = [1, 2, 3]; } - /** - * @dataProvider dataProviderForApplyWithSizeError - */ - public function testApplyWithSizeError(array $source, callable $viewGetter, callable $mapper, array $toApplyWith) - { - $view = ArrayView::toView($source); - - $sourceSize = \count($source); - $argSize = \count($toApplyWith); - - $this->expectException(SizeError::class); - $this->expectExceptionMessage("Length of values array not equal to view length ({$argSize} != {$sourceSize})."); - - $view->applyWith($toApplyWith, $mapper); - } - /** * @dataProvider dataProviderForWriteSizeError */ @@ -237,6 +221,102 @@ public function testWriteBadIndexList(array $source, array $indexes) } } + /** + * @dataProvider dataProviderForSequentialError + */ + public function testMapWithSequentialError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->mapWith($arg, fn ($lhs, $rhs) => $lhs + $rhs); + } catch (ValueError $e) { + $this->assertSame('Argument is not sequential.', $e->getMessage()); + } + } + + /** + * @dataProvider dataProviderForSizeError + */ + public function testMapWithSizeError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->mapWith($arg, fn ($lhs, $rhs) => $lhs + $rhs); + } catch (SizeError $e) { + [$lhsSize, $rhsSize] = array_map('count', [$arg, $source]); + $this->assertSame( + "Length of values array not equal to view length ({$lhsSize} != {$rhsSize}).", + $e->getMessage() + ); + } + } + + /** + * @dataProvider dataProviderForSequentialError + */ + public function testMatchWithSequentialError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->matchWith($arg, fn ($lhs, $rhs) => $lhs && $rhs); + } catch (ValueError $e) { + $this->assertSame('Argument is not sequential.', $e->getMessage()); + } + } + + /** + * @dataProvider dataProviderForSizeError + */ + public function testMatchWithSizeError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->matchWith($arg, fn ($lhs, $rhs) => $lhs && $rhs); + } catch (SizeError $e) { + [$lhsSize, $rhsSize] = array_map('count', [$arg, $source]); + $this->assertSame( + "Length of values array not equal to view length ({$lhsSize} != {$rhsSize}).", + $e->getMessage() + ); + } + } + + /** + * @dataProvider dataProviderForSequentialError + */ + public function testApplyWithSequentialError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->applyWith($arg, fn ($lhs, $rhs) => $lhs + $rhs); + } catch (ValueError $e) { + $this->assertSame('Argument is not sequential.', $e->getMessage()); + } + } + + /** + * @dataProvider dataProviderForSizeError + */ + public function testApplyWithSizeError(array $source, array $arg) + { + $view = ArrayView::toView($source); + + try { + $view->applyWith($arg, fn ($lhs, $rhs) => $lhs && $rhs); + } catch (SizeError $e) { + [$lhsSize, $rhsSize] = array_map('count', [$arg, $source]); + $this->assertSame( + "Length of values array not equal to view length ({$lhsSize} != {$rhsSize}).", + $e->getMessage() + ); + } + } + public function dataProviderForOutOfRangeIndexes(): array { return [ @@ -251,14 +331,14 @@ public function dataProviderForBadKeys(): array return [ [[], ['a', 'b', 'c']], [[], ['1a', 'test', '!']], - [[], [[], [1, 2, 3], ['a' => 'test']], new \stdClass([])], - [[], [null, true, false, [], [1, 2, 3], ['a' => 'test']], new \stdClass([])], + [[], [['a' => 'test']], new \stdClass([])], + [[], [null, true, false, ['a' => 'test']], new \stdClass([])], [[1], ['a', 'b', 'c']], [[1], ['1a', 'test', '!']], - [[1], [null, true, false, [], [1, 2, 3], ['a' => 'test']], new \stdClass([])], + [[1], [null, true, false, ['a' => 'test']], new \stdClass([])], [[1, 2, 3], ['a', 'b', 'c']], [[1, 2, 3], ['1a', 'test', '!']], - [[2], [null, true, false, [], [1, 2, 3], ['a' => 'test']], new \stdClass([])], + [[2], [null, true, false, ['a' => 'test']], new \stdClass([])], ]; } @@ -301,42 +381,6 @@ public function dataProviderForInvalidSlice(): array ]; } - public function dataProviderForApplyWithSizeError(): array - { - return [ - [ - [], - fn (array &$source) => ArrayView::toView($source), - fn (int $item) => $item, - [1], - ], - [ - [1], - fn (array &$source) => ArrayView::toView($source), - fn (int $item) => $item, - [], - ], - [ - [1], - fn (array &$source) => ArrayView::toView($source), - fn (int $item) => $item, - [1, 2], - ], - [ - [1, 2, 3], - fn (array &$source) => ArrayView::toView($source), - fn (int $item) => $item, - [1, 2], - ], - [ - [1, 2, 3], - fn (array &$source) => ArrayView::toView($source), - fn (int $item) => $item, - [1, 2, 3, 4, 5], - ], - ]; - } - public function dataProviderForWriteSizeError(): array { return [ @@ -465,4 +509,27 @@ public function dataProviderForNonSequentialError(): array }], ]; } + + public function dataProviderForSequentialError(): array + { + return [ + [[], ['test' => 123]], + [[], [1 => 1]], + [[], [0, 2 => 1]], + [[1, 2, 3], ['test' => 123]], + [[1, 2, 3], [1 => 1]], + [[1, 2, 3], [0, 2 => 1]], + ]; + } + + public function dataProviderForSizeError(): array + { + return [ + [[], [1]], + [[], [1, 2, 3]], + [[1, 2, 3], []], + [[1, 2, 3], [1, 2]], + [[1, 2, 3], [1, 2, 3, 4]], + ]; + } } diff --git a/tests/unit/ArrayView/IndexTest.php b/tests/unit/ArrayView/IndexTest.php index 410873a..6f77c08 100644 --- a/tests/unit/ArrayView/IndexTest.php +++ b/tests/unit/ArrayView/IndexTest.php @@ -287,7 +287,7 @@ public static function dataProviderForIndexesSmallerThanThanNegativeThree(): arr * @param mixed $i * @return void */ - public function testNonIntegerIndexError($i): void + public function testNonIntegerKeyError($i): void { // Given $array = [10, 20, 30]; @@ -312,4 +312,37 @@ public static function dataProviderForNonIntegerIndexes(): array ['six'], ]; } + + /** + * @dataProvider dataProviderForFloatIndexes + * @param mixed $i + * @return void + */ + public function testNonIntegerIndexError($i): void + { + // Given + $array = [10, 20, 30]; + $arrayView = ArrayView::toView($array); + + // Then + $this->expectException(IndexError::class); + + // When + $number = $arrayView[$i]; + } + + public static function dataProviderForFloatIndexes(): array + { + return [ + [0.1], + ['0.5'], + ['1.5'], + [2.0], + [3.1], + ['45.66'], + [\NAN], + [\INF], + [-\INF], + ]; + } } diff --git a/tests/unit/ArrayView/IssetTest.php b/tests/unit/ArrayView/IssetTest.php index 2961bf4..eb7b1ee 100644 --- a/tests/unit/ArrayView/IssetTest.php +++ b/tests/unit/ArrayView/IssetTest.php @@ -2,7 +2,9 @@ namespace Smoren\ArrayView\Tests\Unit\ArrayView; -use Smoren\ArrayView\Selectors\SliceSelector; +use Smoren\ArrayView\Selectors\IndexListSelector; +use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\PipeSelector; use Smoren\ArrayView\Views\ArrayView; class IssetTest extends \Codeception\Test\Unit @@ -16,6 +18,24 @@ public function testIssetSelectorFalse(array $source, $slice) $this->assertFalse(isset($view[$slice])); } + /** + * @dataProvider dataProviderForIssetPipeSelectorTrue + */ + public function testIssetPipeSelectorTrue(array $source, array $selectors) + { + $view = ArrayView::toView($source); + $this->assertTrue(isset($view[new PipeSelector($selectors)])); + } + + /** + * @dataProvider dataProviderForIssetPipeSelectorFalse + */ + public function testIssetPipeSelectorFalse(array $source, array $selectors) + { + $view = ArrayView::toView($source); + $this->assertFalse(isset($view[new PipeSelector($selectors)])); + } + public function dataProviderForIssetSelectorFalse(): array { return [ @@ -25,10 +45,100 @@ public function dataProviderForIssetSelectorFalse(): array [[1, 2, 3, 4, 5, 6, 7, 8, 9], 1.1], [[1, 2, 3, 4, 5, 6, 7, 8, 9], INF], [[1, 2, 3, 4, 5, 6, 7, 8, 9], -INF], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6]], + [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,66]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], 'asd'], [[1, 2, 3, 4, 5, 6, 7, 8, 9], ['a' => 1]], [[1, 2, 3, 4, 5, 6, 7, 8, 9], new \ArrayObject(['a' => 1])], ]; } + + public function dataProviderForIssetPipeSelectorTrue(): array + { + return [ + [ + [1, 2, 3, 4, 5], + [ + new MaskSelector([true, false, true, false, true]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false, true]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false, true]), + new IndexListSelector([0, 1]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false, true]), + new IndexListSelector([-2]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new PipeSelector([ + new MaskSelector([true, false, true]), + new IndexListSelector([-2]), + ]), + ], + ], + ]; + } + + public function dataProviderForIssetPipeSelectorFalse(): array + { + return [ + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new MaskSelector([]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false, true]), + new IndexListSelector([0, 2]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new MaskSelector([true, false, true]), + new IndexListSelector([-3]), + ], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [ + new IndexListSelector([0, 1, 2]), + new PipeSelector([ + new MaskSelector([true, false, true]), + new IndexListSelector([-3]), + ]), + ], + ], + ]; + } } diff --git a/tests/unit/ArrayView/ReadTest.php b/tests/unit/ArrayView/ReadTest.php index b8dd5ea..6e90cb3 100644 --- a/tests/unit/ArrayView/ReadTest.php +++ b/tests/unit/ArrayView/ReadTest.php @@ -4,6 +4,7 @@ use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\PipeSelector; use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Views\ArrayIndexListView; use Smoren\ArrayView\Views\ArrayMaskView; @@ -43,23 +44,95 @@ public function testReadCombined(array $source, callable $viewGetter, array $exp } /** - * @dataProvider dataProviderForIsAndFilter + * @dataProvider dataProviderForReadPipe */ - public function testIsAndFilter(array $source, callable $predicate, array $expectedMask, array $expectedArray) + public function testReadPipe(array $source, array $selectors, array $expected) + { + $view = ArrayView::toView($source); + $selector = new PipeSelector($selectors); + + $subview = $view->subview($selector); + $subArray = $view[$selector]; + + $this->assertSame($subview->toArray(), $expected); + $this->assertSame($subArray, $expected); + $this->assertSame($selector->getValue(), $selectors); + } + + /** + * @dataProvider dataProviderForMatchAndFilter + */ + public function testMatchAndFilter(array $source, callable $predicate, array $expectedMask, array $expectedArray) { // Given $view = ArrayView::toView($source); // When $boolMask = $view->is($predicate); + $boolMaskCopy = $view->match($predicate); $filtered = $view->filter($predicate); // Then $this->assertSame($expectedMask, $boolMask->getValue()); + $this->assertSame($expectedMask, $boolMaskCopy->getValue()); $this->assertSame($expectedArray, $view->subview($boolMask)->toArray()); $this->assertSame($expectedArray, $filtered->toArray()); } + /** + * @dataProvider dataProviderForMatchWith + */ + public function testMatchWith( + array $source, + $another, + callable $comparator, + array $expectedMask, + array $expectedArray + ) { + // Given + $view = ArrayView::toView($source); + + // When + $boolMask = $view->matchWith($another, $comparator); + + // Then + $this->assertSame($expectedMask, $boolMask->getValue()); + $this->assertSame($expectedArray, $view->subview($boolMask)->toArray()); + } + + /** + * @dataProvider dataProviderForMap + */ + public function testMap( + array $source, + callable $mapper, + array $expected + ) { + // Given + $view = ArrayView::toView($source); + + // When + $actual = $view->map($mapper); + + // Then + $this->assertSame($expected, $actual); + } + + /** + * @dataProvider dataProviderForMapWith + */ + public function testMapWith(array $source, $another, callable $mapper, array $expected) + { + // Given + $view = ArrayView::toView($source); + + // When + $actual = $view->mapWith($another, $mapper); + + // Then + $this->assertSame($expected, $actual); + } + public function dataProviderForArrayRead(): array { return [ @@ -190,10 +263,178 @@ public function dataProviderForReadCombine(): array ->subview('1:'), [9], ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + ])) + ->subview(new IndexListSelector([0, 2])), + [1, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new SliceSelector('::2')) + ->subview(new PipeSelector([ + new MaskSelector([true, false, true, false, true]), + ])) + ->subview(new IndexListSelector([0, 2])), + [1, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new SliceSelector('::2')) + ->subview(new PipeSelector([ + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]) + ])), + [1, 9], + ], ]; } - public function dataProviderForIsAndFilter(): array + public function dataProviderForReadPipe(): array + { + return [ + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [new SliceSelector('::2')], + [1, 3, 5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [new SliceSelector('::2')], + [1, 3, 5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [new SliceSelector('::2')], + [1, 3, 5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new MaskSelector([true, true, true, true, true, true, true, true, true, true]), + new SliceSelector('::2'), + ], + [1, 3, 5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::1'), + new SliceSelector('::2'), + ], + [1, 3, 5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + ], + [1, 5, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]), + ], + [1, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]), + new SliceSelector('1:'), + ], + [9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new MaskSelector([true, false, true, false, true, false, true, false, true, false]), + new MaskSelector([true, false, true, false, true]), + new MaskSelector([true, false, true]), + new MaskSelector([false, true]), + ], + [9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new MaskSelector([true, false, true, false, true, false, true, false, true, false]), + new MaskSelector([true, false, true, false, true]), + new MaskSelector([true, false, true]), + ], + [1, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new IndexListSelector([0, 2, 4, 6, 8]), + new IndexListSelector([0, 2, 4]), + new IndexListSelector([0, 2]), + new IndexListSelector([1]), + ], + [9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new SliceSelector('::2'), + new SliceSelector('::2'), + ], + [1, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new SliceSelector('::2'), + new SliceSelector('::2'), + new SliceSelector('1:'), + ], + [9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new PipeSelector([ + new SliceSelector('::2'), + new SliceSelector('::2'), + ]), + new SliceSelector('1:'), + ], + [9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [ + new SliceSelector('::2'), + new PipeSelector([ + new PipeSelector([ + new SliceSelector('::2'), + new SliceSelector('::2'), + ]), + ]), + new SliceSelector('1:'), + ], + [9], + ], + ]; + } + + public function dataProviderForMatchAndFilter(): array { return [ [ @@ -222,4 +463,149 @@ public function dataProviderForIsAndFilter(): array ], ]; } + + public function dataProviderForMatchWith(): array + { + return [ + [ + [], + [], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [], + [], + ], + [ + [1], + [1], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [false], + [], + ], + [ + [1], + [2], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [true], + [1], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [1, 22, 3, 4, 5, 6, 7, 8, 99, 10], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [false, true, false, false, false, false, false, false, true, false], + [2, 9], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [1, 22, 3, 4, 5, 6, 7, 8, 99, 10], + fn (int $lhs, int $rhs) => $lhs >= $rhs, + [true, false, true, true, true, true, true, true, false, true], + [1, 3, 4, 5, 6, 7, 8, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ArrayView::toUnlinkedView([1, 22, 3, 4, 5, 6, 7, 8, 99, 10]), + fn (int $lhs, int $rhs) => $lhs >= $rhs, + [true, false, true, true, true, true, true, true, false, true], + [1, 3, 4, 5, 6, 7, 8, 10], + ], + [ + [1, 2, 3], + 1, + fn (int $lhs, int $rhs) => $lhs > $rhs, + [false, true, true], + [2, 3], + ], + ]; + } + + public function dataProviderForMap(): array + { + return [ + [ + [], + fn (int $x) => $x, + [], + ], + [ + [1], + fn (int $x) => $x, + [1], + ], + [ + [1], + fn (int $x) => $x * 2, + [2], + ], + [ + [1, 2, 3], + fn (int $x) => $x + 1, + [2, 3, 4], + ], + [ + [1, 2, 3], + fn (int $x) => [$x + 1], + [[2], [3], [4]], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (int $_, int $i) => $i % 2 === 0, + [true, false, true, false, true, false, true, false, true, false], + ], + ]; + } + + public function dataProviderForMapWith(): array + { + return [ + [ + [], + [], + fn (int $lhs, int $rhs) => $rhs + $lhs, + [], + ], + [ + [1], + [2], + fn (int $lhs, int $rhs) => $rhs + $lhs, + [3], + ], + [ + [1], + [2], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [true], + ], + [ + [1, 2, 3], + [10, 20, 30], + fn (int $lhs, int $rhs) => $rhs + $lhs, + [11, 22, 33], + ], + [ + [1, 2, 3], + [10, 20, 30], + fn (int $lhs, int $rhs) => [$lhs, $rhs], + [[1, 10], [2, 20], [3, 30]], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [1, 22, 3, 4, 5, 6, 7, 8, 99, 10], + fn (int $lhs, int $rhs) => $rhs > $lhs, + [false, true, false, false, false, false, false, false, true, false], + ], + [ + [1, 2, 3], + 10, + fn (int $lhs, int $rhs) => $rhs + $lhs, + [11, 12, 13], + ], + [ + [1, 2, 3], + ArrayView::toUnlinkedView([10, 20, 30]), + fn (int $lhs, int $rhs) => [$lhs, $rhs], + [[1, 10], [2, 20], [3, 30]], + ], + ]; + } } diff --git a/tests/unit/ArrayView/WriteTest.php b/tests/unit/ArrayView/WriteTest.php index 17d6ce3..4e50a20 100644 --- a/tests/unit/ArrayView/WriteTest.php +++ b/tests/unit/ArrayView/WriteTest.php @@ -4,6 +4,7 @@ use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\PipeSelector; use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Structs\Slice; use Smoren\ArrayView\Views\ArrayView; @@ -106,6 +107,7 @@ public function testIncrement(array $source, array $expected) /** * @dataProvider dataProviderForWriteCombine + * @dataProvider dataProviderForWritePipe */ public function testWriteBySet(array $source, callable $viewGetter, $toWrite, array $expected) { @@ -118,6 +120,7 @@ public function testWriteBySet(array $source, callable $viewGetter, $toWrite, ar /** * @dataProvider dataProviderForWriteCombine + * @dataProvider dataProviderForWritePipe */ public function testWriteBySlice(array $source, callable $viewGetter, $toWrite, array $expected) { @@ -146,7 +149,7 @@ public function testApply(array $source, callable $viewGetter, callable $mapper, /** * @dataProvider dataProviderForApplyWith */ - public function testApplyWith(array $source, callable $viewGetter, callable $mapper, array $arg, array $expected) + public function testApplyWith(array $source, callable $viewGetter, callable $mapper, $arg, array $expected) { // Given $view = $viewGetter($source); @@ -281,6 +284,142 @@ public function dataProviderForWriteCombine(): array 111, [111, 2, 3, 4, 5, 6, 7, 8, 111, 10], ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + ])) + ->subview(new IndexListSelector([0, 2])), + [11, 99], + [11, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new SliceSelector('::2')) + ->subview(new PipeSelector([ + new MaskSelector([true, false, true, false, true]), + ])) + ->subview(new IndexListSelector([0, 2])), + [11, 99], + [11, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source) + ->subview(new SliceSelector('::2')) + ->subview(new PipeSelector([ + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]), + ])), + [11, 99], + [11, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + ]; + } + + public function dataProviderForWritePipe(): array + { + return [ + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector('::2'), + ]) + ), + [11, 33, 55, 77, 99], + [11, 2, 33, 4, 55, 6, 77, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + ]) + ), + [11, 55, 99], + [11, 2, 3, 4, 55, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]), + ]) + ), + [11, 99], + [11, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, false, true]), + new IndexListSelector([0, 2]), + new SliceSelector('1:'), + ]) + ), + [99], + [1, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new MaskSelector([true, false, true, false, true, false, true, false, true, false]), + new MaskSelector([true, false, true, false, true]), + new MaskSelector([true, false, true]), + new MaskSelector([false, true]), + ]) + ), + [99], + [1, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new IndexListSelector([0, 2, 4, 6, 8]), + new IndexListSelector([0, 2, 4]), + new IndexListSelector([0, 2]), + new IndexListSelector([1]), + ]) + ), + [99], + [1, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector('::2'), + new SliceSelector('::2'), + new SliceSelector('::2'), + new SliceSelector('1:'), + ]) + ), + [99], + [1, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview( + new PipeSelector([ + new SliceSelector(new Slice(null, null, 2)), + new SliceSelector('::2'), + new SliceSelector('::2'), + ]) + ), + [11, 99], + [11, 2, 3, 4, 5, 6, 7, 8, 99, 10], + ], ]; } @@ -380,6 +519,13 @@ public function dataProviderForApplyWith(): array [1, 2, 3, 4, 5], [1, 2, 6, 4, 15, 6, 28, 8, 45, 10], ], + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + fn (array &$source) => ArrayView::toView($source)->subview('::2'), + fn (int $lhs, int $rhs) => $lhs * $rhs, + 10, + [10, 2, 30, 4, 50, 6, 70, 8, 90, 10], + ], ]; } } diff --git a/tests/unit/Examples/BenchTest.php b/tests/unit/Examples/BenchTest.php new file mode 100644 index 0000000..07a5984 --- /dev/null +++ b/tests/unit/Examples/BenchTest.php @@ -0,0 +1,109 @@ +n; + $originalArray = range(0, $n); + $indexes = range(0, $n, 2); + + $ts = \microtime(true); + $view = ArrayView::toView($originalArray); + $result = $view[$indexes]; + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testReadIndexListView] SPENT: {$spent} s\n"; + ob_flush(); + } + + public function testReadIndexListPure() + { + $n = $this->n; + $originalArray = range(0, $n); + $indexes = range(0, $n, 2); + $n_2 = \count($indexes); + + $ts = \microtime(true); + $result = []; + for ($i = 0; $i < $n_2; $i++) { + $result[$i] = $originalArray[$indexes[$i]]; + } + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testReadIndexListPure] SPENT: {$spent} s\n"; + ob_flush(); + } + + public function testReadSliceView() + { + $n = $this->n; + $originalArray = range(0, $n); + + $ts = \microtime(true); + $view = ArrayView::toView($originalArray); + $result = $view['::2']; + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testReadSliceView] SPENT: {$spent} s\n"; + ob_flush(); + } + + public function testReadSlicePure() + { + $n = $this->n; + $n_2 = intval($n / 2); + $originalArray = range(0, $n); + + $ts = \microtime(true); + $result = []; + for ($i = 0; $i < $n_2; $i++) { + $result[$i] = $originalArray[$i * 2]; + } + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testReadSlicePure] SPENT: {$spent} s\n"; + ob_flush(); + } + + public function testWriteSliceView() + { + $n = $this->n; + $n_2 = intval($n / 2); + $originalArray = range(0, $n); + $toWrite = range(0, $n_2); + + $ts = \microtime(true); + $view = ArrayView::toView($originalArray); + $view['::2'] = $toWrite; + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testWriteSliceView] SPENT: {$spent} s\n"; + ob_flush(); + } + + public function testWriteSlicePure() + { + $n = $this->n; + $n_2 = intval($n / 2); + $originalArray = range(0, $n); + $toWrite = range(0, $n_2); + + $ts = \microtime(true); + for ($i = 0; $i < $n_2; $i++) { + $originalArray[$i * 2] = $toWrite[$i]; + } + $spent = \round(\microtime(true) - $ts, 4); + + echo " [testWriteSlicePure] SPENT: {$spent} s\n"; + ob_flush(); + } +} diff --git a/tests/unit/Examples/ExamplesTest.php b/tests/unit/Examples/ExamplesTest.php index 0ae6cbe..22601be 100644 --- a/tests/unit/Examples/ExamplesTest.php +++ b/tests/unit/Examples/ExamplesTest.php @@ -6,6 +6,7 @@ use Smoren\ArrayView\Selectors\IndexListSelector; use Smoren\ArrayView\Selectors\MaskSelector; +use Smoren\ArrayView\Selectors\PipeSelector; use Smoren\ArrayView\Selectors\SliceSelector; use Smoren\ArrayView\Views\ArrayView; @@ -22,6 +23,8 @@ public function testSlicing() $this->assertSame(3, $originalView[2]); $this->assertSame(5, $originalView[4]); + $this->assertSame(9, $originalView[-1]); + $this->assertSame(8, $originalView[-2]); $originalView['1:7:2'] = [22, 44, 66]; $this->assertSame([1, 22, 3, 44, 5, 66, 7, 8, 9], $originalArray); @@ -44,6 +47,15 @@ public function testSubview() [5, 4, 3, 2, 1], $originalView->subview(new SliceSelector('::-1'))->toArray(), ); + + $this->assertSame( + [1, 3, 5], + $originalView->subview([true, false, true, false, true])->toArray(), + ); + $this->assertSame( + [2, 3, 5], + $originalView->subview([1, 2, 4])->toArray(), + ); $this->assertSame( [5, 4, 3, 2, 1], $originalView->subview('::-1')->toArray(), @@ -72,6 +84,15 @@ public function testSubarray() [5, 4, 3, 2, 1], $originalView[new SliceSelector('::-1')], ); + + $this->assertSame( + [1, 3, 5], + $originalView[[true, false, true, false, true]], + ); + $this->assertSame( + [2, 3, 5], + $originalView[[1, 2, 4]], + ); $this->assertSame( [5, 4, 3, 2, 1], $originalView['::-1'], @@ -87,10 +108,75 @@ public function testCombinedSubview() $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; $subview = ArrayView::toView($originalArray) - ->subview('::2') // [1, 3, 5, 7, 9] + ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] - ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] - ->subview('1:'); // [5, 7] + ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] + ->subview(new SliceSelector('1:')); // [5, 7] + + $this->assertSame([5, 7], $subview->toArray()); + $this->assertSame([5, 7], $subview[':']); + + $subview[':'] = [55, 77]; + $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); + } + + public function testCombinedSubviewShort() + { + $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + $subview = ArrayView::toView($originalArray) + ->subview('::2') // [1, 3, 5, 7, 9] + ->subview([true, false, true, true, true]) // [1, 5, 7, 9] + ->subview([0, 1, 2]) // [1, 5, 7] + ->subview('1:'); // [5, 7] + + $this->assertSame([5, 7], $subview->toArray()); + $this->assertSame([5, 7], $subview[':']); + + $subview[':'] = [55, 77]; + $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); + } + + public function testSelectorsPipe() + { + $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + $selector = new PipeSelector([ + new SliceSelector('::2'), + new MaskSelector([true, false, true, true, true]), + new IndexListSelector([0, 1, 2]), + new SliceSelector('1:'), + ]); + + $view = ArrayView::toView($originalArray); + $this->assertTrue(isset($view[$selector])); + + $subview = $view->subview($selector); + + $this->assertSame([5, 7], $subview->toArray()); + $this->assertSame([5, 7], $subview[':']); + + $subview[':'] = [55, 77]; + $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); + } + + public function testSelectorsPipeNested() + { + $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + $selector = new PipeSelector([ + new SliceSelector('::2'), + new PipeSelector([ + new MaskSelector([true, false, true, true, true]), + new IndexListSelector([0, 1, 2]), + ]), + new SliceSelector('1:'), + ]); + + $view = ArrayView::toView($originalArray); + $this->assertTrue(isset($view[$selector])); + + $subview = $view->subview($selector); $this->assertSame([5, 7], $subview->toArray()); $this->assertSame([5, 7], $subview[':']); @@ -98,4 +184,265 @@ public function testCombinedSubview() $subview[':'] = [55, 77]; $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); } + + public function testMap() + { + $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $subview = ArrayView::toView($source)->subview('::2'); + + $actual = $subview->map(fn ($x) => $x * 10); + $this->assertSame([10, 30, 50, 70, 90], $actual); + } + + public function testMapWith() + { + $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $subview = ArrayView::toView($source)->subview('::2'); + $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); + + $data = [9, 27, 45, 63, 81]; + + $actual = $subview->mapWith($data, fn ($lhs, $rhs) => $lhs + $rhs); + $this->assertSame([10, 30, 50, 70, 90], $actual); + } + + public function testIs() + { + $source = [1, 2, 3, 4, 5, 6]; + $view = ArrayView::toView($source); + + $mask = $view->is(fn ($x) => $x % 2 === 0); + $this->assertSame([false, true, false, true, false, true], $mask->getValue()); + + $this->assertSame([2, 4, 6], $view->subview($mask)->toArray()); + $this->assertSame([2, 4, 6], $view[$mask]); + + $view[$mask] = [20, 40, 60]; + $this->assertSame([1, 20, 3, 40, 5, 60], $source); + } + + public function testMatch() + { + $source = [1, 2, 3, 4, 5, 6]; + $view = ArrayView::toView($source); + + $mask = $view->match(fn ($x) => $x % 2 === 0); + $this->assertSame([false, true, false, true, false, true], $mask->getValue()); + + $this->assertSame([2, 4, 6], $view->subview($mask)->toArray()); + $this->assertSame([2, 4, 6], $view[$mask]); + + $view[$mask] = [20, 40, 60]; + $this->assertSame([1, 20, 3, 40, 5, 60], $source); + } + + public function testMatchWith() + { + $source = [1, 2, 3, 4, 5, 6]; + $view = ArrayView::toView($source); + + $data = [6, 5, 4, 3, 2, 1]; + + $mask = $view->matchWith($data, fn ($lhs, $rhs) => $lhs > $rhs); + $this->assertSame([false, false, false, true, true, true], $mask->getValue()); + + $this->assertSame([4, 5, 6], $view->subview($mask)->toArray()); + $this->assertSame([4, 5, 6], $view[$mask]); + + $view[$mask] = [40, 50, 60]; + $this->assertSame([1, 2, 3, 40, 50, 60], $source); + } + + public function testApply() + { + $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $subview = ArrayView::toView($source)->subview('::2'); + + $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); + + $subview->apply(fn ($x) => $x * 10); + + $this->assertSame([10, 30, 50, 70, 90], $subview->toArray()); + $this->assertSame([10, 2, 30, 4, 50, 6, 70, 8, 90, 10], $source); + } + + public function testApplyWith() + { + $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $subview = ArrayView::toView($source)->subview('::2'); + + $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); + + $data = [9, 27, 45, 63, 81]; + + $subview->applyWith($data, fn ($lhs, $rhs) => $lhs + $rhs); + $this->assertSame([10, 30, 50, 70, 90], $subview->toArray()); + + $this->assertSame([10, 2, 30, 4, 50, 6, 70, 8, 90, 10], $source); + } + + public function testFilter() + { + $source = [1, 2, 3, 4, 5, 6]; + $view = ArrayView::toView($source); + + $filtered = $view->filter(fn ($x) => $x % 2 === 0); + $this->assertSame([2, 4, 6], $filtered->toArray()); + + $filtered[':'] = [20, 40, 60]; + $this->assertSame([20, 40, 60], $filtered->toArray()); + + $this->assertSame([1, 20, 3, 40, 5, 60], $source); + } + + public function testCount() + { + $source = [1, 2, 3, 4, 5]; + + $subview = ArrayView::toView($source)->subview('::2'); + + $this->assertSame([1, 3, 5], $subview->toArray()); + $this->assertCount(3, $subview); + } + + public function testIterator() + { + $source = [1, 2, 3, 4, 5]; + $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] + + $actual = []; + foreach ($subview as $item) { + $actual[] = $item; + // 1, 3, 5 + } + $this->assertSame([1, 3, 5], $actual); + $this->assertSame([1, 3, 5], [...$subview]); + } + + public function testIsReadonly() + { + $source = [1, 2, 3, 4, 5]; + + $readonlyView = ArrayView::toView($source, true); + $this->assertTrue($readonlyView->isReadonly()); + + $readonlySubview = ArrayView::toView($source)->subview('::2', true); + $this->assertTrue($readonlySubview->isReadonly()); + + $view = ArrayView::toView($source); + $this->assertFalse($view->isReadonly()); + + $subview = ArrayView::toView($source)->subview('::2'); + $this->assertFalse($subview->isReadonly()); + } + + public function testOffsetExists() + { + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $this->assertTrue(isset($view[0])); + $this->assertTrue(isset($view[-1])); + $this->assertFalse(isset($view[10])); + + $this->assertTrue(isset($view[new SliceSelector('::2')])); + $this->assertTrue(isset($view[new IndexListSelector([0, 2, 4])])); + $this->assertFalse(isset($view[new IndexListSelector([0, 2, 10])])); + $this->assertTrue(isset($view[new MaskSelector([true, true, false, false, true])])); + $this->assertFalse(isset($view[new MaskSelector([true, true, false, false, true, true])])); + + $this->assertTrue(isset($view['::2'])); + $this->assertTrue(isset($view[[0, 2, 4]])); + $this->assertFalse(isset($view[[0, 2, 10]])); + $this->assertTrue(isset($view[[true, true, false, false, true]])); + $this->assertFalse(isset($view[[true, true, false, false, true, true]])); + } + + public function testOffsetGet() + { + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $this->assertSame(1, $view[0]); + $this->assertSame(5, $view[-1]); + + $this->assertSame([1, 3, 5], $view[new SliceSelector('::2')]); + $this->assertSame([1, 3, 5], $view[new IndexListSelector([0, 2, 4])]); + $this->assertSame([1, 2, 5], $view[new MaskSelector([true, true, false, false, true])]); + + $this->assertSame([1, 3, 5], $view['::2']); + $this->assertSame([1, 3, 5], $view[[0, 2, 4]]); + $this->assertSame([1, 2, 5], $view[[true, true, false, false, true]]); + } + + public function testOffsetSet() + { + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $view[0] = 11; + $view[-1] = 55; + + $this->assertSame([11, 2, 3, 4, 55], $source); + + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $view[new SliceSelector('::2')] = [11, 33, 55]; + $this->assertSame([11, 2, 33, 4, 55], $source); + + $view[new IndexListSelector([1, 3])] = [22, 44]; + $this->assertSame([11, 22, 33, 44, 55], $source); + + $view[new MaskSelector([true, false, false, false, true])] = [111, 555]; + $this->assertSame([111, 22, 33, 44, 555], $source); + + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $view['::2'] = [11, 33, 55]; + $this->assertSame([11, 2, 33, 4, 55], $source); + + $view[[1, 3]] = [22, 44]; + $this->assertSame([11, 22, 33, 44, 55], $source); + + $view[[true, false, false, false, true]] = [111, 555]; + $this->assertSame([111, 22, 33, 44, 555], $source); + } + + public function testSet() + { + $source = [1, 2, 3, 4, 5]; + $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] + + $subview->set([11, 33, 55]); + $this->assertSame([11, 33, 55], $subview->toArray()); + $this->assertSame([11, 2, 33, 4, 55], $source); + } + + public function testToUnlinkedView() + { + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toUnlinkedView($source); + + $this->assertSame(1, $view[0]); + $this->assertSame([2, 4], $view['1::2']); + $view['1::2'] = [22, 44]; + + $this->assertSame([1, 22, 3, 44, 5], $view->toArray()); + $this->assertSame([1, 2, 3, 4, 5], $source); + } + + public function testToView() + { + $source = [1, 2, 3, 4, 5]; + $view = ArrayView::toView($source); + + $this->assertSame(1, $view[0]); + $this->assertSame([2, 4], $view['1::2']); + $view['1::2'] = [22, 44]; + + $this->assertSame([1, 22, 3, 44, 5], $view->toArray()); + $this->assertSame([1, 22, 3, 44, 5], $source); + } }