diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b26a9a9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb6265e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/.editorconfig export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..808f8c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +composer.lock +docs +vendor +coverage \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..df16b68 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,19 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..3f78bdd --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,5 @@ +preset: laravel + +disabled: + - single_class_element_per_statement + - self_accessor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..24c2d6c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + +env: + matrix: + - COMPOSER_FLAGS="--prefer-lowest" + - COMPOSER_FLAGS="" + +before_script: + - travis_retry composer self-update + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4768ad1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `period` will be documented in this file + +## 1.0.0 - 201X-XX-XX + +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac50a91 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Very short description of the package + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/period.svg?style=flat-square)](https://packagist.org/packages/spatie/period) +[![Build Status](https://img.shields.io/travis/spatie/period/master.svg?style=flat-square)](https://travis-ci.org/spatie/period) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/period.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/period) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/period.svg?style=flat-square)](https://packagist.org/packages/spatie/period) + +Extensions on [thephpleague/period](https://github.com/thephpleague/period). + +## Installation + +You can install the package via composer: + +```bash +composer require spatie/period +``` + +## Usage + +``` php +$skeleton = new Spatie\Skeleton(); +echo $skeleton->echoPhrase('Hello, Spatie!'); +``` + +### Testing + +``` bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +### Security + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Credits + +- [Brent Roose](https://github.com/brendt) +- [All Contributors](../../contributors) + +## Support us + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..33c9acb --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "spatie/period", + "description": "Extensions on thephpleague/period", + "keywords": [ + "spatie", + "period" + ], + "homepage": "https://github.com/spatie/period", + "license": "MIT", + "authors": [ + { + "name": "Brent Roose", + "email": "brent@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php": "^7.1" + }, + "require-dev": { + "larapack/dd": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "autoload": { + "psr-4": { + "Spatie\\Period\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Spatie\\Period\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + + }, + "config": { + "sort-packages": true + }, + "extra": { + } +} diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4c78188 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/src/InvalidPeriod.php b/src/InvalidPeriod.php new file mode 100644 index 0000000..7f04937 --- /dev/null +++ b/src/InvalidPeriod.php @@ -0,0 +1,16 @@ +periods[$offset] ?? null; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->periods[] = $value; + } else { + $this->periods[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->periods); + } + + public function offsetUnset($offset) + { + unset($this->periods[$offset]); + } + + public function next() + { + $this->position++; + } + + public function key() + { + return $this->position; + } + + public function valid() + { + return array_key_exists($this->position, $this->periods); + } + + public function rewind() + { + $this->position = 0; + } + + public function count(): int + { + return count($this->periods); + } +} diff --git a/src/Period.php b/src/Period.php new file mode 100644 index 0000000..0aafb37 --- /dev/null +++ b/src/Period.php @@ -0,0 +1,212 @@ + $end) { + throw InvalidPeriod::endBeforeStart($start, $end); + } + + $this->start = $start; + $this->end = $end; + } + + public static function make($start, $end, string $format = 'Y-m-d H:i:s'): Period + { + return new self( + DateTimeImmutable::createFromFormat($format, $start . ' 00:00:00'), + DateTimeImmutable::createFromFormat($format, $end . ' 00:00:00') + ); + } + + public function getStart(): DateTimeImmutable + { + return $this->start; + } + + public function getEnd(): DateTimeImmutable + { + return $this->end; + } + + public function length(): int + { + return $this->start->diff($this->end)->days + 1; + } + + public function overlapsWith(Period $period): bool + { + return $this->start <= $period->end + && $period->start <= $this->end; + } + + public function touches(Period $period): bool + { + return $this->end->diff($period->start)->days <= 1 + || $this->start->diff($period->end)->days <= 1; + } + + public function startsAfterOrAt(DateTimeImmutable $date): bool + { + return $this->start >= $date; + } + + public function endsAfterOrAt(DateTimeImmutable $date): bool + { + return $this->end >= $date; + } + + public function startsBeforeOrAt(DateTimeImmutable $date): bool + { + return $this->start <= $date; + } + + public function endsBeforeOrAt(DateTimeImmutable $date): bool + { + return $this->end <= $date; + } + + public function equals(Period $period): bool + { + return $period->start->getTimestamp() === $this->start->getTimestamp() + && $period->end->getTimestamp() === $this->end->getTimestamp(); + } + + public function gap(Period $period): ?Period + { + if ( + $this->overlapsWith($period) + || $this->touches($period) + ) { + return null; + } + + if ($this->start >= $period->end) { + return new Period( + $period->end->add(new DateInterval('P1D')), + $this->start->sub(new DateInterval('P1D')) + ); + } + + return new Period( + $this->end->add(new DateInterval('P1D')), + $period->start->sub(new DateInterval('P1D')) + ); + } + + public function overlapSingle(Period $period): ?Period + { + $start = $this->start > $period->start + ? $this->start + : $period->start; + + $end = $this->end < $period->end + ? $this->end + : $period->end; + + if ($start > $end) { + return null; + } + + return new Period($start, $end); + } + + public function overlap(Period ...$periods): PeriodCollection + { + $overlapCollection = new PeriodCollection(); + + foreach ($periods as $period) { + $overlapCollection[] = $this->overlapSingle($period); + } + + return $overlapCollection; + } + + public function overlapAll(Period ...$periods): Period + { + $overlap = clone $this; + + if (! count($periods)) { + return $overlap; + } + + foreach ($periods as $period) { + $overlap = $overlap->overlapSingle($period); + } + + return $overlap; + } + + public function diffSingle(Period $period): PeriodCollection + { + $periodCollection = new PeriodCollection(); + + if (! $this->overlapsWith($period)) { + $periodCollection[] = clone $this; + $periodCollection[] = clone $period; + + return $periodCollection; + } + + $overlap = $this->overlapSingle($period); + + $start = $this->start < $period->start + ? $this->start + : $period->start; + + $end = $this->end > $period->end + ? $this->end + : $period->end; + + if ($overlap->start > $start) { + $periodCollection[] = new Period( + $start, + $overlap->start->sub(new DateInterval('P1D')) + ); + } + + if ($overlap->end < $end) { + $periodCollection[] = new Period( + $overlap->end->add(new DateInterval('P1D')), + $end + ); + } + + return $periodCollection; + } + + public function diff(Period ...$periods): PeriodCollection + { + if (count($periods) === 1) { + $collection = new PeriodCollection(); + + if (! $this->overlapsWith($periods[0])) { + $collection[] = $this->gap($periods[0]); + } + + return $collection; + } + + $diffs = []; + + foreach ($periods as $period) { + $diffs[] = $this->diffSingle($period); + } + + $collection = (new PeriodCollection($this))->overlap(...$diffs); + + return $collection; + } +} diff --git a/src/PeriodCollection.php b/src/PeriodCollection.php new file mode 100644 index 0000000..9b1eb11 --- /dev/null +++ b/src/PeriodCollection.php @@ -0,0 +1,56 @@ +periods = $periods; + } + + public function current(): Period + { + return $this->periods[$this->position]; + } + + public function overlapSingle(PeriodCollection $periodCollection): PeriodCollection + { + $overlaps = new PeriodCollection(); + + foreach ($this as $period) { + foreach ($periodCollection as $otherPeriod) { + if (! $period->overlapSingle($otherPeriod)) { + continue; + } + + $overlaps[] = $period->overlapSingle($otherPeriod); + } + } + + return $overlaps; + } + + public function overlap(PeriodCollection ...$periodCollections): PeriodCollection + { + $overlap = clone $this; + + foreach ($periodCollections as $periodCollection) { + $overlap = $overlap->overlapSingle($periodCollection); + } + + return $overlap; + } +} diff --git a/tests/PeriodCollectionTest.php b/tests/PeriodCollectionTest.php new file mode 100644 index 0000000..27133c5 --- /dev/null +++ b/tests/PeriodCollectionTest.php @@ -0,0 +1,75 @@ +overlapSingle($b); + + $this->assertCount(2, $overlap); + + [$first, $second] = $overlap; + + $this->assertTrue($first->equals(Period::make('2018-01-05', '2018-01-10'))); + $this->assertTrue($second->equals(Period::make('2018-01-22', '2018-01-25'))); + } + + /** + * @test + * + * + * A [=====] [===========] + * B [=================] + * C [====================] + * + * OVERLAP [=] [====] + */ + public function overlap_collection() + { + $a = new PeriodCollection( + Period::make('2018-01-01', '2018-01-07'), + Period::make('2018-01-15', '2018-01-25') + ); + + $b = new PeriodCollection( + Period::make('2018-01-01', '2018-01-20') + ); + + $c = new PeriodCollection( + Period::make('2018-01-06', '2018-01-25') + ); + + $overlap = $a->overlap($b, $c); + + $this->assertCount(2, $overlap); + + [$first, $second] = $overlap; + + $this->assertTrue($first->equals(Period::make('2018-01-06', '2018-01-07'))); + $this->assertTrue($second->equals(Period::make('2018-01-15', '2018-01-20'))); + } +} diff --git a/tests/PeriodTest.php b/tests/PeriodTest.php new file mode 100644 index 0000000..de4b7a0 --- /dev/null +++ b/tests/PeriodTest.php @@ -0,0 +1,456 @@ +assertEquals(15, $period->length()); + } + + /** + * @test + * @dataProvider overlappingDates + */ + public function overlaps(Period $a, Period $b) + { + $this->assertTrue($a->overlapsWith($b)); + } + + /** @test */ + public function touches() + { + $this->assertTrue( + Period::make('2018-01-01', '2018-01-01') + ->touches(Period::make('2018-01-02', '2018-01-02')) + ); + + $this->assertTrue( + Period::make('2018-01-02', '2018-01-02') + ->touches(Period::make('2018-01-01', '2018-01-01')) + ); + + $this->assertFalse( + Period::make('2018-01-01', '2018-01-01') + ->touches(Period::make('2018-01-03', '2018-01-03')) + ); + + $this->assertFalse( + Period::make('2018-01-03', '2018-01-03') + ->touches(Period::make('2018-01-01', '2018-01-01')) + ); + } + + /** + * @test + * @dataProvider noOverlappingDates + */ + public function does_not_overlap(Period $a, Period $b) + { + $this->assertFalse($a->overlapsWith($b)); + } + + public function overlappingDates(): array + { + return [ + /** + * A [=====] + * B [=====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2018-01-15', '2018-02-15')], + + /** + * A [=====] + * B [=============] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2017-01-01', '2019-01-01')], + + /** + * A [=====] + * B [=====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2017-12-01', '2018-01-15')], + + /** + * A [=============] + * B [=====] + */ + [Period::make('2017-01-01', '2019-01-01'), Period::make('2018-01-01', '2018-02-01')], + + /** + * A [====] + * B [====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2018-01-01', '2018-02-01')], + ]; + } + + public function noOverlappingDates() + { + return [ + /** + * A [===] + * B [===] + */ + [Period::make('2018-01-01', '2018-01-31'), Period::make('2018-02-01', '2018-02-28')], + + /** + * A [===] + * B [===] + */ + [Period::make('2018-02-01', '2018-02-28'), Period::make('2018-01-01', '2018-01-31')], + ]; + } + + /** + * @test + * + * A [===========] + * B [============] + * + * OVERLAP [=======] + */ + public function test_overlapping_period() + { + $a = Period::make('2018-01-01', '2018-01-15'); + + $b = Period::make('2018-01-10', '2018-01-30'); + + $overlap = Period::make('2018-01-10', '2018-01-15'); + + $this->assertTrue($a->overlapSingle($b)->equals($overlap)); + } + + /** + * @test + * + * A [========] + * B [==] + * C [=====] + * D [=============] + * + * OVERLAP [=] [==] [==] + */ + public function test_overlapping_multiple() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-02-10', '2018-02-20'); + $c = Period::make('2018-03-01', '2018-03-31'); + $d = Period::make('2018-01-20', '2018-03-10'); + + $overlap = $d->overlap($a, $b, $c); + + $this->assertCount(3, $overlap); + + [$first, $second, $third] = $overlap; + + $this->assertTrue($first->equals(Period::make('2018-01-20', '2018-01-31'))); + $this->assertTrue($second->equals(Period::make('2018-02-10', '2018-02-20'))); + $this->assertTrue($third->equals(Period::make('2018-03-01', '2018-03-10'))); + } + + /** + * @test + * + * A [============] + * B [==] + * C [=======] + * + * OVERLAP [==] + */ + public function test_overlap_all() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-01-10', '2018-01-15'); + $c = Period::make('2018-01-10', '2018-01-31'); + + $overlap = $a->overlapAll($b, $c); + + $this->assertTrue($overlap->equals(Period::make('2018-01-10', '2018-01-15'))); + } + + /** + * @test + * + */ + public function test_no_overlap_reverse() + { + $a = Period::make('2018-01-05', '2018-01-10'); + $b = Period::make('2018-01-22', '2018-01-30'); + + $overlap = $a->overlapSingle($b); + + $this->assertNull($overlap); + } + + /** + * @test + * + * A [===] + * B [===] + * + * GAP [=] + */ + public function test_gap() + { + $a = Period::make('2018-01-01', '2018-01-10'); + + $b = Period::make('2018-01-15', '2018-01-31'); + + $gap = $a->gap($b); + + $this->assertTrue($gap->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [===] + * B [===] + * + * GAP [=] + */ + public function test_gap_reverse() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-01-01', '2018-01-10'); + + $gap = $a->gap($b); + + $this->assertTrue($gap->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * GAP + */ + public function test_gap_is_null_when_touching() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-02-01', '2018-02-01'); + + $gap = $a->gap($b); + + $this->assertNull($gap); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * GAP + */ + public function test_gap_is_null_when_overlap() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-01-28', '2018-02-01'); + + $gap = $a->gap($b); + + $this->assertNull($gap); + } + + /** + * @test + * + * A [===========] + * B [===========] + * + * DIFF [==] [==] + */ + public function diff_single() + { + $a = Period::make('2018-01-01', '2018-01-15'); + + $b = Period::make('2018-01-10', '2018-01-30'); + + [$first, $second] = $a->diffSingle($b); + + $this->assertTrue($first->equals(Period::make('2018-01-01', '2018-01-09'))); + $this->assertTrue($second->equals(Period::make('2018-01-16', '2018-01-30'))); + } + + /** + * @test + * + * A [==========] + * B [============] + * + * DIFF [==] [==] + */ + public function diff_single_reverse() + { + $a = Period::make('2018-01-10', '2018-01-30'); + + $b = Period::make('2018-01-01', '2018-01-15'); + + [$first, $second] = $a->diffSingle($b); + + $this->assertTrue($first->equals(Period::make('2018-01-01', '2018-01-09'))); + $this->assertTrue($second->equals(Period::make('2018-01-16', '2018-01-30'))); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * DIFF [=====] [=====] + */ + public function diff_no_overlap() + { + $a = Period::make('2018-01-10', '2018-01-15'); + + $b = Period::make('2018-02-10', '2018-02-15'); + + [$first, $second] = $a->diffSingle($b); + + $this->assertTrue($first->equals(Period::make('2018-01-10', '2018-01-15'))); + $this->assertTrue($second->equals(Period::make('2018-02-10', '2018-02-15'))); + } + + /** + * @test + * + * A [=========] + * B [==] + * C [=========] + * CURRENT [===========] + * + * OVERLAP [=] [====] + * DIFF [=] + */ + public function test_diff_multiple_with_double_overlaps() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-02-10', '2018-02-20'); + $c = Period::make('2018-02-11', '2018-03-31'); + + $current = Period::make('2018-01-20', '2018-03-15'); + + $diff = $current->diff($a, $b, $c); + + $this->assertCount(1, $diff); + + [$first] = $diff; + + $this->assertTrue($first->equals(Period::make('2018-02-01', '2018-02-09'))); + } + + /** + * @test + * + * A [========] + * B [=========] + * CURRENT [============] + * + * OVERLAP [============] + * DIFF + */ + public function test_empty_diff() + { + $a = Period::make('2018-01-15', '2018-02-10'); + $b = Period::make('2017-12-20', '2018-01-15'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b); + + $this->assertCount(0, $diff); + } + + /** + * @test + * + * A [========] + * CURRENT [=======] + * + * DIFF [==] + */ + public function test_diff_single() + { + $a = Period::make('2018-02-15', '2018-02-20'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a); + + $this->assertCount(1, $diff); + + [$first] = $diff; + + $this->assertTrue($first->equals(Period::make('2018-02-01', '2018-02-14'))); + } + + /** + * @test + * + * A [====] + * B [========] + * C [=====] + * CURRENT [========================] + * + * DIFF [=] [====] + */ + public function diff_case_1() + { + $a = Period::make('2018-01-05', '2018-01-10'); + $b = Period::make('2018-01-15', '2018-03-01'); + $c = Period::make('2017-01-01', '2018-01-02'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b, $c); + + $this->assertCount(2, $diff); + + [$first, $second] = $diff; + + $this->assertTrue($first->equals(Period::make('2018-01-03', '2018-01-04'))); + $this->assertTrue($second->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [====] + * B [====] + * CURRENT [=============================] + * + * DIFF [======] [====] [===] + */ + public function diff_case_2() + { + $a = Period::make('2018-01-15', '2018-01-20'); + $b = Period::make('2018-01-05', '2018-01-10'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b); + + $this->assertCount(3, $diff); + + [$first, $second, $third] = $diff; + + $this->assertTrue($first->equals(Period::make('2018-01-01', '2018-01-04'))); + $this->assertTrue($second->equals(Period::make('2018-01-11', '2018-01-14'))); + $this->assertTrue($third->equals(Period::make('2018-01-21', '2018-01-31'))); + } +}