From 8175af67dea75af29aeef6244d3b816aa5e4ea5f Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 16 May 2018 20:27:31 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + CHANGELOG.md | 9 ++ LICENSE | 21 +++ README.md | 97 ++++++++++++ composer.json | 54 +++++++ phpcs.xml.dist | 33 ++++ phpunit.xml.dist | 30 ++++ src/DateRange.php | 281 +++++++++++++++++++++++++++++++++++ tests/Unit/DateRangeTest.php | 162 ++++++++++++++++++++ 9 files changed, 689 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpcs.xml.dist create mode 100644 phpunit.xml.dist create mode 100644 src/DateRange.php create mode 100644 tests/Unit/DateRangeTest.php 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')); + } +}