-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add formatting API to LocalDate, LocalDateTime, LocalTime, and ZonedD…
…ateTime
- Loading branch information
Showing
13 changed files
with
924 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Brick\DateTime\Formatter; | ||
|
||
use Brick\DateTime\Field; | ||
use Brick\DateTime\LocalDate; | ||
use Brick\DateTime\LocalDateTime; | ||
use Brick\DateTime\LocalTime; | ||
use Brick\DateTime\ZonedDateTime; | ||
|
||
use function abs; | ||
use function array_shift; | ||
use function floor; | ||
use function sprintf; | ||
|
||
/** | ||
* An intermediate representation of a formatted date-time value. | ||
*/ | ||
final class DateTimeFormatContext | ||
{ | ||
/** @var LocalDate|LocalDateTime|LocalTime|ZonedDateTime */ | ||
private $value; | ||
|
||
/** @var array<string, list<string>> */ | ||
private array $fields = []; | ||
|
||
/** | ||
* @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value | ||
*/ | ||
private function __construct($value) | ||
{ | ||
$this->value = $value; | ||
} | ||
|
||
public static function ofLocalDate(LocalDate $localDate): self | ||
{ | ||
$self = new self($localDate); | ||
$self->addField(Field\DayOfMonth::NAME, (string) $localDate->getDay()); | ||
$self->addField(Field\DayOfWeek::NAME, (string) $localDate->getDayOfWeek()->getValue()); | ||
$self->addField(Field\DayOfYear::NAME, (string) $localDate->getDayOfYear()); | ||
$self->addField(Field\WeekOfYear::NAME, (string) $localDate->getYearWeek()->getWeek()); | ||
$self->addField(Field\MonthOfYear::NAME, (string) $localDate->getMonth()); | ||
$self->addField(Field\Year::NAME, (string) $localDate->getYear()); | ||
|
||
return $self; | ||
} | ||
|
||
public static function ofLocalTime(LocalTime $localTime): self | ||
{ | ||
$self = new self($localTime); | ||
$self->addField(Field\HourOfDay::NAME, (string) $localTime->getHour()); | ||
$self->addField(Field\MinuteOfHour::NAME, (string) $localTime->getMinute()); | ||
$self->addField(Field\SecondOfMinute::NAME, (string) $localTime->getSecond()); | ||
$self->addField(Field\NanoOfSecond::NAME, (string) $localTime->getNano()); | ||
$self->addField(Field\FractionOfSecond::NAME, (string) $localTime->getNano()); | ||
|
||
return $self; | ||
} | ||
|
||
public static function ofLocalDateTime(LocalDateTime $localDateTime): self | ||
{ | ||
$self = new self($localDateTime); | ||
$self->addField(Field\DayOfMonth::NAME, (string) $localDateTime->getDate()->getDay()); | ||
$self->addField(Field\DayOfWeek::NAME, (string) $localDateTime->getDate()->getDayOfWeek()->getValue()); | ||
$self->addField(Field\DayOfYear::NAME, (string) $localDateTime->getDate()->getDayOfYear()); | ||
$self->addField(Field\WeekOfYear::NAME, (string) $localDateTime->getDate()->getYearWeek()->getWeek()); | ||
$self->addField(Field\MonthOfYear::NAME, (string) $localDateTime->getDate()->getMonth()); | ||
$self->addField(Field\Year::NAME, (string) $localDateTime->getDate()->getYear()); | ||
$self->addField(Field\HourOfDay::NAME, (string) $localDateTime->getTime()->getHour()); | ||
$self->addField(Field\MinuteOfHour::NAME, (string) $localDateTime->getTime()->getMinute()); | ||
$self->addField(Field\SecondOfMinute::NAME, (string) $localDateTime->getTime()->getSecond()); | ||
$self->addField(Field\NanoOfSecond::NAME, (string) $localDateTime->getTime()->getNano()); | ||
$self->addField(Field\FractionOfSecond::NAME, (string) $localDateTime->getTime()->getNano()); | ||
|
||
return $self; | ||
} | ||
|
||
public static function ofZonedDateTime(ZonedDateTime $zonedDateTime): self | ||
{ | ||
$self = new self($zonedDateTime); | ||
$self->addField(Field\DayOfMonth::NAME, (string) $zonedDateTime->getDate()->getDay()); | ||
$self->addField(Field\DayOfWeek::NAME, (string) $zonedDateTime->getDate()->getDayOfWeek()->getValue()); | ||
$self->addField(Field\DayOfYear::NAME, (string) $zonedDateTime->getDate()->getDayOfYear()); | ||
$self->addField(Field\WeekOfYear::NAME, (string) $zonedDateTime->getDate()->getYearWeek()->getWeek()); | ||
$self->addField(Field\MonthOfYear::NAME, (string) $zonedDateTime->getDate()->getMonth()); | ||
$self->addField(Field\Year::NAME, (string) $zonedDateTime->getDate()->getYear()); | ||
$self->addField(Field\HourOfDay::NAME, (string) $zonedDateTime->getTime()->getHour()); | ||
$self->addField(Field\MinuteOfHour::NAME, (string) $zonedDateTime->getTime()->getMinute()); | ||
$self->addField(Field\SecondOfMinute::NAME, (string) $zonedDateTime->getTime()->getSecond()); | ||
$self->addField(Field\NanoOfSecond::NAME, (string) $zonedDateTime->getTime()->getNano()); | ||
$self->addField(Field\FractionOfSecond::NAME, (string) $zonedDateTime->getTime()->getNano()); | ||
$self->addField(Field\TimeZoneOffsetHour::NAME, sprintf('%d', floor(abs($zonedDateTime->getTimeZoneOffset()->getTotalSeconds()) / LocalTime::SECONDS_PER_HOUR))); | ||
$self->addField(Field\TimeZoneOffsetMinute::NAME, (string) ((abs($zonedDateTime->getTimeZoneOffset()->getTotalSeconds()) % LocalTime::SECONDS_PER_HOUR) / LocalTime::SECONDS_PER_MINUTE)); | ||
$self->addField(Field\TimeZoneOffsetSign::NAME, $zonedDateTime->getTimeZoneOffset()->getTotalSeconds() === 0 ? 'Z' : ($zonedDateTime->getTimeZoneOffset()->getTotalSeconds() > 0 ? '+' : '-')); | ||
$self->addField(Field\TimeZoneOffsetTotalSeconds::NAME, (string) $zonedDateTime->getTimeZoneOffset()->getTotalSeconds()); | ||
$self->addField(Field\TimeZoneRegion::NAME, $zonedDateTime->getTimeZone()->getId()); | ||
|
||
return $self; | ||
} | ||
|
||
public function addField(string $name, string $value): void | ||
{ | ||
$this->fields[$name][] = $value; | ||
} | ||
|
||
public function hasField(string $name): bool | ||
{ | ||
return isset($this->fields[$name]) && $this->fields[$name]; | ||
} | ||
|
||
public function getField(string $name): string | ||
{ | ||
$value = $this->getOptionalField($name); | ||
|
||
if ($value === '') { | ||
throw new DateTimeFormatException(sprintf('Field %s is not present in the formatting context.', $name)); | ||
} | ||
|
||
return $value; | ||
} | ||
|
||
public function getOptionalField(string $name): string | ||
{ | ||
if (isset($this->fields[$name])) { | ||
if ($this->fields[$name]) { | ||
return array_shift($this->fields[$name]); | ||
} | ||
} | ||
|
||
return ''; | ||
} | ||
|
||
/** | ||
* @return LocalDate|LocalDateTime|LocalTime|ZonedDateTime | ||
*/ | ||
public function getValue() | ||
{ | ||
return $this->value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Brick\DateTime\Formatter; | ||
|
||
use Brick\DateTime\DateTimeException; | ||
|
||
/** | ||
* Exception thrown when a formatting error occurs. | ||
*/ | ||
class DateTimeFormatException extends DateTimeException | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Brick\DateTime\Formatter; | ||
|
||
/** | ||
* Interface that all date-time formatters must implement. | ||
*/ | ||
interface DateTimeFormatter | ||
{ | ||
/** | ||
* @param DateTimeFormatContext $context Formatting context. | ||
* | ||
* @return string The formatted value. | ||
* | ||
* @throws DateTimeFormatException If the given context could not be formatted. | ||
*/ | ||
public function format(DateTimeFormatContext $context): string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Brick\DateTime\Formatter; | ||
|
||
use Brick\DateTime\LocalDate; | ||
use Brick\DateTime\LocalDateTime; | ||
use Brick\DateTime\LocalTime; | ||
use Brick\DateTime\ZonedDateTime; | ||
use DateTimeZone; | ||
use IntlDateFormatter; | ||
use IntlDatePatternGenerator; | ||
|
||
use function array_keys; | ||
use function extension_loaded; | ||
use function get_class; | ||
use function in_array; | ||
use function sprintf; | ||
use function str_split; | ||
|
||
use const PHP_VERSION_ID; | ||
|
||
/** | ||
* Formats the value using the Intl extension. | ||
*/ | ||
final class IntlFormatter implements DateTimeFormatter | ||
{ | ||
public const FULL = IntlDateFormatter::FULL; | ||
public const LONG = IntlDateFormatter::LONG; | ||
public const MEDIUM = IntlDateFormatter::MEDIUM; | ||
public const SHORT = IntlDateFormatter::SHORT; | ||
|
||
private string $locale; | ||
|
||
private int $dateFormat; | ||
|
||
private int $timeFormat; | ||
|
||
private string $pattern; | ||
|
||
private function __construct(string $locale, int $dateFormat, int $timeFormat, string $pattern) | ||
{ | ||
$this->locale = $locale; | ||
$this->dateFormat = $dateFormat; | ||
$this->timeFormat = $timeFormat; | ||
$this->pattern = $pattern; | ||
|
||
if (! extension_loaded('intl')) { | ||
throw new DateTimeFormatException('IntlFormatter requires ext-intl to be installed and enabled.'); | ||
} | ||
} | ||
|
||
/** | ||
* Returns a formatter of given type for a date value. | ||
*/ | ||
public static function ofDate(string $locale, int $format): self | ||
{ | ||
return new self($locale, $format, IntlDateFormatter::NONE, ''); | ||
} | ||
|
||
/** | ||
* Returns a formatter of given type for a time value. | ||
*/ | ||
public static function ofTime(string $locale, int $format): self | ||
{ | ||
return new self($locale, IntlDateFormatter::NONE, $format, ''); | ||
} | ||
|
||
/** | ||
* Returns a formatter of given type for a date-time value. | ||
*/ | ||
public static function ofDateTime(string $locale, int $dateFormat, int $timeFormat): self | ||
{ | ||
return new self($locale, $dateFormat, $timeFormat, ''); | ||
} | ||
|
||
/** | ||
* Returns a formatter with given ICU SimpleFormat pattern. | ||
*/ | ||
public static function ofPattern(string $locale, string $pattern): self | ||
{ | ||
return new self($locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $pattern); | ||
} | ||
|
||
/** | ||
* Returns a formatter with a pattern that best matches given skeleton. | ||
*/ | ||
public static function ofSkeleton(string $locale, string $skeleton): self | ||
{ | ||
if (PHP_VERSION_ID < 80100) { | ||
throw new DateTimeFormatException('IntlFormatter::ofSkeleton() is only available in PHP 8.1 and above.'); | ||
} | ||
|
||
$generator = new IntlDatePatternGenerator($locale); | ||
$pattern = $generator->getBestPattern($skeleton); | ||
|
||
if ($pattern === false) { | ||
throw new DateTimeFormatException('Failed to resolve the best formatting pattern for given locale and skeleton.'); | ||
} | ||
|
||
return self::ofPattern($locale, $pattern); | ||
} | ||
|
||
public function format(DateTimeFormatContext $context): string | ||
{ | ||
$value = $context->getValue(); | ||
|
||
if ($this->dateFormat !== IntlDateFormatter::NONE && $value instanceof LocalTime) { | ||
throw new DateTimeFormatException('IntlFormatter with a date part cannot be used to format Brick\DateTime\LocalTime.'); | ||
} | ||
|
||
if ($this->timeFormat !== IntlDateFormatter::NONE && $value instanceof LocalDate) { | ||
throw new DateTimeFormatException('IntlFormatter with a time part cannot be used to format Brick\DateTime\LocalDate.'); | ||
} | ||
|
||
if (($this->timeFormat === self::FULL || $this->timeFormat === self::LONG) && ! ($value instanceof ZonedDateTime)) { | ||
throw new DateTimeFormatException(sprintf('IntlFormatter with a long or full time part cannot be used to format %s.', get_class($value))); | ||
} | ||
|
||
if ($this->pattern !== '') { | ||
self::checkPattern($this->pattern, $value); | ||
} | ||
|
||
$timeZone = $value instanceof ZonedDateTime ? $value->getTimeZone()->toNativeDateTimeZone() : new DateTimeZone('UTC'); | ||
$formatter = new IntlDateFormatter($this->locale, $this->dateFormat, $this->timeFormat, $timeZone, null, $this->pattern); | ||
|
||
return $formatter->format($value->toNativeDateTimeImmutable()); | ||
} | ||
|
||
/** | ||
* @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value | ||
*/ | ||
private static function checkPattern(string $pattern, $value): void | ||
{ | ||
$supportedTypesMap = [ | ||
LocalDate::class => true, | ||
LocalDateTime::class => true, | ||
LocalTime::class => true, | ||
ZonedDateTime::class => true, | ||
]; | ||
|
||
$inString = false; | ||
foreach (str_split($pattern) as $character) { | ||
if ($character === '\'') { | ||
if ($inString) { | ||
$inString = false; | ||
|
||
continue; | ||
} | ||
|
||
$inString = true; | ||
|
||
continue; | ||
} | ||
|
||
if ($inString) { | ||
continue; | ||
} | ||
|
||
if (in_array($character, ['G', 'y', 'Y', 'u', 'U', 'r', 'Q', 'q', 'M', 'L', 'w', 'W', 'd', 'D', 'F', 'g', 'E', 'e', 'c'], true)) { | ||
$supportedTypesMap[LocalTime::class] = false; | ||
} | ||
|
||
if (in_array($character, ['a', 'h', 'H', 'k', 'K', 'm', 's', 'S', 'A'], true)) { | ||
$supportedTypesMap[LocalDate::class] = false; | ||
} | ||
|
||
if (in_array($character, ['z', 'Z', 'O', 'v', 'V', 'X', 'x'], true)) { | ||
$supportedTypesMap[LocalDate::class] = false; | ||
$supportedTypesMap[LocalDateTime::class] = false; | ||
$supportedTypesMap[LocalTime::class] = false; | ||
} | ||
} | ||
|
||
$supportedTypes = array_keys($supportedTypesMap, true, true); | ||
foreach ($supportedTypes as $supportedType) { | ||
if ($value instanceof $supportedType) { | ||
return; | ||
} | ||
} | ||
|
||
throw new DateTimeFormatException(sprintf("IntlFormatter with pattern '%s' is incompatible with type %s.", $pattern, get_class($value))); | ||
} | ||
} |
Oops, something went wrong.