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
196 changes: 170 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* [Instant](#instant)
* [Duration](#duration)
* [Period](#period)
* [DayOfWeek](#dayofweek)
* [TimeOfDay](#timeofday)
* [Timezone](#timezone)
* [Timezones](#timezones)
* [License](#license)
Expand Down Expand Up @@ -113,9 +115,9 @@ use TinyBlocks\Time\Duration;

$instant = Instant::fromString(value: '2026-02-17T10:00: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
$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:00
```

#### Measuring distance between instants
Expand All @@ -130,15 +132,15 @@ $end = Instant::fromString(value: '2026-02-17T11:30:00+00:00');

$duration = $start->durationUntil(other: $end);

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

The result is always non-negative regardless of direction:

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

#### Comparing instants
Expand Down Expand Up @@ -170,43 +172,62 @@ timeline — it expresses only "how much" time.
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);
$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:

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

#### Arithmetic

```php
use TinyBlocks\Time\Duration;

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

$a->plus(other: $b)->seconds; # 2700 (45 minutes)
$a->minus(other: $b)->seconds; # 900 (15 minutes)
$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:

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

#### Division

Returns the number of times one `Duration` fits wholly into another. The result is truncated toward zero:

```php
use TinyBlocks\Time\Duration;

$total = Duration::fromMinutes(minutes: 90);
$slot = Duration::fromMinutes(minutes: 30);

$total->divide(other: $slot); # 3
```

Division by a zero `Duration` throws an exception:

```php
$total->divide(other: Duration::zero()); # throws InvalidSeconds
```

#### Comparing durations

```php
use TinyBlocks\Time\Duration;

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

$short->isLessThan(other: $long); # true
$long->isGreaterThan(other: $short); # true
Expand All @@ -221,8 +242,9 @@ Conversions truncate toward zero when the duration is not an exact multiple:
```php
use TinyBlocks\Time\Duration;

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

$duration->toSeconds(); # 5400
$duration->toMinutes(); # 90
$duration->toHours(); # 1
$duration->toDays(); # 0
Expand All @@ -239,7 +261,7 @@ end is exclusive.
use TinyBlocks\Time\Instant;
use TinyBlocks\Time\Period;

$period = Period::of(
$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')
);
Expand All @@ -251,7 +273,7 @@ $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
Period::from(from: $later, to: $earlier); # throws InvalidPeriod
```

#### Creating from a start and duration
Expand All @@ -263,7 +285,7 @@ use TinyBlocks\Time\Period;

$period = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::ofMinutes(minutes: 90)
duration: Duration::fromMinutes(minutes: 90)
);

$period->from->toIso8601(); # 2026-02-17T10:00:00+00:00
Expand All @@ -273,7 +295,7 @@ $period->to->toIso8601(); # 2026-02-17T11:30:00+00:00
#### Getting the duration

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

Expand All @@ -300,11 +322,11 @@ use TinyBlocks\Time\Period;

$periodA = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::ofHours(hours: 1)
duration: Duration::fromHours(hours: 1)
);
$periodB = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:30:00+00:00'),
duration: Duration::ofHours(hours: 1)
duration: Duration::fromHours(hours: 1)
);

$periodA->overlapsWith(other: $periodB); # true
Expand All @@ -320,16 +342,138 @@ use TinyBlocks\Time\Period;

$first = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T10:00:00+00:00'),
duration: Duration::ofHours(hours: 1)
duration: Duration::fromHours(hours: 1)
);
$second = Period::startingAt(
from: Instant::fromString(value: '2026-02-17T11:00:00+00:00'),
duration: Duration::ofHours(hours: 1)
duration: Duration::fromHours(hours: 1)
);

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

### DayOfWeek

A `DayOfWeek` represents a day of the week following ISO 8601, where Monday is 1 and Sunday is 7.

#### Deriving from an Instant

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

#### Checking weekday or weekend

```php
use TinyBlocks\Time\DayOfWeek;

DayOfWeek::Monday->isWeekday(); # true
DayOfWeek::Monday->isWeekend(); # false
DayOfWeek::Saturday->isWeekday(); # false
DayOfWeek::Saturday->isWeekend(); # true
```

### TimeOfDay

A `TimeOfDay` represents a time of day (hour and minute) without date or timezone context. Values range from 00:00 to
23:59.

#### Creating from components

```php
use TinyBlocks\Time\TimeOfDay;

$time = TimeOfDay::from(hour: 8, minute: 30);

$time->hour; # 8
$time->minute; # 30
```

#### Creating from a string

Parses a string in `HH:MM` format:

```php
use TinyBlocks\Time\TimeOfDay;

$time = TimeOfDay::fromString(value: '14:30');

$time->hour; # 14
$time->minute; # 30
```

#### Deriving from an Instant

Extracts the time of day from an `Instant` in UTC:

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

#### Named constructors

```php
use TinyBlocks\Time\TimeOfDay;

$midnight = TimeOfDay::midnight(); # 00:00
$noon = TimeOfDay::noon(); # 12:00
```

#### Comparing times

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

#### Measuring distance between times

Returns the `Duration` between two times. The second time must be after the first:

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

#### Converting to other representations

```php
use TinyBlocks\Time\TimeOfDay;

$time = TimeOfDay::from(hour: 8, minute: 30);

$time->toMinutesSinceMidnight(); # 510
$time->toDuration()->toSeconds(); # 30600
$time->toString(); # 08:30
```

### Timezone

A `Timezone` is a Value Object representing a single valid [IANA timezone](https://www.iana.org) identifier.
Expand Down
52 changes: 52 additions & 0 deletions src/DayOfWeek.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Time;

/**
* Represents a day of the week following ISO 8601, where Monday is 1 and Sunday is 7.
*/
enum DayOfWeek: int
{
case Monday = 1;
case Tuesday = 2;
case Wednesday = 3;
case Thursday = 4;
case Friday = 5;
case Saturday = 6;
case Sunday = 7;

/**
* Derives the day of the week from an Instant.
*
* @param Instant $instant The point in time to extract the day from.
* @return DayOfWeek The corresponding day of the week in UTC.
*/
public static function fromInstant(Instant $instant): DayOfWeek
{
$isoDay = (int)$instant->toDateTimeImmutable()->format('N');

return self::from($isoDay);
}

/**
* Checks whether this day falls on a weekday (Monday through Friday).
*
* @return bool True if this is a weekday.
*/
public function isWeekday(): bool
{
return $this->value <= 5;
}

/**
* Checks whether this day falls on a weekend (Saturday or Sunday).
*
* @return bool True if this is a weekend day.
*/
public function isWeekend(): bool
{
return $this->value >= 6;
}
}
Loading