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
Durationinstances - Duration parts.
- Duration string expression.
- PHP’s
DateIntervalandTime\Durationinstances.
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 durationDuration::min()represents the smallest representable durationDuration::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
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);
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'
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'
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()andDuration::sub()methods - multiply or divide a
Durationinstance using theDuration::multiplyBy()andDuration::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;
Returns:
-1if$ais shorter than$b0if$ais equal to$b1if$ais 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