├── _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 | [](http://travis-ci.org/Kirouane/interval)
2 | [](https://coveralls.io/github/Kirouane/interval?branch=develop)
3 | [](https://www.codacy.com/app/Kirouane/interval?utm_source=github.com&utm_medium=referral&utm_content=Kirouane/interval&utm_campaign=Badge_Grade)
4 | [](https://packagist.org/packages/kirouane/interval)
5 | [](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 [](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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
--------------------------------------------------------------------------------