├── .github └── workflows │ ├── docs.yml │ └── test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── docs └── images │ └── logo.png ├── src ├── Exceptions │ ├── IndexError.php │ ├── KeyError.php │ ├── NotSupportedError.php │ ├── ReadonlyError.php │ ├── SizeError.php │ └── ValueError.php ├── Interfaces │ ├── ArraySelectorInterface.php │ ├── ArrayViewInterface.php │ ├── IndexListSelectorInterface.php │ ├── MaskSelectorInterface.php │ ├── PipeSelectorInterface.php │ └── SliceSelectorInterface.php ├── Selectors │ ├── IndexListSelector.php │ ├── MaskSelector.php │ ├── PipeSelector.php │ └── SliceSelector.php ├── Structs │ ├── NormalizedSlice.php │ └── Slice.php ├── Traits │ ├── ArrayViewAccessTrait.php │ └── ArrayViewOperationsTrait.php ├── Util.php └── Views │ ├── ArrayIndexListView.php │ ├── ArrayMaskView.php │ ├── ArraySliceView.php │ └── ArrayView.php └── tests ├── _bootstrap.php ├── _support └── UnitTester.php ├── coding_standard.xml ├── unit.suite.yml └── unit ├── ArrayIndexListView ├── IssetTest.php ├── ReadTest.php └── WriteTest.php ├── ArrayMaskView ├── IssetTest.php ├── ReadTest.php └── WriteTest.php ├── ArraySliceView ├── IssetTest.php ├── ReadTest.php └── WriteTest.php ├── ArrayView ├── ErrorsTest.php ├── IndexTest.php ├── IssetTest.php ├── ReadTest.php ├── ReadonlyTest.php ├── SliceTest.php └── WriteTest.php ├── Examples ├── BenchTest.php └── ExamplesTest.php ├── Fixtures └── PythonDataProviders.php ├── Structs ├── NormalizedSliceTest.php └── SliceTest.php └── Utils ├── IsArraySequentialTest.php └── NormalizeIndexTest.php /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | name: Deploy docs 21 | runs-on: ubuntu-latest 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Generate API docs 32 | run: docker run --rm -v $(pwd):/data phpdoc/phpdoc:3 --directory ./src --visibility public --target ./phpdoc --template default -v 33 | 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v4 36 | 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: './phpdoc' 41 | 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | php: ['7.4', '8.0', '8.1', '8.2', '8.3'] 19 | 20 | steps: 21 | - name: Set up PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | coverage: xdebug 26 | tools: composer:v2 27 | 28 | - name: Checkout code 29 | uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: PHP Version Check 34 | run: php -v 35 | 36 | - name: Validate Composer JSON 37 | run: composer validate 38 | 39 | - name: Run Composer 40 | run: composer install --no-interaction 41 | 42 | - name: Unit tests 43 | run: | 44 | composer test-init 45 | composer test 46 | 47 | - name: PHP Code Sniffer 48 | run: composer codesniffer 49 | 50 | - name: PHPStan analysis 51 | run: composer stan 52 | 53 | code-coverage: 54 | name: Code coverage 55 | runs-on: ubuntu-latest 56 | strategy: 57 | matrix: 58 | php: ['7.4'] 59 | 60 | steps: 61 | - name: Set up PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: ${{ matrix.php }} 65 | coverage: xdebug 66 | tools: composer:v2 67 | 68 | - name: Checkout code 69 | uses: actions/checkout@v3 70 | with: 71 | fetch-depth: 0 72 | 73 | - name: Run Composer 74 | run: composer install --no-interaction 75 | 76 | - name: Unit tests 77 | run: | 78 | composer test-init 79 | composer test-coverage-xml 80 | mkdir -p ./build/logs 81 | cp ./tests/_output/coverage.xml ./build/logs/clover.xml 82 | - name: Code Coverage (Coveralls) 83 | env: 84 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | run: php vendor/bin/php-coveralls -v 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | .phpdoc 4 | phpdoc 5 | tests/_output 6 | tests/_support/_generated 7 | composer.lock 8 | composer.phar 9 | build 10 | g 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Required to run your project under the correct environment 2 | language: php 3 | 4 | # Versions of PHP you want your project run with 5 | php: 6 | - 7.4 7 | - 8.0 8 | - 8.1 9 | 10 | # fast_finish: If your build fails do not continue trying to build, just stop. 11 | matrix: 12 | fast_finish: true 13 | include: 14 | - php: 7.4 15 | - php: 8.0 16 | - php: 8.1 17 | 18 | # Update composer 19 | before-install: 20 | - composer self-update 21 | 22 | # Install composer dependencies, init codeception 23 | install: 24 | - composer install --no-interaction --dev 25 | - composer test-init 26 | 27 | # Run script 28 | script: 29 | - composer test 30 | - composer codesniffer 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Smoren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Array View PHP 2 | ![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/smoren/array-view) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Smoren/array-view-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Smoren/array-view-php/?branch=master) 4 | [![Coverage Status](https://coveralls.io/repos/github/Smoren/array-view-php/badge.svg?branch=master)](https://coveralls.io/github/Smoren/array-view-php?branch=master) 5 | ![Build and test](https://github.com/Smoren/array-view-php/actions/workflows/test.yml/badge.svg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ![Array View Logo](docs/images/logo.png) 9 | 10 | **Array View** is a PHP library that provides powerful abstractions and utilities for working with lists of data. 11 | Create views of arrays, slice and index using Python-like notation, transform and select your data using chained and 12 | fluent operations. 13 | 14 | ## Features 15 | - Array views as an abstraction over an array 16 | - Forward and backward array indexing 17 | - Selecting and slicing using [Python-like slice notation](https://www.geeksforgeeks.org/python-list-slicing/) 18 | - Filtering, mapping, matching and masking 19 | - Chaining operations via pipes and fluent interfaces 20 | 21 | ## How to install to your project 22 | ```bash 23 | composer require smoren/array-view 24 | ``` 25 | 26 | ## Usage 27 | ### Indexing 28 | 29 | Index into an array forward or backwards using positive or negative indexes. 30 | 31 | | Data | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | | ---------------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 33 | | *Positive Index* | *0* | *1* | *2* | *3* | *4* | *5* | *6* | 34 | | *Negative Index* | *-7* | *-6* | *-5* | *-4* | *-3* | *-2* | *-1* | 35 | ```php 36 | use Smoren\ArrayView\Views\ArrayView; 37 | 38 | $view = ArrayView::toView([1, 2, 3, 4, 5, 6, 7]); 39 | 40 | $view[0]; // 1 41 | $view[1]; // 2 42 | $view[-1]; // 7 43 | $view[-2]; // 6 44 | ``` 45 | 46 | ### Slices 47 | 48 | Use [Python-like slice notation](https://www.geeksforgeeks.org/python-list-slicing/) to select a range of elements: `[start, stop, step]`. 49 | ```php 50 | use Smoren\ArrayView\Views\ArrayView; 51 | 52 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 53 | $view = ArrayView::toView($originalArray); 54 | 55 | $view['1:6']; // [2, 3, 4, 5, 6] 56 | $view['1:7:2']; // [2, 4, 6] 57 | $view[':3']; // [1, 2, 3] 58 | $view['::-1']; // [9, 8, 7, 6, 5, 4, 3, 2, 1] 59 | ``` 60 | 61 | Insert into parts of the array. 62 | ```php 63 | $view['1:7:2'] = [22, 44, 66]; 64 | print_r($originalArray); // [1, 22, 3, 44, 5, 66, 7, 8, 9] 65 | ``` 66 | 67 | ### Subviews 68 | 69 | Create subviews of the original view using masks, indexes, and slices. 70 | ```php 71 | use Smoren\ArrayView\Selectors\IndexListSelector; 72 | use Smoren\ArrayView\Selectors\MaskSelector; 73 | use Smoren\ArrayView\Selectors\SliceSelector; 74 | use Smoren\ArrayView\Views\ArrayView; 75 | 76 | $originalArray = [1, 2, 3, 4, 5]; 77 | $view = ArrayView::toView($originalArray); 78 | 79 | // Object-oriented style 80 | $view->subview(new MaskSelector([true, false, true, false, true]))->toArray(); // [1, 3, 5] 81 | $view->subview(new IndexListSelector([1, 2, 4]))->toArray(); // [2, 3, 5] 82 | $view->subview(new SliceSelector('::-1'))->toArray(); // [5, 4, 3, 2, 1] 83 | 84 | // Scripting style 85 | $view->subview([true, false, true, false, true])->toArray(); // [1, 3, 5] 86 | $view->subview([1, 2, 4])->toArray(); // [2, 3, 5] 87 | $view->subview('::-1')->toArray(); // [5, 4, 3, 2, 1] 88 | 89 | $view->subview(new MaskSelector([true, false, true, false, true]))->apply(fn ($x) => x * 10); 90 | print_r($originalArray); // [10, 2, 30, 4, 50] 91 | ``` 92 | 93 | ### Subarray Multi-indexing 94 | 95 | Directly select multiple elements using an array-index multi-selection. 96 | ```php 97 | use Smoren\ArrayView\Selectors\IndexListSelector; 98 | use Smoren\ArrayView\Selectors\MaskSelector; 99 | use Smoren\ArrayView\Selectors\SliceSelector; 100 | use Smoren\ArrayView\Views\ArrayView; 101 | 102 | $originalArray = [1, 2, 3, 4, 5]; 103 | $view = ArrayView::toView($originalArray); 104 | 105 | // Object-oriented style 106 | $view[new MaskSelector([true, false, true, false, true])]; // [1, 3, 5] 107 | $view[new IndexListSelector([1, 2, 4])]; // [2, 3, 5] 108 | $view[new SliceSelector('::-1')]; // [5, 4, 3, 2, 1] 109 | 110 | // Scripting style 111 | $view[[true, false, true, false, true]]; // [1, 3, 5] 112 | $view[[1, 2, 4]]; // [2, 3, 5] 113 | $view['::-1']; // [5, 4, 3, 2, 1] 114 | 115 | $view[new MaskSelector([true, false, true, false, true])] = [10, 30, 50]; 116 | print_r($originalArray); // [10, 2, 30, 4, 50] 117 | ``` 118 | 119 | ### Combining Subviews 120 | 121 | Combine and chain subviews one after another in a fluent interface to perform multiple selection operations. 122 | ```php 123 | use Smoren\ArrayView\Selectors\IndexListSelector; 124 | use Smoren\ArrayView\Selectors\MaskSelector; 125 | use Smoren\ArrayView\Selectors\SliceSelector; 126 | use Smoren\ArrayView\Views\ArrayView; 127 | 128 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 129 | 130 | // Fluent object-oriented style 131 | $subview = ArrayView::toView($originalArray) 132 | ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] 133 | ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] 134 | ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] 135 | ->subview(new SliceSelector('1:')); // [5, 7] 136 | 137 | $subview[':'] = [55, 77]; 138 | print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 139 | 140 | // Fluent scripting style 141 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 142 | $subview = ArrayView::toView($originalArray) 143 | ->subview('::2') // [1, 3, 5, 7, 9] 144 | ->subview([true, false, true, true, true]) // [1, 5, 7, 9] 145 | ->subview([0, 1, 2]) // [1, 5, 7] 146 | ->subview('1:'); // [5, 7] 147 | 148 | $subview[':'] = [55, 77]; 149 | print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 150 | ``` 151 | 152 | ### Selectors Pipe 153 | 154 | Create pipelines of selections that can be saved and applied again and again to new array views. 155 | ```php 156 | use Smoren\ArrayView\Selectors\IndexListSelector; 157 | use Smoren\ArrayView\Selectors\MaskSelector; 158 | use Smoren\ArrayView\Selectors\SliceSelector; 159 | use Smoren\ArrayView\Views\ArrayView; 160 | 161 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 162 | $selector = new PipeSelector([ 163 | new SliceSelector('::2'), 164 | new MaskSelector([true, false, true, true, true]), 165 | new IndexListSelector([0, 1, 2]), 166 | new SliceSelector('1:'), 167 | ]); 168 | 169 | $view = ArrayView::toView($originalArray); 170 | $subview = $view->subview($selector); 171 | print_r($subview[':']); // [5, 7] 172 | 173 | $subview[':'] = [55, 77]; 174 | print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 175 | ``` 176 | 177 | ## Documentation 178 | For detailed documentation and usage examples, please refer to the 179 | [API documentation](https://smoren.github.io/array-view-php/packages/Application.html). 180 | 181 | ## Unit testing 182 | ``` 183 | composer install 184 | composer test-init 185 | composer test 186 | ``` 187 | 188 | ## Contributing 189 | Contributions are welcome! Feel free to open an issue or submit a pull request on the [GitHub repository](https://github.com/Smoren/array-view-php). 190 | 191 | ## Standards 192 | ArrayView conforms to the following standards: 193 | 194 | * PSR-1 — [Basic coding standard](https://www.php-fig.org/psr/psr-1/) 195 | * PSR-4 — [Autoloader](https://www.php-fig.org/psr/psr-4/) 196 | * PSR-12 — [Extended coding style guide](https://www.php-fig.org/psr/psr-12/) 197 | 198 | ## License 199 | ArrayView PHP is licensed under the MIT License. 200 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | bootstrap: _bootstrap.php 3 | paths: 4 | tests: tests 5 | log: tests/_output 6 | output: tests/_output 7 | data: tests/_data 8 | helpers: tests/_support 9 | settings: 10 | memory_limit: 1024M 11 | colors: true 12 | coverage: 13 | #c3_url: http://localhost:8080/index-test.php/ 14 | enabled: true 15 | show_uncovered: false 16 | include: 17 | - src/* 18 | exclude: 19 | - vendor/* 20 | - tests/* 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smoren/array-view", 3 | "description": "Create array views for easy data manipulation, select elements using Python-like slice notation, enable efficient selection of elements using index lists and boolean masks.", 4 | "keywords": [ 5 | "array-view", 6 | "slice", 7 | "slicing", 8 | "array", 9 | "array-index", 10 | "indexing", 11 | "negative-indexes", 12 | "negative", 13 | "range", 14 | "selector", 15 | "collection", 16 | "collections", 17 | "python-like", 18 | "array-viewer", 19 | "view", 20 | "data-view" 21 | ], 22 | "license": "MIT", 23 | "authors": [ 24 | { 25 | "name": "Smoren", 26 | "email": "ofigate@gmail.com" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=7.4" 31 | }, 32 | "require-dev": { 33 | "codeception/codeception": "^4.2.1", 34 | "codeception/module-asserts": "^2.0", 35 | "php-coveralls/php-coveralls": "^2.0", 36 | "squizlabs/php_codesniffer": "3.*", 37 | "phpstan/phpstan": "^1.8", 38 | "phpdocumentor/phpdocumentor": "3.0.*" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Smoren\\ArrayView\\": "src", 43 | "Smoren\\ArrayView\\Tests\\Unit\\": "tests/unit" 44 | } 45 | }, 46 | "config": { 47 | "fxp-asset": { 48 | "enabled": false 49 | }, 50 | "allow-plugins": { 51 | "symfony/flex": true 52 | } 53 | }, 54 | "repositories": [ 55 | { 56 | "type": "composer", 57 | "url": "https://asset-packagist.org" 58 | } 59 | ], 60 | "scripts": { 61 | "test-init": ["./vendor/bin/codecept build"], 62 | "test-all": ["composer test-coverage", "composer codesniffer", "composer stan"], 63 | "test": ["./vendor/bin/codecept run unit tests/unit"], 64 | "test-coverage": ["./vendor/bin/codecept run unit tests/unit --coverage"], 65 | "test-coverage-html": ["./vendor/bin/codecept run unit tests/unit --coverage-html"], 66 | "test-coverage-xml": ["./vendor/bin/codecept run unit tests/unit --coverage-xml"], 67 | "codesniffer": ["./vendor/bin/phpcs --ignore=vendor,tests --standard=tests/coding_standard.xml -s ."], 68 | "stan": ["./vendor/bin/phpstan analyse -l 9 src"], 69 | "phpdoc": [ 70 | "export COMPOSER_PROCESS_TIMEOUT=9000", 71 | "vendor/bin/phpdoc --directory ./src --visibility public --target ./phpdoc --template default -v" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smoren/array-view-php/d253446271d9052b32cfa6539bdbd81fed1777b3/docs/images/logo.png -------------------------------------------------------------------------------- /src/Exceptions/IndexError.php: -------------------------------------------------------------------------------- 1 | $source The source array view to select elements from. 18 | * @param bool|null $readonly Flag indicating if the result view should be read-only. 19 | * 20 | * @return ArrayViewInterface A new view with selected elements from the source. 21 | */ 22 | public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayViewInterface; 23 | 24 | /** 25 | * Checks if the selector is compatible with the given view. 26 | * 27 | * @template T View elements type. 28 | * 29 | * @param ArrayViewInterface $view the view to check compatibility with. 30 | * 31 | * @return bool true if the element is compatible, false otherwise 32 | */ 33 | public function compatibleWith(ArrayViewInterface $view): bool; 34 | 35 | /** 36 | * Return value of the selector. 37 | * 38 | * @return mixed 39 | */ 40 | public function getValue(); 41 | } 42 | -------------------------------------------------------------------------------- /src/Interfaces/ArrayViewInterface.php: -------------------------------------------------------------------------------- 1 | |ArrayViewInterface|ArraySelectorInterface, T|array> 21 | * @extends \IteratorAggregate 22 | */ 23 | interface ArrayViewInterface extends \ArrayAccess, \IteratorAggregate, \Countable 24 | { 25 | /** 26 | * Creates an ArrayView instance from the given source array or ArrayView. 27 | * 28 | * * If the source is not an ArrayView, a new ArrayView is created with the provided source. 29 | * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, 30 | * a new readonly ArrayView is created. 31 | * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. 32 | * 33 | * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. 34 | * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. 35 | * 36 | * @return ArrayViewInterface An ArrayView instance based on the source array or ArrayView. 37 | * 38 | * @throws ValueError if the array is not sequential. 39 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 40 | */ 41 | public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface; 42 | 43 | /** 44 | * Creates an unlinked from source ArrayView instance from the given source array or ArrayView. 45 | * 46 | * * If the source is not an ArrayView, a new ArrayView is created with the provided source. 47 | * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, 48 | * a new readonly ArrayView is created. 49 | * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. 50 | * 51 | * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. 52 | * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. 53 | * 54 | * @return ArrayViewInterface An ArrayView instance based on the source array or ArrayView. 55 | * 56 | * @throws ValueError if the array is not sequential. 57 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 58 | */ 59 | public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface; 60 | 61 | /** 62 | * Returns the array representation of the view. 63 | * 64 | * @return array The array representation of the view. 65 | */ 66 | public function toArray(): array; 67 | 68 | /** 69 | * Returns a subview of this view based on a selector or string slice. 70 | * 71 | * @param string|array|ArrayViewInterface|ArraySelectorInterface $selector The selector or 72 | * string to filter the subview. 73 | * @param bool|null $readonly Flag indicating if the subview should be read-only. 74 | * 75 | * @return ArrayViewInterface A new view representing the subview of this view. 76 | * 77 | * @throws IndexError if the selector is IndexListSelector and some indexes are out of range. 78 | * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view. 79 | */ 80 | public function subview($selector, bool $readonly = null): ArrayViewInterface; 81 | 82 | /** 83 | * Filters the elements in the view based on a predicate function. 84 | * 85 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 86 | * 87 | * @return ArrayViewInterface A new view with elements that satisfy the predicate. 88 | */ 89 | public function filter(callable $predicate): ArrayViewInterface; 90 | 91 | /** 92 | * Checks if all elements in the view satisfy a given predicate function. 93 | * 94 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 95 | * 96 | * @return MaskSelectorInterface Boolean mask for selecting elements that satisfy the predicate. 97 | * 98 | * @see ArrayViewInterface::match() Full synonim. 99 | */ 100 | public function is(callable $predicate): MaskSelectorInterface; 101 | 102 | /** 103 | * Checks if all elements in the view satisfy a given predicate function. 104 | * 105 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 106 | * 107 | * @return MaskSelectorInterface Boolean mask for selecting elements that satisfy the predicate. 108 | * 109 | * @see ArrayViewInterface::is() Full synonim. 110 | */ 111 | public function match(callable $predicate): MaskSelectorInterface; 112 | 113 | /** 114 | * Compares the elements of the current ArrayView instance with another array or ArrayView 115 | * using the provided comparator function. 116 | * 117 | * @template U The type of the elements in the array for comparison with. 118 | * 119 | * @param array|ArrayViewInterface|U $data The array or ArrayView to compare to. 120 | * @param callable(T, U, int): bool $comparator Function that determines the comparison logic between the elements. 121 | * 122 | * @return MaskSelectorInterface A MaskSelector instance representing the results of the element comparisons. 123 | * 124 | * @throws ValueError if the $data is not sequential array. 125 | * @throws SizeError if size of $data not equals to size of the view. 126 | */ 127 | public function matchWith($data, callable $comparator): MaskSelectorInterface; 128 | 129 | /** 130 | * Transforms each element of the array using the given callback function. 131 | * 132 | * The callback function receives two parameters: the current element of the array and its index. 133 | * 134 | * @param callable(T, int): T $mapper Function to transform each element. 135 | * 136 | * @return array New array with transformed elements of this view. 137 | */ 138 | public function map(callable $mapper): array; 139 | 140 | /** 141 | * Transforms each pair of elements from the current array view and the provided data array using the given 142 | * callback function. 143 | * 144 | * The callback function receives three parameters: the current element of the current array view, 145 | * the corresponding element of the data array, and the index. 146 | * 147 | * @template U The type rhs of a binary operation. 148 | * 149 | * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. 150 | * @param callable(T, U, int): T $mapper Function to transform each pair of elements. 151 | * 152 | * @return array New array with transformed elements of this view. 153 | * 154 | * @throws ValueError if the $data is not sequential array. 155 | * @throws SizeError if size of $data not equals to size of the view. 156 | */ 157 | public function mapWith($data, callable $mapper): array; 158 | 159 | /** 160 | * Applies a transformation function to each element in the view. 161 | * 162 | * @param callable(T, int): T $mapper Function to transform each element. 163 | * 164 | * @return ArrayViewInterface this view. 165 | */ 166 | public function apply(callable $mapper): self; 167 | 168 | /** 169 | * Applies a transformation function using another array or view as rhs values for a binary operation. 170 | * 171 | * @template U The type rhs of a binary operation. 172 | * 173 | * @param array|ArrayViewInterface $data The rhs values for a binary operation. 174 | * @param callable(T, U, int): T $mapper Function to transform each pair of elements. 175 | * 176 | * @return ArrayViewInterface this view. 177 | * 178 | * @throws ValueError if the $data is not sequential array. 179 | * @throws SizeError if size of $data not equals to size of the view. 180 | */ 181 | public function applyWith($data, callable $mapper): self; 182 | 183 | /** 184 | * Sets new values for the elements in the view. 185 | * 186 | * @param array|ArrayViewInterface|T $newValues The new values to set. 187 | * 188 | * @return ArrayViewInterface this view. 189 | * 190 | * @throws ValueError if the $newValues is not sequential array. 191 | * @throws SizeError if size of $newValues not equals to size of the view. 192 | */ 193 | public function set($newValues): self; 194 | 195 | /** 196 | * Return true if view is readonly, otherwise false. 197 | * 198 | * @return bool 199 | */ 200 | public function isReadonly(): bool; 201 | 202 | /** 203 | * Return size of the view. 204 | * 205 | * @return int 206 | */ 207 | public function count(): int; 208 | 209 | /** 210 | * @param numeric|string|ArraySelectorInterface $offset 211 | * 212 | * @return bool 213 | * 214 | * {@inheritDoc} 215 | */ 216 | public function offsetExists($offset): bool; 217 | 218 | /** 219 | * @param numeric|string|ArraySelectorInterface $offset 220 | * 221 | * @return T|array 222 | * 223 | * @throws IndexError if the offset is out of range. 224 | * @throws KeyError if the key is invalid. 225 | * 226 | * {@inheritDoc} 227 | */ 228 | #[\ReturnTypeWillChange] 229 | public function offsetGet($offset); 230 | 231 | /** 232 | * @param numeric|string|ArraySelectorInterface $offset 233 | * @param T|array|ArrayViewInterface $value 234 | * 235 | * @return void 236 | * 237 | * @throws IndexError if the offset is out of range. 238 | * @throws KeyError if the key is invalid. 239 | * @throws ReadonlyError if the object is readonly. 240 | * 241 | * {@inheritDoc} 242 | */ 243 | public function offsetSet($offset, $value): void; 244 | 245 | /** 246 | * @param numeric|string|ArraySelectorInterface $offset 247 | * 248 | * @return void 249 | * 250 | * @throws NotSupportedError always. 251 | * 252 | * {@inheritDoc} 253 | */ 254 | public function offsetUnset($offset): void; 255 | 256 | /** 257 | * Return iterator to iterate the view elements. 258 | * 259 | * @return \Generator 260 | */ 261 | public function getIterator(): \Generator; 262 | } 263 | -------------------------------------------------------------------------------- /src/Interfaces/IndexListSelectorInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getValue(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Interfaces/MaskSelectorInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getValue(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Interfaces/PipeSelectorInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getValue(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Interfaces/SliceSelectorInterface.php: -------------------------------------------------------------------------------- 1 | subview($selector)->toArray()); // [1, 3, 5] 22 | * ``` 23 | */ 24 | final class IndexListSelector implements IndexListSelectorInterface 25 | { 26 | /** 27 | * @var array The array of indexes to select elements from. 28 | */ 29 | private array $value; 30 | 31 | /** 32 | * Creates a new IndexListSelector instance with the provided array of indexes. 33 | * 34 | * @param array|ArrayViewInterface $value The array of indexes or array view containing indexes. 35 | */ 36 | public function __construct($value) 37 | { 38 | $this->value = \is_array($value) ? $value : $value->toArray(); 39 | } 40 | 41 | /** 42 | * Selects elements from the source array based on the index list. 43 | * 44 | * @template T The type of elements in the source array view. 45 | * 46 | * @param ArrayViewInterface $source The source array view to select elements from. 47 | * @param bool|null $readonly Whether the selection should be read-only. 48 | * 49 | * @return ArrayIndexListView The view containing the selected elements. 50 | * 51 | * {@inheritDoc} 52 | */ 53 | public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayIndexListView 54 | { 55 | if (!$this->compatibleWith($source)) { 56 | throw new IndexError('Some indexes are out of range.'); 57 | } 58 | 59 | return new ArrayIndexListView($source, $this->value, $readonly ?? $source->isReadonly()); 60 | } 61 | 62 | /** 63 | * Checks if the selector is compatible with the given view. 64 | * 65 | * @template T View elements type. 66 | * 67 | * @param ArrayViewInterface $view the view to check compatibility with. 68 | * 69 | * @return bool true if the element is compatible, false otherwise 70 | * 71 | * {@inheritDoc} 72 | */ 73 | public function compatibleWith(ArrayViewInterface $view): bool 74 | { 75 | return \count($this->value) === 0 || \max($this->value) < \count($view) && \min($this->value) >= -\count($view); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function getValue(): array 82 | { 83 | return $this->value; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Selectors/MaskSelector.php: -------------------------------------------------------------------------------- 1 | subview($selector)->toArray()); // [1, 3, 5] 21 | * ``` 22 | */ 23 | final class MaskSelector implements MaskSelectorInterface 24 | { 25 | /** 26 | * @var array The array of boolean mask values to select elements based on. 27 | */ 28 | private $value; 29 | 30 | /** 31 | * Creates a new MaskSelector instance with the provided array of boolean mask values. 32 | * 33 | * @param array|ArrayViewInterface $value The array or array view of boolean mask values. 34 | */ 35 | public function __construct($value) 36 | { 37 | $this->value = \is_array($value) ? $value : $value->toArray(); 38 | } 39 | 40 | /** 41 | * Selects elements from the source array based on the mask values. 42 | * 43 | * @template T The type of elements in the source array view. 44 | * 45 | * @param ArrayViewInterface $source The source array to select elements from. 46 | * @param bool|null $readonly Whether the selection should be read-only. 47 | * 48 | * @return ArrayMaskView The view containing the selected elements. 49 | * 50 | * {@inheritDoc} 51 | */ 52 | public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayMaskView 53 | { 54 | return new ArrayMaskView($source, $this->value, $readonly ?? $source->isReadonly()); 55 | } 56 | 57 | /** 58 | * Checks if the selector is compatible with the given view. 59 | * 60 | * @template T View elements type. 61 | * 62 | * @param ArrayViewInterface $view the view to check compatibility with. 63 | * 64 | * @return bool true if the element is compatible, false otherwise 65 | * 66 | * {@inheritDoc} 67 | */ 68 | public function compatibleWith(ArrayViewInterface $view): bool 69 | { 70 | return \count($this->value) === \count($view); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function getValue(): array 77 | { 78 | return $this->value; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Selectors/PipeSelector.php: -------------------------------------------------------------------------------- 1 | subview($selector); 26 | * print_r($subview[':']); // [5, 7] 27 | * 28 | * $subview[':'] = [55, 77]; 29 | * print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 30 | * ``` 31 | * 32 | * ##### Example with nested pipes 33 | * ```php 34 | * $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 35 | * $selector = new PipeSelector([ 36 | * new SliceSelector('::2'), 37 | * new PipeSelector([ 38 | * new MaskSelector([true, false, true, true, true]), 39 | * new IndexListSelector([0, 1, 2]), 40 | * ]) 41 | * new SliceSelector('1:'), 42 | * ]); 43 | * 44 | * $view = ArrayView::toView($originalArray); 45 | * $subview = $view->subview($selector); 46 | * print_r($subview[':']); // [5, 7] 47 | * 48 | * $subview[':'] = [55, 77]; 49 | * print_r($originalArray); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 50 | * ``` 51 | */ 52 | final class PipeSelector implements PipeSelectorInterface 53 | { 54 | /** 55 | * @var array An array of selectors to be applied sequentially. 56 | */ 57 | private array $selectors; 58 | 59 | /** 60 | * Creates a new PipeSelector instance with the provided selectors array. 61 | * 62 | * @param array $selectors An array of selectors to be assigned to the PipeSelector. 63 | */ 64 | public function __construct(array $selectors) 65 | { 66 | $this->selectors = $selectors; 67 | } 68 | 69 | /** 70 | * Applies the series of selectors to the given source array view. 71 | * 72 | * @template T The type of elements in the source array view. 73 | * 74 | * @param ArrayViewInterface $source The source array view to select from. 75 | * @param bool|null $readonly Optional parameter to specify if the view should be read-only. 76 | * 77 | * @return ArrayViewInterface The resulting array view after applying all selectors. 78 | */ 79 | public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayViewInterface 80 | { 81 | $view = ArrayView::toView($source, $readonly); 82 | foreach ($this->selectors as $selector) { 83 | $view = $selector->select($view, $readonly); 84 | } 85 | /** @var ArrayViewInterface $view */ 86 | return $view; 87 | } 88 | 89 | /** 90 | * Checks if the series of selectors are compatible with the given array view. 91 | * 92 | * @template T The type of elements in the source array view. 93 | * 94 | * @param ArrayViewInterface $view The array view to check compatibility with. 95 | * 96 | * @return bool True if all selectors are compatible with the array view, false otherwise. 97 | */ 98 | public function compatibleWith(ArrayViewInterface $view): bool 99 | { 100 | foreach ($this->selectors as $selector) { 101 | if (!$selector->compatibleWith($view)) { 102 | return false; 103 | } 104 | $view = $selector->select($view); 105 | } 106 | return true; 107 | } 108 | 109 | /** 110 | * Returns the array of selectors assigned to the PipeSelector. 111 | * 112 | * @return array The array of selectors. 113 | */ 114 | public function getValue(): array 115 | { 116 | return $this->selectors; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Selectors/SliceSelector.php: -------------------------------------------------------------------------------- 1 | subview($selector)->toArray()); // [1, 3, 5, 7, 9] 22 | * 23 | * $selector = new SliceSelector('1::2'); 24 | * print_r($view[$selector]); // [2, 4, 6, 8, 10] 25 | * print_r($view->subview($selector)->toArray()); // [2, 4, 6, 8, 10] 26 | * 27 | * $selector = new SliceSelector('-3::-2'); 28 | * print_r($view[$selector]); // [8, 6, 4, 2] 29 | * print_r($view->subview($selector)->toArray()); // [8, 6, 4, 2] 30 | * 31 | * $selector = new SliceSelector('1:4'); 32 | * print_r($view[$selector]); // [2, 3, 4] 33 | * print_r($view->subview($selector)->toArray()); // [2, 3, 4] 34 | * 35 | * $selector = new SliceSelector('-2:0:-1'); 36 | * print_r($view[$selector]); // [9, 8, 7, 6, 5, 4, 3, 2] 37 | * print_r($view->subview($selector)->toArray()); // [9, 8, 7, 6, 5, 4, 3, 2] 38 | * ``` 39 | */ 40 | final class SliceSelector extends Slice implements ArraySelectorInterface 41 | { 42 | /** 43 | * Creates a new SliceSelector instance with the provided slice parameters. 44 | * 45 | * @param Slice|string|array $slice The slice instance or slice string defining the selection. 46 | */ 47 | public function __construct($slice) 48 | { 49 | $s = Slice::toSlice($slice); 50 | parent::__construct($s->start, $s->end, $s->step); 51 | } 52 | 53 | /** 54 | * Selects elements from the source array based on the slice parameters. 55 | * 56 | * @template T The type of elements in the source array. 57 | * 58 | * @param ArrayViewInterface $source The source array to select elements from. 59 | * @param bool|null $readonly Whether the selection should be read-only. 60 | * 61 | * @return ArraySliceView The view containing the selected elements. 62 | * 63 | * {@inheritDoc} 64 | */ 65 | public function select(ArrayViewInterface $source, ?bool $readonly = null): ArrayViewInterface 66 | { 67 | return new ArraySliceView($source, $this, $readonly ?? $source->isReadonly()); 68 | } 69 | 70 | /** 71 | * Checks if the selector is compatible with the given view. 72 | * 73 | * @template T View elements type. 74 | * 75 | * @param ArrayViewInterface $view the view to check compatibility with. 76 | * 77 | * @return bool true if the element is compatible, false otherwise 78 | * 79 | * {@inheritDoc} 80 | */ 81 | public function compatibleWith(ArrayViewInterface $view): bool 82 | { 83 | return true; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function getValue(): Slice 90 | { 91 | return $this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Structs/NormalizedSlice.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class NormalizedSlice extends Slice implements \Countable, \IteratorAggregate 15 | { 16 | /** 17 | * Creates a new NormalizedSlice instance with optional start, end, and step values. 18 | * 19 | * @param int|null $start The start index of the slice range. 20 | * @param int|null $end The end index of the slice range. 21 | * @param int|null $step The step size for selecting elements in the slice range. 22 | */ 23 | public function __construct(int $start = null, int $end = null, int $step = null) 24 | { 25 | parent::__construct($start, $end, $step); 26 | } 27 | 28 | /** 29 | * Getter for the start index of the normalized slice. 30 | * 31 | * @return int 32 | */ 33 | public function getStart(): int 34 | { 35 | /** @var int */ 36 | return $this->start; 37 | } 38 | 39 | /** 40 | * Getter for the stop index of the normalized slice. 41 | * 42 | * @return int 43 | */ 44 | public function getEnd(): int 45 | { 46 | /** @var int */ 47 | return $this->end; 48 | } 49 | 50 | /** 51 | * Getter for the step of the normalized slice. 52 | * 53 | * @return int 54 | */ 55 | public function getStep(): int 56 | { 57 | /** @var int */ 58 | return $this->step; 59 | } 60 | 61 | /** 62 | * Return size of the slice range. 63 | * 64 | * @return int Size of the slice range. 65 | */ 66 | public function count(): int 67 | { 68 | return \intval(\ceil(\abs((($this->end - $this->start) / $this->step)))); 69 | } 70 | 71 | /** 72 | * Converts the provided index to the actual index based on the normalized slice parameters. 73 | * 74 | * @param int $i The index to convert. 75 | * 76 | * @return int The converted index value. 77 | */ 78 | public function convertIndex(int $i): int 79 | { 80 | return $this->start + Util::normalizeIndex($i, \count($this), false) * $this->step; 81 | } 82 | 83 | /** 84 | * Return iterator to iterate slice range. 85 | * 86 | * @return \Generator 87 | */ 88 | public function getIterator(): \Generator 89 | { 90 | for ($i = 0; $i < \count($this); ++$i) { 91 | yield $this->convertIndex($i); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Structs/Slice.php: -------------------------------------------------------------------------------- 1 | $s The slice string/array or Slice object to convert. 33 | * 34 | * @return Slice The converted Slice instance. 35 | * 36 | * @throws ValueError if the slice representation is invalid. 37 | */ 38 | public static function toSlice($s): Slice 39 | { 40 | /** @var mixed $s */ 41 | if ($s instanceof Slice) { 42 | return $s; 43 | } 44 | 45 | if (\is_array($s) && self::isSliceArray($s)) { 46 | return new Slice(...$s); 47 | } 48 | 49 | if (!self::isSliceString($s)) { 50 | $str = \is_scalar($s) ? "{$s}" : \gettype($s); 51 | throw new ValueError("Invalid slice: \"{$str}\"."); 52 | } 53 | 54 | /** @var string $s */ 55 | $slice = self::parseSliceString($s); 56 | 57 | return new Slice(...$slice); 58 | } 59 | 60 | /** 61 | * Checks if the provided value is a Slice instance or a valid slice string. 62 | * 63 | * @param mixed $s The value to check. 64 | * 65 | * @return bool True if the value is a Slice instance or a valid slice string, false otherwise. 66 | */ 67 | public static function isSlice($s): bool 68 | { 69 | return ($s instanceof Slice) || static::isSliceString($s) || static::isSliceArray($s); 70 | } 71 | 72 | /** 73 | * Checks if the provided value is a valid slice string. 74 | * 75 | * @param mixed $s The value to check. 76 | * 77 | * @return bool True if the value is a valid slice string, false otherwise. 78 | */ 79 | public static function isSliceString($s): bool 80 | { 81 | if (!\is_string($s)) { 82 | return false; 83 | } 84 | 85 | if (\is_numeric($s)) { 86 | return false; 87 | } 88 | 89 | if (!\preg_match('/^-?[0-9]*:?-?[0-9]*:?-?[0-9]*$/', $s)) { 90 | return false; 91 | } 92 | 93 | $slice = self::parseSliceString($s); 94 | 95 | return !(\count($slice) < 1 || \count($slice) > 3); 96 | } 97 | 98 | /** 99 | * Checks if the provided value is a valid slice array. 100 | * 101 | * @param mixed $s The value to check. 102 | * 103 | * @return bool True if the value is a valid slice array, false otherwise. 104 | */ 105 | public static function isSliceArray($s): bool 106 | { 107 | if (!\is_array($s)) { 108 | return false; 109 | } 110 | 111 | if (\count($s) > 3) { 112 | return false; 113 | } 114 | 115 | foreach ($s as $key => $item) { 116 | if (\is_string($key)) { 117 | return false; 118 | } 119 | if ($item !== null && (!\is_numeric($item) || \is_float($item + 0))) { 120 | return false; 121 | } 122 | } 123 | 124 | return true; 125 | } 126 | 127 | /** 128 | * Creates a new Slice instance with optional start, end, and step values. 129 | * 130 | * @param int|null $start The start index of the slice range. 131 | * @param int|null $end The end index of the slice range. 132 | * @param int|null $step The step size for selecting elements in the slice range. 133 | */ 134 | public function __construct(?int $start = null, ?int $end = null, ?int $step = null) 135 | { 136 | $this->start = $start; 137 | $this->end = $end; 138 | $this->step = $step; 139 | } 140 | 141 | /** 142 | * Getter for the start index of the normalized slice. 143 | * 144 | * @return int|null 145 | */ 146 | public function getStart(): ?int 147 | { 148 | return $this->start; 149 | } 150 | 151 | /** 152 | * Getter for the stop index of the normalized slice. 153 | * 154 | * @return int|null 155 | */ 156 | public function getEnd(): ?int 157 | { 158 | return $this->end; 159 | } 160 | 161 | /** 162 | * Getter for the step of the normalized slice. 163 | * 164 | * @return int|null 165 | */ 166 | public function getStep(): ?int 167 | { 168 | return $this->step; 169 | } 170 | 171 | /** 172 | * Normalizes the slice parameters based on the container length. 173 | * 174 | * @param int $containerSize The length of the container or array. 175 | * 176 | * @return NormalizedSlice The normalized slice parameters. 177 | * 178 | * @throws IndexError if the step value is 0. 179 | */ 180 | public function normalize(int $containerSize): NormalizedSlice 181 | { 182 | $step = $this->step ?? 1; 183 | 184 | if ($step > 0) { 185 | return $this->normalizeWithPositiveStep($containerSize, $step); 186 | } elseif ($step < 0) { 187 | return $this->normalizeWithNegativeStep($containerSize, $step); 188 | } 189 | 190 | throw new IndexError("Step cannot be 0."); 191 | } 192 | 193 | /** 194 | * Returns the string representation of the Slice. 195 | * 196 | * @return string The string representation of the Slice. 197 | */ 198 | public function toString(): string 199 | { 200 | [$start, $end, $step] = [$this->start ?? '', $this->end ?? '', $this->step ?? '']; 201 | return "{$start}:{$end}:{$step}"; 202 | } 203 | 204 | /** 205 | * Parses a slice string into an array of start, end, and step values. 206 | * 207 | * @param string $s The slice string to parse. 208 | * 209 | * @return array An array of parsed start, end, and step values. 210 | */ 211 | private static function parseSliceString(string $s): array 212 | { 213 | if ($s === '') { 214 | return []; 215 | } 216 | return array_map(fn($x) => trim($x) === '' ? null : \intval(trim($x)), \explode(':', $s)); 217 | } 218 | 219 | /** 220 | * Constrains a value within a given range. 221 | * 222 | * @param int $x The value to constrain. 223 | * @param int $min The minimum allowed value. 224 | * @param int $max The maximum allowed value. 225 | * 226 | * @return int The constrained value. 227 | */ 228 | private function squeezeInBounds(int $x, int $min, int $max): int 229 | { 230 | return max($min, min($max, $x)); 231 | } 232 | 233 | /** 234 | * Normalizes the slice parameters based on the container length (for positive step only). 235 | * 236 | * @param int $containerSize The length of the container or array. 237 | * @param int $step Step size. 238 | * 239 | * @return NormalizedSlice The normalized slice parameters. 240 | */ 241 | private function normalizeWithPositiveStep(int $containerSize, int $step): NormalizedSlice 242 | { 243 | $start = $this->start ?? 0; 244 | $end = $this->end ?? $containerSize; 245 | 246 | [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)]; 247 | 248 | $start = Util::normalizeIndex($start, $containerSize, false); 249 | $end = Util::normalizeIndex($end, $containerSize, false); 250 | 251 | if ($start >= $containerSize) { 252 | $start = $end = $containerSize - 1; 253 | } 254 | 255 | $start = $this->squeezeInBounds($start, 0, $containerSize - 1); 256 | $end = $this->squeezeInBounds($end, 0, $containerSize); 257 | 258 | if ($end < $start) { 259 | $end = $start; 260 | } 261 | 262 | return new NormalizedSlice($start, $end, $step); 263 | } 264 | 265 | /** 266 | * Normalizes the slice parameters based on the container length (for negative step only). 267 | * 268 | * @param int $containerSize The length of the container or array. 269 | * @param int $step Step size. 270 | * 271 | * @return NormalizedSlice The normalized slice parameters. 272 | */ 273 | private function normalizeWithNegativeStep(int $containerSize, int $step): NormalizedSlice 274 | { 275 | $start = $this->start ?? $containerSize - 1; 276 | $end = $this->end ?? -1; 277 | 278 | [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)]; 279 | 280 | $start = Util::normalizeIndex($start, $containerSize, false); 281 | 282 | if (!($this->end === null)) { 283 | $end = Util::normalizeIndex($end, $containerSize, false); 284 | } 285 | 286 | if ($start < 0) { 287 | $start = $end = 0; 288 | } 289 | 290 | $start = $this->squeezeInBounds($start, 0, $containerSize - 1); 291 | $end = $this->squeezeInBounds($end, -1, $containerSize); 292 | 293 | if ($end > $start) { 294 | $end = $start; 295 | } 296 | 297 | return new NormalizedSlice($start, $end, $step); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/Traits/ArrayViewAccessTrait.php: -------------------------------------------------------------------------------- 1 | |ArrayViewInterface|ArraySelectorInterface Selector type. 27 | */ 28 | trait ArrayViewAccessTrait 29 | { 30 | /** 31 | * Check if the specified offset exists in the ArrayView object. 32 | * 33 | * ```php 34 | * $source = [1, 2, 3, 4, 5]; 35 | * $view = ArrayView::toView($source); 36 | * 37 | * isset($view[0]); // true 38 | * isset($view[-1]); // true 39 | * isset($view[10]); // false 40 | * 41 | * isset($view[new SliceSelector('::2')]); // true 42 | * isset($view[new IndexListSelector([0, 2, 4])]); // true 43 | * isset($view[new IndexListSelector([0, 2, 10])]); // false 44 | * isset($view[new MaskSelector([true, true, false, false, true])]); // true 45 | * isset($view[new MaskSelector([true, true, false, false, true, true])]); // false 46 | * 47 | * isset($view['::2']); // true 48 | * isset($view[[0, 2, 4]]); // true 49 | * isset($view[[0, 2, 10]]); // false 50 | * isset($view[[true, true, false, false, true]]); // true 51 | * isset($view[[true, true, false, false, true, true]]); // false 52 | * ``` 53 | * 54 | * @param numeric|S $offset The offset to check. 55 | * 56 | * @return bool 57 | * 58 | * {@inheritDoc} 59 | */ 60 | public function offsetExists($offset): bool 61 | { 62 | if (\is_numeric($offset)) { 63 | return $this->numericOffsetExists($offset); 64 | } 65 | 66 | try { 67 | return $this->toSelector($offset)->compatibleWith($this); 68 | } catch (KeyError $e) { 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * Get the value at the specified offset in the ArrayView object. 75 | * 76 | * ```php 77 | * $source = [1, 2, 3, 4, 5]; 78 | * $view = ArrayView::toView($source); 79 | * 80 | * $view[0]; // 1 81 | * $view[-1]; // 5 82 | * 83 | * $view[new SliceSelector('::2')]; // [1, 3, 5] 84 | * $view[new IndexListSelector([0, 2, 4])]; // [1, 3, 5] 85 | * $view[new MaskSelector([true, true, false, false, true])]; // [1, 2, 5] 86 | * 87 | * $view['::2']; // [1, 3, 5] 88 | * $view[[0, 2, 4]]; // [1, 3, 5] 89 | * $view[[true, true, false, false, true]]; // [1, 2, 5] 90 | * ``` 91 | * 92 | * @param numeric|S $offset The offset to get the value at. 93 | * 94 | * @return T|array The value at the specified offset. 95 | * 96 | * @throws IndexError if the offset is out of range. 97 | * @throws KeyError if the key is invalid. 98 | * 99 | * {@inheritDoc} 100 | */ 101 | #[\ReturnTypeWillChange] 102 | public function offsetGet($offset) 103 | { 104 | if (\is_numeric($offset)) { 105 | if (!$this->numericOffsetExists($offset)) { 106 | throw new IndexError("Index {$offset} is out of range."); 107 | } 108 | return $this->source[$this->convertIndex(\intval($offset))]; 109 | } 110 | 111 | return $this->subview($this->toSelector($offset))->toArray(); 112 | } 113 | 114 | /** 115 | * Set the value at the specified offset in the ArrayView object. 116 | * 117 | * ```php 118 | * $source = [1, 2, 3, 4, 5]; 119 | * $view = ArrayView::toView($source); 120 | * 121 | * $view[0] = 11; 122 | * $view[-1] = 55; 123 | * 124 | * $source; // [11, 2, 3, 4, 55] 125 | * 126 | * $source = [1, 2, 3, 4, 5]; 127 | * $view = ArrayView::toView($source); 128 | * 129 | * $view[new SliceSelector('::2')] = [11, 33, 55]; 130 | * $source; // [11, 2, 33, 4, 55] 131 | * 132 | * $view[new IndexListSelector([1, 3])] = [22, 44]; 133 | * $source; // [11, 22, 33, 44, 55] 134 | * 135 | * $view[new MaskSelector([true, false, false, false, true])] = [111, 555]; 136 | * $source; // [111, 22, 33, 44, 555] 137 | * 138 | * $source = [1, 2, 3, 4, 5]; 139 | * $view = ArrayView::toView($source); 140 | * 141 | * $view['::2'] = [11, 33, 55]; 142 | * $source; // [11, 2, 33, 4, 55] 143 | * 144 | * $view[[1, 3]] = [22, 44]; 145 | * $source; // [11, 22, 33, 44, 55] 146 | * 147 | * $view[[true, false, false, false, true]] = [111, 555]; 148 | * $source; // [111, 22, 33, 44, 555] 149 | * ``` 150 | * 151 | * @param numeric|S $offset The offset to set the value at. 152 | * @param T|array|ArrayViewInterface $value The value to set. 153 | * 154 | * @return void 155 | * 156 | * @throws IndexError if the offset is out of range. 157 | * @throws KeyError if the key is invalid. 158 | * @throws ReadonlyError if the object is readonly. 159 | * 160 | * {@inheritDoc} 161 | */ 162 | public function offsetSet($offset, $value): void 163 | { 164 | if ($this->isReadonly()) { 165 | throw new ReadonlyError("Cannot modify a readonly view."); 166 | } 167 | 168 | if (!\is_numeric($offset)) { 169 | $this->subview($this->toSelector($offset))->set($value); 170 | return; 171 | } 172 | 173 | if (!$this->numericOffsetExists($offset)) { 174 | throw new IndexError("Index {$offset} is out of range."); 175 | } 176 | 177 | // @phpstan-ignore-next-line 178 | $this->source[$this->convertIndex(\intval($offset))] = $value; 179 | } 180 | 181 | /** 182 | * Unset the value at the specified offset in the array-like object. 183 | * 184 | * @param numeric|S $offset The offset to unset the value at. 185 | * 186 | * @return void 187 | * 188 | * @throws NotSupportedError always. 189 | * 190 | * {@inheritDoc} 191 | */ 192 | public function offsetUnset($offset): void 193 | { 194 | throw new NotSupportedError(); 195 | } 196 | 197 | /** 198 | * Converts array to selector. 199 | * 200 | * @param S $input value to convert. 201 | * 202 | * @return ArraySelectorInterface 203 | */ 204 | protected function toSelector($input): ArraySelectorInterface 205 | { 206 | if ($input instanceof ArraySelectorInterface) { 207 | return $input; 208 | } 209 | 210 | if (\is_string($input) && Slice::isSlice($input)) { 211 | return new SliceSelector($input); 212 | } 213 | 214 | if ($input instanceof ArrayViewInterface) { 215 | $input = $input->toArray(); 216 | } 217 | 218 | if (!\is_array($input) || !Util::isArraySequential($input)) { 219 | $strOffset = \is_scalar($input) ? \strval($input) : \gettype($input); 220 | throw new KeyError("Invalid key: \"{$strOffset}\"."); 221 | } 222 | 223 | if (\count($input) > 0 && \is_bool($input[0])) { 224 | /** @var array $input */ 225 | return new MaskSelector($input); 226 | } 227 | 228 | /** @var array $input */ 229 | return new IndexListSelector($input); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Traits/ArrayViewOperationsTrait.php: -------------------------------------------------------------------------------- 1 | |ArrayViewInterface|ArraySelectorInterface Selector type. 22 | */ 23 | trait ArrayViewOperationsTrait 24 | { 25 | /** 26 | * Filters the elements in the view based on a predicate function. 27 | * 28 | * ##### Example 29 | * ```php 30 | * $source = [1, 2, 3, 4, 5, 6]; 31 | * $view = ArrayView::toView($source); 32 | * 33 | * $filtered = $view->filter(fn ($x) => $x % 2 === 0); 34 | * $filtered->toArray(); // [2, 4, 6] 35 | * 36 | * $filtered[':'] = [20, 40, 60]; 37 | * $filtered->toArray(); // [20, 40, 60] 38 | * $source; // [1, 20, 3, 40, 5, 60] 39 | * ``` 40 | * 41 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 42 | * 43 | * @return ArrayMaskView A new view with elements that satisfy the predicate. 44 | */ 45 | public function filter(callable $predicate): ArrayViewInterface 46 | { 47 | return $this->is($predicate)->select($this); 48 | } 49 | 50 | /** 51 | * Checks if all elements in the view satisfy a given predicate function. 52 | * 53 | * ##### Example 54 | * ```php 55 | * $source = [1, 2, 3, 4, 5, 6]; 56 | * $view = ArrayView::toView($source); 57 | * 58 | * $mask = $view->is(fn ($x) => $x % 2 === 0); 59 | * $mask->getValue(); // [false, true, false, true, false, true] 60 | * 61 | * $view->subview($mask)->toArray(); // [2, 4, 6] 62 | * $view[$mask]; // [2, 4, 6] 63 | * 64 | * $view[$mask] = [20, 40, 60]; 65 | * $source; // [1, 20, 3, 40, 5, 60] 66 | * ``` 67 | * 68 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 69 | * 70 | * @return MaskSelector Boolean mask for selecting elements that satisfy the predicate. 71 | * 72 | * @see ArrayViewInterface::match() Full synonim. 73 | */ 74 | public function is(callable $predicate): MaskSelectorInterface 75 | { 76 | $data = $this->toArray(); 77 | return new MaskSelector(array_map($predicate, $data, array_keys($data))); 78 | } 79 | 80 | /** 81 | * Checks if all elements in the view satisfy a given predicate function. 82 | * 83 | * ##### Example 84 | * ```php 85 | * $source = [1, 2, 3, 4, 5, 6]; 86 | * $view = ArrayView::toView($source); 87 | * 88 | * $mask = $view->match(fn ($x) => $x % 2 === 0); 89 | * $mask->getValue(); // [false, true, false, true, false, true] 90 | * 91 | * $view->subview($mask)->toArray(); // [2, 4, 6] 92 | * $view[$mask]; // [2, 4, 6] 93 | * 94 | * $view[$mask] = [20, 40, 60]; 95 | * $source; // [1, 20, 3, 40, 5, 60] 96 | * ``` 97 | * 98 | * @param callable(T, int): bool $predicate Function that returns a boolean value for each element. 99 | * 100 | * @return MaskSelector Boolean mask for selecting elements that satisfy the predicate. 101 | * 102 | * @see ArrayView::match() Full synonim. 103 | */ 104 | public function match(callable $predicate): MaskSelectorInterface 105 | { 106 | return $this->is($predicate); 107 | } 108 | 109 | /** 110 | * Compares the elements of the current ArrayView instance with another array or ArrayView 111 | * using the provided comparator function. 112 | * 113 | * ##### Example 114 | * ```php 115 | * $source = [1, 2, 3, 4, 5, 6]; 116 | * $view = ArrayView::toView($source); 117 | * 118 | * $data = [6, 5, 4, 3, 2, 1]; 119 | * 120 | * $mask = $view->matchWith($data, fn ($lhs, $rhs) => $lhs > $rhs); 121 | * $mask->getValue(); // [false, false, false, true, true, true] 122 | * 123 | * $view->subview($mask)->toArray(); // [4, 5, 6] 124 | * $view[$mask]; // [4, 5, 6] 125 | * 126 | * $view[$mask] = [40, 50, 60]; 127 | * $source; // [1, 2, 3, 40, 50, 60] 128 | * ``` 129 | * 130 | * @template U The type of the elements in the array for comparison with. 131 | * 132 | * @param array|ArrayViewInterface|U $data The array or ArrayView to compare to. 133 | * @param callable(T, U, int): bool $comparator Function that determines the comparison logic between the elements. 134 | * 135 | * @return MaskSelectorInterface A MaskSelector instance representing the results of the element comparisons. 136 | * 137 | * @throws ValueError if the $data is not sequential array. 138 | * @throws SizeError if size of $data not equals to size of the view. 139 | * 140 | * @see ArrayView::is() Full synonim. 141 | */ 142 | public function matchWith($data, callable $comparator): MaskSelectorInterface 143 | { 144 | $data = $this->checkAndConvertArgument($data); 145 | return new MaskSelector(array_map($comparator, $this->toArray(), $data, array_keys($data))); 146 | } 147 | 148 | /** 149 | * Transforms each element of the array using the given callback function. 150 | * 151 | * The callback function receives two parameters: the current element of the array and its index. 152 | * 153 | * ##### Example 154 | * ```php 155 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 156 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] 157 | * 158 | * $subview->map(fn ($x) => $x * 10); // [10, 30, 50, 70, 90] 159 | * ``` 160 | * 161 | * @param callable(T, int): T $mapper Function to transform each element. 162 | * 163 | * @return array New array with transformed elements of this view. 164 | */ 165 | public function map(callable $mapper): array 166 | { 167 | $result = []; 168 | $size = \count($this); 169 | for ($i = 0; $i < $size; $i++) { 170 | /** @var T $item */ 171 | $item = $this[$i]; 172 | $result[$i] = $mapper($item, $i); 173 | } 174 | return $result; 175 | } 176 | 177 | /** 178 | * Transforms each pair of elements from the current array view and the provided data array using the given 179 | * callback function. 180 | * 181 | * The callback function receives three parameters: the current element of the current array view, 182 | * the corresponding element of the data array, and the index. 183 | * 184 | * ##### Example 185 | * ```php 186 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 187 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] 188 | * 189 | * $data = [9, 27, 45, 63, 81]; 190 | * 191 | * $subview->mapWith($data, fn ($lhs, $rhs) => $lhs + $rhs); // [10, 30, 50, 70, 90] 192 | * ``` 193 | * 194 | * @template U The type rhs of a binary operation. 195 | * 196 | * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. 197 | * @param callable(T, U, int): T $mapper Function to transform each pair of elements. 198 | * 199 | * @return array New array with transformed elements of this view. 200 | * 201 | * @throws ValueError if the $data is not sequential array. 202 | * @throws SizeError if size of $data not equals to size of the view. 203 | */ 204 | public function mapWith($data, callable $mapper): array 205 | { 206 | $data = $this->checkAndConvertArgument($data); 207 | $result = []; 208 | 209 | $size = \count($this); 210 | for ($i = 0; $i < $size; $i++) { 211 | /** @var T $lhs */ 212 | $lhs = $this[$i]; 213 | /** @var U $rhs */ 214 | $rhs = $data[$i]; 215 | $result[$i] = $mapper($lhs, $rhs, $i); 216 | } 217 | 218 | return $result; 219 | } 220 | 221 | /** 222 | * Applies a transformation function to each element in the view. 223 | * 224 | * ##### Example 225 | * ```php 226 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 227 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] 228 | * 229 | * $subview->apply(fn ($x) => $x * 10); 230 | * 231 | * $subview->toArray(); // [10, 30, 50, 70, 90] 232 | * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10] 233 | * ``` 234 | * 235 | * @param callable(T, int): T $mapper Function to transform each element. 236 | * 237 | * @return ArrayView this view. 238 | */ 239 | public function apply(callable $mapper): self 240 | { 241 | $size = \count($this); 242 | for ($i = 0; $i < $size; $i++) { 243 | /** @var T $item */ 244 | $item = $this[$i]; 245 | $this[$i] = $mapper($item, $i); 246 | } 247 | return $this; 248 | } 249 | 250 | /** 251 | * Sets new values for the elements in the view. 252 | * 253 | * ##### Example 254 | * ```php 255 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 256 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9] 257 | * 258 | * $data = [9, 27, 45, 63, 81]; 259 | * 260 | * $subview->applyWith($data, fn ($lhs, $rhs) => $lhs + $rhs); 261 | * $subview->toArray(); // [10, 30, 50, 70, 90] 262 | * 263 | * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10] 264 | * ``` 265 | * 266 | * @template U Type of $data items. 267 | * 268 | * @param array|ArrayViewInterface $data 269 | * @param callable(T, U, int): T $mapper 270 | * 271 | * @return ArrayView this view. 272 | * 273 | * @throws ValueError if the $data is not sequential array. 274 | * @throws SizeError if size of $data not equals to size of the view. 275 | */ 276 | public function applyWith($data, callable $mapper): self 277 | { 278 | $data = $this->checkAndConvertArgument($data); 279 | 280 | $size = \count($this); 281 | for ($i = 0; $i < $size; $i++) { 282 | /** @var T $lhs */ 283 | $lhs = $this[$i]; 284 | /** @var U $rhs */ 285 | $rhs = $data[$i]; 286 | $this[$i] = $mapper($lhs, $rhs, $i); 287 | } 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * Check if the given source array is sequential (indexed from 0 to n-1). 294 | * 295 | * If the array is not sequential, a ValueError is thrown indicating that 296 | * a view cannot be created for a non-sequential array. 297 | * 298 | * @param mixed $source The source array to check for sequential indexing. 299 | * 300 | * @return void 301 | * 302 | * @throws ValueError if the source array is not sequential. 303 | */ 304 | protected function checkSequentialArgument($source): void 305 | { 306 | if ($source instanceof ArrayViewInterface) { 307 | return; 308 | } 309 | 310 | if (\is_array($source) && !Util::isArraySequential($source)) { 311 | throw new ValueError('Argument is not sequential.'); 312 | } 313 | } 314 | 315 | /** 316 | * Util function for checking and converting data argument. 317 | * 318 | * @template U Type of $data items. 319 | * 320 | * @param array|ArrayViewInterface|U $data The rhs values for a binary operation. 321 | * 322 | * @return array converted data. 323 | */ 324 | protected function checkAndConvertArgument($data): array 325 | { 326 | $this->checkSequentialArgument($data); 327 | 328 | if ($data instanceof ArrayViewInterface) { 329 | $data = $data->toArray(); 330 | } elseif (!\is_array($data)) { 331 | $data = \array_fill(0, \count($this), $data); 332 | } 333 | 334 | [$dataSize, $thisSize] = [\count($data), \count($this)]; 335 | if ($dataSize !== $thisSize) { 336 | throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize})."); 337 | } 338 | 339 | return $data; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | = 0 ? $index : \abs($index) - 1; 30 | if ($throwError && $dist >= $containerLength) { 31 | throw new IndexError("Index {$index} is out of range."); 32 | } 33 | return $index < 0 ? $containerLength + $index : $index; 34 | } 35 | 36 | /** 37 | * Check if an array is sequential (indexed from 0 to n-1). 38 | * 39 | * @param array $source The array to check for sequential indexing. 40 | * @param bool $forceCustomImplementation Flag only for tests (to test custom implementation of array_is_list). 41 | * 42 | * @return bool Returns true if the array has sequential indexing, false otherwise. 43 | */ 44 | public static function isArraySequential(array $source, bool $forceCustomImplementation = false): bool 45 | { 46 | if (!\function_exists('array_is_list') || $forceCustomImplementation) { 47 | return \count($source) === 0 || \array_keys($source) === \range(0, \count($source) - 1); 48 | } 49 | return \array_is_list($source); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Views/ArrayIndexListView.php: -------------------------------------------------------------------------------- 1 | subview(new IndexListSelector([0, 2, 4])); 20 | * $view->toArray(); // [1, 3, 5] 21 | * ``` 22 | * 23 | * @template T Type of array source elements. 24 | * 25 | * @extends ArrayView 26 | */ 27 | class ArrayIndexListView extends ArrayView 28 | { 29 | /** 30 | * @var array The indexes array specifying the indexes of elements in the source array to include in the view. 31 | */ 32 | protected array $indexes; 33 | 34 | /** 35 | * Constructs a new ArrayIndexListView instance with the specified source array or ArrayView and indexes array. 36 | * 37 | * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. 38 | * @param array $indexes The indexes array specifying the indexes of elements in the source array. 39 | * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. 40 | * 41 | * @throws ValueError if the array is not sequential. 42 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 43 | */ 44 | public function __construct(&$source, array $indexes, ?bool $readonly = null) 45 | { 46 | parent::__construct($source, $readonly); 47 | $this->indexes = $indexes; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function toArray(): array 54 | { 55 | return array_map(fn (int $index) => $this->source[$index], $this->indexes); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public function count(): int 62 | { 63 | return \count($this->indexes); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | protected function convertIndex(int $i): int 70 | { 71 | return Util::normalizeIndex( 72 | $this->indexes[Util::normalizeIndex($i, \count($this->indexes))], 73 | $this->getParentSize() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Views/ArrayMaskView.php: -------------------------------------------------------------------------------- 1 | subview(new MaskSelector([true, false, true, false, true])); 20 | * $view->toArray(); // [1, 3, 5] 21 | * ``` 22 | * 23 | * @template T Type of array source elements. 24 | * 25 | * @extends ArrayIndexListView 26 | */ 27 | class ArrayMaskView extends ArrayIndexListView 28 | { 29 | /** 30 | * @var array The boolean mask specifying whether each element in the source array 31 | * should be included in the view (true) or excluded (false). 32 | */ 33 | protected array $mask; 34 | 35 | /** 36 | * Constructs a new ArrayMaskView instance with the specified source array or ArrayView and boolean mask. 37 | * 38 | * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. 39 | * @param array $mask Options for configuring the view. 40 | * @param bool|null $readonly The boolean mask for including or excluding elements from the source array. 41 | * 42 | * @throws ValueError if the array is not sequential. 43 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 44 | */ 45 | public function __construct(&$source, array $mask, ?bool $readonly = null) 46 | { 47 | [$sourceSize, $maskSize] = [\count($source), \count($mask)]; 48 | if ($sourceSize !== $maskSize) { 49 | throw new SizeError("Mask size not equal to source length ({$maskSize} != {$sourceSize})."); 50 | } 51 | 52 | $indexes = array_filter( 53 | array_map(fn (bool $v, int $i) => $v ? $i : null, $mask, array_keys($mask)), 54 | fn ($v) => $v !== null 55 | ); 56 | parent::__construct($source, array_values($indexes), $readonly); 57 | $this->mask = $mask; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Views/ArraySliceView.php: -------------------------------------------------------------------------------- 1 | subview(new SliceSelector('::2')); 20 | * $view->toArray(); // [1, 3, 5] 21 | * ``` 22 | * 23 | * @template T Type of array source elements. 24 | * 25 | * @extends ArrayView 26 | */ 27 | class ArraySliceView extends ArrayView 28 | { 29 | /** 30 | * @var NormalizedSlice The normalized slice range defining the view within the source array or ArrayView. 31 | */ 32 | protected NormalizedSlice $slice; 33 | 34 | /** 35 | * Constructs a new ArraySliceView instance with the specified source array or ArrayView and slice range. 36 | * 37 | * @param Array|ArrayViewInterface $source The source array or ArrayView to create a view from. 38 | * @param Slice $slice The slice range specifying the subset of elements to include in the view. 39 | * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. 40 | * 41 | * @throws ValueError if the array is not sequential. 42 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 43 | */ 44 | public function __construct(&$source, Slice $slice, ?bool $readonly = null) 45 | { 46 | parent::__construct($source, $readonly); 47 | $this->slice = $slice->normalize(\count($source)); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function count(): int 54 | { 55 | return \count($this->slice); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | protected function convertIndex(int $i): int 62 | { 63 | return $this->slice->convertIndex($i); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Views/ArrayView.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class ArrayView implements ArrayViewInterface 32 | { 33 | /** 34 | * @use ArrayViewAccessTrait|ArrayViewInterface|ArraySelectorInterface> 35 | * 36 | * for array access methods. 37 | */ 38 | use ArrayViewAccessTrait; 39 | /** 40 | * @use ArrayViewOperationsTrait|ArrayViewInterface|ArraySelectorInterface> 41 | * 42 | * for utils methods. 43 | */ 44 | use ArrayViewOperationsTrait; 45 | 46 | /** 47 | * @var array|ArrayViewInterface The source array or view. 48 | */ 49 | protected $source; 50 | /** 51 | * @var bool Flag indicating if the view is readonly. 52 | */ 53 | protected bool $readonly; 54 | /** 55 | * @var ArrayViewInterface|null The parent view of the current view. 56 | */ 57 | protected ?ArrayViewInterface $parentView; 58 | 59 | /** 60 | * Creates an ArrayView instance from the given source array or ArrayView. 61 | * 62 | * * If the source is not an ArrayView, a new ArrayView is created with the provided source. 63 | * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, 64 | * a new readonly ArrayView is created. 65 | * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. 66 | * 67 | * ##### Example 68 | * ```php 69 | * $source = [1, 2, 3, 4, 5]; 70 | * $view = ArrayView::toView($source); 71 | * 72 | * $view[0]; // 1 73 | * $view['1::2']; // [2, 4] 74 | * $view['1::2'] = [22, 44]; 75 | * 76 | * $view->toArray(); // [1, 22, 3, 44, 5] 77 | * $source; // [1, 22, 3, 44, 5] 78 | * ``` 79 | * 80 | * ##### Readonly example 81 | * ```php 82 | * $source = [1, 2, 3, 4, 5]; 83 | * $view = ArrayView::toView($source, true); 84 | * 85 | * $view['1::2']; // [2, 4] 86 | * $view['1::2'] = [22, 44]; // throws ReadonlyError 87 | * $view[0] = 11; // throws ReadonlyError 88 | * ``` 89 | * 90 | * @param array|ArrayViewInterface $source The source array or ArrayView to create a view from. 91 | * @param bool|null $readonly Optional flag to indicate whether the view should be readonly. 92 | * 93 | * @return ArrayViewInterface An ArrayView instance based on the source array or ArrayView. 94 | * 95 | * @throws ValueError if the array is not sequential. 96 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 97 | */ 98 | public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface 99 | { 100 | if (!($source instanceof ArrayViewInterface)) { 101 | return new ArrayView($source, $readonly); 102 | } 103 | 104 | if (!$source->isReadonly() && $readonly) { 105 | return new ArrayView($source, $readonly); 106 | } 107 | 108 | return $source; 109 | } 110 | 111 | /** 112 | * {@inheritDoc} 113 | * 114 | * ##### Example: 115 | * ```php 116 | * $source = [1, 2, 3, 4, 5]; 117 | * $view = ArrayView::toUnlinkedView($source); 118 | * 119 | * $view[0]; // 1 120 | * $view['1::2']; // [2, 4] 121 | * $view['1::2'] = [22, 44]; 122 | * 123 | * $view->toArray(); // [1, 22, 3, 44, 5] 124 | * $source; // [1, 2, 3, 4, 5] 125 | * ``` 126 | * 127 | * ##### Readonly example: 128 | * ```php 129 | * $source = [1, 2, 3, 4, 5]; 130 | * $view = ArrayView::toUnlinkedView($source, true); 131 | * 132 | * $view['1::2']; // [2, 4] 133 | * $view['1::2'] = [22, 44]; // throws ReadonlyError 134 | * $view[0] = 11; // throws ReadonlyError 135 | * ``` 136 | */ 137 | public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface 138 | { 139 | return static::toView($source, $readonly); 140 | } 141 | 142 | /** 143 | * Constructor to create a new ArrayView. 144 | * 145 | * * If the source is not an ArrayView, a new ArrayView is created with the provided source. 146 | * * If the source is an ArrayView and the `readonly` parameter is specified as `true`, 147 | * a new readonly ArrayView is created. 148 | * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned. 149 | * 150 | * @param array|ArrayViewInterface $source The source array or view. 151 | * @param bool|null $readonly Flag indicating if the view is readonly. 152 | * 153 | * @throws ValueError if the array is not sequential. 154 | * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view. 155 | * 156 | * @see ArrayView::toView() for creating views. 157 | */ 158 | public function __construct(&$source, ?bool $readonly = null) 159 | { 160 | $this->checkSequentialArgument($source); 161 | 162 | $this->source = &$source; 163 | $this->readonly = $readonly ?? (($source instanceof ArrayViewInterface) ? $source->isReadonly() : false); 164 | $this->parentView = ($source instanceof ArrayViewInterface) ? $source : null; 165 | 166 | if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) { 167 | throw new ReadonlyError("Cannot create non-readonly view for readonly source."); 168 | } 169 | } 170 | 171 | /** 172 | * Returns the array representation of the view. 173 | * 174 | * ##### Example 175 | * ```php 176 | * $source = [1, 2, 3, 4, 5]; 177 | * $view = ArrayView::toView($source); 178 | * $view->toArray(); // [1, 2, 3, 4, 5] 179 | * ``` 180 | * 181 | * @return array The array representation of the view. 182 | */ 183 | public function toArray(): array 184 | { 185 | return [...$this]; 186 | } 187 | 188 | /** 189 | * Returns a subview of this view based on a selector or string slice. 190 | * 191 | * ##### Example (using selector objects) 192 | * ```php 193 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 194 | * 195 | * $subview = ArrayView::toView($source) 196 | * ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] 197 | * ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] 198 | * ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] 199 | * ->subview(new SliceSelector('1:')); // [5, 7] 200 | * 201 | * $subview[':'] = [55, 77]; 202 | * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 203 | * ``` 204 | * 205 | * ##### Example (using short objects) 206 | * ```php 207 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 208 | * 209 | * $subview = ArrayView::toView($source) 210 | * ->subview('::2') // [1, 3, 5, 7, 9] 211 | * ->subview([true, false, true, true, true]) // [1, 5, 7, 9] 212 | * ->subview([0, 1, 2]) // [1, 5, 7] 213 | * ->subview('1:'); // [5, 7] 214 | * 215 | * $subview[':'] = [55, 77]; 216 | * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10] 217 | * ``` 218 | * 219 | * ##### Readonly example 220 | * ```php 221 | * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 222 | * $subview = ArrayView::toView($source)->subview('::2'); 223 | * 224 | * $subview[':']; // [1, 3, 5, 7, 9] 225 | * $subview[':'] = [11, 33, 55, 77, 99]; // throws ReadonlyError 226 | * $subview[0] = [11]; // throws ReadonlyError 227 | * ``` 228 | * 229 | * @template S of string|array|ArrayViewInterface|ArraySelectorInterface Selector type. 230 | * 231 | * @param S $selector The selector or string to filter the subview. 232 | * @param bool|null $readonly Flag indicating if the subview should be read-only. 233 | * 234 | * @return ArrayViewInterface A new view representing the subview of this view. 235 | * 236 | * @throws IndexError if the selector is IndexListSelector and some indexes are out of range. 237 | * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view. 238 | * @throws KeyError if the selector is not valid (e.g. non-sequential array). 239 | */ 240 | public function subview($selector, bool $readonly = null): ArrayViewInterface 241 | { 242 | return $this->toSelector($selector)->select($this, $readonly); 243 | } 244 | 245 | /** 246 | * Sets new values for the elements in the view. 247 | * 248 | * ##### Example 249 | * ```php 250 | * $source = [1, 2, 3, 4, 5]; 251 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] 252 | * 253 | * $subview->set([11, 33, 55]); 254 | * $subview->toArray(); // [11, 33, 55] 255 | * 256 | * $source; // [11, 2, 33, 4, 55] 257 | * ``` 258 | * 259 | * @param array|ArrayViewInterface|T $newValues The new values to set. 260 | * 261 | * @return ArrayView this view. 262 | * 263 | * @throws ValueError if the $newValues is not sequential array. 264 | * @throws SizeError if size of $newValues not equals to size of the view. 265 | */ 266 | public function set($newValues): self 267 | { 268 | $this->checkSequentialArgument($newValues); 269 | 270 | if (!\is_array($newValues) && !($newValues instanceof ArrayViewInterface)) { 271 | $size = \count($this); 272 | for ($i = 0; $i < $size; $i++) { 273 | $this[$i] = $newValues; 274 | } 275 | return $this; 276 | } 277 | 278 | [$dataSize, $thisSize] = [\count($newValues), \count($this)]; 279 | if ($dataSize !== $thisSize) { 280 | throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize})."); 281 | } 282 | 283 | $size = \count($this); 284 | 285 | for ($i = 0; $i < $size; $i++) { 286 | $this[$i] = $newValues[$i]; 287 | } 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * Return iterator to iterate the view elements. 294 | * 295 | * ##### Example 296 | * ```php 297 | * $source = [1, 2, 3, 4, 5]; 298 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] 299 | * 300 | * foreach ($subview as $item) { 301 | * // 1, 3, 5 302 | * } 303 | * 304 | * print_r([...$subview]); // [1, 3, 5] 305 | * ``` 306 | * 307 | * @return \Generator 308 | */ 309 | public function getIterator(): \Generator 310 | { 311 | $size = \count($this); 312 | for ($i = 0; $i < $size; $i++) { 313 | /** @var T $item */ 314 | $item = $this[$i]; 315 | yield $item; 316 | } 317 | } 318 | 319 | /** 320 | * Return true if view is readonly, otherwise false. 321 | * 322 | * ##### Example 323 | * ```php 324 | * $source = [1, 2, 3, 4, 5]; 325 | * 326 | * $readonlyView = ArrayView::toView($source, true); 327 | * $readonlyView->isReadonly(); // true 328 | * 329 | * $readonlySubview = ArrayView::toView($source)->subview('::2', true); 330 | * $readonlySubview->isReadonly(); // true 331 | * 332 | * $view = ArrayView::toView($source); 333 | * $view->isReadonly(); // false 334 | * 335 | * $subview = ArrayView::toView($source)->subview('::2'); 336 | * $subview->isReadonly(); // false 337 | * ``` 338 | * 339 | * @return bool 340 | */ 341 | public function isReadonly(): bool 342 | { 343 | return $this->readonly; 344 | } 345 | 346 | /** 347 | * Return size of the view. 348 | * 349 | * ##### Example 350 | * ```php 351 | * $source = [1, 2, 3, 4, 5]; 352 | * 353 | * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] 354 | * count($subview); // 3 355 | * ``` 356 | * 357 | * @return int 358 | */ 359 | public function count(): int 360 | { 361 | return $this->getParentSize(); 362 | } 363 | 364 | /** 365 | * Get the size of the parent view or source array. 366 | * 367 | * @return int The size of the parent view or source array. 368 | */ 369 | protected function getParentSize(): int 370 | { 371 | return ($this->parentView !== null) 372 | ? \count($this->parentView) 373 | : \count($this->source); 374 | } 375 | 376 | /** 377 | * Convert the given index to a valid index within the source array. 378 | * 379 | * @param int $i The index to convert. 380 | * 381 | * @return int The converted index within the source array. 382 | * 383 | * @throws IndexError if the index is out of range and $throwError is true. 384 | */ 385 | protected function convertIndex(int $i): int 386 | { 387 | return Util::normalizeIndex($i, \count($this->source)); 388 | } 389 | 390 | /** 391 | * Check if a numeric offset exists in the source array. 392 | * 393 | * @param numeric $offset The numeric offset to check. 394 | * 395 | * @return bool Returns true if the numeric offset exists in the source, false otherwise. 396 | */ 397 | private function numericOffsetExists($offset): bool 398 | { 399 | // Non-string must be integer 400 | if (!\is_string($offset) && !\is_int($offset)) { 401 | return false; 402 | } 403 | 404 | // Numeric string must be integer 405 | if (!\is_integer($offset + 0)) { 406 | return false; 407 | } 408 | 409 | try { 410 | $index = $this->convertIndex(intval($offset)); 411 | } catch (IndexError $e) { 412 | return false; 413 | } 414 | 415 | return \is_array($this->source) 416 | ? \array_key_exists($index, $this->source) 417 | : $this->source->offsetExists($index); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for ArrayView PHP. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | class_name: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | -------------------------------------------------------------------------------- /tests/unit/ArrayIndexListView/IssetTest.php: -------------------------------------------------------------------------------- 1 | subview(new IndexListSelector($indexes)); 18 | 19 | $existIndexes = [ 20 | ...range(0, \count($indexes) - 1), 21 | ...range(-1, -\count($indexes)), 22 | ]; 23 | 24 | foreach ($existIndexes as $index) { 25 | $this->assertTrue(isset($subview[$index]), $index); 26 | } 27 | } 28 | 29 | /** 30 | * @dataProvider dataProviderForIssetSingleFalse 31 | */ 32 | public function testIssetSingleFalse(array $source, array $indexes, array $expected) 33 | { 34 | $view = ArrayView::toView($source); 35 | $subview = $view->subview(new IndexListSelector($indexes)); 36 | 37 | foreach ($expected as $index) { 38 | $this->assertFalse(isset($subview[$index]), $index); 39 | } 40 | } 41 | 42 | /** 43 | * @dataProvider dataProviderForIssetSelectorTrue 44 | */ 45 | public function testIssetSelectorTrue(array $source, array $indexes) 46 | { 47 | $view = ArrayView::toView($source); 48 | 49 | $this->assertTrue(isset($view[new IndexListSelector($indexes)])); 50 | $this->assertTrue(isset($view[$indexes])); 51 | 52 | $subview = $view->subview(new IndexListSelector($indexes)); 53 | $this->assertSame(\count($indexes), \count($subview)); 54 | 55 | $subview = $view[new IndexListSelector($indexes)]; 56 | $this->assertSame(\count($indexes), \count($subview)); 57 | } 58 | 59 | /** 60 | * @dataProvider dataProviderForIssetSelectorFalse 61 | */ 62 | public function testIssetSelectorFalse(array $source, array $indexes) 63 | { 64 | $view = ArrayView::toView($source); 65 | 66 | $this->assertFalse(isset($view[new IndexListSelector($indexes)])); 67 | 68 | $this->expectException(IndexError::class); 69 | $_ = $view[new IndexListSelector($indexes)]; 70 | } 71 | 72 | public function dataProviderForIssetSingleTrue(): array 73 | { 74 | return [ 75 | [[1], [0]], 76 | [[1], [0, 0]], 77 | [[1], [0, 0, 0]], 78 | [[1, 2], [0]], 79 | [[1, 2], [1]], 80 | [[1, 2], [0, 1]], 81 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, 7]], 82 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [7, 5, 3, 1]], 83 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 3, 7]], 84 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 7, 8]], 85 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 5, 5, 3]], 86 | ]; 87 | } 88 | 89 | public function dataProviderForIssetSingleFalse(): array 90 | { 91 | return [ 92 | [[], [], [-2, -1, 0, 1, 2]], 93 | [[1], [], [-2, -1, 0, 1, 2]], 94 | [[1, 2, 3], [], [-2, -1, 0, 1, 2]], 95 | [[1], [0], [-3, -2, 1, 2]], 96 | [[1], [0, 0], [-5, -4, -3, 2, 3, 4]], 97 | [[1], [0, 0, 0], [-6, -5, -4, 3, 4, 5]], 98 | [[1, 2], [0], [-3, -2, 1, 2]], 99 | [[1, 2], [1], [-3, -2, 1, 2]], 100 | [[1, 2], [0, 1], [-5, -4, -3, 2, 3, 4]], 101 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, 7], [-7, -6, -5, 4, 5, 6]], 102 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [7, 5, 3, 1], [-7, -6, -5, 4, 5, 6]], 103 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 3, 7], [-7, -6, -5, 4, 5, 6]], 104 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 7, 8], [-7, -6, -5, 4, 5, 6]], 105 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 5, 5, 3], [-8, -7, -6, 5, 6, 7]], 106 | ]; 107 | } 108 | 109 | public function dataProviderForIssetSelectorTrue(): array 110 | { 111 | return [ 112 | [[1], []], 113 | [[1], [0]], 114 | [[1], [-1]], 115 | [[1], [0, 0]], 116 | [[1], [0, 0, 0]], 117 | [[1, 2], []], 118 | [[1, 2], [0]], 119 | [[1, 2], [-1, -2]], 120 | [[1, 2], [1]], 121 | [[1, 2], [0, 1]], 122 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], []], 123 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, 7]], 124 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [7, 5, 3, 1]], 125 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 3, 7]], 126 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 7, 8]], 127 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], []], 128 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 5, 5, 3]], 129 | ]; 130 | } 131 | 132 | public function dataProviderForIssetSelectorFalse(): array 133 | { 134 | return [ 135 | [[1], [0, 1]], 136 | [[1], [1, -1, -2]], 137 | [[1], [0, 1, 0, -1, -2]], 138 | [[1], [1, -1]], 139 | [[1], [0, 0, -2]], 140 | [[1, 2], [2]], 141 | [[1, 2], [1, 2]], 142 | [[1, 2], [0, 1, 2]], 143 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, -10]], 144 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [9, 5, 3, 1]], 145 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 10, 9, 7]], 146 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [-10, 1, 7, 10]], 147 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 50, 5, 3]], 148 | ]; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/unit/ArrayIndexListView/ReadTest.php: -------------------------------------------------------------------------------- 1 | assertSame($indexes, $selector->getValue()); 21 | 22 | $subview = $view->subview($selector); 23 | $this->assertInstanceOf(ArrayIndexListView::class, $subview); 24 | 25 | $this->assertSame($expected, [...$subview]); 26 | $this->assertSame(\count($expected), \count($subview)); 27 | 28 | for ($i = 0; $i < \count($subview); ++$i) { 29 | $this->assertSame($expected[$i], $subview[$i]); 30 | } 31 | 32 | for ($i = 0; $i < \count($view); ++$i) { 33 | $this->assertSame($source[$i], $view[$i]); 34 | } 35 | 36 | $this->assertSame($source, $view->toArray()); 37 | $this->assertSame($expected, $subview->toArray()); 38 | 39 | $this->assertSame($source, [...$view]); 40 | $this->assertSame($expected, [...$subview]); 41 | } 42 | 43 | /** 44 | * @dataProvider dataProviderForRead 45 | */ 46 | public function testReadByIndex(array $source, array $indexes, array $expected) 47 | { 48 | $view = ArrayView::toView($source); 49 | $subArray = $view[new IndexListSelector($indexes)]; 50 | 51 | $this->assertSame($expected, $subArray); 52 | $this->assertSame(\count($expected), \count($subArray)); 53 | 54 | for ($i = 0; $i < \count($subArray); ++$i) { 55 | $this->assertSame($expected[$i], $subArray[$i]); 56 | } 57 | 58 | for ($i = 0; $i < \count($view); ++$i) { 59 | $this->assertSame($source[$i], $view[$i]); 60 | } 61 | 62 | $this->assertSame($source, $view->toArray()); 63 | $this->assertSame($source, [...$view]); 64 | $this->assertSame($expected, $subArray); 65 | } 66 | 67 | /** 68 | * @dataProvider dataProviderForRead 69 | */ 70 | public function testReadByArrayIndex(array $source, array $indexes, array $expected) 71 | { 72 | $view = ArrayView::toView($source); 73 | $subArray = $view[$indexes]; 74 | 75 | $this->assertSame($expected, $subArray); 76 | $this->assertSame(\count($expected), \count($subArray)); 77 | 78 | for ($i = 0; $i < \count($subArray); ++$i) { 79 | $this->assertSame($expected[$i], $subArray[$i]); 80 | } 81 | 82 | for ($i = 0; $i < \count($view); ++$i) { 83 | $this->assertSame($source[$i], $view[$i]); 84 | } 85 | 86 | $this->assertSame($source, $view->toArray()); 87 | $this->assertSame($source, [...$view]); 88 | $this->assertSame($expected, $subArray); 89 | } 90 | 91 | /** 92 | * @dataProvider dataProviderForRead 93 | */ 94 | public function testReadByArrayViewIndex(array $source, array $indexes, array $expected) 95 | { 96 | $view = ArrayView::toView($source); 97 | $subArray = $view[ArrayView::toView($indexes)]; 98 | 99 | $this->assertSame($expected, $subArray); 100 | $this->assertSame(\count($expected), \count($subArray)); 101 | 102 | for ($i = 0; $i < \count($subArray); ++$i) { 103 | $this->assertSame($expected[$i], $subArray[$i]); 104 | } 105 | 106 | for ($i = 0; $i < \count($view); ++$i) { 107 | $this->assertSame($source[$i], $view[$i]); 108 | } 109 | 110 | $this->assertSame($source, $view->toArray()); 111 | $this->assertSame($source, [...$view]); 112 | $this->assertSame($expected, $subArray); 113 | } 114 | 115 | public function dataProviderForRead(): array 116 | { 117 | return [ 118 | [[], [], []], 119 | [[1], [], []], 120 | [[1, 2, 3], [], []], 121 | [[1], [0], [1]], 122 | [[1], [-1], [1]], 123 | [[1], [0, 0], [1, 1]], 124 | [[1], [0, -1], [1, 1]], 125 | [[1], [0, 0, 0], [1, 1, 1]], 126 | [[1, 2], [0], [1]], 127 | [[1, 2], [1], [2]], 128 | [[1, 2], [-1], [2]], 129 | [[1, 2], [-2], [1]], 130 | [[1, 2], [0, 1], [1, 2]], 131 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 3, 5, 7], [2, 4, 6, 8]], 132 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [7, 5, 3, 1], [8, 6, 4, 2]], 133 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 3, 7], [2, 6, 4, 8]], 134 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 7, 8], [1, 2, 8, 9]], 135 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 5, 5, 3], [2, 2, 6, 6, 4]], 136 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [-1, -1, 5, 5, 3], [9, 9, 6, 6, 4]], 137 | ]; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/unit/ArrayIndexListView/WriteTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, [...$view]); 20 | $this->assertSame($expected, $source); 21 | } 22 | 23 | /** 24 | * @dataProvider dataProviderForMaskSubviewWrite 25 | */ 26 | public function testWriteByArrayIndex(array $source, array $indexes, array $toWrite, array $expected) 27 | { 28 | $view = ArrayView::toView($source); 29 | 30 | $view[$indexes] = $toWrite; 31 | 32 | $this->assertSame($expected, [...$view]); 33 | $this->assertSame($expected, $source); 34 | } 35 | 36 | /** 37 | * @dataProvider dataProviderForMaskSubviewWrite 38 | */ 39 | public function testWriteByArrayViewIndex(array $source, array $indexes, array $toWrite, array $expected) 40 | { 41 | $view = ArrayView::toView($source); 42 | 43 | $view[ArrayView::toView($indexes)] = $toWrite; 44 | 45 | $this->assertSame($expected, [...$view]); 46 | $this->assertSame($expected, $source); 47 | } 48 | 49 | /** 50 | * @dataProvider dataProviderForMaskSubviewWrite 51 | */ 52 | public function testWriteBySubview(array $source, $config, array $toWrite, array $expected) 53 | { 54 | $view = ArrayView::toView($source); 55 | 56 | $view->subview(new IndexListSelector($config))[':'] = $toWrite; 57 | 58 | $this->assertSame($expected, [...$view]); 59 | $this->assertSame($expected, $source); 60 | } 61 | 62 | public function dataProviderForMaskSubviewWrite(): array 63 | { 64 | return [ 65 | [ 66 | [], 67 | [], 68 | [], 69 | [], 70 | ], 71 | [ 72 | [1], 73 | [], 74 | [], 75 | [1], 76 | ], 77 | [ 78 | [1, 2, 3], 79 | [], 80 | [], 81 | [1, 2, 3], 82 | ], 83 | [ 84 | [1], 85 | [0], 86 | [2], 87 | [2], 88 | ], 89 | [ 90 | [1], 91 | [0, 0], 92 | [3, 3], 93 | [3], 94 | ], 95 | [ 96 | [1], 97 | [0, 0, 0], 98 | [4, 4, 4], 99 | [4], 100 | ], 101 | [ 102 | [1, 2], 103 | [0], 104 | [2], 105 | [2, 2], 106 | ], 107 | [ 108 | [1, 2], 109 | [1], 110 | [3], 111 | [1, 3], 112 | ], 113 | [ 114 | [1, 2], 115 | [0, 1], 116 | [2, 3], 117 | [2, 3], 118 | ], 119 | [ 120 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 121 | [1, 3, 5, 7], 122 | [3, 5, 7, 9], 123 | [1, 3, 3, 5, 5, 7, 7, 9, 9], 124 | ], 125 | [ 126 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 127 | [7, 5, 3, 1], 128 | [9, 7, 5, 3], 129 | [1, 3, 3, 5, 5, 7, 7, 9, 9], 130 | ], 131 | [ 132 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 133 | [1, 5, 3, 7], 134 | [3, 7, 5, 9], 135 | [1, 3, 3, 5, 5, 7, 7, 9, 9], 136 | ], 137 | [ 138 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 139 | [0, 1, 7, 8], 140 | [2, 3, 9, 10], 141 | [2, 3, 3, 4, 5, 6, 7, 9, 10], 142 | ], 143 | [ 144 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 145 | [1, 1, 5, 5, 3], 146 | [4, 4, 8, 8, 5], 147 | [1, 4, 3, 5, 5, 8, 7, 8, 9], 148 | ], 149 | ]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/unit/ArrayMaskView/IssetTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(isset($view[new MaskSelector($boolMask)])); 20 | $this->assertTrue(isset($view[$boolMask])); 21 | $this->assertTrue(isset($view[ArrayView::toView($boolMask)])); 22 | } 23 | 24 | /** 25 | * @dataProvider dataProviderForIssetSelectorFalse 26 | */ 27 | public function testIssetSelectorFalse(array $source, array $boolMask) 28 | { 29 | $view = ArrayView::toView($source); 30 | 31 | $this->assertFalse(isset($view[new MaskSelector($boolMask)])); 32 | $this->assertFalse(isset($view[$boolMask])); 33 | $this->assertFalse(isset($view[ArrayView::toView($boolMask)])); 34 | 35 | $this->expectException(SizeError::class); 36 | $_ = $view[new MaskSelector($boolMask)]; 37 | } 38 | 39 | public function dataProviderForIssetSelectorTrue(): array 40 | { 41 | return [ 42 | [ 43 | [], 44 | [], 45 | [], 46 | ], 47 | [ 48 | [1], 49 | [false], 50 | [], 51 | ], 52 | [ 53 | [1, 2, 3], 54 | [false, false, false], 55 | [], 56 | ], 57 | [ 58 | [1], 59 | [true], 60 | [1], 61 | ], 62 | [ 63 | [1, 2], 64 | [true, false], 65 | [1], 66 | ], 67 | [ 68 | [1, 2], 69 | [false, true], 70 | [2], 71 | ], 72 | [ 73 | [1, 2], 74 | [true, true], 75 | [1, 2], 76 | ], 77 | [ 78 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 79 | [false, true, false, true, false, true, false, true, false], 80 | [2, 4, 6, 8], 81 | ], 82 | [ 83 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 84 | [true, true, true, false, false, false, false, false, true], 85 | [1, 2, 3, 9], 86 | ], 87 | ]; 88 | } 89 | 90 | public function dataProviderForIssetSelectorFalse(): array 91 | { 92 | return [ 93 | [ 94 | [], 95 | [false], 96 | [], 97 | ], 98 | [ 99 | [], 100 | [true], 101 | [], 102 | ], 103 | [ 104 | [], 105 | [false, true], 106 | [], 107 | ], 108 | [ 109 | [1], 110 | [false, false], 111 | [], 112 | ], 113 | [ 114 | [1], 115 | [true, false], 116 | [], 117 | ], 118 | [ 119 | [1], 120 | [true, true, true], 121 | [], 122 | ], 123 | [ 124 | [1, 2, 3], 125 | [false], 126 | [], 127 | ], 128 | [ 129 | [1, 2, 3], 130 | [false, false], 131 | [], 132 | ], 133 | [ 134 | [1, 2, 3], 135 | [false, false, false, false], 136 | [], 137 | ], 138 | [ 139 | [1, 2, 3], 140 | [false, false, false, false, false], 141 | [], 142 | ], 143 | [ 144 | [1, 2, 3], 145 | [true], 146 | [], 147 | ], 148 | [ 149 | [1, 2, 3], 150 | [true, true], 151 | [], 152 | ], 153 | [ 154 | [1, 2, 3], 155 | [true, true, true, true], 156 | [], 157 | ], 158 | [ 159 | [1, 2, 3], 160 | [true, true, true, true, true], 161 | [], 162 | ], 163 | [ 164 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 165 | [false, true, false, true, false, true, false, true], 166 | [2, 4, 6, 8], 167 | ], 168 | [ 169 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 170 | [true, true, true, false, false, false, false, false, true, false], 171 | [1, 2, 3, 9], 172 | ], 173 | ]; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/unit/ArrayMaskView/ReadTest.php: -------------------------------------------------------------------------------- 1 | assertSame($mask, $selector->getValue()); 20 | 21 | $subview = $view->subview($selector); 22 | $this->assertInstanceOf(ArrayMaskView::class, $subview); 23 | 24 | $this->assertSame($expected, [...$subview]); 25 | $this->assertSame(\count($expected), \count($subview)); 26 | 27 | for ($i = 0; $i < \count($subview); ++$i) { 28 | $this->assertSame($expected[$i], $subview[$i]); 29 | } 30 | 31 | for ($i = 0; $i < \count($view); ++$i) { 32 | $this->assertSame($source[$i], $view[$i]); 33 | } 34 | 35 | $this->assertSame($source, $view->toArray()); 36 | $this->assertSame($expected, $subview->toArray()); 37 | 38 | $this->assertSame($source, [...$view]); 39 | $this->assertSame($expected, [...$subview]); 40 | } 41 | 42 | /** 43 | * @dataProvider dataProviderForRead 44 | */ 45 | public function testReadByIndex(array $source, array $mask, array $expected) 46 | { 47 | $view = ArrayView::toView($source); 48 | $subArray = $view[new MaskSelector($mask)]; 49 | 50 | $this->assertSame($expected, $subArray); 51 | $this->assertSame(\count($expected), \count($subArray)); 52 | 53 | for ($i = 0; $i < \count($subArray); ++$i) { 54 | $this->assertSame($expected[$i], $subArray[$i]); 55 | } 56 | 57 | for ($i = 0; $i < \count($view); ++$i) { 58 | $this->assertSame($source[$i], $view[$i]); 59 | } 60 | 61 | $this->assertSame($source, $view->toArray()); 62 | $this->assertSame($source, [...$view]); 63 | $this->assertSame($expected, $subArray); 64 | } 65 | 66 | /** 67 | * @dataProvider dataProviderForRead 68 | */ 69 | public function testReadByArrayViewIndex(array $source, array $mask, array $expected) 70 | { 71 | $view = ArrayView::toView($source); 72 | $subArray = $view[ArrayView::toView($mask)]; 73 | 74 | $this->assertSame($expected, $subArray); 75 | $this->assertSame(\count($expected), \count($subArray)); 76 | 77 | for ($i = 0; $i < \count($subArray); ++$i) { 78 | $this->assertSame($expected[$i], $subArray[$i]); 79 | } 80 | 81 | for ($i = 0; $i < \count($view); ++$i) { 82 | $this->assertSame($source[$i], $view[$i]); 83 | } 84 | 85 | $this->assertSame($source, $view->toArray()); 86 | $this->assertSame($source, [...$view]); 87 | $this->assertSame($expected, $subArray); 88 | } 89 | 90 | /** 91 | * @dataProvider dataProviderForRead 92 | */ 93 | public function testReadByArrayIndex(array $source, array $mask, array $expected) 94 | { 95 | $view = ArrayView::toView($source); 96 | $subArray = $view[$mask]; 97 | 98 | $this->assertSame($expected, $subArray); 99 | $this->assertSame(\count($expected), \count($subArray)); 100 | 101 | for ($i = 0; $i < \count($subArray); ++$i) { 102 | $this->assertSame($expected[$i], $subArray[$i]); 103 | } 104 | 105 | for ($i = 0; $i < \count($view); ++$i) { 106 | $this->assertSame($source[$i], $view[$i]); 107 | } 108 | 109 | $this->assertSame($source, $view->toArray()); 110 | $this->assertSame($source, [...$view]); 111 | $this->assertSame($expected, $subArray); 112 | } 113 | 114 | public function dataProviderForRead(): array 115 | { 116 | return [ 117 | [ 118 | [], 119 | [], 120 | [], 121 | ], 122 | [ 123 | [1], 124 | [false], 125 | [], 126 | ], 127 | [ 128 | [1, 2, 3], 129 | [false, false, false], 130 | [], 131 | ], 132 | [ 133 | [1], 134 | [true], 135 | [1], 136 | ], 137 | [ 138 | [1, 2], 139 | [true, false], 140 | [1], 141 | ], 142 | [ 143 | [1, 2], 144 | [false, true], 145 | [2], 146 | ], 147 | [ 148 | [1, 2], 149 | [true, true], 150 | [1, 2], 151 | ], 152 | [ 153 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 154 | [false, true, false, true, false, true, false, true, false], 155 | [2, 4, 6, 8], 156 | ], 157 | [ 158 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 159 | [true, true, true, false, false, false, false, false, true], 160 | [1, 2, 3, 9], 161 | ], 162 | ]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/unit/ArrayMaskView/WriteTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, [...$view]); 20 | $this->assertSame($expected, $source); 21 | } 22 | 23 | /** 24 | * @dataProvider dataProviderForMaskSubviewWrite 25 | */ 26 | public function testWriteByArrayIndex(array $source, array $mask, array $toWrite, array $expected) 27 | { 28 | $view = ArrayView::toView($source); 29 | 30 | $view[$mask] = $toWrite; 31 | 32 | $this->assertSame($expected, [...$view]); 33 | $this->assertSame($expected, $source); 34 | } 35 | 36 | /** 37 | * @dataProvider dataProviderForMaskSubviewWrite 38 | */ 39 | public function testWriteByArrayViewIndex(array $source, array $mask, array $toWrite, array $expected) 40 | { 41 | $view = ArrayView::toView($source); 42 | 43 | $view[ArrayView::toView($mask)] = $toWrite; 44 | 45 | $this->assertSame($expected, [...$view]); 46 | $this->assertSame($expected, $source); 47 | } 48 | 49 | /** 50 | * @dataProvider dataProviderForMaskSubviewWrite 51 | */ 52 | public function testWriteBySubview(array $source, array $mask, array $toWrite, array $expected) 53 | { 54 | $view = ArrayView::toView($source); 55 | 56 | $view->subview(new MaskSelector($mask))[':'] = $toWrite; 57 | 58 | $this->assertSame($expected, [...$view]); 59 | $this->assertSame($expected, $source); 60 | } 61 | 62 | public function dataProviderForMaskSubviewWrite(): array 63 | { 64 | return [ 65 | [ 66 | [], 67 | [], 68 | [], 69 | [], 70 | ], 71 | [ 72 | [1], 73 | [false], 74 | [], 75 | [1], 76 | ], 77 | [ 78 | [1, 2, 3], 79 | [false, false, false], 80 | [], 81 | [1, 2, 3], 82 | ], 83 | [ 84 | [1], 85 | [true], 86 | [2], 87 | [2], 88 | ], 89 | [ 90 | [1, 2], 91 | [true, false], 92 | [2], 93 | [2, 2], 94 | ], 95 | [ 96 | [1, 2], 97 | [false, true], 98 | [3], 99 | [1, 3], 100 | ], 101 | [ 102 | [1, 2], 103 | [true, true], 104 | [2, 3], 105 | [2, 3], 106 | ], 107 | [ 108 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 109 | [false, true, false, true, false, true, false, true, false], 110 | [3, 5, 7, 9], 111 | [1, 3, 3, 5, 5, 7, 7, 9, 9], 112 | ], 113 | [ 114 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 115 | [true, true, true, false, false, false, false, false, true], 116 | [2, 3, 4, 10], 117 | [2, 3, 4, 4, 5, 6, 7, 8, 10], 118 | ], 119 | ]; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/unit/ArraySliceView/IssetTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(isset($view[new SliceSelector($slice)])); 18 | } 19 | 20 | /** 21 | * @dataProvider dataProviderForIssetSelectorStringTrue 22 | */ 23 | public function testIssetSelectorStringTrue(array $source, string $slice) 24 | { 25 | $view = ArrayView::toView($source); 26 | $this->assertTrue(isset($view[$slice])); 27 | } 28 | 29 | public function dataProviderForIssetSelectorStringTrue(): array 30 | { 31 | return [ 32 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6'], 33 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6:1'], 34 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6:2'], 35 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8'], 36 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8:1'], 37 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8:2'], 38 | 39 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-1::-1'], 40 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-1:0:-1'], 41 | 42 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:9:1'], 43 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:9:2'], 44 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:9:2'], 45 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:10:1'], 46 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:10:2'], 47 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-9:9:1'], 48 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-9:9:2'], 49 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-10:10:1'], 50 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-10:10:2'], 51 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-5:10:1'], 52 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-5:100:2'], 53 | 54 | [[], '0:'], 55 | [[], '0:0'], 56 | [[], '0:0:1'], 57 | [[], '1:-1'], 58 | [[], '-1:-1'], 59 | [[], '-2:-1'], 60 | [[], '-2:-1:2'], 61 | [[], '-1:0:-1'], 62 | [[1], '0:'], 63 | [[1], '0:1'], 64 | [[1], '0:1:1'], 65 | [[1], '0:1:2'], 66 | [[1], '0:-1'], 67 | [[1], '0:-1:1'], 68 | [[1], '0:-1:2'], 69 | [[1], '0:10:100'], 70 | [[1], '1:10:100'], 71 | [[1], '0:'], 72 | [[1, 2, 3], '0:0:1'], 73 | [[1], '1:'], 74 | [[1, 2], '1:0'], 75 | [[1, 2], '1::-1'], 76 | [[1, 2], '0:1'], 77 | [[1, 2], '1:1'], 78 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1::2'], 79 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '::2'], 80 | ]; 81 | } 82 | 83 | public function dataProviderForIssetSelectorArrayTrue(): array 84 | { 85 | return [ 86 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6]], 87 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6,1]], 88 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6,2]], 89 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8]], 90 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8,1]], 91 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8,2]], 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/unit/ArraySliceView/ReadTest.php: -------------------------------------------------------------------------------- 1 | subview($config); 23 | 24 | $this->assertInstanceOf(ArraySliceView::class, $subview); 25 | 26 | $this->assertSame($expected, [...$subview]); 27 | $this->assertSame(\count($expected), \count([...$subview])); 28 | 29 | for ($i = 0; $i < \count($subview); ++$i) { 30 | $this->assertSame($expected[$i], $subview[$i]); 31 | } 32 | 33 | for ($i = 0; $i < \count($view); ++$i) { 34 | $this->assertSame($source[$i], $view[$i]); 35 | } 36 | 37 | $this->assertSame($source, $view->toArray()); 38 | $this->assertSame($expected, $subview->toArray()); 39 | 40 | $this->assertSame($source, [...$view]); 41 | $this->assertSame($expected, [...$subview]); 42 | } 43 | 44 | /** 45 | * @dataProvider dataProviderForRead 46 | * @dataProvider dataProviderForSliceInBoundsPythonAutoGenerated 47 | * @dataProvider dataProviderForSliceOutOfBoundsPythonAutoGenerated 48 | */ 49 | public function testReadByIndex(array $source, string $config, array $expected) 50 | { 51 | $view = ArrayView::toView($source); 52 | $slicedArray = $view[$config]; 53 | 54 | $this->assertSame($expected, $slicedArray); 55 | $this->assertSame(\count($expected), \count($slicedArray)); 56 | 57 | for ($i = 0; $i < \count($slicedArray); ++$i) { 58 | $this->assertSame($expected[$i], $slicedArray[$i]); 59 | } 60 | 61 | for ($i = 0; $i < \count($view); ++$i) { 62 | $this->assertSame($source[$i], $view[$i]); 63 | } 64 | 65 | $this->assertSame($source, $view->toArray()); 66 | $this->assertSame($source, [...$view]); 67 | $this->assertSame($expected, $slicedArray); 68 | } 69 | 70 | /** 71 | * @dataProvider dataProviderForRead 72 | * @dataProvider dataProviderForSliceArraySubviewRead 73 | * @dataProvider dataProviderForSliceInBoundsPythonAutoGenerated 74 | * @dataProvider dataProviderForSliceOutOfBoundsPythonAutoGenerated 75 | */ 76 | public function testReadByMethodAndIndex(array $source, $config, array $expected) 77 | { 78 | $view = ArrayView::toView($source); 79 | $selector = new SliceSelector($config); 80 | $this->assertSame($selector, $selector->getValue()); 81 | 82 | $slicedArray = $view->subview($selector)[':']; 83 | $this->assertSame($expected, $slicedArray); 84 | $this->assertSame(\count($expected), \count($slicedArray)); 85 | 86 | for ($i = 0; $i < \count($slicedArray); ++$i) { 87 | $this->assertSame($expected[$i], $slicedArray[$i]); 88 | } 89 | 90 | for ($i = 0; $i < \count($view); ++$i) { 91 | $this->assertSame($source[$i], $view[$i]); 92 | } 93 | 94 | $this->assertSame($source, $view->toArray()); 95 | $this->assertSame($source, [...$view]); 96 | $this->assertSame($expected, $slicedArray); 97 | } 98 | 99 | public function dataProviderForRead(): array 100 | { 101 | return [ 102 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6', [2, 3, 4, 5, 6]], 103 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6:1', [2, 3, 4, 5, 6]], 104 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:6:2', [2, 4, 6]], 105 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8', [3, 4, 5, 6, 7, 8]], 106 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8:1', [3, 4, 5, 6, 7, 8]], 107 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '2:8:2', [3, 5, 7]], 108 | 109 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-1::-1', [9, 8, 7, 6, 5, 4, 3, 2, 1]], 110 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-1:0:-1', [9, 8, 7, 6, 5, 4, 3, 2]], 111 | 112 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:9:1', [1, 2, 3, 4, 5, 6, 7, 8, 9]], 113 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:9:2', [1, 3, 5, 7, 9]], 114 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1:9:2', [2, 4, 6, 8]], 115 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:10:1', [1, 2, 3, 4, 5, 6, 7, 8, 9]], 116 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '0:10:2', [1, 3, 5, 7, 9]], 117 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-9:9:1', [1, 2, 3, 4, 5, 6, 7, 8, 9]], 118 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-9:9:2', [1, 3, 5, 7, 9]], 119 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-10:10:1', [1, 2, 3, 4, 5, 6, 7, 8, 9]], 120 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-10:10:2', [1, 3, 5, 7, 9]], 121 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-5:10:1', [5, 6, 7, 8, 9]], 122 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '-5:100:2', [5, 7, 9]], 123 | 124 | [[], '0:', []], 125 | [[], '0:0', []], 126 | [[], '0:0:1', []], 127 | [[], '1:-1', []], 128 | [[], '-1:-1', []], 129 | [[], '-2:-1', []], 130 | [[], '-2:-1:2', []], 131 | [[], '-1:0:-1', []], 132 | [[1], '0:', [1]], 133 | [[1], '0:1', [1]], 134 | [[1], '0:1:1', [1]], 135 | [[1], '0:1:2', [1]], 136 | [[1], '0:-1', []], 137 | [[1], '0:-1:1', []], 138 | [[1], '0:-1:2', []], 139 | [[1], '0:10:100', [1]], 140 | [[1], '1:10:100', []], 141 | [[1], '0:', [1]], 142 | [[1, 2, 3], '0:0:1', []], 143 | [[1], '1:', []], 144 | [[1, 2], '1:0', []], 145 | [[1, 2], '1::-1', [2, 1]], 146 | [[1, 2], '0:1', [1]], 147 | [[1, 2], '1:1', []], 148 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '1::2', [2, 4, 6, 8]], 149 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], '::2', [1, 3, 5, 7, 9]], 150 | ]; 151 | } 152 | 153 | public function dataProviderForSliceArraySubviewRead(): array 154 | { 155 | return [ 156 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6], [2, 3, 4, 5, 6]], 157 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6,1], [2, 3, 4, 5, 6]], 158 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,6,2], [2, 4, 6]], 159 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8], [3, 4, 5, 6, 7, 8]], 160 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8,1], [3, 4, 5, 6, 7, 8]], 161 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [2,8,2], [3, 5, 7]], 162 | ]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/unit/ArraySliceView/WriteTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, [...$view]); 20 | $this->assertSame($expected, $source); 21 | } 22 | 23 | /** 24 | * @dataProvider dataProviderForSliceSubviewWrite 25 | * @dataProvider dataProviderForSliceArraySubviewWrite 26 | */ 27 | public function testWriteBySubview(array $source, $config, array $toWrite, array $expected) 28 | { 29 | $view = ArrayView::toView($source); 30 | 31 | $view->subview(new SliceSelector($config))[':'] = $toWrite; 32 | 33 | $this->assertSame($expected, [...$view]); 34 | $this->assertSame($expected, $source); 35 | } 36 | 37 | public function dataProviderForSliceSubviewWrite(): array 38 | { 39 | return [ 40 | [[], ':', [], []], 41 | [[1], ':', [11], [11]], 42 | [[1, 2, 3], ':', [2, 4, 6], [2, 4, 6]], 43 | [[1, 2, 3], '0:', [2, 4, 6], [2, 4, 6]], 44 | [[1, 2, 3], ':3', [2, 4, 6], [2, 4, 6]], 45 | [[1, 2, 3], '0:3', [2, 4, 6], [2, 4, 6]], 46 | [[1, 2, 3], '1:', [22, 33], [1, 22, 33]], 47 | [[1, 2, 3], ':2', [11, 22], [11, 22, 3]], 48 | [[1, 2, 3], ':-1', [11, 22], [11, 22, 3]], 49 | [[1, 2, 3, 4, 5, 6], '::2', [77, 88, 99], [77, 2, 88, 4, 99, 6]], 50 | [[1, 2, 3, 4, 5, 6], '::-2', [77, 88, 99], [1, 99, 3, 88, 5, 77]], 51 | [[1, 2, 3, 4, 5, 6], '1::2', [77, 88, 99], [1, 77, 3, 88, 5, 99]], 52 | [[1, 2, 3, 4, 5, 6], '-2::-2', [77, 88, 99], [99, 2, 88, 4, 77, 6]], 53 | [[1, 2, 3, 4, 5, 6, 7, 8], ':-2:2', [77, 88, 99], [77, 2, 88, 4, 99, 6, 7, 8]], 54 | [[1, 2, 3, 4, 5, 6, 7, 8], ':6:2', [77, 88, 99], [77, 2, 88, 4, 99, 6, 7, 8]], 55 | [[1, 2, 3, 4, 5, 6, 7, 8], '1:-1:2', [77, 88, 99], [1, 77, 3, 88, 5, 99, 7, 8]], 56 | ]; 57 | } 58 | 59 | public function dataProviderForSliceArraySubviewWrite(): array 60 | { 61 | return [ 62 | [[], [], [], []], 63 | [[], [null, null], [], []], 64 | [[1], [null, null], [11], [11]], 65 | [[1, 2, 3], [null, null], [2, 4, 6], [2, 4, 6]], 66 | [[1, 2, 3], [0,], [2, 4, 6], [2, 4, 6]], 67 | [[1, 2, 3], [0, 3], [2, 4, 6], [2, 4, 6]], 68 | [[1, 2, 3], [0, 3], [2, 4, 6], [2, 4, 6]], 69 | [[1, 2, 3], [1,], [22, 33], [1, 22, 33]], 70 | [[1, 2, 3], [null, 2], [11, 22], [11, 22, 3]], 71 | [[1, 2, 3], [null, -1], [11, 22], [11, 22, 3]], 72 | [[1, 2, 3, 4, 5, 6], [null, null, 2], [77, 88, 99], [77, 2, 88, 4, 99, 6]], 73 | [[1, 2, 3, 4, 5, 6], [null, null, -2], [77, 88, 99], [1, 99, 3, 88, 5, 77]], 74 | [[1, 2, 3, 4, 5, 6], [1, null, 2], [77, 88, 99], [1, 77, 3, 88, 5, 99]], 75 | [[1, 2, 3, 4, 5, 6], [-2, null, -2], [77, 88, 99], [99, 2, 88, 4, 77, 6]], 76 | [[1, 2, 3, 4, 5, 6, 7, 8], [null, -2, 2], [77, 88, 99], [77, 2, 88, 4, 99, 6, 7, 8]], 77 | [[1, 2, 3, 4, 5, 6, 7, 8], [null, 6, 2], [77, 88, 99], [77, 2, 88, 4, 99, 6, 7, 8]], 78 | [[1, 2, 3, 4, 5, 6, 7, 8], [1, -1, 2], [77, 88, 99], [1, 77, 3, 88, 5, 99, 7, 8]], 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/ArrayView/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $number); 30 | } 31 | 32 | public static function dataProviderForPositiveIndex(): array 33 | { 34 | return [ 35 | [ 36 | [10, 20, 30, 40, 50], 37 | 0, 38 | 10 39 | ], 40 | [ 41 | [10, 20, 30, 40, 50], 42 | 1, 43 | 20 44 | ], 45 | [ 46 | [10, 20, 30, 40, 50], 47 | 2, 48 | 30 49 | ], 50 | [ 51 | [10, 20, 30, 40, 50], 52 | 3, 53 | 40 54 | ], 55 | [ 56 | [10, 20, 30, 40, 50], 57 | 4, 58 | 50 59 | ], 60 | ]; 61 | } 62 | 63 | /** 64 | * @dataProvider dataProviderForPositiveStringIndex 65 | * @param array $array 66 | * @param string $i 67 | * @param int $expected 68 | * @return void 69 | */ 70 | public function testPositiveStringIndex(array $array, string $i, int $expected): void 71 | { 72 | // Given 73 | $arrayView = ArrayView::toView($array); 74 | 75 | // When 76 | $number = $arrayView[$i]; 77 | 78 | // Then 79 | $this->assertSame($expected, $number); 80 | } 81 | 82 | public static function dataProviderForPositiveStringIndex(): array 83 | { 84 | return [ 85 | [ 86 | [10, 20, 30, 40, 50], 87 | '0', 88 | 10 89 | ], 90 | [ 91 | [10, 20, 30, 40, 50], 92 | '1', 93 | 20 94 | ], 95 | [ 96 | [10, 20, 30, 40, 50], 97 | '2', 98 | 30 99 | ], 100 | [ 101 | [10, 20, 30, 40, 50], 102 | '3', 103 | 40 104 | ], 105 | [ 106 | [10, 20, 30, 40, 50], 107 | '4', 108 | 50 109 | ], 110 | ]; 111 | } 112 | 113 | /** 114 | * @dataProvider dataProviderForNegativeIndex 115 | * @param array $array 116 | * @param int $i 117 | * @param int $expected 118 | * @return void 119 | */ 120 | public function testNegativeIndex(array $array, int $i, int $expected): void 121 | { 122 | // Given 123 | $arrayView = ArrayView::toView($array); 124 | 125 | // When 126 | $number = $arrayView[$i]; 127 | 128 | // Then 129 | $this->assertSame($expected, $number); 130 | } 131 | 132 | public static function dataProviderForNegativeIndex(): array 133 | { 134 | return [ 135 | [ 136 | [10, 20, 30, 40, 50], 137 | -0, 138 | 10 139 | ], 140 | [ 141 | [10, 20, 30, 40, 50], 142 | -1, 143 | 50 144 | ], 145 | [ 146 | [10, 20, 30, 40, 50], 147 | -2, 148 | 40 149 | ], 150 | [ 151 | [10, 20, 30, 40, 50], 152 | -3, 153 | 30 154 | ], 155 | [ 156 | [10, 20, 30, 40, 50], 157 | -4, 158 | 20 159 | ], 160 | [ 161 | [10, 20, 30, 40, 50], 162 | -5, 163 | 10 164 | ], 165 | ]; 166 | } 167 | 168 | /** 169 | * @dataProvider dataProviderForNegativeStringIndex 170 | * @param array $array 171 | * @param string $i 172 | * @param int $expected 173 | * @return void 174 | */ 175 | public function testNegativeStringIndex(array $array, string $i, int $expected): void 176 | { 177 | // Given 178 | $arrayView = ArrayView::toView($array); 179 | 180 | // When 181 | $number = $arrayView[$i]; 182 | 183 | // Then 184 | $this->assertSame($expected, $number); 185 | } 186 | 187 | public static function dataProviderForNegativeStringIndex(): array 188 | { 189 | return [ 190 | [ 191 | [10, 20, 30, 40, 50], 192 | '-0', 193 | 10 194 | ], 195 | [ 196 | [10, 20, 30, 40, 50], 197 | '-1', 198 | 50 199 | ], 200 | [ 201 | [10, 20, 30, 40, 50], 202 | '-2', 203 | 40 204 | ], 205 | [ 206 | [10, 20, 30, 40, 50], 207 | '-3', 208 | 30 209 | ], 210 | [ 211 | [10, 20, 30, 40, 50], 212 | '-4', 213 | 20 214 | ], 215 | [ 216 | [10, 20, 30, 40, 50], 217 | '-5', 218 | 10 219 | ], 220 | ]; 221 | } 222 | 223 | /** 224 | * @dataProvider dataProviderForIndexesLargerThanTwo 225 | * @param mixed $i 226 | * @return void 227 | */ 228 | public function testPositiveIndexError($i): void 229 | { 230 | // Given 231 | $array = [10, 20, 30]; 232 | $arrayView = ArrayView::toView($array); 233 | 234 | // Then 235 | $this->expectException(IndexError::class); 236 | $this->expectExceptionMessageMatches('/Index \d+ is out of range/'); 237 | 238 | // When 239 | $number = $arrayView[$i]; 240 | } 241 | 242 | public static function dataProviderForIndexesLargerThanTwo(): array 243 | { 244 | return [ 245 | [3], 246 | ['3'], 247 | [4], 248 | ['4'], 249 | [100], 250 | ['100'], 251 | ]; 252 | } 253 | 254 | /** 255 | * @dataProvider dataProviderForIndexesSmallerThanThanNegativeThree 256 | * @param mixed $i 257 | * @return void 258 | */ 259 | public function testNegativeIndexError($i): void 260 | { 261 | // Given 262 | $array = [10, 20, 30]; 263 | $arrayView = ArrayView::toView($array); 264 | 265 | // Then 266 | $this->expectException(IndexError::class); 267 | $this->expectExceptionMessageMatches('/Index -\d+ is out of range/'); 268 | 269 | // When 270 | $number = $arrayView[$i]; 271 | } 272 | 273 | public static function dataProviderForIndexesSmallerThanThanNegativeThree(): array 274 | { 275 | return [ 276 | [-4], 277 | ['-4'], 278 | [-5], 279 | ['-5'], 280 | [-100], 281 | ['-100'], 282 | ]; 283 | } 284 | 285 | /** 286 | * @dataProvider dataProviderForNonIntegerIndexes 287 | * @param mixed $i 288 | * @return void 289 | */ 290 | public function testNonIntegerKeyError($i): void 291 | { 292 | // Given 293 | $array = [10, 20, 30]; 294 | $arrayView = ArrayView::toView($array); 295 | 296 | // Then 297 | $this->expectException(KeyError::class); 298 | 299 | // When 300 | $number = $arrayView[$i]; 301 | } 302 | 303 | public static function dataProviderForNonIntegerIndexes(): array 304 | { 305 | return [ 306 | ['a'], 307 | ['1'], 308 | ['Ⅱ'], 309 | ['三'], 310 | ['④'], 311 | ['V'], 312 | ['six'], 313 | ]; 314 | } 315 | 316 | /** 317 | * @dataProvider dataProviderForFloatIndexes 318 | * @param mixed $i 319 | * @return void 320 | */ 321 | public function testNonIntegerIndexError($i): void 322 | { 323 | // Given 324 | $array = [10, 20, 30]; 325 | $arrayView = ArrayView::toView($array); 326 | 327 | // Then 328 | $this->expectException(IndexError::class); 329 | 330 | // When 331 | $number = $arrayView[$i]; 332 | } 333 | 334 | public static function dataProviderForFloatIndexes(): array 335 | { 336 | return [ 337 | [0.1], 338 | ['0.5'], 339 | ['1.5'], 340 | [2.0], 341 | [3.1], 342 | ['45.66'], 343 | [\NAN], 344 | [\INF], 345 | [-\INF], 346 | ]; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /tests/unit/ArrayView/IssetTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(isset($view[$slice])); 19 | } 20 | 21 | /** 22 | * @dataProvider dataProviderForIssetPipeSelectorTrue 23 | */ 24 | public function testIssetPipeSelectorTrue(array $source, array $selectors) 25 | { 26 | $view = ArrayView::toView($source); 27 | $this->assertTrue(isset($view[new PipeSelector($selectors)])); 28 | } 29 | 30 | /** 31 | * @dataProvider dataProviderForIssetPipeSelectorFalse 32 | */ 33 | public function testIssetPipeSelectorFalse(array $source, array $selectors) 34 | { 35 | $view = ArrayView::toView($source); 36 | $this->assertFalse(isset($view[new PipeSelector($selectors)])); 37 | } 38 | 39 | public function dataProviderForIssetSelectorFalse(): array 40 | { 41 | return [ 42 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], null], 43 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], true], 44 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], false], 45 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], 1.1], 46 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], INF], 47 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], -INF], 48 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1,66]], 49 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], 'asd'], 50 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], ['a' => 1]], 51 | [[1, 2, 3, 4, 5, 6, 7, 8, 9], new \ArrayObject(['a' => 1])], 52 | ]; 53 | } 54 | 55 | public function dataProviderForIssetPipeSelectorTrue(): array 56 | { 57 | return [ 58 | [ 59 | [1, 2, 3, 4, 5], 60 | [ 61 | new MaskSelector([true, false, true, false, true]), 62 | ], 63 | ], 64 | [ 65 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 66 | [ 67 | new IndexListSelector([0, 1, 2]), 68 | new MaskSelector([true, false, true]), 69 | ], 70 | ], 71 | [ 72 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 73 | [ 74 | new IndexListSelector([0, 1, 2]), 75 | new MaskSelector([true, false, true]), 76 | new IndexListSelector([0, 1]), 77 | ], 78 | ], 79 | [ 80 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 81 | [ 82 | new IndexListSelector([0, 1, 2]), 83 | new MaskSelector([true, false, true]), 84 | new IndexListSelector([-2]), 85 | ], 86 | ], 87 | [ 88 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 89 | [ 90 | new IndexListSelector([0, 1, 2]), 91 | new PipeSelector([ 92 | new MaskSelector([true, false, true]), 93 | new IndexListSelector([-2]), 94 | ]), 95 | ], 96 | ], 97 | ]; 98 | } 99 | 100 | public function dataProviderForIssetPipeSelectorFalse(): array 101 | { 102 | return [ 103 | [ 104 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 105 | [ 106 | new MaskSelector([]), 107 | ], 108 | ], 109 | [ 110 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 111 | [ 112 | new IndexListSelector([0, 1, 2]), 113 | new MaskSelector([true, false]), 114 | ], 115 | ], 116 | [ 117 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 118 | [ 119 | new IndexListSelector([0, 1, 2]), 120 | new MaskSelector([true, false, true]), 121 | new IndexListSelector([0, 2]), 122 | ], 123 | ], 124 | [ 125 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 126 | [ 127 | new IndexListSelector([0, 1, 2]), 128 | new MaskSelector([true, false, true]), 129 | new IndexListSelector([-3]), 130 | ], 131 | ], 132 | [ 133 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 134 | [ 135 | new IndexListSelector([0, 1, 2]), 136 | new PipeSelector([ 137 | new MaskSelector([true, false, true]), 138 | new IndexListSelector([-3]), 139 | ]), 140 | ], 141 | ], 142 | ]; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/unit/ArrayView/ReadonlyTest.php: -------------------------------------------------------------------------------- 1 | fail(); 24 | } catch (\Exception $e) { 25 | $this->assertInstanceOf(ReadonlyError::class, $e); 26 | $this->assertSame("Cannot modify a readonly view.", $e->getMessage()); 27 | } 28 | } 29 | 30 | $this->assertSame($expected, [...$view]); 31 | } 32 | 33 | /** 34 | * @dataProvider dataProviderForReadonly 35 | * @dataProvider dataProviderForReadonlySubview 36 | */ 37 | public function testAll(array $source, callable $readonlyViewGetter, array $expected) 38 | { 39 | $view = $readonlyViewGetter($source); 40 | 41 | try { 42 | $view[':'] = [...$view]; 43 | $this->fail(); 44 | } catch (\Exception $e) { 45 | $this->assertInstanceOf(ReadonlyError::class, $e); 46 | $this->assertSame("Cannot modify a readonly view.", $e->getMessage()); 47 | $this->assertSame($expected, [...$view]); 48 | } 49 | 50 | $this->assertSame($expected, [...$view]); 51 | } 52 | 53 | /** 54 | * @dataProvider dataProviderForReadonly 55 | * @dataProvider dataProviderForReadonlySubview 56 | */ 57 | public function testApply(array $source, callable $readonlyViewGetter, array $expected) 58 | { 59 | $view = $readonlyViewGetter($source); 60 | 61 | try { 62 | $view->apply(fn ($item) => $item); 63 | $this->fail(); 64 | } catch (\Exception $e) { 65 | $this->assertInstanceOf(ReadonlyError::class, $e); 66 | $this->assertSame("Cannot modify a readonly view.", $e->getMessage()); 67 | $this->assertSame($expected, [...$view]); 68 | } 69 | 70 | $this->assertSame($expected, [...$view]); 71 | } 72 | 73 | /** 74 | * @dataProvider dataProviderForReadonly 75 | * @dataProvider dataProviderForReadonlySubview 76 | */ 77 | public function testApplyWith(array $source, callable $readonlyViewGetter, array $expected) 78 | { 79 | $view = $readonlyViewGetter($source); 80 | 81 | try { 82 | $view->applyWith([...$view], fn ($lhs, $rhs) => $lhs + $rhs); 83 | $this->fail(); 84 | } catch (\Exception $e) { 85 | $this->assertInstanceOf(ReadonlyError::class, $e); 86 | $this->assertSame("Cannot modify a readonly view.", $e->getMessage()); 87 | $this->assertSame($expected, [...$view]); 88 | } 89 | 90 | $this->assertSame($expected, [...$view]); 91 | } 92 | 93 | /** 94 | * @dataProvider dataProviderForReadonlyCreateError 95 | */ 96 | public function testCreateError(array $source, callable $readonlyViewGetter) 97 | { 98 | $this->expectException(ReadonlyError::class); 99 | $this->expectExceptionMessage("Cannot create non-readonly view for readonly source."); 100 | $readonlyViewGetter($source); 101 | } 102 | 103 | public function dataProviderForReadonly(): array 104 | { 105 | return [ 106 | [ 107 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 108 | fn (array &$source) => ArrayView::toView($source, true), 109 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 110 | ], 111 | [ 112 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 113 | fn (array &$source) => ArrayView::toUnlinkedView(ArrayView::toView($source, true), true), 114 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 115 | ], 116 | [ 117 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 118 | fn (array &$source) => ArrayView::toView($source, true) 119 | ->subview('::2'), 120 | [1, 3, 5, 7, 9], 121 | ], 122 | [ 123 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 124 | fn (array &$source) => ArrayView::toView($source, true) 125 | ->subview('::2', true) 126 | ->subview(new MaskSelector([true, false, true, false, true])), 127 | [1, 5, 9], 128 | ], 129 | [ 130 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 131 | fn (array &$source) => ArrayView::toView($source, true) 132 | ->subview('::2') 133 | ->subview(new MaskSelector([true, false, true, false, true])) 134 | ->subview(new IndexListSelector([0, 2])), 135 | [1, 9], 136 | ], 137 | [ 138 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 139 | fn (array &$source) => ArrayView::toView($source, true) 140 | ->subview('::2') 141 | ->subview(new MaskSelector([true, false, true, false, true])) 142 | ->subview(new IndexListSelector([0, 2])) 143 | ->subview('1:'), 144 | [9], 145 | ], 146 | [ 147 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 148 | fn (array &$source) => ArrayView::toView($source, true) 149 | ->subview(new MaskSelector([true, false, true, false, true, false, true, false, true, false])) 150 | ->subview(new MaskSelector([true, false, true, false, true])) 151 | ->subview(new MaskSelector([true, false, true])) 152 | ->subview(new MaskSelector([false, true])), 153 | [9], 154 | ], 155 | [ 156 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 157 | fn (array &$source) => ArrayView::toView($source, true) 158 | ->subview(new MaskSelector([true, false, true, false, true, false, true, false, true, false])) 159 | ->subview(new MaskSelector([true, false, true, false, true])) 160 | ->subview(new MaskSelector([true, false, true])), 161 | [1, 9], 162 | ], 163 | [ 164 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 165 | fn (array &$source) => ArrayView::toView($source, true) 166 | ->subview(new IndexListSelector([0, 2, 4, 6, 8])) 167 | ->subview(new IndexListSelector([0, 2, 4])) 168 | ->subview(new IndexListSelector([0, 2])) 169 | ->subview(new IndexListSelector([1])), 170 | [9], 171 | ], 172 | [ 173 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 174 | fn (array &$source) => ArrayView::toView($source, true) 175 | ->subview('::2') 176 | ->subview('::2') 177 | ->subview('::2'), 178 | [1, 9], 179 | ], 180 | [ 181 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 182 | fn (array &$source) => ArrayView::toView($source, true) 183 | ->subview('::2') 184 | ->subview('::2') 185 | ->subview('::2') 186 | ->subview('1:'), 187 | [9], 188 | ], 189 | ]; 190 | } 191 | 192 | public function dataProviderForReadonlySubview(): array 193 | { 194 | return [ 195 | [ 196 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 197 | fn (array &$source) => ArrayView::toView($source, true) 198 | ->subview('::2', true), 199 | [1, 3, 5, 7, 9], 200 | ], 201 | [ 202 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 203 | fn (array &$source) => ArrayView::toUnlinkedView(new ArrayView($source)) 204 | ->subview('::2', true), 205 | [1, 3, 5, 7, 9], 206 | ], 207 | [ 208 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 209 | fn (array &$source) => ArrayView::toView($source, true) 210 | ->subview('::2', true) 211 | ->subview(new MaskSelector([true, false, true, false, true])), 212 | [1, 5, 9], 213 | ], 214 | [ 215 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 216 | fn (array &$source) => ArrayView::toView($source, true) 217 | ->subview('::2') 218 | ->subview(new MaskSelector([true, false, true, false, true]), true) 219 | ->subview(new IndexListSelector([0, 2])), 220 | [1, 9], 221 | ], 222 | [ 223 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 224 | fn (array &$source) => ArrayView::toView($source, true) 225 | ->subview('::2', true) 226 | ->subview(new MaskSelector([true, false, true, false, true])) 227 | ->subview(new IndexListSelector([0, 2]), true) 228 | ->subview('1:'), 229 | [9], 230 | ], 231 | [ 232 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 233 | fn (array &$source) => ArrayView::toView($source, true) 234 | ->subview(new MaskSelector([true, false, true, false, true, false, true, false, true, false]), true) 235 | ->subview(new MaskSelector([true, false, true, false, true]), true) 236 | ->subview(new MaskSelector([true, false, true]), true) 237 | ->subview(new MaskSelector([false, true]), true), 238 | [9], 239 | ], 240 | [ 241 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 242 | fn (array &$source) => ArrayView::toView($source, true) 243 | ->subview(new MaskSelector([true, false, true, false, true, false, true, false, true, false])) 244 | ->subview(new MaskSelector([true, false, true, false, true])) 245 | ->subview(new MaskSelector([true, false, true]), true), 246 | [1, 9], 247 | ], 248 | [ 249 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 250 | fn (array &$source) => ArrayView::toView($source, true) 251 | ->subview(new IndexListSelector([0, 2, 4, 6, 8])) 252 | ->subview(new IndexListSelector([0, 2, 4])) 253 | ->subview(new IndexListSelector([0, 2])) 254 | ->subview(new IndexListSelector([1]), true), 255 | [9], 256 | ], 257 | [ 258 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 259 | fn (array &$source) => ArrayView::toView($source, true) 260 | ->subview('::2') 261 | ->subview('::2', true) 262 | ->subview('::2'), 263 | [1, 9], 264 | ], 265 | [ 266 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 267 | fn (array &$source) => ArrayView::toView($source, true) 268 | ->subview('::2') 269 | ->subview('::2') 270 | ->subview('::2') 271 | ->subview('1:', true), 272 | [9], 273 | ], 274 | ]; 275 | } 276 | 277 | public function dataProviderForReadonlyCreateError(): array 278 | { 279 | return [ 280 | [ 281 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 282 | fn (array &$source) => ArrayView::toView($source, true) 283 | ->subview('::2', false), 284 | ], 285 | [ 286 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 287 | fn (array &$source) => ArrayView::toUnlinkedView(ArrayView::toView($source), true) 288 | ->subview('::2', false), 289 | ], 290 | [ 291 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 292 | fn (array &$source) => ArrayView::toView($source, true) 293 | ->subview('::2') 294 | ->subview(new MaskSelector([true, false, true, false, true]), false), 295 | ], 296 | [ 297 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 298 | fn (array &$source) => ArrayView::toView($source) 299 | ->subview('::2', true) 300 | ->subview(new MaskSelector([true, false, true, false, true])) 301 | ->subview(new IndexListSelector([0, 2]), false), 302 | ], 303 | [ 304 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 305 | fn (array &$source) => ArrayView::toView($source, true) 306 | ->subview('::2', false) 307 | ->subview(new MaskSelector([true, false, true, false, true])) 308 | ->subview(new IndexListSelector([0, 2])) 309 | ->subview('1:'), 310 | ], 311 | [ 312 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 313 | fn (array &$source) => ArrayView::toView($source, false) 314 | ->subview(new MaskSelector([true, false, true, false, true, false, true, false, true, false])) 315 | ->subview(new MaskSelector([true, false, true, false, true]), true) 316 | ->subview(new MaskSelector([true, false, true]), false) 317 | ->subview(new MaskSelector([false, true])), 318 | ], 319 | [ 320 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 321 | fn (array &$source) => ArrayView::toView($source, true) 322 | ->subview('::2', false) 323 | ->subview('::2') 324 | ->subview('::2'), 325 | ], 326 | [ 327 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 328 | fn (array &$source) => ArrayView::toView($source, false) 329 | ->subview('::2', true) 330 | ->subview('::2', false) 331 | ->subview('::2', true) 332 | ->subview('1:', false), 333 | ], 334 | ]; 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /tests/unit/Examples/BenchTest.php: -------------------------------------------------------------------------------- 1 | n; 16 | $originalArray = range(0, $n); 17 | $indexes = range(0, $n, 2); 18 | 19 | $ts = \microtime(true); 20 | $view = ArrayView::toView($originalArray); 21 | $result = $view[$indexes]; 22 | $spent = \round(\microtime(true) - $ts, 4); 23 | 24 | echo " [testReadIndexListView] SPENT: {$spent} s\n"; 25 | ob_flush(); 26 | } 27 | 28 | public function testReadIndexListPure() 29 | { 30 | $n = $this->n; 31 | $originalArray = range(0, $n); 32 | $indexes = range(0, $n, 2); 33 | $n_2 = \count($indexes); 34 | 35 | $ts = \microtime(true); 36 | $result = []; 37 | for ($i = 0; $i < $n_2; $i++) { 38 | $result[$i] = $originalArray[$indexes[$i]]; 39 | } 40 | $spent = \round(\microtime(true) - $ts, 4); 41 | 42 | echo " [testReadIndexListPure] SPENT: {$spent} s\n"; 43 | ob_flush(); 44 | } 45 | 46 | public function testReadSliceView() 47 | { 48 | $n = $this->n; 49 | $originalArray = range(0, $n); 50 | 51 | $ts = \microtime(true); 52 | $view = ArrayView::toView($originalArray); 53 | $result = $view['::2']; 54 | $spent = \round(\microtime(true) - $ts, 4); 55 | 56 | echo " [testReadSliceView] SPENT: {$spent} s\n"; 57 | ob_flush(); 58 | } 59 | 60 | public function testReadSlicePure() 61 | { 62 | $n = $this->n; 63 | $n_2 = intval($n / 2); 64 | $originalArray = range(0, $n); 65 | 66 | $ts = \microtime(true); 67 | $result = []; 68 | for ($i = 0; $i < $n_2; $i++) { 69 | $result[$i] = $originalArray[$i * 2]; 70 | } 71 | $spent = \round(\microtime(true) - $ts, 4); 72 | 73 | echo " [testReadSlicePure] SPENT: {$spent} s\n"; 74 | ob_flush(); 75 | } 76 | 77 | public function testWriteSliceView() 78 | { 79 | $n = $this->n; 80 | $n_2 = intval($n / 2); 81 | $originalArray = range(0, $n); 82 | $toWrite = range(0, $n_2); 83 | 84 | $ts = \microtime(true); 85 | $view = ArrayView::toView($originalArray); 86 | $view['::2'] = $toWrite; 87 | $spent = \round(\microtime(true) - $ts, 4); 88 | 89 | echo " [testWriteSliceView] SPENT: {$spent} s\n"; 90 | ob_flush(); 91 | } 92 | 93 | public function testWriteSlicePure() 94 | { 95 | $n = $this->n; 96 | $n_2 = intval($n / 2); 97 | $originalArray = range(0, $n); 98 | $toWrite = range(0, $n_2); 99 | 100 | $ts = \microtime(true); 101 | for ($i = 0; $i < $n_2; $i++) { 102 | $originalArray[$i * 2] = $toWrite[$i]; 103 | } 104 | $spent = \round(\microtime(true) - $ts, 4); 105 | 106 | echo " [testWriteSlicePure] SPENT: {$spent} s\n"; 107 | ob_flush(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/unit/Examples/ExamplesTest.php: -------------------------------------------------------------------------------- 1 | assertSame([2, 4, 6], $originalView['1:7:2']); 21 | $this->assertSame([1, 2, 3], $originalView[':3']); 22 | $this->assertSame([9, 8, 7, 6, 5, 4, 3, 2, 1], $originalView['::-1']); 23 | 24 | $this->assertSame(3, $originalView[2]); 25 | $this->assertSame(5, $originalView[4]); 26 | $this->assertSame(9, $originalView[-1]); 27 | $this->assertSame(8, $originalView[-2]); 28 | 29 | $originalView['1:7:2'] = [22, 44, 66]; 30 | $this->assertSame([1, 22, 3, 44, 5, 66, 7, 8, 9], $originalArray); 31 | } 32 | 33 | public function testSubview() 34 | { 35 | $originalArray = [1, 2, 3, 4, 5]; 36 | $originalView = ArrayView::toView($originalArray); 37 | 38 | $this->assertSame( 39 | [1, 3, 5], 40 | $originalView->subview(new MaskSelector([true, false, true, false, true]))->toArray(), 41 | ); 42 | $this->assertSame( 43 | [2, 3, 5], 44 | $originalView->subview(new IndexListSelector([1, 2, 4]))->toArray(), 45 | ); 46 | $this->assertSame( 47 | [5, 4, 3, 2, 1], 48 | $originalView->subview(new SliceSelector('::-1'))->toArray(), 49 | ); 50 | 51 | $this->assertSame( 52 | [1, 3, 5], 53 | $originalView->subview([true, false, true, false, true])->toArray(), 54 | ); 55 | $this->assertSame( 56 | [2, 3, 5], 57 | $originalView->subview([1, 2, 4])->toArray(), 58 | ); 59 | $this->assertSame( 60 | [5, 4, 3, 2, 1], 61 | $originalView->subview('::-1')->toArray(), 62 | ); 63 | 64 | $originalView->subview(new MaskSelector([true, false, true, false, true])) 65 | ->apply(fn(int $x) => $x * 10); 66 | 67 | $this->assertSame([10, 2, 30, 4, 50], $originalArray); 68 | } 69 | 70 | public function testSubarray() 71 | { 72 | $originalArray = [1, 2, 3, 4, 5]; 73 | $originalView = ArrayView::toView($originalArray); 74 | 75 | $this->assertSame( 76 | [1, 3, 5], 77 | $originalView[new MaskSelector([true, false, true, false, true])], 78 | ); 79 | $this->assertSame( 80 | [2, 3, 5], 81 | $originalView[new IndexListSelector([1, 2, 4])], 82 | ); 83 | $this->assertSame( 84 | [5, 4, 3, 2, 1], 85 | $originalView[new SliceSelector('::-1')], 86 | ); 87 | 88 | $this->assertSame( 89 | [1, 3, 5], 90 | $originalView[[true, false, true, false, true]], 91 | ); 92 | $this->assertSame( 93 | [2, 3, 5], 94 | $originalView[[1, 2, 4]], 95 | ); 96 | $this->assertSame( 97 | [5, 4, 3, 2, 1], 98 | $originalView['::-1'], 99 | ); 100 | 101 | $originalView[new MaskSelector([true, false, true, false, true])] = [10, 30, 50]; 102 | 103 | $this->assertSame([10, 2, 30, 4, 50], $originalArray); 104 | } 105 | 106 | public function testCombinedSubview() 107 | { 108 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 109 | 110 | $subview = ArrayView::toView($originalArray) 111 | ->subview(new SliceSelector('::2')) // [1, 3, 5, 7, 9] 112 | ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9] 113 | ->subview(new IndexListSelector([0, 1, 2])) // [1, 5, 7] 114 | ->subview(new SliceSelector('1:')); // [5, 7] 115 | 116 | $this->assertSame([5, 7], $subview->toArray()); 117 | $this->assertSame([5, 7], $subview[':']); 118 | 119 | $subview[':'] = [55, 77]; 120 | $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); 121 | } 122 | 123 | public function testCombinedSubviewShort() 124 | { 125 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 126 | 127 | $subview = ArrayView::toView($originalArray) 128 | ->subview('::2') // [1, 3, 5, 7, 9] 129 | ->subview([true, false, true, true, true]) // [1, 5, 7, 9] 130 | ->subview([0, 1, 2]) // [1, 5, 7] 131 | ->subview('1:'); // [5, 7] 132 | 133 | $this->assertSame([5, 7], $subview->toArray()); 134 | $this->assertSame([5, 7], $subview[':']); 135 | 136 | $subview[':'] = [55, 77]; 137 | $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); 138 | } 139 | 140 | public function testSelectorsPipe() 141 | { 142 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 143 | 144 | $selector = new PipeSelector([ 145 | new SliceSelector('::2'), 146 | new MaskSelector([true, false, true, true, true]), 147 | new IndexListSelector([0, 1, 2]), 148 | new SliceSelector('1:'), 149 | ]); 150 | 151 | $view = ArrayView::toView($originalArray); 152 | $this->assertTrue(isset($view[$selector])); 153 | 154 | $subview = $view->subview($selector); 155 | 156 | $this->assertSame([5, 7], $subview->toArray()); 157 | $this->assertSame([5, 7], $subview[':']); 158 | 159 | $subview[':'] = [55, 77]; 160 | $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); 161 | } 162 | 163 | public function testSelectorsPipeNested() 164 | { 165 | $originalArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 166 | 167 | $selector = new PipeSelector([ 168 | new SliceSelector('::2'), 169 | new PipeSelector([ 170 | new MaskSelector([true, false, true, true, true]), 171 | new IndexListSelector([0, 1, 2]), 172 | ]), 173 | new SliceSelector('1:'), 174 | ]); 175 | 176 | $view = ArrayView::toView($originalArray); 177 | $this->assertTrue(isset($view[$selector])); 178 | 179 | $subview = $view->subview($selector); 180 | 181 | $this->assertSame([5, 7], $subview->toArray()); 182 | $this->assertSame([5, 7], $subview[':']); 183 | 184 | $subview[':'] = [55, 77]; 185 | $this->assertSame([1, 2, 3, 4, 55, 6, 77, 8, 9, 10], $originalArray); 186 | } 187 | 188 | public function testMap() 189 | { 190 | $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 191 | $subview = ArrayView::toView($source)->subview('::2'); 192 | 193 | $actual = $subview->map(fn ($x) => $x * 10); 194 | $this->assertSame([10, 30, 50, 70, 90], $actual); 195 | } 196 | 197 | public function testMapWith() 198 | { 199 | $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 200 | $subview = ArrayView::toView($source)->subview('::2'); 201 | $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); 202 | 203 | $data = [9, 27, 45, 63, 81]; 204 | 205 | $actual = $subview->mapWith($data, fn ($lhs, $rhs) => $lhs + $rhs); 206 | $this->assertSame([10, 30, 50, 70, 90], $actual); 207 | } 208 | 209 | public function testIs() 210 | { 211 | $source = [1, 2, 3, 4, 5, 6]; 212 | $view = ArrayView::toView($source); 213 | 214 | $mask = $view->is(fn ($x) => $x % 2 === 0); 215 | $this->assertSame([false, true, false, true, false, true], $mask->getValue()); 216 | 217 | $this->assertSame([2, 4, 6], $view->subview($mask)->toArray()); 218 | $this->assertSame([2, 4, 6], $view[$mask]); 219 | 220 | $view[$mask] = [20, 40, 60]; 221 | $this->assertSame([1, 20, 3, 40, 5, 60], $source); 222 | } 223 | 224 | public function testMatch() 225 | { 226 | $source = [1, 2, 3, 4, 5, 6]; 227 | $view = ArrayView::toView($source); 228 | 229 | $mask = $view->match(fn ($x) => $x % 2 === 0); 230 | $this->assertSame([false, true, false, true, false, true], $mask->getValue()); 231 | 232 | $this->assertSame([2, 4, 6], $view->subview($mask)->toArray()); 233 | $this->assertSame([2, 4, 6], $view[$mask]); 234 | 235 | $view[$mask] = [20, 40, 60]; 236 | $this->assertSame([1, 20, 3, 40, 5, 60], $source); 237 | } 238 | 239 | public function testMatchWith() 240 | { 241 | $source = [1, 2, 3, 4, 5, 6]; 242 | $view = ArrayView::toView($source); 243 | 244 | $data = [6, 5, 4, 3, 2, 1]; 245 | 246 | $mask = $view->matchWith($data, fn ($lhs, $rhs) => $lhs > $rhs); 247 | $this->assertSame([false, false, false, true, true, true], $mask->getValue()); 248 | 249 | $this->assertSame([4, 5, 6], $view->subview($mask)->toArray()); 250 | $this->assertSame([4, 5, 6], $view[$mask]); 251 | 252 | $view[$mask] = [40, 50, 60]; 253 | $this->assertSame([1, 2, 3, 40, 50, 60], $source); 254 | } 255 | 256 | public function testApply() 257 | { 258 | $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 259 | $subview = ArrayView::toView($source)->subview('::2'); 260 | 261 | $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); 262 | 263 | $subview->apply(fn ($x) => $x * 10); 264 | 265 | $this->assertSame([10, 30, 50, 70, 90], $subview->toArray()); 266 | $this->assertSame([10, 2, 30, 4, 50, 6, 70, 8, 90, 10], $source); 267 | } 268 | 269 | public function testApplyWith() 270 | { 271 | $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 272 | $subview = ArrayView::toView($source)->subview('::2'); 273 | 274 | $this->assertSame([1, 3, 5, 7, 9], $subview->toArray()); 275 | 276 | $data = [9, 27, 45, 63, 81]; 277 | 278 | $subview->applyWith($data, fn ($lhs, $rhs) => $lhs + $rhs); 279 | $this->assertSame([10, 30, 50, 70, 90], $subview->toArray()); 280 | 281 | $this->assertSame([10, 2, 30, 4, 50, 6, 70, 8, 90, 10], $source); 282 | } 283 | 284 | public function testFilter() 285 | { 286 | $source = [1, 2, 3, 4, 5, 6]; 287 | $view = ArrayView::toView($source); 288 | 289 | $filtered = $view->filter(fn ($x) => $x % 2 === 0); 290 | $this->assertSame([2, 4, 6], $filtered->toArray()); 291 | 292 | $filtered[':'] = [20, 40, 60]; 293 | $this->assertSame([20, 40, 60], $filtered->toArray()); 294 | 295 | $this->assertSame([1, 20, 3, 40, 5, 60], $source); 296 | } 297 | 298 | public function testCount() 299 | { 300 | $source = [1, 2, 3, 4, 5]; 301 | 302 | $subview = ArrayView::toView($source)->subview('::2'); 303 | 304 | $this->assertSame([1, 3, 5], $subview->toArray()); 305 | $this->assertCount(3, $subview); 306 | } 307 | 308 | public function testIterator() 309 | { 310 | $source = [1, 2, 3, 4, 5]; 311 | $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] 312 | 313 | $actual = []; 314 | foreach ($subview as $item) { 315 | $actual[] = $item; 316 | // 1, 3, 5 317 | } 318 | $this->assertSame([1, 3, 5], $actual); 319 | $this->assertSame([1, 3, 5], [...$subview]); 320 | } 321 | 322 | public function testIsReadonly() 323 | { 324 | $source = [1, 2, 3, 4, 5]; 325 | 326 | $readonlyView = ArrayView::toView($source, true); 327 | $this->assertTrue($readonlyView->isReadonly()); 328 | 329 | $readonlySubview = ArrayView::toView($source)->subview('::2', true); 330 | $this->assertTrue($readonlySubview->isReadonly()); 331 | 332 | $view = ArrayView::toView($source); 333 | $this->assertFalse($view->isReadonly()); 334 | 335 | $subview = ArrayView::toView($source)->subview('::2'); 336 | $this->assertFalse($subview->isReadonly()); 337 | } 338 | 339 | public function testOffsetExists() 340 | { 341 | $source = [1, 2, 3, 4, 5]; 342 | $view = ArrayView::toView($source); 343 | 344 | $this->assertTrue(isset($view[0])); 345 | $this->assertTrue(isset($view[-1])); 346 | $this->assertFalse(isset($view[10])); 347 | 348 | $this->assertTrue(isset($view[new SliceSelector('::2')])); 349 | $this->assertTrue(isset($view[new IndexListSelector([0, 2, 4])])); 350 | $this->assertFalse(isset($view[new IndexListSelector([0, 2, 10])])); 351 | $this->assertTrue(isset($view[new MaskSelector([true, true, false, false, true])])); 352 | $this->assertFalse(isset($view[new MaskSelector([true, true, false, false, true, true])])); 353 | 354 | $this->assertTrue(isset($view['::2'])); 355 | $this->assertTrue(isset($view[[0, 2, 4]])); 356 | $this->assertFalse(isset($view[[0, 2, 10]])); 357 | $this->assertTrue(isset($view[[true, true, false, false, true]])); 358 | $this->assertFalse(isset($view[[true, true, false, false, true, true]])); 359 | } 360 | 361 | public function testOffsetGet() 362 | { 363 | $source = [1, 2, 3, 4, 5]; 364 | $view = ArrayView::toView($source); 365 | 366 | $this->assertSame(1, $view[0]); 367 | $this->assertSame(5, $view[-1]); 368 | 369 | $this->assertSame([1, 3, 5], $view[new SliceSelector('::2')]); 370 | $this->assertSame([1, 3, 5], $view[new IndexListSelector([0, 2, 4])]); 371 | $this->assertSame([1, 2, 5], $view[new MaskSelector([true, true, false, false, true])]); 372 | 373 | $this->assertSame([1, 3, 5], $view['::2']); 374 | $this->assertSame([1, 3, 5], $view[[0, 2, 4]]); 375 | $this->assertSame([1, 2, 5], $view[[true, true, false, false, true]]); 376 | } 377 | 378 | public function testOffsetSet() 379 | { 380 | $source = [1, 2, 3, 4, 5]; 381 | $view = ArrayView::toView($source); 382 | 383 | $view[0] = 11; 384 | $view[-1] = 55; 385 | 386 | $this->assertSame([11, 2, 3, 4, 55], $source); 387 | 388 | $source = [1, 2, 3, 4, 5]; 389 | $view = ArrayView::toView($source); 390 | 391 | $view[new SliceSelector('::2')] = [11, 33, 55]; 392 | $this->assertSame([11, 2, 33, 4, 55], $source); 393 | 394 | $view[new IndexListSelector([1, 3])] = [22, 44]; 395 | $this->assertSame([11, 22, 33, 44, 55], $source); 396 | 397 | $view[new MaskSelector([true, false, false, false, true])] = [111, 555]; 398 | $this->assertSame([111, 22, 33, 44, 555], $source); 399 | 400 | $source = [1, 2, 3, 4, 5]; 401 | $view = ArrayView::toView($source); 402 | 403 | $view['::2'] = [11, 33, 55]; 404 | $this->assertSame([11, 2, 33, 4, 55], $source); 405 | 406 | $view[[1, 3]] = [22, 44]; 407 | $this->assertSame([11, 22, 33, 44, 55], $source); 408 | 409 | $view[[true, false, false, false, true]] = [111, 555]; 410 | $this->assertSame([111, 22, 33, 44, 555], $source); 411 | } 412 | 413 | public function testSet() 414 | { 415 | $source = [1, 2, 3, 4, 5]; 416 | $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5] 417 | 418 | $subview->set([11, 33, 55]); 419 | $this->assertSame([11, 33, 55], $subview->toArray()); 420 | $this->assertSame([11, 2, 33, 4, 55], $source); 421 | } 422 | 423 | public function testToUnlinkedView() 424 | { 425 | $source = [1, 2, 3, 4, 5]; 426 | $view = ArrayView::toUnlinkedView($source); 427 | 428 | $this->assertSame(1, $view[0]); 429 | $this->assertSame([2, 4], $view['1::2']); 430 | $view['1::2'] = [22, 44]; 431 | 432 | $this->assertSame([1, 22, 3, 44, 5], $view->toArray()); 433 | $this->assertSame([1, 2, 3, 4, 5], $source); 434 | } 435 | 436 | public function testToView() 437 | { 438 | $source = [1, 2, 3, 4, 5]; 439 | $view = ArrayView::toView($source); 440 | 441 | $this->assertSame(1, $view[0]); 442 | $this->assertSame([2, 4], $view['1::2']); 443 | $view['1::2'] = [22, 44]; 444 | 445 | $this->assertSame([1, 22, 3, 44, 5], $view->toArray()); 446 | $this->assertSame([1, 22, 3, 44, 5], $source); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /tests/unit/Structs/NormalizedSliceTest.php: -------------------------------------------------------------------------------- 1 | normalize($containerSize); 17 | $expectedSlice = new Slice(...$expected); 18 | 19 | $this->assertSame($expectedSlice->getStart(), $actual->getStart()); 20 | $this->assertSame($expectedSlice->getEnd(), $actual->getEnd()); 21 | $this->assertSame($expectedSlice->getStep(), $actual->getStep()); 22 | } 23 | 24 | public function dataProviderForToSlice(): array 25 | { 26 | return [ 27 | [':', 0, [0, 0, 1]], 28 | ['::', 0, [0, 0, 1]], 29 | ['::', 1, [0, 1, 1]], 30 | ['::', 2, [0, 2, 1]], 31 | ['0:', 0, [0, 0, 1]], 32 | ['0:', 1, [0, 1, 1]], 33 | ['1:', 0, [0, 0, 1]], 34 | ['-1:', 0, [0, 0, 1]], 35 | ['-1:', 1, [0, 1, 1]], 36 | ['0:0:', 0, [0, 0, 1]], 37 | ['0:0:', 10, [0, 0, 1]], 38 | ['1:1:', 0, [0, 0, 1]], 39 | ['1:1:', 10, [1, 1, 1]], 40 | ['-1:-1:', 0, [0, 0, 1]], 41 | ['-1:-1:', 10, [9, 9, 1]], 42 | ['1:1:-1', 0, [0, 0, -1]], 43 | ['1:1:-1', 1, [0, 0, -1]], 44 | ['1:1:-1', 2, [1, 1, -1]], 45 | ['-1:-1:-1', 0, [0, 0, -1]], 46 | ['-1:-1:-1', 1, [0, 0, -1]], 47 | ['-1:-1:-1', 2, [1, 1, -1]], 48 | ['1:2:3', 0, [0, 0, 3]], 49 | ['1:2:3', 1, [0, 0, 3]], 50 | ['1:2:3', 2, [1, 2, 3]], 51 | ['1:2:3', 3, [1, 2, 3]], 52 | ['1:2:3', 10, [1, 2, 3]], 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/unit/Structs/SliceTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Slice::isSlice($input)); 19 | $this->assertTrue(Slice::isSliceString($input)); 20 | } 21 | 22 | /** 23 | * @dataProvider dataProviderForFalse 24 | */ 25 | public function testIsSliceFalse($input) 26 | { 27 | $this->assertFalse(Slice::isSlice($input)); 28 | $this->assertFalse(Slice::isSliceString($input)); 29 | } 30 | 31 | /** 32 | * @dataProvider dataProviderForFalse 33 | */ 34 | public function testSliceError($input) 35 | { 36 | $this->expectException(ValueError::class); 37 | $strInput = \is_scalar($input) ? "{$input}" : \gettype($input); 38 | $this->expectExceptionMessage("Invalid slice: \"{$strInput}\""); 39 | 40 | Slice::toSlice($input); 41 | } 42 | 43 | /** 44 | * @dataProvider dataProviderForToSlice 45 | */ 46 | public function testToSlice($input, array $expected) 47 | { 48 | $actual = Slice::toSlice($input); 49 | $expectedSlice = new Slice(...$expected); 50 | 51 | $this->assertSame($expectedSlice->getStart(), $actual->getStart()); 52 | $this->assertSame($expectedSlice->getEnd(), $actual->getEnd()); 53 | $this->assertSame($expectedSlice->getStep(), $actual->getStep()); 54 | } 55 | 56 | /** 57 | * @dataProvider dataProviderForSliceToString 58 | */ 59 | public function testSliceToString(string $input, string $expected) 60 | { 61 | $slice = Slice::toSlice($input); 62 | $this->assertSame($expected, $slice->toString()); 63 | } 64 | 65 | /** 66 | * @dataProvider dataProviderForSliceNormalize 67 | */ 68 | public function testSliceNormalize(string $input, int $size, string $expected, array $expectedIndexes) 69 | { 70 | $slice = Slice::toSlice($input); 71 | $normalizedSlice = $slice->normalize($size); 72 | 73 | $this->assertInstanceOf(NormalizedSlice::class, $normalizedSlice); 74 | $this->assertSame($expected, $normalizedSlice->toString()); 75 | $this->assertSame($expectedIndexes, [...$normalizedSlice]); 76 | } 77 | 78 | /** 79 | * @dataProvider dataProviderForIsSliceArrayTrue 80 | */ 81 | public function testIsSliceArrayTrue(array $input) 82 | { 83 | $this->assertTrue(Slice::isSliceArray($input)); 84 | } 85 | 86 | /** 87 | * @dataProvider dataProviderForIsSliceArrayFalse 88 | */ 89 | public function testIsSliceArrayFalse($input) 90 | { 91 | $this->assertFalse(Slice::isSliceArray($input)); 92 | } 93 | 94 | public function dataProviderForTrue(): array 95 | { 96 | return [ 97 | [':'], 98 | ['::'], 99 | ['0:'], 100 | ['1:'], 101 | ['-1:'], 102 | ['0::'], 103 | ['1::'], 104 | ['-1::'], 105 | [':0'], 106 | [':1'], 107 | [':-1'], 108 | [':0:'], 109 | [':1:'], 110 | [':-1:'], 111 | ['0:0:'], 112 | ['1:1:'], 113 | ['-1:-1:'], 114 | ['1:1:-1'], 115 | ['-1:-1:-1'], 116 | ['1:2:3'], 117 | ]; 118 | } 119 | 120 | public function dataProviderForFalse(): array 121 | { 122 | return [ 123 | [''], 124 | ['0'], 125 | ['1'], 126 | ['1:::'], 127 | [':1::'], 128 | ['::1:'], 129 | [':::1'], 130 | ['test'], 131 | ['[::]'], 132 | ['a:b:c'], 133 | [0], 134 | [1], 135 | [1.1], 136 | [true], 137 | [false], 138 | [null], 139 | [new \ArrayObject([])], 140 | [['a' => 1]], 141 | ]; 142 | } 143 | 144 | public function dataProviderForToSlice(): array 145 | { 146 | return [ 147 | [':', [null, null, null]], 148 | ['::', [null, null, null]], 149 | ['0:', [0, null, null]], 150 | ['1:', [1, null, null]], 151 | ['-1:', [-1, null, null]], 152 | ['0::', [0, null, null]], 153 | ['1::', [1, null, null]], 154 | ['-1::', [-1, null, null]], 155 | [':0', [null, 0, null]], 156 | [':1', [null, 1, null]], 157 | [':-1', [null, -1, null]], 158 | [':0:', [null, 0, null]], 159 | [':1:', [null, 1, null]], 160 | [':-1:', [null, -1, null]], 161 | ['0:0:', [0, 0, null]], 162 | ['1:1:', [1, 1, null]], 163 | ['-1:-1:', [-1, -1, null]], 164 | ['1:1:-1', [1, 1, -1]], 165 | ['-1:-1:-1', [-1, -1, -1]], 166 | ['1:2:3', [1, 2, 3]], 167 | ]; 168 | } 169 | 170 | public function dataProviderForSliceToString(): array 171 | { 172 | return [ 173 | [':', '::'], 174 | ['::', '::'], 175 | ['0:', '0::'], 176 | ['1:', '1::'], 177 | ['-1:', '-1::'], 178 | ['0::', '0::'], 179 | ['1::', '1::'], 180 | ['-1::', '-1::'], 181 | [':0', ':0:'], 182 | [':1', ':1:'], 183 | [':-1', ':-1:'], 184 | [':0:', ':0:'], 185 | [':1:', ':1:'], 186 | [':-1:', ':-1:'], 187 | ['0:0:', '0:0:'], 188 | ['1:1:', '1:1:'], 189 | ['-1:-1:', '-1:-1:'], 190 | ['1:1:-1', '1:1:-1'], 191 | ['-1:-1:-1', '-1:-1:-1'], 192 | ['1:2:3', '1:2:3'], 193 | ]; 194 | } 195 | 196 | public function dataProviderForSliceNormalize(): array 197 | { 198 | return [ 199 | [':', 0, '0:0:1', []], 200 | ['::', 1, '0:1:1', [0]], 201 | ['0:', 2, '0:2:1', [0, 1]], 202 | ['1:', 5, '1:5:1', [1, 2, 3, 4]], 203 | ['-1:', 3, '2:3:1', [2]], 204 | ['0::', 10, '0:10:1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], 205 | ['1::', 0, '0:0:1', []], 206 | ['-1::', 0, '0:0:1', []], 207 | [':0', 1, '0:0:1', []], 208 | [':1', 2, '0:1:1', [0]], 209 | [':-1', 5, '0:4:1', [0, 1, 2, 3]], 210 | [':0:', 3, '0:0:1', []], 211 | [':1:', 1, '0:1:1', [0]], 212 | [':-1:', 3, '0:2:1', [0, 1]], 213 | ['0:0:', 3, '0:0:1', []], 214 | ['1:1:', 3, '1:1:1', []], 215 | ['-1:-1:', 10, '9:9:1', []], 216 | ['1:1:-1', 10, '1:1:-1', []], 217 | ['-1:-1:-1', 10, '9:9:-1', []], 218 | ['1:2:3', 10, '1:2:3', [1]], 219 | ['1:2:3', 1, '0:0:3', []], 220 | ['::-1', 1, '0:-1:-1', [0]], 221 | ['1::-1', 1, '0:-1:-1', [0]], 222 | ['2::-1', 1, '0:-1:-1', [0]], 223 | ['2:-3:-1', 1, '0:-1:-1', [0]], 224 | ['2::-1', 10, '2:-1:-1', [2, 1, 0]], 225 | [':3:-1', 10, '9:3:-1', [9, 8, 7, 6, 5, 4]], 226 | ]; 227 | } 228 | 229 | public function dataProviderForIsSliceArrayTrue(): array 230 | { 231 | return [ 232 | [[]], 233 | [[null, null]], 234 | [[null, null, null]], 235 | [[0]], 236 | [[0, null]], 237 | [[0, null, null]], 238 | [[1, null, null]], 239 | [[1, 0, null]], 240 | [[1, 1, null]], 241 | [[-1, 1, null]], 242 | [[1, null, 1]], 243 | [[1, null, 2]], 244 | [[null, null, 1]], 245 | [[null, null, -1]], 246 | [[1, 10, -1]], 247 | ]; 248 | } 249 | 250 | public function dataProviderForIsSliceArrayFalse(): array 251 | { 252 | return [ 253 | [['']], 254 | [['a']], 255 | [[0, 1, 'a']], 256 | [[0, 1, 2, 3]], 257 | [[1.1, 1, 2]], 258 | [[1, 1, 2.2]], 259 | [null], 260 | [0], 261 | [1], 262 | [0.0], 263 | [1.0], 264 | [true], 265 | [false], 266 | [new \ArrayObject([])], 267 | [['a' => 1]], 268 | [[[]]], 269 | [[['a' => 1]]], 270 | ]; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/unit/Utils/IsArraySequentialTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Util::isArraySequential($source)); 18 | $this->assertTrue(Util::isArraySequential($source, true)); 19 | } 20 | 21 | /** 22 | * @dataProvider dataProviderForIsSequentialFalse 23 | */ 24 | public function testIsSequentialFalse(array $source) 25 | { 26 | $this->assertFalse(Util::isArraySequential($source)); 27 | $this->assertFalse(Util::isArraySequential($source, true)); 28 | } 29 | 30 | public function dataProviderForIsSequentialTrue(): array 31 | { 32 | return [ 33 | [[]], 34 | [['']], 35 | [[null]], 36 | [[1]], 37 | [['1']], 38 | [['test']], 39 | [[1, 2, 3]], 40 | [[0 => 1, 1 => 2, 2 => 3]], 41 | [['0' => 1, 1 => 2, 2 => 3]], 42 | [['0' => 1, '1' => 2, '2' => 3]], 43 | [[10, '20', 30.5]], 44 | ]; 45 | } 46 | 47 | public function dataProviderForIsSequentialFalse(): array 48 | { 49 | return [ 50 | [['a' => 1]], 51 | [[1 => 1]], 52 | [[0 => 1, 1 => 2, 3 => 3]], 53 | [[0 => 1, 1 => 2, 2 => 3, 'test' => 123]], 54 | [[1 => 2, 2 => 3]], 55 | [[1 => 2, 3 => 3, 'a' => 1000]], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/unit/Utils/NormalizeIndexTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $source[$normalizedIndex]); 19 | } 20 | 21 | /** 22 | * @dataProvider dataProviderForNormalizeIndexNotThrow 23 | */ 24 | public function testNormalizeIndexNotThrow(array $source, int $index, int $expected) 25 | { 26 | $normalizedIndex = Util::normalizeIndex($index, \count($source), false); 27 | $this->assertSame($expected, $normalizedIndex); 28 | } 29 | 30 | /** 31 | * @dataProvider dataProviderForNormalizeIndexError 32 | */ 33 | public function testNormalizeIndexError(array $source, int $index) 34 | { 35 | $this->expectException(IndexError::class); 36 | $this->expectExceptionMessage("Index {$index} is out of range."); 37 | Util::normalizeIndex($index, \count($source)); 38 | } 39 | 40 | public function dataProviderForNormalizeIndexSuccess(): array 41 | { 42 | return [ 43 | [[1], 0, 1], 44 | [[1], -1, 1], 45 | [[1, 2], 0, 1], 46 | [[1, 2], -1, 2], 47 | [[1, 2], 1, 2], 48 | [[1, 2], -2, 1], 49 | [[1, 2, 3], 0, 1], 50 | [[1, 2, 3], -1, 3], 51 | [[1, 2, 3], 1, 2], 52 | [[1, 2, 3], -2, 2], 53 | [[1, 2, 3], 2, 3], 54 | [[1, 2, 3], -3, 1], 55 | ]; 56 | } 57 | 58 | public function dataProviderForNormalizeIndexNotThrow(): array 59 | { 60 | return [ 61 | [[1], 0, 0], 62 | [[1], -1, 0], 63 | [[1, 2], 0, 0], 64 | [[1, 2], -1, 1], 65 | [[1, 2], 1, 1], 66 | [[1, 2], -2, 0], 67 | [[1, 2, 3], 0, 0], 68 | [[1, 2, 3], -1, 2], 69 | [[1, 2, 3], 1, 1], 70 | [[1, 2, 3], -2, 1], 71 | [[1, 2, 3], 2, 2], 72 | [[1, 2, 3], -3, 0], 73 | 74 | [[], 0, 0], 75 | [[], -1, -1], 76 | [[], 2, 2], 77 | [[], -2, -2], 78 | [[1], 1, 1], 79 | [[1], -2, -1], 80 | [[1, 2], 2, 2], 81 | [[1, 2], -3, -1], 82 | [[1, 2, 3], 3, 3], 83 | [[1, 2, 3], -4, -1], 84 | [[1, 2, 3], 4, 4], 85 | [[1, 2, 3], -5, -2], 86 | [[1, 2, 3], 100, 100], 87 | [[1, 2, 3], -101, -98], 88 | ]; 89 | } 90 | 91 | public function dataProviderForNormalizeIndexError(): array 92 | { 93 | return [ 94 | [[], 0], 95 | [[], -1], 96 | [[], 2], 97 | [[], -2], 98 | [[1], 1], 99 | [[1], -2], 100 | [[1, 2], 2], 101 | [[1, 2], -3], 102 | [[1, 2, 3], 3], 103 | [[1, 2, 3], -4], 104 | [[1, 2, 3], 4], 105 | [[1, 2, 3], -5], 106 | [[1, 2, 3], 100], 107 | [[1, 2, 3], -101], 108 | ]; 109 | } 110 | } 111 | --------------------------------------------------------------------------------