diff --git a/composer.json b/composer.json index b717619..2a0207e 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "vimeo/psalm": "4.23.0" }, "suggest": { + "ext-intl": "This extension is required for locale-based formatting", "ext-timezonedb": "This PECL extension provides up-to-date timezone information" }, "autoload": { diff --git a/src/Formatter/DateTimeFormatContext.php b/src/Formatter/DateTimeFormatContext.php new file mode 100644 index 0000000..b1e1bd7 --- /dev/null +++ b/src/Formatter/DateTimeFormatContext.php @@ -0,0 +1,142 @@ +> */ + 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; + } +} diff --git a/src/Formatter/DateTimeFormatException.php b/src/Formatter/DateTimeFormatException.php new file mode 100644 index 0000000..2ac9af0 --- /dev/null +++ b/src/Formatter/DateTimeFormatException.php @@ -0,0 +1,14 @@ +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))); + } +} diff --git a/src/Formatter/NativeFormatter.php b/src/Formatter/NativeFormatter.php new file mode 100644 index 0000000..aaba7fd --- /dev/null +++ b/src/Formatter/NativeFormatter.php @@ -0,0 +1,95 @@ + */ + private array $supportedValueTypes; + + private function __construct(string $format) + { + $this->format = $format; + $this->supportedValueTypes = self::getSupportedValueTypes($format); + } + + public static function of(string $format): self + { + return new self($format); + } + + public function format(DateTimeFormatContext $context): string + { + $value = $context->getValue(); + + foreach ($this->supportedValueTypes as $supportedValueType) { + if ($value instanceof $supportedValueType) { + return $value->toNativeDateTimeImmutable()->format($this->format); + } + } + + throw new DateTimeFormatException(sprintf("Formatting pattern '%s' is incompatible with type %s.", $this->format, get_class($value))); + } + + /** + * @return list + */ + private static function getSupportedValueTypes(string $format): array + { + $supported = [ + LocalDate::class => true, + LocalDateTime::class => true, + LocalTime::class => true, + ZonedDateTime::class => true, + ]; + + $escaped = false; + foreach (str_split($format) as $character) { + if ($character === '\\') { + $escaped = true; + + continue; + } + + if ($escaped) { + $escaped = false; + + continue; + } + + if (in_array($character, ['d', 'j', 'S', 'D', 'l', 'N', 'w', 'z', 'W', 'o', 'F', 'm', 'M', 'n', 't', 'L', 'Y', 'y'], true)) { + $supported[LocalTime::class] = false; + } + + if (in_array($character, ['a', 'A', 'g', 'G', 'h', 'H', 'B', 'i', 's', 'v', 'u'], true)) { + $supported[LocalDate::class] = false; + } + + if (in_array($character, ['e', 'T', 'I', 'c', 'r', 'U', 'O', 'P', 'p', 'Z'], true)) { + $supported[LocalDate::class] = false; + $supported[LocalDateTime::class] = false; + $supported[LocalTime::class] = false; + } + } + + return array_keys($supported, true, true); + } +} diff --git a/src/LocalDate.php b/src/LocalDate.php index eb5c283..ec815b0 100644 --- a/src/LocalDate.php +++ b/src/LocalDate.php @@ -4,6 +4,8 @@ namespace Brick\DateTime; +use Brick\DateTime\Formatter\DateTimeFormatContext; +use Brick\DateTime\Formatter\DateTimeFormatter; use Brick\DateTime\Parser\DateTimeParseException; use Brick\DateTime\Parser\DateTimeParser; use Brick\DateTime\Parser\DateTimeParseResult; @@ -767,6 +769,16 @@ public function jsonSerialize(): string return (string) $this; } + /** + * Formats this LocalDate using given DateTimeFormatter. + */ + public function format(DateTimeFormatter $formatter): string + { + $context = DateTimeFormatContext::ofLocalDate($this); + + return $formatter->format($context); + } + /** * Returns the ISO 8601 representation of this LocalDate. */ diff --git a/src/LocalDateTime.php b/src/LocalDateTime.php index bb960f3..b1706d5 100644 --- a/src/LocalDateTime.php +++ b/src/LocalDateTime.php @@ -4,6 +4,8 @@ namespace Brick\DateTime; +use Brick\DateTime\Formatter\DateTimeFormatContext; +use Brick\DateTime\Formatter\DateTimeFormatter; use Brick\DateTime\Parser\DateTimeParseException; use Brick\DateTime\Parser\DateTimeParser; use Brick\DateTime\Parser\DateTimeParseResult; @@ -741,6 +743,16 @@ public function toNativeDateTimeImmutable(): DateTimeImmutable return DateTimeImmutable::createFromMutable($this->toNativeDateTime()); } + /** + * Formats this LocalDateTime using given DateTimeFormatter. + */ + public function format(DateTimeFormatter $formatter): string + { + $context = DateTimeFormatContext::ofLocalDateTime($this); + + return $formatter->format($context); + } + /** * Serializes as a string using {@see LocalDateTime::__toString()}. */ diff --git a/src/LocalTime.php b/src/LocalTime.php index e978866..b79d73f 100644 --- a/src/LocalTime.php +++ b/src/LocalTime.php @@ -7,6 +7,8 @@ use Brick\DateTime\Field\HourOfDay; use Brick\DateTime\Field\MinuteOfHour; use Brick\DateTime\Field\SecondOfMinute; +use Brick\DateTime\Formatter\DateTimeFormatContext; +use Brick\DateTime\Formatter\DateTimeFormatter; use Brick\DateTime\Parser\DateTimeParseException; use Brick\DateTime\Parser\DateTimeParser; use Brick\DateTime\Parser\DateTimeParseResult; @@ -658,6 +660,16 @@ public function toDateTimeImmutable(): DateTimeImmutable return $this->toNativeDateTimeImmutable(); } + /** + * Formats this LocalTime using given DateTimeFormatter. + */ + public function format(DateTimeFormatter $formatter): string + { + $context = DateTimeFormatContext::ofLocalTime($this); + + return $formatter->format($context); + } + /** * Serializes as a string using {@see LocalTime::__toString()}. */ diff --git a/src/ZonedDateTime.php b/src/ZonedDateTime.php index 5d6f8f0..db1cec0 100644 --- a/src/ZonedDateTime.php +++ b/src/ZonedDateTime.php @@ -4,6 +4,8 @@ namespace Brick\DateTime; +use Brick\DateTime\Formatter\DateTimeFormatContext; +use Brick\DateTime\Formatter\DateTimeFormatter; use Brick\DateTime\Parser\DateTimeParseException; use Brick\DateTime\Parser\DateTimeParser; use Brick\DateTime\Parser\DateTimeParseResult; @@ -723,6 +725,16 @@ public function toNativeDateTimeImmutable(): DateTimeImmutable return DateTimeImmutable::createFromMutable($this->toNativeDateTime()); } + /** + * Formats this ZonedDateTime using given DateTimeFormatter. + */ + public function format(DateTimeFormatter $formatter): string + { + $context = DateTimeFormatContext::ofZonedDateTime($this); + + return $formatter->format($context); + } + /** * Serializes as a string using {@see ZonedDateTime::__toString()}. */ diff --git a/tests/Formatter/DateTimeFormatContextTest.php b/tests/Formatter/DateTimeFormatContextTest.php new file mode 100644 index 0000000..b96ea34 --- /dev/null +++ b/tests/Formatter/DateTimeFormatContextTest.php @@ -0,0 +1,95 @@ +assertSame('8', $context->getField(Field\DayOfMonth::NAME)); + $this->assertSame('3', $context->getField(Field\DayOfWeek::NAME)); + $this->assertSame('159', $context->getField(Field\DayOfYear::NAME)); + $this->assertSame('23', $context->getField(Field\WeekOfYear::NAME)); + $this->assertSame('6', $context->getField(Field\MonthOfYear::NAME)); + $this->assertSame('2022', $context->getField(Field\Year::NAME)); + + $this->assertFalse($context->hasField(Field\HourOfDay::NAME)); + $this->assertFalse($context->hasField(Field\TimeZoneRegion::NAME)); + } + + public function testOfLocalTime(): void + { + $localTime = LocalTime::of(13, 37, 42, 999999999); + $context = DateTimeFormatContext::ofLocalTime($localTime); + + $this->assertSame('13', $context->getField(Field\HourOfDay::NAME)); + $this->assertSame('37', $context->getField(Field\MinuteOfHour::NAME)); + $this->assertSame('42', $context->getField(Field\SecondOfMinute::NAME)); + $this->assertSame('999999999', $context->getField(Field\NanoOfSecond::NAME)); + $this->assertSame('999999999', $context->getField(Field\FractionOfSecond::NAME)); + + $this->assertFalse($context->hasField(Field\DayOfMonth::NAME)); + $this->assertFalse($context->hasField(Field\TimeZoneRegion::NAME)); + } + + public function testOfLocalDateTime(): void + { + $localDateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + $context = DateTimeFormatContext::ofLocalDateTime($localDateTime); + + $this->assertSame('8', $context->getField(Field\DayOfMonth::NAME)); + $this->assertSame('3', $context->getField(Field\DayOfWeek::NAME)); + $this->assertSame('159', $context->getField(Field\DayOfYear::NAME)); + $this->assertSame('23', $context->getField(Field\WeekOfYear::NAME)); + $this->assertSame('6', $context->getField(Field\MonthOfYear::NAME)); + $this->assertSame('2022', $context->getField(Field\Year::NAME)); + + $this->assertSame('13', $context->getField(Field\HourOfDay::NAME)); + $this->assertSame('37', $context->getField(Field\MinuteOfHour::NAME)); + $this->assertSame('42', $context->getField(Field\SecondOfMinute::NAME)); + $this->assertSame('999999999', $context->getField(Field\NanoOfSecond::NAME)); + $this->assertSame('999999999', $context->getField(Field\FractionOfSecond::NAME)); + + $this->assertFalse($context->hasField(Field\TimeZoneRegion::NAME)); + } + + public function testOfZonedDateTime(): void + { + $localDateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + $zonedDateTime = ZonedDateTime::of($localDateTime, TimeZoneRegion::of('Europe/Prague')); + $context = DateTimeFormatContext::ofZonedDateTime($zonedDateTime); + + $this->assertSame('8', $context->getField(Field\DayOfMonth::NAME)); + $this->assertSame('3', $context->getField(Field\DayOfWeek::NAME)); + $this->assertSame('159', $context->getField(Field\DayOfYear::NAME)); + $this->assertSame('23', $context->getField(Field\WeekOfYear::NAME)); + $this->assertSame('6', $context->getField(Field\MonthOfYear::NAME)); + $this->assertSame('2022', $context->getField(Field\Year::NAME)); + + $this->assertSame('13', $context->getField(Field\HourOfDay::NAME)); + $this->assertSame('37', $context->getField(Field\MinuteOfHour::NAME)); + $this->assertSame('42', $context->getField(Field\SecondOfMinute::NAME)); + $this->assertSame('999999999', $context->getField(Field\NanoOfSecond::NAME)); + $this->assertSame('999999999', $context->getField(Field\FractionOfSecond::NAME)); + + $this->assertSame('2', $context->getField(Field\TimeZoneOffsetHour::NAME)); + $this->assertSame('0', $context->getField(Field\TimeZoneOffsetMinute::NAME)); + $this->assertSame('+', $context->getField(Field\TimeZoneOffsetSign::NAME)); + $this->assertSame('7200', $context->getField(Field\TimeZoneOffsetTotalSeconds::NAME)); + $this->assertSame('Europe/Prague', $context->getField(Field\TimeZoneRegion::NAME)); + } +} diff --git a/tests/Formatter/IntlFormatterTest.php b/tests/Formatter/IntlFormatterTest.php new file mode 100644 index 0000000..801fe8a --- /dev/null +++ b/tests/Formatter/IntlFormatterTest.php @@ -0,0 +1,239 @@ +expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestOfDateData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, IntlFormatter::FULL, 'Wednesday, June 8, 2022']; + yield [$date, IntlFormatter::LONG, 'June 8, 2022']; + yield [$date, IntlFormatter::MEDIUM, 'Jun 8, 2022']; + yield [$date, IntlFormatter::SHORT, '6/8/22']; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, IntlFormatter::FULL, 'Wednesday, June 8, 2022']; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a date part cannot be used to format Brick\DateTime\LocalTime.')]; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, IntlFormatter::FULL, 'Wednesday, June 8, 2022']; + } + + /** + * @dataProvider provideTestOfDateTimeData + * + * @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value + * @param string|Throwable $expectedResult + */ + public function testOfDateTime($value, int $dateFormat, int $timeFormat, $expectedResult): void + { + $formatter = IntlFormatter::ofDateTime('en_US', $dateFormat, $timeFormat); + + if ($expectedResult instanceof Throwable) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestOfDateTimeData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, IntlFormatter::FULL, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a time part cannot be used to format Brick\DateTime\LocalDate.')]; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, IntlFormatter::FULL, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalDateTime.')]; + yield [$dateTime, IntlFormatter::FULL, IntlFormatter::LONG, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalDateTime.')]; + yield [$dateTime, IntlFormatter::FULL, IntlFormatter::MEDIUM, 'Wednesday, June 8, 2022 at 1:37:42 PM']; + yield [$dateTime, IntlFormatter::FULL, IntlFormatter::SHORT, 'Wednesday, June 8, 2022 at 1:37 PM']; + yield [$dateTime, IntlFormatter::LONG, IntlFormatter::SHORT, 'June 8, 2022 at 1:37 PM']; + yield [$dateTime, IntlFormatter::MEDIUM, IntlFormatter::MEDIUM, 'Jun 8, 2022, 1:37:42 PM']; + yield [$dateTime, IntlFormatter::SHORT, IntlFormatter::LONG, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalDateTime.')]; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, IntlFormatter::FULL, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a date part cannot be used to format Brick\DateTime\LocalTime.')]; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, IntlFormatter::FULL, IntlFormatter::FULL, 'Wednesday, June 8, 2022 at 1:37:42 PM Central European Summer Time']; + yield [$zoned, IntlFormatter::FULL, IntlFormatter::LONG, 'Wednesday, June 8, 2022 at 1:37:42 PM GMT+2']; + yield [$zoned, IntlFormatter::FULL, IntlFormatter::MEDIUM, 'Wednesday, June 8, 2022 at 1:37:42 PM']; + yield [$zoned, IntlFormatter::FULL, IntlFormatter::SHORT, 'Wednesday, June 8, 2022 at 1:37 PM']; + yield [$zoned, IntlFormatter::LONG, IntlFormatter::SHORT, 'June 8, 2022 at 1:37 PM']; + yield [$zoned, IntlFormatter::MEDIUM, IntlFormatter::MEDIUM, 'Jun 8, 2022, 1:37:42 PM']; + yield [$zoned, IntlFormatter::SHORT, IntlFormatter::LONG, '6/8/22, 1:37:42 PM GMT+2']; + } + + /** + * @dataProvider provideTestOfTimeData + * + * @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value + * @param string|Throwable $expectedResult + */ + public function testOfTime($value, int $format, $expectedResult): void + { + $formatter = IntlFormatter::ofTime('en_US', $format); + + if ($expectedResult instanceof Throwable) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestOfTimeData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a time part cannot be used to format Brick\DateTime\LocalDate.')]; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalDateTime.')]; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, IntlFormatter::FULL, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalTime.')]; + yield [$time, IntlFormatter::LONG, new DateTimeFormatException('IntlFormatter with a long or full time part cannot be used to format Brick\DateTime\LocalTime.')]; + yield [$time, IntlFormatter::MEDIUM, '1:37:42 PM']; + yield [$time, IntlFormatter::SHORT, '1:37 PM']; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, IntlFormatter::FULL, '1:37:42 PM Central European Summer Time']; + } + + /** + * @dataProvider provideTestOfPatternData + * + * @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value + * @param string|Throwable $expectedResult + */ + public function testOfPattern($value, string $pattern, $expectedResult): void + { + $formatter = IntlFormatter::ofPattern('en_US', $pattern); + + if ($expectedResult instanceof Throwable) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestOfPatternData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, 'eee, dd.MMM.yyyy', 'Wed, 08.Jun.2022']; + yield [$date, 'H:mm:ss', new DateTimeFormatException("IntlFormatter with pattern 'H:mm:ss' is incompatible with type Brick\DateTime\LocalDate.")]; + yield [$date, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalDate.")]; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, 'eee, dd.MMM.yyyy', 'Wed, 08.Jun.2022']; + yield [$dateTime, 'H:mm:ss', '13:37:42']; + yield [$dateTime, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalDateTime.")]; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, 'eee, dd.MMM.yyyy', new DateTimeFormatException("IntlFormatter with pattern 'eee, dd.MMM.yyyy' is incompatible with type Brick\DateTime\LocalTime.")]; + yield [$time, 'H:mm:ss', '13:37:42']; + yield [$time, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalTime.")]; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, 'eee, dd.MMM.yyyy', 'Wed, 08.Jun.2022']; + yield [$zoned, 'H:mm:ss', '13:37:42']; + yield [$zoned, 'VV', 'Europe/Prague']; + yield [$zoned, 'XXX', '+02:00']; + + $zonedWithOffset = ZonedDateTime::of($dateTime, TimeZoneOffset::of(2)); + yield [$zonedWithOffset, 'eee, dd.MMM.yyyy', 'Wed, 08.Jun.2022']; + yield [$zonedWithOffset, 'H:mm:ss', '13:37:42']; + yield [$zonedWithOffset, 'VV', 'GMT+02:00']; + yield [$zonedWithOffset, 'XXX', '+02:00']; + } + + /** + * @dataProvider provideTestOfSkeletonData + * + * @param LocalDate|LocalDateTime|LocalTime|ZonedDateTime $value + * @param string|Throwable $expectedResult + */ + public function testOfSkeleton($value, string $skeleton, $expectedResult): void + { + $formatter = IntlFormatter::ofSkeleton('en_US', $skeleton); + + if ($expectedResult instanceof Throwable) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestOfSkeletonData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, 'dMMMMyyyyeee', 'Wed, June 8, 2022']; + yield [$date, 'Hms', new DateTimeFormatException("IntlFormatter with pattern 'HH:mm:ss' is incompatible with type Brick\DateTime\LocalDate.")]; + yield [$date, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalDate.")]; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, 'dMMMMyyyyeee', 'Wed, June 8, 2022']; + yield [$dateTime, 'Hms', '13:37:42']; + yield [$dateTime, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalDateTime.")]; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, 'dMMMMyyyyeee', new DateTimeFormatException("IntlFormatter with pattern 'EEE, MMMM d, yyyy' is incompatible with type Brick\DateTime\LocalTime.")]; + yield [$time, 'Hms', '13:37:42']; + yield [$time, 'XXX', new DateTimeFormatException("IntlFormatter with pattern 'XXX' is incompatible with type Brick\DateTime\LocalTime.")]; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, 'dMMMMyyyyeee', 'Wed, June 8, 2022']; + yield [$zoned, 'Hms', '13:37:42']; + yield [$zoned, 'VV', 'Europe/Prague']; + yield [$zoned, 'XXX', '+02:00']; + + $zonedWithOffset = ZonedDateTime::of($dateTime, TimeZoneOffset::of(2)); + yield [$zonedWithOffset, 'dMMMMyyyyeee', 'Wed, June 8, 2022']; + yield [$zonedWithOffset, 'Hms', '13:37:42']; + yield [$zonedWithOffset, 'VV', 'GMT+02:00']; + yield [$zonedWithOffset, 'XXX', '+02:00']; + } +} diff --git a/tests/Formatter/NativeFormatterTest.php b/tests/Formatter/NativeFormatterTest.php new file mode 100644 index 0000000..d8f995a --- /dev/null +++ b/tests/Formatter/NativeFormatterTest.php @@ -0,0 +1,85 @@ +expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $formatted = $value->format($formatter); + $this->assertSame($expectedResult, $formatted); + } + + public function provideTestFormatData(): iterable + { + $date = LocalDate::of(2022, 6, 8); + yield [$date, 'd.m.y', '08.06.22']; + yield [$date, 'M j, Y', 'Jun 8, 2022']; + yield [$date, 'D', 'Wed']; + yield [$date, 'H:i:s', new DateTimeFormatException("Formatting pattern 'H:i:s' is incompatible with type Brick\DateTime\LocalDate.")]; + yield [$date, 'U', new DateTimeFormatException("Formatting pattern 'U' is incompatible with type Brick\DateTime\LocalDate.")]; + + $dateTime = LocalDateTime::of(2022, 6, 8, 13, 37, 42, 999999999); + yield [$dateTime, 'd.m.y', '08.06.22']; + yield [$dateTime, 'M j, Y', 'Jun 8, 2022']; + yield [$dateTime, 'D', 'Wed']; + yield [$dateTime, 'H:i:s', '13:37:42']; + yield [$dateTime, 'u', '999999']; + yield [$dateTime, 'U', new DateTimeFormatException("Formatting pattern 'U' is incompatible with type Brick\DateTime\LocalDateTime.")]; + + $time = LocalTime::of(13, 37, 42, 999999999); + yield [$time, 'd.m.y', new DateTimeFormatException("Formatting pattern 'd.m.y' is incompatible with type Brick\DateTime\LocalTime.")]; + yield [$time, 'D', new DateTimeFormatException("Formatting pattern 'D' is incompatible with type Brick\DateTime\LocalTime.")]; + yield [$time, 'H:i:s', '13:37:42']; + yield [$time, 'u', '999999']; + yield [$time, 'U', new DateTimeFormatException("Formatting pattern 'U' is incompatible with type Brick\DateTime\LocalTime.")]; + + $zoned = ZonedDateTime::of($dateTime, TimeZoneRegion::of('Europe/Prague')); + yield [$zoned, 'd.m.y', '08.06.22']; + yield [$zoned, 'M j, Y', 'Jun 8, 2022']; + yield [$zoned, 'D', 'Wed']; + yield [$zoned, 'H:i:s', '13:37:42']; + yield [$zoned, 'u', '999999']; + yield [$zoned, 'U', '1654688262']; + yield [$zoned, 'e', 'Europe/Prague']; + yield [$zoned, 'p', '+02:00']; + + $zonedWithOffset = ZonedDateTime::of($dateTime, TimeZoneOffset::of(2)); + yield [$zonedWithOffset, 'd.m.y', '08.06.22']; + yield [$zonedWithOffset, 'M j, Y', 'Jun 8, 2022']; + yield [$zonedWithOffset, 'D', 'Wed']; + yield [$zonedWithOffset, 'H:i:s', '13:37:42']; + yield [$zonedWithOffset, 'u', '999999']; + yield [$zonedWithOffset, 'U', '1654688262']; + yield [$zonedWithOffset, 'e', '+02:00']; + yield [$zonedWithOffset, 'p', '+02:00']; + } +}