Value Objects representing time in an immutable and strict way, focused on safe parsing, formatting, normalization and temporal arithmetic.
composer require tiny-blocks/timeThe library provides immutable Value Objects for representing points in time, quantities of time and time intervals. All instants are normalized to UTC internally.
An Instant represents a single point on the timeline, always stored in UTC with microsecond precision.
Captures the current moment with microsecond precision, normalized to UTC.
use TinyBlocks\Time\Instant;
$instant = Instant::now();
$instant->toIso8601(); # 2026-02-17T10:30:00+00:00
$instant->toUnixSeconds(); # 1771324200
$instant->toDateTimeImmutable(); # DateTimeImmutable (UTC, with microseconds)Parses a date-time string with an explicit UTC offset. The value is normalized to UTC regardless of the original offset.
use TinyBlocks\Time\Instant;
$instant = Instant::fromString(value: '2026-02-17T13:30:00-03:00');
$instant->toIso8601(); # 2026-02-17T16:30:00+00:00
$instant->toUnixSeconds(); # 1771345800Parses a database date-time string as UTC, with or without microsecond precision (e.g. MySQL DATETIME
or DATETIME(6)).
use TinyBlocks\Time\Instant;
$instant = Instant::fromString(value: '2026-02-17 08:27:21.106011');
$instant->toIso8601(); # 2026-02-17T08:27:21+00:00
$instant->toDateTimeImmutable()->format('Y-m-d H:i:s.u'); # 2026-02-17 08:27:21.106011Also supports timestamps without fractional seconds:
use TinyBlocks\Time\Instant;
$instant = Instant::fromString(value: '2026-02-17 08:27:21');
$instant->toIso8601(); # 2026-02-17T08:27:21+00:00Creates an Instant from a Unix timestamp in seconds.
use TinyBlocks\Time\Instant;
$instant = Instant::fromUnixSeconds(seconds: 0);
$instant->toIso8601(); # 1970-01-01T00:00:00+00:00
$instant->toUnixSeconds(); # 0Returns a new Instant shifted forward or backward by a Duration.
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Duration;
$instant = Instant::fromString(value: '2026-02-17T10:00:00+00:00');
$instant->plus(duration: Duration::fromMinutes(minutes: 30))->toIso8601(); # 2026-02-17T10:30:00+00:00
$instant->plus(duration: Duration::fromHours(hours: 2))->toIso8601(); # 2026-02-17T12:00:00+00:00
$instant->minus(duration: Duration::fromSeconds(seconds: 60))->toIso8601(); # 2026-02-17T09:59:00+00:00Returns the absolute Duration between two Instant objects.
use TinyBlocks\Time\Instant;
$start = Instant::fromString(value: '2026-02-17T10:00:00+00:00');
$end = Instant::fromString(value: '2026-02-17T11:30:00+00:00');
$duration = $start->durationUntil(other: $end);
$duration->toSeconds(); # 5400
$duration->toMinutes(); # 90
$duration->toHours(); # 1The result is always non-negative regardless of direction:
$end->durationUntil(other: $start)->toSeconds(); # 5400Provides strict temporal ordering between two Instant instances.
use TinyBlocks\Time\Instant;
$earlier = Instant::fromString(value: '2026-02-17T10:00:00+00:00');
$later = Instant::fromString(value: '2026-02-17T10:30:00+00:00');
$earlier->isBefore(other: $later); # true
$earlier->isAfter(other: $later); # false
$earlier->isBeforeOrEqual(other: $later); # true
$earlier->isAfterOrEqual(other: $later); # false
$later->isAfter(other: $earlier); # true
$later->isAfterOrEqual(other: $earlier); # trueA Duration represents an immutable, unsigned quantity of time measured in seconds. It has no reference point on the
timeline — it expresses only "how much" time.
use TinyBlocks\Time\Duration;
$zero = Duration::zero();
$seconds = Duration::fromSeconds(seconds: 90);
$minutes = Duration::fromMinutes(minutes: 30);
$hours = Duration::fromHours(hours: 2);
$days = Duration::fromDays(days: 7);All factories reject negative values:
Duration::fromMinutes(minutes: -5); # throws InvalidSecondsuse TinyBlocks\Time\Duration;
$thirtyMinutes = Duration::fromMinutes(minutes: 30);
$fifteenMinutes = Duration::fromMinutes(minutes: 15);
$thirtyMinutes->plus(other: $fifteenMinutes)->toSeconds(); # 2700 (45 minutes)
$thirtyMinutes->minus(other: $fifteenMinutes)->toSeconds(); # 900 (15 minutes)Subtraction that would produce a negative result throws an exception:
$fifteenMinutes->minus(other: $thirtyMinutes); # throws InvalidSecondsReturns the number of times one Duration fits wholly into another. The result is truncated toward zero:
use TinyBlocks\Time\Duration;
$total = Duration::fromMinutes(minutes: 90);
$slot = Duration::fromMinutes(minutes: 30);
$total->divide(other: $slot); # 3Division by a zero Duration throws an exception:
$total->divide(other: Duration::zero()); # throws InvalidSecondsuse TinyBlocks\Time\Duration;
$short = Duration::fromMinutes(minutes: 15);
$long = Duration::fromHours(hours: 2);
$short->isLessThan(other: $long); # true
$long->isGreaterThan(other: $short); # true
$short->isZero(); # false
Duration::zero()->isZero(); # trueConversions truncate toward zero when the duration is not an exact multiple:
use TinyBlocks\Time\Duration;
$duration = Duration::fromSeconds(seconds: 5400);
$duration->toSeconds(); # 5400
$duration->toMinutes(); # 90
$duration->toHours(); # 1
$duration->toDays(); # 0A Period represents a half-open time interval [from, to) between two UTC instants. The start is inclusive and the
end is exclusive.
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;
$period = Period::from(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
to: Instant::fromString(value: '2026-02-17T11:00:00+00:00')
);
$period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
$period->to->toIso8601(); # 2026-02-17T11:00:00+00:00The start must be strictly before the end:
Period::from(from: $later, to: $earlier); # throws InvalidPerioduse TinyBlocks\Time\Duration;
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;
$period = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::fromMinutes(minutes: 90)
);
$period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
$period->to->toIso8601(); # 2026-02-17T11:30:00+00:00$period->duration()->toSeconds(); # 5400
$period->duration()->toMinutes(); # 90The check is inclusive at the start and exclusive at the end:
use TinyBlocks\Time\Instant;
$period->contains(instant: Instant::fromString(value: '2026-02-17T10:00:00+00:00')); # true (start, inclusive)
$period->contains(instant: Instant::fromString(value: '2026-02-17T10:30:00+00:00')); # true (middle)
$period->contains(instant: Instant::fromString(value: '2026-02-17T11:30:00+00:00')); # false (end, exclusive)Two half-open intervals [A, B) and [C, D) overlap when A < D and C < B:
use TinyBlocks\Time\Duration;
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;
$periodA = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::fromHours(hours: 1)
);
$periodB = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:30:00+00:00'),
duration: Duration::fromHours(hours: 1)
);
$periodA->overlapsWith(other: $periodB); # true
$periodB->overlapsWith(other: $periodA); # trueAdjacent periods do not overlap:
use TinyBlocks\Time\Duration;
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;
$first = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::fromHours(hours: 1)
);
$second = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T11:00:00+00:00'),
duration: Duration::fromHours(hours: 1)
);
$first->overlapsWith(other: $second); # falseA DayOfWeek represents a day of the week following ISO 8601, where Monday is 1 and Sunday is 7.
use TinyBlocks\Time\DayOfWeek;
use TinyBlocks\Time\Instant;
$instant = Instant::fromString(value: '2026-02-17T10:30:00+00:00');
$day = DayOfWeek::fromInstant(instant: $instant);
$day; # DayOfWeek::Tuesday
$day->value; # 2use TinyBlocks\Time\DayOfWeek;
DayOfWeek::Monday->isWeekday(); # true
DayOfWeek::Monday->isWeekend(); # false
DayOfWeek::Saturday->isWeekday(); # false
DayOfWeek::Saturday->isWeekend(); # trueA TimeOfDay represents a time of day (hour and minute) without date or timezone context. Values range from 00:00 to
23:59.
use TinyBlocks\Time\TimeOfDay;
$time = TimeOfDay::from(hour: 8, minute: 30);
$time->hour; # 8
$time->minute; # 30Parses a string in HH:MM format:
use TinyBlocks\Time\TimeOfDay;
$time = TimeOfDay::fromString(value: '14:30');
$time->hour; # 14
$time->minute; # 30Extracts the time of day from an Instant in UTC:
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\TimeOfDay;
$instant = Instant::fromString(value: '2026-02-17T14:30:00+00:00');
$time = TimeOfDay::fromInstant(instant: $instant);
$time->hour; # 14
$time->minute; # 30use TinyBlocks\Time\TimeOfDay;
$midnight = TimeOfDay::midnight(); # 00:00
$noon = TimeOfDay::noon(); # 12:00use TinyBlocks\Time\TimeOfDay;
$morning = TimeOfDay::from(hour: 8, minute: 0);
$afternoon = TimeOfDay::from(hour: 14, minute: 30);
$morning->isBefore(other: $afternoon); # true
$morning->isAfter(other: $afternoon); # false
$morning->isBeforeOrEqual(other: $afternoon); # true
$afternoon->isAfterOrEqual(other: $morning); # trueReturns the Duration between two times. The second time must be after the first:
use TinyBlocks\Time\TimeOfDay;
$start = TimeOfDay::from(hour: 8, minute: 0);
$end = TimeOfDay::from(hour: 12, minute: 30);
$duration = $start->durationUntil(other: $end);
$duration->toMinutes(); # 270use TinyBlocks\Time\TimeOfDay;
$time = TimeOfDay::from(hour: 8, minute: 30);
$time->toMinutesSinceMidnight(); # 510
$time->toDuration()->toSeconds(); # 30600
$time->toString(); # 08:30A Timezone is a Value Object representing a single valid IANA timezone identifier.
use TinyBlocks\Time\Timezone;
$timezone = Timezone::from(identifier: 'America/Sao_Paulo');
$timezone->value; # America/Sao_Paulo
$timezone->toString(); # America/Sao_Paulouse TinyBlocks\Time\Timezone;
$timezone = Timezone::utc();
$timezone->value; # UTCuse TinyBlocks\Time\Timezone;
$timezone = Timezone::from(identifier: 'Asia/Tokyo');
$dateTimeZone = $timezone->toDateTimeZone();
$dateTimeZone->getName(); # Asia/TokyoAn immutable collection of Timezone objects.
use TinyBlocks\Time\Timezone;
use TinyBlocks\Time\Timezones;
$timezones = Timezones::from(
Timezone::from(identifier: 'America/Sao_Paulo'),
Timezone::from(identifier: 'America/New_York'),
Timezone::from(identifier: 'Asia/Tokyo')
);
$timezones->count(); # 3use TinyBlocks\Time\Timezones;
$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Europe/London');
$timezones->count(); # 3
$timezones->toStrings(); # ["UTC", "America/Sao_Paulo", "Europe/London"]Returns all Timezone objects in the collection:
$timezones->all(); # [Timezone("UTC"), Timezone("America/Sao_Paulo"), Timezone("Europe/London")]Searches for a specific IANA identifier within the collection. Returns null if not found.
use TinyBlocks\Time\Timezones;
$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Asia/Tokyo');
$timezones->findByIdentifier(iana: 'Asia/Tokyo'); # Timezone("Asia/Tokyo")
$timezones->findByIdentifier(iana: 'Europe/London'); # nullSearches for a specific IANA identifier within the collection. Returns UTC if not found.
use TinyBlocks\Time\Timezones;
$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Asia/Tokyo');
$timezones->findByIdentifierOrUtc(iana: 'Asia/Tokyo'); # Timezone("Asia/Tokyo")
$timezones->findByIdentifierOrUtc(iana: 'Europe/London'); # Timezone("UTC")use TinyBlocks\Time\Timezones;
$timezones = Timezones::fromStrings('America/Sao_Paulo', 'Asia/Tokyo');
$timezones->contains(iana: 'Asia/Tokyo'); # true
$timezones->contains(iana: 'America/New_York'); # falseReturns all timezone identifiers as plain strings:
use TinyBlocks\Time\Timezones;
$timezones = Timezones::fromStrings('UTC', 'America/Sao_Paulo', 'Europe/London');
$timezones->toStrings(); # ["UTC", "America/Sao_Paulo", "Europe/London"]Time is licensed under MIT.
Please follow the contributing guidelines to contribute to the project.