Commit 1cf46237a56 for php.net

commit 1cf46237a56540c14d84d0f04b245a10d4ee5bc5
Author: Tim Düsterhus <tim@tideways-gmbh.com>
Date:   Fri Aug 7 12:04:40 2026 +0200

    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 <tim@bastelstu.be>

diff --git a/NEWS b/NEWS
index cc4ff914844..890c5d89039 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 25a405e1521..ba4bf182911 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 78cca036d38..327242f128a 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 4853882465e..aab6d3d139b 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 0e24e445064..9f9f0a6159e 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 651cc28225f..97a49974f1a 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 00000000000..2c3698f63fb
--- /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 <derick@derickrethans.nl>                    |
+   |          Tim Düsterhus <timwolla@php.net>                            |
+   +----------------------------------------------------------------------+
+ */
+
+#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 00000000000..fad711b519e
--- /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 <derick@derickrethans.nl>                    |
+   |          Tim Düsterhus <timwolla@php.net>                            |
+   +----------------------------------------------------------------------+
+*/
+
+#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 00000000000..06ad23dfe63
--- /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 00000000000..e95f8a6b5b9
--- /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 00000000000..86578488ae8
--- /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 00000000000..825bd45de2f
--- /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 00000000000..f436d0a63ff
--- /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 00000000000..98448409bf1
--- /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 00000000000..488a39c2fc1
--- /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 00000000000..440107d33ed
--- /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 00000000000..21c54538f40
--- /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 00000000000..65828803643
--- /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 00000000000..998ab5d9bea
--- /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 00000000000..719da0e88e2
--- /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 00000000000..109a1dd1307
--- /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 00000000000..04ee27db466
--- /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 00000000000..aef4092b909
--- /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 00000000000..84f317425a4
--- /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 00000000000..d63f20535b4
--- /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 00000000000..4ed7250fb38
--- /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 00000000000..7674ea95f54
--- /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 00000000000..705823165df
--- /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 00000000000..4b3eaf4ad26
--- /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 00000000000..7b425d2eb56
--- /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 00000000000..e68adc28e4a
--- /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 00000000000..8cd26ce4c72
--- /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 00000000000..2deed85ec0e
--- /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 00000000000..70b374d28c2
--- /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 00000000000..7a1cbcb5606
--- /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 00000000000..04b220fe2db
--- /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 00000000000..ce156cd2037
--- /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 00000000000..5552a249d9b
--- /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 00000000000..298b2bfe495
--- /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 00000000000..c8f8fac503b
--- /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 00000000000..312752374e7
--- /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 00000000000..36b407138fe
--- /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 00000000000..b9d0a01b39d
--- /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 00000000000..b1dd72f4c24
Binary files /dev/null and b/ext/date/time_arginfo.h differ
diff --git a/ext/date/time_duration.c b/ext/date/time_duration.c
new file mode 100644
index 00000000000..d9952a829d2
--- /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 <derick@derickrethans.nl>                    |
+   |          Tim Düsterhus <timwolla@php.net>                            |
+   +----------------------------------------------------------------------+
+ */
+
+#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));
+}