Skip to content
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())->setTimestamp($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