diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c55784d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +composer.lock +/vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4ece094 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [0.1.0] - 2018-05-16 +### Added +- Initial release to GitHub. + +[0.1.0]: https://github.com/GaryJones/DateRange/compare/v0.0.0...v0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6e7ecc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Gary Jones, Gamajo + +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..71dfa8c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Gamajo Date Range + +[![Latest Stable Version](https://img.shields.io/packagist/v/gamajo/daterange.svg)](https://packagist.org/packages/gamajo/daterange) +[![Total Downloads](https://img.shields.io/packagist/dt/gamajo/daterange.svg)](https://packagist.org/packages/gamajo/daterange) +[![Latest Unstable Version](https://img.shields.io/packagist/vpre/gamajo/daterange.svg)](https://packagist.org/packages/gamajo/daterange) +[![License](https://img.shields.io/packagist/l/gamajo/daterange.svg)](https://packagist.org/packages/gamajo/daterange) + +Display a range of dates, with consolidated time parts. + +## Table Of Contents + +* [Installation](#installation) +* [Basic Usage](#basic-usage) +* [Advanced Usage](#advanced-usage) +* [Contributing](#contributing) +* [License](#license) + +## Installation + +The best way to use this package is through Composer: + +```BASH +composer require gamajo/daterange +``` + +## Basic Usage + +Create an instance of the `DateRange` class, with `DateTimeImmutable` or `DateTime` start and end date-time objects as arguments. Then choose the format to use as the end date output. The start date will only display the time parts that are not duplicated. + +```php +$dateRange = new DateRange( + new DateTimeImmutable('23rd June 18 14:00'), + new DateTimeImmutable('2018-06-23T15:00') +); +echo $dateRange->format('H:i d M Y'); // 14:00 – 15:00 23 Jun 2018 +``` + +If the formatted date would be the same start and end date, only a single date is displayed: + +```php +$dateRange = new DateRange( + new DateTimeImmutable('23rd June 18 14:00'), + new DateTimeImmutable('2018-06-23T15:00') +); +echo $dateRange->format('jS M Y'); // 23rd Jun 2018 +``` + +## Advanced Usage + +### Change Separator + +The default separator between the start and end date, is a space, en-dash, space: `' – '` + +This can be changed via the `changeSeparator()` method: + +```php +$dateRange = new DateRange( + new DateTimeImmutable('23rd June 18 14:00'), + new DateTimeImmutable('2018-06-23T15:00') +); +$dateRange->changeSeparator(' to '); +echo 'From ', $dateRange->format('H:i d M Y'); // From 14:00 to 15:00 23 Jun 2018 +``` + +### Change Removable Delimiters + +The consolidation and removal of some time parts may leave delimiters from the format: + +```php +$dateRange = new DateRange( + new DateTimeImmutable('23rd June 18'), + new DateTimeImmutable('2018-06-24') +); +echo $dateRange->format('d·M·Y'); // 23·· – 24·Jun·2018 +``` + +Be default, `/`, `-` and `.` are trimmed from the start date, but this can be amended with the `changeRemovableDelimiters()` method: + +```php +$dateRange = new DateRange( + new DateTimeImmutable('23rd June 18'), + new DateTimeImmutable('2018-06-24') +); +$dateRange->changeRemovableDelimiters('·'); +echo $dateRange->format('d·M·Y'); // 23 – 24·Jun·2018 +``` + + +## Contributing + +All feedback / bug reports / pull requests are welcome. + +## License + +Copyright (c) 2018 Gary Jones, Gamajo + +This code is licensed under the [MIT License](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3824991 --- /dev/null +++ b/composer.json @@ -0,0 +1,54 @@ +{ + "name": "gamajo/daterange", + "description": "Display a range of dates, with consolidated time parts.", + "minimum-stability": "dev", + "prefer-stable": true, + "license": "MIT", + "homepage": "https://github.com/GaryJones/date-range", + "authors": [ + { + "name": "Gary Jones", + "homepage": "https://gamajo.com", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/GaryJones/daterange/issues", + "source": "https://github.com/GaryJones/daterange" + }, + "config": { + "sort-packages": true + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "brain/monkey": "^2.2", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "infection/infection": "^0.8", + "mockery/mockery": "^1.1", + "object-calisthenics/phpcs-calisthenics-rules": "^3", + "phpunit/phpunit": "^7", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^3.2", + "sirbrillig/phpcs-variable-analysis": "^2.0", + "wimg/php-compatibility": "^8.1" + }, + "autoload": { + "psr-4": { + "Gamajo\\DateRange\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Gamajo\\DateRange\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit", + "phpcs": "./vendor/bin/phpcs", + "install-codestandards": [ + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run" + ] + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..cebd013 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,33 @@ + + + The code standard for Gamajo dateRange package. + + + . + vendor/ + + + + + + + + + + + + + /tests + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..e2a8432 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + tests/Unit + + + + + + + + + src + + + + + + + + diff --git a/src/DateRange.php b/src/DateRange.php new file mode 100644 index 0000000..cded946 --- /dev/null +++ b/src/DateRange.php @@ -0,0 +1,281 @@ +startDate = $startDate; + $this->endDate = $endDate; + } + + /** + * Change the separator between the start and end date. + * + * @since 1.0.0 + * + * @param string $separator Separator. + */ + public function changeSeparator(string $separator): void + { + $this->separator = $separator; + } + + /** + * Change the delimiters that should be trimmed from the start date format ends. + * + * Avoids a format of 'd/M/Y' having a starting format of `d//` when month and year are consolidated. + * + * @since 1.0.0 + * + * @param string $removableDelimiters [description] + */ + public function changeRemovableDelimiters(string $removableDelimiters): void + { + $this->removableDelimiters = $removableDelimiters; + } + + /** + * Format the date range. + * + * Time parts are consolidated, starting with the largest time part. + * i.e. start and end dates with the same year would not show the year for the start date: + * + * 14th May - 5th June 2018 + * + * If the year and the month are the same, then neither yar or month are shown for the start date: + * + * 14th - 15th May 2018. + * + * This continues for date (day of the month), hours, minutes and seconds. + * + * The output looks best when the format has time parts in increasing order of size. + * It will technically work in other orders though: + * + * 14:00 23rd – 2018 14:00 Jun 24th 2018 + * + * @since 1.0.0 + * + * @param string $endDateFormat Date format as per https://secure.php.net/manual/en/function.date.php + * + * @return string Date range output. + */ + public function format(string $endDateFormat): string + { + // Formatted dates are the same, so return single date. + if ($this->formattedDatesMatch($endDateFormat)) { + return $this->endDate->format($endDateFormat); + } + + $startDateFormat = $this->getStartDateFormat($endDateFormat); + + return $this->startDate->format($startDateFormat) . + $this->separator . + $this->endDate->format($endDateFormat); + } + + /** + * Get the consolidated start date format. + * + * This is based on removing duplicated time parts between the start and end dates. + * + * @since 1.0.0 + * + * @param string $endDateFormat End date format. + * + * @return string Start date format. + */ + protected function getStartDateFormat(string $endDateFormat): string + { + $startDateFormat = trim($endDateFormat); + $timePartCharacters = str_split($startDateFormat); + + $sortedTimePartCharacters = $this->sortTimePartCharacters($timePartCharacters); + + foreach ($sortedTimePartCharacters as $timePartCharacter) { + if ($this->timePartValueInDatesIsInconsistent($timePartCharacter)) { + break; + } + + $startDateFormat = $this->removeTimePartCharacterFromFormat($timePartCharacter, $startDateFormat); + } + + return trim(trim($startDateFormat, $this->removableDelimiters)); + } + + // @codingStandardsChangeSetting ObjectCalisthenics.Metrics.MaxNestingLevel maxNestingLevel 3 + /** + * Sort timePartCharacters by the size of the time part. + * + * i.e. all the year characters first, then month characters etc. + * + * @param array $timePartCharacters + * + * @return array + */ + protected function sortTimePartCharacters(array $timePartCharacters) + { + $sorted = []; + + foreach (self::CHAR_SETS as $charset) { + foreach ($timePartCharacters as $timePartCharacter) { + if (in_array($timePartCharacter, $charset)) { + $sorted[] = $timePartCharacter; + } + } + } + + return array_unique($sorted); + } + // @codingStandardsChangeSetting ObjectCalisthenics.Metrics.MaxNestingLevel maxNestingLevel 2 + + /** + * Remove a time part character and its aliases from a format. + * + * @param string $timePartCharacter Time part character. + * @param string $format Date format. + * + * @return string + */ + protected function removeTimePartCharacterFromFormat(string $timePartCharacter, string $format): string + { + $timePartAliases = $this->getTimePartAliases($timePartCharacter); + + return $this->removeTimePartAliasesFromFormat($timePartAliases, $format); + } + + /** + * @param array $timePartAliases Characters that match the time part. + * @param string $format Date format. + * + * @return string Updated date format. + */ + protected function removeTimePartAliasesFromFormat(array $timePartAliases, string $format): string + { + return str_replace($timePartAliases, '', $format); + } + + /** + * @param string $timePartCharacter Time part character from format. + * + * @return array + */ + protected function getTimePartAliases(string $timePartCharacter): array + { + foreach (self::CHAR_SETS as $timePartAliases) { + if (in_array($timePartCharacter, $timePartAliases)) { + $return = $timePartAliases; + } + } + + return $return; + } + + /** + * Check to see if dates match for the desired format. + * + * @param string $format + * + * @return bool + */ + protected function formattedDatesMatch(string $format): bool + { + return $this->startDate->format($format) === $this->endDate->format($format); + } + + /** + * Check if time part value in dates is inconsistent (i.e. Feb and Mar) + * + * @param string $timePartCharacter Time part character to check. + * + * @return bool + */ + protected function timePartValueInDatesIsInconsistent(string $timePartCharacter): bool + { + return ! $this->formattedDatesMatch($timePartCharacter); + } +} + +// @codingStandardsChangeSetting ObjectCalisthenics.Metrics.MethodPerClassLimit maxCount 10 +// @codingStandardsChangeSetting ObjectCalisthenics.Files.ClassTraitAndInterfaceLength maxLength 200 diff --git a/tests/Unit/DateRangeTest.php b/tests/Unit/DateRangeTest.php new file mode 100644 index 0000000..d924e63 --- /dev/null +++ b/tests/Unit/DateRangeTest.php @@ -0,0 +1,162 @@ +format($format)); + } + + /** + * @return array + */ + public function dataStartEndDates() + { + return [ + 'Single date' => [ + '1980-03-14', + '1980-Mar-14', + 'D jS F Y', + 'Fri 14th March 1980', + ], + 'Common month and year' => [ + '2018-06-18', + '23rd June 2018', + 'jS F Y', + '18th – 23rd June 2018', + ], + 'Same date of the month, but different month' => [ + '2018-06-23', + '23 July 2018', + 'd M Y', + '23 Jun – 23 Jul 2018', + ], + 'Same date of the month, different month, date of the month ignored' => [ + '2017-06-23', + '2018-06-23', + 'M Y', + 'Jun 2017 – Jun 2018', + ], + 'Single date, date of the month different but ignored' => [ + '2017-04-06', + '2017-04-05', + 'M Y', + 'Apr 2017', + ], + 'Different hour, but hour ignored' => [ + '23 Jun 18 14:00', + '2018-06-23T15:00', + 'd M Y', + '23 Jun 2018', + ], + 'Same date, different hour' => [ + '23rd June 18 14:00', + '2018-06-23T15:00', + 'H:i d M Y', + '14:00 – 15:00 23 Jun 2018', + ], + 'Different date and hour' => [ + '2018-06-23T14:00', + 'June 24 18 15:00', + 'H:i dS M Y', + '14:00 23rd – 15:00 24th Jun 2018', + ], + ]; + } + + /** + * Test date range output with modified separator. + * + * @throws Exception + */ + public function testDateRangeModifiedSeparator() + { + $dateRange = new DateRange(new DateTimeImmutable('6th Feb 18'), new DateTimeImmutable('2018-02-07')); + $dateRange->changeSeparator(' to '); + self::assertEquals('06 to 07/2/18', $dateRange->format('d/n/y')); + } + + /** + * Test date range output with modified removable delimiters. + * + * @throws Exception + */ + public function testDateRangeModifiedRemovableDelimiters() + { + $dateRange = new DateRange(new DateTimeImmutable('6th Feb 18'), new DateTimeImmutable('2018-02-07')); + $dateRange->changeRemovableDelimiters('~\\'); + self::assertEquals('06 – 07~Feb\18', $dateRange->format('d~M\\\y')); + } + + /** + * Test date range output with duplicated formatting characters. + * + * @group duplicate + * @throws Exception + */ + public function testDateRangeDuplicatedFormattingCharacters() + { + $dateRange = new DateRange(new DateTimeImmutable('6th Feb 18'), new DateTimeImmutable('2018-02-07')); + $dateRange->changeRemovableDelimiters('~\\'); + self::assertEquals('0606Tue – 201807Feb180207Feb2018Wed02', $dateRange->format('YdMymdMYDm')); + } +}