├── _config.yml ├── .docker ├── memory-limit.ini ├── php.ini ├── debug.ini ├── xdebug.ini └── Dockerfile ├── .gitignore ├── infection.json ├── CONTRIBUTING.md ├── config └── grumphp │ ├── ko.txt │ └── ok.txt ├── docker-compose.yml ├── .travis.yml ├── src ├── Rule │ └── Interval │ │ ├── Ending.php │ │ ├── After.php │ │ ├── Before.php │ │ ├── NeighborhoodAfter.php │ │ ├── NeighborhoodBefore.php │ │ ├── Starting.php │ │ ├── Equality.php │ │ ├── Overlapping.php │ │ └── Inclusion.php ├── Boundary │ ├── Infinity.php │ ├── Integer.php │ ├── DateTime.php │ ├── Real.php │ └── BoundaryAbstract.php ├── Parser │ ├── IntervalsParser.php │ └── IntervalParser.php ├── Operation │ ├── Intervals │ │ └── Exclusion.php │ └── Interval │ │ ├── Union.php │ │ ├── Intersection.php │ │ └── Exclusion.php ├── Di.php ├── Intervals.php └── Interval.php ├── phpunit.xml ├── tests └── unit │ ├── DiTest.php │ ├── Parser │ ├── IntervalsParserTest.php │ └── IntervalParserTest.php │ ├── ScenarioTest.php │ ├── Rule │ └── Interval │ │ ├── AfterTest.php │ │ ├── BeforeTest.php │ │ ├── EndingTest.php │ │ ├── StartingTest.php │ │ ├── EquilityTest.php │ │ ├── NeighborhoodAfterTest.php │ │ ├── NeighborhoodBeforeTest.php │ │ ├── InclusionTest.php │ │ └── OverlappingTest.php │ ├── IntervalsTest.php │ ├── Operation │ ├── Interval │ │ ├── IntersectionTest.php │ │ ├── UnionTest.php │ │ └── ExclusionTest.php │ └── Intervals │ │ └── ExclusionTest.php │ ├── IntervalTest.php │ └── Boundary │ ├── DateTimeTest.php │ ├── RealTest.php │ ├── InfinityTest.php │ └── IntegerTest.php ├── Makefile ├── LICENSE ├── grumphp.yml ├── composer.json ├── .php_cs ├── README.md ├── scripts └── generate-readme.php └── .idea └── inspectionProfiles └── Project_Default.xml /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.docker/memory-limit.ini: -------------------------------------------------------------------------------- 1 | memory_limit = -1 -------------------------------------------------------------------------------- /.docker/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone = "Europe/Paris" 2 | variables_order = "EGPCS" 3 | -------------------------------------------------------------------------------- /.docker/debug.ini: -------------------------------------------------------------------------------- 1 | error_reporting = E_ALL 2 | error_log = /proc/self/fd/2 3 | log_errors = true 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | vendor 3 | .idea/* 4 | .php_cs.cache 5 | composer.lock 6 | build 7 | !.idea/inspectionProfiles 8 | infection-log.txt 9 | composer.phar -------------------------------------------------------------------------------- /infection.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 10, 8 | "logs": { 9 | "text": "infection-log.txt" 10 | } 11 | } -------------------------------------------------------------------------------- /.docker/xdebug.ini: -------------------------------------------------------------------------------- 1 | xdebug.remote_enable=on 2 | xdebug.remote_autostart=off 3 | xdebug.remote_connect_back=off 4 | xdebug.profiler_enable=0 5 | xdebug.profiler_output_dir="/var/www/html" 6 | xdebug.remote_port=9000 7 | xdebug.remote_host="172.17.0.1" 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | You are very welcomed to contribute to this Library! 3 | 4 | * Clone 5 | `git clone https://github.com/Kirouane/interval.git` 6 | 7 | * Test 8 | `vendor/bin/phpunit` 9 | 10 | * Build 11 | `vendor/bin/grumphp run` 12 | -------------------------------------------------------------------------------- /config/grumphp/ko.txt: -------------------------------------------------------------------------------- 1 | 2 | ███▄ █ ▒█████ ██▓███ ▓█████ 3 | ██ ▀█ █ ▒██▒ ██▒▓██░ ██▒▓█ ▀ 4 | ▓██ ▀█ ██▒▒██░ ██▒▓██░ ██▓▒▒███ 5 | ▓██▒ ▐▌██▒▒██ ██░▒██▄█▓▒ ▒▒▓█ ▄ 6 | ▒██░ ▓██░░ ████▓▒░▒██▒ ░ ░░▒████▒ 7 | ░ ▒░ ▒ ▒ ░ ▒░▒░▒░ ▒▓▒░ ░ ░░░ ▒░ ░ 8 | ░ ░░ ░ ▒░ ░ ▒ ▒░ ░▒ ░ ░ ░ ░ 9 | ░ ░ ░ ░ ░ ░ ▒ ░░ ░ 10 | ░ ░ ░ ░ ░ 11 | 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | php: 4 | build: 5 | context: ./.docker/ 6 | working_dir: /var/www/html 7 | tty: true 8 | volumes: 9 | - .:/var/www/html/ 10 | - ./.docker/debug.ini:/usr/local/etc/php/conf.d/debug.ini 11 | - ./.docker/memory-limit.ini:/usr/local/etc/php/conf.d/memory-limit.ini 12 | - ./.docker/php.ini:/usr/local/etc/php/conf.d/php.ini 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | cache: 9 | directories: 10 | - .composer/cache 11 | 12 | before_install: 13 | - alias composer=composer\ --no-interaction && composer selfupdate 14 | - composer global require hirak/prestissimo 15 | 16 | install: 17 | - travis_retry composer update --no-progress --profile --no-scripts --no-suggest 18 | 19 | script: 20 | - mkdir -p build/logs 21 | - vendor/bin/grumphp run 22 | 23 | after_success: 24 | - travis_retry php vendor/bin/coveralls 25 | -------------------------------------------------------------------------------- /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.1-cli 2 | RUN apt-get update && apt-get install -y --no-install-recommends \ 3 | curl \ 4 | git \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN pecl install xdebug-beta 9 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 10 | 11 | ENV XDEBUGINI_PATH=/usr/local/etc/php/conf.d/xdebug.ini 12 | RUN echo "zend_extension="`find /usr/local/lib/php/extensions/ -iname 'xdebug.so'` > $XDEBUGINI_PATH 13 | COPY xdebug.ini /tmp/xdebug.ini 14 | RUN cat /tmp/xdebug.ini >> $XDEBUGINI_PATH -------------------------------------------------------------------------------- /src/Rule/Interval/Ending.php: -------------------------------------------------------------------------------- 1 | getEnd()->equalTo($second->getEnd()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/Interval/After.php: -------------------------------------------------------------------------------- 1 | getStart()->greaterThan($second->getEnd()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/Interval/Before.php: -------------------------------------------------------------------------------- 1 | getEnd()->lessThan($second->getStart()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/Interval/NeighborhoodAfter.php: -------------------------------------------------------------------------------- 1 | getStart()->equalTo($second->getEnd()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/Interval/NeighborhoodBefore.php: -------------------------------------------------------------------------------- 1 | getStart()->equalTo($first->getEnd()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | 13 | ./src 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Rule/Interval/Starting.php: -------------------------------------------------------------------------------- 1 | getStart()->equalTo($second->getStart()) && !$first->getStart()->equalTo($first->getEnd()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/Interval/Equality.php: -------------------------------------------------------------------------------- 1 | getStart()->equalTo($second->getStart()) && 24 | $first->getEnd()->equalTo($second->getEnd()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/grumphp/ok.txt: -------------------------------------------------------------------------------- 1 | 2 | ▄████ ▒█████ ▒█████ ▓█████▄ ▄▄▄██▀▀▀▒█████ ▄▄▄▄ ▐██▌ 3 | ██▒ ▀█▒▒██▒ ██▒▒██▒ ██▒▒██▀ ██▌ ▒██ ▒██▒ ██▒▓█████▄ ▐██▌ 4 | ▒██░▄▄▄░▒██░ ██▒▒██░ ██▒░██ █▌ ░██ ▒██░ ██▒▒██▒ ▄██ ▐██▌ 5 | ░▓█ ██▓▒██ ██░▒██ ██░░▓█▄ ▌ ▓██▄██▓ ▒██ ██░▒██░█▀ ▓██▒ 6 | ░▒▓███▀▒░ ████▓▒░░ ████▓▒░░▒████▓ ▓███▒ ░ ████▓▒░░▓█ ▀█▓ ▒▄▄ 7 | ░▒ ▒ ░ ▒░▒░▒░ ░ ▒░▒░▒░ ▒▒▓ ▒ ▒▓▒▒░ ░ ▒░▒░▒░ ░▒▓███▀▒ ░▀▀▒ 8 | ░ ░ ░ ▒ ▒░ ░ ▒ ▒░ ░ ▒ ▒ ▒ ░▒░ ░ ▒ ▒░ ▒░▒ ░ ░ ░ 9 | ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ 10 | ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 11 | ░ ░ 12 | -------------------------------------------------------------------------------- /src/Rule/Interval/Overlapping.php: -------------------------------------------------------------------------------- 1 | getEnd()->greaterThanOrEqualTo($second->getStart()) && 24 | $first->getStart()->lessThanOrEqualTo($second->getEnd()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Rule/Interval/Inclusion.php: -------------------------------------------------------------------------------- 1 | getStart()->greaterThanOrEqualTo($first->getStart()) && 24 | $second->getEnd()->lessThanOrEqualTo($first->getEnd()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/DiTest.php: -------------------------------------------------------------------------------- 1 | get($serviceName); 26 | self::assertInstanceOf($serviceName, $service); 27 | $sameService = $di->get($serviceName); 28 | self::assertSame($service, $sameService); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: help 2 | 3 | help: ## Show help 4 | @grep -E '(^[a-zA-Z0-9_\-\.]+:.*?##.*$$)|(^##)' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' 5 | 6 | install: image.build start php.install ## Installs everything: dependencies, database, assets, etc. 7 | 8 | image.build: ## builds docker images 9 | docker-compose build 10 | 11 | php.install: ## Installs composer dependencies 12 | docker-compose run --rm -T php composer install --no-interaction 13 | 14 | 15 | start: ## Starts docker-compose 16 | docker-compose up -d 17 | 18 | stop: ## Stops docker-compose 19 | docker-compose down --remove-orphans 20 | 21 | restart: stop start ## Stops and starts docker-compose 22 | 23 | php.shell: 24 | docker-compose exec php bash 25 | 26 | test: 27 | docker-compose exec php vendor/bin/phpunit -c phpunit.xml 28 | 29 | logs.watch: 30 | docker-compose logs -f 31 | 32 | csfixer.fix: #fix PHP CS Fixer issues 33 | docker-compose exec vendor/bin/php-cs-fixer fix -------------------------------------------------------------------------------- /src/Boundary/Infinity.php: -------------------------------------------------------------------------------- 1 | toComparable() <=> $comparable->toComparable(); 24 | } 25 | 26 | /** 27 | * @return float 28 | */ 29 | public function toComparable(): float 30 | { 31 | if ($this->comparable) { 32 | return $this->comparable; 33 | } 34 | 35 | return $this->comparable = $this->getValue(); 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function __toString() 42 | { 43 | $string = ($this->getValue() < 0 ? '-' : '+') . '∞'; 44 | return $this->applyBoundarySymbol($string); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nassim Kirouane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Parser/IntervalsParser.php: -------------------------------------------------------------------------------- 1 | intervalParser = $intervalParser; 26 | } 27 | 28 | /** 29 | * @param array $expressions 30 | * @return Intervals 31 | * @throws \InvalidArgumentException 32 | * @throws \UnexpectedValueException 33 | * @throws \RangeException 34 | * @throws \ErrorException 35 | */ 36 | public function parse(array $expressions) : Intervals 37 | { 38 | $intervals = []; 39 | foreach ($expressions as $expression) { 40 | $intervals[] = $this->intervalParser->parse($expression); 41 | } 42 | 43 | return new Intervals($intervals); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /grumphp.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | git_dir: . 3 | bin_dir: vendor/bin 4 | git_hook_variables: 5 | EXEC_GRUMPHP_COMMAND: docker-compose exec -T php 6 | ascii: 7 | failed: config/grumphp/ko.txt 8 | succeeded: config/grumphp/ok.txt 9 | tasks: 10 | phpunit: 11 | always_execute: true 12 | metadata: 13 | priority: 100 14 | clover_coverage: 15 | clover_file: build/logs/clover.xml 16 | level: 90 17 | metadata: 18 | priority: 99 19 | infection: 20 | threads: 4 21 | min_msi: 80 22 | min_covered_msi: 80 23 | metadata: 24 | priority: 98 25 | composer: ~ 26 | composer_require_checker: ~ 27 | phpcpd: 28 | exclude: ['vendor', 'tests'] 29 | file_size: 30 | max_size: 10M 31 | git_blacklist: 32 | keywords: 33 | - "die(" 34 | - "var_dump(" 35 | - "exit;" 36 | phpcsfixer: ~ 37 | phplint: ~ 38 | phpparser: ~ 39 | phpstan: ~ 40 | securitychecker: ~ -------------------------------------------------------------------------------- /src/Boundary/Integer.php: -------------------------------------------------------------------------------- 1 | toComparable() <=> $comparable->toComparable(); 24 | } 25 | 26 | /** 27 | * @return int|mixed 28 | */ 29 | public function toComparable() 30 | { 31 | if ($this->comparable) { 32 | return $this->comparable; 33 | } 34 | 35 | if ($this->isClosed()) { 36 | return $this->comparable = $this->getValue(); 37 | } 38 | 39 | $sign = $this->isLeft() ? 1 : -1; 40 | 41 | return $this->comparable = $this->getValue() + $sign * 1; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function __toString() 48 | { 49 | return $this->applyBoundarySymbol($this->getValue()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Boundary/DateTime.php: -------------------------------------------------------------------------------- 1 | toComparable() <=> $comparable->toComparable(); 24 | } 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function toComparable(): int 30 | { 31 | if ($this->comparable) { 32 | return $this->comparable; 33 | } 34 | 35 | $value = $this->getValue()->getTimestamp(); 36 | 37 | if ($this->isClosed()) { 38 | return $this->comparable = $value; 39 | } 40 | 41 | $sign = $this->isLeft() ? 1 : -1; 42 | 43 | return $this->comparable = $value + $sign * 1; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function __toString() 50 | { 51 | return $this->applyBoundarySymbol($this->getValue()->format(\DateTime::RFC3339)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/unit/Parser/IntervalsParserTest.php: -------------------------------------------------------------------------------- 1 | parse($expressions); 38 | $this->assertInstanceOf(Intervals::class, $intervals); 39 | $this->assertCount(count($expected), $intervals); 40 | foreach ($intervals as $i => $interval) { 41 | self::assertSame($expected[$i][0], $interval->getStart()->getValue()); 42 | self::assertSame($expected[$i][1], $interval->getEnd()->getValue()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kirouane/interval", 3 | "description": "Library to handel intervals", 4 | "type": "library", 5 | "keywords": [ 6 | "php", "interval", "period", "time", "algebra", "infinity", "planning", "booking", "exclusion" 7 | ], 8 | "homepage": "https://github.com/Kirouane/interval", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "nassim.kirouane", 13 | "email": "nassim.kirouane@gmail.com", 14 | "role": "lead" 15 | } 16 | ], 17 | "support": { 18 | "issues": "https://github.com/Kirouane/interval/issues" 19 | }, 20 | "autoload": { 21 | "psr-4": {"Interval\\": "src/"} 22 | }, 23 | "autoload-dev": { 24 | "psr-4": {"Interval\\": "tests/unit"} 25 | }, 26 | "config": { 27 | "platform": { 28 | "php": "7.1" 29 | } 30 | }, 31 | "require": { 32 | "php" : ">= 7.1" 33 | }, 34 | "require-dev": { 35 | "phpro/grumphp": "^0.15.2", 36 | "infection/infection": "^0.11.5", 37 | "phpunit/phpunit": "^7.5", 38 | "mockery/mockery": "^1.2", 39 | "php-coveralls/php-coveralls": "^2.1", 40 | "friendsofphp/php-cs-fixer": "^2.15", 41 | "phpstan/phpstan": "^0.11.8", 42 | "jakub-onderka/php-parallel-lint": "^1.0", 43 | "maglnet/composer-require-checker": "^2.0", 44 | "phpmd/phpmd": "^2.6", 45 | "sebastian/phpcpd": "^4.1", 46 | "sensiolabs/security-checker": "^5.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Boundary/Real.php: -------------------------------------------------------------------------------- 1 | isClosed() && $comparable->isClosed()) { 24 | return $this->getValue() <=> $comparable->getValue(); 25 | } 26 | 27 | if ($this->getValue() !== $comparable->getValue()) { 28 | return $this->getValue() <=> $comparable->getValue(); 29 | } 30 | 31 | return $this->toComparable() <=> $comparable->toComparable(); 32 | } 33 | 34 | /** 35 | * @return float|mixed 36 | */ 37 | public function toComparable() 38 | { 39 | if ($this->comparable) { 40 | return $this->comparable; 41 | } 42 | 43 | if ($this->isClosed()) { 44 | return $this->comparable = $this->getValue(); 45 | } 46 | 47 | $sign = $this->isLeft() ? 1 : -1; 48 | 49 | return $this->comparable = $this->getValue() + $sign * 1; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function __toString() 56 | { 57 | return $this->applyBoundarySymbol($this->getValue()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 7 | ->setRules([ 8 | '@PSR2' => true, 9 | 'psr4' => true, 10 | 'binary_operator_spaces' => ['align_equals' => true, 'align_double_arrow' => true], 11 | 'whitespace_after_comma_in_array' => true, 12 | 'array_syntax' => array('syntax' => 'short'), 13 | 'phpdoc_add_missing_param_annotation' => true, 14 | 'phpdoc_order' => false, 15 | 'single_quote' => true, 16 | 'no_unused_imports' => true, 17 | 'no_extra_consecutive_blank_lines' => ['extra', 'continue', 'return', 'throw', 'curly_brace_block', 'parenthesis_brace_block', 'square_brace_block'], 18 | 'no_empty_phpdoc' => true, 19 | 'no_empty_comment' => true, 20 | 'no_whitespace_in_blank_line' => true, 21 | 'single_blank_line_before_namespace' => true, 22 | 'no_empty_statement' => true, 23 | 'blank_line_after_opening_tag' => false, 24 | 'no_leading_import_slash' => true, 25 | 'no_leading_namespace_whitespace' => true, 26 | 'no_trailing_comma_in_list_call' => true, 27 | 'ordered_imports' => true, 28 | 'trailing_comma_in_multiline_array' => true, 29 | 'standardize_not_equals' => true, 30 | 'no_leading_namespace_whitespace' => true, 31 | 'object_operator_without_whitespace' => true, 32 | 'no_blank_lines_after_class_opening' => true, 33 | ]) 34 | ->setFinder( 35 | PhpCsFixer\Finder::create() 36 | ->in('src') 37 | ->in('tests') 38 | ); 39 | -------------------------------------------------------------------------------- /src/Operation/Intervals/Exclusion.php: -------------------------------------------------------------------------------- 1 | compute($first, $second); 24 | } 25 | 26 | /** 27 | * Excludes an interval from another one. Exp 28 | * 29 | * |_________________| 30 | * 31 | * - 32 | * |_________________| 33 | * 34 | * = 35 | * |___________| 36 | * 37 | * @param Intervals $firstInterval 38 | * @param Intervals $secondInterval 39 | * @return Intervals 40 | */ 41 | 42 | public function compute(Intervals $firstInterval, Intervals $secondInterval): Intervals 43 | { 44 | $first = $firstInterval->getArrayCopy(); 45 | $second = $secondInterval->getArrayCopy(); 46 | if (0 === count($second)) { 47 | return $firstInterval; 48 | } 49 | 50 | $count = count($second); 51 | while ($count > 0) { 52 | $intervalToExclude = \array_shift($second); 53 | 54 | $newIntervals = []; 55 | 56 | /** @var Interval $interval */ 57 | foreach ($first as $interval) { 58 | $newIntervals = \array_merge($newIntervals, $interval->exclude($intervalToExclude)->getArrayCopy()); 59 | } 60 | 61 | $first = $newIntervals; 62 | $count = count($second); 63 | } 64 | 65 | return new Intervals($first); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Operation/Interval/Union.php: -------------------------------------------------------------------------------- 1 | compute($first, $second); 27 | } 28 | 29 | /** 30 | * Compute the union between two intervals. Exp : 31 | * 32 | * |_________________| 33 | * 34 | * ∪ 35 | * |_________________| 36 | * 37 | * = 38 | * |_____________________________| 39 | * 40 | * @param Interval $first 41 | * @param Interval $second 42 | * @return Intervals 43 | * @throws \InvalidArgumentException 44 | * @throws \UnexpectedValueException 45 | * @throws \RangeException 46 | */ 47 | public function compute(Interval $first, Interval $second) : Intervals 48 | { 49 | if ($first->overlaps($second) || $first->isNeighborAfter($second) || $first->isNeighborBefore($second)) { 50 | return new Intervals([new Interval( 51 | $first->getStart()->lessThan($second->getStart()) ? $first->getStart() : $second->getStart(), 52 | $first->getEnd()->greaterThan($second->getEnd()) ? $first->getEnd() : $second->getEnd() 53 | )]); 54 | } 55 | 56 | return new Intervals([ 57 | new Interval($first->getStart(), $first->getEnd()), 58 | new Interval($second->getStart(), $second->getEnd()), 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/unit/ScenarioTest.php: -------------------------------------------------------------------------------- 1 | intersect(Interval::create(']1, 20[')); 17 | 18 | $this->assertSameInterval( 19 | Interval::create(']1, 10]'), 20 | $interval 21 | ); 22 | 23 | $intervals = $interval->union(Interval::create('[3, 30[')); 24 | 25 | $this->assertSameIntervals( 26 | Intervals::create([']1, 30[']), 27 | $intervals 28 | ); 29 | 30 | $intervals = $intervals->exclude(Intervals::create(['[5, 7[', '[9, 9]', ']16, 20['])); 31 | 32 | $this->assertSameIntervals( 33 | Intervals::create([']1, 5[', '[7, 9[', ']9, 16]', '[20, 30[']), 34 | $intervals 35 | ); 36 | } 37 | 38 | private function assertSameInterval(Interval $interval, Interval $comparedInterval) 39 | { 40 | self::assertSame($interval->getStart()->getValue(), $comparedInterval->getStart()->getValue()); 41 | self::assertSame($interval->getStart()->isLeft(), $comparedInterval->getStart()->isLeft()); 42 | self::assertSame($interval->getStart()->isOpen(), $comparedInterval->getStart()->isOpen()); 43 | 44 | self::assertSame($interval->getEnd()->getValue(), $comparedInterval->getEnd()->getValue()); 45 | self::assertSame($interval->getEnd()->isLeft(), $comparedInterval->getEnd()->isLeft()); 46 | self::assertSame($interval->getEnd()->isOpen(), $comparedInterval->getEnd()->isOpen()); 47 | } 48 | 49 | private function assertSameIntervals(Intervals $intervals, Intervals $comparedIntervals) 50 | { 51 | foreach ($intervals as $i => $interval) { 52 | $this->assertSameInterval($interval, $comparedIntervals[$i]); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Operation/Interval/Intersection.php: -------------------------------------------------------------------------------- 1 | compute($first, $second); 26 | } 27 | 28 | /** 29 | * Compute the intersection of two intervals. Exp 30 | * 31 | * |_________________| 32 | * 33 | * ∩ 34 | * |_________________| 35 | * 36 | * = 37 | * |_____| 38 | * 39 | * @param Interval $first 40 | * @param Interval $second 41 | * @return Interval 42 | * @throws \InvalidArgumentException 43 | * @throws \UnexpectedValueException 44 | * @throws \RangeException 45 | */ 46 | public function compute(Interval $first, Interval $second): ?Interval 47 | { 48 | if ($first->isNeighborBefore($second)) { 49 | return new Interval( 50 | $first->getEnd()->changeSide(), 51 | $second->getStart()->changeSide() 52 | ); 53 | } 54 | 55 | if ($first->isNeighborAfter($second)) { 56 | return new Interval( 57 | $second->getEnd()->changeSide(), 58 | $first->getStart()->changeSide() 59 | ); 60 | } 61 | 62 | if (!$first->overlaps($second)) { 63 | return null; 64 | } 65 | 66 | return new Interval( 67 | $first->getStart()->greaterThan($second->getStart()) ? $first->getStart() : $second->getStart(), 68 | $first->getEnd()->lessThan($second->getEnd()) ? $first->getEnd() : $second->getEnd() 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Operation/Interval/Exclusion.php: -------------------------------------------------------------------------------- 1 | compute($first, $second); 29 | } 30 | 31 | /** 32 | * Excludes an interval from another one. Exp 33 | * 34 | * |_________________| 35 | * 36 | * - 37 | * |_________________| 38 | * 39 | * = 40 | * |___________| 41 | * 42 | * @param Interval $first 43 | * @param Interval $second 44 | * @return Intervals 45 | * @throws \InvalidArgumentException 46 | * @throws UnexpectedValueException 47 | * @throws RangeException 48 | */ 49 | public function compute(Interval $first, Interval $second): Intervals 50 | { 51 | if (!$first->overlaps($second)) { 52 | return new Intervals([$first]); 53 | } 54 | 55 | if ($second->includes($first)) { 56 | return new Intervals([]); 57 | } 58 | 59 | if ($second->contains($first->getStart())) { 60 | return new Intervals([ 61 | new Interval($second->getEnd()->flip(), $first->getEnd()), 62 | ]); 63 | } 64 | 65 | if ($second->contains($first->getEnd())) { 66 | return new Intervals([ 67 | new Interval($first->getStart(), $second->getStart()->flip()), 68 | ]); 69 | } 70 | 71 | if ($first->includes($second)) { 72 | return new Intervals([ 73 | new Interval($first->getStart(), $second->getStart()->flip()), 74 | new Interval($second->getEnd()->flip(), $first->getEnd()), 75 | ]); 76 | } 77 | 78 | throw new \RangeException('Unexpected calculation case'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Di.php: -------------------------------------------------------------------------------- 1 | [ 36 | self::PARSER_INTERVAL, 37 | ], 38 | ]; 39 | 40 | /** 41 | * @var array 42 | */ 43 | private $services = []; 44 | 45 | /** 46 | * Instantiates and/or returns a service by its name 47 | * @param $name 48 | * @return mixed 49 | */ 50 | public function get($name) 51 | { 52 | if (isset($this->services[$name])) { 53 | return $this->services[$name]; 54 | } 55 | 56 | if (isset(self::DI[$name])) { 57 | /** @var array $argServicesName */ 58 | $argServicesName = self::DI[$name]; 59 | $args = []; 60 | foreach ($argServicesName as $argServiceName) { 61 | $args[] = $this->get($argServiceName); 62 | } 63 | return $this->services[$name] = new $name(...$args); 64 | } 65 | 66 | return $this->services[$name] = new $name(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Intervals.php: -------------------------------------------------------------------------------- 1 | getArrayCopy()); 27 | $str .= '}'; 28 | return $str; 29 | } 30 | 31 | /** 32 | * Intervals constructor. 33 | * @param array $input 34 | */ 35 | public function __construct(array $input) 36 | { 37 | parent::__construct($input); 38 | self::loadDi(); 39 | } 40 | 41 | /** 42 | * Loads the service Di 43 | * @return Di 44 | */ 45 | private static function loadDi(): Di 46 | { 47 | if (!self::$di) { 48 | self::$di = new Di(); 49 | } 50 | 51 | return self::$di; 52 | } 53 | 54 | /** 55 | * Excludes this interval from another one. Exp 56 | * 57 | * |________________________________________________________________________________| 58 | * 59 | * - 60 | * |_________________| |_________________| 61 | * 62 | * = 63 | * |___________| |___________________| |____________| 64 | * 65 | * @param Intervals $intervals 66 | * @return Intervals 67 | */ 68 | public function exclude(Intervals $intervals) : Intervals 69 | { 70 | /** @var Exclusion $operation */ 71 | $operation = self::$di->get(Di::OPERATION_INTERVALS_EXCLUSION); 72 | return $operation($this, $intervals); 73 | } 74 | 75 | /** 76 | * Creates a new Interval from expression 77 | * Exp Intervals::create(['[10, 26]', '[11, 13]') 78 | * @param array|string $expressions 79 | * @return Intervals 80 | * @throws \InvalidArgumentException 81 | * @throws \UnexpectedValueException 82 | * @throws \RangeException 83 | * @throws \ErrorException 84 | */ 85 | public static function create(array $expressions) : Intervals 86 | { 87 | /** @var IntervalsParser $parser */ 88 | $parser = self::loadDi()->get(Di::PARSER_INTERVALS); 89 | return $parser->parse($expressions); 90 | } 91 | 92 | /** 93 | * @param callable $callable 94 | * @return Intervals 95 | */ 96 | public function filter(callable $callable): Intervals 97 | { 98 | return new Intervals(\array_values(\array_filter($this->getArrayCopy(), $callable))); 99 | } 100 | 101 | /** 102 | * @param callable $callable 103 | * @return Intervals 104 | */ 105 | public function map(callable $callable): Intervals 106 | { 107 | return new Intervals(\array_map($callable, $this->getArrayCopy())); 108 | } 109 | 110 | /** 111 | * @param callable $callable 112 | * @return Intervals 113 | */ 114 | public function sort(callable $callable): Intervals 115 | { 116 | $arrayIntervals = $this->getArrayCopy(); 117 | \usort($arrayIntervals, $callable); 118 | return new Intervals($arrayIntervals); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/AfterTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/BeforeTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/EndingTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/StartingTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/EquilityTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/NeighborhoodAfterTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/NeighborhoodBeforeTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 85 | $this->assertInternalType('bool', $result); 86 | $this->assertSame($expected, $result); 87 | } 88 | 89 | public function tearDown(): void 90 | { 91 | m::close(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/unit/IntervalsTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, (string)$intervals); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function create() 47 | { 48 | $intervals = Intervals::create(['[10, 15]']); 49 | $this->assertSame(10, $intervals[0]->getStart()->getValue()); 50 | $this->assertSame(15, $intervals[0]->getEnd()->getValue()); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function filter() 57 | { 58 | $intervals = Intervals::create(['[10, 15]', '[10, 11]']); 59 | $filtered = $intervals->filter(function (Interval $interval) { 60 | return $interval->getEnd()->getValue() === 11; 61 | }); 62 | self::assertCount(1, $filtered); 63 | self::assertSame(10, $filtered[0]->getStart()->getValue()); 64 | self::assertSame(11, $filtered[0]->getEnd()->getValue()); 65 | } 66 | 67 | /** 68 | * @test 69 | */ 70 | public function map() 71 | { 72 | $intervals = Intervals::create(['[10, 15]', '[10, 11]']); 73 | $filtered = $intervals->map(function (Interval $interval) { 74 | return new Interval(0, $interval->getEnd()); 75 | }); 76 | 77 | self::assertCount(2, $filtered); 78 | self::assertSame(0, $filtered[0]->getStart()->getValue()); 79 | self::assertSame(15, $filtered[0]->getEnd()->getValue()); 80 | self::assertSame(0, $filtered[1]->getStart()->getValue()); 81 | self::assertSame(11, $filtered[1]->getEnd()->getValue()); 82 | } 83 | 84 | /** 85 | * @test 86 | */ 87 | public function sort() 88 | { 89 | $intervals = Intervals::create(['[12, 15]', '[10, 11]']); 90 | $filtered = $intervals->sort(function (Interval $first, Interval $second) { 91 | return $first->getStart() <=> $second->getEnd(); 92 | }); 93 | 94 | self::assertCount(2, $filtered); 95 | self::assertSame(10, $filtered[0]->getStart()->getValue()); 96 | self::assertSame(11, $filtered[0]->getEnd()->getValue()); 97 | self::assertSame(12, $filtered[1]->getStart()->getValue()); 98 | self::assertSame(15, $filtered[1]->getEnd()->getValue()); 99 | } 100 | 101 | /** 102 | * @test 103 | */ 104 | public function exclude() 105 | { 106 | $intervals = new Intervals([ 107 | new Interval(1, 10), 108 | new Interval(16, 20), 109 | ]); 110 | 111 | $results = $intervals->exclude(new Intervals([ 112 | new Interval(2, 7), 113 | new Interval(17, 18), 114 | ])); 115 | 116 | $this->assertInstanceOf(Intervals::class, $results); 117 | $this->assertCount(4, $results); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/unit/Operation/Interval/IntersectionTest.php: -------------------------------------------------------------------------------- 1 | assertNull($interval); 82 | } else { 83 | $this->assertInstanceOf(\Interval\Interval::class, $interval); 84 | $this->assertSame($expected[0], $interval->getStart()->getValue()); 85 | $this->assertSame($expected[1], $interval->getEnd()->getValue()); 86 | $this->assertTrue($interval->getStart()->isLeft()); 87 | $this->assertTrue($interval->getEnd()->isRight()); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Boundary/BoundaryAbstract.php: -------------------------------------------------------------------------------- 1 | value = $value; 36 | $this->left = $isLeft; 37 | $this->open = $isOpen; 38 | } 39 | 40 | /** 41 | * @param BoundaryAbstract $comparable 42 | * @return int 43 | */ 44 | abstract public function compare(BoundaryAbstract $comparable) : int; 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | abstract public function toComparable(); 50 | 51 | /** 52 | * @param BoundaryAbstract $comparable 53 | * @return bool 54 | */ 55 | public function equalTo(BoundaryAbstract $comparable): bool 56 | { 57 | return $this->compare($comparable) === 0; 58 | } 59 | 60 | /** 61 | * @param BoundaryAbstract $comparable 62 | * @return bool 63 | */ 64 | public function greaterThan(BoundaryAbstract $comparable): bool 65 | { 66 | return $this->compare($comparable) === 1; 67 | } 68 | 69 | /** 70 | * @param BoundaryAbstract $comparable 71 | * @return bool 72 | */ 73 | public function lessThan(BoundaryAbstract $comparable): bool 74 | { 75 | return $this->compare($comparable) === -1; 76 | } 77 | 78 | /** 79 | * @param BoundaryAbstract $comparable 80 | * @return bool 81 | */ 82 | public function greaterThanOrEqualTo(BoundaryAbstract $comparable): bool 83 | { 84 | return \in_array($this->compare($comparable), [0, 1], true); 85 | } 86 | 87 | /** 88 | * @param BoundaryAbstract $comparable 89 | * @return bool 90 | */ 91 | public function lessThanOrEqualTo(BoundaryAbstract $comparable): bool 92 | { 93 | return \in_array($this->compare($comparable), [0, -1], true); 94 | } 95 | 96 | /** 97 | * @return mixed 98 | */ 99 | public function getValue() 100 | { 101 | return $this->value; 102 | } 103 | 104 | /** 105 | * @return bool 106 | */ 107 | public function isLeft(): bool 108 | { 109 | return $this->left; 110 | } 111 | 112 | /** 113 | * @return bool 114 | */ 115 | public function isRight(): bool 116 | { 117 | return !$this->isLeft(); 118 | } 119 | 120 | /** 121 | * @return bool 122 | */ 123 | public function isOpen(): bool 124 | { 125 | return $this->open; 126 | } 127 | 128 | /** 129 | * @return bool 130 | */ 131 | public function isClosed(): bool 132 | { 133 | return !$this->isOpen(); 134 | } 135 | 136 | /** 137 | * @param $string 138 | * @return string 139 | */ 140 | protected function applyBoundarySymbol($string): string 141 | { 142 | if ($this->isLeft()) { 143 | return ($this->isOpen() ? ']' : '[') . $string; 144 | } 145 | 146 | return $string . ($this->isOpen() ? '[' : ']'); 147 | } 148 | 149 | /** 150 | * @return BoundaryAbstract 151 | */ 152 | public function flip(): BoundaryAbstract 153 | { 154 | return new static($this->getValue(), $this->isRight(), $this->isClosed()); 155 | } 156 | 157 | /** 158 | * @return BoundaryAbstract 159 | */ 160 | public function changeSide(): BoundaryAbstract 161 | { 162 | return new static($this->getValue(), $this->isRight(), $this->isOpen()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/unit/Operation/Interval/UnionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Interval\Intervals::class, $intervals); 81 | $data = [] ; 82 | /** @var Interval $interval */ 83 | foreach ($intervals as $interval) { 84 | $this->assertInstanceOf('\Interval\Interval', $interval); 85 | $data[] = [$interval->getStart()->getValue(), $interval->getEnd()->getValue()]; 86 | } 87 | 88 | $this->assertSame($expected, $data); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/unit/Operation/Intervals/ExclusionTest.php: -------------------------------------------------------------------------------- 1 | compute($intervals, $intervalsToExclude); 125 | 126 | $this->assertCount(count($expected), $results); 127 | $count = count($results); 128 | for ($i = 0; $i < $count; $i++) { 129 | $interval = $results[$i]; 130 | $this->assertInstanceOf(Interval::class, $interval); 131 | $this->assertSame($interval->getStart()->getValue()->format('H:i'), $expected[$i][0]); 132 | $this->assertSame($interval->getEnd()->getValue()->format('H:i'), $expected[$i][1]); 133 | $this->assertTrue($interval->getStart()->isLeft()); 134 | $this->assertTrue($interval->getEnd()->isRight()); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/InclusionTest.php: -------------------------------------------------------------------------------- 1 | assert(new Interval($firstStart, $firstEnd), new Interval($secondStart, $secondEnd)); 95 | $this->assertInternalType('bool', $result); 96 | $this->assertSame($expected, $result); 97 | } 98 | 99 | public function testOpenBoundary() 100 | { 101 | $first = new Interval(10, 20, true, true); 102 | $this->assertSame(false, $first->includes(new Interval(10, 11, false, false))); 103 | $this->assertSame(true, $first->includes(new Interval(10, 11, true, false))); 104 | $this->assertSame(true, $first->includes(new Interval(19, 20, false, true))); 105 | $this->assertSame(false, $first->includes(new Interval(19, 20, false, false))); 106 | } 107 | 108 | public function tearDown(): void 109 | { 110 | m::close(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/unit/Parser/IntervalParserTest.php: -------------------------------------------------------------------------------- 1 | parse($expression); 43 | $this->assertInstanceOf(Interval::class, $interval); 44 | if ($start instanceof \DateTimeInterface) { 45 | self::assertInstanceOf(DateTime::class, $interval->getStart()); 46 | self::assertInstanceOf(DateTime::class, $interval->getEnd()); 47 | self::assertInstanceOf(\DateTimeInterface::class, $interval->getStart()->getValue()); 48 | self::assertInstanceOf(\DateTimeInterface::class, $interval->getEnd()->getValue()); 49 | $this->assertEquals($start, $interval->getStart()->getValue()); 50 | $this->assertEquals($end, $interval->getEnd()->getValue()); 51 | } else { 52 | $this->assertSame($start, $interval->getStart()->getValue()); 53 | $this->assertSame($end, $interval->getEnd()->getValue()); 54 | } 55 | 56 | $this->assertSame(true, $interval->getStart()->isLeft()); 57 | $this->assertSame(true, $interval->getStart()->isClosed()); 58 | 59 | $this->assertSame(true, $interval->getEnd()->isRight()); 60 | $this->assertSame(true, $interval->getEnd()->isClosed()); 61 | } 62 | 63 | public function parseExceptionProvider() 64 | { 65 | return [ 66 | [''], 67 | ['invalid'], 68 | ['[]'], 69 | ['[,]'], 70 | ['[,'], 71 | [',]'], 72 | ['[1]'], 73 | ['[1 2]'], 74 | ['[1 2'], 75 | ['1 2]'], 76 | ['1,2)'], 77 | ['(1,2)'], 78 | ['(1,2'], 79 | ['[1,2'], 80 | ['1,2]'], 81 | ]; 82 | } 83 | 84 | /** 85 | * @test 86 | * @expectedException \Exception 87 | * @dataProvider parseExceptionProvider 88 | * @param mixed $expression 89 | */ 90 | public function parseException($expression) 91 | { 92 | $parser = new IntervalParser(); 93 | $parser->parse($expression); 94 | } 95 | 96 | public function parseOpenProvider() 97 | { 98 | return [ 99 | [']1,3[', 1, 3, true, true], 100 | ]; 101 | } 102 | 103 | /** 104 | * @test 105 | * @dataProvider parseOpenProvider 106 | * @param mixed $expression 107 | * @param mixed $start 108 | * @param mixed $end 109 | * @param mixed $isOpenStart 110 | * @param mixed $isOpenEnd 111 | */ 112 | public function parseOpen($expression, $start, $end, $isOpenStart, $isOpenEnd) 113 | { 114 | $parser = new IntervalParser(); 115 | 116 | /** @var Interval $interval */ 117 | $interval = $parser->parse($expression); 118 | $this->assertInstanceOf(Interval::class, $interval); 119 | if ($start instanceof \DateTimeInterface) { 120 | self::assertInstanceOf(\DateTimeInterface::class, $interval->getStart()->getValue()); 121 | $this->assertEquals($start, $interval->getStart()->getValue()); 122 | $this->assertEquals($end, $interval->getEnd()->getValue()); 123 | } else { 124 | $this->assertSame($start, $interval->getStart()->getValue()); 125 | $this->assertSame($end, $interval->getEnd()->getValue()); 126 | } 127 | 128 | $this->assertSame(true, $interval->getStart()->isLeft()); 129 | $this->assertSame($isOpenStart, $interval->getStart()->isOpen()); 130 | 131 | $this->assertSame(true, $interval->getEnd()->isRight()); 132 | $this->assertSame($isOpenEnd, $interval->getEnd()->isOpen()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Parser/IntervalParser.php: -------------------------------------------------------------------------------- 1 | parseStartTerm($startTerm); 42 | $endTerm = $this->parseEndTerm($endTerm); 43 | 44 | return new Interval( 45 | $startTerm, 46 | $endTerm 47 | ); 48 | } 49 | 50 | /** 51 | * Parses the start term 52 | * @param string $startTerm 53 | * @return float|int|string 54 | * @throws \InvalidArgumentException 55 | * @throws \ErrorException 56 | */ 57 | private function parseStartTerm(string $startTerm) 58 | { 59 | if ('' === $startTerm) { 60 | throw new \ErrorException('Parse interval expression'); 61 | } 62 | 63 | $startInclusion = $startTerm[0]; 64 | 65 | if (!\in_array($startInclusion, [self::LEFT, self::RIGHT], true)) { 66 | throw new \ErrorException('Parse interval expression'); 67 | } 68 | 69 | $isOpen = $startInclusion === self::LEFT; 70 | $startValue = \substr($startTerm, 1); 71 | return $this->parseValue($startValue, true, $isOpen); 72 | } 73 | 74 | /** 75 | * Pareses the end term 76 | * @param string $endTerm 77 | * @return float|int|string 78 | * @throws \InvalidArgumentException 79 | * @throws \ErrorException 80 | */ 81 | private function parseEndTerm(string $endTerm) 82 | { 83 | if ('' === $endTerm) { 84 | throw new \ErrorException('Parse interval expression'); 85 | } 86 | 87 | $endInclusion = \substr($endTerm, -1); 88 | 89 | if (!\in_array($endInclusion, [self::LEFT, self::RIGHT], true)) { 90 | throw new \ErrorException('Parse interval expression'); 91 | } 92 | 93 | $isOpen = $endInclusion === self::RIGHT; 94 | $endValue = \substr($endTerm, 0, -1); 95 | return $this->parseValue($endValue, false, $isOpen); 96 | } 97 | 98 | /** 99 | * Cast a value to its expected type 100 | * @param mixed $value 101 | * @param bool $isLeft 102 | * @param bool $isOpen 103 | * @return float|int|string 104 | * @throws \InvalidArgumentException 105 | */ 106 | private function parseValue($value, bool $isLeft, bool $isOpen) 107 | { 108 | if ($this->isInt($value)) { 109 | return new Integer((int)$value, $isLeft, $isOpen); 110 | } 111 | 112 | if ($this->isInfinity($value)) { 113 | $value = '-INF' === $value ? -\INF : \INF; 114 | return new Infinity($value, $isLeft, $isOpen); 115 | } 116 | 117 | if ($this->isFloat($value)) { 118 | return new Real((float)$value, $isLeft, $isOpen); 119 | } 120 | 121 | if ($this->isDate($value)) { 122 | $value = \DateTimeImmutable::createFromFormat('U', (string)\strtotime($value)); 123 | $value = $value->setTimezone(new \DateTimeZone(\date_default_timezone_get())); 124 | return new DateTime($value, $isLeft, $isOpen); 125 | } 126 | 127 | throw new \InvalidArgumentException('Unexpected $value type'); 128 | } 129 | 130 | /** 131 | * Returns true if the value is an integer 132 | * @param $value 133 | * @return bool 134 | */ 135 | private function isInt(string $value): bool 136 | { 137 | return \is_numeric($value) && (float)\round($value, 0) === (float)$value; 138 | } 139 | 140 | /** 141 | * Returns true if the value is infinite 142 | * @param string $value 143 | * @return bool 144 | */ 145 | private function isInfinity(string $value): bool 146 | { 147 | return false !== \strpos($value, 'INF'); 148 | } 149 | 150 | /** 151 | * Returns true if the value is a float 152 | * @param string $value 153 | * @return bool 154 | */ 155 | private function isFloat(string $value): bool 156 | { 157 | return \is_numeric($value) && !$this->isInt($value); 158 | } 159 | 160 | /** 161 | * @param $value 162 | * @return bool 163 | */ 164 | private function isDate($value): bool 165 | { 166 | return true === (bool)\strtotime($value); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis](https://img.shields.io/travis/Kirouane/interval/master.svg)](http://travis-ci.org/Kirouane/interval) 2 | [![Coverage Status](https://coveralls.io/repos/github/Kirouane/interval/badge.svg)](https://coveralls.io/github/Kirouane/interval?branch=develop) 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/783c18637e574894bc6a37e1c5c75e93)](https://www.codacy.com/app/Kirouane/interval?utm_source=github.com&utm_medium=referral&utm_content=Kirouane/interval&utm_campaign=Badge_Grade) 4 | [![Total Downloads](https://poser.pugx.org/kirouane/interval/downloads)](https://packagist.org/packages/kirouane/interval) 5 | [![Latest Stable Version](https://poser.pugx.org/kirouane/interval/v/stable)](https://packagist.org/packages/kirouane/interval) 6 | 7 | Interval 8 | ====== 9 | 10 | This library provides some tools to handle intervals. For instance, you can compute the union or intersection of two intervals. 11 | 12 | Use cases 13 | ------ 14 | * Availabilities calculation. 15 | * Scheduling/calendar/planning. 16 | * Mathematics interval computation with open/closed boundaries 17 | * etc 18 | 19 | Features 20 | ------ 21 | 22 | * It computes some operations between two **intervals**: union, intersection and exclusion. 23 | * It computes some operations between two **sets of intervals**: exclusion for now. 24 | * It handles several types of boundaries : float, **\DateTime** and integer. 25 | * It handles **infinity** type as boundary. 26 | * Ability to **combine** infinity with \DateTime and other types. 27 | * filter, sort, map. 28 | * Immutability. 29 | * Chain operations. 30 | 31 | Quality 32 | ------- 33 | 34 | * Code coverage [![Coverage Status](https://coveralls.io/repos/github/Kirouane/interval/badge.svg)](https://coveralls.io/github/Kirouane/interval?branch=develop) 35 | * Mutation test : Code coverage more than **90%** 36 | * Takes care of **performance** and **memory usage** 37 | * PSR1/PSR2, Code Smell 38 | 39 | 40 | Install 41 | ------ 42 | 43 | `composer require kirouane/interval` 44 | 45 | 46 | 47 | Basic usage 48 | --------- 49 | 50 | Let's assume an interval [20, 40]. 51 | We instantiate a new Interval object . 52 | 53 | ```php 54 | $interval = new Interval(20, 40);// [20, 40]; 55 | ``` 56 | 57 | or 58 | 59 | ```php 60 | $interval = Interval::create('[20,40]');// [20, 40]; 61 | ``` 62 | 63 | 64 | We can do some operations like : 65 | * Intersection : 66 | 67 | ```php 68 | echo $interval->intersect(new Interval(30, 60)); // [30, 40]; 69 | ``` 70 | 71 | * Union : 72 | 73 | ```php 74 | echo $interval->union(new Interval(30, 60)); // {[20, 60]}; 75 | ``` 76 | 77 | or 78 | 79 | ```php 80 | echo $interval->union(new Interval(60, 100)); // {[20, 40], [60, 100]}; 81 | ``` 82 | 83 | * Exclusion : 84 | 85 | ```php 86 | echo $interval->exclude(new Interval(30, 60)); // {[20, 30[}; 87 | ``` 88 | 89 | or 90 | 91 | ```php 92 | echo $interval->exclude(new Interval(30, 35)); // {[20, 30[, ]35, 40]}; 93 | ``` 94 | 95 | We can compare two intervals as well: 96 | * Overlapping test : 97 | 98 | ```php 99 | echo $interval->overlaps(new Interval(30, 60)); // true; 100 | ``` 101 | 102 | * Inclusion test : 103 | 104 | ```php 105 | echo $interval->includes(new Interval(30, 60)); // false; 106 | ``` 107 | Use DateTimeInterface as boundary 108 | --------- 109 | 110 | ```php 111 | $interval = new Interval(new \DateTime('2016-01-01'), new \DateTime('2016-01-10')); 112 | // [2016-01-01T00:00:00+01:00, 2016-01-10T00:00:00+01:00]; 113 | ``` 114 | 115 | * Union : 116 | 117 | ```php 118 | echo $interval->union(Interval::create('[2016-01-10, 2016-01-15]')); 119 | // {[2016-01-01T00:00:00+01:00, 2016-01-15T00:00:00+01:00]}; 120 | ``` 121 | 122 | Use Infinity as boundary 123 | --------- 124 | 125 | ```php 126 | $interval = new Interval(-INF, INF);// ]-∞, +∞[; 127 | ``` 128 | 129 | * Exclusion : 130 | 131 | ```php 132 | echo $interval->exclude(Interval::create('[2016-01-10, 2016-01-15]')); 133 | // {]-∞, 2016-01-10T00:00:00+01:00[, ]2016-01-15T00:00:00+01:00, +∞[}; 134 | ``` 135 | 136 | Operations on sets (arrays) of intervals 137 | --------- 138 | 139 | ```php 140 | $intervals = Intervals::create(['[0,5]', '[8,12]']);// {[0, 5], [8, 12]}; 141 | ``` 142 | 143 | * Exclusion : 144 | 145 | ```php 146 | echo $intervals->exclude(Intervals::create(['[3,10]'])); // {[0, 3[, ]10, 12]}; 147 | ``` 148 | 149 | Chaining 150 | --------- 151 | 152 | ```php 153 | 154 | $result = Interval 155 | ::create('[10, 20]') 156 | ->intersect(new Interval(11, 30)) 157 | ->union(new Interval(15, INF)) 158 | ->exclude(Intervals::create(['[18, 20]', '[25, 30]', '[32, 35]', '[12, 13]'])) 159 | ->sort(function (Interval $first, Interval $second) { 160 | return $first->getStart()->getValue() <=> $second->getStart()->getValue(); 161 | }) 162 | ->map(function (Interval $interval) { 163 | return new Interval( 164 | $interval->getStart()->getValue() ** 2, 165 | $interval->getEnd()->getValue() ** 2 166 | ); 167 | }) 168 | ->filter(function (Interval $interval) { 169 | return $interval->getEnd()->getValue() > 170; 170 | }); 171 | 172 | // {[169, 324], [400, 625], [900, 1024], [1225, +∞[}; 173 | 174 | echo $result; 175 | ``` 176 | 177 | Advanced usage 178 | --------- 179 | 180 | You can create intervals with **open** boundaries : 181 | 182 | ```php 183 | 184 | $result = Intervals 185 | ::create([']10, +INF[']) 186 | ->exclude(Intervals::create([']18, 20]', ']25, 30[', '[32, 35]', ']12, 13]'])); 187 | 188 | // {]10, 12], ]13, 18], ]20, 25], [30, 32[, ]35, +∞[} 189 | 190 | 191 | ``` 192 | 193 | 194 | 195 | Contributing 196 | ---------------------- 197 | 198 | You are very welcomed to contribute to this Library! 199 | 200 | * Clone 201 | `git clone https://github.com/Kirouane/interval.git` 202 | 203 | * Install 204 | `composer install` 205 | or 206 | `make install` (with docker and docker-compose) 207 | 208 | * Test 209 | `vendor/bin/phpunit` 210 | 211 | * Build 212 | `vendor/bin/grumphp run` 213 | 214 | -------------------------------------------------------------------------------- /tests/unit/Rule/Interval/OverlappingTest.php: -------------------------------------------------------------------------------- 1 | assert( 129 | new Interval($firstStart, $firstEnd, $firstLeftOpen, $firstRightOpen), 130 | new Interval($secondStart, $secondEnd, $secondLeftOpen, $secondRightOpen) 131 | ); 132 | $this->assertIsBool($result); 133 | $this->assertSame($expected, $result); 134 | } 135 | 136 | public function tearDown(): void 137 | { 138 | m::close(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/unit/IntervalTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Intervals::class, $interval->union(new \Interval\Interval(2, 3))); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function intersection() 50 | { 51 | $interval = new \Interval\Interval(1, 4); 52 | $this->assertInstanceOf(Interval::class, $interval->intersect(new \Interval\Interval(3, 5))); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function exclusion() 59 | { 60 | $interval = new \Interval\Interval(1, 4); 61 | $this->assertInstanceOf(Intervals::class, $interval->exclude(new \Interval\Interval(3, 5))); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function includes() 68 | { 69 | $interval = new \Interval\Interval(1, 4); 70 | $this->assertInternalType('bool', $interval->includes(new \Interval\Interval(3, 5))); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function overlaps() 77 | { 78 | $interval = new \Interval\Interval(1, 4); 79 | $this->assertInternalType('bool', $interval->overlaps(new \Interval\Interval(3, 5))); 80 | } 81 | 82 | /** 83 | * @test 84 | */ 85 | public function isNeighborBeforeOf() 86 | { 87 | $interval = new \Interval\Interval(1, 4); 88 | $this->assertInternalType('bool', $interval->isNeighborBefore(new \Interval\Interval(3, 5))); 89 | } 90 | 91 | /** 92 | * @test 93 | */ 94 | public function isNeighborAfterOf() 95 | { 96 | $interval = new \Interval\Interval(1, 4); 97 | $this->assertInternalType('bool', $interval->isNeighborAfter(new \Interval\Interval(3, 5))); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function isBeforeOf() 104 | { 105 | $interval = new \Interval\Interval(1, 4); 106 | $this->assertInternalType('bool', $interval->isBefore(new \Interval\Interval(3, 5))); 107 | } 108 | 109 | /** 110 | * @test 111 | */ 112 | public function isAfter() 113 | { 114 | $interval = new \Interval\Interval(1, 4); 115 | $this->assertInternalType('bool', $interval->isAfter(new \Interval\Interval(3, 5))); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function starts() 122 | { 123 | $interval = new \Interval\Interval(1, 4); 124 | $this->assertInternalType('bool', $interval->starts(new \Interval\Interval(3, 5))); 125 | } 126 | 127 | /** 128 | * @test 129 | */ 130 | public function ends() 131 | { 132 | $interval = new \Interval\Interval(1, 4); 133 | $this->assertInternalType('bool', $interval->ends(new \Interval\Interval(3, 5))); 134 | } 135 | 136 | /** 137 | * @test 138 | */ 139 | public function equals() 140 | { 141 | $interval = new \Interval\Interval(1, 4); 142 | $this->assertInternalType('bool', $interval->equals(new \Interval\Interval(3, 5))); 143 | } 144 | 145 | public function toStringProvider() 146 | { 147 | return [ 148 | [1, 2, '[1, 2]'], 149 | [1.2, 2.2, '[1.2, 2.2]'], 150 | [new \DateTime('2016-01-01', new \DateTimeZone('UTC')), new \DateTime('2016-01-02', new \DateTimeZone('UTC')), '[2016-01-01T00:00:00+00:00, 2016-01-02T00:00:00+00:00]'], 151 | [-INF, +INF, ']-∞, +∞['], 152 | [-INF, 1, ']-∞, 1]'], 153 | [1, +INF, '[1, +∞['], 154 | [null, 1, ']-∞, 1]'], 155 | [1, null, '[1, +∞['], 156 | ]; 157 | } 158 | 159 | /** 160 | * @test 161 | * @dataProvider toStringProvider 162 | * @param mixed $start 163 | * @param mixed $end 164 | * @param mixed $expected 165 | */ 166 | public function toStringTest($start, $end, $expected) 167 | { 168 | $interval = new \Interval\Interval($start, $end); 169 | $this->assertSame($expected, $interval->__toString()); 170 | } 171 | 172 | public function toComparableProvider() 173 | { 174 | return [ 175 | [1, 1], 176 | [1.1, 1.1], 177 | ['1', '1'], 178 | ['a', 'a'], 179 | [true, true], 180 | [false, false], 181 | [new \DateTime('2016-01-01 10:00:00', new \DateTimeZone('UTC')), 1451642400], 182 | [INF, INF], 183 | [-INF, -INF], 184 | ]; 185 | } 186 | 187 | /** 188 | * @test 189 | * @dataProvider toComparableProvider 190 | * @param mixed $boundary 191 | * @param mixed $expected 192 | */ 193 | public function toComparable($boundary, $expected) 194 | { 195 | $this->assertSame($expected, Interval::toComparable($boundary)); 196 | } 197 | 198 | public function toComparableExceptionProvider() 199 | { 200 | return [ 201 | [[]], 202 | [new \stdClass()], 203 | [null], 204 | ]; 205 | } 206 | 207 | /** 208 | * @test 209 | * @dataProvider toComparableExceptionProvider 210 | * @expectedException UnexpectedValueException 211 | * @param mixed $boundary 212 | */ 213 | public function toComparableException($boundary) 214 | { 215 | Interval::toComparable($boundary); 216 | } 217 | 218 | /** 219 | * @test 220 | */ 221 | public function create() 222 | { 223 | $interval = Interval::create('[10, 15]'); 224 | $this->assertSame(10, $interval->getStart()->getValue()); 225 | $this->assertSame(15, $interval->getEnd()->getValue()); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/unit/Boundary/DateTimeTest.php: -------------------------------------------------------------------------------- 1 | getArguments($symbole); 74 | $argumentsToCompare = $this->getArguments($comparedSymbole); 75 | 76 | $bounday = new DateTime(...$arguments); 77 | self::assertSame($expected, $bounday->compare(new DateTime(...$argumentsToCompare)) === 0); 78 | } 79 | 80 | private function getArguments($symoble) 81 | { 82 | $value = (int)str_replace(['[', ']'], '', $symoble); 83 | $value = new \DateTime('2017-11-01 10:00:' . $value); 84 | $isLeft = substr($symoble, 0, 1) === '[' || substr($symoble, 0, 1) === ']' ; 85 | $isOpen = ($isLeft && substr($symoble, 0, 1) === ']') || (!$isLeft && substr($symoble, -1) === '['); 86 | 87 | return [$value, $isLeft, $isOpen]; 88 | } 89 | 90 | public function compareGreaterProvider() 91 | { 92 | return [ 93 | [']10', ']11', false], 94 | [']10', '[11', false], 95 | [']10', '11[', true], 96 | [']10', '11]', false], 97 | 98 | ['[10', ']11', false], 99 | ['[10', '[11', false], 100 | ['[10', '11[', false], 101 | ['[10', '11]', false], 102 | 103 | ['10[', ']11', false], 104 | ['10[', '[11', false], 105 | ['10[', '11[', false], 106 | ['10[', '11]', false], 107 | 108 | ['10]', ']11', false], 109 | ['10]', '[11', false], 110 | ['10]', '11[', false], 111 | ['10]', '11]', false], 112 | 113 | //------------------- 114 | 115 | [']10', ']10', false], 116 | [']10', '[10', true], 117 | [']10', '10[', true], 118 | [']10', '10]', true], 119 | 120 | ['[10', ']10', false], 121 | ['[10', '[10', false], 122 | ['[10', '10[', true], 123 | ['[10', '10]', false], 124 | 125 | ['10[', ']10', false], 126 | ['10[', '[10', false], 127 | ['10[', '10[', false], 128 | ['10[', '10]', false], 129 | 130 | ['10]', ']10', false], 131 | ['10]', '[10', false], 132 | ['10]', '10[', true], 133 | ['10]', '10]', false], 134 | ]; 135 | } 136 | 137 | /** 138 | * @dataProvider compareGreaterProvider 139 | * @test 140 | * @param mixed $symbole 141 | * @param mixed $comparedSymbole 142 | * @param mixed $expected 143 | */ 144 | public function compareGreater($symbole, $comparedSymbole, $expected) 145 | { 146 | $arguments = $this->getArguments($symbole); 147 | $argumentsToCompare = $this->getArguments($comparedSymbole); 148 | 149 | $bounday = new DateTime(...$arguments); 150 | $bounday->compare(new DateTime(...$argumentsToCompare)); 151 | self::assertSame($expected, $bounday->compare(new DateTime(...$argumentsToCompare)) === 1); 152 | } 153 | 154 | public function compareLessProvider() 155 | { 156 | return [ 157 | [']10', ']11', true], 158 | [']10', '[11', false], 159 | [']10', '11[', false], 160 | [']10', '11]', false], 161 | 162 | ['[10', ']11', true], 163 | ['[10', '[11', true], 164 | ['[10', '11[', false], 165 | ['[10', '11]', true], 166 | 167 | ['10[', ']11', true], 168 | ['10[', '[11', true], 169 | ['10[', '11[', true], 170 | ['10[', '11]', true], 171 | 172 | ['10]', ']11', true], 173 | ['10]', '[11', true], 174 | ['10]', '11[', false], 175 | ['10]', '11]', true], 176 | 177 | //------------------- 178 | 179 | [']10', ']10', false], 180 | [']10', '[10', false], 181 | [']10', '10[', false], 182 | [']10', '10]', false], 183 | 184 | ['[10', ']10', true], 185 | ['[10', '[10', false], 186 | ['[10', '10[', false], 187 | ['[10', '10]', false], 188 | 189 | ['10[', ']10', true], 190 | ['10[', '[10', true], 191 | ['10[', '10[', false], 192 | ['10[', '10]', true], 193 | 194 | ['10]', ']10', true], 195 | ['10]', '[10', false], 196 | ['10]', '10[', false], 197 | ['10]', '10]', false], 198 | ]; 199 | } 200 | 201 | /** 202 | * @dataProvider compareLessProvider 203 | * @test 204 | * @param mixed $symbole 205 | * @param mixed $comparedSymbole 206 | * @param mixed $expected 207 | */ 208 | public function compareLess($symbole, $comparedSymbole, $expected) 209 | { 210 | $arguments = $this->getArguments($symbole); 211 | $argumentsToCompare = $this->getArguments($comparedSymbole); 212 | 213 | $bounday = new DateTime(...$arguments); 214 | self::assertSame($expected, $bounday->compare(new DateTime(...$argumentsToCompare)) === -1); 215 | } 216 | 217 | /** 218 | * @test 219 | */ 220 | public function toStringTest() 221 | { 222 | $this->assertSame('[2010-10-10T00:00:00+00:00', (string)new DateTime(new \DateTime('2010-10-10', new \DateTimeZone('UTC')), true)); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/unit/Operation/Interval/ExclusionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Intervals::class, $intervals); 136 | 137 | $this->assertSame($expected, (string)$intervals); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/unit/Boundary/RealTest.php: -------------------------------------------------------------------------------- 1 | getArguments($symbole); 74 | $argumentsToCompare = $this->getArguments($comparedSymbole); 75 | 76 | $bounday = new Real(...$arguments); 77 | self::assertSame($expected, $bounday->compare(new Real(...$argumentsToCompare)) === 0); 78 | } 79 | 80 | private function getArguments($symoble) 81 | { 82 | $value = (float)str_replace(['[', ']'], '', $symoble); 83 | $isLeft = substr($symoble, 0, 1) === '[' || substr($symoble, 0, 1) === ']' ; 84 | $isOpen = ($isLeft && substr($symoble, 0, 1) === ']') || (!$isLeft && substr($symoble, -1) === '['); 85 | 86 | return [$value, $isLeft, $isOpen]; 87 | } 88 | 89 | public function compareGreaterProvider() 90 | { 91 | return [ 92 | [']10.0', ']11.0', false], 93 | [']10.0', '[11.0', false], 94 | [']10.0', '11.0[', false], 95 | [']10.0', '11.0]', false], 96 | 97 | ['[10.0', ']11.0', false], 98 | ['[10.0', '[11.0', false], 99 | ['[10.0', '11.0[', false], 100 | ['[10.0', '11.0]', false], 101 | 102 | ['10.0[', ']11.0', false], 103 | ['10.0[', '[11.0', false], 104 | ['10.0[', '11.0[', false], 105 | ['10.0[', '11.0]', false], 106 | 107 | ['10.0]', ']11.0', false], 108 | ['10.0]', '[11.0', false], 109 | ['10.0]', '11.0[', false], 110 | ['10.0]', '11.0]', false], 111 | 112 | //------------------- 113 | 114 | [']10.0', ']10.0', false], 115 | [']10.0', '[10.0', true], 116 | [']10.0', '10.0[', true], 117 | [']10.0', '10.0]', true], 118 | 119 | ['[10.0', ']10.0', false], 120 | ['[10.0', '[10.0', false], 121 | ['[10.0', '10.0[', true], 122 | ['[10.0', '10.0]', false], 123 | 124 | ['10.0[', ']10.0', false], 125 | ['10.0[', '[10.0', false], 126 | ['10.0[', '10.0[', false], 127 | ['10.0[', '10.0]', false], 128 | 129 | ['10.0]', ']10.0', false], 130 | ['10.0]', '[10.0', false], 131 | ['10.0]', '10.0[', true], 132 | ['10.0]', '10.0]', false], 133 | ]; 134 | } 135 | 136 | /** 137 | * @dataProvider compareGreaterProvider 138 | * @test 139 | * @param mixed $symbole 140 | * @param mixed $comparedSymbole 141 | * @param mixed $expected 142 | */ 143 | public function compareGreater($symbole, $comparedSymbole, $expected) 144 | { 145 | $arguments = $this->getArguments($symbole); 146 | $argumentsToCompare = $this->getArguments($comparedSymbole); 147 | 148 | $bounday = new Real(...$arguments); 149 | self::assertSame($expected, $bounday->compare(new Real(...$argumentsToCompare)) === 1); 150 | } 151 | 152 | public function compareLessProvider() 153 | { 154 | return [ 155 | [']10.0', ']11.0', true], 156 | [']10.0', '[11.0', true], 157 | [']10.0', '11.0[', true], 158 | [']10.0', '11.0]', true], 159 | 160 | ['[10.0', ']11.0', true], 161 | ['[10.0', '[11.0', true], 162 | ['[10.0', '11.0[', true], 163 | ['[10.0', '11.0]', true], 164 | 165 | ['10.0[', ']11.0', true], 166 | ['10.0[', '[11.0', true], 167 | ['10.0[', '11.0[', true], 168 | ['10.0[', '11.0]', true], 169 | 170 | ['10.0]', ']11.0', true], 171 | ['10.0]', '[11.0', true], 172 | ['10.0]', '11.0[', true], 173 | ['10.0]', '11.0]', true], 174 | 175 | //------------------- 176 | 177 | [']10.0', ']10.0', false], 178 | [']10.0', '[10.0', false], 179 | [']10.0', '10.0[', false], 180 | [']10.0', '10.0]', false], 181 | 182 | ['[10.0', ']10.0', true], 183 | ['[10.0', '[10.0', false], 184 | ['[10.0', '10.0[', false], 185 | ['[10.0', '10.0]', false], 186 | 187 | ['10.0[', ']10.0', true], 188 | ['10.0[', '[10.0', true], 189 | ['10.0[', '10.0[', false], 190 | ['10.0[', '10.0]', true], 191 | 192 | ['10.0]', ']10.0', true], 193 | ['10.0]', '[10.0', false], 194 | ['10.0]', '10.0[', false], 195 | ['10.0]', '10.0]', false], 196 | ]; 197 | } 198 | 199 | /** 200 | * @dataProvider compareLessProvider 201 | * @test 202 | * @param mixed $symbole 203 | * @param mixed $comparedSymbole 204 | * @param mixed $expected 205 | */ 206 | public function compareLess($symbole, $comparedSymbole, $expected) 207 | { 208 | $arguments = $this->getArguments($symbole); 209 | $argumentsToCompare = $this->getArguments($comparedSymbole); 210 | 211 | $bounday = new Real(...$arguments); 212 | $bounday->compare(new Real(...$argumentsToCompare)); 213 | self::assertSame($expected, $bounday->compare(new Real(...$argumentsToCompare)) === -1); 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function toStringTest() 220 | { 221 | $this->assertSame('[10.5', (string)new Real(10.5, true)); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/unit/Boundary/InfinityTest.php: -------------------------------------------------------------------------------- 1 | getArguments($symbole); 74 | $argumentsToCompare = $this->getArguments($comparedSymbole); 75 | 76 | $bounday = new Infinity(...$arguments); 77 | self::assertSame($expected, $bounday->compare(new Infinity(...$argumentsToCompare)) === 0); 78 | } 79 | 80 | private function getArguments($symoble) 81 | { 82 | $value = (float)str_replace(['[', ']'], '', $symoble); 83 | $isLeft = substr($symoble, 0, 1) === '[' || substr($symoble, 0, 1) === ']' ; 84 | $isOpen = ($isLeft && substr($symoble, 0, 1) === ']') || (!$isLeft && substr($symoble, -1) === '['); 85 | 86 | return [$value, $isLeft, $isOpen]; 87 | } 88 | 89 | public function compareGreaterProvider() 90 | { 91 | return [ 92 | [']-INF', ']11.0', false], 93 | [']-INF', '[11.0', false], 94 | [']-INF', '11.0[', false], 95 | [']-INF', '11.0]', false], 96 | 97 | ['[-INF', ']11.0', false], 98 | ['[-INF', '[11.0', false], 99 | ['[-INF', '11.0[', false], 100 | ['[-INF', '11.0]', false], 101 | 102 | ['-INF[', ']11.0', false], 103 | ['-INF[', '[11.0', false], 104 | ['-INF[', '11.0[', false], 105 | ['-INF[', '11.0]', false], 106 | 107 | ['-INF]', ']11.0', false], 108 | ['-INF]', '[11.0', false], 109 | ['-INF]', '11.0[', false], 110 | ['-INF]', '11.0]', false], 111 | 112 | //------------------- 113 | 114 | [']-INF', ']-INF', false], 115 | [']-INF', '[-INF', false], 116 | [']-INF', '-INF[', false], 117 | [']-INF', '-INF]', false], 118 | 119 | ['[-INF', ']-INF', false], 120 | ['[-INF', '[-INF', false], 121 | ['[-INF', '-INF[', false], 122 | ['[-INF', '-INF]', false], 123 | 124 | ['-INF[', ']-INF', false], 125 | ['-INF[', '[-INF', false], 126 | ['-INF[', '-INF[', false], 127 | ['-INF[', '-INF]', false], 128 | 129 | ['-INF]', ']-INF', false], 130 | ['-INF]', '[-INF', false], 131 | ['-INF]', '-INF[', false], 132 | ['-INF]', '-INF]', false], 133 | ]; 134 | } 135 | 136 | /** 137 | * @dataProvider compareGreaterProvider 138 | * @test 139 | * @param mixed $symbole 140 | * @param mixed $comparedSymbole 141 | * @param mixed $expected 142 | */ 143 | public function compareGreater($symbole, $comparedSymbole, $expected) 144 | { 145 | $arguments = $this->getArguments($symbole); 146 | $argumentsToCompare = $this->getArguments($comparedSymbole); 147 | 148 | $bounday = new Infinity(...$arguments); 149 | self::assertSame($expected, $bounday->compare(new Infinity(...$argumentsToCompare)) === 1); 150 | } 151 | 152 | public function compareLessProvider() 153 | { 154 | return [ 155 | [']-INF', ']11.0', true], 156 | [']-INF', '[11.0', true], 157 | [']-INF', '11.0[', true], 158 | [']-INF', '11.0]', true], 159 | 160 | ['[-INF', ']11.0', true], 161 | ['[-INF', '[11.0', true], 162 | ['[-INF', '11.0[', true], 163 | ['[-INF', '11.0]', true], 164 | 165 | ['-INF[', ']11.0', true], 166 | ['-INF[', '[11.0', true], 167 | ['-INF[', '11.0[', true], 168 | ['-INF[', '11.0]', true], 169 | 170 | ['-INF]', ']11.0', true], 171 | ['-INF]', '[11.0', true], 172 | ['-INF]', '11.0[', true], 173 | ['-INF]', '11.0]', true], 174 | 175 | //------------------- 176 | 177 | [']-INF', ']-INF', false], 178 | [']-INF', '[-INF', false], 179 | [']-INF', '-INF[', false], 180 | [']-INF', '-INF]', false], 181 | 182 | ['[-INF', ']-INF', false], 183 | ['[-INF', '[-INF', false], 184 | ['[-INF', '-INF[', false], 185 | ['[-INF', '-INF]', false], 186 | 187 | ['-INF[', ']-INF', false], 188 | ['-INF[', '[-INF', false], 189 | ['-INF[', '-INF[', false], 190 | ['-INF[', '-INF]', false], 191 | 192 | ['-INF]', ']-INF', false], 193 | ['-INF]', '[-INF', false], 194 | ['-INF]', '-INF[', false], 195 | ['-INF]', '-INF]', false], 196 | ]; 197 | } 198 | 199 | /** 200 | * @dataProvider compareLessProvider 201 | * @test 202 | * @param mixed $symbole 203 | * @param mixed $comparedSymbole 204 | * @param mixed $expected 205 | */ 206 | public function compareLess($symbole, $comparedSymbole, $expected) 207 | { 208 | $arguments = $this->getArguments($symbole); 209 | $argumentsToCompare = $this->getArguments($comparedSymbole); 210 | 211 | $bounday = new Infinity(...$arguments); 212 | $bounday->compare(new Infinity(...$argumentsToCompare)); 213 | self::assertSame($expected, $bounday->compare(new Infinity(...$argumentsToCompare)) === -1); 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function toStringTest() 220 | { 221 | $this->assertSame('[-∞', (string)new Infinity(-INF, true)); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /scripts/generate-readme.php: -------------------------------------------------------------------------------- 1 | intersect(new Interval(30, 60)); // ' . $interval->intersect(Interval::create('[30,60]')) . '; 80 | ```' . "\n"; 81 | 82 | echo "\n"; 83 | echo "* Union : \n\n"; 84 | echo '```php 85 | echo $interval->union(new Interval(30, 60)); // ' . $interval->union(new Interval(30, 60)) . '; 86 | ```' . "\n\n"; 87 | echo "or\n\n"; 88 | echo '```php 89 | echo $interval->union(new Interval(60, 100)); // ' . $interval->union(new Interval(60, 100)) . '; 90 | ```' . "\n"; 91 | 92 | echo "\n"; 93 | echo "* Exclusion : \n\n"; 94 | echo '```php 95 | echo $interval->exclude(new Interval(30, 60)); // ' . $interval->exclude(new Interval(30, 60)) . '; 96 | ```' . "\n\n"; 97 | echo "or\n\n"; 98 | echo '```php 99 | echo $interval->exclude(new Interval(30, 35)); // ' . $interval->exclude(new Interval(30, 35)) . '; 100 | ```' . "\n"; 101 | echo "\n"; 102 | echo 'We can compare two intervals as well: '; 103 | echo "\n"; 104 | echo "* Overlapping test : \n\n"; 105 | 106 | echo '```php 107 | echo $interval->overlaps(new Interval(30, 60)); // ' . ($interval->overlaps(new Interval(30, 60)) ? 'true' : 'false') . '; 108 | ```' . "\n"; 109 | echo "\n"; 110 | echo "* Inclusion test : \n\n"; 111 | 112 | echo '```php 113 | echo $interval->includes(new Interval(30, 60)); // ' . ($interval->includes(new Interval(30, 60)) ? 'true' : 'false') . '; 114 | ```' . "\n"; 115 | 116 | echo "Use DateTimeInterface as boundary 117 | ---------\n\n"; 118 | $interval = new Interval(new \DateTime('2016-01-01'), new \DateTime('2016-01-10')); 119 | echo '```php 120 | $interval = new Interval(new \DateTime(\'2016-01-01\'), new \DateTime(\'2016-01-10\'));' . "\n" . '// ' . $interval . '; 121 | ```' . "\n"; 122 | echo "\n"; 123 | echo "* Union : \n\n"; 124 | echo '```php 125 | echo $interval->union(Interval::create(\'[2016-01-10, 2016-01-15]\')); ' . "\n" . '// ' . $interval->union(Interval::create('[2016-01-10, 2016-01-15]')) . '; 126 | ```' . "\n\n"; 127 | 128 | echo "Use Infinity as boundary 129 | ---------\n\n"; 130 | $interval = new Interval(-INF, INF); 131 | echo '```php 132 | $interval = new Interval(-INF, INF);// ' . $interval . '; 133 | ```' . "\n"; 134 | echo "\n"; 135 | echo "* Exclusion : \n\n"; 136 | echo '```php 137 | echo $interval->exclude(Interval::create(\'[2016-01-10, 2016-01-15]\')); ' . "\n" . '// ' . $interval->exclude(Interval::create('[2016-01-10, 2016-01-15]')) . '; 138 | ```' . "\n\n"; 139 | 140 | echo "Operations on sets (arrays) of intervals 141 | ---------\n\n"; 142 | $intervals = Intervals::create(['[0,5]', '[8,12]']); 143 | echo '```php 144 | $intervals = Intervals::create([\'[0,5]\', \'[8,12]\']);// ' . $intervals . '; 145 | ```' . "\n"; 146 | echo "\n"; 147 | echo "* Exclusion : \n\n"; 148 | echo '```php 149 | echo $intervals->exclude(Intervals::create([\'[3,10]\'])); // ' . $intervals->exclude(Intervals::create(['[3,10]'])) . '; 150 | ```' . "\n\n"; 151 | 152 | echo "Chaining 153 | ---------\n\n"; 154 | 155 | $result = Interval 156 | ::create('[10, 20]') 157 | ->intersect(new Interval(11, 30)) 158 | ->union(new Interval(15, INF)) 159 | ->exclude(Intervals::create(['[18, 20]', '[25, 30]', '[32, 35]', '[12, 13]'])) 160 | ->sort(function (Interval $first, Interval $second) { 161 | return $first->getStart()->getValue() <=> $second->getStart()->getValue(); 162 | }) 163 | ->map(function (Interval $interval) { 164 | return new Interval( 165 | $interval->getStart()->getValue() ** 2, 166 | $interval->getEnd()->getValue() ** 2 167 | ); 168 | }) 169 | ->filter(function (Interval $interval) { 170 | return $interval->getEnd()->getValue() > 170; 171 | }); 172 | 173 | echo '```php 174 | 175 | $result = Interval 176 | ::create(\'[10, 20]\') 177 | ->intersect(new Interval(11, 30)) 178 | ->union(new Interval(15, INF)) 179 | ->exclude(Intervals::create([\'[18, 20]\', \'[25, 30]\', \'[32, 35]\', \'[12, 13]\'])) 180 | ->sort(function (Interval $first, Interval $second) { 181 | return $first->getStart()->getValue() <=> $second->getStart()->getValue(); 182 | }) 183 | ->map(function (Interval $interval) { 184 | return new Interval( 185 | $interval->getStart()->getValue() ** 2, 186 | $interval->getEnd()->getValue() ** 2 187 | ); 188 | }) 189 | ->filter(function (Interval $interval) { 190 | return $interval->getEnd()->getValue() > 170; 191 | }); ' . "\n\n" . '// ' .$result . '; 192 | 193 | echo $result; 194 | ```' . "\n\n"; 195 | 196 | echo 'Advanced usage 197 | --------- 198 | 199 | You can create intervals with **open** boundaries : 200 | 201 | '; 202 | 203 | $result = Intervals 204 | ::create([']10, +INF[']) 205 | ->exclude(Intervals::create([']18, 20]', ']25, 30[', '[32, 35]', ']12, 13]'])); 206 | 207 | echo '```php 208 | 209 | $result = Intervals 210 | ::create([\']10, +INF[\']) 211 | ->exclude(Intervals::create([\']18, 20]\', \']25, 30[\', \'[32, 35]\', \']12, 13]\'])); 212 | 213 | // ' . $result . "\n\n" . ' 214 | ```' . "\n\n"; 215 | 216 | echo ' 217 | Contributing 218 | ---------------------- 219 | 220 | You are very welcomed to contribute to this Library! 221 | 222 | * Clone 223 | `git clone https://github.com/Kirouane/interval.git` 224 | 225 | * Install 226 | `composer install` 227 | 228 | * Test 229 | `vendor/bin/phpunit` 230 | 231 | * Build 232 | `vendor/bin/grumphp run` 233 | 234 | '; 235 | -------------------------------------------------------------------------------- /tests/unit/Boundary/IntegerTest.php: -------------------------------------------------------------------------------- 1 | getArguments($symbole); 74 | $argumentsToCompare = $this->getArguments($comparedSymbole); 75 | 76 | $bounday = new Integer(...$arguments); 77 | self::assertSame($expected, $bounday->compare(new Integer(...$argumentsToCompare)) === 0); 78 | } 79 | 80 | private function getArguments($symoble) 81 | { 82 | $value = (int)str_replace(['[', ']'], '', $symoble); 83 | $isLeft = substr($symoble, 0, 1) === '[' || substr($symoble, 0, 1) === ']' ; 84 | $isOpen = ($isLeft && substr($symoble, 0, 1) === ']') || (!$isLeft && substr($symoble, -1) === '['); 85 | 86 | return [$value, $isLeft, $isOpen]; 87 | } 88 | 89 | public function compareGreaterProvider() 90 | { 91 | return [ 92 | [']10', ']11', false], 93 | [']10', '[11', false], 94 | [']10', '11[', true], 95 | [']10', '11]', false], 96 | 97 | ['[10', ']11', false], 98 | ['[10', '[11', false], 99 | ['[10', '11[', false], 100 | ['[10', '11]', false], 101 | 102 | ['10[', ']11', false], 103 | ['10[', '[11', false], 104 | ['10[', '11[', false], 105 | ['10[', '11]', false], 106 | 107 | ['10]', ']11', false], 108 | ['10]', '[11', false], 109 | ['10]', '11[', false], 110 | ['10]', '11]', false], 111 | 112 | //------------------- 113 | 114 | [']10', ']10', false], 115 | [']10', '[10', true], 116 | [']10', '10[', true], 117 | [']10', '10]', true], 118 | 119 | ['[10', ']10', false], 120 | ['[10', '[10', false], 121 | ['[10', '10[', true], 122 | ['[10', '10]', false], 123 | 124 | ['10[', ']10', false], 125 | ['10[', '[10', false], 126 | ['10[', '10[', false], 127 | ['10[', '10]', false], 128 | 129 | ['10]', ']10', false], 130 | ['10]', '[10', false], 131 | ['10]', '10[', true], 132 | ['10]', '10]', false], 133 | ]; 134 | } 135 | 136 | /** 137 | * @dataProvider compareGreaterProvider 138 | * @test 139 | * @param mixed $symbole 140 | * @param mixed $comparedSymbole 141 | * @param mixed $expected 142 | */ 143 | public function compareGreater($symbole, $comparedSymbole, $expected) 144 | { 145 | $arguments = $this->getArguments($symbole); 146 | $argumentsToCompare = $this->getArguments($comparedSymbole); 147 | 148 | $bounday = new Integer(...$arguments); 149 | $bounday->compare(new Integer(...$argumentsToCompare)); 150 | self::assertSame($expected, $bounday->compare(new Integer(...$argumentsToCompare)) === 1); 151 | } 152 | 153 | public function compareLessProvider() 154 | { 155 | return [ 156 | [']10', ']11', true], 157 | [']10', '[11', false], 158 | [']10', '11[', false], 159 | [']10', '11]', false], 160 | 161 | ['[10', ']11', true], 162 | ['[10', '[11', true], 163 | ['[10', '11[', false], 164 | ['[10', '11]', true], 165 | 166 | ['10[', ']11', true], 167 | ['10[', '[11', true], 168 | ['10[', '11[', true], 169 | ['10[', '11]', true], 170 | 171 | ['10]', ']11', true], 172 | ['10]', '[11', true], 173 | ['10]', '11[', false], 174 | ['10]', '11]', true], 175 | 176 | //------------------- 177 | 178 | [']10', ']10', false], 179 | [']10', '[10', false], 180 | [']10', '10[', false], 181 | [']10', '10]', false], 182 | 183 | ['[10', ']10', true], 184 | ['[10', '[10', false], 185 | ['[10', '10[', false], 186 | ['[10', '10]', false], 187 | 188 | ['10[', ']10', true], 189 | ['10[', '[10', true], 190 | ['10[', '10[', false], 191 | ['10[', '10]', true], 192 | 193 | ['10]', ']10', true], 194 | ['10]', '[10', false], 195 | ['10]', '10[', false], 196 | ['10]', '10]', false], 197 | ]; 198 | } 199 | 200 | /** 201 | * @dataProvider compareLessProvider 202 | * @test 203 | * @param mixed $symbole 204 | * @param mixed $comparedSymbole 205 | * @param mixed $expected 206 | */ 207 | public function compareLess($symbole, $comparedSymbole, $expected) 208 | { 209 | $arguments = $this->getArguments($symbole); 210 | $argumentsToCompare = $this->getArguments($comparedSymbole); 211 | 212 | $bounday = new Integer(...$arguments); 213 | self::assertSame($expected, $bounday->compare(new Integer(...$argumentsToCompare)) === -1); 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function equalToTest() 220 | { 221 | $bounday = new Integer(1, true); 222 | $this->assertInternalType('boolean', $bounday->equalTo(new Integer(1, true))); 223 | } 224 | 225 | /** 226 | * @test 227 | */ 228 | public function greaterThanTest() 229 | { 230 | $bounday = new Integer(1, true); 231 | $this->assertInternalType('boolean', $bounday->greaterThan(new Integer(1, true))); 232 | } 233 | 234 | /** 235 | * @test 236 | */ 237 | public function lessThanTest() 238 | { 239 | $bounday = new Integer(1, true); 240 | $this->assertInternalType('boolean', $bounday->lessThan(new Integer(1, true))); 241 | } 242 | 243 | /** 244 | * @test 245 | */ 246 | public function greaterThanOrEqualToTest() 247 | { 248 | $bounday = new Integer(1, true); 249 | $this->assertInternalType('boolean', $bounday->greaterThanOrEqualTo(new Integer(1, true))); 250 | } 251 | 252 | /** 253 | * @test 254 | */ 255 | public function lessThanOrEqualToTest() 256 | { 257 | $bounday = new Integer(1, true); 258 | $this->assertInternalType('boolean', $bounday->lessThanOrEqualTo(new Integer(1, true))); 259 | } 260 | 261 | public function toStringProvider() 262 | { 263 | return [ 264 | [10, true, true, ']10'], 265 | [10, true, false, '[10'], 266 | [10, false, true, '10['], 267 | [10, false, false, '10]'], 268 | ]; 269 | } 270 | 271 | /** 272 | * @test 273 | * @dataProvider toStringProvider 274 | * @param mixed $value 275 | * @param mixed $isLeft 276 | * @param mixed $isOpen 277 | * @param mixed $expected 278 | */ 279 | public function toStringTest($value, $isLeft, $isOpen, $expected) 280 | { 281 | $this->assertSame($expected, (string)new Integer($value, $isLeft, $isOpen)); 282 | } 283 | 284 | public function flipProvider() 285 | { 286 | return [ 287 | ']x => x]' => ['left', 'open', 'right', 'closed'], 288 | '[x => x[' => ['left', 'closed', 'right', 'open'], 289 | 'x] => ]x' => ['right', 'closed', 'left', 'open'], 290 | 'x[ => [x' => ['right', 'open', 'left', 'closed'], 291 | ]; 292 | } 293 | 294 | /** 295 | * @test 296 | * @dataProvider flipProvider 297 | * @param mixed $side 298 | * @param mixed $openClosed 299 | * @param mixed $expectedSide 300 | * @param mixed $expectedopenClosed 301 | */ 302 | public function flip($side, $openClosed, $expectedSide, $expectedopenClosed) 303 | { 304 | $boundary = new Integer(1, $side === 'left', $openClosed === 'open'); 305 | $newBoundary = $boundary->flip(); 306 | 307 | self::assertSame($expectedSide == 'left', $newBoundary->isLeft()); 308 | self::assertSame($expectedopenClosed == 'open', $newBoundary->isOpen()); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Interval.php: -------------------------------------------------------------------------------- 1 | start = $this->toBoundary($start, true, $isLeftOpen); 48 | $this->end = $this->toBoundary($end, false, $isRightOpen); 49 | 50 | if (!$this->isConsistent()) { 51 | throw new \RangeException('Inconsistent Interval'); 52 | } 53 | } 54 | 55 | /** 56 | * @param $value 57 | * @param bool $isLeft 58 | * @param bool $isOpen 59 | * @return BoundaryAbstract 60 | * @throws \InvalidArgumentException 61 | */ 62 | private function toBoundary($value, bool $isLeft, bool $isOpen): Boundary\BoundaryAbstract 63 | { 64 | if ($value instanceof BoundaryAbstract) { 65 | return $value; 66 | } 67 | 68 | if (\is_int($value)) { 69 | return new Integer($value, $isLeft, $isOpen); 70 | } 71 | 72 | if (null === $value) { 73 | return new Infinity($isLeft ? -INF : INF, $isLeft, true); 74 | } 75 | 76 | if (\is_float($value) && \is_infinite($value)) { 77 | return new Infinity($value, $isLeft, true); 78 | } 79 | 80 | if (\is_float($value)) { 81 | return new Real($value, $isLeft, $isOpen); 82 | } 83 | 84 | if ($value instanceof \DateTimeInterface) { 85 | return new DateTime($value, $isLeft, $isOpen); 86 | } 87 | 88 | throw new \InvalidArgumentException('Unexpected $value type'); 89 | } 90 | 91 | /** 92 | * Returns false if the interval is not consistent like endTime <= starTime 93 | * @return bool 94 | */ 95 | private function isConsistent(): bool 96 | { 97 | return $this->getStart()->lessThanOrEqualTo($this->getEnd()); 98 | } 99 | 100 | /** 101 | * @param string $name 102 | * @param Interval $interval 103 | * @return mixed 104 | */ 105 | private function operate(string $name, Interval $interval) 106 | { 107 | return self::$di->get($name)($this, $interval); 108 | } 109 | 110 | /** 111 | * @param string $name 112 | * @param Interval $interval 113 | * @return mixed 114 | */ 115 | private function assert(string $name, Interval $interval) 116 | { 117 | return self::$di->get($name)->assert($this, $interval); 118 | } 119 | 120 | /** 121 | * Compute the union between two intervals. Exp : 122 | * 123 | * |_________________| 124 | * 125 | * ∪ 126 | * |_________________| 127 | * 128 | * = 129 | * |_____________________________| 130 | * 131 | * @param Interval $interval 132 | * @return Intervals 133 | */ 134 | public function union(Interval $interval) : Intervals 135 | { 136 | return $this->operate(Di::OPERATION_INTERVAL_UNION, $interval); 137 | } 138 | 139 | /** 140 | * Compute the intersection of two intervals. Exp 141 | * 142 | * |_________________| 143 | * 144 | * ∩ 145 | * |_________________| 146 | * 147 | * = 148 | * |_____| 149 | * 150 | * @param Interval $interval 151 | * @return Interval 152 | */ 153 | public function intersect(Interval $interval): Interval 154 | { 155 | return $this->operate(Di::OPERATION_INTERVAL_INTERSECTION, $interval); 156 | } 157 | 158 | /** 159 | * Excludes this interval from another one. Exp 160 | * 161 | * |_________________| 162 | * 163 | * - 164 | * |_________________| 165 | * 166 | * = 167 | * |___________| 168 | * 169 | * @param Interval $interval 170 | * @return Intervals 171 | */ 172 | public function exclude(Interval $interval) : Intervals 173 | { 174 | return $this->operate(Di::OPERATION_INTERVAL_EXCLUSION, $interval); 175 | } 176 | 177 | /** 178 | * Checks whether or not this interval overlaps another one 179 | * 180 | * @param Interval $interval 181 | * @return bool 182 | */ 183 | public function overlaps(Interval $interval) : bool 184 | { 185 | return $this->assert(Di::RULE_INTERVAL_OVERLAPPING, $interval); 186 | } 187 | 188 | /** 189 | * Checks whether or not this interval includes entirely another one 190 | * 191 | * |_________________| 192 | * 193 | * includes 194 | * |_______| 195 | * 196 | * = 197 | * true 198 | * 199 | * @param Interval $interval 200 | * @return bool 201 | */ 202 | public function includes(Interval $interval) : bool 203 | { 204 | return $this->assert(Di::RULE_INTERVAL_INCLUSION, $interval); 205 | } 206 | 207 | /** 208 | * Checks whether or not this interval is neighbor (before) of another one. 209 | * Exp : 210 | * 211 | * |_________________| 212 | * |_________________| 213 | * 214 | * @param Interval $interval 215 | * @return bool 216 | */ 217 | public function isNeighborBefore(Interval $interval) : bool 218 | { 219 | return $this->assert(Di::RULE_INTERVAL_NEIGHBORHOOD_BEFORE, $interval); 220 | } 221 | 222 | /** 223 | * Checks whether or not this interval is neighbor (after) of another one. 224 | * Exp : 225 | * 226 | * |_________________| 227 | * |_________________| 228 | * 229 | * @param Interval $interval 230 | * @return bool 231 | */ 232 | public function isNeighborAfter(Interval $interval) : bool 233 | { 234 | return $this->assert(Di::RULE_INTERVAL_NEIGHBORHOOD_AFTER, $interval); 235 | } 236 | 237 | /** 238 | * 239 | * |__________________________| 240 | * |_________________| 241 | * 242 | * @param Interval $interval 243 | * @return bool 244 | */ 245 | public function starts(Interval $interval) : bool 246 | { 247 | return $this->assert(Di::RULE_INTERVAL_STARTING, $interval); 248 | } 249 | 250 | /** 251 | * |__________________________| 252 | * |_________________| 253 | * 254 | * @param Interval $interval 255 | * @return bool 256 | */ 257 | public function ends(Interval $interval) : bool 258 | { 259 | return $this->assert(Di::RULE_INTERVAL_ENDING, $interval); 260 | } 261 | 262 | /** 263 | * |__________________________| 264 | * |__________________________| 265 | * 266 | * 267 | * @param Interval $interval 268 | * @return bool 269 | */ 270 | public function equals(Interval $interval) : bool 271 | { 272 | return $this->assert(Di::RULE_INTERVAL_EQUALITY, $interval); 273 | } 274 | 275 | /** 276 | * 277 | * |_______________| 278 | * |________| 279 | * 280 | * 281 | * @param Interval $interval 282 | * @return bool 283 | */ 284 | public function isBefore(Interval $interval) : bool 285 | { 286 | return $this->assert(Di::RULE_INTERVAL_BEFORE, $interval); 287 | } 288 | 289 | /** 290 | * |_______________| 291 | * |________| 292 | * 293 | * @param Interval $interval 294 | * @return bool 295 | */ 296 | public function isAfter(Interval $interval) : bool 297 | { 298 | return $this->assert(Di::RULE_INTERVAL_AFTER, $interval); 299 | } 300 | 301 | /** 302 | * 303 | * Returns the start boundary 304 | * @return BoundaryAbstract 305 | */ 306 | public function getStart(): BoundaryAbstract 307 | { 308 | return $this->start; 309 | } 310 | 311 | /** 312 | * Returns the end boundary 313 | * @return BoundaryAbstract 314 | */ 315 | public function getEnd(): BoundaryAbstract 316 | { 317 | return $this->end; 318 | } 319 | 320 | /** 321 | * @return string 322 | */ 323 | public function __toString() 324 | { 325 | return $this->getStart() . ', ' . $this->getEnd(); 326 | } 327 | 328 | /** 329 | * Convert an boundary to comparable 330 | * @param mixed $boundary 331 | * @return mixed 332 | * @throws \UnexpectedValueException 333 | */ 334 | public static function toComparable($boundary) 335 | { 336 | $isInternallyType = \is_numeric($boundary) || \is_bool($boundary) || \is_string($boundary); 337 | 338 | $comparable = null; 339 | if ($isInternallyType) { 340 | $comparable = $boundary; 341 | } elseif ($boundary instanceof \DateTimeInterface) { 342 | $comparable = $boundary->getTimestamp(); 343 | } else { 344 | throw new \UnexpectedValueException('Unexpected boundary type'); 345 | } 346 | 347 | return $comparable; 348 | } 349 | 350 | /** 351 | * Loads the service di 352 | * @return Di 353 | */ 354 | private static function loadDi(): Di 355 | { 356 | if (!self::$di) { 357 | self::$di = new Di(); 358 | } 359 | 360 | return self::$di; 361 | } 362 | 363 | /** 364 | * Creates a new Interval from expression 365 | * Exp Interval::create('[10, 26[') 366 | * @param string $expression 367 | * @return Interval 368 | * @throws \InvalidArgumentException 369 | * @throws \UnexpectedValueException 370 | * @throws \RangeException 371 | * @throws \ErrorException 372 | */ 373 | public static function create(string $expression) : Interval 374 | { 375 | /** @var IntervalParser $parser */ 376 | $parser = self::loadDi()->get(Di::PARSER_INTERVAL); 377 | return $parser->parse($expression); 378 | } 379 | 380 | /** 381 | * @param BoundaryAbstract $boundary 382 | * @return bool 383 | */ 384 | public function contains(BoundaryAbstract $boundary): bool 385 | { 386 | return $this->getStart()->lessThanOrEqualTo($boundary) && $this->getEnd()->greaterThanOrEqualTo($boundary); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 203 | --------------------------------------------------------------------------------