├── .github └── workflows │ └── php.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── benchmarks └── TreeMapBench.php ├── cite.bib ├── composer.json ├── composer.lock ├── docs ├── Doxyfile ├── README.md ├── api.rst ├── api │ └── application.rst ├── conf.py ├── index.rst ├── requirements.txt └── usage.md ├── examples ├── ReversedMap.php ├── ReversedSet.php ├── SubMap.php ├── SubSet.php ├── TreeMap.php └── TreeSet.php ├── phpbench.json ├── phpunit.xml.dist ├── src └── SortedCollection │ ├── AbstractMap.php │ ├── AbstractSet.php │ ├── Iterator.php │ ├── ReversedMap.php │ ├── ReversedSet.php │ ├── SortedCollection.php │ ├── SortedMap.php │ ├── SortedSet.php │ ├── SubMap.php │ ├── SubSet.php │ ├── TreeMap.php │ ├── TreeNode.php │ └── TreeSet.php └── tests ├── SortedCollection ├── ReversedMapTest.php ├── ReversedSetTest.php ├── SubMapTest.php ├── SubSetTest.php ├── TreeMapTest.php ├── TreeNodeTest.php └── TreeSetTest.php └── bootstrap.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "main", "develop" ] 6 | pull_request: 7 | branches: [ "main", "develop" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | php-versions: ['8.2', '8.3'] 19 | name: PHP ${{ matrix.php-versions }} 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Install PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | 30 | - name: Check PHP Version 31 | run: php -v 32 | 33 | - name: Validate composer.json and composer.lock 34 | run: composer validate --strict 35 | 36 | - name: Cache Composer packages 37 | id: composer-cache 38 | uses: actions/cache@v3 39 | with: 40 | path: vendor 41 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-php- 44 | 45 | - name: Install dependencies 46 | run: composer install --no-progress 47 | 48 | - name: Run style 49 | run: composer run style 50 | 51 | - name: Run test 52 | run: composer run test 53 | 54 | coverage: 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | 61 | - name: Install PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: '8.3' 65 | 66 | - name: Install dependencies 67 | run: composer install --no-progress 68 | 69 | - name: Run test 70 | run: composer run test 71 | 72 | - name: Upload coverage results to Coveralls 73 | env: 74 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 75 | run: vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v 76 | 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | *~ 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optionally build your docs in additional formats such as PDF and ePub 18 | formats: all 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2024, Christophe Demko 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Sorted Collections 2 | 3 | [![PHP package](https://img.shields.io/github/actions/workflow/status/chdemko/php-sorted-collections/php.yml?logo=github&branch=develop)](https://github.com/chdemko/php-sorted-collections/actions/workflows/php.yml) 4 | [![Coveralls](https://img.shields.io/coveralls/chdemko/php-sorted-collections.svg?logo=Codecov&logoColor=white)](https://coveralls.io/r/chdemko/php-sorted-collections?branch=develop) 5 | [![Scrutinizer](https://img.shields.io/scrutinizer/g/chdemko/php-sorted-collections/develop.svg?logo=scrutinizer)](https://scrutinizer-ci.com/g/chdemko/php-sorted-collections/?branch=develop) 6 | [![Code Climate](https://img.shields.io/codeclimate/maintainability/chdemko/php-sorted-collections?logo=codeclimate&barnch=develop)](https://codeclimate.com/github/chdemko/php-sorted-collections/) 7 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/chdemko/php-sorted-collections/develop.svg?logo=codefactor)](https://www.codefactor.io/repository/github/chdemko/php-sorted-collections) 8 | [![Codacy](https://img.shields.io/codacy/grade/1307547b7a984fe19e98603a91a4cdb6.svg?logo=codacy)](https://app.codacy.com/gh/chdemko/php-sorted-collections/dashboard) 9 | [![PHP versions](https://img.shields.io/packagist/dependency-v/chdemko/sorted-collections/php?logo=php&logoColor=white)](https://packagist.org/packages/chdemko/sorted-collections) 10 | [![Latest Stable Version](https://img.shields.io/packagist/v/chdemko/sorted-collections.svg?logo=packagist&logoColor=white)](https://packagist.org/packages/chdemko/sorted-collections) 11 | [![Downloads](https://img.shields.io/packagist/dt/chdemko/sorted-collections.svg?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+Cgo8c3ZnCiAgIHdpZHRoPSI4MDBweCIKICAgaGVpZ2h0PSI4MDBweCIKICAgdmlld0JveD0iMCAwIDI0IDI0IgogICBmaWxsPSJub25lIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmc0IgogICBzb2RpcG9kaTpkb2NuYW1lPSJkb3dubG9hZC1zdmdyZXBvLWNvbS5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMS4yICgwYTAwY2Y1MzM5LCAyMDIyLTAyLTA0KSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMC44OTg3NSIKICAgICBpbmtzY2FwZTpjeD0iNDAwIgogICAgIGlua3NjYXBlOmN5PSIzOTkuNDQzNjciCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjExNjMiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjAiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc0IiAvPgogIDxwYXRoCiAgICAgZmlsbC1ydWxlPSJldmVub2RkIgogICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIKICAgICBkPSJNOCAxMEM4IDcuNzkwODYgOS43OTA4NiA2IDEyIDZDMTQuMjA5MSA2IDE2IDcuNzkwODYgMTYgMTBWMTFIMTdDMTguOTMzIDExIDIwLjUgMTIuNTY3IDIwLjUgMTQuNUMyMC41IDE2LjQzMyAxOC45MzMgMTggMTcgMThIMTYuOUMxNi4zNDc3IDE4IDE1LjkgMTguNDQ3NyAxNS45IDE5QzE1LjkgMTkuNTUyMyAxNi4zNDc3IDIwIDE2LjkgMjBIMTdDMjAuMDM3NiAyMCAyMi41IDE3LjUzNzYgMjIuNSAxNC41QzIyLjUgMTEuNzc5MyAyMC41MjQ1IDkuNTE5OTcgMTcuOTI5NiA5LjA3ODI0QzE3LjQ4NjIgNi4yMDIxMyAxNS4wMDAzIDQgMTIgNEM4Ljk5OTc0IDQgNi41MTM4MSA2LjIwMjEzIDYuMDcwMzYgOS4wNzgyNEMzLjQ3NTUxIDkuNTE5OTcgMS41IDExLjc3OTMgMS41IDE0LjVDMS41IDE3LjUzNzYgMy45NjI0MyAyMCA3IDIwSDcuMUM3LjY1MjI4IDIwIDguMSAxOS41NTIzIDguMSAxOUM4LjEgMTguNDQ3NyA3LjY1MjI4IDE4IDcuMSAxOEg3QzUuMDY3IDE4IDMuNSAxNi40MzMgMy41IDE0LjVDMy41IDEyLjU2NyA1LjA2NyAxMSA3IDExSDhWMTBaTTEzIDExQzEzIDEwLjQ0NzcgMTIuNTUyMyAxMCAxMiAxMEMxMS40NDc3IDEwIDExIDEwLjQ0NzcgMTEgMTFWMTYuNTg1OEw5LjcwNzExIDE1LjI5MjlDOS4zMTY1OCAxNC45MDI0IDguNjgzNDIgMTQuOTAyNCA4LjI5Mjg5IDE1LjI5MjlDNy45MDIzNyAxNS42ODM0IDcuOTAyMzcgMTYuMzE2NiA4LjI5Mjg5IDE2LjcwNzFMMTEuMjkyOSAxOS43MDcxQzExLjY4MzQgMjAuMDk3NiAxMi4zMTY2IDIwLjA5NzYgMTIuNzA3MSAxOS43MDcxTDE1LjcwNzEgMTYuNzA3MUMxNi4wOTc2IDE2LjMxNjYgMTYuMDk3NiAxNS42ODM0IDE1LjcwNzEgMTUuMjkyOUMxNS4zMTY2IDE0LjkwMjQgMTQuNjgzNCAxNC45MDI0IDE0LjI5MjkgMTUuMjkyOUwxMyAxNi41ODU4VjExWiIKICAgICBmaWxsPSIjMDAwMDAwIgogICAgIGlkPSJwYXRoMiIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmIiAvPgo8L3N2Zz4K)](https://packagist.org/packages/chdemko/sorted-collections) 12 | [![Latest Unstable Version](https://poser.pugx.org/chdemko/sorted-collections/v/unstable.svg)](https://packagist.org/packages/chdemko/sorted-collections) 13 | [![License](https://img.shields.io/github/license/chdemko/php-sorted-collections.svg?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+Cgo8c3ZnCiAgIGhlaWdodD0iODAwcHgiCiAgIHdpZHRoPSI4MDBweCIKICAgdmVyc2lvbj0iMS4xIgogICBpZD0iX3gzMl8iCiAgIHZpZXdCb3g9IjAgMCA1MTIgNTEyIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBzb2RpcG9kaTpkb2NuYW1lPSJiYWxhbmNlLTEtc3ZncmVwby1jb20uc3ZnIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEuMiAoMGEwMGNmNTMzOSwgMjAyMi0wMi0wNCkiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnMKICAgaWQ9ImRlZnMzMzg2IiAvPjxzb2RpcG9kaTpuYW1lZHZpZXcKICAgaWQ9Im5hbWVkdmlldzMzODQiCiAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgc2hvd2dyaWQ9ImZhbHNlIgogICBpbmtzY2FwZTp6b29tPSIwLjg5ODc1IgogICBpbmtzY2FwZTpjeD0iNDAwIgogICBpbmtzY2FwZTpjeT0iMzk5LjQ0MzY3IgogICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiCiAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjExNjMiCiAgIGlua3NjYXBlOndpbmRvdy14PSIxOTIwIgogICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iX3gzMl8iIC8+CjxzdHlsZQogICB0eXBlPSJ0ZXh0L2NzcyIKICAgaWQ9InN0eWxlMzM3NyI+Cgkuc3Qwe2ZpbGw6IzAwMDAwMDt9Cjwvc3R5bGU+CjxnCiAgIGlkPSJnMzM4MSIKICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiI+Cgk8cGF0aAogICBjbGFzcz0ic3QwIgogICBkPSJNNTAwLjYwOSwzMjIuMjc1bC01Ny40MjgtMTYyLjgzNGMwLjEzNSwwLjAwOCwwLjI3OSwwLjAyNSwwLjQwNiwwLjAyNSAgIGMzOS41MzgsMC42NzcsNDkuNDIyLTIwLjU4LDU0LjU2Ni0yNy45NGM3LjExOC0xMC4xNzEtNy45MS0yMC4zNDMtMTUuODE2LTEzLjU1OGMtNy45MDYsNi43NzUtMjkuMjY0LDIxLjAxMS03MC4zODYsMi4wMjQgICBDMzc0Ljg3NCwxMDIuODc1LDMwOS44Nyw3My4wOTgsMjcxLjkyLDY3LjM5OXYtMzljMC04Ljc5OS03LjEyNy0xNS45MjEtMTUuOTE4LTE1LjkyMWMtOC43OTUsMC0xNS45MjIsNy4xMjItMTUuOTIyLDE1LjkyMXYzOSAgIGMtMzcuOTUsNS42OTktMTAyLjk1MywzNS40NzYtMTQwLjAzMSw1Mi41OTNjLTQxLjEyMSwxOC45ODctNjIuNDgsNC43NTEtNzAuMzg2LTIuMDI0Yy03LjkwNi02Ljc4NC0yMi45MzUsMy4zODgtMTUuODE2LDEzLjU1OCAgIGM1LjE0NSw3LjM2LDE1LjAyOCwyOC42MTcsNTQuNTY2LDI3Ljk0YzAuMTMyLDAsMC4yNzYtMC4wMTcsMC40MDItMC4wMjVMMTEuMzkxLDMyMi4yNzVIMCAgIGMxMS40OTcsMzguMDI1LDQ2LjgwNCw2NS43MzYsODguNTk1LDY1LjczNmM0MS43ODYsMCw3Ny4wOTMtMjcuNzExLDg4LjU5LTY1LjczNmgtMTEuMzg2bC02MC4zNTUtMTcxLjEzNCAgIGMzNy4xODMtMTEuNDY3LDg5LjU2OS0zMS4wNTYsMTM0LjYzNi0zNC4wNzJ2MjQuMjNoLTguNTA3djI2Ny43NDhIMjE4LjM3djIzLjg1OGMtOC43MTUsMC0xNy41NjksMC0yNC44NzQsMCAgIGMtMjMuMzU0LDAtMjIuNjYzLDMyLjk2OS0yMi42NjMsMzIuOTY5Yy0xOS4yMzMsMC0yOC44NSwxNS4xMDEtMjguODUsMzMuNjQ4aDIyOC4wMzNjMC0xOC41NDYtOS42MTYtMzMuNjQ4LTI4Ljg0NS0zMy42NDggICBjMCwwLDAuNjg2LTMyLjk2OS0yMi42NjgtMzIuOTY5Yy03LjMwNSwwLTE2LjE1OSwwLTI0Ljg3NCwwdi0yMy44NThoLTEzLjIwM1YxNDEuM2gtOC41MDd2LTI0LjIzICAgYzQ1LjA3MiwzLjAxNSw5Ny40NTcsMjIuNjA0LDEzNC42NCwzNC4wNzJsLTYwLjM1OCwxNzEuMTM0aC0xMS4zODdjMTEuNDk2LDM4LjAyNSw0Ni44MDQsNjUuNzM2LDg4LjU5LDY1LjczNiAgIGM0MS43OSwwLDc3LjA5OC0yNy43MTEsODguNTk0LTY1LjczNkg1MDAuNjA5eiBNMTQxLjI0MywzMjIuMjc1SDM1Ljk0OEw4OC41OTUsMTczTDE0MS4yNDMsMzIyLjI3NXogTTM3MC43NTgsMzIyLjI3NUw0MjMuNDEsMTczICAgbDUyLjY0MywxNDkuMjc1SDM3MC43NTh6IgogICBpZD0icGF0aDMzNzkiCiAgIHN0eWxlPSJmaWxsOiNmZmZmZmYiIC8+CjwvZz4KPC9zdmc+Cg==)](https://raw.githubusercontent.com/chdemko/php-sorted-collections/develop/LICENSE) 14 | [![Last commit](https://img.shields.io/github/last-commit/chdemko/php-sorted-collections/develop?logo=github)](https://github.com/chdemko/php-sorted-collections/commit/develop/) 15 | [![Documentation Status](https://img.shields.io/readthedocs/php-sorted-collections.svg?logo=read-the-docs&logoColor=white)](http://php-sorted-collections.readthedocs.io/en/latest/?badge=latest) 16 | [![Repo Size](https://img.shields.io/github/repo-size/chdemko/php-sorted-collections.svg?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+Cgo8c3ZnCiAgIGZpbGw9IiMwMDAwMDAiCiAgIHdpZHRoPSI4MDBweCIKICAgaGVpZ2h0PSI4MDBweCIKICAgdmlld0JveD0iMCAwIDI0IDI0IgogICBkYXRhLW5hbWU9IkxheWVyIDEiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzE0NzYiCiAgIHNvZGlwb2RpOmRvY25hbWU9IndlaWdodC1zdmdyZXBvLWNvbS5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMS4yICgwYTAwY2Y1MzM5LCAyMDIyLTAyLTA0KSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzMTQ4MCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzE0NzgiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMC44OTg3NSIKICAgICBpbmtzY2FwZTpjeD0iNDAwIgogICAgIGlua3NjYXBlOmN5PSIzOTkuNDQzNjciCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjExNjMiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjAiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmcxNDc2IiAvPgogIDxwYXRoCiAgICAgZD0iTTE5LDRIMTcuNTVBMy4wOCwzLjA4LDAsMCwwLDE3LDNhMywzLDAsMCwwLTIuMjUtMUg5LjI3QTMsMywwLDAsMCw3LDNhMy4wOCwzLjA4LDAsMCwwLS41NywxSDVBMywzLDAsMCwwLDIsN1YxOWEzLDMsMCwwLDAsMywzSDE5YTMsMywwLDAsMCwzLTNWN0EzLDMsMCwwLDAsMTksNFpNOC41Miw0LjM0QTEsMSwwLDAsMSw5LjI3LDRoNS40NmExLDEsMCwwLDEsLjc1LjM0LDEsMSwwLDAsMSwuMjUuNzhsLS41LDRhMSwxLDAsMCwxLTEsLjg4SDEyLjU5bDEuMTQtMi40YTEsMSwwLDAsMC0xLjgtLjg2TDEwLjM3LDEwaC0uNmExLDEsMCwwLDEtMS0uODhsLS41LTRBMSwxLDAsMCwxLDguNTIsNC4zNFpNMjAsMTlhMSwxLDAsMCwxLTEsMUg1YTEsMSwwLDAsMS0xLTFWN0ExLDEsMCwwLDEsNSw2SDYuMzdsLjQyLDMuMzdhMywzLDAsMCwwLDMsMi42M2g0LjQ2YTMsMywwLDAsMCwzLTIuNjNMMTcuNjMsNkgxOWExLDEsMCwwLDEsMSwxWm0tNi0zSDEwYTEsMSwwLDAsMCwwLDJoNGExLDEsMCwwLDAsMC0yWiIKICAgICBpZD0icGF0aDE0NzQiCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==)](http://php-sorted-collections.readthedocs.io/en/latest/) 17 | [![Code Size](https://img.shields.io/github/languages/code-size/chdemko/php-sorted-collections.svg?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+Cgo8c3ZnCiAgIGZpbGw9IiMwMDAwMDAiCiAgIHdpZHRoPSI4MDBweCIKICAgaGVpZ2h0PSI4MDBweCIKICAgdmlld0JveD0iMCAwIDI0IDI0IgogICBkYXRhLW5hbWU9IkxheWVyIDEiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzE0NzYiCiAgIHNvZGlwb2RpOmRvY25hbWU9IndlaWdodC1zdmdyZXBvLWNvbS5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMS4yICgwYTAwY2Y1MzM5LCAyMDIyLTAyLTA0KSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzMTQ4MCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzE0NzgiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMC44OTg3NSIKICAgICBpbmtzY2FwZTpjeD0iNDAwIgogICAgIGlua3NjYXBlOmN5PSIzOTkuNDQzNjciCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjExNjMiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjAiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmcxNDc2IiAvPgogIDxwYXRoCiAgICAgZD0iTTE5LDRIMTcuNTVBMy4wOCwzLjA4LDAsMCwwLDE3LDNhMywzLDAsMCwwLTIuMjUtMUg5LjI3QTMsMywwLDAsMCw3LDNhMy4wOCwzLjA4LDAsMCwwLS41NywxSDVBMywzLDAsMCwwLDIsN1YxOWEzLDMsMCwwLDAsMywzSDE5YTMsMywwLDAsMCwzLTNWN0EzLDMsMCwwLDAsMTksNFpNOC41Miw0LjM0QTEsMSwwLDAsMSw5LjI3LDRoNS40NmExLDEsMCwwLDEsLjc1LjM0LDEsMSwwLDAsMSwuMjUuNzhsLS41LDRhMSwxLDAsMCwxLTEsLjg4SDEyLjU5bDEuMTQtMi40YTEsMSwwLDAsMC0xLjgtLjg2TDEwLjM3LDEwaC0uNmExLDEsMCwwLDEtMS0uODhsLS41LTRBMSwxLDAsMCwxLDguNTIsNC4zNFpNMjAsMTlhMSwxLDAsMCwxLTEsMUg1YTEsMSwwLDAsMS0xLTFWN0ExLDEsMCwwLDEsNSw2SDYuMzdsLjQyLDMuMzdhMywzLDAsMCwwLDMsMi42M2g0LjQ2YTMsMywwLDAsMCwzLTIuNjNMMTcuNjMsNkgxOWExLDEsMCwwLDEsMSwxWm0tNi0zSDEwYTEsMSwwLDAsMCwwLDJoNGExLDEsMCwwLDAsMC0yWiIKICAgICBpZD0icGF0aDE0NzQiCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==)](http://php-sorted-collections.readthedocs.io/en/latest/) 18 | 19 | Sorted Collection for PHP. Insertion, search, and removal compute in 20 | `log(n)` time where `n` is the number of items present in the collection. 21 | It uses AVL threaded tree [see @Knuth97, 1:320, Sect. 2.3.1] as internal 22 | structure. 23 | 24 | @Knuth97: Donald E. Knuth, The Art of Computer Programming, Addison-Wesley, 25 | volumes 1 and 2, 2nd edition, 1997. 26 | 27 | This project uses: 28 | 29 | * [PHP Code Sniffer](https://github.com/squizlabs/php_codesniffer) 30 | for checking PHP code style 31 | * [PHPUnit](http://phpunit.de/) for unit test (100% covered) 32 | * [Sphinx](https://www.sphinx-doc.org/) and [Doxygen](https://www.doxygen.nl/) 33 | for the 34 | [documentation](http://php-sorted-collections.readthedocs.io/en/latest/?badge=latest) 35 | 36 | ## Instructions 37 | 38 | Using composer: either 39 | 40 | ~~~shell 41 | $ composer create-project chdemko/sorted-collections:1.0.*@dev; cd sorted-collections 42 | Creating a "chdemko/sorted-collections:1.0.*@dev" project at "./sorted-collections" 43 | ... 44 | ~~~ 45 | 46 | or create a `composer.json` file containing 47 | 48 | ~~~json 49 | { 50 | "require": { 51 | "chdemko/sorted-collections": "1.0.*@dev" 52 | } 53 | } 54 | ~~~ 55 | 56 | and run 57 | 58 | ~~~shell 59 | $ composer install 60 | Loading composer repositories with package information 61 | ... 62 | ~~~ 63 | 64 | Create a `test.php` file containg 65 | 66 | ~~~php 67 | put( 74 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 75 | ); 76 | echo $tree . PHP_EOL; 77 | ~~~ 78 | 79 | And run 80 | 81 | ~~~shell 82 | $ php test.php 83 | [0,1,2,3,4,5,6,7,8,9] 84 | ~~~ 85 | 86 | See the 87 | [examples](https://github.com/chdemko/php-sorted-collections/tree/develop/examples) 88 | and 89 | [benchmarks](https://github.com/chdemko/php-sorted-collections/tree/develop/benchmarks) 90 | folders for more information. 91 | 92 | ## Documentation 93 | 94 | Run 95 | 96 | ~~~shell 97 | $ sudo apt install doxygen python3-pip python3-virtualenv 98 | $ virtualenv venv 99 | $ venv/bin/activate 100 | (venv) $ pip install -r docs/requirements.txt 101 | (venv) $ sphinx-build -b html docs/ html/ 102 | (venv) $ deactivate 103 | $ 104 | ~~~ 105 | 106 | if you want to create local documentation with Sphinx. 107 | 108 | ## Citation 109 | 110 | If you are using this project including publication in research activities, 111 | you have to cite it using 112 | ([BibTeX format](https://raw.github.com/chdemko/php-sorted-collections/develop/cite.bib)). 113 | You are also pleased to send me an email to . 114 | 115 | * authors: Christophe Demko 116 | * title: php-sorted-collections: a PHP library for handling sorted collections 117 | * year: 2014 118 | * how published: 119 | 120 | All releases can be found 121 | [here](https://github.com/chdemko/php-sorted-collections/releases) 122 | -------------------------------------------------------------------------------- /benchmarks/TreeMapBench.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection\Benchmark namespace 15 | namespace chdemko\SortedCollection\Benchmark; 16 | 17 | use chdemko\SortedCollection\TreeMap; 18 | use chdemko\SortedCollection\ReversedMap; 19 | use chdemko\SortedCollection\SubMap; 20 | 21 | /** 22 | * TreeMap benchmark class 23 | * 24 | * @since 1.0.5 25 | */ 26 | class TreeMapBench 27 | { 28 | /** 29 | * @var TreeMap The tree map 30 | * 31 | * @since 1.0.5 32 | */ 33 | protected $tree; 34 | 35 | /** 36 | * @var chdemko\SortedCollection\SortedMap The sorted map 37 | * 38 | * @since 1.0.5 39 | */ 40 | protected $data; 41 | 42 | /** 43 | * Provider for counts 44 | * 45 | * @return \Generator on count 46 | * 47 | * @since 1.0.5 48 | */ 49 | public function provideCounts() 50 | { 51 | yield ['count' => 100]; 52 | yield ['count' => 1000]; 53 | yield ['count' => 10000]; 54 | yield ['count' => 100000]; 55 | } 56 | 57 | /** 58 | * Provider for counts 59 | * 60 | * @return \Generator on type 61 | * 62 | * @since 1.0.5 63 | */ 64 | public function provideTypes() 65 | { 66 | yield ['type' => 'tree']; 67 | yield ['type' => 'reversed']; 68 | yield ['type' => 'sub', 'from' => 0.30, 'to' => 0.70]; 69 | yield ['type' => 'sub', 'from' => 0.40, 'to' => 0.80]; 70 | } 71 | 72 | /** 73 | * Create the tree map. 74 | * 75 | * @param array $params Array of parameters 76 | * 77 | * @return void 78 | * 79 | * @since 1.0.5 80 | */ 81 | public function init($params) 82 | { 83 | $this->tree = TreeMap::create(); 84 | } 85 | 86 | /** 87 | * Create the sorted map. 88 | * 89 | * @param array $params Array of parameters 90 | * 91 | * @return void 92 | * 93 | * @since 1.0.5 94 | */ 95 | public function data($params) 96 | { 97 | if (isset($params['type'])) { 98 | switch ($params['type']) { 99 | case 'tree': 100 | $this->data = $this->tree; 101 | break; 102 | case 'reversed': 103 | $this->data = ReversedMap::create($this->tree); 104 | break; 105 | case 'sub': 106 | if (isset($params['from']) && isset($params['to'])) { 107 | $this->data = SubMap::create( 108 | $this->tree, 109 | (int) ($params['from'] * $params['count']), 110 | (int) ($params['to'] * $params['count']) 111 | ); 112 | } else { 113 | $this->data = SubMap::create( 114 | $this->tree, 115 | null, 116 | null 117 | ); 118 | } 119 | break; 120 | } 121 | } else { 122 | $this->data = $this->tree; 123 | } 124 | } 125 | 126 | /** 127 | * Clear the tree map. 128 | * 129 | * @param array $params Array of parameters 130 | * 131 | * @return void 132 | * 133 | * @since 1.0.5 134 | */ 135 | public function finish($params) 136 | { 137 | $this->tree->clear(); 138 | } 139 | 140 | /** 141 | * @BeforeMethods({"init", "data"}) 142 | * @AfterMethods({"finish"}) 143 | * @Revs(5) 144 | * @ParamProviders({"provideCounts"}) 145 | * 146 | * @param array $params Array of parameters 147 | * 148 | * @return void 149 | * 150 | * @since 1.0.5 151 | */ 152 | public function benchFill($params) 153 | { 154 | for ($i = 0; $i < $params['count']; $i++) { 155 | $this->tree[$i] = $i; 156 | } 157 | } 158 | 159 | /** 160 | * @BeforeMethods({"init", "benchFill", "data"}) 161 | * @AfterMethods({"finish"}) 162 | * @Revs(5) 163 | * @ParamProviders({"provideCounts", "provideTypes"}) 164 | * 165 | * @param array $params Array of parameters 166 | * 167 | * @return void 168 | * 169 | * @since 1.0.5 170 | */ 171 | public function benchSearch($params) 172 | { 173 | if (isset($params['from'])) { 174 | $min = (int) ($params['from'] * $params['count']); 175 | } else { 176 | $min = 0; 177 | } 178 | 179 | if (isset($params['to'])) { 180 | $max = (int) ($params['to'] * $params['count']); 181 | } else { 182 | $max = $params['count']; 183 | } 184 | 185 | for ($i = $min; $i < $max; $i++) { 186 | $value = $this->data[$i]; 187 | } 188 | } 189 | 190 | /** 191 | * @BeforeMethods({"init", "benchFill", "data"}) 192 | * @AfterMethods({"finish"}) 193 | * @Revs(5) 194 | * @ParamProviders({"provideCounts"}) 195 | * 196 | * @param array $params Array of parameters 197 | * 198 | * @return void 199 | * 200 | * @since 1.0.5 201 | */ 202 | public function benchClean($params) 203 | { 204 | for ($i = 0; $i < $params['count']; $i++) { 205 | unset($this->tree[$i]); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /cite.bib: -------------------------------------------------------------------------------- 1 | @Misc{php-sorted-collections-2014, 2 | Author = {Christophe Demko}, 3 | Title = {{php-sorted-collections}: a PHP library for handling sorted collections}, 4 | HowPublished = {https://packagist.org/packages/chdemko/sorted-collections}, 5 | year = 2014 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chdemko/sorted-collections", 3 | "type": "library", 4 | "description": "Sorted Collections for PHP >= 8.2", 5 | "keywords": ["collection","set","tree","map","sorted","ordered","iterator","treeset","treemap","avl"], 6 | "homepage": "https://php-sorted-collections.readthedocs.io/en/latest/?badge=latest", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Christophe Demko", 11 | "email": "chdemko@gmail.com", 12 | "homepage": "https://chdemko.pagelab.univ-lr.fr/", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.2" 18 | }, 19 | "require-dev": { 20 | "php-coveralls/php-coveralls": "^2.7", 21 | "squizlabs/php_codesniffer": "^3.10", 22 | "phpunit/phpunit": "^11.3", 23 | "phpbench/phpbench": "^1.3" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "chdemko\\SortedCollection\\": "src/SortedCollection" 28 | } 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-main": "1.0.x-dev" 33 | } 34 | }, 35 | "scripts": { 36 | "style": "vendor/bin/phpcs --report=full --extensions=php --standard=PSR12 src tests examples benchmarks", 37 | "test": "XDEBUG_MODE=coverage vendor/bin/phpunit --log-junit junit.xml", 38 | "benchmark": "vendor/bin/phpbench run --report=default" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | api/* 8 | 9 | -------------------------------------------------------------------------------- /docs/api/application.rst: -------------------------------------------------------------------------------- 1 | Interfaces 2 | ========== 3 | 4 | Sorted Collection 5 | ------------------ 6 | 7 | .. doxygeninterface:: chdemko::SortedCollection::SortedCollection 8 | :no-link: 9 | 10 | Sorted Set 11 | ---------- 12 | 13 | .. doxygeninterface:: chdemko::SortedCollection::SortedSet 14 | :no-link: 15 | 16 | Sorted Map 17 | ---------- 18 | 19 | .. doxygeninterface:: chdemko::SortedCollection::SortedMap 20 | :no-link: 21 | 22 | Abstract classes 23 | ================ 24 | 25 | Abstract Set 26 | ------------ 27 | 28 | .. doxygenclass:: chdemko::SortedCollection::AbstractSet 29 | :no-link: 30 | 31 | Abstract Map 32 | ------------ 33 | 34 | .. doxygenclass:: chdemko::SortedCollection::AbstractMap 35 | :no-link: 36 | 37 | Concrete classes 38 | ================ 39 | 40 | Tree Set 41 | -------- 42 | 43 | .. doxygenclass:: chdemko::SortedCollection::TreeSet 44 | :no-link: 45 | 46 | Sub Set 47 | ------- 48 | 49 | .. doxygenclass:: chdemko::SortedCollection::SubSet 50 | :no-link: 51 | 52 | Reversed Set 53 | ------------ 54 | 55 | .. doxygenclass:: chdemko::SortedCollection::ReversedSet 56 | :no-link: 57 | 58 | Tree Map 59 | -------- 60 | 61 | .. doxygenclass:: chdemko::SortedCollection::TreeMap 62 | :no-link: 63 | 64 | Sub Map 65 | ------- 66 | 67 | .. doxygenclass:: chdemko::SortedCollection::SubMap 68 | :no-link: 69 | 70 | Reversed Map 71 | ------------ 72 | 73 | .. doxygenclass:: chdemko::SortedCollection::ReversedMap 74 | :no-link: 75 | 76 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | import os 20 | 21 | import subprocess 22 | 23 | # Doxygen 24 | subprocess.call('doxygen Doxyfile', shell=True) 25 | 26 | # -- Project information ----------------------------------------------------- 27 | 28 | project = 'PHP Sorted Collections' 29 | copyright = '2024, Ch. Demko' 30 | author = 'Ch. Demko' 31 | 32 | # The short X.Y version 33 | version = '1.0' 34 | # The full version, including alpha/beta/rc tags 35 | release = '1.0.8' 36 | 37 | 38 | # -- General configuration --------------------------------------------------- 39 | 40 | # If your documentation needs a minimal Sphinx version, state it here. 41 | # 42 | # needs_sphinx = '1.0' 43 | needs_sphinx = '6.0' 44 | 45 | # Add any Sphinx extension module names here, as strings. They can be 46 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 47 | # ones. 48 | 49 | extensions = [ 50 | 'sphinx.ext.autodoc', 51 | 'sphinxcontrib.phpdomain', 52 | 'breathe', 53 | 'myst_parser' 54 | ] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ['_templates'] 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | # This pattern also affects html_static_path and html_extra_path. 62 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 63 | 64 | highlight_language = 'php' 65 | 66 | from sphinx.highlighting import lexers 67 | from pygments.lexers.web import PhpLexer 68 | lexers['php'] = PhpLexer(startinline=True, linenos=1) 69 | lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1) 70 | primary_domain = 'php' 71 | 72 | # Add any paths that contain templates here, relative to this directory. 73 | #templates_path = ['_templates'] 74 | 75 | # The suffix(es) of source filenames. 76 | # You can specify multiple suffix as a list of string: 77 | # 78 | # source_suffix = ['.rst', '.md'] 79 | source_suffix = { 80 | '.rst': 'restructuredtext', 81 | '.md': 'markdown', 82 | } 83 | 84 | # The master toctree document. 85 | master_doc = 'index' 86 | 87 | # The language for content autogenerated by Sphinx. Refer to documentation 88 | # for a list of supported languages. 89 | # 90 | # This is also used if you do content translation via gettext catalogs. 91 | # Usually you set "language" from the command line for these cases. 92 | language = 'en' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This pattern also affects html_static_path and html_extra_path. 97 | exclude_patterns = [] 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = None 101 | 102 | 103 | # -- Options for HTML output ------------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | # 108 | #html_theme = 'alabaster' 109 | html_theme = 'sphinx_rtd_theme' 110 | 111 | from sphinx.highlighting import lexers 112 | from pygments.lexers.web import PhpLexer 113 | lexers['php'] = PhpLexer(startinline=True, linenos=1) 114 | lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1) 115 | 116 | # Set domain 117 | primary_domain = 'php' 118 | 119 | 120 | # -- Breathe configuration ------------------------------------------------- 121 | 122 | breathe_projects = { 123 | 'SortedCollection': '_build/xml/' 124 | } 125 | breathe_default_project = 'SortedCollection' 126 | breathe_default_members = ('members', 'undoc-members') 127 | breathe_domain_by_extension = { 128 | 'php' : 'php', 129 | } 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | # 135 | # html_theme_options = {} 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | #html_static_path = ['_static'] 141 | 142 | # Custom sidebar templates, must be a dictionary that maps document names 143 | # to template names. 144 | # 145 | # The default sidebars (for documents that don't match any pattern) are 146 | # defined by theme itself. Builtin themes are using these templates by 147 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 148 | # 'searchbox.html']``. 149 | # 150 | # html_sidebars = {} 151 | 152 | 153 | # -- Options for HTMLHelp output --------------------------------------------- 154 | 155 | # Output file base name for HTML help builder. 156 | htmlhelp_basename = 'PHPSortedCollectionsdoc' 157 | 158 | 159 | # -- Options for LaTeX output ------------------------------------------------ 160 | 161 | latex_elements = { 162 | # The paper size ('letterpaper' or 'a4paper'). 163 | # 164 | # 'papersize': 'letterpaper', 165 | 166 | # The font size ('10pt', '11pt' or '12pt'). 167 | # 168 | # 'pointsize': '10pt', 169 | 170 | # Additional stuff for the LaTeX preamble. 171 | # 172 | # 'preamble': '', 173 | 174 | # Latex figure (float) alignment 175 | # 176 | # 'figure_align': 'htbp', 177 | } 178 | 179 | # Grouping the document tree into LaTeX files. List of tuples 180 | # (source start file, target name, title, 181 | # author, documentclass [howto, manual, or own class]). 182 | 183 | # -- Options for manual page output ------------------------------------------ 184 | 185 | # One entry per manual page. List of tuples 186 | # (source start file, name, description, authors, manual section). 187 | 188 | # -- Options for Texinfo output ---------------------------------------------- 189 | 190 | # Grouping the document tree into Texinfo files. List of tuples 191 | # (source start file, target name, title, author, 192 | # dir menu entry, description, category) 193 | 194 | # -- Options for Epub output ------------------------------------------------- 195 | 196 | # Bibliographic Dublin Core info. 197 | epub_title = project 198 | 199 | # The unique identifier of the text. This can be a ISBN number 200 | # or the project homepage. 201 | # 202 | # epub_identifier = '' 203 | 204 | # A unique identification for the text. 205 | # 206 | # epub_uid = '' 207 | 208 | # A list of files that should not be packed into the epub file. 209 | epub_exclude_files = ['search.html'] 210 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PHP Sorted Collections documentation! 2 | ================================================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents 7 | 8 | Installation 9 | Usage 10 | Application Programming Interface 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | 17 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=7.2 2 | sphinx_rtd_theme>=2.0 3 | sphinxcontrib-phpdomain>=0.11 4 | breathe>=4.35 5 | myst-parser>=2.0 6 | 7 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Creation 5 | -------- 6 | 7 | The base class for storing sorted maps is the `TreeMap` class. 8 | 9 | ~~~php 10 | require __DIR__ . '/vendor/autoload.php'; 11 | use chdemko\SortedCollection\TreeMap; 12 | 13 | // This will create a map indexed by numbers 14 | // it contains 10 key/value pairs from 0/0 to 9/9 15 | $map = TreeMap::create()->put( 16 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 17 | ); 18 | ~~~ 19 | 20 | There are two other classes to create maps which are in fact views on another sorted map. 21 | 22 | ~~~php 23 | require __DIR__ . '/vendor/autoload.php'; 24 | use chdemko\SortedCollection\TreeMap; 25 | use chdemko\SortedCollection\ReversedMap; 26 | use chdemko\SortedCollection\SubMap; 27 | 28 | $map = TreeMap::create()->put( 29 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 30 | ); 31 | 32 | // This will create a map which is the reverse of $map 33 | $reversed = ReversedMap::create($map); 34 | 35 | // This will create a map which is a sub map of $reversed 36 | $sub = SubMap::create($reversed, 7, 3); 37 | 38 | // This will display {"7":7,"6":6,"5":5,"4":4} 39 | echo $sub . PHP_EOL; 40 | ~~~ 41 | 42 | For sub maps there are other methods for creation 43 | 44 | ~~~php 45 | require __DIR__ . '/vendor/autoload.php'; 46 | use chdemko\SortedCollection\TreeMap; 47 | use chdemko\SortedCollection\SubMap; 48 | 49 | $map = TreeMap::create()->put( 50 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 51 | ); 52 | 53 | // This will create a map which is a sub map of $map from key 3 to the end 54 | $tail = SubMap::tail($map, 3); 55 | 56 | $map[10] = 10; 57 | 58 | // This will display {"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9,"10":10} 59 | echo $tail . PHP_EOL; 60 | 61 | // This will create a sub map of $map from beginning to key 7 (inclusive) 62 | $head = SubMap::head($map, 7, true); 63 | 64 | // This will display [0,1,2,3,4,5,6,7] 65 | echo $head . PHP_EOL; 66 | ~~~ 67 | 68 | Sets are created using similar functions 69 | 70 | ~~~php 71 | require __DIR__ . '/vendor/autoload.php'; 72 | use chdemko\SortedCollection\TreeSet; 73 | use chdemko\SortedCollection\ReversedSet; 74 | use chdemko\SortedCollection\SubSet; 75 | 76 | $set = TreeSet::create()->put([1, 9, 5, 2, 6, 3, 0, 8, 7, 4]); 77 | $reversed = ReversedSet::create($set); 78 | $sub = SubSet::create($reversed, 7, 3); 79 | 80 | // This will display [7,6,5,4] 81 | echo $sub . PHP_EOL; 82 | ~~~ 83 | 84 | Iteration 85 | --------- 86 | 87 | These collections support PHP iteration. 88 | 89 | Using maps 90 | 91 | ~~~php 92 | require __DIR__ . '/vendor/autoload.php'; 93 | use chdemko\SortedCollection\TreeMap; 94 | use chdemko\SortedCollection\ReversedMap; 95 | use chdemko\SortedCollection\SubMap; 96 | 97 | $map = TreeMap::create()->put( 98 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 99 | ); 100 | $reversed = ReversedMap::create($map); 101 | $sub = SubMap::create($reversed, 7, 3); 102 | 103 | // This will display 7:7;6:6;5:5;4:4; 104 | foreach ($sub as $key => $value) 105 | { 106 | echo $key . ':' . $value . ';'; 107 | } 108 | echo PHP_EOL; 109 | ~~~ 110 | 111 | Using sets 112 | 113 | ~~~php 114 | require __DIR__ . '/vendor/autoload.php'; 115 | use chdemko\SortedCollection\TreeSet; 116 | use chdemko\SortedCollection\ReversedSet; 117 | use chdemko\SortedCollection\SubSet; 118 | 119 | $set = TreeSet::create()->put([1, 9, 5, 2, 6, 3, 0, 8, 7, 4]); 120 | $reversed = ReversedSet::create($set); 121 | $sub = SubSet::create($reversed, 7, 3); 122 | 123 | // This will display 0:7;1:6;2:5;3:4; 124 | foreach ($sub as $key => $value) 125 | { 126 | echo $key . ':' . $value . ';'; 127 | } 128 | echo PHP_EOL; 129 | ~~~ 130 | 131 | **The behavior is unpredictable if the current key of an iterator is removed of the collection.** 132 | 133 | Counting 134 | -------- 135 | 136 | These collections support PHP counting 137 | 138 | ~~~php 139 | require __DIR__ . '/vendor/autoload.php'; 140 | use chdemko\SortedCollection\TreeMap; 141 | use chdemko\SortedCollection\ReversedMap; 142 | use chdemko\SortedCollection\SubMap; 143 | 144 | $map = TreeMap::create()->put( 145 | [1=>1, 9=>9, 5=>5, 2=>2, 6=>6, 3=>3, 0=>0, 8=>8, 7=>7, 4=>4] 146 | ); 147 | $reversed = ReversedMap::create($map); 148 | $sub = SubMap::create($reversed, 7, 3); 149 | 150 | // This will display 4 151 | echo count($sub) . PHP_EOL; 152 | ~~~ 153 | 154 | Array access 155 | ------------ 156 | 157 | Insertion, modification, access and removal has been designed to work using PHP array access features 158 | 159 | Using maps 160 | 161 | ~~~php 162 | require __DIR__ . '/vendor/autoload.php'; 163 | use chdemko\SortedCollection\TreeMap; 164 | 165 | $map = TreeMap::create(); 166 | $map[4] = 4; 167 | $map[2] = 2; 168 | $map[6] = 6; 169 | unset($map[4]); 170 | 171 | // This will display 1 172 | echo isset($map[2]) . PHP_EOL; 173 | 174 | // This will display 2 175 | echo $map[2] . PHP_EOL; 176 | ~~~ 177 | 178 | Using sets 179 | 180 | ~~~php 181 | require __DIR__ . '/vendor/autoload.php'; 182 | use chdemko\SortedCollection\TreeSet; 183 | 184 | $set = TreeSet::create(); 185 | $set[4] = true; 186 | $set[2] = true; 187 | $set[6] = true; 188 | unset($set[4]); 189 | 190 | // This will display 1 191 | echo isset($set[2]) . PHP_EOL; 192 | 193 | // This will display 1 194 | echo $set[2] . PHP_EOL; 195 | 196 | // This will display nothing 197 | echo $set[4] . PHP_EOL; 198 | ~~~ 199 | 200 | A lot of methods has been implemented to give access to the minimum element, the lower element.... 201 | 202 | -------------------------------------------------------------------------------- /examples/ReversedMap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeMap; 20 | use chdemko\SortedCollection\ReversedMap; 21 | 22 | $tree = TreeMap::create()->put(array(1 => 1, 9 => 9, 5 => 5, 2 => 2, 6 => 6, 3 => 3, 0 => 0, 8 => 8, 7 => 7, 4 => 4)); 23 | $reversed = ReversedMap::create($tree); 24 | 25 | // Print {"9":9,"8":8,"7":7,"6":6,"5":5,"4":4,"3":3,"2":2,"1":1,"0":0} 26 | echo $reversed . PHP_EOL; 27 | 28 | // Print {"8":8,"7":7,"6":6,"5":5,"4":4,"3":3,"2":2,"1":1,"0":0} 29 | unset($tree[9]); 30 | echo $reversed . PHP_EOL; 31 | -------------------------------------------------------------------------------- /examples/ReversedSet.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeSet; 20 | use chdemko\SortedCollection\ReversedSet; 21 | 22 | $set = TreeSet::create()->put(array(1, 9, 5, 2, 6, 3, 0, 8, 7, 4)); 23 | $reversed = ReversedSet::create($set); 24 | 25 | // Print [9,8,7,6,5,4,3,2,1,0] 26 | echo $reversed . PHP_EOL; 27 | 28 | // Print [8,7,6,5,4,3,2,1,0] 29 | unset($set[9]); 30 | echo $reversed . PHP_EOL; 31 | -------------------------------------------------------------------------------- /examples/SubMap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeMap; 20 | use chdemko\SortedCollection\ReversedMap; 21 | use chdemko\SortedCollection\SubMap; 22 | 23 | $tree = TreeMap::create()->put(array(1 => 1, 9 => 9, 5 => 5, 2 => 2, 6 => 6, 3 => 3, 0 => 0, 8 => 8, 7 => 7, 4 => 4)); 24 | $reversed = ReversedMap::create($tree); 25 | $sub = SubMap::create($reversed, 7, 2); 26 | 27 | // Print {"7":7,"6":6,"5":5,"4":4,"3":3} 28 | echo $sub . PHP_EOL; 29 | 30 | // Print {"7":7,"6":6,"5":5,"3":3} 31 | unset($tree[4]); 32 | echo $sub . PHP_EOL; 33 | 34 | // Print {"9":9,"8":8,"7":7,"6":6,"5":5,"3":3} 35 | unset($sub->fromKey); 36 | echo $sub . PHP_EOL; 37 | -------------------------------------------------------------------------------- /examples/SubSet.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeSet; 20 | use chdemko\SortedCollection\ReversedSet; 21 | use chdemko\SortedCollection\SubSet; 22 | 23 | $set = TreeSet::create()->put(array(1, 9, 5, 2, 6, 3, 0, 8, 7, 4)); 24 | $reversed = ReversedSet::create($set); 25 | $sub = SubSet::create($reversed, 7, 2); 26 | 27 | // Print [7,6,5,4,3] 28 | echo $sub . PHP_EOL; 29 | 30 | // Print [7,6,5,3] 31 | unset($set[4]); 32 | echo $sub . PHP_EOL; 33 | 34 | // Print [9,8,7,6,5,3] 35 | unset($sub->from); 36 | echo $sub . PHP_EOL; 37 | -------------------------------------------------------------------------------- /examples/TreeMap.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeMap; 20 | 21 | $tree = TreeMap::create()->put(array(1 => 1, 9 => 9, 5 => 5, 2 => 2, 6 => 6, 3 => 3, 0 => 0, 8 => 8, 7 => 7, 4 => 4)); 22 | 23 | // Print [0,1,2,3,4,5,6,7,8,9] 24 | echo $tree . PHP_EOL; 25 | 26 | // Print 0 27 | echo $tree->firstKey . PHP_EOL; 28 | 29 | // Print 9 30 | echo $tree->lastValue . PHP_EOL; 31 | 32 | // Print 10 33 | echo count($tree) . PHP_EOL; 34 | 35 | // Print 5 36 | echo $tree[5] . PHP_EOL; 37 | 38 | // Change value for $tree[5] 39 | $tree[5] = 10; 40 | 41 | // Print [0,1,2,3,4,10,6,7,8,9] 42 | echo $tree . PHP_EOL; 43 | 44 | // Unset $tree[5] 45 | unset($tree[5]); 46 | 47 | // Print {"0":0,"1":1,"2":2,"3":3,"4":4,"6":6,"7":7,"8":8,"9":9} 48 | echo $tree . PHP_EOL; 49 | 50 | // Print 0-0;1-1;2-2;3-3;4-4;6-6;7-7;8-8;9-9; 51 | foreach ($tree as $key => $value) { 52 | echo $key . '-' . $value . ';'; 53 | } 54 | 55 | echo PHP_EOL; 56 | -------------------------------------------------------------------------------- /examples/TreeSet.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 11 | * 12 | * @license BSD 3-Clause License 13 | * 14 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 15 | */ 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | use chdemko\SortedCollection\TreeSet; 20 | 21 | $set = TreeSet::create()->put(array(1, 9, 5, 2, 6, 3, 0, 8, 7, 4)); 22 | 23 | // Print [0,1,2,3,4,5,6,7,8,9] 24 | echo $set . PHP_EOL; 25 | 26 | // Print 0 27 | echo $set->first . PHP_EOL; 28 | 29 | // Print 9 30 | echo $set->last . PHP_EOL; 31 | 32 | // Print 10 33 | echo count($set) . PHP_EOL; 34 | 35 | // Print 1 36 | echo $set[5] . PHP_EOL; 37 | 38 | // Change value for $set[5] 39 | $set[5] = false; 40 | 41 | // Print [0,1,2,3,4,6,7,8,9] 42 | echo $set . PHP_EOL; 43 | 44 | // Unset $set[6] 45 | unset($set[6]); 46 | 47 | // Print [0,1,2,3,4,7,8,9] 48 | echo $set . PHP_EOL; 49 | 50 | // Print 0-0;1-1;2-2;3-3;4-4;5-7;6-8;7-9; 51 | foreach ($set as $key => $value) { 52 | echo $key . '-' . $value . ';'; 53 | } 54 | 55 | echo PHP_EOL; 56 | -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "runner.bootstrap": "vendor/autoload.php", 3 | "runner.path": "benchmarks" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tests/SortedCollection 12 | 13 | 14 | 15 | 16 | 17 | src/SortedCollection/ 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/SortedCollection/AbstractMap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * AbstractMap 19 | * 20 | * @package SortedCollection 21 | * @subpackage Map 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The key comparison function 26 | * @property-read TreeNode $first The first element of the map 27 | * @property-read mixed $firstKey The first key of the map 28 | * @property-read mixed $firstValue The first value of the map 29 | * @property-read TreeNode $last The last element of the map 30 | * @property-read mixed $lastKey The last key of the map 31 | * @property-read mixed $lastValue The last value of the map 32 | * @property-read Iterator $keys The keys iterator 33 | * @property-read Iterator $values The values iterator 34 | * @property-read integer $count The number of elements in the map 35 | */ 36 | abstract class AbstractMap implements SortedMap 37 | { 38 | /** 39 | * Magic get method 40 | * 41 | * @param string $property The property 42 | * 43 | * @throws RuntimeException If the property does not exist 44 | * 45 | * @return mixed The value associated to the property 46 | * 47 | * @since 1.0.0 48 | */ 49 | public function __get($property) 50 | { 51 | switch ($property) { 52 | case 'comparator': 53 | return $this->comparator(); 54 | case 'firstKey': 55 | return $this->firstKey(); 56 | case 'lastKey': 57 | return $this->lastKey(); 58 | case 'firstValue': 59 | return $this->firstValue(); 60 | case 'lastValue': 61 | return $this->lastValue(); 62 | case 'first': 63 | return $this->first(); 64 | case 'last': 65 | return $this->last(); 66 | case 'keys': 67 | return $this->keys(); 68 | case 'values': 69 | return $this->values(); 70 | case 'count': 71 | return $this->count(); 72 | default: 73 | throw new \RuntimeException('Undefined property'); 74 | } 75 | } 76 | 77 | /** 78 | * Get the first key 79 | * 80 | * @return mixed The first key 81 | * 82 | * @throws OutOfBoundsException If there is no element 83 | * 84 | * @since 1.0.0 85 | */ 86 | public function firstKey() 87 | { 88 | return $this->first()->key; 89 | } 90 | 91 | /** 92 | * Get the first value 93 | * 94 | * @return mixed The first value 95 | * 96 | * @throws OutOfBoundsException If there is no element 97 | * 98 | * @since 1.0.0 99 | */ 100 | public function firstValue() 101 | { 102 | return $this->first()->value; 103 | } 104 | 105 | /** 106 | * Get the last key 107 | * 108 | * @return mixed The last key 109 | * 110 | * @throws OutOfBoundsException If there is no element 111 | * 112 | * @since 1.0.0 113 | */ 114 | public function lastKey() 115 | { 116 | return $this->last()->key; 117 | } 118 | 119 | /** 120 | * Get the last value 121 | * 122 | * @return mixed The last value 123 | * 124 | * @throws OutOfBoundsException If there is no element 125 | * 126 | * @since 1.0.0 127 | */ 128 | public function lastValue() 129 | { 130 | return $this->last()->value; 131 | } 132 | 133 | /** 134 | * Returns the greatest key lesser than the given key 135 | * 136 | * @param mixed $key The searched key 137 | * 138 | * @return mixed The found key 139 | * 140 | * @throws OutOfBoundsException If there is no lower element 141 | * 142 | * @since 1.0.0 143 | */ 144 | public function lowerKey($key) 145 | { 146 | return $this->lower($key)->key; 147 | } 148 | 149 | /** 150 | * Returns the value whose key is the greatest key lesser than the given key 151 | * 152 | * @param mixed $key The searched key 153 | * 154 | * @return mixed The found value 155 | * 156 | * @throws OutOfBoundsException If there is no lower element 157 | * 158 | * @since 1.0.0 159 | */ 160 | public function lowerValue($key) 161 | { 162 | return $this->lower($key)->value; 163 | } 164 | 165 | /** 166 | * Returns the greatest key lesser than or equal to the given key 167 | * 168 | * @param mixed $key The searched key 169 | * 170 | * @return mixed The found key 171 | * 172 | * @throws OutOfBoundsException If there is no floor element 173 | * 174 | * @since 1.0.0 175 | */ 176 | public function floorKey($key) 177 | { 178 | return $this->floor($key)->key; 179 | } 180 | 181 | /** 182 | * Returns the value whose key is the greatest key lesser than or equal to the given key 183 | * 184 | * @param mixed $key The searched key 185 | * 186 | * @return mixed The found value 187 | * 188 | * @throws OutOfBoundsException If there is no floor element 189 | * 190 | * @since 1.0.0 191 | */ 192 | public function floorValue($key) 193 | { 194 | return $this->floor($key)->value; 195 | } 196 | 197 | /** 198 | * Returns the key equal to the given key 199 | * 200 | * @param mixed $key The searched key 201 | * 202 | * @return mixed The found key 203 | * 204 | * @throws OutOfBoundsException If there is no such element 205 | * 206 | * @since 1.0.0 207 | */ 208 | public function findKey($key) 209 | { 210 | return $this->find($key)->key; 211 | } 212 | 213 | /** 214 | * Returns the value whose key equal to the given key 215 | * 216 | * @param mixed $key The searched key 217 | * 218 | * @return mixed The found value 219 | * 220 | * @throws OutOfBoundsException If there is no such element 221 | * 222 | * @since 1.0.0 223 | */ 224 | public function findValue($key) 225 | { 226 | return $this->find($key)->value; 227 | } 228 | 229 | /** 230 | * Returns the lowest key greater than or equal to the given key 231 | * 232 | * @param mixed $key The searched key 233 | * 234 | * @return mixed The found key 235 | * 236 | * @throws OutOfBoundsException If there is no ceiling element 237 | * 238 | * @since 1.0.0 239 | */ 240 | public function ceilingKey($key) 241 | { 242 | return $this->ceiling($key)->key; 243 | } 244 | 245 | /** 246 | * Returns the value whose key is the lowest key greater than or equal to the given key 247 | * 248 | * @param mixed $key The searched key 249 | * 250 | * @return mixed The found value 251 | * 252 | * @throws OutOfBoundsException If there is no ceiling element 253 | * 254 | * @since 1.0.0 255 | */ 256 | public function ceilingValue($key) 257 | { 258 | return $this->ceiling($key)->value; 259 | } 260 | 261 | /** 262 | * Returns the lowest key greater than to the given key 263 | * 264 | * @param mixed $key The searched key 265 | * 266 | * @return mixed The found key 267 | * 268 | * @throws OutOfBoundsException If there is no higher element 269 | * 270 | * @since 1.0.0 271 | */ 272 | public function higherKey($key) 273 | { 274 | return $this->higher($key)->key; 275 | } 276 | 277 | /** 278 | * Returns the value whose key is the lowest key greater than to the given key 279 | * 280 | * @param mixed $key The searched key 281 | * 282 | * @return mixed The found value 283 | * 284 | * @throws OutOfBoundsException If there is no higher element 285 | * 286 | * @since 1.0.0 287 | */ 288 | public function higherValue($key) 289 | { 290 | return $this->higher($key)->value; 291 | } 292 | 293 | /** 294 | * Keys iterator 295 | * 296 | * @return Iterator The keys iterator 297 | * 298 | * @since 1.0.0 299 | */ 300 | public function keys() 301 | { 302 | return Iterator::keys($this); 303 | } 304 | 305 | /** 306 | * Values iterator 307 | * 308 | * @return Iterator The values iterator 309 | * 310 | * @since 1.0.0 311 | */ 312 | public function values() 313 | { 314 | return Iterator::values($this); 315 | } 316 | 317 | /** 318 | * Convert the object to a string 319 | * 320 | * @return string String representation of the object 321 | * 322 | * @since 1.0.0 323 | */ 324 | public function __toString() 325 | { 326 | return json_encode($this->toArray()); 327 | } 328 | 329 | /** 330 | * Convert the object to an array 331 | * 332 | * @return array Array representation of the object 333 | * 334 | * @since 1.0.0 335 | */ 336 | public function toArray() 337 | { 338 | $array = array(); 339 | 340 | foreach ($this as $key => $value) { 341 | $array[$key] = $value; 342 | } 343 | 344 | return $array; 345 | } 346 | 347 | /** 348 | * Create an iterator 349 | * 350 | * @return Iterator A new iterator 351 | * 352 | * @since 1.0.0 353 | */ 354 | public function getIterator(): Iterator 355 | { 356 | return Iterator::create($this); 357 | } 358 | 359 | /** 360 | * Get the value for a key 361 | * 362 | * @param mixed $key The key 363 | * 364 | * @return mixed The found value 365 | * 366 | * @throws OutOfRangeException If there is no such element 367 | * 368 | * @since 1.0.0 369 | */ 370 | public function offsetGet($key): mixed 371 | { 372 | try { 373 | return $this->find($key)->value; 374 | } catch (\OutOfBoundsException $e) { 375 | throw new \OutOfRangeException('Undefined offset'); 376 | } 377 | } 378 | 379 | /** 380 | * Test the existence of a key 381 | * 382 | * @param mixed $key The key 383 | * 384 | * @return boolean TRUE if the key exists, false otherwise 385 | * 386 | * @since 1.0.0 387 | */ 388 | public function offsetExists($key): bool 389 | { 390 | try { 391 | return (bool) $this->find($key); 392 | } catch (\OutOfBoundsException $e) { 393 | return false; 394 | } 395 | } 396 | 397 | /** 398 | * Set the value for a key 399 | * 400 | * @param mixed $key The key 401 | * @param mixed $value The value 402 | * 403 | * @return void 404 | * 405 | * @throws RuntimeOperation The operation is not supported by this class 406 | * 407 | * @since 1.0.0 408 | */ 409 | public function offsetSet($key, $value): void 410 | { 411 | throw new \RuntimeException('Unsupported operation'); 412 | } 413 | 414 | /** 415 | * Unset the existence of a key 416 | * 417 | * @param mixed $key The key 418 | * 419 | * @return void 420 | * 421 | * @throws RuntimeOperation The operation is not supported by this class 422 | * 423 | * @since 1.0.0 424 | */ 425 | public function offsetUnset($key): void 426 | { 427 | throw new \RuntimeException('Unsupported operation'); 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/SortedCollection/AbstractSet.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Abstract set 19 | * 20 | * @package SortedCollection 21 | * @subpackage Set 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The element comparison function 26 | * @property-read mixed $first The first element of the set 27 | * @property-read mixed $last The last element of the set 28 | * @property-read integer $count The number of elements in the set 29 | */ 30 | abstract class AbstractSet implements SortedSet 31 | { 32 | /** 33 | * @var SortedMap Underlying map 34 | * 35 | * @since 1.0.0 36 | */ 37 | private $map; 38 | 39 | /** 40 | * Get the map 41 | * 42 | * @return SortedMap The underlying map 43 | * 44 | * @since 1.0.0 45 | */ 46 | protected function getMap() 47 | { 48 | return $this->map; 49 | } 50 | 51 | /** 52 | * Set the map 53 | * 54 | * @param SortedMap $map The underlying map 55 | * 56 | * @return AbstractSet $this for chaining 57 | * 58 | * @since 1.0.0 59 | */ 60 | protected function setMap(SortedMap $map) 61 | { 62 | $this->map = $map; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Magic get method 69 | * 70 | * @param string $property The property 71 | * 72 | * @throws RuntimeException If the property does not exist 73 | * 74 | * @return mixed The value associated to the property 75 | * 76 | * @since 1.0.0 77 | */ 78 | public function __get($property) 79 | { 80 | switch ($property) { 81 | case 'comparator': 82 | return $this->comparator(); 83 | case 'first': 84 | return $this->first(); 85 | case 'last': 86 | return $this->last(); 87 | case 'count': 88 | return $this->count(); 89 | default: 90 | throw new \RuntimeException('Undefined property'); 91 | } 92 | } 93 | 94 | /** 95 | * Get the comparator 96 | * 97 | * @return callable The comparator 98 | * 99 | * @since 1.0.0 100 | */ 101 | public function comparator() 102 | { 103 | return $this->map->comparator(); 104 | } 105 | 106 | /** 107 | * Get the first element 108 | * 109 | * @return mixed The first element 110 | * 111 | * @throws OutOfBoundsException If there is no element 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function first() 116 | { 117 | return $this->map->firstKey(); 118 | } 119 | 120 | /** 121 | * Get the last element 122 | * 123 | * @return mixed The last element 124 | * 125 | * @throws OutOfBoundsException If there is no element 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function last() 130 | { 131 | return $this->map->lastKey(); 132 | } 133 | 134 | /** 135 | * Returns the greatest element lesser than the given element 136 | * 137 | * @param mixed $element The searched element 138 | * 139 | * @return mixed The found element 140 | * 141 | * @throws OutOfBoundsException If there is no lower element 142 | * 143 | * @since 1.0.0 144 | */ 145 | public function lower($element) 146 | { 147 | return $this->map->lowerKey($element); 148 | } 149 | 150 | /** 151 | * Returns the greatest element lesser than or equal to the given element 152 | * 153 | * @param mixed $element The searched element 154 | * 155 | * @return mixed The found element 156 | * 157 | * @throws OutOfBoundsException If there is no floor element 158 | * 159 | * @since 1.0.0 160 | */ 161 | public function floor($element) 162 | { 163 | return $this->map->floorKey($element); 164 | } 165 | 166 | /** 167 | * Returns the element equal to the given element 168 | * 169 | * @param mixed $element The searched element 170 | * 171 | * @return mixed The found element 172 | * 173 | * @throws OutOfBoundsException If there is no such element 174 | * 175 | * @since 1.0.0 176 | */ 177 | public function find($element) 178 | { 179 | return $this->map->findKey($element); 180 | } 181 | 182 | /** 183 | * Returns the lowest element greater than or equal to the given element 184 | * 185 | * @param mixed $element The searched element 186 | * 187 | * @return mixed The found element 188 | * 189 | * @throws OutOfBoundsException If there is no ceiling element 190 | * 191 | * @since 1.0.0 192 | */ 193 | public function ceiling($element) 194 | { 195 | return $this->map->ceilingKey($element); 196 | } 197 | 198 | /** 199 | * Returns the lowest element greater than to the given element 200 | * 201 | * @param mixed $element The searched element 202 | * 203 | * @return mixed The found element 204 | * 205 | * @throws OutOfBoundsException If there is no higher element 206 | * 207 | * @since 1.0.0 208 | */ 209 | public function higher($element) 210 | { 211 | return $this->map->higherKey($element); 212 | } 213 | 214 | /** 215 | * Convert the object to a string 216 | * 217 | * @return string String representation of the object 218 | * 219 | * @since 1.0.0 220 | */ 221 | public function __toString() 222 | { 223 | return json_encode($this->toArray()); 224 | } 225 | 226 | /** 227 | * Convert the object to an array 228 | * 229 | * @return array Array representation of the object 230 | * 231 | * @since 1.0.0 232 | */ 233 | public function toArray() 234 | { 235 | $array = array(); 236 | 237 | foreach ($this as $value) { 238 | $array[] = $value; 239 | } 240 | 241 | return $array; 242 | } 243 | 244 | /** 245 | * Create an iterator 246 | * 247 | * @return Iterator A new iterator 248 | * 249 | * @since 1.0.0 250 | */ 251 | public function getIterator(): Iterator 252 | { 253 | return Iterator::keys($this->map); 254 | } 255 | 256 | /** 257 | * Get the value for an element 258 | * 259 | * @param mixed $element The element 260 | * 261 | * @return mixed The found value 262 | * 263 | * @since 1.0.0 264 | */ 265 | public function offsetGet($element): mixed 266 | { 267 | try { 268 | return (bool) $this->map->find($element); 269 | } catch (\OutOfBoundsException $e) { 270 | return false; 271 | } 272 | } 273 | 274 | /** 275 | * Test the existence of an element 276 | * 277 | * @param mixed $element The element 278 | * 279 | * @return boolean TRUE if the element exists, false otherwise 280 | * 281 | * @since 1.0.0 282 | */ 283 | public function offsetExists($element): bool 284 | { 285 | return $this->offsetGet($element); 286 | } 287 | 288 | /** 289 | * Set the value for an element 290 | * 291 | * @param mixed $element The element 292 | * @param mixed $value The value 293 | * 294 | * @return void 295 | * 296 | * @throws RuntimeOperation The operation is not supported by this class 297 | * 298 | * @since 1.0.0 299 | */ 300 | public function offsetSet($element, $value): void 301 | { 302 | throw new \RuntimeException('Unsupported operation'); 303 | } 304 | 305 | /** 306 | * Unset the existence of an element 307 | * 308 | * @param mixed $element The element 309 | * 310 | * @return void 311 | * 312 | * @throws RuntimeOperation The operation is not supported by this class 313 | * 314 | * @since 1.0.0 315 | */ 316 | public function offsetUnset($element): void 317 | { 318 | throw new \RuntimeException('Unsupported operation'); 319 | } 320 | 321 | /** 322 | * Count the number of elements 323 | * 324 | * @return integer 325 | * 326 | * @since 1.0.0 327 | */ 328 | public function count(): int 329 | { 330 | return count($this->map); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/SortedCollection/Iterator.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Iterator 19 | * 20 | * @package SortedCollection 21 | * 22 | * @since 1.0.0 23 | */ 24 | class Iterator implements \Iterator 25 | { 26 | /** 27 | * Iterate on pairs 28 | * 29 | * @since 1.0.0 30 | */ 31 | private const PAIRS = 0; 32 | 33 | /** 34 | * Iterate on keys 35 | * 36 | * @since 1.0.0 37 | */ 38 | private const KEYS = 1; 39 | 40 | /** 41 | * Iterate on values 42 | * 43 | * @since 1.0.0 44 | */ 45 | private const VALUES = 2; 46 | 47 | /** 48 | * @var integer Type: self::PAIRS, self::KEYS or self::VALUES 49 | * 50 | * @since 1.0.0 51 | */ 52 | private $type; 53 | 54 | /** 55 | * @var integer Index 56 | * 57 | * @since 1.0.0 58 | */ 59 | private $index; 60 | 61 | /** 62 | * @var SortedMap Map 63 | * 64 | * @since 1.0.0 65 | */ 66 | private $map; 67 | 68 | /** 69 | * Constructor 70 | * 71 | * @param SortedMap $map Sorted map 72 | * @param integer $type Iterator type 73 | * 74 | * @since 1.0.0 75 | */ 76 | protected function __construct(SortedMap $map, $type) 77 | { 78 | $this->map = $map; 79 | $this->type = $type; 80 | $this->rewind(); 81 | } 82 | 83 | /** 84 | * Create a new iterator on pairs 85 | * 86 | * @param SortedMap $map Sorted map 87 | * 88 | * @return Iterator A new iterator on pairs 89 | * 90 | * @since 1.0.0 91 | */ 92 | public static function create(SortedMap $map) 93 | { 94 | return new static($map, self::PAIRS); 95 | } 96 | 97 | /** 98 | * Create a new iterator on keys 99 | * 100 | * @param SortedMap $map Sorted map 101 | * 102 | * @return Iterator A new iterator on keys 103 | * 104 | * @since 1.0.0 105 | */ 106 | public static function keys(SortedMap $map) 107 | { 108 | return new static($map, self::KEYS); 109 | } 110 | 111 | /** 112 | * Create a new iterator on values 113 | * 114 | * @param SortedMap $map Sorted map 115 | * 116 | * @return Iterator A new iterator on values 117 | * 118 | * @since 1.0.0 119 | */ 120 | public static function values(SortedMap $map) 121 | { 122 | return new static($map, self::VALUES); 123 | } 124 | 125 | /** 126 | * @var TreeNode The current node 127 | * 128 | * @since 1.0.0 129 | */ 130 | protected $current; 131 | 132 | /** 133 | * Rewind the Iterator to the first element 134 | * 135 | * @return void 136 | * 137 | * @since 1.0.0 138 | */ 139 | public function rewind(): void 140 | { 141 | $this->index = 0; 142 | 143 | try { 144 | $this->current = $this->map->first(); 145 | } catch (\OutOfBoundsException $e) { 146 | $this->current = null; 147 | } 148 | } 149 | 150 | /** 151 | * Return the current key 152 | * 153 | * @return mixed The current key 154 | * 155 | * @since 1.0.0 156 | */ 157 | public function key(): mixed 158 | { 159 | if ($this->type == self::PAIRS) { 160 | return $this->current->key; 161 | } else { 162 | return $this->index; 163 | } 164 | } 165 | 166 | /** 167 | * Return the current value 168 | * 169 | * @return mixed The current value 170 | * 171 | * @since 1.0.0 172 | */ 173 | public function current(): mixed 174 | { 175 | if ($this->type == self::KEYS) { 176 | return $this->current->key; 177 | } else { 178 | return $this->current->value; 179 | } 180 | } 181 | 182 | /** 183 | * Move forward to the next element 184 | * 185 | * @return void 186 | * 187 | * @since 1.0.0 188 | */ 189 | public function next(): void 190 | { 191 | try { 192 | $this->current = $this->map->successor($this->current); 193 | } catch (\OutOfBoundsException $e) { 194 | $this->current = null; 195 | } 196 | 197 | $this->index++; 198 | } 199 | 200 | /** 201 | * Checks if current position is valid 202 | * 203 | * @return boolean 204 | * 205 | * @since 1.0.0 206 | */ 207 | public function valid(): bool 208 | { 209 | return (bool) $this->current; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/SortedCollection/ReversedMap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Reversed map 19 | * 20 | * @package SortedCollection 21 | * @subpackage Map 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The key comparison function 26 | * @property-read TreeNode $first The first element of the map 27 | * @property-read mixed $firstKey The first key of the map 28 | * @property-read mixed $firstValue The first value of the map 29 | * @property-read TreeNode $last The last element of the map 30 | * @property-read mixed $lastKey The last key of the map 31 | * @property-read mixed $lastValue The last value of the map 32 | * @property-read Iterator $keys The keys iterator 33 | * @property-read Iterator $values The values iterator 34 | * @property-read integer $count The number of elements in the map 35 | * @property-read SortedMap $map The underlying map 36 | */ 37 | class ReversedMap extends AbstractMap 38 | { 39 | /** 40 | * @var SortedMap Internal map 41 | * 42 | * @since 1.0.0 43 | */ 44 | private $map; 45 | 46 | /** 47 | * @var callable Comparator function 48 | * 49 | * @param mixed $key1 First key 50 | * @param mixed $key2 Second key 51 | * 52 | * @return integer negative if $key1 is lesser than $key2, 53 | * 0 if $key1 is equal to $key2, 54 | * positive if $key1 is greater than $key2 55 | * 56 | * @since 1.0.0 57 | */ 58 | private $comparator; 59 | 60 | /** 61 | * Constructor 62 | * 63 | * @param SortedMap $map Internal map 64 | * 65 | * @since 1.0.0 66 | */ 67 | protected function __construct(SortedMap $map) 68 | { 69 | $this->map = $map; 70 | $this->comparator = function ($key1, $key2) { 71 | return - call_user_func($this->map->comparator, $key1, $key2); 72 | }; 73 | } 74 | 75 | /** 76 | * Create 77 | * 78 | * @param SortedMap $map Internal map 79 | * 80 | * @return ReversedMap A new reversed map 81 | * 82 | * @since 1.0.0 83 | */ 84 | public static function create(SortedMap $map) 85 | { 86 | return new static($map); 87 | } 88 | 89 | /** 90 | * Magic get method 91 | * 92 | * @param string $property The property 93 | * 94 | * @return mixed The value associated to the property 95 | * 96 | * @since 1.0.0 97 | */ 98 | public function __get($property) 99 | { 100 | switch ($property) { 101 | case 'map': 102 | return $this->map; 103 | default: 104 | return parent::__get($property); 105 | } 106 | } 107 | 108 | /** 109 | * Get the comparator 110 | * 111 | * @return callable The comparator 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function comparator() 116 | { 117 | return $this->comparator; 118 | } 119 | 120 | /** 121 | * Get the first element 122 | * 123 | * @return mixed The first element 124 | * 125 | * @throws OutOfBoundsException If there is no element 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function first() 130 | { 131 | return $this->map->last(); 132 | } 133 | 134 | /** 135 | * Get the last element 136 | * 137 | * @return mixed The last element 138 | * 139 | * @throws OutOfBoundsException If there is no element 140 | * 141 | * @since 1.0.0 142 | */ 143 | public function last() 144 | { 145 | return $this->map->first(); 146 | } 147 | 148 | /** 149 | * Get the predecessor element 150 | * 151 | * @param TreeNode $element A tree node member of the underlying TreeMap 152 | * 153 | * @return mixed The predecessor element 154 | * 155 | * @throws OutOfBoundsException If there is no predecessor 156 | * 157 | * @since 1.0.0 158 | */ 159 | public function predecessor($element) 160 | { 161 | return $this->map->successor($element); 162 | } 163 | 164 | /** 165 | * Get the successor element 166 | * 167 | * @param TreeNode $element A tree node member of the underlying TreeMap 168 | * 169 | * @return mixed The successor element 170 | * 171 | * @throws OutOfBoundsException If there is no successor 172 | */ 173 | public function successor($element) 174 | { 175 | return $this->map->predecessor($element); 176 | } 177 | 178 | /** 179 | * Returns the element whose key is the greatest key lesser than the given key 180 | * 181 | * @param mixed $key The searched key 182 | * 183 | * @return mixed The found element 184 | * 185 | * @throws OutOfBoundsException If there is no lower element 186 | * 187 | * @since 1.0.0 188 | */ 189 | public function lower($key) 190 | { 191 | return $this->map->higher($key); 192 | } 193 | 194 | /** 195 | * Returns the element whose key is the greatest key lesser than or equal to the given key 196 | * 197 | * @param mixed $key The searched key 198 | * 199 | * @return mixed The found element 200 | * 201 | * @throws OutOfBoundsException If there is no floor element 202 | * 203 | * @since 1.0.0 204 | */ 205 | public function floor($key) 206 | { 207 | return $this->map->ceiling($key); 208 | } 209 | 210 | /** 211 | * Returns the element whose key is equal to the given key 212 | * 213 | * @param mixed $key The searched key 214 | * 215 | * @return mixed The found element 216 | * 217 | * @throws OutOfBoundsException If there is no such element 218 | * 219 | * @since 1.0.0 220 | */ 221 | public function find($key) 222 | { 223 | return $this->map->find($key); 224 | } 225 | 226 | /** 227 | * Returns the element whose key is the lowest key greater than or equal to the given key 228 | * 229 | * @param mixed $key The searched key 230 | * 231 | * @return mixed The found element 232 | * 233 | * @throws OutOfBoundsException If there is no ceiling element 234 | * 235 | * @since 1.0.0 236 | */ 237 | public function ceiling($key) 238 | { 239 | return $this->map->floor($key); 240 | } 241 | 242 | /** 243 | * Returns the element whose key is the lowest key greater than to the given key 244 | * 245 | * @param mixed $key The searched key 246 | * 247 | * @return mixed The found element 248 | * 249 | * @throws OutOfBoundsException If there is no higher element 250 | * 251 | * @since 1.0.0 252 | */ 253 | public function higher($key) 254 | { 255 | return $this->map->lower($key); 256 | } 257 | 258 | /** 259 | * Serialize the object 260 | * 261 | * @return array Array of values 262 | * 263 | * @since 1.0.0 264 | */ 265 | public function jsonSerialize(): array 266 | { 267 | return array('ReversedMap' => $this->map->jsonSerialize()); 268 | } 269 | 270 | /** 271 | * Count the number of key/value pairs 272 | * 273 | * @return integer 274 | * 275 | * @since 1.0.0 276 | */ 277 | public function count(): int 278 | { 279 | return $this->map->count(); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/SortedCollection/ReversedSet.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Reversed set 19 | * 20 | * @package SortedCollection 21 | * @subpackage Set 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The element comparison function 26 | * @property-read mixed $first The first element of the set 27 | * @property-read mixed $last The last element of the set 28 | * @property-read integer $count The number of elements in the set 29 | * @property-read SortedSet $set The underlying set 30 | */ 31 | class ReversedSet extends AbstractSet 32 | { 33 | /** 34 | * @var SortedSet Internal set 35 | * 36 | * @since 1.0.0 37 | */ 38 | private $set; 39 | 40 | /** 41 | * Constructor 42 | * 43 | * @param SortedSet $set Internal set 44 | * 45 | * @since 1.0.0 46 | */ 47 | protected function __construct(SortedSet $set) 48 | { 49 | $this->setMap(ReversedMap::create($set->getMap()))->set = $set; 50 | } 51 | 52 | /** 53 | * Create 54 | * 55 | * @param SortedSet $set Internal set 56 | * 57 | * @return ReversedSet A new reversed set 58 | * 59 | * @since 1.0.0 60 | */ 61 | public static function create(SortedSet $set) 62 | { 63 | return new static($set); 64 | } 65 | 66 | /** 67 | * Magic get method 68 | * 69 | * @param string $property The property 70 | * 71 | * @return mixed The value associated to the property 72 | * 73 | * @since 1.0.0 74 | */ 75 | public function __get($property) 76 | { 77 | switch ($property) { 78 | case 'set': 79 | return $this->set; 80 | default: 81 | return parent::__get($property); 82 | } 83 | } 84 | 85 | /** 86 | * Serialize the object 87 | * 88 | * @return array Array of values 89 | * 90 | * @since 1.0.0 91 | */ 92 | public function jsonSerialize(): array 93 | { 94 | return array('ReversedSet' => $this->set->jsonSerialize()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/SortedCollection/SortedCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * The SortedCollection interface is the root of the hierarchy. It extends: 19 | * 20 | * * :php:class:`ArrayAccess` 21 | * * :php:class:`IteratorAggregate` 22 | * * :php:class:`JsonSerializable` 23 | * * :php:class:`Countable` 24 | * 25 | * And it is implemented by two classes: :php:class:`chdemko\\SortedCollection\\SortedMap` 26 | * and :php:class:`chdemko\\SortedCollection\\SortedSet` 27 | * 28 | * @package SortedCollection 29 | * 30 | * @since 1.0.0 31 | */ 32 | interface SortedCollection extends \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable 33 | { 34 | /** 35 | * Get the comparator 36 | * 37 | * @return callable The comparator 38 | * 39 | * @since 1.0.0 40 | */ 41 | public function comparator(); 42 | 43 | /** 44 | * Get the first element 45 | * 46 | * @return mixed The first element 47 | * 48 | * @throws OutOfBoundsException If there is no element 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function first(); 53 | 54 | /** 55 | * Get the last element 56 | * 57 | * @return mixed The last element 58 | * 59 | * @throws OutOfBoundsException If there is no element 60 | * 61 | * @since 1.0.0 62 | */ 63 | public function last(); 64 | 65 | /** 66 | * Returns the greatest element lesser than the given key 67 | * 68 | * @param mixed $key The searched key 69 | * 70 | * @return mixed The found node 71 | * 72 | * @throws OutOfBoundsException If there is no lower element 73 | * 74 | * @since 1.0.0 75 | */ 76 | public function lower($key); 77 | 78 | /** 79 | * Returns the greatest element lesser than or equal to the given key 80 | * 81 | * @param mixed $key The searched key 82 | * 83 | * @return mixed The found node 84 | * 85 | * @throws OutOfBoundsException If there is no floor element 86 | * 87 | * @since 1.0.0 88 | */ 89 | public function floor($key); 90 | 91 | /** 92 | * Returns the element equal to the given key 93 | * 94 | * @param mixed $key The searched key 95 | * 96 | * @return mixed The found node 97 | * 98 | * @throws OutOfBoundsException If there is no such element 99 | * 100 | * @since 1.0.0 101 | */ 102 | public function find($key); 103 | 104 | /** 105 | * Returns the lowest element greater than or equal to the given key 106 | * 107 | * @param mixed $key The searched key 108 | * 109 | * @return mixed The found node 110 | * 111 | * @throws OutOfBoundsException If there is no ceiling element 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function ceiling($key); 116 | 117 | /** 118 | * Returns the lowest element greater than to the given key 119 | * 120 | * @param mixed $key The searched key 121 | * 122 | * @return mixed The found node 123 | * 124 | * @throws OutOfBoundsException If there is no higher element 125 | * 126 | * @since 1.0.0 127 | */ 128 | public function higher($key); 129 | } 130 | -------------------------------------------------------------------------------- /src/SortedCollection/SortedMap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * SortedMap 19 | * 20 | * @package SortedCollection 21 | * @subpackage Map 22 | * 23 | * @since 1.0.0 24 | */ 25 | interface SortedMap extends SortedCollection 26 | { 27 | /** 28 | * Get the first key or throw an exception if there is no element 29 | * 30 | * @return mixed The first key 31 | * 32 | * @throws OutOfBoundsException If there is no element 33 | * 34 | * @since 1.0.0 35 | */ 36 | public function firstKey(); 37 | 38 | /** 39 | * Get the last key or throw an exception if there is no element 40 | * 41 | * @return mixed The last key 42 | * 43 | * @throws OutOfBoundsException If there is no element 44 | * 45 | * @since 1.0.0 46 | */ 47 | public function lastKey(); 48 | 49 | /** 50 | * Returns the greatest key lesser than the given key or throw an exception if there is no such key 51 | * 52 | * @param mixed $key The searched key 53 | * 54 | * @return mixed The found key 55 | * 56 | * @throws OutOfBoundsException If there is no lower element 57 | * 58 | * @since 1.0.0 59 | */ 60 | public function lowerKey($key); 61 | 62 | /** 63 | * Returns the greatest key lesser than or equal to the given key or throw an exception if there is no such key 64 | * 65 | * @param mixed $key The searched key 66 | * 67 | * @return mixed The found key 68 | * 69 | * @throws OutOfBoundsException If there is no floor element 70 | * 71 | * @since 1.0.0 72 | */ 73 | public function floorKey($key); 74 | 75 | /** 76 | * Returns the key equal to the given key or throw an exception if there is no such key 77 | * 78 | * @param mixed $key The searched key 79 | * 80 | * @return mixed The found key 81 | * 82 | * @throws OutOfBoundsException If there is no such element 83 | * 84 | * @since 1.0.0 85 | */ 86 | public function findKey($key); 87 | 88 | /** 89 | * Returns the lowest key greater than or equal to the given key or throw an exception if there is no such key 90 | * 91 | * @param mixed $key The searched key 92 | * 93 | * @return mixed The found key 94 | * 95 | * @throws OutOfBoundsException If there is no ceiling element 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function ceilingKey($key); 100 | 101 | /** 102 | * Returns the lowest key greater than to the given key or throw an exception if there is no such key 103 | * 104 | * @param mixed $key The searched key 105 | * 106 | * @return mixed The found key 107 | * 108 | * @throws OutOfBoundsException If there is no higher element 109 | * 110 | * @since 1.0.0 111 | */ 112 | public function higherKey($key); 113 | 114 | /** 115 | * Get the predecessor node 116 | * 117 | * @param TreeNode $node A tree node member of the underlying TreeMap 118 | * 119 | * @return mixed The predecessor node 120 | * 121 | * @since 1.0.0 122 | */ 123 | public function predecessor($node); 124 | 125 | /** 126 | * Get the successor node 127 | * 128 | * @param TreeNode $node A tree node member of the underlying TreeMap 129 | * 130 | * @return mixed The successor node 131 | * 132 | * @since 1.0.0 133 | */ 134 | public function successor($node); 135 | 136 | /** 137 | * Keys generator 138 | * 139 | * @return mixed The keys generator 140 | * 141 | * @since 1.0.0 142 | */ 143 | public function keys(); 144 | 145 | /** 146 | * Values generator 147 | * 148 | * @return mixed The values generator 149 | * 150 | * @since 1.0.0 151 | */ 152 | public function values(); 153 | } 154 | -------------------------------------------------------------------------------- /src/SortedCollection/SortedSet.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Sorted set 19 | * 20 | * @package SortedCollection 21 | * @subpackage Set 22 | * 23 | * @since 1.0.0 24 | */ 25 | interface SortedSet extends SortedCollection 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /src/SortedCollection/SubSet.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Sub set 19 | * 20 | * @package SortedCollection 21 | * @subpackage Set 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property mixed $from The from element 26 | * @property boolean $fromInclusive The from inclusive flag 27 | * @property mixed $to The to element 28 | * @property boolean $toInclusive The to inclusive flag 29 | * @property-read callable $comparator The element comparison function 30 | * @property-read mixed $first The first element of the set 31 | * @property-read mixed $last The last element of the set 32 | * @property-read integer $count The number of elements in the set 33 | * @property-read SortedSet $set The underlying set 34 | */ 35 | class SubSet extends AbstractSet 36 | { 37 | /** 38 | * When the from or to value is unused 39 | * 40 | * @since 1.0.0 41 | */ 42 | private const UNUSED = 0; 43 | 44 | /** 45 | * When the from or to value is inclusive 46 | * 47 | * @since 1.0.0 48 | */ 49 | private const INCLUSIVE = 1; 50 | 51 | /** 52 | * When the from or to value is exclusive 53 | * 54 | * @since 1.0.0 55 | */ 56 | private const EXCLUSIVE = 2; 57 | 58 | /** 59 | * @var SortedSet Internal set 60 | * 61 | * @since 1.0.0 62 | */ 63 | private $set; 64 | 65 | /** 66 | * Magic get method 67 | * 68 | * @param string $property The property 69 | * 70 | * @return mixed The value associated to the property 71 | * 72 | * @since 1.0.0 73 | */ 74 | public function __get($property) 75 | { 76 | switch ($property) { 77 | case 'from': 78 | return $this->getMap()->fromKey; 79 | case 'to': 80 | return $this->getMap()->toKey; 81 | case 'fromInclusive': 82 | return $this->getMap()->fromInclusive; 83 | case 'toInclusive': 84 | return $this->getMap()->toInclusive; 85 | case 'set': 86 | return $this->set; 87 | default: 88 | return parent::__get($property); 89 | } 90 | } 91 | 92 | /** 93 | * Magic set method 94 | * 95 | * @param string $property The property 96 | * @param mixed $value The new value 97 | * 98 | * @throws RuntimeException If the property does not exist 99 | * 100 | * @return void 101 | * 102 | * @since 1.0.0 103 | */ 104 | public function __set($property, $value) 105 | { 106 | switch ($property) { 107 | case 'from': 108 | $this->getMap()->fromKey = $value; 109 | break; 110 | case 'to': 111 | $this->getMap()->toKey = $value; 112 | break; 113 | case 'fromInclusive': 114 | $this->getMap()->fromInclusive = $value; 115 | break; 116 | case 'toInclusive': 117 | $this->getMap()->toInclusive = $value; 118 | break; 119 | default: 120 | throw new \RuntimeException('Undefined property'); 121 | } 122 | } 123 | 124 | /** 125 | * Magic unset method 126 | * 127 | * @param string $property The property 128 | * 129 | * @throws RuntimeException If the property does not exist 130 | * 131 | * @return void 132 | * 133 | * @since 1.0.0 134 | */ 135 | public function __unset($property) 136 | { 137 | switch ($property) { 138 | case 'from': 139 | unset($this->getMap()->fromKey); 140 | break; 141 | case 'to': 142 | unset($this->getMap()->toKey); 143 | break; 144 | case 'fromInclusive': 145 | unset($this->getMap()->fromInclusive); 146 | break; 147 | case 'toInclusive': 148 | unset($this->getMap()->toInclusive); 149 | break; 150 | default: 151 | throw new \RuntimeException('Undefined property'); 152 | } 153 | } 154 | 155 | /** 156 | * Magic isset method 157 | * 158 | * @param string $property The property 159 | * 160 | * @return boolean 161 | * 162 | * @since 1.0.0 163 | */ 164 | public function __isset($property) 165 | { 166 | switch ($property) { 167 | case 'from': 168 | return isset($this->getMap()->fromKey); 169 | case 'to': 170 | return isset($this->getMap()->toKey); 171 | case 'fromInclusive': 172 | return isset($this->getMap()->fromInclusive); 173 | case 'toInclusive': 174 | return isset($this->getMap()->toInclusive); 175 | default: 176 | return false; 177 | } 178 | } 179 | 180 | /** 181 | * Constructor 182 | * 183 | * @param SortedSet $set Internal set 184 | * @param mixed $from The from element 185 | * @param integer $fromOption The option for from (SubSet::UNUSED, SubSet::INCLUSIVE or SubSet::EXCLUSIVE) 186 | * @param mixed $to The to element 187 | * @param integer $toOption The option for to (SubSet::UNUSED, SubSet::INCLUSIVE or SubSet::EXCLUSIVE) 188 | * 189 | * @since 1.0.0 190 | */ 191 | protected function __construct(SortedSet $set, $from, $fromOption, $to, $toOption) 192 | { 193 | if ($fromOption == self::UNUSED) { 194 | if ($toOption == self::UNUSED) { 195 | $this->setMap(SubMap::view($set->getMap())); 196 | } else { 197 | $this->setMap(SubMap::head($set->getMap(), $to, $toOption == self::INCLUSIVE)); 198 | } 199 | } elseif ($toOption == self::UNUSED) { 200 | $this->setMap(SubMap::tail($set->getMap(), $from, $fromOption == self::INCLUSIVE)); 201 | } else { 202 | $this->setMap( 203 | SubMap::create($set->getMap(), $from, $to, $fromOption == self::INCLUSIVE, $toOption == self::INCLUSIVE) 204 | ); 205 | } 206 | 207 | $this->set = $set; 208 | } 209 | 210 | /** 211 | * Create 212 | * 213 | * @param SortedSet $set Internal set 214 | * @param mixed $from The from element 215 | * @param mixed $to The to element 216 | * @param boolean $fromInclusive The inclusive flag for from 217 | * @param boolean $toInclusive The inclusive flag for to 218 | * 219 | * @return SubSet A new sub set 220 | * 221 | * @since 1.0.0 222 | */ 223 | public static function create(SortedSet $set, $from, $to, $fromInclusive = true, $toInclusive = false) 224 | { 225 | return new static( 226 | $set, 227 | $from, 228 | $fromInclusive ? self::INCLUSIVE : self::EXCLUSIVE, 229 | $to, 230 | $toInclusive ? self::INCLUSIVE : self::EXCLUSIVE 231 | ); 232 | } 233 | 234 | /** 235 | * Head 236 | * 237 | * @param SortedSet $set Internal set 238 | * @param mixed $to The to element 239 | * @param boolean $toInclusive The inclusive flag for to 240 | * 241 | * @return SubSet A new head set 242 | * 243 | * @since 1.0.0 244 | */ 245 | public static function head(SortedSet $set, $to, $toInclusive = false) 246 | { 247 | return new static($set, null, self::UNUSED, $to, $toInclusive ? self::INCLUSIVE : self::EXCLUSIVE); 248 | } 249 | 250 | /** 251 | * Tail 252 | * 253 | * @param SortedSet $set Internal set 254 | * @param mixed $from The from element 255 | * @param boolean $fromInclusive The inclusive flag for from 256 | * 257 | * @return SubSet A new tail set 258 | * 259 | * @since 1.0.0 260 | */ 261 | public static function tail(SortedSet $set, $from, $fromInclusive = true) 262 | { 263 | return new static($set, $from, $fromInclusive ? self::INCLUSIVE : self::EXCLUSIVE, null, self::UNUSED); 264 | } 265 | 266 | /** 267 | * View 268 | * 269 | * @param SortedSet $set Internal set 270 | * 271 | * @return SubSet A new sub set 272 | * 273 | * @since 1.0.0 274 | */ 275 | public static function view(SortedSet $set) 276 | { 277 | return new static($set, null, self::UNUSED, null, self::UNUSED); 278 | } 279 | 280 | /** 281 | * Serialize the object 282 | * 283 | * @return array Array of values 284 | * 285 | * @since 1.0.0 286 | */ 287 | public function jsonSerialize(): array 288 | { 289 | if (isset($this->from)) { 290 | if (isset($this->to)) { 291 | return array( 292 | 'SubSet' => array( 293 | 'set' => $this->set->jsonSerialize(), 294 | 'from' => $this->from, 295 | 'fromInclusive' => $this->fromInclusive, 296 | 'to' => $this->to, 297 | 'toInclusive' => $this->toInclusive, 298 | ) 299 | ); 300 | } else { 301 | return array( 302 | 'TailSet' => array( 303 | 'set' => $this->set->jsonSerialize(), 304 | 'from' => $this->from, 305 | 'fromInclusive' => $this->fromInclusive, 306 | ) 307 | ); 308 | } 309 | } else { 310 | if (isset($this->to)) { 311 | return array( 312 | 'HeadSet' => array( 313 | 'set' => $this->set->jsonSerialize(), 314 | 'to' => $this->to, 315 | 'toInclusive' => $this->toInclusive, 316 | ) 317 | ); 318 | } else { 319 | return array( 320 | 'ViewSet' => array( 321 | 'set' => $this->set->jsonSerialize(), 322 | ) 323 | ); 324 | } 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/SortedCollection/TreeMap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Tree map 19 | * 20 | * @package SortedCollection 21 | * @subpackage Map 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The key comparison function 26 | * @property-read TreeNode $first The first element of the map 27 | * @property-read mixed $firstKey The first key of the map 28 | * @property-read mixed $firstValue The first value of the map 29 | * @property-read TreeNode $last The last element of the map 30 | * @property-read mixed $lastKey The last key of the map 31 | * @property-read mixed $lastValue The last value of the map 32 | * @property-read Iterator $keys The keys iterator 33 | * @property-read Iterator $values The values iterator 34 | * @property-read integer $count The number of elements in the map 35 | */ 36 | class TreeMap extends AbstractMap 37 | { 38 | /** 39 | * @var TreeNode Root of the tree 40 | * 41 | * @since 1.0.0 42 | */ 43 | private $root; 44 | 45 | /** 46 | * @var callable Comparator function 47 | * 48 | * @param mixed $key1 First key 49 | * @param mixed $key2 Second key 50 | * 51 | * @return integer negative if $key1 is lesser than $key2, 52 | * 0 if $key1 is equal to $key2, 53 | * positive if $key1 is greater than $key2 54 | * 55 | * @since 1.0.0 56 | */ 57 | private $comparator; 58 | 59 | /** 60 | * Constructor 61 | * 62 | * @param callable $comparator Comparison function 63 | * 64 | * @since 1.0.0 65 | */ 66 | protected function __construct($comparator = null) 67 | { 68 | if ($comparator == null) { 69 | $this->comparator = function ($key1, $key2) { 70 | return $key1 - $key2; 71 | }; 72 | } else { 73 | $this->comparator = $comparator; 74 | } 75 | } 76 | 77 | /** 78 | * Create 79 | * 80 | * @param callable $comparator Comparison function 81 | * 82 | * @return TreeMap A new TreeMap 83 | * 84 | * @since 1.0.0 85 | */ 86 | public static function create($comparator = null) 87 | { 88 | return new static($comparator); 89 | } 90 | 91 | /** 92 | * Get the comparator 93 | * 94 | * @return callable The comparator 95 | * 96 | * @since 1.0.0 97 | */ 98 | public function comparator() 99 | { 100 | return $this->comparator; 101 | } 102 | 103 | /** 104 | * Get the first element 105 | * 106 | * @return mixed The first element 107 | * 108 | * @throws OutOfBoundsException If there is no element 109 | * 110 | * @since 1.0.0 111 | */ 112 | public function first() 113 | { 114 | if ($this->root) { 115 | return $this->root->first; 116 | } else { 117 | throw new \OutOfBoundsException('First element unexisting'); 118 | } 119 | } 120 | 121 | /** 122 | * Get the last element 123 | * 124 | * @return mixed The last element 125 | * 126 | * @throws OutOfBoundsException If there is no element 127 | * 128 | * @since 1.0.0 129 | */ 130 | public function last() 131 | { 132 | if ($this->root) { 133 | return $this->root->last; 134 | } else { 135 | throw new \OutOfBoundsException('Last element unexisting'); 136 | } 137 | } 138 | 139 | /** 140 | * Get the predecessor element 141 | * 142 | * @param TreeNode $element A tree node member of the underlying TreeMap 143 | * 144 | * @return mixed The predecessor element 145 | * 146 | * @throws OutOfBoundsException If there is no predecessor 147 | * 148 | * @since 1.0.0 149 | */ 150 | public function predecessor($element) 151 | { 152 | $predecessor = $element->predecessor; 153 | 154 | if ($predecessor) { 155 | return $predecessor; 156 | } else { 157 | throw new \OutOfBoundsException('Predecessor element unexisting'); 158 | } 159 | } 160 | 161 | /** 162 | * Get the successor element 163 | * 164 | * @param TreeNode $element A tree node member of the underlying TreeMap 165 | * 166 | * @return mixed The successor element 167 | * 168 | * @throws OutOfBoundsException If there is no successor 169 | * 170 | * @since 1.0.0 171 | */ 172 | public function successor($element) 173 | { 174 | $successor = $element->successor; 175 | 176 | if ($successor) { 177 | return $successor; 178 | } else { 179 | throw new \OutOfBoundsException('Successor element unexisting'); 180 | } 181 | } 182 | 183 | /** 184 | * Returns the element whose key is the greatest key lesser than the given key 185 | * 186 | * @param mixed $key The searched key 187 | * 188 | * @return mixed The found element 189 | * 190 | * @throws OutOfBoundsException If there is no lower element 191 | * 192 | * @since 1.0.0 193 | */ 194 | public function lower($key) 195 | { 196 | if ($this->root) { 197 | $lower = $this->root->find($key, $this->comparator, -2); 198 | } else { 199 | $lower = null; 200 | } 201 | 202 | if ($lower) { 203 | return $lower; 204 | } else { 205 | throw new \OutOfBoundsException('Lower element unexisting'); 206 | } 207 | } 208 | 209 | /** 210 | * Returns the element whose key is the greatest key lesser than or equal to the given key 211 | * 212 | * @param mixed $key The searched key 213 | * 214 | * @return mixed The found element 215 | * 216 | * @throws OutOfBoundsException If there is no floor element 217 | * 218 | * @since 1.0.0 219 | */ 220 | public function floor($key) 221 | { 222 | if ($this->root) { 223 | $floor = $this->root->find($key, $this->comparator, -1); 224 | } else { 225 | $floor = null; 226 | } 227 | 228 | if ($floor) { 229 | return $floor; 230 | } else { 231 | throw new \OutOfBoundsException('Floor element unexisting'); 232 | } 233 | } 234 | 235 | /** 236 | * Returns the element whose key is equal to the given key 237 | * 238 | * @param mixed $key The searched key 239 | * 240 | * @return mixed The found element 241 | * 242 | * @throws OutOfBoundsException If there is no such element 243 | * 244 | * @since 1.0.0 245 | */ 246 | public function find($key) 247 | { 248 | if ($this->root) { 249 | $find = $this->root->find($key, $this->comparator, 0); 250 | } else { 251 | $find = null; 252 | } 253 | 254 | if ($find) { 255 | return $find; 256 | } else { 257 | throw new \OutOfBoundsException('Element unexisting'); 258 | } 259 | } 260 | 261 | /** 262 | * Returns the element whose key is the lowest key greater than or equal to the given key 263 | * 264 | * @param mixed $key The searched key 265 | * 266 | * @return mixed The found element 267 | * 268 | * @throws OutOfBoundsException If there is no ceiling element 269 | * 270 | * @since 1.0.0 271 | */ 272 | public function ceiling($key) 273 | { 274 | if ($this->root) { 275 | $ceiling = $this->root->find($key, $this->comparator, 1); 276 | } else { 277 | $ceiling = null; 278 | } 279 | 280 | if ($ceiling) { 281 | return $ceiling; 282 | } else { 283 | throw new \OutOfBoundsException('Ceiling element unexisting'); 284 | } 285 | } 286 | 287 | /** 288 | * Returns the element whose key is the lowest key greater than to the given key 289 | * 290 | * @param mixed $key The searched key 291 | * 292 | * @return mixed The found element 293 | * 294 | * @throws OutOfBoundsException If there is no higher element 295 | * 296 | * @since 1.0.0 297 | */ 298 | public function higher($key) 299 | { 300 | if ($this->root) { 301 | $higher = $this->root->find($key, $this->comparator, 2); 302 | } else { 303 | $higher = null; 304 | } 305 | 306 | if ($higher) { 307 | return $higher; 308 | } else { 309 | throw new \OutOfBoundsException('Higher element unexisting'); 310 | } 311 | } 312 | 313 | /** 314 | * Put values in the map 315 | * 316 | * @param \Traversable $traversable Values to put in the map 317 | * 318 | * @return TreeMap $this for chaining 319 | * 320 | * @since 1.0.0 321 | */ 322 | public function put($traversable = array()) 323 | { 324 | foreach ($traversable as $key => $value) { 325 | $this[$key] = $value; 326 | } 327 | 328 | return $this; 329 | } 330 | 331 | /** 332 | * Clear the map 333 | * 334 | * @return TreeMap $this for chaining 335 | * 336 | * @since 1.0.0 337 | */ 338 | public function clear() 339 | { 340 | $this->root = null; 341 | 342 | return $this; 343 | } 344 | 345 | /** 346 | * Initialise the map 347 | * 348 | * @param \Traversable $traversable Values to initialise the map 349 | * 350 | * @return TreeMap $this for chaining 351 | * 352 | * @since 1.0.0 353 | */ 354 | public function initialise($traversable = array()) 355 | { 356 | return $this->clear()->put($traversable); 357 | } 358 | 359 | /** 360 | * Clone the map 361 | * 362 | * @return void 363 | * 364 | * @since 1.0.0 365 | */ 366 | public function __clone() 367 | { 368 | if ($this->root != null) { 369 | $root = $this->root; 370 | $this->root = null; 371 | $node = $root->first; 372 | 373 | while ($node != null) { 374 | $this[$node->key] = $node->value; 375 | $node = $node->successor; 376 | } 377 | } 378 | } 379 | 380 | /** 381 | * Serialize the object 382 | * 383 | * @return array Array of values 384 | * 385 | * @since 1.0.0 386 | */ 387 | public function jsonSerialize(): array 388 | { 389 | $array = array(); 390 | 391 | foreach ($this as $key => $value) { 392 | $array[$key] = $value; 393 | } 394 | 395 | return array('TreeMap' => $array); 396 | } 397 | 398 | /** 399 | * Set the value for a key 400 | * 401 | * @param mixed $key The key 402 | * @param mixed $value The value 403 | * 404 | * @return void 405 | * 406 | * @since 1.0.0 407 | */ 408 | public function offsetSet($key, $value): void 409 | { 410 | if ($this->root) { 411 | $this->root = $this->root->insert($key, $value, $this->comparator); 412 | } else { 413 | $this->root = TreeNode::create($key, $value); 414 | } 415 | } 416 | 417 | /** 418 | * Unset the existence of a key 419 | * 420 | * @param mixed $key The key 421 | * 422 | * @return void 423 | * 424 | * @since 1.0.0 425 | */ 426 | public function offsetUnset($key): void 427 | { 428 | if ($this->root) { 429 | $this->root = $this->root->remove($key, $this->comparator); 430 | } 431 | } 432 | 433 | /** 434 | * Count the number of key/value pairs 435 | * 436 | * @return integer 437 | * 438 | * @since 1.0.0 439 | */ 440 | public function count(): int 441 | { 442 | if ($this->root) { 443 | return count($this->root); 444 | } else { 445 | return 0; 446 | } 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/SortedCollection/TreeNode.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * TreeNode 19 | * 20 | * @package SortedCollection 21 | * 22 | * @since 1.0.0 23 | * 24 | * @property-read TreeNode $first The first node of the tree 25 | * @property-read TreeNode $last The last node of the tree 26 | * @property-read TreeNode $predecessor The predecessor node 27 | * @property-read TreeNode $successor The successor node 28 | * @property-read mixed $key The key 29 | * @property-read integer $count The number of elements in the tree 30 | */ 31 | class TreeNode implements \Countable 32 | { 33 | /** 34 | * @var integer Information associated to that node. 35 | * Bits of order 0 and 1 are reserved for the existence of left and right tree. 36 | * Other bits are for the balance 37 | * 38 | * @since 1.0.0 39 | */ 40 | private $information = 0; 41 | 42 | /** 43 | * @var TreeNode Left|Predecessor node 44 | * 45 | * @since 1.0.0 46 | */ 47 | private $left; 48 | 49 | /** 50 | * @var TreeNode Right|Successor node 51 | * 52 | * @since 1.0.0 53 | */ 54 | private $right; 55 | 56 | /** 57 | * @var mixed Node key 58 | * 59 | * @since 1.0.0 60 | */ 61 | private $key; 62 | 63 | /** 64 | * @var mixed Node value 65 | * 66 | * @since 1.0.0 67 | */ 68 | public $value; 69 | 70 | /** 71 | * Create a node 72 | * 73 | * @param mixed $key The node key 74 | * @param mixed $value The node value 75 | * 76 | * @return A new node 77 | * 78 | * @since 1.0.0 79 | */ 80 | public static function create($key, $value) 81 | { 82 | return new static($key, $value); 83 | } 84 | 85 | /** 86 | * Constructor 87 | * 88 | * @param mixed $key The node key 89 | * @param mixed $value The node value 90 | * @param TreeNode $predecessor The left node 91 | * @param TreeNode $successor The right node 92 | * 93 | * @since 1.0.0 94 | */ 95 | protected function __construct($key, $value, $predecessor = null, $successor = null) 96 | { 97 | $this->key = $key; 98 | $this->value = $value; 99 | $this->left = $predecessor; 100 | $this->right = $successor; 101 | } 102 | 103 | /** 104 | * Magic get method 105 | * 106 | * @param string $property The node property 107 | * 108 | * @return mixed The value associated to the property 109 | * 110 | * @throws RuntimeException If the property is undefined 111 | * 112 | * @since 1.0.0 113 | */ 114 | public function __get($property) 115 | { 116 | switch ($property) { 117 | case 'first': 118 | return $this->first(); 119 | case 'last': 120 | return $this->last(); 121 | case 'predecessor': 122 | return $this->predecessor(); 123 | case 'successor': 124 | return $this->successor(); 125 | case 'key': 126 | return $this->key; 127 | case 'count': 128 | return $this->count(); 129 | default: 130 | throw new \RuntimeException('Undefined property'); 131 | } 132 | } 133 | 134 | /** 135 | * Get the first node 136 | * 137 | * @return the first node 138 | * 139 | * @since 1.0.0 140 | */ 141 | public function first() 142 | { 143 | $node = $this; 144 | 145 | while ($node->information & 2) { 146 | $node = $node->left; 147 | } 148 | 149 | return $node; 150 | } 151 | 152 | /** 153 | * Get the last node 154 | * 155 | * @return the last node 156 | * 157 | * @since 1.0.0 158 | */ 159 | public function last() 160 | { 161 | $node = $this; 162 | 163 | while ($node->information & 1) { 164 | $node = $node->right; 165 | } 166 | 167 | return $node; 168 | } 169 | 170 | /** 171 | * Get the predecessor 172 | * 173 | * @return the predecessor node 174 | * 175 | * @since 1.0.0 176 | */ 177 | public function predecessor() 178 | { 179 | if ($this->information & 2) { 180 | $node = $this->left; 181 | 182 | while ($node->information & 1) { 183 | $node = $node->right; 184 | } 185 | 186 | return $node; 187 | } else { 188 | return $this->left; 189 | } 190 | } 191 | 192 | /** 193 | * Get the successor 194 | * 195 | * @return the successor node 196 | * 197 | * @since 1.0.0 198 | */ 199 | public function successor() 200 | { 201 | if ($this->information & 1) { 202 | $node = $this->right; 203 | 204 | while ($node->information & 2) { 205 | $node = $node->left; 206 | } 207 | 208 | return $node; 209 | } else { 210 | return $this->right; 211 | } 212 | } 213 | 214 | /** 215 | * Count the number of key/value pair 216 | * 217 | * @return integer 218 | * 219 | * @since 1.0.0 220 | */ 221 | public function count(): int 222 | { 223 | $count = 1; 224 | 225 | if ($this->information & 2) { 226 | $count += $this->left->count; 227 | } 228 | 229 | if ($this->information & 1) { 230 | $count += $this->right->count; 231 | } 232 | 233 | return $count; 234 | } 235 | 236 | /** 237 | * Get the node for a key 238 | * 239 | * @param mixed $key The key 240 | * @param callable $comparator The comparator function 241 | * @param integer $type The operation type 242 | * -2 for the 243 | * greatest key 244 | * lesser than the 245 | * given key -1 for 246 | * the greatest key 247 | * lesser than or 248 | * equal to the given 249 | * key 0 for the 250 | * given key +1 for 251 | * the lowest key 252 | * greater than or 253 | * equal to the given 254 | * key +2 for the 255 | * lowest key greater 256 | * than the given key 257 | * 258 | * @return mixed The node or null if not found 259 | * 260 | * @since 1.0.0 261 | */ 262 | public function find($key, $comparator, $type = 0) 263 | { 264 | $node = $this; 265 | 266 | while (true) { 267 | $cmp = call_user_func($comparator, $key, $node->key); 268 | 269 | if ($cmp < 0 && $node->information & 2) { 270 | $node = $node->left; 271 | } elseif ($cmp > 0 && $node->information & 1) { 272 | $node = $node->right; 273 | } else { 274 | break; 275 | } 276 | } 277 | 278 | if ($cmp < 0) { 279 | if ($type < 0) { 280 | return $node->left; 281 | } elseif ($type > 0) { 282 | return $node; 283 | } else { 284 | return null; 285 | } 286 | } elseif ($cmp > 0) { 287 | if ($type < 0) { 288 | return $node; 289 | } elseif ($type > 0) { 290 | return $node->right; 291 | } else { 292 | return null; 293 | } 294 | } else { 295 | if ($type < -1) { 296 | return $node->predecessor; 297 | } elseif ($type > 1) { 298 | return $node->successor; 299 | } else { 300 | return $node; 301 | } 302 | } 303 | } 304 | 305 | /** 306 | * Rotate the node to the left 307 | * 308 | * @return TreeNode The rotated node 309 | * 310 | * @since 1.0.0 311 | */ 312 | private function rotateLeft() 313 | { 314 | $right = $this->right; 315 | 316 | if ($right->information & 2) { 317 | $this->right = $right->left; 318 | $right->left = $this; 319 | } else { 320 | $right->information |= 2; 321 | $this->information &= ~ 1; 322 | } 323 | 324 | $this->information -= 4; 325 | 326 | if ($right->information >= 4) { 327 | $this->information -= $right->information & ~3; 328 | } 329 | 330 | $right->information -= 4; 331 | 332 | if ($this->information < 0) { 333 | $right->information += $this->information & ~3; 334 | } 335 | 336 | return $right; 337 | } 338 | 339 | /** 340 | * Rotate the node to the right 341 | * 342 | * @return TreeNode The rotated node 343 | * 344 | * @since 1.0.0 345 | */ 346 | private function rotateRight() 347 | { 348 | $left = $this->left; 349 | 350 | if ($left->information & 1) { 351 | $this->left = $left->right; 352 | $left->right = $this; 353 | } else { 354 | $this->information &= ~ 2; 355 | $left->information |= 1; 356 | } 357 | 358 | $this->information += 4; 359 | 360 | if ($left->information < 0) { 361 | $this->information -= $left->information & ~3; 362 | } 363 | 364 | $left->information += 4; 365 | 366 | if ($this->information >= 4) { 367 | $left->information += $this->information & ~3; 368 | } 369 | 370 | return $left; 371 | } 372 | 373 | /** 374 | * Increment the balance of the node 375 | * 376 | * @return TreeNode $this or a rotated version of $this 377 | * 378 | * @since 1.0.0 379 | */ 380 | private function incBalance() 381 | { 382 | $this->information += 4; 383 | 384 | if ($this->information >= 8) { 385 | if ($this->right->information < 0) { 386 | $this->right = $this->right->rotateRight(); 387 | } 388 | 389 | return $this->rotateLeft(); 390 | } 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * Decrement the balance of the node 397 | * 398 | * @return TreeNode $this or a rotated version of $this 399 | * 400 | * @since 1.0.0 401 | */ 402 | private function decBalance() 403 | { 404 | $this->information -= 4; 405 | 406 | if ($this->information < - 4) { 407 | if ($this->left->information >= 4) { 408 | $this->left = $this->left->rotateLeft(); 409 | } 410 | 411 | return $this->rotateRight(); 412 | } 413 | 414 | return $this; 415 | } 416 | 417 | /** 418 | * Insert a key/value pair 419 | * 420 | * @param mixed $key The key 421 | * @param mixed $value The value 422 | * @param callable $comparator The comparator function 423 | * 424 | * @return TreeNode The new root 425 | * 426 | * @since 1.0.0 427 | */ 428 | public function insert($key, $value, $comparator) 429 | { 430 | $node = $this; 431 | $cmp = call_user_func($comparator, $key, $this->key); 432 | 433 | if ($cmp < 0) { 434 | if ($this->information & 2) { 435 | $leftBalance = $this->left->information & ~3; 436 | $this->left = $this->left->insert($key, $value, $comparator); 437 | 438 | if (($this->left->information & ~3) && ($this->left->information & ~3) != $leftBalance) { 439 | $node = $this->decBalance(); 440 | } 441 | } else { 442 | $this->left = new static($key, $value, $this->left, $this); 443 | $this->information |= 2; 444 | $node = $this->decBalance(); 445 | } 446 | } elseif ($cmp > 0) { 447 | if ($this->information & 1) { 448 | $rightBalance = $this->right->information & ~3; 449 | $this->right = $this->right->insert($key, $value, $comparator); 450 | 451 | if (($this->right->information & ~3) && ($this->right->information & ~3) != $rightBalance) { 452 | $node = $this->incBalance(); 453 | } 454 | } else { 455 | $this->right = new static($key, $value, $this, $this->right); 456 | $this->information |= 1; 457 | $node = $this->incBalance(); 458 | } 459 | } else { 460 | $this->value = $value; 461 | } 462 | 463 | return $node; 464 | } 465 | 466 | /** 467 | * Pull up the left most node of a node 468 | * 469 | * @return TreeNode The new root 470 | * 471 | * @since 1.0.0 472 | */ 473 | private function pullUpLeftMost() 474 | { 475 | if ($this->information & 2) { 476 | $leftBalance = $this->left->information & ~3; 477 | $this->left = $this->left->pullUpLeftMost(); 478 | 479 | if (!($this->information & 2) || $leftBalance != 0 && ($this->left->information & ~3) == 0) { 480 | return $this->incBalance(); 481 | } else { 482 | return $this; 483 | } 484 | } else { 485 | $this->left->key = $this->key; 486 | $this->left->value = $this->value; 487 | 488 | if ($this->information & 1) { 489 | $this->right->left = $this->left; 490 | 491 | return $this->right; 492 | } else { 493 | if ($this->left->right == $this) { 494 | $this->left->information &= ~ 1; 495 | 496 | return $this->right; 497 | } else { 498 | $this->right->information &= ~ 2; 499 | 500 | return $this->left; 501 | } 502 | } 503 | } 504 | } 505 | 506 | /** 507 | * Remove a key 508 | * 509 | * @param mixed $key The key 510 | * @param callable $comparator The comparator function 511 | * 512 | * @return TreeNode The new root 513 | * 514 | * @since 1.0.0 515 | */ 516 | public function remove($key, $comparator) 517 | { 518 | $cmp = call_user_func($comparator, $key, $this->key); 519 | 520 | if ($cmp < 0) { 521 | if ($this->information & 2) { 522 | $leftBalance = $this->left->information & ~3; 523 | $this->left = $this->left->remove($key, $comparator); 524 | 525 | if (!($this->information & 2) || $leftBalance != 0 && ($this->left->information & ~3) == 0) { 526 | return $this->incBalance(); 527 | } 528 | } 529 | } elseif ($cmp > 0) { 530 | if ($this->information & 1) { 531 | $rightBalance = $this->right->information & ~3; 532 | $this->right = $this->right->remove($key, $comparator); 533 | 534 | if (!($this->information & 1) || $rightBalance != 0 && ($this->right->information & ~3) == 0) { 535 | return $this->decBalance(); 536 | } 537 | } 538 | } else { 539 | if ($this->information & 1) { 540 | $rightBalance = $this->right->information & ~3; 541 | $this->right = $this->right->pullUpLeftMost(); 542 | 543 | if (!($this->information & 1) || $rightBalance != 0 && ($this->right->information & ~3) == 0) { 544 | return $this->decBalance(); 545 | } 546 | } else { 547 | $left = $this->left; 548 | $right = $this->right; 549 | 550 | if ($this->information & 2) { 551 | $left->right = $right; 552 | 553 | return $left; 554 | } else { 555 | if ($left && $left->right == $this) { 556 | $left->information &= ~ 1; 557 | 558 | return $right; 559 | } elseif ($right && $right->left == $this) { 560 | $right->information &= ~ 2; 561 | 562 | return $left; 563 | } else { 564 | return null; 565 | } 566 | } 567 | } 568 | } 569 | 570 | return $this; 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /src/SortedCollection/TreeSet.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | /** 18 | * Tree set 19 | * 20 | * @package SortedCollection 21 | * @subpackage Set 22 | * 23 | * @since 1.0.0 24 | * 25 | * @property-read callable $comparator The element comparison function 26 | * @property-read mixed $first The first element of the set 27 | * @property-read mixed $last The last element of the set 28 | * @property-read integer $count The number of elements in the set 29 | */ 30 | class TreeSet extends AbstractSet 31 | { 32 | /** 33 | * Constructor 34 | * 35 | * @param callable $comparator Comparison function 36 | * 37 | * @since 1.0.0 38 | */ 39 | protected function __construct($comparator = null) 40 | { 41 | $this->setMap(TreeMap::create($comparator)); 42 | } 43 | 44 | /** 45 | * Create 46 | * 47 | * @param callable $comparator Comparison function 48 | * 49 | * @return TreeSet A new TreeSet 50 | * 51 | * @since 1.0.0 52 | */ 53 | public static function create($comparator = null) 54 | { 55 | return new static($comparator); 56 | } 57 | 58 | /** 59 | * Put values in the set 60 | * 61 | * @param \Traversable $traversable Values to put in the set 62 | * 63 | * @return TreeSet $this for chaining 64 | * 65 | * @since 1.0.0 66 | */ 67 | public function put($traversable = array()) 68 | { 69 | foreach ($traversable as $value) { 70 | $this[$value] = true; 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Clear the set 78 | * 79 | * @return TreeSet $this for chaining 80 | * 81 | * @since 1.0.0 82 | */ 83 | public function clear() 84 | { 85 | $this->getMap()->clear(); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Initialise the set 92 | * 93 | * @param \Traversable $traversable Values to initialise the set 94 | * 95 | * @return TreeSet $this for chaining 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function initialise($traversable = array()) 100 | { 101 | return $this->clear()->put($traversable); 102 | } 103 | 104 | /** 105 | * Clone the set 106 | * 107 | * @return void 108 | * 109 | * @since 1.0.0 110 | */ 111 | public function __clone() 112 | { 113 | $this->setMap(clone $this->getMap()); 114 | } 115 | 116 | /** 117 | * Set the value for an element 118 | * 119 | * @param mixed $element The element 120 | * @param mixed $value The value 121 | * 122 | * @return void 123 | * 124 | * @since 1.0.0 125 | */ 126 | public function offsetSet($element, $value): void 127 | { 128 | $map = $this->getMap(); 129 | 130 | if ($value) { 131 | $map[$element] = true; 132 | } else { 133 | unset($map[$element]); 134 | } 135 | } 136 | 137 | /** 138 | * Serialize the object 139 | * 140 | * @return array Array of values 141 | * 142 | * @since 1.0.0 143 | */ 144 | public function jsonSerialize(): array 145 | { 146 | $array = array(); 147 | 148 | foreach ($this as $value) { 149 | $array[] = $value; 150 | } 151 | 152 | return array('TreeSet' => $array); 153 | } 154 | 155 | /** 156 | * Unset the existence of an element 157 | * 158 | * @param mixed $element The element 159 | * 160 | * @return void 161 | * 162 | * @since 1.0.0 163 | */ 164 | public function offsetUnset($element): void 165 | { 166 | $map = $this->getMap(); 167 | unset($map[$element]); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/SortedCollection/ReversedMapTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * ReversedMap class test 22 | * 23 | * @package SortedCollection 24 | * @subpackage Map 25 | * 26 | * @since 1.0.0 27 | */ 28 | class ReversedMapTest extends TestCase 29 | { 30 | /** 31 | * Tests ReversedMap::__construct 32 | * 33 | * @return void 34 | * 35 | * @since 1.0.0 36 | */ 37 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::__construct')] 38 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::create')] 39 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::__get')] 40 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::comparator')] 41 | public function testConstruct() 42 | { 43 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 44 | $reversed = ReversedMap::create($tree); 45 | 46 | $this->assertEquals( 47 | $tree, 48 | $reversed->map 49 | ); 50 | 51 | $this->assertEquals( 52 | -call_user_func($tree->comparator, 4, 6), 53 | call_user_func($reversed->comparator, 4, 6) 54 | ); 55 | 56 | $doubleReversed = ReversedMap::create($reversed); 57 | 58 | $this->assertEquals( 59 | $tree, 60 | $doubleReversed->map->map 61 | ); 62 | 63 | $this->assertEquals( 64 | call_user_func($tree->comparator, 4, 6), 65 | call_user_func($doubleReversed->comparator, 4, 6) 66 | ); 67 | } 68 | 69 | /** 70 | * Tests ReversedMap::first 71 | * 72 | * @return void 73 | * 74 | * @since 1.0.0 75 | */ 76 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::first')] 77 | public function testFirst() 78 | { 79 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 80 | $reversed = ReversedMap::create($tree); 81 | 82 | $this->assertEquals( 83 | 9, 84 | $reversed->first->key 85 | ); 86 | } 87 | 88 | /** 89 | * Tests ReversedMap::last 90 | * 91 | * @return void 92 | * 93 | * @since 1.0.0 94 | */ 95 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::last')] 96 | public function testLast() 97 | { 98 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 99 | $reversed = ReversedMap::create($tree); 100 | 101 | $this->assertEquals( 102 | 0, 103 | $reversed->last->key 104 | ); 105 | } 106 | 107 | /** 108 | * Tests ReversedMap::predecessor 109 | * 110 | * @return void 111 | * 112 | * @since 1.0.0 113 | */ 114 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::predecessor')] 115 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::predecessor')] 116 | public function testPredecessor() 117 | { 118 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 119 | $reversed = ReversedMap::create($tree); 120 | $this->assertEquals( 121 | 1, 122 | $reversed->predecessor($reversed->last)->key 123 | ); 124 | $this->expectException('OutOfBoundsException'); 125 | $predecessor = $reversed->predecessor($reversed->first); 126 | } 127 | 128 | /** 129 | * Tests ReversedMap::successor 130 | * 131 | * @return void 132 | * 133 | * @since 1.0.0 134 | */ 135 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::successor')] 136 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::successor')] 137 | public function testSuccessor() 138 | { 139 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 140 | $reversed = ReversedMap::create($tree); 141 | $this->assertEquals( 142 | 8, 143 | $reversed->successor($reversed->first)->key 144 | ); 145 | $this->expectException('OutOfBoundsException'); 146 | $successor = $reversed->successor($reversed->last); 147 | } 148 | 149 | /** 150 | * Data provider for testLowerKey 151 | * 152 | * @return array 153 | * 154 | * @since 1.0.0 155 | */ 156 | public static function casesLowerKey() 157 | { 158 | return array( 159 | array(array(), 10, null, 'OutOfBoundsException'), 160 | array(array(1 => 1), 1, null, 'OutOfBoundsException'), 161 | array(array(1 => 1, 0 => 0), 0, 1, null), 162 | array(array(2 => 2, 1 => 1), 0, 1, null), 163 | array(array(0 => 0, 1 => 1), 1, null, 'OutOfBoundsException'), 164 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfBoundsException'), 165 | ); 166 | } 167 | 168 | /** 169 | * Tests ReversedMap::lower 170 | * 171 | * @param array $values Values array 172 | * @param mixed $key Key to search for 173 | * @param mixed $expected Expected key 174 | * @param mixed $exception Exception to be thrown 175 | * 176 | * @return void 177 | * 178 | * @since 1.0.0 179 | */ 180 | #[DataProvider('casesLowerKey')] 181 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::lower')] 182 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::lowerKey')] 183 | public function testLowerKey($values, $key, $expected, $exception) 184 | { 185 | if ($exception) { 186 | $this->expectException($exception); 187 | } 188 | 189 | $tree = TreeMap::create()->initialise($values); 190 | $reversed = ReversedMap::create($tree); 191 | 192 | $this->assertEquals( 193 | $expected, 194 | $reversed->lowerKey($key) 195 | ); 196 | } 197 | 198 | /** 199 | * Data provider for testFloorKey 200 | * 201 | * @return array 202 | * 203 | * @since 1.0.0 204 | */ 205 | public static function casesFloorKey() 206 | { 207 | return array( 208 | array(array(), 10, null, 'OutOfBoundsException'), 209 | array(array(1 => 1), 1, 1, null), 210 | array(array(1 => 1, 0 => 0), 0, 0, null), 211 | array(array(2 => 2, 1 => 1), 0, 1, null), 212 | array(array(0 => 0, 1 => 1), 1, 1, null), 213 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfBoundsException'), 214 | ); 215 | } 216 | 217 | /** 218 | * Tests ReversedMap::floor 219 | * 220 | * @param array $values Values array 221 | * @param mixed $key Key to search for 222 | * @param mixed $found Expected key 223 | * @param mixed $exception Exception to be thrown 224 | * 225 | * @return void 226 | * 227 | * @since 1.0.0 228 | */ 229 | #[DataProvider('casesFloorKey')] 230 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::floor')] 231 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::floorKey')] 232 | public function testFloorKey($values, $key, $found, $exception) 233 | { 234 | if ($exception) { 235 | $this->expectException($exception); 236 | } 237 | 238 | $tree = TreeMap::create()->initialise($values); 239 | $reversed = ReversedMap::create($tree); 240 | 241 | $this->assertEquals( 242 | $found, 243 | $reversed->floorKey($key) 244 | ); 245 | } 246 | 247 | /** 248 | * Tests ReversedMap::find 249 | * 250 | * @return void 251 | * 252 | * @since 1.0.0 253 | */ 254 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::find')] 255 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::findKey')] 256 | public function testFindKey() 257 | { 258 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 259 | $reversed = ReversedMap::create($tree); 260 | 261 | $this->assertEquals( 262 | 0, 263 | $reversed->findKey(0) 264 | ); 265 | 266 | $tree->clear(); 267 | $this->expectException('OutOfBoundsException'); 268 | 269 | $key = $reversed->findKey(10); 270 | } 271 | 272 | /** 273 | * Data provider for testCeilingKey 274 | * 275 | * @return array 276 | * 277 | * @since 1.0.0 278 | */ 279 | public static function casesCeilingKey() 280 | { 281 | return array( 282 | array(array(), 10, null, 'OutOfBoundsException'), 283 | array(array(1 => 1), 1, 1, null), 284 | array(array(1 => 1, 0 => 0), 0, 0, null), 285 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfBoundsException'), 286 | array(array(0 => 0, 1 => 1), 1, 1, null), 287 | array(array(0 => 0, 1 => 1), 2, 1, null), 288 | ); 289 | } 290 | 291 | /** 292 | * Tests ReversedMap::ceiling 293 | * 294 | * @param array $values Values array 295 | * @param mixed $key Key to search for 296 | * @param mixed $expected Expected key 297 | * @param mixed $exception Exception to be thrown 298 | * 299 | * @return void 300 | * 301 | * @since 1.0.0 302 | */ 303 | #[DataProvider('casesCeilingKey')] 304 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::ceiling')] 305 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::floorKey')] 306 | public function testCeilingKey($values, $key, $expected, $exception) 307 | { 308 | if ($exception) { 309 | $this->expectException($exception); 310 | } 311 | 312 | $tree = TreeMap::create()->initialise($values); 313 | $reversed = ReversedMap::create($tree); 314 | 315 | $this->assertEquals( 316 | $expected, 317 | $reversed->ceilingKey($key) 318 | ); 319 | } 320 | 321 | /** 322 | * Data provider for testHigherKey 323 | * 324 | * @return array 325 | * 326 | * @since 1.0.0 327 | */ 328 | public static function casesHigherKey() 329 | { 330 | return array( 331 | array(array(), 10, null, 'OutOfBoundsException'), 332 | array(array(1 => 1), 1, null, 'OutOfBoundsException'), 333 | array(array(1 => 1, 0 => 0), 0, null, 'OutOfBoundsException'), 334 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfBoundsException'), 335 | array(array(0 => 0, 1 => 1), 1, 0, null), 336 | array(array(0 => 0, 1 => 1), 2, 1, null), 337 | ); 338 | } 339 | 340 | /** 341 | * Tests ReversedMap::higher 342 | * 343 | * @param array $values Values array 344 | * @param mixed $key Key to search for 345 | * @param mixed $expected Expected key 346 | * @param mixed $exception Exception to be thrown 347 | * 348 | * @return void 349 | * 350 | * @since 1.0.0 351 | */ 352 | #[DataProvider('casesHigherKey')] 353 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::higher')] 354 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::higherKey')] 355 | public function testHigherKey($values, $key, $expected, $exception) 356 | { 357 | if ($exception) { 358 | $this->expectException($exception); 359 | } 360 | 361 | $tree = TreeMap::create()->initialise($values); 362 | $reversed = ReversedMap::create($tree); 363 | 364 | $this->assertEquals( 365 | $expected, 366 | $reversed->higherKey($key) 367 | ); 368 | } 369 | 370 | /** 371 | * Tests ReversedMap::keys 372 | * 373 | * @return void 374 | * 375 | * @since 1.0.0 376 | */ 377 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::keys')] 378 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 379 | public function testKeys() 380 | { 381 | $empty = true; 382 | $tree = TreeMap::create(); 383 | $reversed = ReversedMap::create($tree); 384 | 385 | foreach ($reversed->keys as $key) { 386 | $empty = false; 387 | } 388 | 389 | $this->assertEquals(true, $empty); 390 | 391 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 392 | $reversed = ReversedMap::create($tree); 393 | $i = 9; 394 | 395 | foreach ($reversed->keys as $key) { 396 | $this->assertEquals($i, $key); 397 | $i--; 398 | } 399 | } 400 | 401 | /** 402 | * Tests AbstractMap::values 403 | * 404 | * @return void 405 | * 406 | * @since 1.0.0 407 | */ 408 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::values')] 409 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 410 | public function testValues() 411 | { 412 | $empty = true; 413 | $tree = TreeMap::create(); 414 | $reversed = ReversedMap::create($tree); 415 | 416 | foreach ($reversed->values as $value) { 417 | $empty = false; 418 | } 419 | 420 | $this->assertEquals(true, $empty); 421 | 422 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 423 | $reversed = ReversedMap::create($tree); 424 | $i = 9; 425 | 426 | foreach ($reversed->values as $value) { 427 | $this->assertEquals($i, $value); 428 | $i--; 429 | } 430 | } 431 | 432 | /** 433 | * Data provider for testOffsetGet 434 | * 435 | * @return array 436 | * 437 | * @since 1.0.0 438 | */ 439 | public static function casesOffsetGet() 440 | { 441 | return array( 442 | array(array(), 10, null, 'OutOfRangeException'), 443 | array(array(1 => 1), 1, 1, null), 444 | array(array(1 => 1, 0 => 0), 0, 0, null), 445 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfRangeException'), 446 | array(array(0 => 0, 1 => 1), 1, 1, null), 447 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfRangeException'), 448 | ); 449 | } 450 | 451 | /** 452 | * Tests AbstractMap::offsetGet 453 | * 454 | * @param array $values Values array 455 | * @param mixed $key Key to search for 456 | * @param mixed $value Expected value 457 | * @param mixed $exception Exception to be thrown 458 | * 459 | * @return void 460 | * 461 | * @since 1.0.0 462 | */ 463 | #[DataProvider('casesOffsetGet')] 464 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::offsetGet')] 465 | public function testOffsetGet($values, $key, $value, $exception) 466 | { 467 | if ($exception) { 468 | $this->expectException($exception); 469 | } 470 | 471 | $tree = TreeMap::create()->initialise($values); 472 | $reversed = ReversedMap::create($tree); 473 | 474 | $this->assertEquals( 475 | $value, 476 | $reversed[$key] 477 | ); 478 | } 479 | 480 | /** 481 | * Tests AbstractMap::offsetSet 482 | * 483 | * @return void 484 | * 485 | * @since 1.0.0 486 | */ 487 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::offsetSet')] 488 | public function testOffsetSet() 489 | { 490 | $this->expectException('RuntimeException'); 491 | 492 | $tree = TreeMap::create(); 493 | $reversed = ReversedMap::create($tree); 494 | $reversed[0] = 0; 495 | } 496 | 497 | /** 498 | * Tests ReversedMap::offsetExists 499 | * 500 | * @return void 501 | * 502 | * @since 1.0.0 503 | */ 504 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::offsetExists')] 505 | public function testOffsetExists() 506 | { 507 | $tree = TreeMap::create(); 508 | $reversed = ReversedMap::create($tree); 509 | 510 | $this->assertFalse( 511 | isset($reversed[10]) 512 | ); 513 | 514 | $tree->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 515 | 516 | $this->assertTrue( 517 | isset($reversed[5]) 518 | ); 519 | $this->assertFalse( 520 | isset($reversed[10]) 521 | ); 522 | } 523 | 524 | /** 525 | * Tests AbstractMap::offsetUnset 526 | * 527 | * @return void 528 | * 529 | * @since 1.0.0 530 | */ 531 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::offsetUnset')] 532 | public function testOffsetUnset() 533 | { 534 | $this->expectException('RuntimeException'); 535 | 536 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 537 | $reversed = ReversedMap::create($tree); 538 | unset($reversed[0]); 539 | } 540 | 541 | /** 542 | * Tests ReversedMap::count 543 | * 544 | * @return void 545 | * 546 | * @since 1.0.0 547 | */ 548 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::count')] 549 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 550 | public function testCount() 551 | { 552 | $tree = TreeMap::create(); 553 | $reversed = ReversedMap::create($tree); 554 | 555 | $this->assertEquals( 556 | 0, 557 | $reversed->count 558 | ); 559 | 560 | $tree->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 561 | 562 | $this->assertEquals( 563 | 10, 564 | count($reversed) 565 | ); 566 | } 567 | 568 | /** 569 | * Tests ReversedMap::jsonSerialize 570 | * 571 | * @return void 572 | * 573 | * @since 1.0.0 574 | */ 575 | #[CoversFunction('chdemko\SortedCollection\ReversedMap::jsonSerialize')] 576 | public function testJsonSerialize() 577 | { 578 | $tree = TreeMap::create(); 579 | $reversed = ReversedMap::create($tree); 580 | 581 | $this->assertEquals( 582 | '{"ReversedMap":{"TreeMap":[]}}', 583 | json_encode($reversed) 584 | ); 585 | 586 | $tree->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 587 | 588 | $this->assertEquals( 589 | '{"ReversedMap":{"TreeMap":[0,1,2,3,4,5,6,7,8,9]}}', 590 | json_encode($reversed) 591 | ); 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /tests/SortedCollection/ReversedSetTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * ReversedSet class test 22 | * 23 | * @package SortedCollection 24 | * @subpackage Set 25 | * 26 | * @since 1.0.0 27 | */ 28 | class ReversedSetTest extends TestCase 29 | { 30 | /** 31 | * Data provider for testCreate 32 | * 33 | * @return array 34 | * 35 | * @since 1.0.0 36 | */ 37 | public static function casesCreate() 38 | { 39 | return array( 40 | array(array(), null, null, '[]'), 41 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 3, null, '[9,8,7,6,5,4,3,2,1,0]'), 42 | array( 43 | array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 44 | 3, 45 | function ($key1, $key2) { 46 | return $key1 - $key2; 47 | }, 48 | '[9,8,7,6,5,4,3,2,1,0]' 49 | ) 50 | ); 51 | } 52 | 53 | /** 54 | * Tests ReversedSet::create 55 | * 56 | * @param array $values Initial values 57 | * @param mixed $key Expected root key 58 | * @param callable $comparator Comparator 59 | * @param callable $string String representation 60 | * 61 | * @return void 62 | * 63 | * @since 1.0.0 64 | */ 65 | #[DataProvider('casesCreate')] 66 | #[CoversFunction('chdemko\SortedCollection\ReversedSet::__construct')] 67 | #[CoversFunction('chdemko\SortedCollection\ReversedSet::create')] 68 | public function testCreate($values, $key, $comparator, $string) 69 | { 70 | $set = TreeSet::create($comparator)->initialise($values); 71 | $reversed = ReversedSet::create($set); 72 | $this->assertEquals( 73 | $string, 74 | (string) $reversed 75 | ); 76 | } 77 | 78 | /** 79 | * Tests ReversedSet::__get 80 | * 81 | * @return void 82 | * 83 | * @since 1.0.0 84 | */ 85 | #[CoversFunction('chdemko\SortedCollection\ReversedSet::__get')] 86 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 87 | public function testGet() 88 | { 89 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 90 | $reversed = ReversedSet::create($set); 91 | $this->assertEquals( 92 | $set, 93 | $reversed->set 94 | ); 95 | $this->assertEquals( 96 | 9, 97 | $reversed->first 98 | ); 99 | } 100 | 101 | /** 102 | * Tests ReversedSet::offsetSet 103 | * 104 | * @return void 105 | * 106 | * @since 1.0.0 107 | */ 108 | #[CoversFunction('chdemko\SortedCollection\ReversedSet::offsetSet')] 109 | public function testOffsetSet() 110 | { 111 | $this->expectException('RuntimeException'); 112 | 113 | $set = TreeSet::create(); 114 | $reversed = ReversedSet::create($set); 115 | $reversed[0] = 0; 116 | } 117 | 118 | /** 119 | * Tests AbstractSet::offsetUnset 120 | * 121 | * @return void 122 | * 123 | * @since 1.0.0 124 | */ 125 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::offsetUnset')] 126 | public function testOffsetUnset() 127 | { 128 | $this->expectException('RuntimeException'); 129 | 130 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 131 | $reversed = ReversedSet::create($set); 132 | unset($reversed[0]); 133 | } 134 | 135 | /** 136 | * Tests ReversedSet::jsonSerialize 137 | * 138 | * @return void 139 | * 140 | * @since 1.0.0 141 | */ 142 | #[CoversFunction('chdemko\SortedCollection\ReversedSet::jsonSerialize')] 143 | public function testJsonSerialize() 144 | { 145 | $set = TreeSet::create(); 146 | $reversed = ReversedSet::create($set); 147 | 148 | $this->assertEquals( 149 | '{"ReversedSet":{"TreeSet":[]}}', 150 | json_encode($reversed) 151 | ); 152 | 153 | $set->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 154 | 155 | $this->assertEquals( 156 | '{"ReversedSet":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]}}', 157 | json_encode($reversed) 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/SortedCollection/SubSetTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * SubSet class test 22 | * 23 | * @package SortedCollection 24 | * @subpackage Set 25 | * 26 | * @since 1.0.0 27 | */ 28 | class SubSetTest extends TestCase 29 | { 30 | /** 31 | * Data provider for testCreate 32 | * 33 | * @return array 34 | * 35 | * @since 1.0.0 36 | */ 37 | public static function casesCreate() 38 | { 39 | return array( 40 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, 7, true, false, '[2,3,4,5,6]'), 41 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, 7, true, true, '[2,3,4,5,6,7]'), 42 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, 7, false, false, '[3,4,5,6]'), 43 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, 7, false, true, '[3,4,5,6,7]'), 44 | ); 45 | } 46 | 47 | /** 48 | * Tests SubSet::create 49 | * 50 | * @param array $values Initial values 51 | * @param mixed $from From element 52 | * @param mixed $to To element 53 | * @param boolean $fromInclusive From inclusive flag 54 | * @param boolean $toInclusive To inclusive flag 55 | * @param callable $string String representation 56 | * 57 | * @return void 58 | * 59 | * @since 1.0.0 60 | */ 61 | #[DataProvider('casesCreate')] 62 | #[CoversFunction('chdemko\SortedCollection\SubSet::__construct')] 63 | #[CoversFunction('chdemko\SortedCollection\SubSet::create')] 64 | public function testCreate($values, $from, $to, $fromInclusive, $toInclusive, $string) 65 | { 66 | $set = TreeSet::create()->initialise($values); 67 | $sub = SubSet::create($set, $from, $to, $fromInclusive, $toInclusive); 68 | $this->assertEquals( 69 | $string, 70 | (string) $sub 71 | ); 72 | } 73 | 74 | /** 75 | * Data provider for testHead 76 | * 77 | * @return array 78 | * 79 | * @since 1.0.0 80 | */ 81 | public static function casesHead() 82 | { 83 | return array( 84 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 7, false, '[0,1,2,3,4,5,6]'), 85 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 7, true, '[0,1,2,3,4,5,6,7]'), 86 | ); 87 | } 88 | 89 | /** 90 | * Tests SubSet::head 91 | * 92 | * @param array $values Initial values 93 | * @param mixed $to To element 94 | * @param boolean $toInclusive To inclusive flag 95 | * @param callable $string String representation 96 | * 97 | * @return void 98 | * 99 | * @since 1.0.0 100 | */ 101 | #[DataProvider('casesHead')] 102 | #[CoversFunction('chdemko\SortedCollection\SubSet::__construct')] 103 | #[CoversFunction('chdemko\SortedCollection\SubSet::head')] 104 | public function testHead($values, $to, $toInclusive, $string) 105 | { 106 | $set = TreeSet::create()->initialise($values); 107 | $head = SubSet::head($set, $to, $toInclusive); 108 | $this->assertEquals( 109 | $string, 110 | (string) $head 111 | ); 112 | } 113 | 114 | /** 115 | * Data provider for testTail 116 | * 117 | * @return array 118 | * 119 | * @since 1.0.0 120 | */ 121 | public static function casesTail() 122 | { 123 | return array( 124 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, false, '[3,4,5,6,7,8,9]'), 125 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 2, true, '[2,3,4,5,6,7,8,9]'), 126 | ); 127 | } 128 | 129 | /** 130 | * Tests SubSet::tail 131 | * 132 | * @param array $values Initial values 133 | * @param mixed $from To element 134 | * @param boolean $fromInclusive To inclusive flag 135 | * @param callable $string String representation 136 | * 137 | * @return void 138 | * 139 | * @since 1.0.0 140 | */ 141 | #[DataProvider('casesTail')] 142 | #[CoversFunction('chdemko\SortedCollection\SubSet::__construct')] 143 | #[CoversFunction('chdemko\SortedCollection\SubSet::tail')] 144 | public function testTail($values, $from, $fromInclusive, $string) 145 | { 146 | $set = TreeSet::create()->initialise($values); 147 | $tail = SubSet::tail($set, $from, $fromInclusive); 148 | $this->assertEquals( 149 | $string, 150 | (string) $tail 151 | ); 152 | } 153 | 154 | /** 155 | * Tests SubSet::view 156 | * 157 | * @return void 158 | * 159 | * @since 1.0.0 160 | */ 161 | #[CoversFunction('chdemko\SortedCollection\SubSet::__construct')] 162 | #[CoversFunction('chdemko\SortedCollection\SubSet::view')] 163 | public function testView() 164 | { 165 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 166 | $view = SubSet::view($set); 167 | $this->assertEquals( 168 | (string) $set, 169 | (string) $view 170 | ); 171 | } 172 | 173 | /** 174 | * Tests SubSet::__get 175 | * 176 | * @return void 177 | * 178 | * @since 1.0.0 179 | */ 180 | #[CoversFunction('chdemko\SortedCollection\SubSet::__get')] 181 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 182 | public function testGet() 183 | { 184 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 185 | $sub = SubSet::create($set, 2, 7); 186 | $this->assertEquals( 187 | $set, 188 | $sub->set 189 | ); 190 | $this->assertEquals( 191 | 2, 192 | $sub->first 193 | ); 194 | $this->assertEquals( 195 | 2, 196 | $sub->from 197 | ); 198 | $this->assertEquals( 199 | 7, 200 | $sub->to 201 | ); 202 | $this->assertEquals( 203 | true, 204 | $sub->fromInclusive 205 | ); 206 | $this->assertEquals( 207 | false, 208 | $sub->toInclusive 209 | ); 210 | } 211 | 212 | /** 213 | * Tests SubSet::__set 214 | * 215 | * @return void 216 | * 217 | * @since 1.0.0 218 | */ 219 | #[CoversFunction('chdemko\SortedCollection\SubSet::__set')] 220 | public function testSet() 221 | { 222 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 223 | $sub = SubSet::create($set, 2, 7); 224 | $sub->from = 3; 225 | $sub->fromInclusive = false; 226 | $sub->to = 6; 227 | $sub->toInclusive = true; 228 | $this->assertEquals( 229 | 3, 230 | $sub->from 231 | ); 232 | $this->assertEquals( 233 | 6, 234 | $sub->to 235 | ); 236 | $this->assertEquals( 237 | false, 238 | $sub->fromInclusive 239 | ); 240 | $this->assertEquals( 241 | true, 242 | $sub->toInclusive 243 | ); 244 | 245 | $this->expectException('RuntimeException'); 246 | $sub->unexisting = true; 247 | } 248 | 249 | /** 250 | * Generates a sub set 251 | * 252 | * @param mixed $from The from element 253 | * @param mixed $to The to element 254 | * @param mixed $fromInclusive The from inclusive flag 255 | * @param mixed $toInclusive The to inclusive flag 256 | * 257 | * @return SubMap A sub map 258 | * 259 | * @since 1.0.0 260 | */ 261 | protected function createSub($from, $to, $fromInclusive, $toInclusive) 262 | { 263 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 264 | 265 | if ($fromInclusive === null) { 266 | if ($toInclusive === null) { 267 | $sub = SubSet::view($set); 268 | } else { 269 | $sub = SubSet::head($set, $to, $toInclusive); 270 | } 271 | } elseif ($toInclusive === null) { 272 | $sub = SubSet::tail($set, $from, $fromInclusive); 273 | } else { 274 | $sub = SubSet::create($set, $from, $to, $fromInclusive, $toInclusive); 275 | } 276 | 277 | return $sub; 278 | } 279 | 280 | /** 281 | * Tests SubSet::__unset and SubSet::__isset 282 | * 283 | * @return void 284 | * 285 | * @since 1.0.0 286 | */ 287 | #[CoversFunction('chdemko\SortedCollection\SubSet::__unset')] 288 | #[CoversFunction('chdemko\SortedCollection\SubSet::__isset')] 289 | public function testUnsetIsset() 290 | { 291 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 292 | $sub = SubSet::create($set, 2, 7); 293 | unset($sub->from); 294 | unset($sub->fromInclusive); 295 | unset($sub->to); 296 | unset($sub->toInclusive); 297 | $this->assertEquals( 298 | false, 299 | isset($sub->from) 300 | ); 301 | $this->assertEquals( 302 | false, 303 | isset($sub->to) 304 | ); 305 | $this->assertEquals( 306 | false, 307 | isset($sub->fromInclusive) 308 | ); 309 | $this->assertEquals( 310 | false, 311 | isset($sub->toInclusive) 312 | ); 313 | $this->assertEquals( 314 | false, 315 | isset($sub->unexisting) 316 | ); 317 | $this->expectException('RuntimeException'); 318 | unset($sub->unexisting); 319 | } 320 | 321 | /** 322 | * Data provider for testJsonSerialize 323 | * 324 | * @return array 325 | * 326 | * @since 1.0.0 327 | */ 328 | public static function casesJsonSerialize() 329 | { 330 | return array( 331 | array( 332 | 2, 333 | 7, 334 | true, 335 | false, 336 | '{"SubSet":{' . 337 | '"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},' . 338 | '"from":2,' . 339 | '"fromInclusive":true,' . 340 | '"to":7,' . 341 | '"toInclusive":false' . 342 | '}}' 343 | ), 344 | array( 345 | 2, 346 | 7, 347 | false, 348 | false, 349 | '{"SubSet":{' . 350 | '"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},' . 351 | '"from":2,' . 352 | '"fromInclusive":false,' . 353 | '"to":7,' . 354 | '"toInclusive":false' . 355 | '}}' 356 | ), 357 | array( 358 | 2, 359 | 7, 360 | false, 361 | true, 362 | '{"SubSet":{' . 363 | '"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},' . 364 | '"from":2,' . 365 | '"fromInclusive":false,' . 366 | '"to":7,' . 367 | '"toInclusive":true' . 368 | '}}' 369 | ), 370 | array( 371 | 2, 372 | 7, 373 | true, 374 | true, 375 | '{"SubSet":{' . 376 | '"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},' . 377 | '"from":2,' . 378 | '"fromInclusive":true,' . 379 | '"to":7,' . 380 | '"toInclusive":true' . 381 | '}}' 382 | ), 383 | array( 384 | 9, 385 | -1, 386 | true, 387 | false, 388 | '{"SubSet":{' . 389 | '"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},' . 390 | '"from":9,' . 391 | '"fromInclusive":true,' . 392 | '"to":-1,' . 393 | '"toInclusive":false' . 394 | '}}' 395 | ), 396 | array( 397 | null, 398 | 7, 399 | null, 400 | true, 401 | '{"HeadSet":{"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},"to":7,"toInclusive":true}}' 402 | ), 403 | array( 404 | 2, 405 | null, 406 | true, 407 | null, 408 | '{"TailSet":{"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]},"from":2,"fromInclusive":true}}' 409 | ), 410 | array( 411 | null, 412 | null, 413 | null, 414 | null, 415 | '{"ViewSet":{"set":{"TreeSet":[0,1,2,3,4,5,6,7,8,9]}}}' 416 | ), 417 | ); 418 | } 419 | 420 | /** 421 | * Tests SubSet::jsonSerialize 422 | * 423 | * @param mixed $fromKey The from key 424 | * @param mixed $toKey The to key 425 | * @param mixed $fromInclusive The from inclusive flag 426 | * @param mixed $toInclusive The to inclusive flag 427 | * @param mixed $string The expected string 428 | * 429 | * @return void 430 | * 431 | * @since 1.0.0 432 | */ 433 | #[DataProvider('casesJsonSerialize')] 434 | #[CoversFunction('chdemko\SortedCollection\SubSet::jsonSerialize')] 435 | public function testJsonSerialize($fromKey, $toKey, $fromInclusive, $toInclusive, $string) 436 | { 437 | $sub = $this->createSub($fromKey, $toKey, $fromInclusive, $toInclusive); 438 | $this->assertEquals( 439 | $string, 440 | json_encode($sub) 441 | ); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /tests/SortedCollection/TreeMapTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * TreeMap class test 22 | * 23 | * @package SortedCollection 24 | * @subpackage Map 25 | * 26 | * @since 1.0.0 27 | */ 28 | class TreeMapTest extends TestCase 29 | { 30 | /** 31 | * Data provider for testCreate 32 | * 33 | * @return array 34 | * 35 | * @since 1.0.0 36 | */ 37 | public static function casesCreate() 38 | { 39 | return array( 40 | array(array(), null, null), 41 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 3, null), 42 | array( 43 | array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 44 | 3, 45 | function ($key1, $key2) { 46 | return $key1 - $key2; 47 | } 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Tests TreeMap::create 54 | * 55 | * @param array $values Initial values 56 | * @param mixed $key Expected root key 57 | * @param callable $comparator Comparator 58 | * 59 | * @return void 60 | * 61 | * @since 1.0.0 62 | */ 63 | #[DataProvider('casesCreate')] 64 | #[CoversFunction('chdemko\SortedCollection\TreeMap::__construct')] 65 | #[CoversFunction('chdemko\SortedCollection\TreeMap::create')] 66 | #[CoversFunction('chdemko\SortedCollection\TreeMap::put')] 67 | #[CoversFunction('chdemko\SortedCollection\TreeMap::initialise')] 68 | public function testCreate($values, $key, $comparator) 69 | { 70 | $tree = TreeMap::create($comparator)->initialise($values); 71 | 72 | if ($comparator !== null) { 73 | $this->assertEquals( 74 | $comparator, 75 | $tree->comparator 76 | ); 77 | } else { 78 | $this->assertTrue( 79 | is_callable($tree->comparator) 80 | ); 81 | } 82 | 83 | // Set the root property accessible 84 | $root = (new \ReflectionClass($tree))->getProperty('root'); 85 | $root->setAccessible(true); 86 | 87 | if ($values) { 88 | $this->assertEquals( 89 | $key, 90 | $root->getValue($tree)->key 91 | ); 92 | } else { 93 | $this->assertEquals( 94 | null, 95 | $root->getValue($tree) 96 | ); 97 | } 98 | } 99 | 100 | /** 101 | * Tests TreeMap::clear 102 | * 103 | * @return void 104 | * 105 | * @since 1.0.0 106 | */ 107 | #[CoversFunction('chdemko\SortedCollection\TreeMap::clear')] 108 | public function testClear() 109 | { 110 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 111 | 112 | // Set the root property accessible 113 | $root = (new \ReflectionClass($tree))->getProperty('root'); 114 | $root->setAccessible(true); 115 | 116 | $this->assertEquals( 117 | $tree, 118 | $tree->clear() 119 | ); 120 | 121 | $this->assertEquals( 122 | null, 123 | $root->getValue($tree) 124 | ); 125 | } 126 | 127 | /** 128 | * Tests TreeMap::__clone 129 | * 130 | * @return void 131 | * 132 | * @since 1.0.0 133 | */ 134 | #[CoversFunction('chdemko\SortedCollection\TreeMap::__clone')] 135 | public function testClone() 136 | { 137 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 138 | $clone = clone $tree; 139 | $tree->clear(); 140 | $this->assertEquals( 141 | 10, 142 | count($clone) 143 | ); 144 | } 145 | 146 | /** 147 | * Tests TreeMap::comparator 148 | * 149 | * @return void 150 | * 151 | * @since 1.0.0 152 | */ 153 | #[CoversFunction('chdemko\SortedCollection\TreeMap::comparator')] 154 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 155 | public function testComparator() 156 | { 157 | $comparator = function ($key1, $key2) { 158 | return $key1 - $key2; 159 | }; 160 | $tree = TreeMap::create($comparator)->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 161 | $this->assertEquals( 162 | $comparator, 163 | $tree->comparator 164 | ); 165 | } 166 | 167 | /** 168 | * Tests TreeMap::first 169 | * 170 | * @return void 171 | * 172 | * @since 1.0.0 173 | */ 174 | #[CoversFunction('chdemko\SortedCollection\TreeMap::first')] 175 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 176 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::firstKey')] 177 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::firstValue')] 178 | public function testFirst() 179 | { 180 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 181 | $this->assertEquals( 182 | 0, 183 | $tree->firstKey 184 | ); 185 | $this->assertEquals( 186 | 0, 187 | $tree->firstValue 188 | ); 189 | $this->assertEquals( 190 | 0, 191 | $tree->first->key 192 | ); 193 | $tree->clear(); 194 | $this->expectException('OutOfBoundsException'); 195 | $key = $tree->firstKey; 196 | } 197 | 198 | /** 199 | * Tests TreeMap::last 200 | * 201 | * @return void 202 | * 203 | * @since 1.0.0 204 | */ 205 | #[CoversFunction('chdemko\SortedCollection\TreeMap::last')] 206 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 207 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::lastKey')] 208 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::lastValue')] 209 | public function testLast() 210 | { 211 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 212 | $this->assertEquals( 213 | 9, 214 | $tree->lastKey 215 | ); 216 | $this->assertEquals( 217 | 9, 218 | $tree->lastValue 219 | ); 220 | $this->assertEquals( 221 | 9, 222 | $tree->last->key 223 | ); 224 | $tree->clear(); 225 | $this->expectException('OutOfBoundsException'); 226 | $key = $tree->lastKey; 227 | } 228 | 229 | /** 230 | * Tests TreeMap::predecessor 231 | * 232 | * @return void 233 | * 234 | * @since 1.0.0 235 | */ 236 | #[CoversFunction('chdemko\SortedCollection\TreeMap::predecessor')] 237 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::predecessor')] 238 | public function testPredecessor() 239 | { 240 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 241 | $this->assertEquals( 242 | 8, 243 | $tree->predecessor($tree->last)->key 244 | ); 245 | $this->expectException('OutOfBoundsException'); 246 | $predecessor = $tree->predecessor($tree->first); 247 | } 248 | 249 | /** 250 | * Tests TreeMap::successor 251 | * 252 | * @return void 253 | * 254 | * @since 1.0.0 255 | */ 256 | #[CoversFunction('chdemko\SortedCollection\TreeMap::successor')] 257 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::successor')] 258 | public function testSuccessor() 259 | { 260 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 261 | $this->assertEquals( 262 | 1, 263 | $tree->successor($tree->first)->key 264 | ); 265 | $this->expectException('OutOfBoundsException'); 266 | $successor = $tree->successor($tree->last); 267 | } 268 | 269 | /** 270 | * Tests AbstractMap::keys 271 | * 272 | * @return void 273 | * 274 | * @since 1.0.0 275 | */ 276 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::keys')] 277 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 278 | #[CoversFunction('chdemko\SortedCollection\Iterator::keys')] 279 | #[CoversFunction('chdemko\SortedCollection\Iterator::rewind')] 280 | #[CoversFunction('chdemko\SortedCollection\Iterator::key')] 281 | #[CoversFunction('chdemko\SortedCollection\Iterator::current')] 282 | #[CoversFunction('chdemko\SortedCollection\Iterator::next')] 283 | #[CoversFunction('chdemko\SortedCollection\Iterator::valid')] 284 | public function testKeys() 285 | { 286 | $empty = true; 287 | $tree = TreeMap::create(); 288 | 289 | foreach ($tree->keys as $key) { 290 | $empty = false; 291 | } 292 | 293 | $this->assertEquals(true, $empty); 294 | 295 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 296 | $i = 0; 297 | 298 | foreach ($tree->keys as $index => $key) { 299 | $this->assertEquals($i, $index); 300 | $this->assertEquals($i, $key); 301 | $i++; 302 | } 303 | } 304 | 305 | /** 306 | * Tests AbstractMap::values 307 | * 308 | * @return void 309 | * 310 | * @since 1.0.0 311 | */ 312 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::values')] 313 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 314 | #[CoversFunction('chdemko\SortedCollection\Iterator::values')] 315 | #[CoversFunction('chdemko\SortedCollection\Iterator::rewind')] 316 | #[CoversFunction('chdemko\SortedCollection\Iterator::key')] 317 | #[CoversFunction('chdemko\SortedCollection\Iterator::current')] 318 | #[CoversFunction('chdemko\SortedCollection\Iterator::next')] 319 | #[CoversFunction('chdemko\SortedCollection\Iterator::valid')] 320 | public function testValues() 321 | { 322 | $empty = true; 323 | $tree = TreeMap::create(); 324 | 325 | foreach ($tree->values as $values) { 326 | $empty = false; 327 | } 328 | 329 | $this->assertEquals(true, $empty); 330 | 331 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 332 | $i = 0; 333 | 334 | foreach ($tree->values as $value) { 335 | $this->assertEquals($i, $value); 336 | $i++; 337 | } 338 | } 339 | 340 | /** 341 | * Tests TreeMap::__get 342 | * 343 | * @return void 344 | * 345 | * @since 1.0.0 346 | */ 347 | #[CoversFunction('chdemko\SortedCollection\TreeMap::__get')] 348 | public function testGetUnexisting() 349 | { 350 | $tree = TreeMap::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 351 | $this->expectException('RuntimeException'); 352 | $unexisting = $tree->unexisting; 353 | } 354 | 355 | /** 356 | * Data provider for testLower 357 | * 358 | * @return array 359 | * 360 | * @since 1.0.0 361 | */ 362 | public static function casesLower() 363 | { 364 | return array( 365 | array(array(), 10, null, 'OutOfBoundsException'), 366 | array(array(1 => 1), 1, null, 'OutOfBoundsException'), 367 | array(array(1 => 1, 0 => 0), 0, null, 'OutOfBoundsException'), 368 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfBoundsException'), 369 | array(array(0 => 0, 1 => 1), 1, 0, null), 370 | array(array(0 => 0, 1 => 1), 2, 1, null), 371 | ); 372 | } 373 | 374 | /** 375 | * Tests TreeMap::lower 376 | * 377 | * @param array $values Values array 378 | * @param mixed $key Key to search for 379 | * @param mixed $expected Expected key/value 380 | * @param mixed $exception Exception to be thrown 381 | * 382 | * @return void 383 | * 384 | * @since 1.0.0 385 | */ 386 | #[DataProvider('casesLower')] 387 | #[CoversFunction('chdemko\SortedCollection\TreeMap::lower')] 388 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::lowerKey')] 389 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::lowerValue')] 390 | public function testLower($values, $key, $expected, $exception) 391 | { 392 | if ($exception) { 393 | $this->expectException($exception); 394 | } 395 | 396 | $tree = TreeMap::create()->initialise($values); 397 | 398 | $this->assertEquals( 399 | $expected, 400 | $tree->lowerKey($key) 401 | ); 402 | $this->assertEquals( 403 | $expected, 404 | $tree->lowerValue($key) 405 | ); 406 | } 407 | 408 | /** 409 | * Data provider for testFloor 410 | * 411 | * @return array 412 | * 413 | * @since 1.0.0 414 | */ 415 | public static function casesFloor() 416 | { 417 | return array( 418 | array(array(), 10, null, 'OutOfBoundsException'), 419 | array(array(1 => 1), 1, 1, null), 420 | array(array(1 => 1, 0 => 0), 0, 0, null), 421 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfBoundsException'), 422 | array(array(0 => 0, 1 => 1), 1, 1, null), 423 | array(array(0 => 0, 1 => 1), 2, 1, null), 424 | ); 425 | } 426 | 427 | /** 428 | * Tests TreeMap::floor 429 | * 430 | * @param array $values Values array 431 | * @param mixed $key Key to search for 432 | * @param mixed $expected Expected key/value 433 | * @param mixed $exception Exception to be thrown 434 | * 435 | * @return void 436 | * 437 | * @since 1.0.0 438 | */ 439 | #[DataProvider('casesFloor')] 440 | #[CoversFunction('chdemko\SortedCollection\TreeMap::floor')] 441 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::floorKey')] 442 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::floorValue')] 443 | public function testFloor($values, $key, $expected, $exception) 444 | { 445 | if ($exception) { 446 | $this->expectException($exception); 447 | } 448 | 449 | $tree = TreeMap::create()->initialise($values); 450 | 451 | $this->assertEquals( 452 | $expected, 453 | $tree->floorKey($key) 454 | ); 455 | $this->assertEquals( 456 | $expected, 457 | $tree->floorValue($key) 458 | ); 459 | } 460 | 461 | /** 462 | * Tests TreeMap::find 463 | * 464 | * @return void 465 | * 466 | * @since 1.0.0 467 | */ 468 | #[CoversFunction('chdemko\SortedCollection\TreeMap::find')] 469 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::findKey')] 470 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::findValue')] 471 | public function testFind() 472 | { 473 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 474 | 475 | $this->assertEquals( 476 | 0, 477 | $tree->findKey(0) 478 | ); 479 | $this->assertEquals( 480 | 0, 481 | $tree->findValue(0) 482 | ); 483 | 484 | $tree->clear(); 485 | $this->expectException('OutOfBoundsException'); 486 | 487 | $key = $tree->findKey(10); 488 | } 489 | 490 | /** 491 | * Data provider for testCeiling 492 | * 493 | * @return array 494 | * 495 | * @since 1.0.0 496 | */ 497 | public static function casesCeiling() 498 | { 499 | return array( 500 | array(array(), 10, null, 'OutOfBoundsException'), 501 | array(array(1 => 1), 1, 1, null), 502 | array(array(1 => 1, 0 => 0), 0, 0, null), 503 | array(array(2 => 2, 1 => 1), 0, 1, null), 504 | array(array(0 => 0, 1 => 1), 1, 1, null), 505 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfBoundsException'), 506 | ); 507 | } 508 | 509 | /** 510 | * Tests TreeMap::ceiling 511 | * 512 | * @param array $values Values array 513 | * @param mixed $key Key to search for 514 | * @param mixed $expected Expected key/value 515 | * @param mixed $exception Exception to be thrown 516 | * 517 | * @return void 518 | * 519 | * @since 1.0.0 520 | */ 521 | #[DataProvider('casesCeiling')] 522 | #[CoversFunction('chdemko\SortedCollection\TreeMap::ceiling')] 523 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::ceilingKey')] 524 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::ceilingValue')] 525 | public function testCeiling($values, $key, $expected, $exception) 526 | { 527 | if ($exception) { 528 | $this->expectException($exception); 529 | } 530 | 531 | $tree = TreeMap::create()->initialise($values); 532 | 533 | $this->assertEquals( 534 | $expected, 535 | $tree->ceilingKey($key) 536 | ); 537 | $this->assertEquals( 538 | $expected, 539 | $tree->ceilingValue($key) 540 | ); 541 | } 542 | 543 | /** 544 | * Data provider for testHigher 545 | * 546 | * @return array 547 | * 548 | * @since 1.0.0 549 | */ 550 | public static function casesHigher() 551 | { 552 | return array( 553 | array(array(), 10, null, 'OutOfBoundsException'), 554 | array(array(1 => 1), 1, null, 'OutOfBoundsException'), 555 | array(array(1 => 1, 0 => 0), 0, 1, null), 556 | array(array(2 => 2, 1 => 1), 0, 1, null), 557 | array(array(0 => 0, 1 => 1), 1, null, 'OutOfBoundsException'), 558 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfBoundsException'), 559 | ); 560 | } 561 | 562 | /** 563 | * Tests TreeMap::higher 564 | * 565 | * @param array $values Values array 566 | * @param mixed $key Key to search for 567 | * @param mixed $expected Expected key/value 568 | * @param mixed $exception Exception to be thrown 569 | * 570 | * @return void 571 | * 572 | * @since 1.0.0 573 | */ 574 | #[DataProvider('casesHigher')] 575 | #[CoversFunction('chdemko\SortedCollection\TreeMap::higher')] 576 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::higherKey')] 577 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::higherValue')] 578 | public function testHigher($values, $key, $expected, $exception) 579 | { 580 | if ($exception) { 581 | $this->expectException($exception); 582 | } 583 | 584 | $tree = TreeMap::create()->initialise($values); 585 | 586 | $this->assertEquals( 587 | $expected, 588 | $tree->higherKey($key) 589 | ); 590 | $this->assertEquals( 591 | $expected, 592 | $tree->higherValue($key) 593 | ); 594 | } 595 | 596 | /** 597 | * Data provider for testOffsetGet 598 | * 599 | * @return array 600 | * 601 | * @since 1.0.0 602 | */ 603 | public static function casesOffsetGet() 604 | { 605 | return array( 606 | array(array(), 10, null, 'OutOfRangeException'), 607 | array(array(1 => 1), 1, 1, null), 608 | array(array(1 => 1, 0 => 0), 0, 0, null), 609 | array(array(2 => 2, 1 => 1), 0, null, 'OutOfRangeException'), 610 | array(array(0 => 0, 1 => 1), 1, 1, null), 611 | array(array(0 => 0, 1 => 1), 2, null, 'OutOfRangeException'), 612 | ); 613 | } 614 | 615 | /** 616 | * Tests TreeMap::offsetGet 617 | * 618 | * @param array $values Values array 619 | * @param mixed $key Key to search for 620 | * @param mixed $value Expected value 621 | * @param mixed $exception Exception to be thrown 622 | * 623 | * @return void 624 | * 625 | * @covers 626 | * 627 | * @dataProvider 628 | * 629 | * @since 1.0.0 630 | */ 631 | #[DataProvider('casesOffsetGet')] 632 | #[CoversFunction('chdemko\SortedCollection\TreeMap::offsetGet')] 633 | public function testOffsetGet($values, $key, $value, $exception) 634 | { 635 | if ($exception) { 636 | $this->expectException($exception); 637 | } 638 | 639 | $tree = TreeMap::create()->initialise($values); 640 | 641 | $this->assertEquals( 642 | $value, 643 | $tree[$key] 644 | ); 645 | } 646 | 647 | /** 648 | * Tests TreeMap::offsetSet 649 | * 650 | * @return void 651 | * 652 | * @since 1.0.0 653 | */ 654 | #[CoversFunction('chdemko\SortedCollection\TreeMap::offsetSet')] 655 | public function testOffsetSet() 656 | { 657 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 658 | $tree[5] = 6; 659 | 660 | $this->assertEquals( 661 | 6, 662 | $tree[5] 663 | ); 664 | } 665 | 666 | /** 667 | * Tests AbstractMap::offsetExists 668 | * 669 | * @return void 670 | * 671 | * @since 1.0.0 672 | */ 673 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::offsetExists')] 674 | public function testOffsetExists() 675 | { 676 | $tree = TreeMap::create(); 677 | 678 | $this->assertFalse( 679 | isset($tree[10]) 680 | ); 681 | 682 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 683 | 684 | $this->assertTrue( 685 | isset($tree[5]) 686 | ); 687 | $this->assertFalse( 688 | isset($tree[10]) 689 | ); 690 | } 691 | 692 | /** 693 | * Tests TreeMap::offsetUnset 694 | * 695 | * @return void 696 | * 697 | * @since 1.0.0 698 | */ 699 | #[CoversFunction('chdemko\SortedCollection\TreeMap::offsetUnset')] 700 | public function testOffsetUnset() 701 | { 702 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 703 | unset($tree[3]); 704 | 705 | $this->assertEquals( 706 | 9, 707 | count($tree) 708 | ); 709 | 710 | $this->expectException('OutOfRangeException'); 711 | $value = $tree[3]; 712 | } 713 | 714 | /** 715 | * Tests TreeMap::count 716 | * 717 | * @return void 718 | * 719 | * @since 1.0.0 720 | */ 721 | #[CoversFunction('chdemko\SortedCollection\TreeMap::count')] 722 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__get')] 723 | public function testCount() 724 | { 725 | $tree = TreeMap::create(); 726 | 727 | $this->assertEquals( 728 | 0, 729 | $tree->count 730 | ); 731 | 732 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 733 | 734 | $this->assertEquals( 735 | 10, 736 | count($tree) 737 | ); 738 | } 739 | 740 | /** 741 | * Tests AbstractMap::__toString 742 | * 743 | * @return void 744 | * 745 | * @since 1.0.0 746 | */ 747 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::__toString')] 748 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::toArray')] 749 | #[CoversFunction('chdemko\SortedCollection\AbstractMap::getIterator')] 750 | #[CoversFunction('chdemko\SortedCollection\Iterator::__construct')] 751 | #[CoversFunction('chdemko\SortedCollection\Iterator::create')] 752 | #[CoversFunction('chdemko\SortedCollection\Iterator::rewind')] 753 | #[CoversFunction('chdemko\SortedCollection\Iterator::key')] 754 | #[CoversFunction('chdemko\SortedCollection\Iterator::current')] 755 | #[CoversFunction('chdemko\SortedCollection\Iterator::next')] 756 | #[CoversFunction('chdemko\SortedCollection\Iterator::valid')] 757 | public function testToString() 758 | { 759 | $tree = TreeMap::create(); 760 | 761 | $this->assertEquals( 762 | '[]', 763 | (string) $tree 764 | ); 765 | 766 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 767 | 768 | $this->assertEquals( 769 | '[0,1,2,3,4,5,6,7,8,9]', 770 | (string) $tree 771 | ); 772 | } 773 | 774 | /** 775 | * Tests TreeMap::jsonSerialize 776 | * 777 | * @return void 778 | * 779 | * @since 1.0.0 780 | */ 781 | #[CoversFunction('chdemko\SortedCollection\TreeMap::jsonSerialize')] 782 | public function testJsonSerialize() 783 | { 784 | $tree = TreeMap::create(); 785 | 786 | $this->assertEquals( 787 | '{"TreeMap":[]}', 788 | json_encode($tree) 789 | ); 790 | 791 | $tree = TreeMap::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 792 | 793 | $this->assertEquals( 794 | '{"TreeMap":[0,1,2,3,4,5,6,7,8,9]}', 795 | json_encode($tree) 796 | ); 797 | } 798 | } 799 | -------------------------------------------------------------------------------- /tests/SortedCollection/TreeSetTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 8 | * 9 | * @license BSD 3-Clause License 10 | * 11 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 12 | */ 13 | 14 | // Declare chdemko\SortedCollection namespace 15 | namespace chdemko\SortedCollection; 16 | 17 | use PHPUnit\Framework\Attributes\DataProvider; 18 | use PHPUnit\Framework\TestCase; 19 | 20 | /** 21 | * TreeSet class test 22 | * 23 | * @package SortedCollection 24 | * @subpackage Set 25 | * 26 | * @since 1.0.0 27 | */ 28 | class TreeSetTest extends TestCase 29 | { 30 | /** 31 | * Data provider for testCreate 32 | * 33 | * @return array 34 | * 35 | * @since 1.0.0 36 | */ 37 | public static function casesCreate() 38 | { 39 | return array( 40 | array(array(), null, null), 41 | array(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 3, null), 42 | array( 43 | array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 44 | 3, 45 | function ($key1, $key2) { 46 | return $key1 - $key2; 47 | } 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Tests TreeSet::create 54 | * 55 | * @param array $values Initial values 56 | * @param mixed $key Expected root key 57 | * @param callable $comparator Comparator 58 | * 59 | * @return void 60 | * 61 | * @since 1.0.0 62 | */ 63 | #[DataProvider('casesCreate')] 64 | #[CoversFunction('chdemko\SortedCollection\TreeSet::__construct')] 65 | #[CoversFunction('chdemko\SortedCollection\TreeSet::create')] 66 | #[CoversFunction('chdemko\SortedCollection\TreeSet::put')] 67 | #[CoversFunction('chdemko\SortedCollection\TreeSet::initialise')] 68 | public function testCreate($values, $key, $comparator) 69 | { 70 | $set = TreeSet::create($comparator)->initialise($values); 71 | 72 | if ($comparator !== null) { 73 | $this->assertEquals( 74 | $comparator, 75 | $set->comparator 76 | ); 77 | } else { 78 | $this->assertTrue( 79 | is_callable($set->comparator) 80 | ); 81 | } 82 | 83 | // Set the map property accessible 84 | $getMap = (new \ReflectionClass($set))->getMethod('getMap'); 85 | $getMap->setAccessible(true); 86 | $map = $getMap->invoke($set); 87 | 88 | // Set the root property accessible 89 | $root = (new \ReflectionClass($map))->getProperty('root'); 90 | $root->setAccessible(true); 91 | 92 | if ($values) { 93 | $this->assertEquals( 94 | $key, 95 | $root->getValue($map)->key 96 | ); 97 | } else { 98 | $this->assertEquals( 99 | null, 100 | $root->getValue($map) 101 | ); 102 | } 103 | } 104 | 105 | /** 106 | * Tests TreeSet::getMap 107 | * 108 | * @return void 109 | * 110 | * @since 1.0.0 111 | */ 112 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::getMap')] 113 | public function testGetMap() 114 | { 115 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 116 | 117 | // Set the map property accessible 118 | $getMap = (new \ReflectionClass($set))->getMethod('getMap'); 119 | $getMap->setAccessible(true); 120 | $map = $getMap->invoke($set); 121 | 122 | $this->assertEquals( 123 | '[true,true,true,true,true,true,true,true,true,true]', 124 | (string) $map 125 | ); 126 | } 127 | 128 | /** 129 | * Tests TreeSet::setMap 130 | * 131 | * @return void 132 | * 133 | * @since 1.0.0 134 | */ 135 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::setMap')] 136 | public function testSetMap() 137 | { 138 | $set = TreeSet::create(); 139 | 140 | // Set the map property accessible 141 | $setMap = (new \ReflectionClass($set))->getMethod('setMap'); 142 | $setMap->setAccessible(true); 143 | $setMap->invoke($set, TreeMap::create()->put(array(true, true, true))); 144 | 145 | $getMap = (new \ReflectionClass($set))->getMethod('getMap'); 146 | $getMap->setAccessible(true); 147 | $map = $getMap->invoke($set); 148 | 149 | $this->assertEquals( 150 | '[true,true,true]', 151 | (string) $map 152 | ); 153 | } 154 | 155 | /** 156 | * Tests TreeSet::clear 157 | * 158 | * @return void 159 | * 160 | * @since 1.0.0 161 | */ 162 | #[CoversFunction('chdemko\SortedCollection\TreeSet::clear')] 163 | public function testClear() 164 | { 165 | $set = TreeSet::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 166 | 167 | // Set the map property accessible 168 | $getMap = (new \ReflectionClass($set))->getMethod('getMap'); 169 | $getMap->setAccessible(true); 170 | $map = $getMap->invoke($set); 171 | 172 | // Set the root property accessible 173 | $root = (new \ReflectionClass($map))->getProperty('root'); 174 | $root->setAccessible(true); 175 | 176 | $this->assertEquals( 177 | $set, 178 | $set->clear() 179 | ); 180 | 181 | $this->assertEquals( 182 | null, 183 | $root->getValue($map) 184 | ); 185 | } 186 | 187 | /** 188 | * Tests TreeSet::__clone 189 | * 190 | * @return void 191 | * 192 | * @since 1.0.0 193 | */ 194 | #[CoversFunction('chdemko\SortedCollection\TreeSet::__clone')] 195 | public function testClone() 196 | { 197 | $set = TreeSet::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 198 | $clone = clone $set; 199 | $set->clear(); 200 | $this->assertEquals( 201 | 10, 202 | count($clone) 203 | ); 204 | } 205 | 206 | /** 207 | * Tests AbstractSet::comparator 208 | * 209 | * @return void 210 | * 211 | * @since 1.0.0 212 | */ 213 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::comparator')] 214 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 215 | public function testComparator() 216 | { 217 | $comparator = function ($key1, $key2) { 218 | return $key1 - $key2; 219 | }; 220 | $set = TreeSet::create($comparator)->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 221 | $this->assertEquals( 222 | $comparator, 223 | $set->comparator 224 | ); 225 | } 226 | 227 | /** 228 | * Tests AbstractSet::first 229 | * 230 | * @return void 231 | * 232 | * @since 1.0.0 233 | */ 234 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::first')] 235 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 236 | public function testFirst() 237 | { 238 | $set = TreeSet::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 239 | $this->assertEquals( 240 | 0, 241 | $set->first 242 | ); 243 | $set->clear(); 244 | $this->expectException('OutOfBoundsException'); 245 | $key = $set->first; 246 | } 247 | 248 | /** 249 | * Tests AbstractSet::last 250 | * 251 | * @return void 252 | * 253 | * @since 1.0.0 254 | */ 255 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::last')] 256 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 257 | public function testLast() 258 | { 259 | $set = TreeSet::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 260 | $this->assertEquals( 261 | 9, 262 | $set->last 263 | ); 264 | $set->clear(); 265 | $this->expectException('OutOfBoundsException'); 266 | $key = $set->last; 267 | } 268 | 269 | /** 270 | * Tests AbstractSet::__get 271 | * 272 | * @return void 273 | * 274 | * @since 1.0.0 275 | */ 276 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 277 | public function testGetUnexisting() 278 | { 279 | $set = TreeSet::create()->put(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 280 | $this->expectException('RuntimeException'); 281 | $unexisting = $set->unexisting; 282 | } 283 | 284 | /** 285 | * Data provider for testLower 286 | * 287 | * @return array 288 | * 289 | * @since 1.0.0 290 | */ 291 | public static function casesLower() 292 | { 293 | return array( 294 | array(array(), 10, null, 'OutOfBoundsException'), 295 | array(array(1), 1, null, 'OutOfBoundsException'), 296 | array(array(0, 1), 0, null, 'OutOfBoundsException'), 297 | array(array(1, 2), 0, null, 'OutOfBoundsException'), 298 | array(array(0, 1), 1, 0, null), 299 | array(array(0, 1), 2, 1, null), 300 | ); 301 | } 302 | 303 | /** 304 | * Tests AbstractSet::lower 305 | * 306 | * @param array $values Values array 307 | * @param mixed $key Key to search for 308 | * @param mixed $expected Expected key 309 | * @param mixed $exception Exception to be thrown 310 | * 311 | * @return void 312 | * 313 | * @since 1.0.0 314 | */ 315 | #[DataProvider('casesLower')] 316 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::lower')] 317 | public function testLower($values, $key, $expected, $exception) 318 | { 319 | if ($exception) { 320 | $this->expectException($exception); 321 | } 322 | 323 | $set = TreeSet::create()->initialise($values); 324 | 325 | $this->assertEquals( 326 | $expected, 327 | $set->lower($key) 328 | ); 329 | } 330 | 331 | /** 332 | * Data provider for testFloor 333 | * 334 | * @return array 335 | * 336 | * @since 1.0.0 337 | */ 338 | public static function casesFloor() 339 | { 340 | return array( 341 | array(array(), 10, null, 'OutOfBoundsException'), 342 | array(array(1), 1, 1, null), 343 | array(array(0, 1), 0, 0, null), 344 | array(array(1, 2), 0, null, 'OutOfBoundsException'), 345 | array(array(0, 1), 1, 1, null), 346 | array(array(0, 1), 2, 1, null), 347 | ); 348 | } 349 | 350 | /** 351 | * Tests AbstractSet::floor 352 | * 353 | * @param array $values Values array 354 | * @param mixed $key Key to search for 355 | * @param mixed $expected Expected key 356 | * @param mixed $exception Exception to be thrown 357 | * 358 | * @return void 359 | * 360 | * @since 1.0.0 361 | */ 362 | #[DataProvider('casesFloor')] 363 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::floor')] 364 | public function testFloor($values, $key, $expected, $exception) 365 | { 366 | if ($exception) { 367 | $this->expectException($exception); 368 | } 369 | 370 | $set = TreeSet::create()->initialise($values); 371 | 372 | $this->assertEquals( 373 | $expected, 374 | $set->floor($key) 375 | ); 376 | } 377 | 378 | /** 379 | * Tests AbstractSet::find 380 | * 381 | * @return void 382 | * 383 | * @since 1.0.0 384 | */ 385 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::find')] 386 | public function testFind() 387 | { 388 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 389 | 390 | $this->assertEquals( 391 | 0, 392 | $set->find(0) 393 | ); 394 | 395 | $set->clear(); 396 | $this->expectException('OutOfBoundsException'); 397 | 398 | $key = $set->find(10); 399 | } 400 | 401 | /** 402 | * Data provider for testCeiling 403 | * 404 | * @return array 405 | * 406 | * @since 1.0.0 407 | */ 408 | public static function casesCeiling() 409 | { 410 | return array( 411 | array(array(), 10, null, 'OutOfBoundsException'), 412 | array(array(1), 1, 1, null), 413 | array(array(0, 1), 0, 0, null), 414 | array(array(1, 2), 0, 1, null), 415 | array(array(0, 1), 1, 1, null), 416 | array(array(0, 1), 2, null, 'OutOfBoundsException'), 417 | ); 418 | } 419 | 420 | /** 421 | * Tests AbstractSet::ceiling 422 | * 423 | * @param array $values Values array 424 | * @param mixed $key Key to search for 425 | * @param mixed $expected Expected key 426 | * @param mixed $exception Exception to be thrown 427 | * 428 | * @return void 429 | * 430 | * @since 1.0.0 431 | */ 432 | #[DataProvider('casesCeiling')] 433 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::ceiling')] 434 | public function testCeiling($values, $key, $expected, $exception) 435 | { 436 | if ($exception) { 437 | $this->expectException($exception); 438 | } 439 | 440 | $set = TreeSet::create()->initialise($values); 441 | 442 | $this->assertEquals( 443 | $expected, 444 | $set->ceiling($key) 445 | ); 446 | } 447 | 448 | /** 449 | * Data provider for testHigher 450 | * 451 | * @return array 452 | * 453 | * @since 1.0.0 454 | */ 455 | public static function casesHigher() 456 | { 457 | return array( 458 | array(array(), 10, null, 'OutOfBoundsException'), 459 | array(array(1), 1, null, 'OutOfBoundsException'), 460 | array(array(0, 1), 0, 1, null), 461 | array(array(1, 2), 0, 1, null), 462 | array(array(0, 1), 1, null, 'OutOfBoundsException'), 463 | array(array(0, 1), 2, null, 'OutOfBoundsException'), 464 | ); 465 | } 466 | 467 | /** 468 | * Tests AbstractSet::higher 469 | * 470 | * @param array $values Values array 471 | * @param mixed $key Key to search for 472 | * @param mixed $expected Expected key 473 | * @param mixed $exception Exception to be thrown 474 | * 475 | * @return void 476 | * 477 | * @since 1.0.0 478 | */ 479 | #[DataProvider('casesHigher')] 480 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::higher')] 481 | public function testHigher($values, $key, $expected, $exception) 482 | { 483 | if ($exception) { 484 | $this->expectException($exception); 485 | } 486 | 487 | $set = TreeSet::create()->initialise($values); 488 | 489 | $this->assertEquals( 490 | $expected, 491 | $set->higher($key) 492 | ); 493 | } 494 | 495 | /** 496 | * Data provider for testOffsetGet 497 | * 498 | * @return array 499 | * 500 | * @since 1.0.0 501 | */ 502 | public static function casesOffsetGet() 503 | { 504 | return array( 505 | array(array(), 0, false), 506 | array(array(0), 1, false), 507 | array(array(1), 1, true), 508 | array(array(0, 1), 0, true), 509 | array(array(0, 1), 1, true), 510 | array(array(0, 1), 2, false), 511 | ); 512 | } 513 | 514 | /** 515 | * Tests AbstractSet::offsetGet 516 | * 517 | * @param array $values Values array 518 | * @param mixed $key Key to search for 519 | * @param mixed $value Expected value 520 | * 521 | * @return void 522 | * 523 | * @since 1.0.0 524 | */ 525 | #[DataProvider('casesOffsetGet')] 526 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::offsetGet')] 527 | public function testOffsetGet($values, $key, $value) 528 | { 529 | $set = TreeSet::create()->initialise($values); 530 | 531 | $this->assertEquals( 532 | $value, 533 | $set[$key] 534 | ); 535 | } 536 | 537 | /** 538 | * Tests TreeSet::offsetSet 539 | * 540 | * @return void 541 | * 542 | * @since 1.0.0 543 | */ 544 | #[CoversFunction('chdemko\SortedCollection\TreeSet::offsetSet')] 545 | public function testOffsetSet() 546 | { 547 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 548 | $set[5] = false; 549 | 550 | $this->assertEquals( 551 | false, 552 | $set[5] 553 | ); 554 | $this->assertEquals( 555 | true, 556 | $set[4] 557 | ); 558 | } 559 | 560 | /** 561 | * Tests AbstractSet::offsetExists 562 | * 563 | * @return void 564 | * 565 | * @since 1.0.0 566 | */ 567 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::offsetExists')] 568 | public function testOffsetExists() 569 | { 570 | $set = TreeSet::create(); 571 | 572 | $this->assertFalse( 573 | isset($set[10]) 574 | ); 575 | 576 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 577 | 578 | $this->assertTrue( 579 | isset($set[5]) 580 | ); 581 | $this->assertFalse( 582 | isset($set[10]) 583 | ); 584 | } 585 | 586 | /** 587 | * Tests TreeSet::offsetUnset 588 | * 589 | * @return void 590 | * 591 | * @since 1.0.0 592 | */ 593 | #[CoversFunction('chdemko\SortedCollection\TreeSet::offsetUnset')] 594 | public function testOffsetUnset() 595 | { 596 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 597 | unset($set[5]); 598 | 599 | $this->assertEquals( 600 | false, 601 | $set[5] 602 | ); 603 | } 604 | 605 | /** 606 | * Tests AbstractSet::count 607 | * 608 | * @return void 609 | * 610 | * @since 1.0.0 611 | */ 612 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::count')] 613 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__get')] 614 | public function testCount() 615 | { 616 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 617 | 618 | $this->assertEquals( 619 | 10, 620 | $set->count 621 | ); 622 | } 623 | 624 | /** 625 | * Tests AbstractSet::__toString 626 | * 627 | * @return void 628 | * 629 | * @since 1.0.0 630 | */ 631 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::__toString')] 632 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::toArray')] 633 | #[CoversFunction('chdemko\SortedCollection\AbstractSet::getIterator')] 634 | #[CoversFunction('chdemko\SortedCollection\Iterator::create')] 635 | #[CoversFunction('chdemko\SortedCollection\Iterator::rewind')] 636 | #[CoversFunction('chdemko\SortedCollection\Iterator::key')] 637 | #[CoversFunction('chdemko\SortedCollection\Iterator::current')] 638 | #[CoversFunction('chdemko\SortedCollection\Iterator::next')] 639 | #[CoversFunction('chdemko\SortedCollection\Iterator::valid')] 640 | public function testToString() 641 | { 642 | $set = TreeSet::create(); 643 | 644 | $this->assertEquals( 645 | '[]', 646 | (string) $set 647 | ); 648 | 649 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 650 | 651 | $this->assertEquals( 652 | '[0,1,2,3,4,5,6,7,8,9]', 653 | (string) $set 654 | ); 655 | } 656 | 657 | /** 658 | * Tests TreeSet::jsonSerialize 659 | * 660 | * @return void 661 | * 662 | * @since 1.0.0 663 | */ 664 | #[CoversFunction('chdemko\SortedCollection\TreeSet::jsonSerialize')] 665 | public function testJsonSerialize() 666 | { 667 | $set = TreeSet::create(); 668 | 669 | $this->assertEquals( 670 | '{"TreeSet":[]}', 671 | json_encode($set) 672 | ); 673 | 674 | $set = TreeSet::create()->initialise(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); 675 | 676 | $this->assertEquals( 677 | '{"TreeSet":[0,1,2,3,4,5,6,7,8,9]}', 678 | json_encode($set) 679 | ); 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. 10 | * 11 | * @license BSD 3-Clause License 12 | * 13 | * This file is part of the php-sorted-collections package https://github.com/chdemko/php-sorted-collections 14 | */ 15 | 16 | require __DIR__ . '/../vendor/autoload.php'; 17 | date_default_timezone_set('UTC'); 18 | --------------------------------------------------------------------------------