An immutable, nanosecond-precise time duration type for PHP 8.3+.
$span = TimeSpan::from(hours: 1, minutes: 30);
echo $span->toMinutes(); // 90
echo $span->format(); // 01:30:00composer require thesis/time-spanPHP's built-in DateInterval covers calendar durations — periods like "3 months" or "1 year" that only make sense
relative to a specific date. TimeSpan covers the complementary case: a fixed amount of elapsed time that exists
independently of any calendar.
DateInterval |
TimeSpan |
|
|---|---|---|
| Represents | calendar period | fixed duration |
| Supports months/years | yes | no |
| Immutable | no | yes |
| Arithmetic | no | add sub mul div |
| Comparison | no | compareTo isLessThan … |
| Negative values | awkward invert flag |
signed int |
| Precision | microseconds | nanoseconds |
DateInterval is the right tool when you need "30 days from now" or "next month". TimeSpan is the right tool when you
need "wait 30 seconds" or "this request took 42 ms".
// HTTP / DB timeouts
$client = new HttpClient(timeout: TimeSpan::fromSeconds(30));
// Cache TTL
$cache->set($key, $value, ttl: TimeSpan::fromMinutes(5));
// Rate limiting — window duration
$limiter = new RateLimiter(window: TimeSpan::fromHours(1), limit: 1000);
// Retry with exponential backoff
$delay = TimeSpan::fromMilliseconds(100);
foreach (range(1, 5) as $attempt) {
try {
return $this->call();
} catch (TransientException) {
sleep($delay->toSeconds());
$delay = $delay->mul(2);
}
}
// Benchmarking
$start = TimeSpan::hrtime();
$result = $this->heavyComputation();
$elapsed = TimeSpan::hrtime()->sub($start);
$this->logger->info("Computed in {$elapsed->toMilliseconds(precision: 2)} ms");
// SLA / deadline check
$elapsed = TimeSpan::between($requestTime, new \DateTimeImmutable());
if ($elapsed->isGreaterThan(TimeSpan::fromSeconds(5))) {
$this->metrics->increment('sla.violated');
}use Thesis\TimeSpan;
$span = TimeSpan::from(days: 1, hours: 2, minutes: 30, seconds: 15);
$span = TimeSpan::from(milliseconds: 250);
$span = TimeSpan::from(seconds: 90); // same as 1 minute 30 seconds$span = TimeSpan::fromNanoseconds(1_500_000);
$span = TimeSpan::fromMicroseconds(1_500);
$span = TimeSpan::fromMilliseconds(1.5);
$span = TimeSpan::fromSeconds(90);
$span = TimeSpan::fromMinutes(1.5);
$span = TimeSpan::fromHours(0.25);
$span = TimeSpan::fromDays(7);All constructors accept int|float. Floats are rounded to the nearest nanosecond.
TimeSpan::fromInterval(new \DateInterval('PT90S')); // 90 seconds
TimeSpan::fromInterval(new \DateInterval('P7D')); // 7 daysNote: Intervals with years or months cannot be converted to a fixed duration and will throw an
InvalidArgumentException. Intervals produced byDateTimeInterface::diff()are also rejected due to DST ambiguity — useTimeSpan::between()instead.
$start = new \DateTimeImmutable('2024-01-01 10:00:00');
$end = new \DateTimeImmutable('2024-01-01 11:30:00');
$span = TimeSpan::between($start, $end); // 1 hour 30 minutesThe result is signed: between($a, $b) returns a negative span if $b is in the past relative to $a.
$start = TimeSpan::hrtime();
doSomething();
$elapsed = TimeSpan::hrtime()->sub($start);
echo $elapsed->toMilliseconds(precision: 3); // e.g. 42.731new TimeSpan(5_000_000_000); // 5 seconds
new TimeSpan(); // zero spanEvery to*() method returns int by default. Pass a $precision argument to get a float with that many decimal
places.
$span = TimeSpan::from(
days: 1,
hours: 1,
minutes: 30,
seconds: 45,
milliseconds: 500,
microseconds: 89,
nanoseconds: 23,
);
$span->toNanoseconds(); // 91_845_500_089_023
$span->toMicroseconds(); // 91_845_500_089
$span->toMilliseconds(); // 91_845_500
$span->toSeconds(); // 91846
$span->toSeconds(precision: 1); // 91845.5
$span->toMinutes(); // 1531
$span->toMinutes(precision: 4); // 1530.7583
$span->toHours(); // 26
$span->toDays(); // 1The optional second argument controls rounding mode (defaults to PHP_ROUND_HALF_UP):
$span->toSeconds(precision: 2, roundingMode: PHP_ROUND_HALF_DOWN);
$span->toSeconds(precision: 3, roundingMode: PHP_ROUND_HALF_EVEN);All arithmetic methods return a new TimeSpan instance and throw \OverflowException
if the result exceeds the int range (~292 years in nanoseconds on 64-bit platform).
$m30 = TimeSpan::fromMinutes(30);
$m15 = TimeSpan::fromMinutes(15);
$m30->add($m15); // 45 minutes
$m30->sub($m15); // 15 minutes
$m30->mul(3); // 90 minutes
$m30->mul(0.5); // 15 minutes
$m30->div(2); // 15 minutes
$m30->div(3); // 10 minutesdiv() throws \DivisionByZeroError when the factor is 0.
$s10 = TimeSpan::fromSeconds(10);
$s20 = TimeSpan::fromSeconds(20);
$s10->compareTo($s20); // -1 (less than)
$s20->compareTo($s10); // 1 (greater than)
$s10->compareTo($s10); // 0 (equal)
$s10->isEqualTo($s20); // false
$s10->isLessThan($s20); // true
$s10->isLessThanOrEqualTo($s20); // true
$s10->isGreaterThan($s20); // false
$s10->isGreaterThanOrEqualTo($s20); // false$s_5 = TimeSpan::fromSeconds(-5);
$s_5->isNegative(); // true
$s_5->isNegativeOrZero(); // true
$s_5->isPositive(); // false
$s_5->isPositiveOrZero(); // false
$s_5->isZero(); // false
$s_5->abs(); // TimeSpan(5 seconds)
$s_5->negated(); // TimeSpan(5 seconds)
TimeSpan::fromSeconds(5)->negated(); // TimeSpan(-5 seconds)format() renders a span as a human-readable string. The default pattern is %-%h:%i:%s.
| Placeholder | Unit | Width |
|---|---|---|
%- |
sign | 0–1 |
%d |
days | >=1 (unpadded) |
%h |
hours | >=2 (zero-padded) |
%i |
minutes | >=2 (zero-padded) |
%s |
seconds | >=2 (zero-padded) |
%ms |
milliseconds | >=3 (zero-padded) |
%us |
microseconds | >=3 (zero-padded) |
%ns |
nanoseconds | >=3 (zero-padded) |
The largest unit present in the format receives the total cumulative value; each smaller unit shows only the remainder after the larger ones are subtracted:
$span = TimeSpan::from(
days: 1,
hours: 2,
minutes: 3,
seconds: 4,
milliseconds: 500,
microseconds: 600,
nanoseconds: 700,
);
$span->format('%-%d %h:%i:%s.%ms_%us_%ns'); // "1 02:03:04.500_600_700"
$span->format('%-%d %h:%i:%s'); // "1 02:03:04"
$span->format('%-%d'); // "1"
$span->format(); // "26:03:04"
$span->format('%-%h:%i:%s.%ms'); // "26:03:04.500"
$span->format('%-%h:%i:%s.%ms_%us_%ns'); // "26:03:04.500_600_700"
$span->format('%-%i:%s.%ms_%us_%ns'); // "1563:04.500_600_700"
$span->format('%-%ns'); // "93784500600700"
$span->format('%-%h h %h:%i:%s'); // "26 h 26:03:04" (repeated placeholders are fine)
$span->format('fixed 5 seconds'); // "fixed 5 seconds" (no placeholders — literal string)The sign is only included when %- is explicitly present in the format:
$span = TimeSpan::fromSeconds(-90);
$span->format('%-%i:%s'); // "-01:30"
$span->format('%i:%s'); // "01:30" — no sign without %-__toString() is equivalent to format() with the default pattern:
$span = TimeSpan::from(hours: 1, minutes: 30);
echo $span; // "01:30:00"MIT