Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relative date format feature #26

Merged
merged 14 commits into from
Nov 25, 2024
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ NumberParser::parseFloat('1,000,050.5');
Formats dates and times.
Input can be timestamps, strings (compatible with strtotime) and DateTimeInterface objects
```php
$dateFormatter = new DateFormatService('nl_NL', date_default_timezone_get());
$dateFormatOptions = new DateFormatOptions('nl_NL', date_default_timezone_get())
$dateFormatter = new DateFormatService($dateFormatOptions);

$dateFormatter->format(time(), 'eeee dd LLLL Y - HH:mm:ss');
// example output: zaterdag 02 juni 2040 - 05:57:02

Expand All @@ -100,6 +102,32 @@ $dateFormatter->format(new DateTime(), 'eeee dd LLLL Y - HH:mm:ss');
// example output: zaterdag 02 juni 2040 - 05:57:02
```

It is also possible to format dates and times to relative dates, such as 'today' and 'tomorrow'
The RelativeDateFormatOptions decides how many days ahead it will try to convert a date to a relative date.

```php
$dateFormatOptions = new DateFormatOptions('nl_NL', date_default_timezone_get())
$dateFormatter = new DateFormatService($dateFormatOptions);

$dateFormatter->formatRelative(time(), 'Y-m-d', new RelativeDateFormatOptions(1));
// example output: Vandaag

$dateFormatter->formatRelative(new DateTime('+1 day'), 'Y-m-d', new RelativeDateFormatOptions(1);
// example output: Morgen

$dateFormatter->formatRelative(new DateTime('+2 days'), 'Y-m-d', new RelativeDateFormatOptions(2));
// example output: Overmorgen

$dateFormatter->formatRelative(new DateTime('-2 days'), 'Y-m-d', new RelativeDateFormatOptions(2));
// example output: Eergisteren

// This will not convert the date to a relative date, as the options limit it one day ahead. Instead, it formats the date to the given pattern.
$dateFormatter->formatRelative(new DateTime('+2 days'), 'Y-m-d', new RelativeDateFormatOptions(1));
// example output: 2024-01-03
```



### DayOfTheWeekFormatter
Format the PHP Date day of the week to string

Expand Down
51 changes: 51 additions & 0 deletions src/Date/DateFormatHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

use DateTimeImmutable;
use DateTimeInterface;
use IntlDateFormatter;
use RuntimeException;

class DateFormatHelper
{
private DateFormatterCacheInterface $cache;
private DateFormatterFactory $dateFactory;

public function __construct(?DateFormatterCacheInterface $cache = null, ?DateFormatterFactory $dateFactory = null)
{
$this->cache = $cache ?? new DateFormatterCache();
$this->dateFactory = $dateFactory ?? new DateFormatterFactory();
}

public function getDateFormatter(DateFormatOptions $options, string $pattern): IntlDateFormatter
{
// Get or create from cache.
return $this->cache->get($options . $pattern, fn() => $this->dateFactory->create($options, $pattern));
}

public function getParsedDate(int|string|DateTimeInterface $date): DateTimeInterface
{
if (is_string($date)) {
return new DateTimeImmutable($date);
}

if (is_int($date)) {
return new DateTimeImmutable('@' . $date);
}

return $date;
}

public function validateResult(bool|string|null $result, int|string|DateTimeInterface $value, string $pattern): string
{
if (is_bool($result) || $result === null) {
$scalarValue = $value instanceof DateTimeInterface ? $value->getTimestamp() : $value;
throw new RuntimeException(sprintf('Unable to format date `%s` to format `%s`', $scalarValue, $pattern));
}

return $result;
}
}
125 changes: 125 additions & 0 deletions src/Date/DateFormatOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

use IntlDateFormatter;
use IntlTimeZone;

/**
* @phpstan-type CalendarType IntlDateFormatter::GREGORIAN|IntlDateFormatter::TRADITIONAL
* @phpstan-type DateFormatType IntlTimeZone::*
*/
class DateFormatOptions
{
/** @phpstan-var DateFormatType $dateType */
protected int $dateType = IntlDateFormatter::FULL;

/** @phpstan-var DateFormatType $timeType */
protected int $timeType = IntlDateFormatter::FULL;

/** @phpstan-var CalendarType $calendar */
protected int $calendar = IntlDateFormatter::GREGORIAN;

public function __construct(protected string $locale, protected string $timezone)
{
}

public function getLocale(): string
{
return $this->locale;
}

/**
* Set the preferred locale for the formatting. Expects an POSIX code (nl_NL, nl_BE, en_GB, etc...). Defaults to system configuration.
* @return static
*/
public function setLocale(string $locale): self
{
$this->locale = $locale;

return $this;
}

public function getTimezone(): string
{
return $this->timezone;
}

/**
* Set the preferred timezone for the formatting. Expects a timezone identifier (Europe/Amsterdam, UTC, etc...). Defaults to system configuration.
* @return static
*/
public function setTimezone(string $timezone): self
{
$this->timezone = $timezone;

return $this;
}

/**
* @phpstan-return DateFormatType
*/
public function getDateType(): int
{
return $this->dateType;
}

/**
* @phpstan-param DateFormatType $dateType
*/
public function setDateType(int $dateType): DateFormatOptions
{
$this->dateType = $dateType;

return $this;
}

/**
* @phpstan-return DateFormatType
*/
public function getTimeType(): int
{
return $this->timeType;
}

/**
* @phpstan-param DateFormatType $timeType
*/
public function setTimeType(int $timeType): DateFormatOptions
{
$this->timeType = $timeType;

return $this;
}

/**
* @phpstan-return CalendarType
*/
public function getCalendar(): int
{
return $this->calendar;
}

/**
* @phpstan-param CalendarType $calendar
*/
public function setCalendar(int $calendar): DateFormatOptions
{
$this->calendar = $calendar;

return $this;
}

public function __toString(): string
{
return "date:" . serialize([
'locale' => $this->locale,
'timezone' => $this->timezone,
'dateType' => $this->dateType,
'timeType' => $this->timeType,
'calendar' => $this->calendar,
]);
}
}
20 changes: 20 additions & 0 deletions src/Date/DateFormatterCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);

namespace DR\Internationalization\Date;

use IntlDateFormatter;

/**
* @internal
*/
class DateFormatterCache implements DateFormatterCacheInterface
{
/** @var IntlDateFormatter[] */
private array $formatters = [];

public function get(string $key, callable $factoryCallback): IntlDateFormatter
{
return $this->formatters[$key] ??= $factoryCallback();
}
}
15 changes: 15 additions & 0 deletions src/Date/DateFormatterCacheInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

use IntlDateFormatter;

interface DateFormatterCacheInterface
{
/**
* @param callable():IntlDateFormatter $factoryCallback
*/
public function get(string $key, callable $factoryCallback): IntlDateFormatter;
}
21 changes: 21 additions & 0 deletions src/Date/DateFormatterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);

namespace DR\Internationalization\Date;

use IntlDateFormatter;

class DateFormatterFactory
{
public function create(DateFormatOptions $options, string $pattern): IntlDateFormatter
{
return new IntlDateFormatter(
$options->getLocale(),
$options->getDateType(),
$options->getTimeType(),
$options->getTimezone(),
$options->getCalendar(),
$pattern
);
}
}
22 changes: 22 additions & 0 deletions src/Date/RelativeDateFallbackResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

class RelativeDateFallbackResult
{
public function __construct(private readonly bool $fallback, private readonly string $date = '')
{
}

public function isFallback(): bool
{
return $this->fallback;
}

public function getDate(): string
{
return $this->date;
}
}
58 changes: 58 additions & 0 deletions src/Date/RelativeDateFallbackService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

use DateTimeImmutable;
use DateTimeInterface;
use RuntimeException;

class RelativeDateFallbackService
{
private const MAX_TRANSLATABLE_DAYS_AMOUNT = 4;
private RelativeDateFormatterFactory $relativeFormatterFactory;

public function __construct(?RelativeDateFormatterFactory $relativeFormatterFactory = null)
{
$this->relativeFormatterFactory = $relativeFormatterFactory ?? new RelativeDateFormatterFactory();
}

public function getFallbackResult(
string $locale,
DateTimeInterface $dateTime,
RelativeDateFormatOptions $relativeOptions
): RelativeDateFallbackResult {
$currentDateTime = (new DateTimeImmutable())->setTime(0, 0);

if ($dateTime->diff($currentDateTime)->d > self::MAX_TRANSLATABLE_DAYS_AMOUNT
|| $relativeOptions->getRelativeDaysAmount() === 0
|| $relativeOptions->getRelativeDaysAmount() === null
|| $dateTime->diff($currentDateTime)->d > $relativeOptions->getRelativeDaysAmount()
) {
return new RelativeDateFallbackResult(true);
}

$relativeDateFormatter = $this->relativeFormatterFactory->createRelativeFull($locale);
$fullDateFormatter = $this->relativeFormatterFactory->createFull($locale);

$relativeDate = $relativeDateFormatter->format($dateTime);
$fullDate = $fullDateFormatter->format($dateTime);

if ($relativeDate === false) {
throw new RuntimeException(
sprintf(
'An error occurred while trying to parse the relative date. Error code: %s, %s',
$relativeDateFormatter->getErrorCode(),
$relativeDateFormatter->getErrorMessage()
)
);
}

if ($relativeDate === $fullDate) {
return new RelativeDateFallbackResult(true);
}

return new RelativeDateFallbackResult(false, $relativeDate);
}
}
17 changes: 17 additions & 0 deletions src/Date/RelativeDateFormatOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace DR\Internationalization\Date;

class RelativeDateFormatOptions
{
public function __construct(private readonly ?int $relativeDaysAmount)
{
}

public function getRelativeDaysAmount(): ?int
{
return $this->relativeDaysAmount;
}
}
Loading