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
+
+[](https://packagist.org/packages/gamajo/daterange)
+[](https://packagist.org/packages/gamajo/daterange)
+[](https://packagist.org/packages/gamajo/daterange)
+[](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'));
+ }
+}