Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 224 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* [Installation](#installation)
* [How to use](#how-to-use)
* [Instant](#instant)
* [Duration](#duration)
* [Period](#period)
* [Timezone](#timezone)
* [Timezones](#timezones)
* [License](#license)
Expand All @@ -15,7 +17,8 @@

## Overview

Value Object representing time in an immutable and strict way, focused on safe parsing, formatting and normalization.
Value Objects representing time in an immutable and strict way, focused on safe parsing, formatting, normalization and
temporal arithmetic.

<div id='installation'></div>

Expand All @@ -29,8 +32,8 @@ composer require tiny-blocks/time

## How to use

The library provides immutable Value Objects for representing points in time and IANA timezones. All instants are
normalized to UTC internally.
The library provides immutable Value Objects for representing points in time, quantities of time and time intervals.
All instants are normalized to UTC internally.

### Instant

Expand All @@ -45,9 +48,9 @@ use TinyBlocks\Time\Instant;

$instant = Instant::now();

$instant->toIso8601(); # 2026-02-17T10:30:00+00:00 (current UTC time)
$instant->toUnixSeconds(); # 1771324200 (current Unix timestamp)
$instant->toDateTimeImmutable(); # DateTimeImmutable (UTC, with microseconds)
$instant->toIso8601(); # 2026-02-17T10:30:00+00:00
$instant->toUnixSeconds(); # 1771324200
$instant->toDateTimeImmutable(); # DateTimeImmutable (UTC, with microseconds)
```

#### Creating from a string
Expand All @@ -59,9 +62,8 @@ 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(); # 1771345800
$instant->toDateTimeImmutable(); # DateTimeImmutable (UTC)
$instant->toIso8601(); # 2026-02-17T16:30:00+00:00
$instant->toUnixSeconds(); # 1771345800
```

#### Creating from a database timestamp
Expand All @@ -74,8 +76,8 @@ 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.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.106011
```

Also supports timestamps without fractional seconds:
Expand All @@ -101,30 +103,231 @@ $instant->toIso8601(); # 1970-01-01T00:00:00+00:00
$instant->toUnixSeconds(); # 0
```

#### Formatting as ISO 8601
#### Adding and subtracting time

The `toIso8601` method always returns the format `YYYY-MM-DDTHH:MM:SS+00:00`, without fractional seconds.
Returns a new `Instant` shifted forward or backward by a `Duration`.

```php
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Duration;

$instant = Instant::fromString(value: '2026-02-17T19:30:00+09:00');
$instant = Instant::fromString(value: '2026-02-17T10:00:00+00:00');

$instant->toIso8601(); # 2026-02-17T10:30:00+00:00
$instant->plus(duration: Duration::ofMinutes(minutes: 30))->toIso8601(); # 2026-02-17T10:30:00+00:00
$instant->plus(duration: Duration::ofHours(hours: 2))->toIso8601(); # 2026-02-17T12:00:00+00:00
$instant->minus(duration: Duration::ofSeconds(seconds: 60))->toIso8601(); # 2026-02-17T09:59:00+00:00
```

#### Accessing the underlying DateTimeImmutable
#### Measuring distance between instants

Returns a `DateTimeImmutable` in UTC with full microsecond precision.
Returns the absolute `Duration` between two `Instant` objects.

```php
use TinyBlocks\Time\Instant;

$instant = Instant::fromString(value: '2026-02-17T10:30:00+00:00');
$dateTime = $instant->toDateTimeImmutable();
$start = Instant::fromString(value: '2026-02-17T10:00:00+00:00');
$end = Instant::fromString(value: '2026-02-17T11:30:00+00:00');

$dateTime->getTimezone()->getName(); # UTC
$dateTime->format('Y-m-d\TH:i:s.u'); # 2026-02-17T10:30:00.000000
$duration = $start->durationUntil(other: $end);

$duration->seconds; # 5400
$duration->toMinutes(); # 90
$duration->toHours(); # 1
```

The result is always non-negative regardless of direction:

```php
$end->durationUntil(other: $start)->seconds; # 5400
```

#### Comparing instants

Provides strict temporal ordering between two `Instant` instances.

```php
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); # true
```

### Duration

A `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.

#### Creating durations

```php
use TinyBlocks\Time\Duration;

$zero = Duration::zero();
$seconds = Duration::ofSeconds(seconds: 90);
$minutes = Duration::ofMinutes(minutes: 30);
$hours = Duration::ofHours(hours: 2);
$days = Duration::ofDays(days: 7);
```

All factories reject negative values:

```php
Duration::ofMinutes(minutes: -5); # throws InvalidDuration
```

#### Arithmetic

```php
use TinyBlocks\Time\Duration;

$a = Duration::ofMinutes(minutes: 30);
$b = Duration::ofMinutes(minutes: 15);

$a->plus(other: $b)->seconds; # 2700 (45 minutes)
$a->minus(other: $b)->seconds; # 900 (15 minutes)
```

Subtraction that would produce a negative result throws an exception:

```php
$b->minus(other: $a); # throws InvalidDuration
```

#### Comparing durations

```php
use TinyBlocks\Time\Duration;

$short = Duration::ofMinutes(minutes: 15);
$long = Duration::ofHours(hours: 2);

$short->isLessThan(other: $long); # true
$long->isGreaterThan(other: $short); # true
$short->isZero(); # false
Duration::zero()->isZero(); # true
```

#### Converting to other units

Conversions truncate toward zero when the duration is not an exact multiple:

```php
use TinyBlocks\Time\Duration;

$duration = Duration::ofSeconds(seconds: 5400);

$duration->toMinutes(); # 90
$duration->toHours(); # 1
$duration->toDays(); # 0
```

### Period

A `Period` represents a half-open time interval `[from, to)` between two UTC instants. The start is inclusive and the
end is exclusive.

#### Creating from two instants

```php
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;

$period = Period::of(
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:00
```

The start must be strictly before the end:

```php
Period::of(from: $later, to: $earlier); # throws InvalidPeriod
```

#### Creating from a start and duration

```php
use 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::ofMinutes(minutes: 90)
);

$period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
$period->to->toIso8601(); # 2026-02-17T11:30:00+00:00
```

#### Getting the duration

```php
$period->duration()->seconds; # 5400
$period->duration()->toMinutes(); # 90
```

#### Checking if an instant is contained

The check is inclusive at the start and exclusive at the end:

```php
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)
```

#### Detecting overlap

Two half-open intervals `[A, B)` and `[C, D)` overlap when `A < D` and `C < B`:

```php
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::ofHours(hours: 1)
);
$periodB = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:30:00+00:00'),
duration: Duration::ofHours(hours: 1)
);

$periodA->overlapsWith(other: $periodB); # true
$periodB->overlapsWith(other: $periodA); # true
```

Adjacent periods do not overlap:

```php
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::ofHours(hours: 1)
);
$second = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T11:00:00+00:00'),
duration: Duration::ofHours(hours: 1)
);

$first->overlapsWith(other: $second); # false
```

### Timezone
Expand Down
Loading