Duration

The Bakame\Tokei\Duration immutable value object provides utilities for working with durations

Instantiation

The Duration class can be instantiated using:

  • a set of commonly expected Duration instances
  • Duration parts.
  • Duration string expression.
  • PHP’s DateInterval and Time\Duration instances.

Special instances

Four specific named constructors are added to represent special semantic values.

  • Duration::zero() represents a duration of 0 second.
  • Duration::fullDay() represents a complete 24-hour clock duration
  • Duration::min() represents the smallest representable duration
  • Duration::max() represents the largest representable duration

Using duration parts

Duration::of(
    int $weeks = 0,
    int $days = 0,
    int $hours = 0,
    int $minutes = 0,
    int $seconds = 0,
    int $milliseconds = 0,
    int $microseconds = 0,
    int $nanoseconds = 0,
): Duration

Duration::of only using non-negative integer otherwise and exception is thrown

use Bakame\Tokei\Duration;

Duration::of(hours: 2, minutes: 63);
// works returns 
Duration::of(hours: 2, minutes: -63);
// throws InvalidDuration because the minutes are negative

Tokei\Duration class mimics Time\Duration API and exposes the same Duration::from* methods for each unit separately. It also includes Duration::fromDays and Duration::fronWeeks for consistency.

use Bakame\Tokei\Duration;

Duration::fromWeeks(3);
// is equivalent to
Duration::of(weeks: 3);

Duration::fromHours(3);
// is equivalent to
Duration::of(hours: 3);

Duration::fromMinutes(3);
// is equivalent to
Duration::of(minutes: 3);

Using PHP classes

Time\Duration

PHP’s Time\Duration instances can be converted to Tokei\Duration instances.

use Time\Duration as TimeDuration;

Duration::fromNative(TimeDuration::fromHours(12));

DateInterval

PHP’s DateInterval instances can be converted to Tokei\Duration instances.

use Bakame\Tokei\Duration;

$duration = Duration::fromDateInterval(new DateInterval('PT23M3S'));

There are some restrictions when converting a DateInterval to a Duration. If the DateInterval contains years (Y) or months (M), the duration cannot be determined from the interval alone, because the number of days represented by a year or month depends on the dates involved. In this case, an InvalidDuration exception is thrown.

use Bakame\Tokei\Duration;

$duration = Duration::fromDateInterval(new DateInterval('P2M'));
// throws an InvalidDuration

However, when a DateInterval is generated by DateTimeInterface::diff(), PHP also provides the total number of days in the $days property. This allows Duration::fromDateInterval() to determine the exact elapsed duration, even when the interval contains years or months.

use Bakame\Tokei\Duration;

$dateInterval = new DateInterval('P2M');

$start = new DateTimeImmutable('2024-02-01');
$end = $start->add($dateInterval);

echo $start->format('Y-m-d'), ' → ', $end->format('Y-m-d'), ': ',
    Duration::fromDateInterval($start->diff($end))
        ->format(DurationFormat::LargestUnit), PHP_EOL;
// "2024-02-01 → 2024-04-01: 60d"

$start = new DateTimeImmutable('2025-02-01');
$end = $start->add($dateInterval);

echo $start->format('Y-m-d'), ' → ', $end->format('Y-m-d'), ': ', 
    Duration::fromDateInterval($start->diff($end))
        ->format(DurationFormat::LargestUnit), PHP_EOL;
// "2025-02-01 → 2025-04-01: 59d"

Using string formats

Duration::fromFormat() returns a Duration instance governed by the selected DurationFormat case.

ISO-8601

When using the DurationFormat::Iso8601 Enum case, the Duration::fromFormat() expects a string notification that satisfies ISO 8601 duration specification.

Duration::fromFormat('PT25S', DurationFormat::Iso8601);
// is equivalent to
Duration::of(seconds: 25);

Non-deterministic part (ie: years and months are excluded) are excluded and will throw an exception.

Duration::fromFormat('P2025Y3DT25S', DurationFormat::Iso8601);
// throws a Bakame\Tokei\InvalidDuration exception 
// because of the presence of the Y component

Negative duration and fractional seconds are supported

Duration::fromFormat('-PT3.02S', DurationFormat::Iso8601);
// is equivalent to
Duration::of(seconds: 3, milliseconds: 20)->negate();

Timer

When used with DurationFormat::Timer Enum case The Duration::fromFormat expects a string whic follow this specificatio.

[-]HH:mm:ss[.fffffffff]
  • The fraction part is optional.
  • The hours and minutes parts must always be present to avoid confusion on parsing.
use Bakame\Tokei\Duration;
use Bakame\Tokei\DurationFormat;

Duration::fromFormat('00:30:24', DurationFormat::Timer);
// returns the equivalent to
Duration::of(minutes: 30, seconds: 24);

Compact

DurationFormat::Compact parses a compact sequence of duration components.

Each component consists of a numeric value followed by a unit abbreviation:

1d2h30m15s500ms

Components may be combined to express a duration using multiple units.

use Bakame\Tokei\Duration;
use Bakame\Tokei\DurationFormat;

Duration::fromFormat('1d2m12ms', DurationFormat::Compact);
// returns the equivalent to
Duration::of(days: 1, minutes: 2, milliseconds: 12);

Weeks and days are supported but their value will be converted as a week represents 7 days and a day 24 hours. The format accept notation with a + or a - sign in front to explicitly state the duration sign.

use Bakame\Tokei\Duration;
use Bakame\Tokei\DurationFormat;

Duration::fromFormat('-3s3us', DurationFormat::Compact);
// returns the equivalent to
Duration::of(seconds: 1, microseconds: 3)->negate();

The supported unit abbreviations are:

Unit Abbreviations
nanosecond ns
microsecond us, µs
millisecond ms
second s
minute m
hour h
day d
week w

The abbreviation are case-insensitive.

Largest and Total Unit

When using the DurationFormat::LargestUnit or the DurationFormat::TotalUnit Enum case, the Duration::fromFormat() expects a duration expressed as a numeric quantity followed by exactly one unit.

  • For DurationFormat::LargestUnit, the numeric value may contain a fractional part:
  • For DurationFormat::TotalUnit, the numeric value cannot contain a fractional part:
use Bakame\Tokei\Duration;
use Bakame\Tokei\DurationFormat;

$duration = Duration::fromFormat(
    notation: '1.5h',
    format: DurationFormat::LargestUnit,
);

$duration = Duration::fromFormat(
    notation: '90min',
    format: DurationFormat::TotalUnit,
);

// both returns the equivalent to
Duration::of(hours: 1, minutes: 30);

Unit names are case-insensitive and may be surrounded by whitespace. Value may be prefixed with the - to denote a negative duration. The following notations therefore all represent the same duration:

Duration::fromFormat('-1Min', DurationFormat::TotalUnit);
Duration::fromFormat(' -1MINUTE ', DurationFormat::TotalUnit);
Duration::fromFormat('-1MiNutes   ', DurationFormat::TotalUnit);

Whitespace between the numeric value and the unit is also permitted:

Duration::fromFormat('1    minute', DurationFormat::TotalUnit);
Duration::fromFormat('1.5   HourS', DurationFormat::LargestUnit);

The supported units and their accepted names are:

Unit Accepted names
nanosecond n, ns, nanosecond, nanoseconds
microsecond us, µs, microsecond, microseconds
millisecond ms, millisecond, milliseconds
second s, second, seconds
minute m, min, minute, minutes
hour h, hour, hours
day d, day, days
week w, week, weeks

The accepted names are case-insensitive.

When using DurationFormat::LargestUnit, fractional quantities must be exactly representable at nanosecond precision. A quantity requiring a fraction of a nanosecond is rejected.

For example:

Duration::fromFormat(
    notation: '1.000004 microseconds',
    format: DurationFormat::LargestUnit,
);
// throws InvalidDuration

because 1.000004 microseconds corresponds to 1000.004 nanoseconds.

In contrast:

Duration::fromFormat(
    notation: '1.000004 milliseconds',
    format: DurationFormat::LargestUnit,
);

is valid because it corresponds to exactly 1 000 004 nanoseconds.

Accessors

Once instantiated, you can access the duration properties directly. The object exposes a sign property, which indicates whether the original value is negative, zero, or positive. For convenience, it also provides the isNegative() and isZero() methods to test the sign without comparing the sign property directly.

The duration is represented by the following properties:

  • seconds: the whole number of seconds in the duration.
  • nanoseconds: the fractional part of the duration, expressed in nanoseconds.
  • negative: tells whether the duration is negative or not.

The in() method returns the total duration expressed in a specific unit.

Depending on the requested unit and the duration itself, the returned value may be either an integer or a floating-point number.

$duration = Duration::fromDateInterval(new DateInterval('PT23M3S'));
$duration->in(Unit::Microsecond); // returns 1383_000_000
$duration->in(Unit::Minute);      // returns 23.05
$duration->isZero();              // returns false
$duration->nanoseconds;           // returns 0
$duration->seconds;               // returns 1383
$duration->negative;              // returns false

Formatting

Duration::format(DurationFormat $format): string
Duration::toNumberString(
    Unit $unit,
    int $precision = 0,
    DisplaySign $displaySign = DisplaySign::Auto,
): string

Duration::format() and Duration::toNumberString() provide two complementary ways to obtain a string representation of a Duration.

Duration::format() returns a representation governed by the selected DurationFormat case.

ISO-8601

DurationFormat::Iso8601 formats a duration as an ISO-8601-compatible duration string.

Only deterministic duration units are used. Years (Y) and months (M) are not used because their durations are not fixed.

Weeks (W) are also not used in the output to provide a predictable representation. A week is instead represented as 168 hours.

$duration = Duration::of(hours: 23, seconds: 3);

$duration->format(DurationFormat::Iso8601);
// returns 'PT23H3S'

For example, an ISO-8601 duration containing weeks is normalized to hours:

$duration = Duration::fromFormat('-P2W', DurationFormat::Iso8601);

$duration->format(DurationFormat::Iso8601);
// returns '-PT336H'
The returned string may not be compatible with PHP's DateInterval constructor, but it is valid within the ISO-8601 extended specification.

Timer

DurationFormat::Timer formats a duration using human-readable timer notation:

[-]HH:mm:ss[.fffffffff]

Fractional seconds are optional and can contain up to 9 digits of nanosecond precision. Negative durations are prefixed with -.

$duration = Duration::of(hours: 23, seconds: 3);

$duration->format(DurationFormat::Timer);
// returns '23:00:03'

Compact

DurationFormat::Compact formats a duration using a compact representation. Zero-valued components are omitted, and each remaining component is suffixed with its unit abbreviation.

$duration = Duration::fromFormat('-PT25H0.5S', DurationFormat::Iso8601);

$duration->format(DurationFormat::Compact);
// returns '-1d1h500ms'

The compact format expresses weeks as 7 days and days as 24 hours.

Largest and Total unit

Largest and Total unit

DurationFormat::LargestUnit formats a duration using the largest suitable unit, allowing fractional values when necessary.

DurationFormat::TotalUnit formats a duration as a total quantity using a single unit, automatically selecting the largest unit that preserves the duration’s precision.

For example:

$duration = Duration::of(days: 21, hours: 13, minutes: 55, seconds: 12);

$duration->format(DurationFormat::LargestUnit);
// returns '21.58d'

$duration->format(DurationFormat::TotalUnit);
// returns '1864512s'

Unlike Compact, which can contain multiple units, DurationFormat::LargestUnit and DurationFormat::TotalUnit always uses exactly one unit.

$duration = Duration::of( hours: 3, minutes: 13, seconds: 17 * 60)->negate();

$duration->format(DurationFormat::Iso8601);
// returns '-PT3H30M'

$duration->format(DurationFormat::Timer);
// returns '-03:30:00'

$duration->format(DurationFormat::Compact);
// returns '-3h30m'

$duration->format(DurationFormat::LargestUnit);
// returns '-3.5h'

$duration->format(DurationFormat::LargestUnit);
// returns '-210m'

Numeric representation

Duration::toNumberString() returns the numeric representation of a duration converted to an explicitly selected unit.

It complements Duration::in() by providing control over the textual representation of the converted value.

$duration = Duration::of(hours: 23, seconds: 3);

$duration->toNumberString(Unit::Minute);
// returns '1380'

$duration->toNumberString(
    Unit::Minute,
    precision: 2,
    displaySign: DisplaySign::Always,
);
// returns '+1380.05'

The precision argument specifies the number of fractional digits to include in the resulting string. If the converted value is an integer, the argument is ignored.

Passing a negative value for precision throws a ValueError.

Duration::toNumberString() is intended solely for formatting the numeric representation of a duration. If you need to round or truncate the duration before converting it to a string, use Duration::roundTo() first.

This keeps rounding behavior explicit and ensures consistent, reproducible results.

Duration::fromFormat vs Duration::format

Both methods use DurationFormat consistently to identify the format being parsed or produced:

$duration = Duration::fromFormat('3.5h',DurationFormat::LargestUnit);

$duration->format(DurationFormat::LargestUnit);
// returns '3.5h'

However, parsing and formatting do not necessarily preserve the original representation.

For DurationFormat::LargestUnit and DurationFormat::TotalUnit formatting automatically selects a suitable unit, whereas parsing accepts any supported single-unit representation explicitly provided by the caller:

$duration = Duration::fromFormat('210 minutes',DurationFormat::LargestUnit);

$duration->format(DurationFormat::LargestUnit);
// returns '3.5h'
$duration->format(DurationFormat::TotalUnit);
// returns '210m'

The same principle applies to DurationFormat::Iso8601. Parsing accepts different ISO-8601 representations of the same duration, while formatting produces a canonical representation:

$duration = Duration::fromFormat('-P2W', DurationFormat::Iso8601);
$duration->format(DurationFormat::Iso8601);
// returns '-PT336H'

For DurationFormat::LargestUnit multiple representations of a unit name are accepted when parsing. Formatting always uses the abbreviated unit name without whitespace between the numeric value and the unit:

$duration = Duration::fromFormat('1.5 hours', DurationFormat::LargestUnit);
$duration->format(DurationFormat::LargestUnit);
// returns '1.5h'

Duration::fromFormat interprets the unit and format supplied by the caller, whereas Duration::format chooses the unit and representation used to format the duration.

Modifying duration

The Duration class is immutable. All operations described here return a new instance instead of modifying the original object.

Arithmetic

Duration::absolute(): Duration
Duration::negate(): Duration
Duration::add(Duration ...$duration): Duration
Duration::sub(Duration ...$duration): Duration
Duration::multiplyBy(int $factor): Duration
Duration::divideBy(int $factor): Duration

You can:

  • make it unsigned using the Duration::absolute() method
  • invert its sign using the Duration::negate() method
  • combine multiple duration instances using the Duration::add() and Duration::sub() methods
  • multiply or divide a Duration instance using the Duration::multiplyBy() and Duration::divideBy() methods
use Bakame\Tokei\Duration;
use Bakame\Tokei\DurationFormat;

$microseconds = 3_661_500_000;
$a = Duration::of(microseconds: $microseconds);
$b = $a->negate();
$c = $b->sub(Duration::of(minutes: 10));

echo $a->format(DurationFormat::Timer);
// returns "1:01:01.500000"
echo $b->format(DurationFormat::Timer);
// returns "-01:01:01.500"
echo $c->format(DurationFormat::Timer);
// returns "-01:11:01.500"
echo $c->absolute()->format(DurationFormat::Timer);
// returns "01:11:01.500"
echo $a->add($b, $c)->format(DurationFormat::Timer);
// returns "-01:11:01.500"

Rounding and clamping

Duration::roundTo(Unit $precision, SnapMode $mode): Duration
Duration::clamp(Duration $min, Duration $max): Duration
Use Bakame\Tokei\Duration;

$microseconds = 3_761_500_000;
$a = Duration::of(microseconds: $microseconds);
$a->format(DurationFormat::Timer);
// returns "1:02:41.500000"
$a->roundTo(Unit::Minute, SnapMode::Floor)->format(DurationFormat::Timer);
// returns "1:02:00"
$a->roundTo(Unit::Minute, SnapMode::Ceil)->format(DurationFormat::Timer);
// returns "1:03:00"
$a->clamp(
    Duration::of(hours: 1), 
    Duration::of(days: 1)
)->format(DurationFormat::Timer);
// returns "1:00:00"

Dividing durations

Duration::divideInto(Duration $factor): DivisionResult

Duration provides two ways to divide durations. divideBy() divides a duration by an integer factor and returns a Duration, while divideInto() divides one duration by another and returns a DivisionResult containing the quotient and remainder.

$duration = Duration::fromFormat('-PT5H30M', DurationFormat::Iso8601);
$oneHour = Duration::of(hours: 1);
$result = $duration->divideInto($oneHour);
$result->quotient;
// returns '-5'
$result->remainder->format(DurationFormat::Iso8601);
// returns '-PT30M'

[$quotient, $remainder] = $result;
$quotient;
// returns '-5'
$remainder->format(DurationFormat::Iso8601);
// returns '-PT30M'

Remainder

Duration::modulo(Duration $factor): Duration

The modulo() method provides direct access to the remainder. This is especially useful when dealing with circular ranges, as the result of modulo() is always a non-negative Duration instance.

Unlike the remainder returned by divideInto(), which preserves the sign of the dividend, modulo() always returns a non-negative duration.

$duration = Duration::of(hours: 3, seconds: 35);
$factor = Duration::of(hours: 1);

echo $duration
    ->modulo($factor)
    ->format(DurationFormat::Compact), PHP_EOL;
//returns 35s;

echo $duration
    ->divideInto($factor)
    ->remainder
    ->format(DurationFormat::Compact), PHP_EOL;
//returns 35s;

echo $duration
    ->negeate()
    ->modulo($factor)
    ->format(DurationFormat::Compact), PHP_EOL;
//returns 59m25s;

echo $duration
    ->negeate()
    ->divideInto($factor)
    ->remainder
    ->format(DurationFormat::Compact), PHP_EOL;
//returns -35s;

Comparing duration

It is possible to compare duration using common methods terminology

Duration::compare(Duration $a, Duration $b): int;

The method is static to allow broader usage with other PHP sorting functions.

Returns:

  • -1 if $a is shorter than $b
  • 0 if $a is equal to $b
  • 1 if $a is longer than $b

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

$duration = Duration::of(microseconds: 3_661_500_000);
$other = Duration::fromFormat('PT1H1S', DurationFormat::Iso8601);

Duration::compare($duration, $other);    //returns 1
$duration->isShorterThan($other);        // returns false
$duration->isShorterThanOrEqual($other); // returns false
$duration->equals($other);               // returns false
$duration->isLongerThan($other);         // returns true
$duration->isLongerThanOrEqual($other);  // returns true
Logo