diff --git a/README.md b/README.md
index 5163a12..fb7db74 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,6 @@
* [Alpha3Code](#alpha3code)
* [Country](#country-1)
* [Timezones](#timezones)
- * [Timezone](#timezone)
* [License](#license)
* [Contributing](#contributing)
@@ -141,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;
@@ -160,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"), ...]
```
@@ -178,9 +182,9 @@ $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 country's timezones. Returns UTC if not found.
```php
use TinyBlocks\Country\Country;
@@ -188,8 +192,8 @@ use TinyBlocks\Country\Alpha2Code;
$country = Country::from(alphaCode: Alpha2Code::UNITED_STATES_OF_AMERICA);
-$country->timezones->findByIdentifier(iana: 'America/New_York'); # Timezone("America/New_York")
-$country->timezones->findByIdentifier(iana: 'Asia/Tokyo'); # null
+$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
@@ -204,29 +208,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
{