bakame/tokei

Local time and duration primitives without timezone complexity

Maintainers

Package info

github.com/bakame-php/tokei

Documentation

pkg:composer/bakame/tokei

Fund package maintenance!

nyamsprod

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-05-14 12:03 UTC

This package is auto-updated.

Last update: 2026-05-14 12:03:40 UTC


README

Author Software License Build Latest Version Total Downloads Sponsor development of this project

Tokei (pronounced: [to̞ke̞ː] or [tokeː]) is a lightweight package to work with time and duration without timezone information attached to them in PHP.

The framework-agnostic package provides strict, immutable value objects designed for precise and reliable time handling. This offers a consistent and expressive way to work with temporal values in a safe and predictable manner.

Installation

composer require bakame/tokei

You need:

  • PHP >= 8.3 but the latest stable version of PHP is recommended

Documentation

Time

The Bakame\Tokei\Time object is designed to be, cyclic (24h wrap-around) and precision-aware (microseconds supported)

Instantiation

You can create a Time instance:

  • using its time components via the Time::at method;
  • by parsing a time string using the Time::parse method;
  • using Time::atMicroOfDay, Time::atMilliOfDay, Time::atSecondOfDay or Time::atMinuteOfDay; The value will represent respectively the microseconds, milliseconds, seconds or minutes from midnight.
Time::at(int $hour = 0, int $minute = 0, int $second = 0, int $microsecond = 0): Time;
Time::parse(string $value,  string $separator = ':'): ?Time
Time::atMicroOfDay(int $value): Time
Time::atMilliOfDay(int $value): Time
Time::atSecondOfDay(int $value): Time
Time::atMinuteOfDay(int $value): Time

Here's some usage example.

use Bakame\Tokei\Time;

$timeB = Time::parse("10:30:15.123456");
$timeA = Time::at(hour: 10, minute: 30, second: 15);
$timeC = Time::atMicroOfDay(123_456_789);
$timeC = Time::atMilliOfDay(123_456); 
$timeC = Time::atSecondOfDay(123); 
$timeC = Time::atMinuteOfDay(456); 

Warning

On failure, with Time::parse, null is returned instead of an exception being thrown.

To ease instantiation, predefined instances can be obtained with the following methods:

Time::min();      // 00:00:00
Time::max();      // 23:59:59.999999
Time::noon();     // 12:00:00
Time::midnight(); // alias of min()

Accessors

Once instantiated you can access each time component using the following methods

$time = Time::parse("10:30:15.123456");
$time->hour;         // returns 10
$time->minute;       // returns 30
$time->second;       // returns 15
$time->microsecond;  // returns 123456

Formatting

Time::toMicroOfDay();  // returns 37_815_123_456 (the microseconds offset since midnight)
Time::format(
    string $separator = ':',
    PaddingMode $padding = PaddingMode::Padded,
    SubSecondDisplay $subSecond = SubSecondDisplay::Auto,
): string

Example:

$time = Time::parse("10:30:15.123456");

$time->format(subSecond: SubSecondDisplay::Auto);   // 10:30:15.123456 (default)
$time->format(subSecond: SubSecondDisplay::Never);  // 10:30:15
$time->format(subSecond: SubSecondDisplay::Always); // 10:30:15.123456
$time->toMicroOfDay(); // 37815123456
  • SubSecondDisplay::Auto only show the microseconds if their value is less than 0.
  • SubSecondDisplay::Never never show the fraction.
  • SubSecondDisplay::Always always show the fraction..

Modifying time

Because Time is an immutable VO, any change to its value will return a new instance with the updated value and leave the original object unchanged. You can modify the time with the following methods:

  • Time::add will add a duration to change the time;
  • Time::with will adjust a specif time component;
Time::add(Duration $duration): Time
Time::with(?int $hour = null, ?int $minute = null, ?int $second = null, ?int $microsecond = null): Time

Both methods act differently in regard to wrapping around 24hours automatically. The Time::add supports wrapping whereas Time::with does not and instead throws an InvalidTime exception instead

// adding 2 hours
$time = Time::noon()->add(Duration::of(hours: 2, minutes: 15));
$time->format(); // returns "14:15:00"

// adding 12 hours
$time = Time::noon()->add(Duration::of(hours: 12, minutes: 15));
$time->format(); // returns "00:15:00"

// setting the hour to
$time = Time::noon()->with(hour: 2);
$time->format(); // returns "02:15:00"

Time::noon()->with(hours: 25); 
//throws a Bakame\Tokei\InvalidTime exception

Comparing times

It is possible to compare two Time instances using the Time::compareTo method.

Time::compareTo(Time $other): int;

the method returns:

  • -1 if earlier
  • 0 if equal
  • 1 if later

Convenient methods derived from Time::compareTo are also available to ease usage:

$time = Time::at(hours: 10);
$other = Time::noon();

$time->isBefore($other);         // returns true
$time->isAfter($other);          // returns false
$time->isBeforeOrEqual($other);  // returns true
$time->isAfterOrEqual($other);   // returns false
$time->equals($other);           // returns false

Differences

The class provides two methods to account for differences between two Time instances:

Time::diff(Time $other): Duration;
Time::distance(Time $other): Duration;
  • the Time::diff returns the signed difference between both instances;
  • the Time::distance returns the forward cyclic difference (24 wrap) between both instances;

Here's an example usage to highlight the distinction in returned values between both differences methods:

$a = Time::at(hours: 23); // 23:00
$b = Time::at(hours: 1);  // 01:00

$a->diff($b)->toIso8601();     // returns "-PT22H"
$a->distance($b)->toIso8601(); // returns "PT2H"

Interacting with PHP's native Date API

Time::extractFrom(DateTimeInterface $datetime): Time
Time::applyTo(DateTimeInterface $datetime): DateTimeImmutable;

In one hand, it is possible to extract the time part of any DateTimeInterface implementing class using the extractFrom method. On the other hand, you can apply the time to an DateTimeInterface object using the applyTo method

Note

If the DateTimeInterface instance submitted extends the DateTimeImmutable class then the return type will be of that same type otherwise PHP's DateTimeImmutable is returned.

use Bakame\Tokei\Time;
use Carbon\Carbon;
use Carbon\CarbonImmutable;

$time = Time::extractFrom(new DateTime('2025-12-27 23:00', new DateTimeZone('Africa/Nairobi'))); // 23:00

$newDate = $time->applyTo(CarbonImmutable::parse('2025-02-23'));
$newDate->toDateTimeString(); // returns '2025-02-23 23:00'
$newDate::class; // returns CarbonImmutable

$altDate = $time->applyTo(Carbon::parse('2025-02-23'));
$altDate->toDateTimeString(); // returns '2025-02-23 23:00'
$altDate::class; // returns DateTimeImmutable

Duration

The Bakame\Tokei\Duration Value Object provides lightweight utilities for working with durations

Instantiation

The Duration class can be instantiated either by providing:

  • each duration parts using the complementaryDuration::of method.
  • a ISO8601 duration expression.
use Bakame\Tokei\Duration;

$durationA = Duration::of(hours: 2, seconds:59);
$durationB = Duration::fromIso8601('P2WT3H'); //2 weeks and 3 hours

Important

Duration::fromIso8601 only parse ISO8601 notations with deterministic part (ie: years and months are excluded)

$duration = Duration::fromIso8601('-P2YT3H');
// throws a Bakame\Tokei\InvalidDuration exception 
// because of the presence of the Y component

Accessors

Once instantiated you can access the duration properties directly. The object exposes a inverted property which indicates if the original value was negative or not. And provides a toMicro method to get the microseconds based representation of the duration.

$durationB->hours;        // returns 1
$durationB->minutes;      // returns 1
$durationB->seconds;      // returns 1
$durationB->microseconds; // returns 234_000
$durationB->inverted;     // returns false
$durationB->isEmpty()     // returns true when the duration is zero, false otherhwise 

Formatting

Duration::toClockFormat(): string

Formats the instance value into a human-readable string. The following format is used:

[-]H:mm:ss[.microseconds]
  • microseconds are optional (only shown if non-zero)
  • negative values are prefixed with -
Duration::toIso8601(): string

Formats the instance value into a ISO8601 compatible string. The returned string may not be compatible with PHP's DateInterval constructor but is valid withing the ISO8601 specification.

$duration = Duration::of(hours: 25, seconds: 5); 
$duration->toIso8601(); // returns 'P1D1H5S'

Important

  • Only deterministic duration interval are used Y, M for month are not used
  • to have a predictive representation W is not used; 7D multiple are used instead.
$duration = Duration::fromIso8610('-P2W'); 
$duration->toIso8601(); // returns '-P14D'

The Duration class also allows conversion in microseconds and in DateInterval instances.

Duration::toDateInterval(): DateInterval
Duration::toMicro(): int

The method Duration::toDateInterval converts the instance into a PHP DateInterval instance while preserving its sign (inverted intervals are supported).

$duration = Duration::of(microseconds: 3_661_234_000);
$duration->toDateInterval(); // returns DateInterval
$durationB->toMicro();       // returns the full duration in microseconds format

Modifying duration

Duration::abs(): Duration
Duration::negate(): Duration
Duration::truncateTo(Precision $precision): Duration
Duration::add(Duration ...$duration): Duration
Duration::increment(int $hours = 0, int $minutes = 0, int $seconds = 0, int $microseconds = 0): Duration

You can:

  • make it unsigned using the Duration::abs method
  • invert its signing using the Duration::negate method
  • update the duration using fixed duration parts Duration::increment method
  • truncate its value to one of the unit declare on the Bakame\Tokei\Precision enum
  • sum multiple Duration instance using the Duration::sum method
$microseconds = 3_661_500_000;
$a = Duration::of(microseconds: $microseconds);
$b = $a->truncateTo(Precision::Minutes);
$c = $b->negate();
$d = $c->increment(minutes: -10);

echo $a->toClockFormat();                  // returns "1:01:01.500000"
echo $b->toClockFormat();                  // returns "1:01:00"
echo $c->toClockFormat();                  // returns "-1:01:00"
echo $c->abs()->toClockFormat();           // returns "1:01:00"
echo $a->sum($b, $c, $d)->toClockFormat(); // returns "-0:09:58.500000"

Comparing duration

It is possible to compare duration using common methods terminology

Duration::compareTo(Duration $other): int;

Returns:

  • -1 if lesser
  • 0 if equal
  • 1 if greater

Convenient methods based on Duration::compareTo are also available:

$duration = Duration::of(microseconds: 3_661_500_000);
$other = Duration::fromIso8601('PT1H1S');

$duration->isLessThan($other);           // returns false
$duration->isLessThanOrEqual($other);    // returns false
$duration->equals($other);               // returns false
$duration->isGreaterThan($other);        // returns true
$duration->isGreaterThanOrEqual($other); // returns true

Testing

The library has:

  • a PHPUnit test suite.
  • a coding style compliance test suite using PHP CS Fixer.
  • a code analysis compliance test suite using PHPStan.

To run the tests, run the following command from the project folder.

composer test

Contributing

Contributions are welcome and will be fully credited. Please see CONTRIBUTING and CONDUCT for details.

Security

If you discover any security related issues, please email nyamsprod@gmail.com instead of using the issue tracker.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Credits