diff --git a/README.md b/README.md index 1f7e85d..164d37a 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,22 @@ DateTimeHumanizer::preciseDifference(new \DateTime("2014-04-26 13:00:00"), new \ DateTimeHumanizer::preciseDifference(new \DateTime("2014-04-26 13:00:00"), new \DateTime("2016-04-27 13:00:00")); // 2 years, 1 day from now ``` +## Aeon Calendar + +[Aeon PHP](https://aeon-php.org/) is a date&time oriented set of libraries. + +```php +use Coduo\PHPHumanizer\DateTimeHumanizer; + +$timeUnit = TimeUnit::days(2) + ->add(TimeUnit::hours(3)) + ->add(TimeUnit::minutes(25)) + ->add(TimeUnit::seconds(30)) + ->add(TimeUnit::milliseconds(200)); + +DateTimeHumanizer::timeUnit($timeUnit); // 2 days, 3 hours, 25 minutes, and 30.2 seconds +``` + Currently we support following languages: * [Azerbaijani](src/Coduo/PHPHumanizer/Resources/translations/difference.az.yml) * [English](src/Coduo/PHPHumanizer/Resources/translations/difference.en.yml) diff --git a/composer.json b/composer.json index 58f5108..92dbc7a 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "require": { "php": "^7.4 | ^8.0", "symfony/translation": "^4.4|^5.0", - "symfony/yaml": "^4.4|^5.0" + "symfony/yaml": "^4.4|^5.0", + "aeon-php/calendar": ">=0.16.1" }, "require-dev": { "thunderer/shortcode": "^0.7", @@ -40,6 +41,10 @@ "ext-intl": "Required if you are going to use humanizer with locales different than en_EN" }, "scripts": { + "build": [ + "@static:analyze", + "@test" + ], "cs:php:fix": [ "tools/php-cs-fixer fix --using-cache=no" ], diff --git a/composer.lock b/composer.lock index d0fe9ed..1753b0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2266b8e08228e1596ce0366b445cd246", + "content-hash": "c74a1cca580ca33e81761900ec3f88f1", "packages": [ + { + "name": "aeon-php/calendar", + "version": "0.16.1", + "source": { + "type": "git", + "url": "https://github.com/aeon-php/calendar.git", + "reference": "9509656c6b01024aad6158628fc8c950f9cf1199" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aeon-php/calendar/zipball/9509656c6b01024aad6158628fc8c950f9cf1199", + "reference": "9509656c6b01024aad6158628fc8c950f9cf1199", + "shasum": "" + }, + "require": { + "php": ">=7.4.2" + }, + "require-dev": { + "ext-bcmath": "*" + }, + "suggest": { + "ext-bcmath": "Compare time units with high precision" + }, + "type": "library", + "autoload": { + "psr-4": { + "Aeon\\": [ + "src/Aeon" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP type safe, immutable calendar library", + "keywords": [ + "calendar", + "date", + "datetime", + "immutable", + "time" + ], + "support": { + "issues": "https://github.com/aeon-php/calendar/issues", + "source": "https://github.com/aeon-php/calendar/tree/0.16.1" + }, + "funding": [ + { + "url": "https://github.com/norberttech", + "type": "github" + } + ], + "time": "2021-02-23T08:58:57+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v2.2.0", @@ -808,16 +863,16 @@ }, { "name": "phar-io/version", - "version": "3.0.4", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" + "reference": "bae7c545bef187884426f042434e561ab1ddb182" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", "shasum": "" }, "require": { @@ -853,9 +908,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.0.4" + "source": "https://github.com/phar-io/version/tree/3.1.0" }, - "time": "2020-12-13T23:18:30+00:00" + "time": "2021-02-23T14:00:09+00:00" }, { "name": "phpdocumentor/reflection-common", diff --git a/src/Coduo/PHPHumanizer/Aeon/Calendar/Formatter.php b/src/Coduo/PHPHumanizer/Aeon/Calendar/Formatter.php new file mode 100644 index 0000000..3576da6 --- /dev/null +++ b/src/Coduo/PHPHumanizer/Aeon/Calendar/Formatter.php @@ -0,0 +1,42 @@ +translator = $translator; + } + + public function timeUnit(Unit $unit, string $locale = 'en') : string + { + $parts = []; + + foreach ((new UnitCompound($unit))->components() as $component) { + $parts[] = $this->translator->trans( + 'compound.'.$component->getUnit()->getName(), + ['%count%' => $component->getQuantity()], + 'difference', + $locale + ); + } + + return CollectionHumanizer::oxford($parts, null, $locale); + } +} diff --git a/src/Coduo/PHPHumanizer/Aeon/Calendar/UnitCompound.php b/src/Coduo/PHPHumanizer/Aeon/Calendar/UnitCompound.php new file mode 100644 index 0000000..b461b11 --- /dev/null +++ b/src/Coduo/PHPHumanizer/Aeon/Calendar/UnitCompound.php @@ -0,0 +1,83 @@ +unit = $timeUnit; + } + + /** + * @return array + */ + public function components() : array + { + $unit = $this->unit; + $compoundResults = []; + + if ($unit instanceof RelativeTimeUnit) { + if ($unit->inYears()) { + $compoundResults[] = new CompoundResult(new Year(), $unit->inYears()); + } + + if ($unit->inCalendarMonths()) { + $compoundResults[] = new CompoundResult(new Month(), $unit->inCalendarMonths()); + } + + return (new DateIntervalCompound($unit->toDateInterval()))->components(); + } + + if ($unit instanceof TimeUnit) { + if ($unit->inDaysAbs() > 0) { + $compoundResults[] = new CompoundResult(new Day(), $unit->inDaysAbs()); + } + + if ($unit->inTimeHours()) { + $compoundResults[] = new CompoundResult(new Hour(), $unit->inTimeHours()); + } + + if ($unit->inTimeMinutes()) { + $compoundResults[] = new CompoundResult(new Minute(), $unit->inTimeMinutes()); + } + + if ($unit->inTimeSeconds()) { + $seconds = $unit->inTimeSeconds(); + + if ($unit->inTimeMilliseconds() > 0) { + $seconds += $unit->inTimeMilliseconds() / 1000; + } + + $compoundResults[] = new CompoundResult(new Second(), $seconds); + } + + return $compoundResults; + } + + throw new \RuntimeException('Unsupported unit type ' . \get_class($unit)); + } +} diff --git a/src/Coduo/PHPHumanizer/DateTime/DateIntervalCompound.php b/src/Coduo/PHPHumanizer/DateTime/DateIntervalCompound.php new file mode 100644 index 0000000..c94c7d9 --- /dev/null +++ b/src/Coduo/PHPHumanizer/DateTime/DateIntervalCompound.php @@ -0,0 +1,58 @@ +dateInterval = $dateInterval; + } + + /** + * @return array + */ + public function components() : array + { + /* @var Unit[] $units */ + $units = [ + new Year(), + new Month(), + new Day(), + new Hour(), + new Minute(), + new Second(), + ]; + + + /** @var array $compoundResults */ + $compoundResults = []; + + foreach ($units as $unit) { + if ($this->dateInterval->{$unit->getDateIntervalSymbol()} > 0) { + $compoundResults[] = new CompoundResult($unit, (int) $this->dateInterval->{$unit->getDateIntervalSymbol()}); + } + } + + return $compoundResults; + } +} diff --git a/src/Coduo/PHPHumanizer/DateTime/Difference.php b/src/Coduo/PHPHumanizer/DateTime/Difference.php index 2ca8258..25f8025 100644 --- a/src/Coduo/PHPHumanizer/DateTime/Difference.php +++ b/src/Coduo/PHPHumanizer/DateTime/Difference.php @@ -22,9 +22,9 @@ final class Difference { - private \DateTime $fromDate; + private \DateTimeInterface $fromDate; - private \DateTime $toDate; + private \DateTimeInterface $toDate; /** * @psalm-suppress PropertyNotSetInConstructor @@ -33,7 +33,7 @@ final class Difference private ?int $quantity = null; - public function __construct(\DateTime $fromDate, \DateTime $toDate) + public function __construct(\DateTimeInterface $fromDate, \DateTimeInterface $toDate) { $this->fromDate = $fromDate; $this->toDate = $toDate; diff --git a/src/Coduo/PHPHumanizer/DateTime/Difference/CompoundResult.php b/src/Coduo/PHPHumanizer/DateTime/Difference/CompoundResult.php index 512fd1b..7cedcbb 100644 --- a/src/Coduo/PHPHumanizer/DateTime/Difference/CompoundResult.php +++ b/src/Coduo/PHPHumanizer/DateTime/Difference/CompoundResult.php @@ -16,29 +16,29 @@ final class CompoundResult { private Unit $unit; - private int $quantity; - public function __construct(Unit $unit, int $quantity) - { - $this->unit = $unit; - $this->quantity = $quantity; - } + /** + * @var int|float + */ + private $quantity; - public function setQuantity(int $quantity): void + /** + * @param int|float $quantity + */ + public function __construct(Unit $unit, $quantity) { + $this->unit = $unit; $this->quantity = $quantity; } - public function getQuantity() : int + /** + * @return int|float + */ + public function getQuantity() { return $this->quantity; } - public function setUnit(Unit $unit): void - { - $this->unit = $unit; - } - public function getUnit(): Unit { return $this->unit; diff --git a/src/Coduo/PHPHumanizer/DateTime/PreciseDifference.php b/src/Coduo/PHPHumanizer/DateTime/PreciseDifference.php index 0a75a80..c16f59e 100644 --- a/src/Coduo/PHPHumanizer/DateTime/PreciseDifference.php +++ b/src/Coduo/PHPHumanizer/DateTime/PreciseDifference.php @@ -11,65 +11,33 @@ namespace Coduo\PHPHumanizer\DateTime; -use Coduo\PHPHumanizer\DateTime\Unit\Day; -use Coduo\PHPHumanizer\DateTime\Unit\Hour; -use Coduo\PHPHumanizer\DateTime\Unit\Minute; -use Coduo\PHPHumanizer\DateTime\Unit\Month; -use Coduo\PHPHumanizer\DateTime\Unit\Second; -use Coduo\PHPHumanizer\DateTime\Unit\Year; use Coduo\PHPHumanizer\DateTime\Difference\CompoundResult; final class PreciseDifference { - private \DateTime $fromDate; + private \DateTimeInterface $fromDate; - private \DateTime $toDate; + private \DateTimeInterface $toDate; - /** - * @var array - */ - private array $units = []; - - /** - * @var array - */ - private array $compoundResults = []; + private ?DateIntervalCompound $compoundResults ; - public function __construct(\DateTime $fromDate, \DateTime $toDate) + public function __construct(\DateTimeInterface $fromDate, \DateTimeInterface $toDate) { $this->fromDate = $fromDate; $this->toDate = $toDate; - $this->calculate(); + $this->compoundResults = null; } /** * @return array */ - public function getCompoundResults(): array + public function components() : array { - return $this->compoundResults; - } - - private function calculate(): void - { - /* @var $units Unit[] */ - $units = [ - new Year(), - new Month(), - new Day(), - new Hour(), - new Minute(), - new Second(), - ]; - - $diff = $this->fromDate->diff($this->toDate); - - foreach ($units as $unit) { - if ($diff->{$unit->getDateIntervalSymbol()} > 0) { - $this->units[] = $unit; - $this->compoundResults[] = new CompoundResult($unit, $diff->{$unit->getDateIntervalSymbol()}); - } + if ($this->compoundResults === null) { + $this->compoundResults = new DateIntervalCompound($this->fromDate->diff($this->toDate)); } + + return $this->compoundResults->components(); } public function isPast(): bool diff --git a/src/Coduo/PHPHumanizer/DateTime/PreciseFormatter.php b/src/Coduo/PHPHumanizer/DateTime/PreciseFormatter.php index 2036fb9..45a1f7f 100644 --- a/src/Coduo/PHPHumanizer/DateTime/PreciseFormatter.php +++ b/src/Coduo/PHPHumanizer/DateTime/PreciseFormatter.php @@ -11,6 +11,7 @@ namespace Coduo\PHPHumanizer\DateTime; +use Coduo\PHPHumanizer\CollectionHumanizer; use Symfony\Contracts\Translation\TranslatorInterface; final class PreciseFormatter @@ -26,7 +27,7 @@ public function formatDifference(PreciseDifference $difference, string $locale = { $diff = []; - foreach ($difference->getCompoundResults() as $result) { + foreach ($difference->components() as $result) { $diff[] = $this->translator->trans( 'compound.'.$result->getUnit()->getName(), ['%count%' => $result->getQuantity()], @@ -42,4 +43,20 @@ public function formatDifference(PreciseDifference $difference, string $locale = $locale ); } + + public function formatInterval(\DateInterval $dateInterval, string $locale = 'en') : string + { + $parts = []; + + foreach ((new DateIntervalCompound($dateInterval))->components() as $component) { + $parts[] = $this->translator->trans( + 'compound.'.$component->getUnit()->getName(), + ['%count%' => $component->getQuantity()], + 'difference', + $locale + ); + } + + return CollectionHumanizer::oxford($parts, null, $locale); + } } diff --git a/src/Coduo/PHPHumanizer/DateTimeHumanizer.php b/src/Coduo/PHPHumanizer/DateTimeHumanizer.php index 2c51cf5..2e6951d 100644 --- a/src/Coduo/PHPHumanizer/DateTimeHumanizer.php +++ b/src/Coduo/PHPHumanizer/DateTimeHumanizer.php @@ -11,6 +11,8 @@ namespace Coduo\PHPHumanizer; +use Aeon\Calendar\Gregorian\TimePeriod; +use Aeon\Calendar\Unit; use Coduo\PHPHumanizer\DateTime\Difference; use Coduo\PHPHumanizer\DateTime\PreciseDifference; use Coduo\PHPHumanizer\DateTime\Formatter; @@ -19,17 +21,38 @@ final class DateTimeHumanizer { - public static function difference(\DateTime $fromDate, \DateTime $toDate, string $locale = 'en'): string + public static function difference(\DateTimeInterface $fromDate, \DateTimeInterface $toDate, string $locale = 'en'): string { $formatter = new Formatter(Builder::build($locale)); return $formatter->formatDifference(new Difference($fromDate, $toDate), $locale); } - public static function preciseDifference(\DateTime $fromDate, \DateTime $toDate, string $locale = 'en'): string + public static function preciseDifference(\DateTimeInterface $fromDate, \DateTimeInterface $toDate, string $locale = 'en'): string { $formatter = new PreciseFormatter(Builder::build($locale)); return $formatter->formatDifference(new PreciseDifference($fromDate, $toDate), $locale); } + + public static function timeUnit(Unit $unit, string $locale = 'en') : string + { + $formatter = new Aeon\Calendar\Formatter(Builder::build($locale)); + + return $formatter->timeUnit($unit, $locale); + } + + public static function timePeriod(TimePeriod $timePeriod, string $locale = 'en') : string + { + $formatter = new Formatter(Builder::build($locale)); + + return $formatter->formatDifference(new Difference($timePeriod->start()->toDateTimeImmutable(), $timePeriod->end()->toDateTimeImmutable()), $locale); + } + + public static function timePeriodPrecise(TimePeriod $timePeriod, string $locale = 'en') : string + { + $formatter = new PreciseFormatter(Builder::build($locale)); + + return $formatter->formatDifference(new PreciseDifference($timePeriod->start()->toDateTimeImmutable(), $timePeriod->end()->toDateTimeImmutable()), $locale); + } } diff --git a/tests/Coduo/PHPHumanizer/Tests/Aeon/Calendar/FormatterTest.php b/tests/Coduo/PHPHumanizer/Tests/Aeon/Calendar/FormatterTest.php new file mode 100644 index 0000000..dca4d01 --- /dev/null +++ b/tests/Coduo/PHPHumanizer/Tests/Aeon/Calendar/FormatterTest.php @@ -0,0 +1,49 @@ +add(TimeUnit::hours(3)) + ->add(TimeUnit::minutes(25)) + ->add(TimeUnit::seconds(30)) + ->add(TimeUnit::milliseconds(200)); + + $formatter = new Formatter(Builder::build('en')); + + $this->assertSame( + '2 days, 3 hours, 25 minutes, and 30.2 seconds', + $formatter->timeUnit($timeUnit) + ); + } + + public function test_format_relative_time_unit() : void + { + $timeUnit = RelativeTimeUnit::months(14); + + $formatter = new Formatter(Builder::build('en')); + + $this->assertSame( + '1 year and 2 months', + $formatter->timeUnit($timeUnit) + ); + } +} diff --git a/tests/Coduo/PHPHumanizer/Tests/DateTime/PreciseFormatterTest.php b/tests/Coduo/PHPHumanizer/Tests/DateTime/PreciseFormatterTest.php index a14cb81..99b1c11 100644 --- a/tests/Coduo/PHPHumanizer/Tests/DateTime/PreciseFormatterTest.php +++ b/tests/Coduo/PHPHumanizer/Tests/DateTime/PreciseFormatterTest.php @@ -41,4 +41,16 @@ public function test_format_compound_datetime_diff_for_specific_locale() $this->assertSame('через 10 дней, 5 часов', $formatter->formatDifference($diff, 'ru')); } + + public function test_format_date_interval() : void + { + $interval = new \DateInterval('P1DT5H25M43S'); + + $formatter = new PreciseFormatter(Builder::build('en')); + + $this->assertSame( + '1 day, 5 hours, 25 minutes, and 43 seconds', + $formatter->formatInterval($interval) + ); + } } diff --git a/tests/Coduo/PHPHumanizer/Tests/DateTimeHumanizerTest.php b/tests/Coduo/PHPHumanizer/Tests/DateTimeHumanizerTest.php index a4c441e..23ce537 100644 --- a/tests/Coduo/PHPHumanizer/Tests/DateTimeHumanizerTest.php +++ b/tests/Coduo/PHPHumanizer/Tests/DateTimeHumanizerTest.php @@ -11,42 +11,57 @@ namespace Coduo\PHPHumanizer\Tests; +use Aeon\Calendar\Gregorian\DateTime; +use Aeon\Calendar\Gregorian\TimePeriod; +use Aeon\Calendar\RelativeTimeUnit; +use Aeon\Calendar\TimeUnit; +use Aeon\Calendar\Unit; use Coduo\PHPHumanizer\DateTimeHumanizer; use PHPUnit\Framework\TestCase; class DateTimeHumanizerTest extends TestCase { /** - * @test * @dataProvider humanizeDataProvider - * - * @param $firstDate - * @param $secondDate - * @param $expected - * @param string $locale */ - public function test_humanize_difference_between_dates($firstDate, $secondDate, $expected, $locale) + public function test_humanize_difference_between_dates(string $firstDate, string $secondDate, string $expected, string $locale) : void { $this->assertEquals($expected, DateTimeHumanizer::difference(new \DateTime($firstDate), new \DateTime($secondDate), $locale)); } + /** + * @dataProvider humanizeDataProvider + */ + public function test_humanize_time_period(string $firstDate, string $secondDate, string $expected, string $locale) : void + { + $this->assertEquals($expected, DateTimeHumanizer::timePeriod(new TimePeriod(DateTime::fromString($firstDate), DateTime::fromString($secondDate)), $locale)); + } + /** * @dataProvider preciseDifferenceDataProvider - * - * @param $firstDate - * @param $secondDate - * @param $expected - * @param string $locale */ - public function test_humanize_precise_difference_between_dates($firstDate, $secondDate, $expected, $locale) + public function test_humanize_precise_difference_between_dates(string $firstDate, string $secondDate, string $expected, string $locale) : void + { + $this->assertEquals($expected, DateTimeHumanizer::timePeriodPrecise(new TimePeriod(DateTime::fromString($firstDate), DateTime::fromString($secondDate)), $locale)); + } + + /** + * @dataProvider preciseDifferenceDataProvider + */ + public function test_humanize_time_period_precise(string $firstDate, string $secondDate, string $expected, string $locale) : void { $this->assertEquals($expected, DateTimeHumanizer::preciseDifference(new \DateTime($firstDate), new \DateTime($secondDate), $locale)); } /** - * @return array + * @dataProvider timeUnitDataProvider */ - public function humanizeDataProvider() + public function test_humanize_time_unit(Unit $unit, string $expected, string $locale) : void + { + $this->assertSame($expected, DateTimeHumanizer::timeUnit($unit, $locale)); + } + + public function humanizeDataProvider() : array { return [ // English @@ -264,10 +279,7 @@ public function humanizeDataProvider() ]; } - /** - * @return array - */ - public function preciseDifferenceDataProvider() + public function preciseDifferenceDataProvider() : array { return [ // Azerbaijani @@ -461,4 +473,39 @@ public function preciseDifferenceDataProvider() ['2014-04-26 13:00:00', '2016-04-27 13:00:00', '2 年, 1 日後', 'ja'], ]; } + + public function timeUnitDataProvider() : array + { + return [ + // English + [TimeUnit::seconds(20), '20 seconds', 'en'], + [TimeUnit::minutes(20), '20 minutes', 'en'], + [TimeUnit::minutes(20)->add(TimeUnit::seconds(5)), '20 minutes and 5 seconds', 'en'], + [ + TimeUnit::days(2) + ->add(TimeUnit::hours(3)) + ->add(TimeUnit::minutes(25)) + ->add(TimeUnit::seconds(30)) + ->add(TimeUnit::milliseconds(200)), + '2 days, 3 hours, 25 minutes, and 30.2 seconds', + 'en' + ], + [RelativeTimeUnit::months(14), '1 year and 2 months', 'en'], + + // Polish + [TimeUnit::seconds(20), '20 sekund', 'pl'], + [TimeUnit::minutes(20), '20 minut', 'pl'], + [TimeUnit::minutes(20)->add(TimeUnit::seconds(5)), '20 minut i 5 sekund', 'pl'], + [ + TimeUnit::days(2) + ->add(TimeUnit::hours(3)) + ->add(TimeUnit::minutes(25)) + ->add(TimeUnit::seconds(30)) + ->add(TimeUnit::milliseconds(200)), + '2 dni, 3 godziny, 25 minut i 30.2 sekund', + 'pl' + ], + [RelativeTimeUnit::months(14), '1 rok i 2 miesiące', 'pl'] + ]; + } }