diff --git a/src/Field/WeekOfYear.php b/src/Field/WeekOfYear.php index c6a4383..604e39c 100644 --- a/src/Field/WeekOfYear.php +++ b/src/Field/WeekOfYear.php @@ -18,6 +18,11 @@ final class WeekOfYear */ public const NAME = 'week-of-year'; + /** + * The regular expression pattern of the ISO 8601 representation. + */ + public const PATTERN = '[0-9]{2}'; + /** * @param int $weekOfYear The week-of-year to check. * @param int|null $year An optional year to check against, validated. diff --git a/src/Parser/IsoParsers.php b/src/Parser/IsoParsers.php index c341405..4d45bc4 100644 --- a/src/Parser/IsoParsers.php +++ b/src/Parser/IsoParsers.php @@ -15,6 +15,7 @@ use Brick\DateTime\Field\TimeZoneOffsetSecond; use Brick\DateTime\Field\TimeZoneOffsetSign; use Brick\DateTime\Field\TimeZoneRegion; +use Brick\DateTime\Field\WeekOfYear; use Brick\DateTime\Field\Year; /** @@ -184,6 +185,25 @@ public static function yearMonth(): PatternParser ->toParser(); } + /** + * Returns a parser for a year-week such as `2014-W15`. + */ + public static function yearWeek(): PatternParser + { + /** @var PatternParser|null $parser */ + static $parser; + + if ($parser) { + return $parser; + } + + return $parser = (new PatternParserBuilder()) + ->appendCapturePattern(Year::PATTERN, Year::NAME) + ->appendLiteral('-W') + ->appendCapturePattern(WeekOfYear::PATTERN, WeekOfYear::NAME) + ->toParser(); + } + /** * Returns a parser for a month-day such as `12-31`. */ diff --git a/src/YearWeek.php b/src/YearWeek.php index 8a410cc..e261f1e 100644 --- a/src/YearWeek.php +++ b/src/YearWeek.php @@ -4,6 +4,10 @@ namespace Brick\DateTime; +use Brick\DateTime\Parser\DateTimeParseException; +use Brick\DateTime\Parser\DateTimeParser; +use Brick\DateTime\Parser\DateTimeParseResult; +use Brick\DateTime\Parser\IsoParsers; use JsonSerializable; use function sprintf; @@ -49,6 +53,36 @@ public static function of(int $year, int $week): YearWeek return new YearWeek($year, $week); } + /** + * @throws DateTimeException If the year-week is not valid. + * @throws DateTimeParseException If required fields are missing from the result. + */ + public static function from(DateTimeParseResult $result): YearWeek + { + return YearWeek::of( + (int) $result->getField(Field\Year::NAME), + (int) $result->getField(Field\WeekOfYear::NAME) + ); + } + + /** + * Obtains an instance of `YearWeek` from a text string. + * + * @param string $text The text to parse, such as `2007-W48`. + * @param DateTimeParser|null $parser The parser to use, defaults to the ISO 8601 parser. + * + * @throws DateTimeException If the year-week is not valid. + * @throws DateTimeParseException If the text string does not follow the expected format. + */ + public static function parse(string $text, ?DateTimeParser $parser = null): YearWeek + { + if (! $parser) { + $parser = IsoParsers::yearWeek(); + } + + return YearWeek::from($parser->parse($text)); + } + public static function now(TimeZone $timeZone, ?Clock $clock = null): YearWeek { return LocalDate::now($timeZone, $clock)->getYearWeek(); diff --git a/tests/YearWeekTest.php b/tests/YearWeekTest.php index 682cea2..f60e201 100644 --- a/tests/YearWeekTest.php +++ b/tests/YearWeekTest.php @@ -336,6 +336,49 @@ public function testToString(int $year, int $week, string $expected): void self::assertSame($expected, (string) $yearWeek); } + /** + * @dataProvider providerParse + */ + public function testParse(string $string, int $expectedYear, int $expectedWeek): void + { + $yearWeek = YearWeek::parse($string); + self::assertYearWeekIs($expectedYear, $expectedWeek, $yearWeek); + } + + public function providerParse(): array + { + return [ + ['-2000-W12', -2000, 12], + ['-0100-W01', -100, 1], + ['2015-W01', 2015, 1], + ['2015-W48', 2015, 48], + ['2026-W53', 2026, 53], + ['120195-W23', 120195, 23], + ]; + } + + /** + * @dataProvider providerParseInvalidYearWeekThrowsException + */ + public function testParseInvalidYearWeekThrowsException(string $invalidValue, ?string $error = null): void + { + $this->expectException(DateTimeException::class); + $this->expectExceptionMessage($error ?? 'Failed to parse "' . $invalidValue . '"'); + + YearWeek::parse($invalidValue); + } + + public function providerParseInvalidYearWeekThrowsException(): array + { + return [ + [''], + ['+2000-W01'], + ['2000W01'], + ['2000-W54', 'Invalid week-of-year: 54 is not in the range 1 to 53.'], + ['2025-W53', 'Year 2025 does not have 53 weeks'], + ]; + } + public function testNow(): void { $now = new FixedClock(Instant::of(2000000000));