66* [ Installation] ( #installation )
77* [ How to use] ( #how-to-use )
88 * [ Instant] ( #instant )
9+ * [ Duration] ( #duration )
10+ * [ Period] ( #period )
911 * [ Timezone] ( #timezone )
1012 * [ Timezones] ( #timezones )
1113* [ License] ( #license )
1517
1618## Overview
1719
18- Value Object representing time in an immutable and strict way, focused on safe parsing, formatting and normalization.
20+ Value Objects representing time in an immutable and strict way, focused on safe parsing, formatting, normalization and
21+ temporal arithmetic.
1922
2023<div id =' installation ' ></div >
2124
@@ -29,8 +32,8 @@ composer require tiny-blocks/time
2932
3033## How to use
3134
32- The library provides immutable Value Objects for representing points in time and IANA timezones. All instants are
33- normalized to UTC internally.
35+ The library provides immutable Value Objects for representing points in time, quantities of time and time intervals.
36+ All instants are normalized to UTC internally.
3437
3538### Instant
3639
@@ -45,9 +48,9 @@ use TinyBlocks\Time\Instant;
4548
4649$instant = Instant::now();
4750
48- $instant->toIso8601(); # 2026-02-17T10:30:00+00:00 (current UTC time)
49- $instant->toUnixSeconds(); # 1771324200 (current Unix timestamp)
50- $instant->toDateTimeImmutable(); # DateTimeImmutable (UTC, with microseconds)
51+ $instant->toIso8601(); # 2026-02-17T10:30:00+00:00
52+ $instant->toUnixSeconds(); # 1771324200
53+ $instant->toDateTimeImmutable(); # DateTimeImmutable (UTC, with microseconds)
5154```
5255
5356#### Creating from a string
@@ -59,9 +62,8 @@ use TinyBlocks\Time\Instant;
5962
6063$instant = Instant::fromString(value: '2026-02-17T13:30:00-03:00');
6164
62- $instant->toIso8601(); # 2026-02-17T16:30:00+00:00
63- $instant->toUnixSeconds(); # 1771345800
64- $instant->toDateTimeImmutable(); # DateTimeImmutable (UTC)
65+ $instant->toIso8601(); # 2026-02-17T16:30:00+00:00
66+ $instant->toUnixSeconds(); # 1771345800
6567```
6668
6769#### Creating from a database timestamp
@@ -74,8 +76,8 @@ use TinyBlocks\Time\Instant;
7476
7577$instant = Instant::fromString(value: '2026-02-17 08:27:21.106011');
7678
77- $instant->toIso8601(); # 2026-02-17T08:27:21+00:00
78- $instant->toDateTimeImmutable()->format('Y-m-d H:i:s.u'); # 2026-02-17 08:27:21.106011
79+ $instant->toIso8601(); # 2026-02-17T08:27:21+00:00
80+ $instant->toDateTimeImmutable()->format('Y-m-d H:i:s.u'); # 2026-02-17 08:27:21.106011
7981```
8082
8183Also supports timestamps without fractional seconds:
@@ -101,30 +103,231 @@ $instant->toIso8601(); # 1970-01-01T00:00:00+00:00
101103$instant->toUnixSeconds(); # 0
102104```
103105
104- #### Formatting as ISO 8601
106+ #### Adding and subtracting time
105107
106- The ` toIso8601 ` method always returns the format ` YYYY-MM-DDTHH:MM:SS+00:00 ` , without fractional seconds .
108+ Returns a new ` Instant ` shifted forward or backward by a ` Duration ` .
107109
108110``` php
109111use TinyBlocks\Time\Instant;
112+ use TinyBlocks\Time\Duration;
110113
111- $instant = Instant::fromString(value: '2026-02-17T19:30 :00+09 :00');
114+ $instant = Instant::fromString(value: '2026-02-17T10:00 :00+00 :00');
112115
113- $instant->toIso8601(); # 2026-02-17T10:30:00+00:00
116+ $instant->plus(duration: Duration::ofMinutes(minutes: 30))->toIso8601(); # 2026-02-17T10:30:00+00:00
117+ $instant->plus(duration: Duration::ofHours(hours: 2))->toIso8601(); # 2026-02-17T12:00:00+00:00
118+ $instant->minus(duration: Duration::ofSeconds(seconds: 60))->toIso8601(); # 2026-02-17T09:59:00+00:00
114119```
115120
116- #### Accessing the underlying DateTimeImmutable
121+ #### Measuring distance between instants
117122
118- Returns a ` DateTimeImmutable ` in UTC with full microsecond precision .
123+ Returns the absolute ` Duration ` between two ` Instant ` objects .
119124
120125``` php
121126use TinyBlocks\Time\Instant;
122127
123- $instant = Instant::fromString(value: '2026-02-17T10:30 :00+00:00');
124- $dateTime = $instant->toDateTimeImmutable( );
128+ $start = Instant::fromString(value: '2026-02-17T10:00 :00+00:00');
129+ $end = Instant::fromString(value: '2026-02-17T11:30:00+00:00' );
125130
126- $dateTime->getTimezone()->getName(); # UTC
127- $dateTime->format('Y-m-d\TH:i:s.u'); # 2026-02-17T10:30:00.000000
131+ $duration = $start->durationUntil(other: $end);
132+
133+ $duration->seconds; # 5400
134+ $duration->toMinutes(); # 90
135+ $duration->toHours(); # 1
136+ ```
137+
138+ The result is always non-negative regardless of direction:
139+
140+ ``` php
141+ $end->durationUntil(other: $start)->seconds; # 5400
142+ ```
143+
144+ #### Comparing instants
145+
146+ Provides strict temporal ordering between two ` Instant ` instances.
147+
148+ ``` php
149+ use TinyBlocks\Time\Instant;
150+
151+ $earlier = Instant::fromString(value: '2026-02-17T10:00:00+00:00');
152+ $later = Instant::fromString(value: '2026-02-17T10:30:00+00:00');
153+
154+ $earlier->isBefore(other: $later); # true
155+ $earlier->isAfter(other: $later); # false
156+ $earlier->isBeforeOrEqual(other: $later); # true
157+ $earlier->isAfterOrEqual(other: $later); # false
158+ $later->isAfter(other: $earlier); # true
159+ $later->isAfterOrEqual(other: $earlier); # true
160+ ```
161+
162+ ### Duration
163+
164+ A ` Duration ` represents an immutable, unsigned quantity of time measured in seconds. It has no reference point on the
165+ timeline — it expresses only "how much" time.
166+
167+ #### Creating durations
168+
169+ ``` php
170+ use TinyBlocks\Time\Duration;
171+
172+ $zero = Duration::zero();
173+ $seconds = Duration::ofSeconds(seconds: 90);
174+ $minutes = Duration::ofMinutes(minutes: 30);
175+ $hours = Duration::ofHours(hours: 2);
176+ $days = Duration::ofDays(days: 7);
177+ ```
178+
179+ All factories reject negative values:
180+
181+ ``` php
182+ Duration::ofMinutes(minutes: -5); # throws InvalidDuration
183+ ```
184+
185+ #### Arithmetic
186+
187+ ``` php
188+ use TinyBlocks\Time\Duration;
189+
190+ $a = Duration::ofMinutes(minutes: 30);
191+ $b = Duration::ofMinutes(minutes: 15);
192+
193+ $a->plus(other: $b)->seconds; # 2700 (45 minutes)
194+ $a->minus(other: $b)->seconds; # 900 (15 minutes)
195+ ```
196+
197+ Subtraction that would produce a negative result throws an exception:
198+
199+ ``` php
200+ $b->minus(other: $a); # throws InvalidDuration
201+ ```
202+
203+ #### Comparing durations
204+
205+ ``` php
206+ use TinyBlocks\Time\Duration;
207+
208+ $short = Duration::ofMinutes(minutes: 15);
209+ $long = Duration::ofHours(hours: 2);
210+
211+ $short->isLessThan(other: $long); # true
212+ $long->isGreaterThan(other: $short); # true
213+ $short->isZero(); # false
214+ Duration::zero()->isZero(); # true
215+ ```
216+
217+ #### Converting to other units
218+
219+ Conversions truncate toward zero when the duration is not an exact multiple:
220+
221+ ``` php
222+ use TinyBlocks\Time\Duration;
223+
224+ $duration = Duration::ofSeconds(seconds: 5400);
225+
226+ $duration->toMinutes(); # 90
227+ $duration->toHours(); # 1
228+ $duration->toDays(); # 0
229+ ```
230+
231+ ### Period
232+
233+ A ` Period ` represents a half-open time interval ` [from, to) ` between two UTC instants. The start is inclusive and the
234+ end is exclusive.
235+
236+ #### Creating from two instants
237+
238+ ``` php
239+ use TinyBlocks\Time\Instant;
240+ use TinyBlocks\Time\Period;
241+
242+ $period = Period::of(
243+ from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
244+ to: Instant::fromString(value: '2026-02-17T11:00:00+00:00')
245+ );
246+
247+ $period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
248+ $period->to->toIso8601(); # 2026-02-17T11:00:00+00:00
249+ ```
250+
251+ The start must be strictly before the end:
252+
253+ ``` php
254+ Period::of(from: $later, to: $earlier); # throws InvalidPeriod
255+ ```
256+
257+ #### Creating from a start and duration
258+
259+ ``` php
260+ use TinyBlocks\Time\Duration;
261+ use TinyBlocks\Time\Instant;
262+ use TinyBlocks\Time\Period;
263+
264+ $period = Period::startingAt(
265+ from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
266+ duration: Duration::ofMinutes(minutes: 90)
267+ );
268+
269+ $period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
270+ $period->to->toIso8601(); # 2026-02-17T11:30:00+00:00
271+ ```
272+
273+ #### Getting the duration
274+
275+ ``` php
276+ $period->duration()->seconds; # 5400
277+ $period->duration()->toMinutes(); # 90
278+ ```
279+
280+ #### Checking if an instant is contained
281+
282+ The check is inclusive at the start and exclusive at the end:
283+
284+ ``` php
285+ use TinyBlocks\Time\Instant;
286+
287+ $period->contains(instant: Instant::fromString(value: '2026-02-17T10:00:00+00:00')); # true (start, inclusive)
288+ $period->contains(instant: Instant::fromString(value: '2026-02-17T10:30:00+00:00')); # true (middle)
289+ $period->contains(instant: Instant::fromString(value: '2026-02-17T11:30:00+00:00')); # false (end, exclusive)
290+ ```
291+
292+ #### Detecting overlap
293+
294+ Two half-open intervals ` [A, B) ` and ` [C, D) ` overlap when ` A < D ` and ` C < B ` :
295+
296+ ``` php
297+ use TinyBlocks\Time\Duration;
298+ use TinyBlocks\Time\Instant;
299+ use TinyBlocks\Time\Period;
300+
301+ $periodA = Period::startingAt(
302+ from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
303+ duration: Duration::ofHours(hours: 1)
304+ );
305+ $periodB = Period::startingAt(
306+ from: Instant::fromString(value: '2026-02-17T10:30:00+00:00'),
307+ duration: Duration::ofHours(hours: 1)
308+ );
309+
310+ $periodA->overlapsWith(other: $periodB); # true
311+ $periodB->overlapsWith(other: $periodA); # true
312+ ```
313+
314+ Adjacent periods do not overlap:
315+
316+ ``` php
317+ use TinyBlocks\Time\Duration;
318+ use TinyBlocks\Time\Instant;
319+ use TinyBlocks\Time\Period;
320+
321+ $first = Period::startingAt(
322+ from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
323+ duration: Duration::ofHours(hours: 1)
324+ );
325+ $second = Period::startingAt(
326+ from: Instant::fromString(value: '2026-02-17T11:00:00+00:00'),
327+ duration: Duration::ofHours(hours: 1)
328+ );
329+
330+ $first->overlapsWith(other: $second); # false
128331```
129332
130333### Timezone
0 commit comments