[PHP-CVS] [php-src] master: Time\Duration (#23073)

[email protected] (Tim Düsterhus via GitHub)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Tim Düsterhus (TimWolla)
Committer: GitHub (web-flow)
Pusher: TimWolla
Date: 2026-08-07T12:04:40+02:00

Commit: https://github.com/php/php-src/commit/1cf46237a56540c14d84d0f04b245a10d4ee5bc5
Raw diff: https://github.com/php/php-src/commit/1cf46237a56540c14d84d0f04b245a10d4ee5bc5.diff

Time\Duration (#23073)

* date: Add `Time\Duration`

RFC: https://wiki.php.net/rfc/duration_class

* date: Cache the most recently `Time\Duration` object created with `from*()`

This is useful for patterns like the following:

    for (;;) {
        $watchers = $poll->wait(Time\Duration::fromSeconds(1));
        // …
    }

which is repeatedly creating identical duration objects for every loop
iteration.

* date: Add `Z_PARAM_ULONG()` helper to `time_duration.c`

* date: Make `tests/time/duration/helper.inc` compatible with PHP < 8.6

This will allow polyfills to more easily consume the upstream tests.

* NEWS / UPGRADING

Co-authored-by: Tim Düsterhus <[email protected]>

Changed paths:
  A  ext/date/php_time.c
  A  ext/date/php_time.h
  A  ext/date/tests/time/duration/clone.phpt
  A  ext/date/tests/time/duration/helper.inc
  A  ext/date/tests/time/duration/methods/absolute.phpt
  A  ext/date/tests/time/duration/methods/add.phpt
  A  ext/date/tests/time/duration/methods/add_32.phpt
  A  ext/date/tests/time/duration/methods/add_64.phpt
  A  ext/date/tests/time/duration/methods/compare.phpt
  A  ext/date/tests/time/duration/methods/divideBy.phpt
  A  ext/date/tests/time/duration/methods/divideBy_64.phpt
  A  ext/date/tests/time/duration/methods/fromHours.phpt
  A  ext/date/tests/time/duration/methods/fromHours_32.phpt
  A  ext/date/tests/time/duration/methods/fromHours_64.phpt
  A  ext/date/tests/time/duration/methods/fromIso8601DurationString.phpt
  A  ext/date/tests/time/duration/methods/fromIso8601DurationString_64.phpt
  A  ext/date/tests/time/duration/methods/fromMicroseconds.phpt
  A  ext/date/tests/time/duration/methods/fromMicroseconds_64.phpt
  A  ext/date/tests/time/duration/methods/fromMilliseconds.phpt
  A  ext/date/tests/time/duration/methods/fromMilliseconds_64.phpt
  A  ext/date/tests/time/duration/methods/fromMinutes.phpt
  A  ext/date/tests/time/duration/methods/fromMinutes_32.phpt
  A  ext/date/tests/time/duration/methods/fromMinutes_64.phpt
  A  ext/date/tests/time/duration/methods/fromNanoseconds.phpt
  A  ext/date/tests/time/duration/methods/fromNanoseconds_64.phpt
  A  ext/date/tests/time/duration/methods/fromSeconds.phpt
  A  ext/date/tests/time/duration/methods/fromSeconds_64.phpt
  A  ext/date/tests/time/duration/methods/multiplyBy.phpt
  A  ext/date/tests/time/duration/methods/multiplyBy_32.phpt
  A  ext/date/tests/time/duration/methods/multiplyBy_64.phpt
  A  ext/date/tests/time/duration/methods/negate.phpt
  A  ext/date/tests/time/duration/methods/sub.phpt
  A  ext/date/tests/time/duration/methods/sub_32.phpt
  A  ext/date/tests/time/duration/methods/sub_64.phpt
  A  ext/date/tests/time/duration/new.phpt
  A  ext/date/tests/time/duration/readonly.phpt
  A  ext/date/time.stub.php
  A  ext/date/time_arginfo.h
  A  ext/date/time_duration.c
  M  NEWS
  M  UPGRADING
  M  ext/date/config.w32
  M  ext/date/config0.m4
  M  ext/date/php_date.c
  M  ext/date/php_date.h


Diff:

diff --git a/NEWS b/NEWS
index cc4ff914844c..890c5d89039d 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,7 @@ PHP                                                                        NEWS
 
 - Date:
   . Update timelib to 2026.02. (Derick, timwolla)
+  . Added Time\Duration. (timwolla, Derick)
 
 - GMP:
   . Added optional $definitely_prime output parameter to gmp_prevprime().
diff --git a/UPGRADING b/UPGRADING
index 25a405e1521d..ba4bf182911f 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -313,6 +313,10 @@ PHP 8.6 UPGRADE NOTES
     offset and origin, and must return one of CURL_SEEKFUNC_OK,
     CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
 
+- Date:
+  . Added a new Time\Duration class.
+    RFC: https://wiki.php.net/rfc/duration_class
+
 - Fileinfo:
   . finfo_file() now works with remote streams.
 
@@ -553,6 +557,12 @@ PHP 8.6 UPGRADE NOTES
 7. New Classes and Interfaces
 ========================================
 
+- Date:
+  . Time\Duration
+    RFC: https://wiki.php.net/rfc/duration_class
+  . Time\TimeException
+    RFC: https://wiki.php.net/rfc/duration_class
+
 - Intl:
   . IntlNumberRangeFormatter
 
diff --git a/ext/date/config.w32 b/ext/date/config.w32
index 78cca036d380..327242f128ac 100644
--- a/ext/date/config.w32
+++ b/ext/date/config.w32
@@ -1,6 +1,6 @@
 // vim:ft=javascript
 
-EXTENSION("date", "php_date.c", false, "/Iext/date/lib /DHAVE_TIMELIB_CONFIG_H=1");
+EXTENSION("date", "php_date.c php_time.c time_duration.c", false, "/Iext/date/lib /DHAVE_TIMELIB_CONFIG_H=1");
 PHP_DATE = "yes";
 ADD_SOURCES("ext/date/lib", "astro.c timelib.c dow.c duration.c parse_date.c parse_posix.c parse_tz.c tm2unixtime.c unixtime2tm.c parse_iso_intervals.c interval.c", "date");
 
diff --git a/ext/date/config0.m4 b/ext/date/config0.m4
index 4853882465e0..aab6d3d139b3 100644
--- a/ext/date/config0.m4
+++ b/ext/date/config0.m4
@@ -18,7 +18,7 @@ timelib_sources="lib/astro.c lib/dow.c lib/duration.c lib/parse_date.c lib/parse
                  lib/timelib.c lib/tm2unixtime.c lib/unixtime2tm.c lib/parse_iso_intervals.c lib/interval.c"
 
 PHP_NEW_EXTENSION([date],
-  [php_date.c],
+  [php_date.c php_time.c time_duration.c],
   [no],,
   [$PHP_DATE_CFLAGS])
 
diff --git a/ext/date/php_date.c b/ext/date/php_date.c
index 0e24e4450641..9f9f0a6159ed 100644
--- a/ext/date/php_date.c
+++ b/ext/date/php_date.c
@@ -18,6 +18,7 @@
 #include "ext/standard/info.h"
 #include "ext/standard/php_versioning.h"
 #include "php_date.h"
+#include "php_time.h"
 #include "zend_attributes.h"
 #include "zend_interfaces.h"
 #include "zend_exceptions.h"
@@ -386,6 +387,7 @@ static PHP_GINIT_FUNCTION(date)
 	date_globals->default_timezone = NULL;
 	date_globals->timezone = NULL;
 	date_globals->tzcache = NULL;
+	date_globals->duration_cache = NULL;
 }
 /* }}} */
 
@@ -418,6 +420,10 @@ PHP_RSHUTDOWN_FUNCTION(date)
 		efree(DATEG(timezone));
 	}
 	DATEG(timezone) = NULL;
+	if (DATEG(duration_cache)) {
+		zend_object_release(DATEG(duration_cache));
+	}
+	DATEG(duration_cache) = NULL;
 
 	return SUCCESS;
 }
@@ -447,6 +453,7 @@ PHP_MINIT_FUNCTION(date)
 	REGISTER_INI_ENTRIES();
 	date_register_classes();
 	register_php_date_symbols(module_number);
+	PHP_MINIT(date_time)(INIT_FUNC_ARGS_PASSTHRU);
 
 	php_date_global_timezone_db = NULL;
 	php_date_global_timezone_db_enabled = 0;
diff --git a/ext/date/php_date.h b/ext/date/php_date.h
index 651cc28225fd..97a49974f1a4 100644
--- a/ext/date/php_date.h
+++ b/ext/date/php_date.h
@@ -115,8 +115,11 @@ ZEND_BEGIN_MODULE_GLOBALS(date)
 	char                    *timezone;
 	HashTable               *tzcache;
 	timelib_error_container *last_errors;
+	zend_object             *duration_cache;
 ZEND_END_MODULE_GLOBALS(date)
 
+PHPAPI ZEND_EXTERN_MODULE_GLOBALS(date)
+
 #define DATEG(v) ZEND_MODULE_GLOBALS_ACCESSOR(date, v)
 
 PHPAPI time_t php_time(void);
diff --git a/ext/date/php_time.c b/ext/date/php_time.c
new file mode 100644
index 000000000000..2c3698f63fbd
--- /dev/null
+++ b/ext/date/php_time.c
@@ -0,0 +1,65 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright © The PHP Group and Contributors.                          |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Derick Rethans <[email protected]>                    |
+   |          Tim Düsterhus <[email protected]>                            |
+   +----------------------------------------------------------------------+
+ */
+
+#include "php.h"
+#include "Zend/zend_exceptions.h"
+
+#include "php_time.h"
+#include "time_arginfo.h"
+
+zend_class_entry *php_date_ce_time_duration;
+zend_class_entry *php_date_ce_time_timeexception;
+
+static zend_object_handlers time_duration_object_handlers;
+
+static zend_object *time_duration_object_create(zend_class_entry *ce)
+{
+	php_date_time_duration *obj = zend_object_alloc(sizeof(*obj), ce);
+
+	zend_object_std_init(&obj->std, ce);
+	object_properties_init(&obj->std, ce);
+
+	timelib_duration_ctor_static(&obj->duration, /* seconds */ 0, /* nanoseconds */ 0, /* negative */ false);
+
+	return &obj->std;
+}
+
+static zend_object *time_duration_object_clone(zend_object *object)
+{
+	const php_date_time_duration *obj = php_date_time_duration_from_obj(object);
+
+	php_date_time_duration *new_obj = php_date_time_duration_from_obj(object->ce->create_object(object->ce));
+
+	new_obj->duration = obj->duration;
+	zend_objects_clone_members(&new_obj->std, &obj->std);
+
+	return &new_obj->std;
+}
+
+PHP_MINIT_FUNCTION(date_time)
+{
+	/* Time\TimeException */
+	php_date_ce_time_timeexception = register_class_Time_TimeException(zend_ce_exception);
+
+	/* Time\Duration */
+	memcpy(&time_duration_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+	time_duration_object_handlers.offset = offsetof(php_date_time_duration, std);
+	time_duration_object_handlers.clone_obj = time_duration_object_clone;
+	php_date_ce_time_duration = register_class_Time_Duration();
+	php_date_ce_time_duration->create_object = time_duration_object_create;
+	php_date_ce_time_duration->default_object_handlers = &time_duration_object_handlers;
+
+	return SUCCESS;
+}
diff --git a/ext/date/php_time.h b/ext/date/php_time.h
new file mode 100644
index 000000000000..fad711b519e7
--- /dev/null
+++ b/ext/date/php_time.h
@@ -0,0 +1,48 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright © The PHP Group and Contributors.                          |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Derick Rethans <[email protected]>                    |
+   |          Tim Düsterhus <[email protected]>                            |
+   +----------------------------------------------------------------------+
+*/
+
+#ifndef PHP_DATE_TIME_H
+# define PHP_DATE_TIME_H
+
+# include "php.h"
+# include "lib/timelib.h"
+
+typedef struct php_date_time_duration {
+	timelib_duration  duration;
+	zend_object       std;
+} php_date_time_duration;
+
+# define php_date_time_duration_from_obj(obj) ZEND_CONTAINER_OF(obj, php_date_time_duration, std)
+
+# define Z_DATE_TIME_DURATION_P(zv)  php_date_time_duration_from_obj(Z_OBJ_P((zv)))
+
+# define Z_PARAM_DATE_TIME_DURATION(d) do { \
+		zend_object *__d; \
+		Z_PARAM_OBJ_OF_CLASS(__d, php_date_ce_time_duration); \
+		d = php_date_time_duration_from_obj(__d); \
+	} while (0);
+
+# define Z_PARAM_DATE_TIME_DURATION_OR_NULL(d) do { \
+		zend_object *__d; \
+		Z_PARAM_OBJ_OF_CLASS_OR_NULL(__d, php_date_ce_time_duration); \
+		d = __d ? php_date_time_duration_from_obj(__d) : NULL; \
+	} while (0);
+
+PHPAPI extern zend_class_entry *php_date_ce_time_duration;
+PHPAPI extern zend_class_entry *php_date_ce_time_timeexception;
+
+PHP_MINIT_FUNCTION(date_time);
+
+#endif /* PHP_DATE_TIME_H */
diff --git a/ext/date/tests/time/duration/clone.phpt b/ext/date/tests/time/duration/clone.phpt
new file mode 100644
index 000000000000..06ad23dfe633
--- /dev/null
+++ b/ext/date/tests/time/duration/clone.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Time\Duration: clone
+--FILE--
+<?php
+
+require __DIR__ . '/helper.inc';
+
+function fc(Time\Duration $d): string {
+	return f(clone($d));
+}
+
+echo fc(Time\Duration::fromSeconds(0, 0)), PHP_EOL;
+echo fc(Time\Duration::fromSeconds(0, 1)), PHP_EOL;
+echo fc(Time\Duration::fromSeconds(1, 1)), PHP_EOL;
+echo fc(Time\Duration::fromSeconds(0, 1)->negate()), PHP_EOL;
+echo fc(Time\Duration::fromSeconds(1, 1)->negate()), PHP_EOL;
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.000000001
+         +1.000000001
+         -0.000000001
+         -1.000000001
diff --git a/ext/date/tests/time/duration/helper.inc b/ext/date/tests/time/duration/helper.inc
new file mode 100644
index 000000000000..e95f8a6b5b9d
--- /dev/null
+++ b/ext/date/tests/time/duration/helper.inc
@@ -0,0 +1,32 @@
+<?php
+
+function f(Time\Duration $d, bool $pad = true, bool $plus_sign = true): string {
+    $result = sprintf("%s%d.%09d", ($d->negative ? "-" : ($plus_sign ? "+" : " ")), $d->seconds, $d->nanoseconds);
+    if ($pad) {
+        $result = str_pad($result, 21, pad_type: STR_PAD_LEFT);
+    }
+
+    return $result;
+}
+
+function as_int(Time\Duration $d): int {
+    return ($d->negative ? -1 : 1) * ($d->seconds * 1_000_000_000 + $d->nanoseconds);
+}
+
+function negate(Time\Duration $d): Time\Duration {
+    return $d->negate();
+}
+
+function is_negative(Time\Duration $d): bool {
+    return $d->negative;
+}
+
+/** @return Time\Duration[] */
+function negate_all(
+    array $d /** @param Time\Duration[] */,
+): array {
+    $negated = array_map(negate(...), $d);
+
+    /* Filter out values that were not successfully negated (i.e. 0-values). */
+    return array_filter($negated, is_negative(...));
+}
diff --git a/ext/date/tests/time/duration/methods/absolute.phpt b/ext/date/tests/time/duration/methods/absolute.phpt
new file mode 100644
index 000000000000..86578488ae89
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/absolute.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Time\Duration::absolute()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromSeconds(0, 0)->absolute()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(0, 0)->negate()->absolute()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(1, 0)->absolute()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1, 0)->negate()->absolute()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(0, 1)->absolute()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(0, 1)->negate()->absolute()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(1, 1)->absolute()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1, 1)->negate()->absolute()), PHP_EOL;
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.000000000
+         +1.000000000
+         +1.000000000
+         +0.000000001
+         +0.000000001
+         +1.000000001
+         +1.000000001
diff --git a/ext/date/tests/time/duration/methods/add.phpt b/ext/date/tests/time/duration/methods/add.phpt
new file mode 100644
index 000000000000..825bd45de2fe
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/add.phpt
@@ -0,0 +1,371 @@
+--TEST--
+Time\Duration::add()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(0, 0),
+    Time\Duration::fromSeconds(0, 1),
+    Time\Duration::fromSeconds(0, 2),
+    Time\Duration::fromSeconds(1, 0),
+    Time\Duration::fromSeconds(1, 1),
+    Time\Duration::fromSeconds(1, 2),
+    Time\Duration::fromSeconds(2, 0),
+    Time\Duration::fromSeconds(2, 1),
+    Time\Duration::fromSeconds(2, 2),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+foreach ($durations as $a) {
+    if ($a === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($durations as $b) {
+        if ($b === null) {
+            echo "====", PHP_EOL;
+            continue;
+        }
+
+        $result = $a->add($b);
+        echo f($a, pad: false, plus_sign: false), " + ", f($b, pad: false, plus_sign: false), " = ", f($result, pad: false), PHP_EOL;
+        if (PHP_INT_SIZE != 4 && (as_int($a) + as_int($b) !== as_int($result))) {
+            throw new \Exception('Verification failed');
+        }
+    }
+}
+
+?>
+--EXPECT--
+========
+ 0.000000000 +  0.000000000 = +0.000000000
+ 0.000000000 +  0.000000001 = +0.000000001
+ 0.000000000 +  0.000000002 = +0.000000002
+ 0.000000000 +  1.000000000 = +1.000000000
+ 0.000000000 +  1.000000001 = +1.000000001
+ 0.000000000 +  1.000000002 = +1.000000002
+ 0.000000000 +  2.000000000 = +2.000000000
+ 0.000000000 +  2.000000001 = +2.000000001
+ 0.000000000 +  2.000000002 = +2.000000002
+====
+ 0.000000000 + -0.000000001 = -0.000000001
+ 0.000000000 + -0.000000002 = -0.000000002
+ 0.000000000 + -1.000000000 = -1.000000000
+ 0.000000000 + -1.000000001 = -1.000000001
+ 0.000000000 + -1.000000002 = -1.000000002
+ 0.000000000 + -2.000000000 = -2.000000000
+ 0.000000000 + -2.000000001 = -2.000000001
+ 0.000000000 + -2.000000002 = -2.000000002
+========
+ 0.000000001 +  0.000000000 = +0.000000001
+ 0.000000001 +  0.000000001 = +0.000000002
+ 0.000000001 +  0.000000002 = +0.000000003
+ 0.000000001 +  1.000000000 = +1.000000001
+ 0.000000001 +  1.000000001 = +1.000000002
+ 0.000000001 +  1.000000002 = +1.000000003
+ 0.000000001 +  2.000000000 = +2.000000001
+ 0.000000001 +  2.000000001 = +2.000000002
+ 0.000000001 +  2.000000002 = +2.000000003
+====
+ 0.000000001 + -0.000000001 = +0.000000000
+ 0.000000001 + -0.000000002 = -0.000000001
+ 0.000000001 + -1.000000000 = -0.999999999
+ 0.000000001 + -1.000000001 = -1.000000000
+ 0.000000001 + -1.000000002 = -1.000000001
+ 0.000000001 + -2.000000000 = -1.999999999
+ 0.000000001 + -2.000000001 = -2.000000000
+ 0.000000001 + -2.000000002 = -2.000000001
+========
+ 0.000000002 +  0.000000000 = +0.000000002
+ 0.000000002 +  0.000000001 = +0.000000003
+ 0.000000002 +  0.000000002 = +0.000000004
+ 0.000000002 +  1.000000000 = +1.000000002
+ 0.000000002 +  1.000000001 = +1.000000003
+ 0.000000002 +  1.000000002 = +1.000000004
+ 0.000000002 +  2.000000000 = +2.000000002
+ 0.000000002 +  2.000000001 = +2.000000003
+ 0.000000002 +  2.000000002 = +2.000000004
+====
+ 0.000000002 + -0.000000001 = +0.000000001
+ 0.000000002 + -0.000000002 = +0.000000000
+ 0.000000002 + -1.000000000 = -0.999999998
+ 0.000000002 + -1.000000001 = -0.999999999
+ 0.000000002 + -1.000000002 = -1.000000000
+ 0.000000002 + -2.000000000 = -1.999999998
+ 0.000000002 + -2.000000001 = -1.999999999
+ 0.000000002 + -2.000000002 = -2.000000000
+========
+ 1.000000000 +  0.000000000 = +1.000000000
+ 1.000000000 +  0.000000001 = +1.000000001
+ 1.000000000 +  0.000000002 = +1.000000002
+ 1.000000000 +  1.000000000 = +2.000000000
+ 1.000000000 +  1.000000001 = +2.000000001
+ 1.000000000 +  1.000000002 = +2.000000002
+ 1.000000000 +  2.000000000 = +3.000000000
+ 1.000000000 +  2.000000001 = +3.000000001
+ 1.000000000 +  2.000000002 = +3.000000002
+====
+ 1.000000000 + -0.000000001 = +0.999999999
+ 1.000000000 + -0.000000002 = +0.999999998
+ 1.000000000 + -1.000000000 = +0.000000000
+ 1.000000000 + -1.000000001 = -0.000000001
+ 1.000000000 + -1.000000002 = -0.000000002
+ 1.000000000 + -2.000000000 = -1.000000000
+ 1.000000000 + -2.000000001 = -1.000000001
+ 1.000000000 + -2.000000002 = -1.000000002
+========
+ 1.000000001 +  0.000000000 = +1.000000001
+ 1.000000001 +  0.000000001 = +1.000000002
+ 1.000000001 +  0.000000002 = +1.000000003
+ 1.000000001 +  1.000000000 = +2.000000001
+ 1.000000001 +  1.000000001 = +2.000000002
+ 1.000000001 +  1.000000002 = +2.000000003
+ 1.000000001 +  2.000000000 = +3.000000001
+ 1.000000001 +  2.000000001 = +3.000000002
+ 1.000000001 +  2.000000002 = +3.000000003
+====
+ 1.000000001 + -0.000000001 = +1.000000000
+ 1.000000001 + -0.000000002 = +0.999999999
+ 1.000000001 + -1.000000000 = +0.000000001
+ 1.000000001 + -1.000000001 = +0.000000000
+ 1.000000001 + -1.000000002 = -0.000000001
+ 1.000000001 + -2.000000000 = -0.999999999
+ 1.000000001 + -2.000000001 = -1.000000000
+ 1.000000001 + -2.000000002 = -1.000000001
+========
+ 1.000000002 +  0.000000000 = +1.000000002
+ 1.000000002 +  0.000000001 = +1.000000003
+ 1.000000002 +  0.000000002 = +1.000000004
+ 1.000000002 +  1.000000000 = +2.000000002
+ 1.000000002 +  1.000000001 = +2.000000003
+ 1.000000002 +  1.000000002 = +2.000000004
+ 1.000000002 +  2.000000000 = +3.000000002
+ 1.000000002 +  2.000000001 = +3.000000003
+ 1.000000002 +  2.000000002 = +3.000000004
+====
+ 1.000000002 + -0.000000001 = +1.000000001
+ 1.000000002 + -0.000000002 = +1.000000000
+ 1.000000002 + -1.000000000 = +0.000000002
+ 1.000000002 + -1.000000001 = +0.000000001
+ 1.000000002 + -1.000000002 = +0.000000000
+ 1.000000002 + -2.000000000 = -0.999999998
+ 1.000000002 + -2.000000001 = -0.999999999
+ 1.000000002 + -2.000000002 = -1.000000000
+========
+ 2.000000000 +  0.000000000 = +2.000000000
+ 2.000000000 +  0.000000001 = +2.000000001
+ 2.000000000 +  0.000000002 = +2.000000002
+ 2.000000000 +  1.000000000 = +3.000000000
+ 2.000000000 +  1.000000001 = +3.000000001
+ 2.000000000 +  1.000000002 = +3.000000002
+ 2.000000000 +  2.000000000 = +4.000000000
+ 2.000000000 +  2.000000001 = +4.000000001
+ 2.000000000 +  2.000000002 = +4.000000002
+====
+ 2.000000000 + -0.000000001 = +1.999999999
+ 2.000000000 + -0.000000002 = +1.999999998
+ 2.000000000 + -1.000000000 = +1.000000000
+ 2.000000000 + -1.000000001 = +0.999999999
+ 2.000000000 + -1.000000002 = +0.999999998
+ 2.000000000 + -2.000000000 = +0.000000000
+ 2.000000000 + -2.000000001 = -0.000000001
+ 2.000000000 + -2.000000002 = -0.000000002
+========
+ 2.000000001 +  0.000000000 = +2.000000001
+ 2.000000001 +  0.000000001 = +2.000000002
+ 2.000000001 +  0.000000002 = +2.000000003
+ 2.000000001 +  1.000000000 = +3.000000001
+ 2.000000001 +  1.000000001 = +3.000000002
+ 2.000000001 +  1.000000002 = +3.000000003
+ 2.000000001 +  2.000000000 = +4.000000001
+ 2.000000001 +  2.000000001 = +4.000000002
+ 2.000000001 +  2.000000002 = +4.000000003
+====
+ 2.000000001 + -0.000000001 = +2.000000000
+ 2.000000001 + -0.000000002 = +1.999999999
+ 2.000000001 + -1.000000000 = +1.000000001
+ 2.000000001 + -1.000000001 = +1.000000000
+ 2.000000001 + -1.000000002 = +0.999999999
+ 2.000000001 + -2.000000000 = +0.000000001
+ 2.000000001 + -2.000000001 = +0.000000000
+ 2.000000001 + -2.000000002 = -0.000000001
+========
+ 2.000000002 +  0.000000000 = +2.000000002
+ 2.000000002 +  0.000000001 = +2.000000003
+ 2.000000002 +  0.000000002 = +2.000000004
+ 2.000000002 +  1.000000000 = +3.000000002
+ 2.000000002 +  1.000000001 = +3.000000003
+ 2.000000002 +  1.000000002 = +3.000000004
+ 2.000000002 +  2.000000000 = +4.000000002
+ 2.000000002 +  2.000000001 = +4.000000003
+ 2.000000002 +  2.000000002 = +4.000000004
+====
+ 2.000000002 + -0.000000001 = +2.000000001
+ 2.000000002 + -0.000000002 = +2.000000000
+ 2.000000002 + -1.000000000 = +1.000000002
+ 2.000000002 + -1.000000001 = +1.000000001
+ 2.000000002 + -1.000000002 = +1.000000000
+ 2.000000002 + -2.000000000 = +0.000000002
+ 2.000000002 + -2.000000001 = +0.000000001
+ 2.000000002 + -2.000000002 = +0.000000000
+========
+-0.000000001 +  0.000000000 = -0.000000001
+-0.000000001 +  0.000000001 = +0.000000000
+-0.000000001 +  0.000000002 = +0.000000001
+-0.000000001 +  1.000000000 = +0.999999999
+-0.000000001 +  1.000000001 = +1.000000000
+-0.000000001 +  1.000000002 = +1.000000001
+-0.000000001 +  2.000000000 = +1.999999999
+-0.000000001 +  2.000000001 = +2.000000000
+-0.000000001 +  2.000000002 = +2.000000001
+====
+-0.000000001 + -0.000000001 = -0.000000002
+-0.000000001 + -0.000000002 = -0.000000003
+-0.000000001 + -1.000000000 = -1.000000001
+-0.000000001 + -1.000000001 = -1.000000002
+-0.000000001 + -1.000000002 = -1.000000003
+-0.000000001 + -2.000000000 = -2.000000001
+-0.000000001 + -2.000000001 = -2.000000002
+-0.000000001 + -2.000000002 = -2.000000003
+========
+-0.000000002 +  0.000000000 = -0.000000002
+-0.000000002 +  0.000000001 = -0.000000001
+-0.000000002 +  0.000000002 = +0.000000000
+-0.000000002 +  1.000000000 = +0.999999998
+-0.000000002 +  1.000000001 = +0.999999999
+-0.000000002 +  1.000000002 = +1.000000000
+-0.000000002 +  2.000000000 = +1.999999998
+-0.000000002 +  2.000000001 = +1.999999999
+-0.000000002 +  2.000000002 = +2.000000000
+====
+-0.000000002 + -0.000000001 = -0.000000003
+-0.000000002 + -0.000000002 = -0.000000004
+-0.000000002 + -1.000000000 = -1.000000002
+-0.000000002 + -1.000000001 = -1.000000003
+-0.000000002 + -1.000000002 = -1.000000004
+-0.000000002 + -2.000000000 = -2.000000002
+-0.000000002 + -2.000000001 = -2.000000003
+-0.000000002 + -2.000000002 = -2.000000004
+========
+-1.000000000 +  0.000000000 = -1.000000000
+-1.000000000 +  0.000000001 = -0.999999999
+-1.000000000 +  0.000000002 = -0.999999998
+-1.000000000 +  1.000000000 = +0.000000000
+-1.000000000 +  1.000000001 = +0.000000001
+-1.000000000 +  1.000000002 = +0.000000002
+-1.000000000 +  2.000000000 = +1.000000000
+-1.000000000 +  2.000000001 = +1.000000001
+-1.000000000 +  2.000000002 = +1.000000002
+====
+-1.000000000 + -0.000000001 = -1.000000001
+-1.000000000 + -0.000000002 = -1.000000002
+-1.000000000 + -1.000000000 = -2.000000000
+-1.000000000 + -1.000000001 = -2.000000001
+-1.000000000 + -1.000000002 = -2.000000002
+-1.000000000 + -2.000000000 = -3.000000000
+-1.000000000 + -2.000000001 = -3.000000001
+-1.000000000 + -2.000000002 = -3.000000002
+========
+-1.000000001 +  0.000000000 = -1.000000001
+-1.000000001 +  0.000000001 = -1.000000000
+-1.000000001 +  0.000000002 = -0.999999999
+-1.000000001 +  1.000000000 = -0.000000001
+-1.000000001 +  1.000000001 = +0.000000000
+-1.000000001 +  1.000000002 = +0.000000001
+-1.000000001 +  2.000000000 = +0.999999999
+-1.000000001 +  2.000000001 = +1.000000000
+-1.000000001 +  2.000000002 = +1.000000001
+====
+-1.000000001 + -0.000000001 = -1.000000002
+-1.000000001 + -0.000000002 = -1.000000003
+-1.000000001 + -1.000000000 = -2.000000001
+-1.000000001 + -1.000000001 = -2.000000002
+-1.000000001 + -1.000000002 = -2.000000003
+-1.000000001 + -2.000000000 = -3.000000001
+-1.000000001 + -2.000000001 = -3.000000002
+-1.000000001 + -2.000000002 = -3.000000003
+========
+-1.000000002 +  0.000000000 = -1.000000002
+-1.000000002 +  0.000000001 = -1.000000001
+-1.000000002 +  0.000000002 = -1.000000000
+-1.000000002 +  1.000000000 = -0.000000002
+-1.000000002 +  1.000000001 = -0.000000001
+-1.000000002 +  1.000000002 = +0.000000000
+-1.000000002 +  2.000000000 = +0.999999998
+-1.000000002 +  2.000000001 = +0.999999999
+-1.000000002 +  2.000000002 = +1.000000000
+====
+-1.000000002 + -0.000000001 = -1.000000003
+-1.000000002 + -0.000000002 = -1.000000004
+-1.000000002 + -1.000000000 = -2.000000002
+-1.000000002 + -1.000000001 = -2.000000003
+-1.000000002 + -1.000000002 = -2.000000004
+-1.000000002 + -2.000000000 = -3.000000002
+-1.000000002 + -2.000000001 = -3.000000003
+-1.000000002 + -2.000000002 = -3.000000004
+========
+-2.000000000 +  0.000000000 = -2.000000000
+-2.000000000 +  0.000000001 = -1.999999999
+-2.000000000 +  0.000000002 = -1.999999998
+-2.000000000 +  1.000000000 = -1.000000000
+-2.000000000 +  1.000000001 = -0.999999999
+-2.000000000 +  1.000000002 = -0.999999998
+-2.000000000 +  2.000000000 = +0.000000000
+-2.000000000 +  2.000000001 = +0.000000001
+-2.000000000 +  2.000000002 = +0.000000002
+====
+-2.000000000 + -0.000000001 = -2.000000001
+-2.000000000 + -0.000000002 = -2.000000002
+-2.000000000 + -1.000000000 = -3.000000000
+-2.000000000 + -1.000000001 = -3.000000001
+-2.000000000 + -1.000000002 = -3.000000002
+-2.000000000 + -2.000000000 = -4.000000000
+-2.000000000 + -2.000000001 = -4.000000001
+-2.000000000 + -2.000000002 = -4.000000002
+========
+-2.000000001 +  0.000000000 = -2.000000001
+-2.000000001 +  0.000000001 = -2.000000000
+-2.000000001 +  0.000000002 = -1.999999999
+-2.000000001 +  1.000000000 = -1.000000001
+-2.000000001 +  1.000000001 = -1.000000000
+-2.000000001 +  1.000000002 = -0.999999999
+-2.000000001 +  2.000000000 = -0.000000001
+-2.000000001 +  2.000000001 = +0.000000000
+-2.000000001 +  2.000000002 = +0.000000001
+====
+-2.000000001 + -0.000000001 = -2.000000002
+-2.000000001 + -0.000000002 = -2.000000003
+-2.000000001 + -1.000000000 = -3.000000001
+-2.000000001 + -1.000000001 = -3.000000002
+-2.000000001 + -1.000000002 = -3.000000003
+-2.000000001 + -2.000000000 = -4.000000001
+-2.000000001 + -2.000000001 = -4.000000002
+-2.000000001 + -2.000000002 = -4.000000003
+========
+-2.000000002 +  0.000000000 = -2.000000002
+-2.000000002 +  0.000000001 = -2.000000001
+-2.000000002 +  0.000000002 = -2.000000000
+-2.000000002 +  1.000000000 = -1.000000002
+-2.000000002 +  1.000000001 = -1.000000001
+-2.000000002 +  1.000000002 = -1.000000000
+-2.000000002 +  2.000000000 = -0.000000002
+-2.000000002 +  2.000000001 = -0.000000001
+-2.000000002 +  2.000000002 = +0.000000000
+====
+-2.000000002 + -0.000000001 = -2.000000003
+-2.000000002 + -0.000000002 = -2.000000004
+-2.000000002 + -1.000000000 = -3.000000002
+-2.000000002 + -1.000000001 = -3.000000003
+-2.000000002 + -1.000000002 = -3.000000004
+-2.000000002 + -2.000000000 = -4.000000002
+-2.000000002 + -2.000000001 = -4.000000003
+-2.000000002 + -2.000000002 = -4.000000004
diff --git a/ext/date/tests/time/duration/methods/add_32.phpt b/ext/date/tests/time/duration/methods/add_32.phpt
new file mode 100644
index 000000000000..f436d0a63ffa
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/add_32.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::add() (32 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$a = Time\Duration::fromSeconds(2_147_483_647, 999999999);
+$b = Time\Duration::fromSeconds(0, 1);
+
+try {
+    $a->add($b);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
diff --git a/ext/date/tests/time/duration/methods/add_64.phpt b/ext/date/tests/time/duration/methods/add_64.phpt
new file mode 100644
index 000000000000..98448409bf19
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/add_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::add() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$a = Time\Duration::fromSeconds(9_223_372_035, 999999999);
+$b = Time\Duration::fromSeconds(0, 1);
+
+try {
+    $a->add($b);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/compare.phpt b/ext/date/tests/time/duration/methods/compare.phpt
new file mode 100644
index 000000000000..488a39c2fc16
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/compare.phpt
@@ -0,0 +1,369 @@
+--TEST--
+Time\Duration::absolute()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(0, 0),
+    Time\Duration::fromSeconds(0, 1),
+    Time\Duration::fromSeconds(0, 2),
+    Time\Duration::fromSeconds(1, 0),
+    Time\Duration::fromSeconds(1, 1),
+    Time\Duration::fromSeconds(1, 2),
+    Time\Duration::fromSeconds(2, 0),
+    Time\Duration::fromSeconds(2, 1),
+    Time\Duration::fromSeconds(2, 2),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+foreach ($durations as $a) {
+    if ($a === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($durations as $b) {
+        if ($b === null) {
+            echo "====", PHP_EOL;
+            continue;
+        }
+
+        $result = Time\Duration::compare($a, $b);
+
+        echo f($a, pad: false), " ", ($result === 0 ? '=' : ($result < 0 ? '<' : '>')), " ", f($b, pad: false), PHP_EOL;
+    }
+}
+
+?>
+--EXPECT--
+========
++0.000000000 = +0.000000000
++0.000000000 < +0.000000001
++0.000000000 < +0.000000002
++0.000000000 < +1.000000000
++0.000000000 < +1.000000001
++0.000000000 < +1.000000002
++0.000000000 < +2.000000000
++0.000000000 < +2.000000001
++0.000000000 < +2.000000002
+====
++0.000000000 > -0.000000001
++0.000000000 > -0.000000002
++0.000000000 > -1.000000000
++0.000000000 > -1.000000001
++0.000000000 > -1.000000002
++0.000000000 > -2.000000000
++0.000000000 > -2.000000001
++0.000000000 > -2.000000002
+========
++0.000000001 > +0.000000000
++0.000000001 = +0.000000001
++0.000000001 < +0.000000002
++0.000000001 < +1.000000000
++0.000000001 < +1.000000001
++0.000000001 < +1.000000002
++0.000000001 < +2.000000000
++0.000000001 < +2.000000001
++0.000000001 < +2.000000002
+====
++0.000000001 > -0.000000001
++0.000000001 > -0.000000002
++0.000000001 > -1.000000000
++0.000000001 > -1.000000001
++0.000000001 > -1.000000002
++0.000000001 > -2.000000000
++0.000000001 > -2.000000001
++0.000000001 > -2.000000002
+========
++0.000000002 > +0.000000000
++0.000000002 > +0.000000001
++0.000000002 = +0.000000002
++0.000000002 < +1.000000000
++0.000000002 < +1.000000001
++0.000000002 < +1.000000002
++0.000000002 < +2.000000000
++0.000000002 < +2.000000001
++0.000000002 < +2.000000002
+====
++0.000000002 > -0.000000001
++0.000000002 > -0.000000002
++0.000000002 > -1.000000000
++0.000000002 > -1.000000001
++0.000000002 > -1.000000002
++0.000000002 > -2.000000000
++0.000000002 > -2.000000001
++0.000000002 > -2.000000002
+========
++1.000000000 > +0.000000000
++1.000000000 > +0.000000001
++1.000000000 > +0.000000002
++1.000000000 = +1.000000000
++1.000000000 < +1.000000001
++1.000000000 < +1.000000002
++1.000000000 < +2.000000000
++1.000000000 < +2.000000001
++1.000000000 < +2.000000002
+====
++1.000000000 > -0.000000001
++1.000000000 > -0.000000002
++1.000000000 > -1.000000000
++1.000000000 > -1.000000001
++1.000000000 > -1.000000002
++1.000000000 > -2.000000000
++1.000000000 > -2.000000001
++1.000000000 > -2.000000002
+========
++1.000000001 > +0.000000000
++1.000000001 > +0.000000001
++1.000000001 > +0.000000002
++1.000000001 > +1.000000000
++1.000000001 = +1.000000001
++1.000000001 < +1.000000002
++1.000000001 < +2.000000000
++1.000000001 < +2.000000001
++1.000000001 < +2.000000002
+====
++1.000000001 > -0.000000001
++1.000000001 > -0.000000002
++1.000000001 > -1.000000000
++1.000000001 > -1.000000001
++1.000000001 > -1.000000002
++1.000000001 > -2.000000000
++1.000000001 > -2.000000001
++1.000000001 > -2.000000002
+========
++1.000000002 > +0.000000000
++1.000000002 > +0.000000001
++1.000000002 > +0.000000002
++1.000000002 > +1.000000000
++1.000000002 > +1.000000001
++1.000000002 = +1.000000002
++1.000000002 < +2.000000000
++1.000000002 < +2.000000001
++1.000000002 < +2.000000002
+====
++1.000000002 > -0.000000001
++1.000000002 > -0.000000002
++1.000000002 > -1.000000000
++1.000000002 > -1.000000001
++1.000000002 > -1.000000002
++1.000000002 > -2.000000000
++1.000000002 > -2.000000001
++1.000000002 > -2.000000002
+========
++2.000000000 > +0.000000000
++2.000000000 > +0.000000001
++2.000000000 > +0.000000002
++2.000000000 > +1.000000000
++2.000000000 > +1.000000001
++2.000000000 > +1.000000002
++2.000000000 = +2.000000000
++2.000000000 < +2.000000001
++2.000000000 < +2.000000002
+====
++2.000000000 > -0.000000001
++2.000000000 > -0.000000002
++2.000000000 > -1.000000000
++2.000000000 > -1.000000001
++2.000000000 > -1.000000002
++2.000000000 > -2.000000000
++2.000000000 > -2.000000001
++2.000000000 > -2.000000002
+========
++2.000000001 > +0.000000000
++2.000000001 > +0.000000001
++2.000000001 > +0.000000002
++2.000000001 > +1.000000000
++2.000000001 > +1.000000001
++2.000000001 > +1.000000002
++2.000000001 > +2.000000000
++2.000000001 = +2.000000001
++2.000000001 < +2.000000002
+====
++2.000000001 > -0.000000001
++2.000000001 > -0.000000002
++2.000000001 > -1.000000000
++2.000000001 > -1.000000001
++2.000000001 > -1.000000002
++2.000000001 > -2.000000000
++2.000000001 > -2.000000001
++2.000000001 > -2.000000002
+========
++2.000000002 > +0.000000000
++2.000000002 > +0.000000001
++2.000000002 > +0.000000002
++2.000000002 > +1.000000000
++2.000000002 > +1.000000001
++2.000000002 > +1.000000002
++2.000000002 > +2.000000000
++2.000000002 > +2.000000001
++2.000000002 = +2.000000002
+====
++2.000000002 > -0.000000001
++2.000000002 > -0.000000002
++2.000000002 > -1.000000000
++2.000000002 > -1.000000001
++2.000000002 > -1.000000002
++2.000000002 > -2.000000000
++2.000000002 > -2.000000001
++2.000000002 > -2.000000002
+========
+-0.000000001 < +0.000000000
+-0.000000001 < +0.000000001
+-0.000000001 < +0.000000002
+-0.000000001 < +1.000000000
+-0.000000001 < +1.000000001
+-0.000000001 < +1.000000002
+-0.000000001 < +2.000000000
+-0.000000001 < +2.000000001
+-0.000000001 < +2.000000002
+====
+-0.000000001 = -0.000000001
+-0.000000001 > -0.000000002
+-0.000000001 > -1.000000000
+-0.000000001 > -1.000000001
+-0.000000001 > -1.000000002
+-0.000000001 > -2.000000000
+-0.000000001 > -2.000000001
+-0.000000001 > -2.000000002
+========
+-0.000000002 < +0.000000000
+-0.000000002 < +0.000000001
+-0.000000002 < +0.000000002
+-0.000000002 < +1.000000000
+-0.000000002 < +1.000000001
+-0.000000002 < +1.000000002
+-0.000000002 < +2.000000000
+-0.000000002 < +2.000000001
+-0.000000002 < +2.000000002
+====
+-0.000000002 < -0.000000001
+-0.000000002 = -0.000000002
+-0.000000002 > -1.000000000
+-0.000000002 > -1.000000001
+-0.000000002 > -1.000000002
+-0.000000002 > -2.000000000
+-0.000000002 > -2.000000001
+-0.000000002 > -2.000000002
+========
+-1.000000000 < +0.000000000
+-1.000000000 < +0.000000001
+-1.000000000 < +0.000000002
+-1.000000000 < +1.000000000
+-1.000000000 < +1.000000001
+-1.000000000 < +1.000000002
+-1.000000000 < +2.000000000
+-1.000000000 < +2.000000001
+-1.000000000 < +2.000000002
+====
+-1.000000000 < -0.000000001
+-1.000000000 < -0.000000002
+-1.000000000 = -1.000000000
+-1.000000000 > -1.000000001
+-1.000000000 > -1.000000002
+-1.000000000 > -2.000000000
+-1.000000000 > -2.000000001
+-1.000000000 > -2.000000002
+========
+-1.000000001 < +0.000000000
+-1.000000001 < +0.000000001
+-1.000000001 < +0.000000002
+-1.000000001 < +1.000000000
+-1.000000001 < +1.000000001
+-1.000000001 < +1.000000002
+-1.000000001 < +2.000000000
+-1.000000001 < +2.000000001
+-1.000000001 < +2.000000002
+====
+-1.000000001 < -0.000000001
+-1.000000001 < -0.000000002
+-1.000000001 < -1.000000000
+-1.000000001 = -1.000000001
+-1.000000001 > -1.000000002
+-1.000000001 > -2.000000000
+-1.000000001 > -2.000000001
+-1.000000001 > -2.000000002
+========
+-1.000000002 < +0.000000000
+-1.000000002 < +0.000000001
+-1.000000002 < +0.000000002
+-1.000000002 < +1.000000000
+-1.000000002 < +1.000000001
+-1.000000002 < +1.000000002
+-1.000000002 < +2.000000000
+-1.000000002 < +2.000000001
+-1.000000002 < +2.000000002
+====
+-1.000000002 < -0.000000001
+-1.000000002 < -0.000000002
+-1.000000002 < -1.000000000
+-1.000000002 < -1.000000001
+-1.000000002 = -1.000000002
+-1.000000002 > -2.000000000
+-1.000000002 > -2.000000001
+-1.000000002 > -2.000000002
+========
+-2.000000000 < +0.000000000
+-2.000000000 < +0.000000001
+-2.000000000 < +0.000000002
+-2.000000000 < +1.000000000
+-2.000000000 < +1.000000001
+-2.000000000 < +1.000000002
+-2.000000000 < +2.000000000
+-2.000000000 < +2.000000001
+-2.000000000 < +2.000000002
+====
+-2.000000000 < -0.000000001
+-2.000000000 < -0.000000002
+-2.000000000 < -1.000000000
+-2.000000000 < -1.000000001
+-2.000000000 < -1.000000002
+-2.000000000 = -2.000000000
+-2.000000000 > -2.000000001
+-2.000000000 > -2.000000002
+========
+-2.000000001 < +0.000000000
+-2.000000001 < +0.000000001
+-2.000000001 < +0.000000002
+-2.000000001 < +1.000000000
+-2.000000001 < +1.000000001
+-2.000000001 < +1.000000002
+-2.000000001 < +2.000000000
+-2.000000001 < +2.000000001
+-2.000000001 < +2.000000002
+====
+-2.000000001 < -0.000000001
+-2.000000001 < -0.000000002
+-2.000000001 < -1.000000000
+-2.000000001 < -1.000000001
+-2.000000001 < -1.000000002
+-2.000000001 < -2.000000000
+-2.000000001 = -2.000000001
+-2.000000001 > -2.000000002
+========
+-2.000000002 < +0.000000000
+-2.000000002 < +0.000000001
+-2.000000002 < +0.000000002
+-2.000000002 < +1.000000000
+-2.000000002 < +1.000000001
+-2.000000002 < +1.000000002
+-2.000000002 < +2.000000000
+-2.000000002 < +2.000000001
+-2.000000002 < +2.000000002
+====
+-2.000000002 < -0.000000001
+-2.000000002 < -0.000000002
+-2.000000002 < -1.000000000
+-2.000000002 < -1.000000001
+-2.000000002 < -1.000000002
+-2.000000002 < -2.000000000
+-2.000000002 < -2.000000001
+-2.000000002 = -2.000000002
diff --git a/ext/date/tests/time/duration/methods/divideBy.phpt b/ext/date/tests/time/duration/methods/divideBy.phpt
new file mode 100644
index 000000000000..440107d33ed4
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/divideBy.phpt
@@ -0,0 +1,133 @@
+--TEST--
+Time\Duration::divideBy()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(0, 0),
+    Time\Duration::fromSeconds(0, 1),
+    Time\Duration::fromSeconds(0, 999999999),
+    Time\Duration::fromSeconds(1, 0),
+    Time\Duration::fromSeconds(2_147_483_647, 999999999),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+$divisors = [
+    1,
+    2,
+    3,
+    4,
+    999999999,
+    1_000000000,
+    2_147483647,
+];
+
+foreach ($durations as $d) {
+    if ($d === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($divisors as $divisor) {
+        $result = $d->divideBy($divisor);
+        echo f($d), " / ", sprintf("%10d", $divisor), " = ", f($result), PHP_EOL;
+
+        if (PHP_INT_SIZE != 4 && (intdiv(as_int($d), $divisor) !== as_int($result))) {
+            throw new \Exception('Verification failed');
+        }
+    }
+}
+
+echo "========", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(1, 0);
+try {
+    $d->divideBy(0);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+========
+         +0.000000000 /          1 =          +0.000000000
+         +0.000000000 /          2 =          +0.000000000
+         +0.000000000 /          3 =          +0.000000000
+         +0.000000000 /          4 =          +0.000000000
+         +0.000000000 /  999999999 =          +0.000000000
+         +0.000000000 / 1000000000 =          +0.000000000
+         +0.000000000 / 2147483647 =          +0.000000000
+========
+         +0.000000001 /          1 =          +0.000000001
+         +0.000000001 /          2 =          +0.000000000
+         +0.000000001 /          3 =          +0.000000000
+         +0.000000001 /          4 =          +0.000000000
+         +0.000000001 /  999999999 =          +0.000000000
+         +0.000000001 / 1000000000 =          +0.000000000
+         +0.000000001 / 2147483647 =          +0.000000000
+========
+         +0.999999999 /          1 =          +0.999999999
+         +0.999999999 /          2 =          +0.499999999
+         +0.999999999 /          3 =          +0.333333333
+         +0.999999999 /          4 =          +0.249999999
+         +0.999999999 /  999999999 =          +0.000000001
+         +0.999999999 / 1000000000 =          +0.000000000
+         +0.999999999 / 2147483647 =          +0.000000000
+========
+         +1.000000000 /          1 =          +1.000000000
+         +1.000000000 /          2 =          +0.500000000
+         +1.000000000 /          3 =          +0.333333333
+         +1.000000000 /          4 =          +0.250000000
+         +1.000000000 /  999999999 =          +0.000000001
+         +1.000000000 / 1000000000 =          +0.000000001
+         +1.000000000 / 2147483647 =          +0.000000000
+========
++2147483647.999999999 /          1 = +2147483647.999999999
++2147483647.999999999 /          2 = +1073741823.999999999
++2147483647.999999999 /          3 =  +715827882.666666666
++2147483647.999999999 /          4 =  +536870911.999999999
++2147483647.999999999 /  999999999 =          +2.147483650
++2147483647.999999999 / 1000000000 =          +2.147483647
++2147483647.999999999 / 2147483647 =          +1.000000000
+========
+         -0.000000001 /          1 =          -0.000000001
+         -0.000000001 /          2 =          +0.000000000
+         -0.000000001 /          3 =          +0.000000000
+         -0.000000001 /          4 =          +0.000000000
+         -0.000000001 /  999999999 =          +0.000000000
+         -0.000000001 / 1000000000 =          +0.000000000
+         -0.000000001 / 2147483647 =          +0.000000000
+========
+         -0.999999999 /          1 =          -0.999999999
+         -0.999999999 /          2 =          -0.499999999
+         -0.999999999 /          3 =          -0.333333333
+         -0.999999999 /          4 =          -0.249999999
+         -0.999999999 /  999999999 =          -0.000000001
+         -0.999999999 / 1000000000 =          +0.000000000
+         -0.999999999 / 2147483647 =          +0.000000000
+========
+         -1.000000000 /          1 =          -1.000000000
+         -1.000000000 /          2 =          -0.500000000
+         -1.000000000 /          3 =          -0.333333333
+         -1.000000000 /          4 =          -0.250000000
+         -1.000000000 /  999999999 =          -0.000000001
+         -1.000000000 / 1000000000 =          -0.000000001
+         -1.000000000 / 2147483647 =          +0.000000000
+========
+-2147483647.999999999 /          1 = -2147483647.999999999
+-2147483647.999999999 /          2 = -1073741823.999999999
+-2147483647.999999999 /          3 =  -715827882.666666666
+-2147483647.999999999 /          4 =  -536870911.999999999
+-2147483647.999999999 /  999999999 =          -2.147483650
+-2147483647.999999999 / 1000000000 =          -2.147483647
+-2147483647.999999999 / 2147483647 =          -1.000000000
+========
+DivisionByZeroError: Division by zero
diff --git a/ext/date/tests/time/duration/methods/divideBy_64.phpt b/ext/date/tests/time/duration/methods/divideBy_64.phpt
new file mode 100644
index 000000000000..21c54538f40b
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/divideBy_64.phpt
@@ -0,0 +1,78 @@
+--TEST--
+Time\Duration::divideBy() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(2_147_483_648, 0),
+    Time\Duration::fromSeconds(9_223_372_035, 999999999),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+$divisors = [
+    1,
+    2,
+    3,
+    4,
+    999999999,
+    1_000000000,
+    9_223_372_035_999999999,
+];
+
+foreach ($durations as $d) {
+    if ($d === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($divisors as $factor) {
+        echo f($d), " / ", sprintf("%19d", $factor), " = ", f($d->divideBy($factor)), PHP_EOL;
+    }
+}
+
+?>
+--EXPECT--
+========
++2147483648.000000000 /                   1 = +2147483648.000000000
++2147483648.000000000 /                   2 = +1073741824.000000000
++2147483648.000000000 /                   3 =  +715827882.666666666
++2147483648.000000000 /                   4 =  +536870912.000000000
++2147483648.000000000 /           999999999 =          +2.147483650
++2147483648.000000000 /          1000000000 =          +2.147483648
++2147483648.000000000 / 9223372035999999999 =          +0.000000000
+========
++9223372035.999999999 /                   1 = +9223372035.999999999
++9223372035.999999999 /                   2 = +4611686017.999999999
++9223372035.999999999 /                   3 = +3074457345.333333333
++9223372035.999999999 /                   4 = +2305843008.999999999
++9223372035.999999999 /           999999999 =          +9.223372045
++9223372035.999999999 /          1000000000 =          +9.223372035
++9223372035.999999999 / 9223372035999999999 =          +0.000000001
+========
+-2147483648.000000000 /                   1 = -2147483648.000000000
+-2147483648.000000000 /                   2 = -1073741824.000000000
+-2147483648.000000000 /                   3 =  -715827882.666666666
+-2147483648.000000000 /                   4 =  -536870912.000000000
+-2147483648.000000000 /           999999999 =          -2.147483650
+-2147483648.000000000 /          1000000000 =          -2.147483648
+-2147483648.000000000 / 9223372035999999999 =          +0.000000000
+========
+-9223372035.999999999 /                   1 = -9223372035.999999999
+-9223372035.999999999 /                   2 = -4611686017.999999999
+-9223372035.999999999 /                   3 = -3074457345.333333333
+-9223372035.999999999 /                   4 = -2305843008.999999999
+-9223372035.999999999 /           999999999 =          -9.223372045
+-9223372035.999999999 /          1000000000 =          -9.223372035
+-9223372035.999999999 / 9223372035999999999 =          -0.000000001
diff --git a/ext/date/tests/time/duration/methods/fromHours.phpt b/ext/date/tests/time/duration/methods/fromHours.phpt
new file mode 100644
index 000000000000..658288036433
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromHours.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromHours()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromHours(0)), PHP_EOL;
+echo f(Time\Duration::fromHours(1)), PHP_EOL;
+echo f(Time\Duration::fromHours(596523)), PHP_EOL;
+
+try {
+    Time\Duration::fromHours(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+      +3600.000000000
++2147482800.000000000
+ValueError: Time\Duration::fromHours(): Argument #1 ($hours) must be greater than or equal to 0
diff --git a/ext/date/tests/time/duration/methods/fromHours_32.phpt b/ext/date/tests/time/duration/methods/fromHours_32.phpt
new file mode 100644
index 000000000000..998ab5d9beaf
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromHours_32.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromHours() (32 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromHours(596523)), PHP_EOL;
+
+try {
+    echo f(Time\Duration::fromHours(596524)), PHP_EOL;
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECTF--
++2147482800.000000000
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
diff --git a/ext/date/tests/time/duration/methods/fromHours_64.phpt b/ext/date/tests/time/duration/methods/fromHours_64.phpt
new file mode 100644
index 000000000000..719da0e88e20
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromHours_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromHours() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromHours(2562047)), PHP_EOL;
+
+try {
+    Time\Duration::fromHours(2562048);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223369200.000000000
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromIso8601DurationString.phpt b/ext/date/tests/time/duration/methods/fromIso8601DurationString.phpt
new file mode 100644
index 000000000000..109a1dd1307a
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromIso8601DurationString.phpt
@@ -0,0 +1,74 @@
+--TEST--
+Time\Duration::fromIso8601DurationString()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$specifications = [
+    'PT0S',
+    'PT1S',
+    'PT60S',
+    'PT2147483647S',
+
+    'PT0.1S',
+
+    'PT0M',
+    'PT1M',
+    'PT1M1S',
+    'PT1M60S',
+
+    'PT0H',
+    'PT1H',
+    'PT1H1M',
+    'PT1H1M1S',
+    'PT1H60M',
+    'PT1H60M60S',
+
+    '',
+    'P',
+    'PT',
+    'P1D',
+    'P1W',
+    'P1M',
+    'P1DT0S',
+    '2000-01-01T00:00:00Z',
+    '2000-01-01T00:00:00Z/PT1H',
+];
+
+foreach ($specifications as $specification) {
+    printf("%-25s: ", $specification);
+
+    try {
+        echo f(Time\Duration::fromIso8601DurationString($specification)), PHP_EOL;
+    } catch (Throwable $e) {
+        echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+    }
+}
+
+?>
+--EXPECT--
+PT0S                     :          +0.000000000
+PT1S                     :          +1.000000000
+PT60S                    :         +60.000000000
+PT2147483647S            : +2147483647.000000000
+PT0.1S                   : Time\TimeException: The ISO 8601 duration string could not be parsed
+PT0M                     :          +0.000000000
+PT1M                     :         +60.000000000
+PT1M1S                   :         +61.000000000
+PT1M60S                  :        +120.000000000
+PT0H                     :          +0.000000000
+PT1H                     :       +3600.000000000
+PT1H1M                   :       +3660.000000000
+PT1H1M1S                 :       +3661.000000000
+PT1H60M                  :       +7200.000000000
+PT1H60M60S               :       +7260.000000000
+                         : Time\TimeException: The ISO 8601 duration string could not be parsed
+P                        : Time\TimeException: The ISO 8601 duration string could not be parsed
+PT                       : Time\TimeException: The ISO 8601 duration string could not be parsed
+P1D                      : Time\TimeException: The ISO 8601 duration string may only contain the time (T) aspect
+P1W                      : Time\TimeException: The ISO 8601 duration string may only contain the time (T) aspect
+P1M                      : Time\TimeException: The ISO 8601 duration string may only contain the time (T) aspect
+P1DT0S                   : Time\TimeException: The ISO 8601 duration string may only contain the time (T) aspect
+2000-01-01T00:00:00Z     : Time\TimeException: The ISO 8601 duration string is missing the period (P) aspect
+2000-01-01T00:00:00Z/PT1H: Time\TimeException: The ISO 8601 duration string may only contain the period (P) aspect
diff --git a/ext/date/tests/time/duration/methods/fromIso8601DurationString_64.phpt b/ext/date/tests/time/duration/methods/fromIso8601DurationString_64.phpt
new file mode 100644
index 000000000000..04ee27db4660
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromIso8601DurationString_64.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Time\Duration::fromIso8601DurationString() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$specifications = [
+    'PT2147483648S',
+    'PT9223372035S',
+    'PT9223372036S',
+];
+
+foreach ($specifications as $specification) {
+    printf("%-25s: ", $specification);
+
+    try {
+        echo f(Time\Duration::fromIso8601DurationString($specification)), PHP_EOL;
+    } catch (Throwable $e) {
+        echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+    }
+}
+
+?>
+--EXPECT--
+PT2147483648S            : +2147483648.000000000
+PT9223372035S            : +9223372035.000000000
+PT9223372036S            : Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromMicroseconds.phpt b/ext/date/tests/time/duration/methods/fromMicroseconds.phpt
new file mode 100644
index 000000000000..aef4092b9094
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMicroseconds.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Time\Duration::fromMicroseconds()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMicroseconds(0)), PHP_EOL;
+echo f(Time\Duration::fromMicroseconds(1)), PHP_EOL;
+echo f(Time\Duration::fromMicroseconds(999999)), PHP_EOL;
+echo f(Time\Duration::fromMicroseconds(1_000000)), PHP_EOL;
+echo f(Time\Duration::fromMicroseconds(2147_483647)), PHP_EOL;
+
+try {
+    Time\Duration::fromMicroseconds(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.000001000
+         +0.999999000
+         +1.000000000
+      +2147.483647000
+ValueError: Time\Duration::fromMicroseconds(): Argument #1 ($microseconds) must be greater than or equal to 0
diff --git a/ext/date/tests/time/duration/methods/fromMicroseconds_64.phpt b/ext/date/tests/time/duration/methods/fromMicroseconds_64.phpt
new file mode 100644
index 000000000000..84f317425a44
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMicroseconds_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromMicroseconds() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMicroseconds(9_223_372_035_999999)), PHP_EOL;
+
+try {
+    Time\Duration::fromMicroseconds(9_223_372_036_000000);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372035.999999000
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromMilliseconds.phpt b/ext/date/tests/time/duration/methods/fromMilliseconds.phpt
new file mode 100644
index 000000000000..d63f20535b4b
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMilliseconds.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Time\Duration::fromMilliseconds()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMilliseconds(0)), PHP_EOL;
+echo f(Time\Duration::fromMilliseconds(1)), PHP_EOL;
+echo f(Time\Duration::fromMilliseconds(999)), PHP_EOL;
+echo f(Time\Duration::fromMilliseconds(1_000)), PHP_EOL;
+echo f(Time\Duration::fromMilliseconds(2147483_647)), PHP_EOL;
+
+try {
+    Time\Duration::fromMilliseconds(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.001000000
+         +0.999000000
+         +1.000000000
+   +2147483.647000000
+ValueError: Time\Duration::fromMilliseconds(): Argument #1 ($milliseconds) must be greater than or equal to 0
diff --git a/ext/date/tests/time/duration/methods/fromMilliseconds_64.phpt b/ext/date/tests/time/duration/methods/fromMilliseconds_64.phpt
new file mode 100644
index 000000000000..4ed7250fb38a
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMilliseconds_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromMilliseconds() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMilliseconds(9_223_372_035_999)), PHP_EOL;
+
+try {
+    Time\Duration::fromMilliseconds(9_223_372_036_000);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372035.999000000
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromMinutes.phpt b/ext/date/tests/time/duration/methods/fromMinutes.phpt
new file mode 100644
index 000000000000..7674ea95f54c
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMinutes.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromMinutes()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMinutes(0)), PHP_EOL;
+echo f(Time\Duration::fromMinutes(1)), PHP_EOL;
+echo f(Time\Duration::fromMinutes(35791394)), PHP_EOL;
+
+try {
+    Time\Duration::fromMinutes(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+        +60.000000000
++2147483640.000000000
+ValueError: Time\Duration::fromMinutes(): Argument #1 ($minutes) must be greater than or equal to 0
diff --git a/ext/date/tests/time/duration/methods/fromMinutes_32.phpt b/ext/date/tests/time/duration/methods/fromMinutes_32.phpt
new file mode 100644
index 000000000000..705823165df6
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMinutes_32.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromMinutes() (32 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMinutes(35791394)), PHP_EOL;
+
+try {
+    echo f(Time\Duration::fromMinutes(35791395)), PHP_EOL;
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECTF--
++2147483640.000000000
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
diff --git a/ext/date/tests/time/duration/methods/fromMinutes_64.phpt b/ext/date/tests/time/duration/methods/fromMinutes_64.phpt
new file mode 100644
index 000000000000..4b3eaf4ad26c
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromMinutes_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromMinutes() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromMinutes(153722867)), PHP_EOL;
+
+try {
+    Time\Duration::fromMinutes(153722868);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372020.000000000
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromNanoseconds.phpt b/ext/date/tests/time/duration/methods/fromNanoseconds.phpt
new file mode 100644
index 000000000000..7b425d2eb562
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromNanoseconds.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Time\Duration::fromNanoseconds()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromNanoseconds(0)), PHP_EOL;
+echo f(Time\Duration::fromNanoseconds(1)), PHP_EOL;
+echo f(Time\Duration::fromNanoseconds(999999999)), PHP_EOL;
+echo f(Time\Duration::fromNanoseconds(1_000000000)), PHP_EOL;
+echo f(Time\Duration::fromNanoseconds(2_147483647)), PHP_EOL;
+
+try {
+    Time\Duration::fromNanoseconds(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.000000001
+         +0.999999999
+         +1.000000000
+         +2.147483647
+ValueError: Time\Duration::fromNanoseconds(): Argument #1 ($nanoseconds) must be greater than or equal to 0
diff --git a/ext/date/tests/time/duration/methods/fromNanoseconds_64.phpt b/ext/date/tests/time/duration/methods/fromNanoseconds_64.phpt
new file mode 100644
index 000000000000..e68adc28e4a3
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromNanoseconds_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::fromNanoseconds() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromNanoseconds(9_223_372_035_999999999)), PHP_EOL;
+
+try {
+    Time\Duration::fromNanoseconds(9_223_372_036_000000000);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372035.999999999
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/fromSeconds.phpt b/ext/date/tests/time/duration/methods/fromSeconds.phpt
new file mode 100644
index 000000000000..8cd26ce4c725
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromSeconds.phpt
@@ -0,0 +1,46 @@
+--TEST--
+Time\Duration::fromSeconds()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromSeconds(0)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(2147483647)), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(0, 0)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(0, 1)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1, 1)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(2147483647, 999999999)), PHP_EOL;
+
+try {
+    Time\Duration::fromSeconds(-1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+    Time\Duration::fromSeconds(0, -1);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+    Time\Duration::fromSeconds(0, 1000000000);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+         +0.000000000
+         +1.000000000
++2147483647.000000000
+         +0.000000000
+         +0.000000001
+         +1.000000001
++2147483647.999999999
+ValueError: Time\Duration::fromSeconds(): Argument #1 ($seconds) must be greater than or equal to 0
+ValueError: Time\Duration::fromSeconds(): Argument #2 ($nanoseconds) must be greater than or equal to 0
+ValueError: Time\Duration::fromSeconds(): Argument #2 ($nanoseconds) must be less than 1_000_000_000
diff --git a/ext/date/tests/time/duration/methods/fromSeconds_64.phpt b/ext/date/tests/time/duration/methods/fromSeconds_64.phpt
new file mode 100644
index 000000000000..2deed85ec0e8
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/fromSeconds_64.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Time\Duration::fromSeconds() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromSeconds(9_223_372_035)), PHP_EOL;
+echo f(Time\Duration::fromSeconds(9_223_372_035, 999999999)), PHP_EOL;
+
+try {
+    Time\Duration::fromSeconds(9_223_372_036);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372035.000000000
++9223372035.999999999
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/multiplyBy.phpt b/ext/date/tests/time/duration/methods/multiplyBy.phpt
new file mode 100644
index 000000000000..70b374d28c2b
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/multiplyBy.phpt
@@ -0,0 +1,206 @@
+--TEST--
+Time\Duration::multiplyBy()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(0, 0),
+    Time\Duration::fromSeconds(0, 1),
+    Time\Duration::fromSeconds(0, 2),
+    Time\Duration::fromSeconds(1, 0),
+    Time\Duration::fromSeconds(1, 1),
+    Time\Duration::fromSeconds(1, 2),
+    Time\Duration::fromSeconds(2, 0),
+    Time\Duration::fromSeconds(2, 1),
+    Time\Duration::fromSeconds(2, 2),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+$factors = [
+    0,
+    1,
+    2,
+    3,
+    4,
+    999999999,
+    1_000000000,
+];
+
+foreach ($durations as $d) {
+    if ($d === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($factors as $factor) {
+        $result = $d->multiplyBy($factor);
+        echo f($d), " * ", sprintf("%10d", $factor), " = ", f($result), PHP_EOL;
+
+        if (PHP_INT_SIZE != 4 && (as_int($d) * $factor !== as_int($result))) {
+            throw new \Exception('Verification failed');
+        }
+    }
+}
+
+echo "========", PHP_EOL;
+$d = Time\Duration::fromSeconds(0, 1);
+$factor = 2_147_483_647;
+$result = $d->multiplyBy($factor);
+echo f($d), " * ", sprintf("%10d", $factor), " = ", f($result), PHP_EOL;
+
+echo "========", PHP_EOL;
+$d = Time\Duration::fromSeconds(1, 0);
+$factor = 2_147_483_647;
+$result = $d->multiplyBy($factor);
+echo f($d), " * ", sprintf("%10d", $factor), " = ", f($result), PHP_EOL;
+
+?>
+--EXPECT--
+========
+         +0.000000000 *          0 =          +0.000000000
+         +0.000000000 *          1 =          +0.000000000
+         +0.000000000 *          2 =          +0.000000000
+         +0.000000000 *          3 =          +0.000000000
+         +0.000000000 *          4 =          +0.000000000
+         +0.000000000 *  999999999 =          +0.000000000
+         +0.000000000 * 1000000000 =          +0.000000000
+========
+         +0.000000001 *          0 =          +0.000000000
+         +0.000000001 *          1 =          +0.000000001
+         +0.000000001 *          2 =          +0.000000002
+         +0.000000001 *          3 =          +0.000000003
+         +0.000000001 *          4 =          +0.000000004
+         +0.000000001 *  999999999 =          +0.999999999
+         +0.000000001 * 1000000000 =          +1.000000000
+========
+         +0.000000002 *          0 =          +0.000000000
+         +0.000000002 *          1 =          +0.000000002
+         +0.000000002 *          2 =          +0.000000004
+         +0.000000002 *          3 =          +0.000000006
+         +0.000000002 *          4 =          +0.000000008
+         +0.000000002 *  999999999 =          +1.999999998
+         +0.000000002 * 1000000000 =          +2.000000000
+========
+         +1.000000000 *          0 =          +0.000000000
+         +1.000000000 *          1 =          +1.000000000
+         +1.000000000 *          2 =          +2.000000000
+         +1.000000000 *          3 =          +3.000000000
+         +1.000000000 *          4 =          +4.000000000
+         +1.000000000 *  999999999 =  +999999999.000000000
+         +1.000000000 * 1000000000 = +1000000000.000000000
+========
+         +1.000000001 *          0 =          +0.000000000
+         +1.000000001 *          1 =          +1.000000001
+         +1.000000001 *          2 =          +2.000000002
+         +1.000000001 *          3 =          +3.000000003
+         +1.000000001 *          4 =          +4.000000004
+         +1.000000001 *  999999999 =  +999999999.999999999
+         +1.000000001 * 1000000000 = +1000000001.000000000
+========
+         +1.000000002 *          0 =          +0.000000000
+         +1.000000002 *          1 =          +1.000000002
+         +1.000000002 *          2 =          +2.000000004
+         +1.000000002 *          3 =          +3.000000006
+         +1.000000002 *          4 =          +4.000000008
+         +1.000000002 *  999999999 = +1000000000.999999998
+         +1.000000002 * 1000000000 = +1000000002.000000000
+========
+         +2.000000000 *          0 =          +0.000000000
+         +2.000000000 *          1 =          +2.000000000
+         +2.000000000 *          2 =          +4.000000000
+         +2.000000000 *          3 =          +6.000000000
+         +2.000000000 *          4 =          +8.000000000
+         +2.000000000 *  999999999 = +1999999998.000000000
+         +2.000000000 * 1000000000 = +2000000000.000000000
+========
+         +2.000000001 *          0 =          +0.000000000
+         +2.000000001 *          1 =          +2.000000001
+         +2.000000001 *          2 =          +4.000000002
+         +2.000000001 *          3 =          +6.000000003
+         +2.000000001 *          4 =          +8.000000004
+         +2.000000001 *  999999999 = +1999999998.999999999
+         +2.000000001 * 1000000000 = +2000000001.000000000
+========
+         +2.000000002 *          0 =          +0.000000000
+         +2.000000002 *          1 =          +2.000000002
+         +2.000000002 *          2 =          +4.000000004
+         +2.000000002 *          3 =          +6.000000006
+         +2.000000002 *          4 =          +8.000000008
+         +2.000000002 *  999999999 = +1999999999.999999998
+         +2.000000002 * 1000000000 = +2000000002.000000000
+========
+         -0.000000001 *          0 =          +0.000000000
+         -0.000000001 *          1 =          -0.000000001
+         -0.000000001 *          2 =          -0.000000002
+         -0.000000001 *          3 =          -0.000000003
+         -0.000000001 *          4 =          -0.000000004
+         -0.000000001 *  999999999 =          -0.999999999
+         -0.000000001 * 1000000000 =          -1.000000000
+========
+         -0.000000002 *          0 =          +0.000000000
+         -0.000000002 *          1 =          -0.000000002
+         -0.000000002 *          2 =          -0.000000004
+         -0.000000002 *          3 =          -0.000000006
+         -0.000000002 *          4 =          -0.000000008
+         -0.000000002 *  999999999 =          -1.999999998
+         -0.000000002 * 1000000000 =          -2.000000000
+========
+         -1.000000000 *          0 =          +0.000000000
+         -1.000000000 *          1 =          -1.000000000
+         -1.000000000 *          2 =          -2.000000000
+         -1.000000000 *          3 =          -3.000000000
+         -1.000000000 *          4 =          -4.000000000
+         -1.000000000 *  999999999 =  -999999999.000000000
+         -1.000000000 * 1000000000 = -1000000000.000000000
+========
+         -1.000000001 *          0 =          +0.000000000
+         -1.000000001 *          1 =          -1.000000001
+         -1.000000001 *          2 =          -2.000000002
+         -1.000000001 *          3 =          -3.000000003
+         -1.000000001 *          4 =          -4.000000004
+         -1.000000001 *  999999999 =  -999999999.999999999
+         -1.000000001 * 1000000000 = -1000000001.000000000
+========
+         -1.000000002 *          0 =          +0.000000000
+         -1.000000002 *          1 =          -1.000000002
+         -1.000000002 *          2 =          -2.000000004
+         -1.000000002 *          3 =          -3.000000006
+         -1.000000002 *          4 =          -4.000000008
+         -1.000000002 *  999999999 = -1000000000.999999998
+         -1.000000002 * 1000000000 = -1000000002.000000000
+========
+         -2.000000000 *          0 =          +0.000000000
+         -2.000000000 *          1 =          -2.000000000
+         -2.000000000 *          2 =          -4.000000000
+         -2.000000000 *          3 =          -6.000000000
+         -2.000000000 *          4 =          -8.000000000
+         -2.000000000 *  999999999 = -1999999998.000000000
+         -2.000000000 * 1000000000 = -2000000000.000000000
+========
+         -2.000000001 *          0 =          +0.000000000
+         -2.000000001 *          1 =          -2.000000001
+         -2.000000001 *          2 =          -4.000000002
+         -2.000000001 *          3 =          -6.000000003
+         -2.000000001 *          4 =          -8.000000004
+         -2.000000001 *  999999999 = -1999999998.999999999
+         -2.000000001 * 1000000000 = -2000000001.000000000
+========
+         -2.000000002 *          0 =          +0.000000000
+         -2.000000002 *          1 =          -2.000000002
+         -2.000000002 *          2 =          -4.000000004
+         -2.000000002 *          3 =          -6.000000006
+         -2.000000002 *          4 =          -8.000000008
+         -2.000000002 *  999999999 = -1999999999.999999998
+         -2.000000002 * 1000000000 = -2000000002.000000000
+========
+         +0.000000001 * 2147483647 =          +2.147483647
+========
+         +1.000000000 * 2147483647 = +2147483647.000000000
diff --git a/ext/date/tests/time/duration/methods/multiplyBy_32.phpt b/ext/date/tests/time/duration/methods/multiplyBy_32.phpt
new file mode 100644
index 000000000000..7a1cbcb56065
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/multiplyBy_32.phpt
@@ -0,0 +1,55 @@
+--TEST--
+Time\Duration::multiplyBy() (32 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$d = Time\Duration::fromSeconds(2_147_483_647, 999999999);
+
+echo f($d->multiplyBy(1)), PHP_EOL;
+
+try {
+    $d->multiplyBy(2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(1_073_741_823, 999999999);
+
+echo f($d->multiplyBy(2)), PHP_EOL;
+
+try {
+    $d->multiplyBy(3);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(715_827_882, 666666666);
+
+echo f($d->multiplyBy(3)), PHP_EOL;
+
+try {
+    $d->multiplyBy(4);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++2147483647.999999999
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
+====
++2147483647.999999998
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
+====
++2147483647.999999998
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
diff --git a/ext/date/tests/time/duration/methods/multiplyBy_64.phpt b/ext/date/tests/time/duration/methods/multiplyBy_64.phpt
new file mode 100644
index 000000000000..04b220fe2dbc
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/multiplyBy_64.phpt
@@ -0,0 +1,70 @@
+--TEST--
+Time\Duration::multiplyBy() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$d = Time\Duration::fromSeconds(9_223_372_035, 999999999);
+
+echo f($d->multiplyBy(1)), PHP_EOL;
+
+try {
+    $d->multiplyBy(2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(4_611_686_017, 999999999);
+
+echo f($d->multiplyBy(2)), PHP_EOL;
+
+try {
+    $d->multiplyBy(3);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(3_074_457_345, 333333333);
+
+echo f($d->multiplyBy(3)), PHP_EOL;
+
+try {
+    $d->multiplyBy(4);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+$d = Time\Duration::fromSeconds(0, 1);
+
+echo f($d->multiplyBy(9_223_372_035_999999999)), PHP_EOL;
+
+try {
+    $d->multiplyBy(9_223_372_036_000000000);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
++9223372035.999999999
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
+====
++9223372035.999999998
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
+====
++9223372035.999999999
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
+====
++9223372035.999999999
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/methods/negate.phpt b/ext/date/tests/time/duration/methods/negate.phpt
new file mode 100644
index 000000000000..ce156cd20374
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/negate.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Time\Duration::negate()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+echo f(Time\Duration::fromSeconds(0, 0)->negate()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(0, 0)->negate()->negate()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(1, 0)->negate()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1, 0)->negate()->negate()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(0, 1)->negate()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(0, 1)->negate()->negate()), PHP_EOL;
+
+echo f(Time\Duration::fromSeconds(1, 1)->negate()), PHP_EOL;
+echo f(Time\Duration::fromSeconds(1, 1)->negate()->negate()), PHP_EOL;
+
+?>
+--EXPECT--
+         +0.000000000
+         +0.000000000
+         -1.000000000
+         +1.000000000
+         -0.000000001
+         +0.000000001
+         -1.000000001
+         +1.000000001
diff --git a/ext/date/tests/time/duration/methods/sub.phpt b/ext/date/tests/time/duration/methods/sub.phpt
new file mode 100644
index 000000000000..5552a249d9bf
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/sub.phpt
@@ -0,0 +1,371 @@
+--TEST--
+Time\Duration::sub()
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$durations = [
+    Time\Duration::fromSeconds(0, 0),
+    Time\Duration::fromSeconds(0, 1),
+    Time\Duration::fromSeconds(0, 2),
+    Time\Duration::fromSeconds(1, 0),
+    Time\Duration::fromSeconds(1, 1),
+    Time\Duration::fromSeconds(1, 2),
+    Time\Duration::fromSeconds(2, 0),
+    Time\Duration::fromSeconds(2, 1),
+    Time\Duration::fromSeconds(2, 2),
+];
+
+$durations = [
+    ...$durations,
+    null,
+    ...negate_all($durations),
+];
+
+foreach ($durations as $a) {
+    if ($a === null) {
+        continue;
+    }
+
+    echo "========", PHP_EOL;
+
+    foreach ($durations as $b) {
+        if ($b === null) {
+            echo "====", PHP_EOL;
+            continue;
+        }
+
+        $result = $a->sub($b);
+        echo f($a, pad: false, plus_sign: false), " - ", f($b, pad: false, plus_sign: false), " = ", f($result, pad: false), PHP_EOL;
+        if (PHP_INT_SIZE != 4 && (as_int($a) - as_int($b) !== as_int($result))) {
+            throw new \Exception('Verification failed');
+        }
+    }
+}
+
+?>
+--EXPECT--
+========
+ 0.000000000 -  0.000000000 = +0.000000000
+ 0.000000000 -  0.000000001 = -0.000000001
+ 0.000000000 -  0.000000002 = -0.000000002
+ 0.000000000 -  1.000000000 = -1.000000000
+ 0.000000000 -  1.000000001 = -1.000000001
+ 0.000000000 -  1.000000002 = -1.000000002
+ 0.000000000 -  2.000000000 = -2.000000000
+ 0.000000000 -  2.000000001 = -2.000000001
+ 0.000000000 -  2.000000002 = -2.000000002
+====
+ 0.000000000 - -0.000000001 = +0.000000001
+ 0.000000000 - -0.000000002 = +0.000000002
+ 0.000000000 - -1.000000000 = +1.000000000
+ 0.000000000 - -1.000000001 = +1.000000001
+ 0.000000000 - -1.000000002 = +1.000000002
+ 0.000000000 - -2.000000000 = +2.000000000
+ 0.000000000 - -2.000000001 = +2.000000001
+ 0.000000000 - -2.000000002 = +2.000000002
+========
+ 0.000000001 -  0.000000000 = +0.000000001
+ 0.000000001 -  0.000000001 = +0.000000000
+ 0.000000001 -  0.000000002 = -0.000000001
+ 0.000000001 -  1.000000000 = -0.999999999
+ 0.000000001 -  1.000000001 = -1.000000000
+ 0.000000001 -  1.000000002 = -1.000000001
+ 0.000000001 -  2.000000000 = -1.999999999
+ 0.000000001 -  2.000000001 = -2.000000000
+ 0.000000001 -  2.000000002 = -2.000000001
+====
+ 0.000000001 - -0.000000001 = +0.000000002
+ 0.000000001 - -0.000000002 = +0.000000003
+ 0.000000001 - -1.000000000 = +1.000000001
+ 0.000000001 - -1.000000001 = +1.000000002
+ 0.000000001 - -1.000000002 = +1.000000003
+ 0.000000001 - -2.000000000 = +2.000000001
+ 0.000000001 - -2.000000001 = +2.000000002
+ 0.000000001 - -2.000000002 = +2.000000003
+========
+ 0.000000002 -  0.000000000 = +0.000000002
+ 0.000000002 -  0.000000001 = +0.000000001
+ 0.000000002 -  0.000000002 = +0.000000000
+ 0.000000002 -  1.000000000 = -0.999999998
+ 0.000000002 -  1.000000001 = -0.999999999
+ 0.000000002 -  1.000000002 = -1.000000000
+ 0.000000002 -  2.000000000 = -1.999999998
+ 0.000000002 -  2.000000001 = -1.999999999
+ 0.000000002 -  2.000000002 = -2.000000000
+====
+ 0.000000002 - -0.000000001 = +0.000000003
+ 0.000000002 - -0.000000002 = +0.000000004
+ 0.000000002 - -1.000000000 = +1.000000002
+ 0.000000002 - -1.000000001 = +1.000000003
+ 0.000000002 - -1.000000002 = +1.000000004
+ 0.000000002 - -2.000000000 = +2.000000002
+ 0.000000002 - -2.000000001 = +2.000000003
+ 0.000000002 - -2.000000002 = +2.000000004
+========
+ 1.000000000 -  0.000000000 = +1.000000000
+ 1.000000000 -  0.000000001 = +0.999999999
+ 1.000000000 -  0.000000002 = +0.999999998
+ 1.000000000 -  1.000000000 = +0.000000000
+ 1.000000000 -  1.000000001 = -0.000000001
+ 1.000000000 -  1.000000002 = -0.000000002
+ 1.000000000 -  2.000000000 = -1.000000000
+ 1.000000000 -  2.000000001 = -1.000000001
+ 1.000000000 -  2.000000002 = -1.000000002
+====
+ 1.000000000 - -0.000000001 = +1.000000001
+ 1.000000000 - -0.000000002 = +1.000000002
+ 1.000000000 - -1.000000000 = +2.000000000
+ 1.000000000 - -1.000000001 = +2.000000001
+ 1.000000000 - -1.000000002 = +2.000000002
+ 1.000000000 - -2.000000000 = +3.000000000
+ 1.000000000 - -2.000000001 = +3.000000001
+ 1.000000000 - -2.000000002 = +3.000000002
+========
+ 1.000000001 -  0.000000000 = +1.000000001
+ 1.000000001 -  0.000000001 = +1.000000000
+ 1.000000001 -  0.000000002 = +0.999999999
+ 1.000000001 -  1.000000000 = +0.000000001
+ 1.000000001 -  1.000000001 = +0.000000000
+ 1.000000001 -  1.000000002 = -0.000000001
+ 1.000000001 -  2.000000000 = -0.999999999
+ 1.000000001 -  2.000000001 = -1.000000000
+ 1.000000001 -  2.000000002 = -1.000000001
+====
+ 1.000000001 - -0.000000001 = +1.000000002
+ 1.000000001 - -0.000000002 = +1.000000003
+ 1.000000001 - -1.000000000 = +2.000000001
+ 1.000000001 - -1.000000001 = +2.000000002
+ 1.000000001 - -1.000000002 = +2.000000003
+ 1.000000001 - -2.000000000 = +3.000000001
+ 1.000000001 - -2.000000001 = +3.000000002
+ 1.000000001 - -2.000000002 = +3.000000003
+========
+ 1.000000002 -  0.000000000 = +1.000000002
+ 1.000000002 -  0.000000001 = +1.000000001
+ 1.000000002 -  0.000000002 = +1.000000000
+ 1.000000002 -  1.000000000 = +0.000000002
+ 1.000000002 -  1.000000001 = +0.000000001
+ 1.000000002 -  1.000000002 = +0.000000000
+ 1.000000002 -  2.000000000 = -0.999999998
+ 1.000000002 -  2.000000001 = -0.999999999
+ 1.000000002 -  2.000000002 = -1.000000000
+====
+ 1.000000002 - -0.000000001 = +1.000000003
+ 1.000000002 - -0.000000002 = +1.000000004
+ 1.000000002 - -1.000000000 = +2.000000002
+ 1.000000002 - -1.000000001 = +2.000000003
+ 1.000000002 - -1.000000002 = +2.000000004
+ 1.000000002 - -2.000000000 = +3.000000002
+ 1.000000002 - -2.000000001 = +3.000000003
+ 1.000000002 - -2.000000002 = +3.000000004
+========
+ 2.000000000 -  0.000000000 = +2.000000000
+ 2.000000000 -  0.000000001 = +1.999999999
+ 2.000000000 -  0.000000002 = +1.999999998
+ 2.000000000 -  1.000000000 = +1.000000000
+ 2.000000000 -  1.000000001 = +0.999999999
+ 2.000000000 -  1.000000002 = +0.999999998
+ 2.000000000 -  2.000000000 = +0.000000000
+ 2.000000000 -  2.000000001 = -0.000000001
+ 2.000000000 -  2.000000002 = -0.000000002
+====
+ 2.000000000 - -0.000000001 = +2.000000001
+ 2.000000000 - -0.000000002 = +2.000000002
+ 2.000000000 - -1.000000000 = +3.000000000
+ 2.000000000 - -1.000000001 = +3.000000001
+ 2.000000000 - -1.000000002 = +3.000000002
+ 2.000000000 - -2.000000000 = +4.000000000
+ 2.000000000 - -2.000000001 = +4.000000001
+ 2.000000000 - -2.000000002 = +4.000000002
+========
+ 2.000000001 -  0.000000000 = +2.000000001
+ 2.000000001 -  0.000000001 = +2.000000000
+ 2.000000001 -  0.000000002 = +1.999999999
+ 2.000000001 -  1.000000000 = +1.000000001
+ 2.000000001 -  1.000000001 = +1.000000000
+ 2.000000001 -  1.000000002 = +0.999999999
+ 2.000000001 -  2.000000000 = +0.000000001
+ 2.000000001 -  2.000000001 = +0.000000000
+ 2.000000001 -  2.000000002 = -0.000000001
+====
+ 2.000000001 - -0.000000001 = +2.000000002
+ 2.000000001 - -0.000000002 = +2.000000003
+ 2.000000001 - -1.000000000 = +3.000000001
+ 2.000000001 - -1.000000001 = +3.000000002
+ 2.000000001 - -1.000000002 = +3.000000003
+ 2.000000001 - -2.000000000 = +4.000000001
+ 2.000000001 - -2.000000001 = +4.000000002
+ 2.000000001 - -2.000000002 = +4.000000003
+========
+ 2.000000002 -  0.000000000 = +2.000000002
+ 2.000000002 -  0.000000001 = +2.000000001
+ 2.000000002 -  0.000000002 = +2.000000000
+ 2.000000002 -  1.000000000 = +1.000000002
+ 2.000000002 -  1.000000001 = +1.000000001
+ 2.000000002 -  1.000000002 = +1.000000000
+ 2.000000002 -  2.000000000 = +0.000000002
+ 2.000000002 -  2.000000001 = +0.000000001
+ 2.000000002 -  2.000000002 = +0.000000000
+====
+ 2.000000002 - -0.000000001 = +2.000000003
+ 2.000000002 - -0.000000002 = +2.000000004
+ 2.000000002 - -1.000000000 = +3.000000002
+ 2.000000002 - -1.000000001 = +3.000000003
+ 2.000000002 - -1.000000002 = +3.000000004
+ 2.000000002 - -2.000000000 = +4.000000002
+ 2.000000002 - -2.000000001 = +4.000000003
+ 2.000000002 - -2.000000002 = +4.000000004
+========
+-0.000000001 -  0.000000000 = -0.000000001
+-0.000000001 -  0.000000001 = -0.000000002
+-0.000000001 -  0.000000002 = -0.000000003
+-0.000000001 -  1.000000000 = -1.000000001
+-0.000000001 -  1.000000001 = -1.000000002
+-0.000000001 -  1.000000002 = -1.000000003
+-0.000000001 -  2.000000000 = -2.000000001
+-0.000000001 -  2.000000001 = -2.000000002
+-0.000000001 -  2.000000002 = -2.000000003
+====
+-0.000000001 - -0.000000001 = +0.000000000
+-0.000000001 - -0.000000002 = +0.000000001
+-0.000000001 - -1.000000000 = +0.999999999
+-0.000000001 - -1.000000001 = +1.000000000
+-0.000000001 - -1.000000002 = +1.000000001
+-0.000000001 - -2.000000000 = +1.999999999
+-0.000000001 - -2.000000001 = +2.000000000
+-0.000000001 - -2.000000002 = +2.000000001
+========
+-0.000000002 -  0.000000000 = -0.000000002
+-0.000000002 -  0.000000001 = -0.000000003
+-0.000000002 -  0.000000002 = -0.000000004
+-0.000000002 -  1.000000000 = -1.000000002
+-0.000000002 -  1.000000001 = -1.000000003
+-0.000000002 -  1.000000002 = -1.000000004
+-0.000000002 -  2.000000000 = -2.000000002
+-0.000000002 -  2.000000001 = -2.000000003
+-0.000000002 -  2.000000002 = -2.000000004
+====
+-0.000000002 - -0.000000001 = -0.000000001
+-0.000000002 - -0.000000002 = +0.000000000
+-0.000000002 - -1.000000000 = +0.999999998
+-0.000000002 - -1.000000001 = +0.999999999
+-0.000000002 - -1.000000002 = +1.000000000
+-0.000000002 - -2.000000000 = +1.999999998
+-0.000000002 - -2.000000001 = +1.999999999
+-0.000000002 - -2.000000002 = +2.000000000
+========
+-1.000000000 -  0.000000000 = -1.000000000
+-1.000000000 -  0.000000001 = -1.000000001
+-1.000000000 -  0.000000002 = -1.000000002
+-1.000000000 -  1.000000000 = -2.000000000
+-1.000000000 -  1.000000001 = -2.000000001
+-1.000000000 -  1.000000002 = -2.000000002
+-1.000000000 -  2.000000000 = -3.000000000
+-1.000000000 -  2.000000001 = -3.000000001
+-1.000000000 -  2.000000002 = -3.000000002
+====
+-1.000000000 - -0.000000001 = -0.999999999
+-1.000000000 - -0.000000002 = -0.999999998
+-1.000000000 - -1.000000000 = +0.000000000
+-1.000000000 - -1.000000001 = +0.000000001
+-1.000000000 - -1.000000002 = +0.000000002
+-1.000000000 - -2.000000000 = +1.000000000
+-1.000000000 - -2.000000001 = +1.000000001
+-1.000000000 - -2.000000002 = +1.000000002
+========
+-1.000000001 -  0.000000000 = -1.000000001
+-1.000000001 -  0.000000001 = -1.000000002
+-1.000000001 -  0.000000002 = -1.000000003
+-1.000000001 -  1.000000000 = -2.000000001
+-1.000000001 -  1.000000001 = -2.000000002
+-1.000000001 -  1.000000002 = -2.000000003
+-1.000000001 -  2.000000000 = -3.000000001
+-1.000000001 -  2.000000001 = -3.000000002
+-1.000000001 -  2.000000002 = -3.000000003
+====
+-1.000000001 - -0.000000001 = -1.000000000
+-1.000000001 - -0.000000002 = -0.999999999
+-1.000000001 - -1.000000000 = -0.000000001
+-1.000000001 - -1.000000001 = +0.000000000
+-1.000000001 - -1.000000002 = +0.000000001
+-1.000000001 - -2.000000000 = +0.999999999
+-1.000000001 - -2.000000001 = +1.000000000
+-1.000000001 - -2.000000002 = +1.000000001
+========
+-1.000000002 -  0.000000000 = -1.000000002
+-1.000000002 -  0.000000001 = -1.000000003
+-1.000000002 -  0.000000002 = -1.000000004
+-1.000000002 -  1.000000000 = -2.000000002
+-1.000000002 -  1.000000001 = -2.000000003
+-1.000000002 -  1.000000002 = -2.000000004
+-1.000000002 -  2.000000000 = -3.000000002
+-1.000000002 -  2.000000001 = -3.000000003
+-1.000000002 -  2.000000002 = -3.000000004
+====
+-1.000000002 - -0.000000001 = -1.000000001
+-1.000000002 - -0.000000002 = -1.000000000
+-1.000000002 - -1.000000000 = -0.000000002
+-1.000000002 - -1.000000001 = -0.000000001
+-1.000000002 - -1.000000002 = +0.000000000
+-1.000000002 - -2.000000000 = +0.999999998
+-1.000000002 - -2.000000001 = +0.999999999
+-1.000000002 - -2.000000002 = +1.000000000
+========
+-2.000000000 -  0.000000000 = -2.000000000
+-2.000000000 -  0.000000001 = -2.000000001
+-2.000000000 -  0.000000002 = -2.000000002
+-2.000000000 -  1.000000000 = -3.000000000
+-2.000000000 -  1.000000001 = -3.000000001
+-2.000000000 -  1.000000002 = -3.000000002
+-2.000000000 -  2.000000000 = -4.000000000
+-2.000000000 -  2.000000001 = -4.000000001
+-2.000000000 -  2.000000002 = -4.000000002
+====
+-2.000000000 - -0.000000001 = -1.999999999
+-2.000000000 - -0.000000002 = -1.999999998
+-2.000000000 - -1.000000000 = -1.000000000
+-2.000000000 - -1.000000001 = -0.999999999
+-2.000000000 - -1.000000002 = -0.999999998
+-2.000000000 - -2.000000000 = +0.000000000
+-2.000000000 - -2.000000001 = +0.000000001
+-2.000000000 - -2.000000002 = +0.000000002
+========
+-2.000000001 -  0.000000000 = -2.000000001
+-2.000000001 -  0.000000001 = -2.000000002
+-2.000000001 -  0.000000002 = -2.000000003
+-2.000000001 -  1.000000000 = -3.000000001
+-2.000000001 -  1.000000001 = -3.000000002
+-2.000000001 -  1.000000002 = -3.000000003
+-2.000000001 -  2.000000000 = -4.000000001
+-2.000000001 -  2.000000001 = -4.000000002
+-2.000000001 -  2.000000002 = -4.000000003
+====
+-2.000000001 - -0.000000001 = -2.000000000
+-2.000000001 - -0.000000002 = -1.999999999
+-2.000000001 - -1.000000000 = -1.000000001
+-2.000000001 - -1.000000001 = -1.000000000
+-2.000000001 - -1.000000002 = -0.999999999
+-2.000000001 - -2.000000000 = -0.000000001
+-2.000000001 - -2.000000001 = +0.000000000
+-2.000000001 - -2.000000002 = +0.000000001
+========
+-2.000000002 -  0.000000000 = -2.000000002
+-2.000000002 -  0.000000001 = -2.000000003
+-2.000000002 -  0.000000002 = -2.000000004
+-2.000000002 -  1.000000000 = -3.000000002
+-2.000000002 -  1.000000001 = -3.000000003
+-2.000000002 -  1.000000002 = -3.000000004
+-2.000000002 -  2.000000000 = -4.000000002
+-2.000000002 -  2.000000001 = -4.000000003
+-2.000000002 -  2.000000002 = -4.000000004
+====
+-2.000000002 - -0.000000001 = -2.000000001
+-2.000000002 - -0.000000002 = -2.000000000
+-2.000000002 - -1.000000000 = -1.000000002
+-2.000000002 - -1.000000001 = -1.000000001
+-2.000000002 - -1.000000002 = -1.000000000
+-2.000000002 - -2.000000000 = -0.000000002
+-2.000000002 - -2.000000001 = -0.000000001
+-2.000000002 - -2.000000002 = +0.000000000
diff --git a/ext/date/tests/time/duration/methods/sub_32.phpt b/ext/date/tests/time/duration/methods/sub_32.phpt
new file mode 100644
index 000000000000..298b2bfe4953
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/sub_32.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::sub() (32 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$a = Time\Duration::fromSeconds(0, 1)->negate();
+$b = Time\Duration::fromSeconds(2_147_483_647, 999999999);
+
+try {
+    $a->sub($b);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Time\TimeException: The maximum representable range is 2_147_483_647 seconds (roughly 68 years)
diff --git a/ext/date/tests/time/duration/methods/sub_64.phpt b/ext/date/tests/time/duration/methods/sub_64.phpt
new file mode 100644
index 000000000000..c8f8fac503bf
--- /dev/null
+++ b/ext/date/tests/time/duration/methods/sub_64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Time\Duration::sub() (64 bit variation)
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platforms only");
+?>
+--FILE--
+<?php
+
+require __DIR__ . '/../helper.inc';
+
+$a = Time\Duration::fromSeconds(0, 1)->negate();
+$b = Time\Duration::fromSeconds(9_223_372_035, 999999999);
+
+try {
+    $a->sub($b);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Time\TimeException: The maximum representable range is 9_223_372_035 seconds (roughly 292 years)
diff --git a/ext/date/tests/time/duration/new.phpt b/ext/date/tests/time/duration/new.phpt
new file mode 100644
index 000000000000..312752374e73
--- /dev/null
+++ b/ext/date/tests/time/duration/new.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Time\Duration: new
+--FILE--
+<?php
+
+require __DIR__ . '/helper.inc';
+
+try {
+    new Time\Duration();
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+    (new ReflectionClass(Time\Duration::class))->newInstance();
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+try {
+    (new ReflectionClass(Time\Duration::class))->newInstanceWithoutConstructor();
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Error: Call to private Time\Duration::__construct() from global scope
+ReflectionException: Access to non-public constructor of class Time\Duration
+ReflectionException: Class Time\Duration is an internal class marked as final that cannot be instantiated without invoking its constructor
diff --git a/ext/date/tests/time/duration/readonly.phpt b/ext/date/tests/time/duration/readonly.phpt
new file mode 100644
index 000000000000..36b407138fee
--- /dev/null
+++ b/ext/date/tests/time/duration/readonly.phpt
@@ -0,0 +1,114 @@
+--TEST--
+Time\Duration: readonly
+--FILE--
+<?php
+
+require __DIR__ . '/helper.inc';
+
+$d = Time\Duration::fromSeconds(1);
+
+try {
+    $d->seconds = 2;
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setValue($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setRawValue($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setRawValueWithoutLazyInitialization($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+echo "====", PHP_EOL;
+
+/* Recheck after cloning to verify that "modification allowed during cloning" has no lasting effect. */
+
+$d = clone($d);
+
+try {
+    $d->seconds = 2;
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setValue($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setRawValue($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+try {
+    (new ReflectionProperty($d, 'seconds'))->setRawValueWithoutLazyInitialization($d, 2);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo f($d), PHP_EOL;
+
+echo "====", PHP_EOL;
+
+try {
+    clone($d, ['seconds' => 2]);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+echo "====", PHP_EOL;
+
+var_dump((new ReflectionProperty($d, 'seconds'))->isWritable(null, $d));
+
+?>
+--EXPECT--
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+====
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+Error: Cannot modify readonly property Time\Duration::$seconds
+         +1.000000000
+====
+Error: Cannot modify protected(set) readonly property Time\Duration::$seconds from global scope
+====
+bool(false)
diff --git a/ext/date/time.stub.php b/ext/date/time.stub.php
new file mode 100644
index 000000000000..b9d0a01b39d6
--- /dev/null
+++ b/ext/date/time.stub.php
@@ -0,0 +1,80 @@
+<?php
+
+/** @generate-class-entries */
+
+namespace Time {
+    /** @strict-properties */
+    class TimeException extends \Exception { }
+
+    /**
+     * @strict-properties
+     */
+    final readonly class Duration
+    {
+        public readonly int $seconds;
+
+        public readonly int $nanoseconds;
+
+        public readonly bool $negative;
+
+        private function __construct()
+        {
+        }
+
+        public static function fromSeconds(int $seconds, int $nanoseconds = 0): Duration
+        {
+        }
+
+        public static function fromNanoseconds(int $nanoseconds): Duration
+        {
+        }
+
+        public static function fromMicroseconds(int $microseconds): Duration
+        {
+        }
+
+        public static function fromMilliseconds(int $milliseconds): Duration
+        {
+        }
+
+        public static function fromMinutes(int $minutes): Duration
+        {
+        }
+
+        public static function fromHours(int $hours): Duration
+        {
+        }
+
+        public static function fromIso8601DurationString(string $specification): Duration
+        {
+        }
+
+        public function negate(): Duration
+        {
+        }
+
+        public function absolute(): Duration
+        {
+        }
+
+        public function add(Duration $duration): Duration
+        {
+        }
+
+        public function sub(Duration $duration): Duration
+        {
+        }
+
+        public function multiplyBy(int $factor): Duration
+        {
+        }
+
+        public function divideBy(int $divisor): Duration
+        {
+        }
+
+        public static function compare(Duration $a, Duration $b): int
+        {
+        }
+    }
+}
diff --git a/ext/date/time_arginfo.h b/ext/date/time_arginfo.h
new file mode 100644
index 000000000000..b1dd72f4c24a
--- /dev/null
+++ b/ext/date/time_arginfo.h
@@ -0,0 +1,131 @@
+/* This is a generated file, edit time.stub.php instead.
+ * Stub hash: b145db05ac54d90df4a1d5eb139c2d0c5e70401c */
+
+ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Time_Duration___construct, 0, 0, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromSeconds, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)
+	ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, nanoseconds, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromNanoseconds, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, nanoseconds, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromMicroseconds, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, microseconds, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromMilliseconds, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, milliseconds, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromMinutes, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, minutes, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromHours, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, hours, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_fromIso8601DurationString, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, specification, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_negate, 0, 0, Time\\Duration, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Time_Duration_absolute arginfo_class_Time_Duration_negate
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_add, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_OBJ_INFO(0, duration, Time\\Duration, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_Time_Duration_sub arginfo_class_Time_Duration_add
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_multiplyBy, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, factor, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Time_Duration_divideBy, 0, 1, Time\\Duration, 0)
+	ZEND_ARG_TYPE_INFO(0, divisor, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Time_Duration_compare, 0, 2, IS_LONG, 0)
+	ZEND_ARG_OBJ_INFO(0, a, Time\\Duration, 0)
+	ZEND_ARG_OBJ_INFO(0, b, Time\\Duration, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_METHOD(Time_Duration, __construct);
+ZEND_METHOD(Time_Duration, fromSeconds);
+ZEND_METHOD(Time_Duration, fromNanoseconds);
+ZEND_METHOD(Time_Duration, fromMicroseconds);
+ZEND_METHOD(Time_Duration, fromMilliseconds);
+ZEND_METHOD(Time_Duration, fromMinutes);
+ZEND_METHOD(Time_Duration, fromHours);
+ZEND_METHOD(Time_Duration, fromIso8601DurationString);
+ZEND_METHOD(Time_Duration, negate);
+ZEND_METHOD(Time_Duration, absolute);
+ZEND_METHOD(Time_Duration, add);
+ZEND_METHOD(Time_Duration, sub);
+ZEND_METHOD(Time_Duration, multiplyBy);
+ZEND_METHOD(Time_Duration, divideBy);
+ZEND_METHOD(Time_Duration, compare);
+
+static const zend_function_entry class_Time_Duration_methods[] = {
+	ZEND_ME(Time_Duration, __construct, arginfo_class_Time_Duration___construct, ZEND_ACC_PRIVATE)
+	ZEND_ME(Time_Duration, fromSeconds, arginfo_class_Time_Duration_fromSeconds, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromNanoseconds, arginfo_class_Time_Duration_fromNanoseconds, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromMicroseconds, arginfo_class_Time_Duration_fromMicroseconds, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromMilliseconds, arginfo_class_Time_Duration_fromMilliseconds, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromMinutes, arginfo_class_Time_Duration_fromMinutes, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromHours, arginfo_class_Time_Duration_fromHours, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, fromIso8601DurationString, arginfo_class_Time_Duration_fromIso8601DurationString, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_ME(Time_Duration, negate, arginfo_class_Time_Duration_negate, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, absolute, arginfo_class_Time_Duration_absolute, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, add, arginfo_class_Time_Duration_add, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, sub, arginfo_class_Time_Duration_sub, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, multiplyBy, arginfo_class_Time_Duration_multiplyBy, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, divideBy, arginfo_class_Time_Duration_divideBy, ZEND_ACC_PUBLIC)
+	ZEND_ME(Time_Duration, compare, arginfo_class_Time_Duration_compare, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+	ZEND_FE_END
+};
+
+static zend_class_entry *register_class_Time_TimeException(zend_class_entry *class_entry_Exception)
+{
+	zend_class_entry ce, *class_entry;
+
+	INIT_NS_CLASS_ENTRY(ce, "Time", "TimeException", NULL);
+	class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Exception, ZEND_ACC_NO_DYNAMIC_PROPERTIES);
+
+	return class_entry;
+}
+
+static zend_class_entry *register_class_Time_Duration(void)
+{
+	zend_class_entry ce, *class_entry;
+
+	INIT_NS_CLASS_ENTRY(ce, "Time", "Duration", class_Time_Duration_methods);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_READONLY_CLASS);
+
+	zval property_seconds_default_value;
+	ZVAL_UNDEF(&property_seconds_default_value);
+	zend_string *property_seconds_name = zend_string_init("seconds", sizeof("seconds") - 1, true);
+	zend_declare_typed_property(class_entry, property_seconds_name, &property_seconds_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+	zend_string_release_ex(property_seconds_name, true);
+
+	zval property_nanoseconds_default_value;
+	ZVAL_UNDEF(&property_nanoseconds_default_value);
+	zend_string *property_nanoseconds_name = zend_string_init("nanoseconds", sizeof("nanoseconds") - 1, true);
+	zend_declare_typed_property(class_entry, property_nanoseconds_name, &property_nanoseconds_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+	zend_string_release_ex(property_nanoseconds_name, true);
+
+	zval property_negative_default_value;
+	ZVAL_UNDEF(&property_negative_default_value);
+	zend_string *property_negative_name = zend_string_init("negative", sizeof("negative") - 1, true);
+	zend_declare_typed_property(class_entry, property_negative_name, &property_negative_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL));
+	zend_string_release_ex(property_negative_name, true);
+
+	return class_entry;
+}
diff --git a/ext/date/time_duration.c b/ext/date/time_duration.c
new file mode 100644
index 000000000000..d9952a829d2c
--- /dev/null
+++ b/ext/date/time_duration.c
@@ -0,0 +1,417 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright © The PHP Group and Contributors.                          |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Derick Rethans <[email protected]>                    |
+   |          Tim Düsterhus <[email protected]>                            |
+   +----------------------------------------------------------------------+
+ */
+
+#include "php.h"
+#include "Zend/zend_exceptions.h"
+
+#include "php_date.h"
+#include "php_time.h"
+
+#define NANOS_IN_SEC 1000000000
+#define NANOS_IN_MICRO 1000
+#define MICROS_IN_SEC 1000000
+#define NANOS_IN_MILLI 1000000
+#define MILLIS_IN_SEC 1000
+
+ZEND_STATIC_ASSERT(NANOS_IN_MICRO * MICROS_IN_SEC == NANOS_IN_SEC, "");
+ZEND_STATIC_ASSERT(NANOS_IN_MILLI * MILLIS_IN_SEC == NANOS_IN_SEC, "");
+
+#define Z_PARAM_ULONG(l) { \
+		zend_long __l; \
+		Z_PARAM_LONG(__l); \
+		if (__l < 0) { \
+			zend_argument_value_error(_i, "must be greater than or equal to 0"); \
+			_error_code = ZPP_ERROR_FAILURE; \
+			break; \
+		} \
+		l = __l; \
+	}
+
+ZEND_COLD static void throw_out_of_range_exception(void)
+{
+#if SIZEOF_ZEND_LONG != 4
+	zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 9_223_372_035 seconds (roughly 292 years)", 0);
+#else
+	zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 2_147_483_647 seconds (roughly 68 years)", 0);
+#endif
+}
+
+ZEND_COLD static void throw_timelib_error(int error)
+{
+	switch (error) {
+		case TIMELIB_ERROR_SECONDS_OUT_OF_RANGE:
+			throw_out_of_range_exception();
+			break;
+		case TIMELIB_ERROR_DIVISION_BY_ZERO:
+			zend_throw_exception_ex(zend_ce_division_by_zero_error, 0, "Division by zero");
+			break;
+		case TIMELIB_ERROR_ISO8601_DURATION_PARSE_FAILURE:
+		case TIMELIB_ERROR_DURATION_MISSING_PERIOD:
+		case TIMELIB_ERROR_DURATION_ONLY_PERIOD_ALLOWED:
+		case TIMELIB_ERROR_DURATION_DAYS_FOUND:
+			zend_throw_exception(php_date_ce_time_timeexception, timelib_get_error_message(error), 0);
+			break;
+		default:
+			/* This should be unreachable in practice. */
+			zend_throw_exception_ex(php_date_ce_time_timeexception, 0, "Failed to create a Time\\Duration: %s", timelib_get_error_message(error));
+			break;
+	}
+}
+
+static inline php_date_time_duration *create_duration_shell(zval *target)
+{
+	object_init_ex(target, php_date_ce_time_duration);
+
+	return Z_DATE_TIME_DURATION_P(target);
+}
+
+ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
+{
+	if (
+		/* Check if the duration would overflow the $seconds property. */
+		object->duration.seconds > ((uint64_t)ZEND_LONG_MAX)
+		/* This constraint is an explicit part of PHP's API: It is the maximum $seconds
+		 * value that allows storing the entire duration as a single int64_t counting
+		 * nanoseconds, which might be desirable in the future when userland `int` is
+		 * consistently 64 bits.
+		 *
+		 * While it is currently also enforced by timelib, this might change
+		 * in a future version of timelib, thus we also enforce it manually. */
+		|| object->duration.seconds > UINT64_C(9223372035)
+	) {
+		throw_out_of_range_exception();
+		return FAILURE;
+	}
+
+	ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 0)));
+	ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 1)));
+	ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 2)));
+
+	ZVAL_LONG(OBJ_PROP_NUM(&object->std, 0), object->duration.seconds);
+	Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 0)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
+	ZVAL_LONG(OBJ_PROP_NUM(&object->std, 1), object->duration.nanoseconds);
+	Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 1)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
+	ZVAL_BOOL(OBJ_PROP_NUM(&object->std, 2), object->duration.negative);
+	Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 2)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
+
+	return SUCCESS;
+}
+
+ZEND_ATTRIBUTE_NODISCARD static zend_result create_duration(zval *target, zend_ulong seconds, zend_ulong nanoseconds)
+{
+	ZEND_ASSERT(nanoseconds < NANOS_IN_SEC);
+
+	if (EXPECTED(DATEG(duration_cache))) {
+		php_date_time_duration *cached = php_date_time_duration_from_obj(DATEG(duration_cache));
+		ZEND_ASSERT(!cached->duration.negative);
+
+		if (cached->duration.seconds == seconds && cached->duration.nanoseconds == nanoseconds) {
+			ZVAL_OBJ_COPY(target, &cached->std);
+			return SUCCESS;
+		}
+	}
+
+	php_date_time_duration *obj = create_duration_shell(target);
+
+	int error = timelib_duration_ctor_static(&obj->duration, seconds, nanoseconds, /* negative */ false);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		return FAILURE;
+	}
+
+	if (sync_properties(obj) == FAILURE) {
+		return FAILURE;
+	}
+
+	if (DATEG(duration_cache)) {
+		zend_object_release(DATEG(duration_cache));
+	}
+	GC_ADDREF(&obj->std);
+	DATEG(duration_cache) = &obj->std;
+
+	return SUCCESS;
+}
+
+PHP_METHOD(Time_Duration, __construct)
+{
+}
+
+PHP_METHOD(Time_Duration, fromSeconds)
+{
+	zend_ulong seconds;
+	zend_ulong nanoseconds = 0;
+
+	ZEND_PARSE_PARAMETERS_START(1, 2)
+		Z_PARAM_ULONG(seconds);
+		Z_PARAM_OPTIONAL;
+		Z_PARAM_ULONG(nanoseconds);
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (nanoseconds >= NANOS_IN_SEC) {
+		zend_argument_value_error(2, "must be less than 1_000_000_000");
+		RETURN_THROWS();
+	}
+
+	if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromNanoseconds)
+{
+	zend_ulong nanoseconds;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(nanoseconds);
+	ZEND_PARSE_PARAMETERS_END();
+
+	zend_ulong seconds = nanoseconds / NANOS_IN_SEC;
+	nanoseconds %= NANOS_IN_SEC;
+
+	if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromMicroseconds)
+{
+	zend_ulong microseconds;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(microseconds);
+	ZEND_PARSE_PARAMETERS_END();
+
+	zend_ulong seconds = microseconds / MICROS_IN_SEC;
+	zend_ulong nanoseconds = (microseconds % MICROS_IN_SEC) * NANOS_IN_MICRO;
+
+	if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromMilliseconds)
+{
+	zend_ulong milliseconds;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(milliseconds);
+	ZEND_PARSE_PARAMETERS_END();
+
+	zend_ulong seconds = milliseconds / MILLIS_IN_SEC;
+	zend_ulong nanoseconds = (milliseconds % MILLIS_IN_SEC) * NANOS_IN_MILLI;
+
+	if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromMinutes)
+{
+	zend_ulong minutes;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(minutes);
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (minutes > (ZEND_ULONG_MAX / 60)) {
+		throw_out_of_range_exception();
+		RETURN_THROWS();
+	}
+
+	if (create_duration(return_value, minutes * 60, /* nanoseconds */ 0) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromHours)
+{
+	zend_ulong hours;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(hours);
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (hours > (ZEND_ULONG_MAX / 3600)) {
+		throw_out_of_range_exception();
+		RETURN_THROWS();
+	}
+
+	if (create_duration(return_value, hours * 3600, /* nanoseconds */ 0) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, fromIso8601DurationString)
+{
+	zend_string *specification;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_STR(specification);
+	ZEND_PARSE_PARAMETERS_END();
+
+	int error;
+	timelib_duration *d = timelib_duration_create_from_iso8601string(ZSTR_VAL(specification), &error);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+	new->duration = *d;
+	timelib_duration_dtor(d);
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, negate)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+
+	int error = timelib_duration_negate_static(&new->duration, &original->duration);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, absolute)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	if (!original->duration.negative) {
+		RETURN_COPY(ZEND_THIS);
+	}
+
+	if (create_duration(return_value, original->duration.seconds, original->duration.nanoseconds) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, add)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	const php_date_time_duration *additional;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_DATE_TIME_DURATION(additional);
+	ZEND_PARSE_PARAMETERS_END();
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+
+	int error = timelib_duration_add_static(&new->duration, &original->duration, &additional->duration);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, sub)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	const php_date_time_duration *minus;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_DATE_TIME_DURATION(minus);
+	ZEND_PARSE_PARAMETERS_END();
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+
+	int error = timelib_duration_sub_static(&new->duration, &original->duration, &minus->duration);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, multiplyBy)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	zend_ulong factor;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(factor);
+	ZEND_PARSE_PARAMETERS_END();
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+
+	int error = timelib_duration_mul_static(&new->duration, &original->duration, factor);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, divideBy)
+{
+	const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
+
+	zend_ulong divisor;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_ULONG(divisor);
+	ZEND_PARSE_PARAMETERS_END();
+
+	php_date_time_duration *new = create_duration_shell(return_value);
+
+	int error = timelib_duration_div_static(&new->duration, &original->duration, divisor);
+	if (error != TIMELIB_ERROR_NO_ERROR) {
+		throw_timelib_error(error);
+		RETURN_THROWS();
+	}
+
+	if (sync_properties(new) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+PHP_METHOD(Time_Duration, compare)
+{
+	const php_date_time_duration *a;
+	const php_date_time_duration *b;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_DATE_TIME_DURATION(a);
+		Z_PARAM_DATE_TIME_DURATION(b);
+	ZEND_PARSE_PARAMETERS_END();
+
+	RETURN_LONG(timelib_duration_compare(&a->duration, &b->duration));
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.