From a8ced246d3e838b1e5ea36d5a9e2f1e6db84d03d Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Tue, 17 Feb 2026 05:10:02 -0300 Subject: [PATCH 1/2] feat: Introduce CountryTimezones class and update timezone handling in Country. --- README.md | 39 ++--------- composer.json | 1 + src/Country.php | 4 +- src/{Timezones.php => CountryTimezones.php} | 72 +++++++++----------- src/Internal/Exceptions/InvalidTimezone.php | 17 ----- src/Timezone.php | 74 --------------------- tests/CountryTest.php | 45 ++----------- 7 files changed, 46 insertions(+), 206 deletions(-) rename src/{Timezones.php => CountryTimezones.php} (51%) delete mode 100644 src/Internal/Exceptions/InvalidTimezone.php delete mode 100644 src/Timezone.php diff --git a/README.md b/README.md index 5163a12..e400896 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ * [Alpha3Code](#alpha3code) * [Country](#country-1) * [Timezones](#timezones) - * [Timezone](#timezone) * [License](#license) * [Contributing](#contributing) @@ -178,19 +177,18 @@ $country = Country::from(alphaCode: Alpha2Code::BOUVET_ISLAND); $country->timezones->default(); # Timezone("UTC") ``` -#### Finding a timezone by identifier +#### Finding a timezone by identifier with UTC fallback -Searches for a specific IANA identifier within the country's timezones: +Searches for a specific IANA identifier within the collection. Returns UTC if not found. ```php -use TinyBlocks\Country\Country; -use TinyBlocks\Country\Alpha2Code; +use TinyBlocks\Time\Timezones; -$country = Country::from(alphaCode: Alpha2Code::UNITED_STATES_OF_AMERICA); +$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Asia/Tokyo'); -$country->timezones->findByIdentifier(iana: 'America/New_York'); # Timezone("America/New_York") -$country->timezones->findByIdentifier(iana: 'Asia/Tokyo'); # null -``` +$timezones->findByIdentifierOrUtc(iana: 'Asia/Tokyo'); # Timezone("Asia/Tokyo") +$timezones->findByIdentifierOrUtc(iana: 'Europe/London'); # Timezone("UTC") +``` #### Checking if a timezone belongs to the country @@ -204,29 +202,6 @@ $country->timezones->contains(iana: 'Asia/Tokyo'); # true $country->timezones->contains(iana: 'America/New_York'); # false ``` -### Timezone - -A `Timezone` is a Value Object representing a single valid IANA timezone identifier. - -```php -use TinyBlocks\Country\Timezone; - -$timezone = Timezone::from(identifier: 'America/Sao_Paulo'); - -$timezone->value; # America/Sao_Paulo -$timezone->toString(); # America/Sao_Paulo -``` - -Creating a UTC timezone: - -```php -use TinyBlocks\Country\Timezone; - -$timezone = Timezone::utc(); - -$timezone->value; # UTC -``` -
## License diff --git a/composer.json b/composer.json index 11abb39..6b4e960 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ }, "require": { "php": "^8.5", + "tiny-blocks/time": "^1.1", "tiny-blocks/value-object": "^3.2" }, "require-dev": { diff --git a/src/Country.php b/src/Country.php index 0077bc5..b318ef7 100644 --- a/src/Country.php +++ b/src/Country.php @@ -25,7 +25,7 @@ private function __construct( public readonly string $name, public readonly Alpha2Code $alpha2, public readonly Alpha3Code $alpha3, - public readonly Timezones $timezones + public readonly CountryTimezones $timezones ) { } @@ -55,7 +55,7 @@ public static function from(AlphaCode $alphaCode, ?string $name = null): static name: $resolvedName, alpha2: $alpha2, alpha3: $alpha3, - timezones: Timezones::fromAlpha2(alpha2: $alpha2) + timezones: CountryTimezones::fromAlpha2(alpha2: $alpha2) ); } diff --git a/src/Timezones.php b/src/CountryTimezones.php similarity index 51% rename from src/Timezones.php rename to src/CountryTimezones.php index e590c01..68c476f 100644 --- a/src/Timezones.php +++ b/src/CountryTimezones.php @@ -6,6 +6,10 @@ use Countable; use TinyBlocks\Country\Internal\TimezoneCatalog; +use TinyBlocks\Time\Timezone; +use TinyBlocks\Time\Timezones; +use TinyBlocks\Vo\ValueObject; +use TinyBlocks\Vo\ValueObjectBehavior; /** * Immutable collection of Timezone objects for a country. @@ -13,30 +17,27 @@ * Built from PHP's ICU/IANA timezone database — the authoritative source for timezone data. * The first element is considered the default/primary timezone for the country. */ -final readonly class Timezones implements Countable +final readonly class CountryTimezones implements ValueObject, Countable { - /** - * @param list $items All timezone objects for the country. - * @param Timezone $default The default/primary timezone (first in the IANA list, or UTC as fallback). - */ - private function __construct(private array $items, private Timezone $default) + use ValueObjectBehavior; + + private function __construct(private Timezones $timezones, private Timezone $default) { } /** - * Creates a Timezones collection from an Alpha-2 country code. + * Creates a CountryTimezones instance from an Alpha-2 country code. * - * @param Alpha2Code $alpha2 The two-letter country code. - * @return Timezones The timezones collection for the given country. + * @param Alpha2Code $alpha2 The Alpha-2 country code (e.g. "US" for United States). + * @return CountryTimezones A new CountryTimezones instance containing the timezones for the specified country. */ - public static function fromAlpha2(Alpha2Code $alpha2): Timezones + public static function fromAlpha2(Alpha2Code $alpha2): CountryTimezones { - $items = array_map( - static fn(string $id): Timezone => Timezone::from(identifier: $id), - TimezoneCatalog::forAlpha2(alpha2Value: $alpha2->value) - ); + $identifiers = TimezoneCatalog::forAlpha2(alpha2Value: $alpha2->value); + $timezones = Timezones::fromStrings(...$identifiers); + $default = $timezones->all()[0] ?? Timezone::utc(); - return new Timezones(items: $items, default: $items[0] ?? Timezone::utc()); + return new CountryTimezones(timezones: $timezones, default: $default); } /** @@ -46,7 +47,7 @@ public static function fromAlpha2(Alpha2Code $alpha2): Timezones */ public function all(): array { - return $this->items; + return $this->timezones->all(); } /** @@ -56,7 +57,7 @@ public function all(): array */ public function count(): int { - return count($this->items); + return $this->timezones->count(); } /** @@ -73,43 +74,34 @@ public function default(): Timezone } /** - * Checks whether the given IANA identifier belongs to this country's timezones. + * Returns all timezone identifiers as plain strings. * - * @param string $iana The IANA timezone identifier to check (e.g. America/New_York). - * @return bool True if the identifier belongs to this country, false otherwise. + * @return list The list of IANA timezone identifier strings. */ - public function contains(string $iana): bool + public function toStrings(): array { - return array_any( - $this->items, - static fn(Timezone $timezone): bool => $timezone->value === $iana - ); + return $this->timezones->toStrings(); } /** - * Finds a Timezone by its IANA identifier. + * Checks whether the given IANA identifier belongs to this country's timezones. * - * @param string $iana The IANA timezone identifier to search for (e.g. America/Sao_Paulo). - * @return Timezone The matching Timezone, or UTC if not found in this country. + * @param string $iana The IANA timezone identifier to check (e.g. America/New_York). + * @return bool True if the identifier belongs to this country, false otherwise. */ - public function findByIdentifier(string $iana): Timezone + public function contains(string $iana): bool { - return array_find( - $this->items, - static fn(Timezone $timezone): bool => $timezone->value === $iana - ) ?? Timezone::utc(); + return $this->timezones->contains(iana: $iana); } /** - * Returns all timezone identifiers as plain strings. + * Finds a Timezone object by its IANA identifier. * - * @return list The list of IANA timezone identifier strings. + * @param string $iana The IANA timezone identifier to find (e.g. America/New_York). + * @return Timezone The corresponding Timezone object if found, or UTC if not found. */ - public function toStrings(): array + public function findByIdentifierOrUtc(string $iana): Timezone { - return array_map( - static fn(Timezone $timezone): string => $timezone->toString(), - $this->items - ); + return $this->timezones->findByIdentifierOrUtc(iana: $iana); } } diff --git a/src/Internal/Exceptions/InvalidTimezone.php b/src/Internal/Exceptions/InvalidTimezone.php deleted file mode 100644 index afda4cb..0000000 --- a/src/Internal/Exceptions/InvalidTimezone.php +++ /dev/null @@ -1,17 +0,0 @@ - is invalid.'; - - parent::__construct(message: sprintf($template, $this->identifier)); - } -} diff --git a/src/Timezone.php b/src/Timezone.php deleted file mode 100644 index f1b8dc4..0000000 --- a/src/Timezone.php +++ /dev/null @@ -1,74 +0,0 @@ -value = $identifier; - } - - /** - * Creates a Timezone representing UTC. - * - * @return Timezone The UTC Timezone instance. - */ - public static function utc(): Timezone - { - return new Timezone(identifier: 'UTC'); - } - - /** - * Creates a Timezone from a valid IANA identifier. - * - * @param string $identifier The IANA timezone identifier (e.g. America/Sao_Paulo). - * @return Timezone The created Timezone instance. - * @throws InvalidTimezone If the identifier is not a valid IANA timezone. - */ - public static function from(string $identifier): Timezone - { - return new Timezone(identifier: $identifier); - } - - /** - * Returns the IANA timezone identifier as a string. - * - * @return string The IANA timezone identifier. - */ - public function toString(): string - { - return $this->value; - } - - /** - * Returns all valid IANA timezone identifiers available in the runtime. - * - * @return list The list of all IANA timezone identifiers. - */ - protected static function allIdentifiers(): array - { - /** @var list|null $identifiers */ - static $identifiers = null; - - return $identifiers ??= DateTimeZone::listIdentifiers(); - } -} diff --git a/tests/CountryTest.php b/tests/CountryTest.php index 369572e..2269488 100644 --- a/tests/CountryTest.php +++ b/tests/CountryTest.php @@ -13,8 +13,7 @@ use TinyBlocks\Country\Country; use TinyBlocks\Country\Internal\Exceptions\InvalidAlphaCode; use TinyBlocks\Country\Internal\Exceptions\InvalidAlphaCodeImplementation; -use TinyBlocks\Country\Internal\Exceptions\InvalidTimezone; -use TinyBlocks\Country\Timezone; +use TinyBlocks\Time\Timezone; final class CountryTest extends TestCase { @@ -231,30 +230,18 @@ public function testCountryTimezonesContainsKnownIdentifier(): void self::assertFalse($country->timezones->contains(iana: 'America/New_York')); } - public function testCountryTimezonesFindByIdentifierReturnsTimezone(): void + public function testCountryTimezonesFindByIdentifierOrUtcReturnsTimezone(): void { /** @Given a Country created from Alpha-2 code US */ $country = Country::from(alphaCode: Alpha2Code::UNITED_STATES_OF_AMERICA); /** @When searching for a known timezone identifier */ - $timezone = $country->timezones->findByIdentifier(iana: 'America/New_York'); + $timezone = $country->timezones->findByIdentifierOrUtc(iana: 'America/New_York'); /** @Then the returned Timezone value should match the searched identifier */ self::assertSame('America/New_York', $timezone->value); } - public function testCountryTimezonesFindByIdentifierReturnsUtcWhenNotFound(): void - { - /** @Given a Country created from Alpha-2 code DE (Germany) */ - $country = Country::from(alphaCode: Alpha2Code::GERMANY); - - /** @When searching for a timezone that does not belong to Germany */ - $timezone = $country->timezones->findByIdentifier(iana: 'Asia/Tokyo'); - - /** @Then the fallback UTC timezone should be returned */ - self::assertSame('UTC', $timezone->value); - } - public function testCountryTimezonesCountMatchesAllSize(): void { /** @Given a Country with multiple timezones */ @@ -316,7 +303,7 @@ public function testCountryWithMultipleTimezonesPreservesAll(): void /** @And each timezone should be findable by its identifier */ foreach ($country->timezones->all() as $timezone) { - self::assertSame($timezone->value, $country->timezones->findByIdentifier(iana: $timezone->value)->value); + self::assertSame($timezone->value, $country->timezones->findByIdentifierOrUtc(iana: $timezone->value)->value); } } @@ -333,30 +320,6 @@ public function testCountryTimezonesCreatedFromSameCodeAreConsistent(): void self::assertSame($first->timezones->count(), $second->timezones->count()); } - public function testCountryWhenInvalidTimezone(): void - { - /** @Given a non-empty string that is not a valid IANA timezone */ - $invalidIdentifier = 'Invalid/Timezone'; - - /** @Then an InvalidTimezone exception should be thrown */ - $this->expectException(InvalidTimezone::class); - $this->expectExceptionMessage(sprintf('Timezone <%s> is invalid.', $invalidIdentifier)); - - /** @When trying to create a Timezone from the invalid identifier */ - Timezone::from(identifier: $invalidIdentifier); - } - - public function testCountryWhenEmptyTimezoneIdentifier(): void - { - /** @Given an empty string as timezone identifier */ - /** @Then an InvalidTimezone exception should be thrown */ - $this->expectException(InvalidTimezone::class); - $this->expectExceptionMessage('Timezone <> is invalid.'); - - /** @When trying to create a Timezone from an empty string */ - Timezone::from(identifier: ''); - } - #[DataProvider('invalidAlphaCodeStringsDataProvider')] public function testCountryWhenInvalidAlphaCode(string $alphaCode): void { From 6538deffbce881add8ef3289fb3b123f39fcf9f4 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Tue, 17 Feb 2026 05:19:51 -0300 Subject: [PATCH 2/2] feat: Introduce CountryTimezones class and update timezone handling in Country. --- README.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e400896..fb7db74 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ $country->alpha3->value; # USA ### Timezones -Every `Country` includes an immutable `Timezones` collection, built from the IANA timezone database (via PHP's ICU -integration). +Every `Country` includes an immutable `CountryTimezones` collection, built from the IANA timezone database (via PHP's +ICU integration). ```php use TinyBlocks\Country\Country; @@ -159,6 +159,11 @@ $country->timezones->toStrings(); # ["America/Noronha", "America/Belem", "Ameri Returns all `Timezone` objects for the country: ```php +use TinyBlocks\Country\Country; +use TinyBlocks\Country\Alpha2Code; + +$country = Country::from(alphaCode: Alpha2Code::BRAZIL); + $country->timezones->all(); # [Timezone("America/Noronha"), Timezone("America/Belem"), ...] ``` @@ -179,16 +184,17 @@ $country->timezones->default(); # Timezone("UTC") #### Finding a timezone by identifier with UTC fallback -Searches for a specific IANA identifier within the collection. Returns UTC if not found. +Searches for a specific IANA identifier within the country's timezones. Returns UTC if not found. ```php -use TinyBlocks\Time\Timezones; +use TinyBlocks\Country\Country; +use TinyBlocks\Country\Alpha2Code; -$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Asia/Tokyo'); +$country = Country::from(alphaCode: Alpha2Code::UNITED_STATES_OF_AMERICA); -$timezones->findByIdentifierOrUtc(iana: 'Asia/Tokyo'); # Timezone("Asia/Tokyo") -$timezones->findByIdentifierOrUtc(iana: 'Europe/London'); # Timezone("UTC") -``` +$country->timezones->findByIdentifierOrUtc(iana: 'America/New_York'); # Timezone("America/New_York") +$country->timezones->findByIdentifierOrUtc(iana: 'Asia/Tokyo'); # Timezone("UTC") +``` #### Checking if a timezone belongs to the country