Commit c242f17cf4b for php
commit c242f17cf4bd84654e528353609526c6b97a2213
Author: Weilin Du <weilindu@php.net>
Date: Thu Oct 22 09:47:01 2026 +0800
[RFC] ext/intl: Implement IntlRelativeDateTimeFormatter (#23514)
Add locale-aware formatting of relative offsets, enum-based style,
capitalization and unit options, and custom NumberFormatter support.
Observe subsequent NumberFormatter configuration changes and cover
formatting, error handling, and object lifetime in tests.
RFC: https://wiki.php.net/rfc/reldateformatter
Co-authored-by: NickSdot <32384907+NickSdot@users.noreply.github.com>
Co-authored-by: David Carlier <devnexen@gmail.com>
diff --git a/NEWS b/NEWS
index cbd8903c061..67896bb73a8 100644
--- a/NEWS
+++ b/NEWS
@@ -23,6 +23,9 @@ PHP NEWS
throwing a ValueError. (Marc Bennewitz)
- Intl:
+ . Added IntlRelativeDateTimeFormatter for locale-aware relative date and time
+ formatting, with enums for styles, capitalization contexts, and units.
+ (Weilin Du)
. Added Transliterator::toRules() and transliterator_to_rules().
(Weilin Du, David Carlier)
. Fixed Collator attribute and strength methods not rejecting an
diff --git a/UPGRADING b/UPGRADING
index fbc8ddce787..60b7020cb1e 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -50,6 +50,18 @@ PHP 8.7 UPGRADE NOTES
array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS.
- Intl:
+ . Added IntlRelativeDateTimeFormatter to format relative date and time offsets
+ using locale-specific text, such as "yesterday" or "in 3 days". Its format()
+ method uses a textual form when available and a numeric form otherwise,
+ while formatNumeric() always uses a numeric form. combineDateAndTime()
+ combines already formatted date and time strings according to the locale.
+ The constructor accepts an optional NumberFormatter to customize numeric
+ output; subsequent configuration changes to that object are reflected in
+ formatting results. Styles, capitalization contexts, and units are selected
+ using the IntlRelativeDateTimeFormatterStyle,
+ IntlRelativeDateTimeFormatterCapitalization, and
+ IntlRelativeDateTimeFormatterUnit enums, respectively.
+ RFC: https://wiki.php.net/rfc/reldateformatter
. Locale::acceptFromHttp() and locale_accept_from_http() now accept an
optional $availableLocales array argument. When given, the
Accept-Language header is matched against these locales instead of all
@@ -95,6 +107,11 @@ PHP 8.7 UPGRADE NOTES
========================================
- Intl:
+ . IntlRelativeDateTimeFormatter
+ . enum IntlRelativeDateTimeFormatterStyle
+ . enum IntlRelativeDateTimeFormatterCapitalization
+ . enum IntlRelativeDateTimeFormatterUnit
+ RFC: https://wiki.php.net/rfc/reldateformatter
. Added IntlNumberFormatter class to format a number with a given locale
and ICU number skeleton, which also covers measurement units, e.g.
"measure-unit/speed-kilometer-per-hour unit-width-full-name".
diff --git a/ext/intl/CREDITS b/ext/intl/CREDITS
index 02a0f12a139..c038d413541 100644
--- a/ext/intl/CREDITS
+++ b/ext/intl/CREDITS
@@ -1,2 +1,2 @@
Internationalization
-Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar
+Ed Batutis, David Carlier, Weilin Du, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar
diff --git a/ext/intl/config.m4 b/ext/intl/config.m4
index 0f426e45af7..a9de76180be 100644
--- a/ext/intl/config.m4
+++ b/ext/intl/config.m4
@@ -75,6 +75,7 @@ if test "$PHP_INTL" != "no"; then
breakiterator/codepointiterator_internal.cpp \
breakiterator/codepointiterator_methods.cpp \
listformatter/listformatter_class.cpp \
+ reldateformatter/reldateformatter_class.cpp \
transliterator/transliterator_class.cpp \
transliterator/transliterator_methods.cpp \
idn/idn.cpp \
@@ -125,6 +126,7 @@ if test "$PHP_INTL" != "no"; then
$ext_builddir/idn
$ext_builddir/locale
$ext_builddir/listformatter
+ $ext_builddir/reldateformatter
$ext_builddir/msgformat
$ext_builddir/normalizer
$ext_builddir/numberformatter
diff --git a/ext/intl/config.w32 b/ext/intl/config.w32
index 8ca7096152b..93719608d46 100644
--- a/ext/intl/config.w32
+++ b/ext/intl/config.w32
@@ -42,6 +42,9 @@ if (PHP_INTL != "no") {
ADD_SOURCES(configure_module_dirname + "/listformatter", "\
listformatter_class.cpp \
", "intl");
+ ADD_SOURCES(configure_module_dirname + "/reldateformatter", "\
+ reldateformatter_class.cpp \
+ ", "intl");
ADD_SOURCES(configure_module_dirname + "/locale", "\
locale.cpp \
locale_class.cpp \
diff --git a/ext/intl/formatter/formatter_attr.cpp b/ext/intl/formatter/formatter_attr.cpp
index ff215fdffbd..dee5a203b06 100644
--- a/ext/intl/formatter/formatter_attr.cpp
+++ b/ext/intl/formatter/formatter_attr.cpp
@@ -181,6 +181,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_attribute )
INTL_METHOD_CHECK_STATUS( nfo, "Error setting attribute value" );
+ nfo->configuration_version++;
RETURN_TRUE;
}
/* }}} */
@@ -212,6 +213,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_text_attribute )
unum_setTextAttribute(FORMATTER_UNUM(nfo), static_cast<UNumberFormatTextAttribute>(attribute), svalue.getBuffer(), svalue.length(), &INTL_DATA_ERROR_CODE(nfo));
INTL_METHOD_CHECK_STATUS( nfo, "Error setting text attribute" );
+ nfo->configuration_version++;
RETURN_TRUE;
}
/* }}} */
@@ -293,6 +295,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_symbol )
unum_setSymbol(FORMATTER_UNUM(nfo), symbol, svalue.getBuffer(), svalue.length(), &INTL_DATA_ERROR_CODE(nfo));
INTL_METHOD_CHECK_STATUS( nfo, "Error setting symbol value" );
+ nfo->configuration_version++;
RETURN_TRUE;
}
/* }}} */
@@ -363,6 +366,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_pattern )
RETURN_FALSE;
}
+ nfo->configuration_version++;
RETURN_TRUE;
}
/* }}} */
diff --git a/ext/intl/formatter/formatter_class.cpp b/ext/intl/formatter/formatter_class.cpp
index 29817132aca..bcdfbee6355 100644
--- a/ext/intl/formatter/formatter_class.cpp
+++ b/ext/intl/formatter/formatter_class.cpp
@@ -52,6 +52,7 @@ U_CFUNC zend_object *NumberFormatter_object_create(zend_class_entry *ce)
intern = reinterpret_cast<NumberFormatter_object *>(zend_object_alloc(sizeof(NumberFormatter_object), ce));
formatter_data_init( &intern->nf_data );
+ intern->configuration_version = 0;
zend_object_std_init( &intern->zo, ce );
object_properties_init(&intern->zo, ce);
@@ -68,6 +69,7 @@ U_CFUNC zend_object *NumberFormatter_object_clone(zend_object *object)
/* clone standard parts */
zend_objects_clone_members(&new_nfo->zo, &nfo->zo);
+ new_nfo->configuration_version = nfo->configuration_version;
/* clone formatter object. It may fail, the destruction code must handle this case */
if (FORMATTER_OBJECT(nfo) != nullptr) {
diff --git a/ext/intl/formatter/formatter_class.h b/ext/intl/formatter/formatter_class.h
index 44bdfae8deb..0d4a4208a16 100644
--- a/ext/intl/formatter/formatter_class.h
+++ b/ext/intl/formatter/formatter_class.h
@@ -29,6 +29,7 @@ extern "C" {
typedef struct {
formatter_data nf_data;
+ uint64_t configuration_version;
zend_object zo;
} NumberFormatter_object;
diff --git a/ext/intl/php_intl.c b/ext/intl/php_intl.c
index 5b51d98dba0..6464f6e2033 100644
--- a/ext/intl/php_intl.c
+++ b/ext/intl/php_intl.c
@@ -42,6 +42,7 @@
#include "locale/locale_class.h"
#include "listformatter/listformatter_class.h"
+#include "reldateformatter/reldateformatter_class.h"
#include "numberformatter/numberformatter_class.h"
#include "rangeformatter/rangeformatter_class.h"
@@ -190,6 +191,9 @@ PHP_MINIT_FUNCTION( intl )
/* Register 'ListFormatter' PHP class */
listformatter_register_class( );
+ /* Register 'IntlRelativeDateTimeFormatter' PHP class */
+ reldateformatter_register_class( );
+
#if U_ICU_VERSION_MAJOR_NUM >= 63
/* Register 'IntlNumberFormatter' PHP class */
numberformatter_register_class( );
diff --git a/ext/intl/reldateformatter/reldateformatter.stub.php b/ext/intl/reldateformatter/reldateformatter.stub.php
new file mode 100644
index 00000000000..3418e7b85d3
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter.stub.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @generate-class-entries
+ * @generate-c-enums
+ */
+
+enum IntlRelativeDateTimeFormatterStyle
+{
+ case Long;
+ case Short;
+ case Narrow;
+}
+
+enum IntlRelativeDateTimeFormatterCapitalization
+{
+ case None;
+ case MiddleOfSentence;
+ case BeginningOfSentence;
+ case UiListAndMenu;
+ case Standalone;
+}
+
+enum IntlRelativeDateTimeFormatterUnit
+{
+ case Year;
+ case Quarter;
+ case Month;
+ case Week;
+ case Day;
+ case Hour;
+ case Minute;
+ case Second;
+ case Sunday;
+ case Monday;
+ case Tuesday;
+ case Wednesday;
+ case Thursday;
+ case Friday;
+ case Saturday;
+}
+
+/**
+ * @not-serializable
+ * @strict-properties
+ */
+final class IntlRelativeDateTimeFormatter
+{
+ public function __construct(
+ ?string $locale = null,
+ IntlRelativeDateTimeFormatterStyle $style = IntlRelativeDateTimeFormatterStyle::Long,
+ IntlRelativeDateTimeFormatterCapitalization $capitalizationContext = IntlRelativeDateTimeFormatterCapitalization::None,
+ ?NumberFormatter $numberFormatter = null,
+ ) {}
+
+ public function format(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false {}
+
+ public function formatNumeric(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false {}
+
+ public function combineDateAndTime(string $relativeDate, string $time): string|false {}
+
+ public function getErrorCode(): int {}
+
+ public function getErrorMessage(): string {}
+}
diff --git a/ext/intl/reldateformatter/reldateformatter_arginfo.h b/ext/intl/reldateformatter/reldateformatter_arginfo.h
new file mode 100644
index 00000000000..aa8b7a81802
Binary files /dev/null and b/ext/intl/reldateformatter/reldateformatter_arginfo.h differ
diff --git a/ext/intl/reldateformatter/reldateformatter_class.cpp b/ext/intl/reldateformatter/reldateformatter_class.cpp
new file mode 100644
index 00000000000..e2b5db0b7b0
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter_class.cpp
@@ -0,0 +1,541 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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: Weilin Du <weilindu@php.net> |
+ +----------------------------------------------------------------------+
+ */
+
+extern "C" {
+#include "php.h"
+#include "zend_enum.h"
+#include "zend_exceptions.h"
+}
+
+#include "../formatter/formatter_class.h"
+
+#include <unicode/brkiter.h>
+#include <unicode/locid.h>
+#include <unicode/reldatefmt.h>
+#include <unicode/uchar.h>
+#include <unicode/ucasemap.h>
+#include <unicode/udisplaycontext.h>
+#include <unicode/ureldatefmt.h>
+#include <unicode/unistr.h>
+
+#include <memory>
+#include <new>
+
+extern "C" {
+#include "../php_intl.h"
+#include "../intl_data.h"
+#include "../intl_error.h"
+}
+
+#include "../intl_convertcpp.h"
+#include "reldateformatter_class.h"
+#include "reldateformatter_arginfo.h"
+#include "reldateformatter_decl.h"
+
+using icu::Locale;
+using icu::UnicodeString;
+
+struct reldateformatter_data {
+ Locale locale;
+ std::unique_ptr<icu::RelativeDateTimeFormatter> formatter;
+ std::unique_ptr<icu::BreakIterator> sentence_iterator;
+};
+
+zend_class_entry *IntlRelativeDateTimeFormatter_ce_ptr;
+static zend_class_entry *reldateformatter_style_ce;
+static zend_class_entry *reldateformatter_capitalization_ce;
+static zend_class_entry *reldateformatter_unit_ce;
+static zend_object_handlers reldateformatter_handlers;
+
+static void reldateformatter_free_object(zend_object *object)
+{
+ IntlRelativeDateTimeFormatter_object *obj = php_intl_reldateformatter_fetch_object(object);
+
+ if (RELDATEFORMATTER_OBJECT(obj) != nullptr) {
+ delete RELDATEFORMATTER_OBJECT(obj);
+ RELDATEFORMATTER_OBJECT(obj) = nullptr;
+ }
+ if (obj->number_formatter != nullptr) {
+ OBJ_RELEASE(obj->number_formatter);
+ obj->number_formatter = nullptr;
+ }
+ if (obj->locale != nullptr) {
+ zend_string_release(obj->locale);
+ obj->locale = nullptr;
+ }
+ intl_error_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+ zend_object_std_dtor(&obj->zo);
+}
+
+static zend_object *reldateformatter_create_object(zend_class_entry *class_type)
+{
+ IntlRelativeDateTimeFormatter_object *obj = reinterpret_cast<IntlRelativeDateTimeFormatter_object *>(
+ zend_object_alloc(sizeof(IntlRelativeDateTimeFormatter_object), class_type));
+
+ intl_error_init(RELDATEFORMATTER_ERROR_P(obj));
+ RELDATEFORMATTER_OBJECT(obj) = nullptr;
+ obj->number_formatter = nullptr;
+ obj->number_formatter_version = 0;
+ obj->locale = nullptr;
+ obj->style = UDAT_STYLE_LONG;
+ obj->capitalization_context = UDISPCTX_CAPITALIZATION_NONE;
+
+ zend_object_std_init(&obj->zo, class_type);
+ object_properties_init(&obj->zo, class_type);
+
+ return &obj->zo;
+}
+
+static HashTable *reldateformatter_get_gc(zend_object *object, zval **table, int *n)
+{
+ IntlRelativeDateTimeFormatter_object *obj = php_intl_reldateformatter_fetch_object(object);
+
+ if (obj->number_formatter != nullptr) {
+ zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
+ zend_get_gc_buffer_add_obj(gc_buffer, obj->number_formatter);
+ zend_get_gc_buffer_use(gc_buffer, table, n);
+ } else {
+ *table = nullptr;
+ *n = 0;
+ }
+
+ return nullptr;
+}
+
+static UDateRelativeDateTimeFormatterStyle reldateformatter_style_from_enum(zend_object *style)
+{
+ switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterStyle>(zend_enum_fetch_case_id(style))) {
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Long:
+ return UDAT_STYLE_LONG;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Short:
+ return UDAT_STYLE_SHORT;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Narrow:
+ return UDAT_STYLE_NARROW;
+ }
+ ZEND_UNREACHABLE();
+}
+
+static UDisplayContext reldateformatter_capitalization_from_enum(zend_object *context)
+{
+ switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterCapitalization>(zend_enum_fetch_case_id(context))) {
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_None:
+ return UDISPCTX_CAPITALIZATION_NONE;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_MiddleOfSentence:
+ return UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_BeginningOfSentence:
+ return UDISPCTX_CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_UiListAndMenu:
+ return UDISPCTX_CAPITALIZATION_FOR_UI_LIST_OR_MENU;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_Standalone:
+ return UDISPCTX_CAPITALIZATION_FOR_STANDALONE;
+ }
+ ZEND_UNREACHABLE();
+}
+
+static URelativeDateTimeUnit reldateformatter_unit_from_enum(zend_object *unit)
+{
+ switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterUnit>(zend_enum_fetch_case_id(unit))) {
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Year:
+ return UDAT_REL_UNIT_YEAR;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Quarter:
+ return UDAT_REL_UNIT_QUARTER;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Month:
+ return UDAT_REL_UNIT_MONTH;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Week:
+ return UDAT_REL_UNIT_WEEK;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Day:
+ return UDAT_REL_UNIT_DAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Hour:
+ return UDAT_REL_UNIT_HOUR;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Minute:
+ return UDAT_REL_UNIT_MINUTE;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Second:
+ return UDAT_REL_UNIT_SECOND;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Sunday:
+ return UDAT_REL_UNIT_SUNDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Monday:
+ return UDAT_REL_UNIT_MONDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Tuesday:
+ return UDAT_REL_UNIT_TUESDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Wednesday:
+ return UDAT_REL_UNIT_WEDNESDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Thursday:
+ return UDAT_REL_UNIT_THURSDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Friday:
+ return UDAT_REL_UNIT_FRIDAY;
+ case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Saturday:
+ return UDAT_REL_UNIT_SATURDAY;
+ }
+ ZEND_UNREACHABLE();
+}
+
+static void reldateformatter_throw_constructor_failure(
+ IntlRelativeDateTimeFormatter_object *obj,
+ UErrorCode status,
+ const char *message
+)
+{
+ const bool old_use_exceptions = INTL_G(use_exceptions);
+ const zend_long old_error_level = INTL_G(error_level);
+ INTL_G(use_exceptions) = true;
+ INTL_G(error_level) = 0;
+
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, message);
+
+ INTL_G(use_exceptions) = old_use_exceptions;
+ INTL_G(error_level) = old_error_level;
+}
+
+static reldateformatter_data *reldateformatter_open(
+ const char *locale,
+ UDateRelativeDateTimeFormatterStyle style,
+ UDisplayContext capitalization_context,
+ const NumberFormatter_object *number_formatter,
+ UErrorCode *status
+)
+{
+ if (U_FAILURE(*status)) {
+ return nullptr;
+ }
+
+ std::unique_ptr<reldateformatter_data> result(new (std::nothrow) reldateformatter_data());
+ if (result == nullptr) {
+ *status = U_MEMORY_ALLOCATION_ERROR;
+ return nullptr;
+ }
+
+ result->locale = Locale(locale);
+ if (capitalization_context == UDISPCTX_CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) {
+ /* ICU can fail to create this iterator before adopting the number formatter. */
+ result->sentence_iterator.reset(icu::BreakIterator::createSentenceInstance(result->locale, *status));
+ if (U_FAILURE(*status)) {
+ return nullptr;
+ }
+ if (result->sentence_iterator == nullptr) {
+ *status = U_MEMORY_ALLOCATION_ERROR;
+ return nullptr;
+ }
+ capitalization_context = UDISPCTX_CAPITALIZATION_NONE;
+ }
+
+ std::unique_ptr<icu::NumberFormat> number_formatter_clone;
+ if (number_formatter != nullptr) {
+ number_formatter_clone.reset(static_cast<icu::NumberFormat *>(FORMATTER_OBJECT(number_formatter)->clone()));
+ if (number_formatter_clone == nullptr) {
+ *status = U_MEMORY_ALLOCATION_ERROR;
+ return nullptr;
+ }
+ }
+
+ /* C++11 may evaluate constructor arguments even if allocation fails, so keep
+ * ownership of the clone until we know that the constructor ran. */
+ result->formatter.reset(new icu::RelativeDateTimeFormatter(
+ result->locale, number_formatter_clone.get(), style, capitalization_context, *status));
+ if (result->formatter == nullptr) {
+ *status = U_MEMORY_ALLOCATION_ERROR;
+ return nullptr;
+ }
+ /* With a valid style/context and no sentence iterator to create, the constructor
+ * adopts the clone even if initialization fails. Release it before checking status. */
+ number_formatter_clone.release();
+ if (U_FAILURE(*status)) {
+ return nullptr;
+ }
+
+ return result.release();
+}
+
+static bool reldateformatter_refresh_number_formatter(IntlRelativeDateTimeFormatter_object *obj)
+{
+ if (obj->number_formatter == nullptr) {
+ return true;
+ }
+
+ NumberFormatter_object *number_formatter = php_intl_number_format_fetch_object(obj->number_formatter);
+ if (obj->number_formatter_version == number_formatter->configuration_version) {
+ return true;
+ }
+
+ UErrorCode status = U_ZERO_ERROR;
+ reldateformatter_data *formatter = reldateformatter_open(
+ ZSTR_VAL(obj->locale),
+ obj->style,
+ obj->capitalization_context,
+ number_formatter,
+ &status);
+ if (U_FAILURE(status) || formatter == nullptr) {
+ if (U_SUCCESS(status)) {
+ status = U_MEMORY_ALLOCATION_ERROR;
+ }
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+ "Failed to update IntlRelativeDateTimeFormatter from NumberFormatter");
+ return false;
+ }
+
+ delete RELDATEFORMATTER_OBJECT(obj);
+ RELDATEFORMATTER_OBJECT(obj) = formatter;
+ obj->number_formatter_version = number_formatter->configuration_version;
+ return true;
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, __construct)
+{
+ IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+ char *locale = nullptr;
+ size_t locale_len = 0;
+ zend_object *style = nullptr;
+ zend_object *capitalization_context = nullptr;
+ zval *number_formatter = nullptr;
+
+ ZEND_PARSE_PARAMETERS_START(0, 4)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_STRING_OR_NULL(locale, locale_len)
+ Z_PARAM_OBJ_OF_CLASS(style, reldateformatter_style_ce)
+ Z_PARAM_OBJ_OF_CLASS(capitalization_context, reldateformatter_capitalization_ce)
+ Z_PARAM_OBJECT_OF_CLASS_OR_NULL(number_formatter, NumberFormatter_ce_ptr)
+ ZEND_PARSE_PARAMETERS_END();
+
+ intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+ if (RELDATEFORMATTER_OBJECT(obj) != nullptr) {
+ zend_throw_error(NULL, "IntlRelativeDateTimeFormatter object is already constructed");
+ RETURN_THROWS();
+ }
+
+ if (UNEXPECTED(locale_len > INTL_MAX_LOCALE_LEN)) {
+ zend_argument_value_error(1, "must be less than or equal to %d characters", INTL_MAX_LOCALE_LEN);
+ RETURN_THROWS();
+ }
+
+ if (locale == nullptr || locale_len == 0) {
+ locale = const_cast<char *>(intl_locale_get_default());
+ }
+
+ if (Locale(locale).getISO3Language()[0] == '\0') {
+ zend_argument_value_error(1, "\"%s\" is invalid", locale);
+ RETURN_THROWS();
+ }
+
+ const UDateRelativeDateTimeFormatterStyle icu_style =
+ style != nullptr ? reldateformatter_style_from_enum(style) : UDAT_STYLE_LONG;
+ const UDisplayContext icu_capitalization_context = capitalization_context != nullptr
+ ? reldateformatter_capitalization_from_enum(capitalization_context) : UDISPCTX_CAPITALIZATION_NONE;
+
+ NumberFormatter_object *number_formatter_obj = nullptr;
+ if (number_formatter != nullptr) {
+ number_formatter_obj = Z_INTL_NUMBERFORMATTER_P(number_formatter);
+ if (FORMATTER_OBJECT(number_formatter_obj) == nullptr) {
+ zend_throw_error(NULL, "Found unconstructed NumberFormatter");
+ RETURN_THROWS();
+ }
+ }
+
+ UErrorCode status = U_ZERO_ERROR;
+ RELDATEFORMATTER_OBJECT(obj) = reldateformatter_open(
+ locale,
+ icu_style,
+ icu_capitalization_context,
+ number_formatter_obj,
+ &status);
+
+ if (U_FAILURE(status) || RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+ if (U_SUCCESS(status)) {
+ status = U_MEMORY_ALLOCATION_ERROR;
+ }
+ reldateformatter_throw_constructor_failure(
+ obj, status, "Failed to create IntlRelativeDateTimeFormatter");
+ RETURN_THROWS();
+ }
+
+ obj->locale = zend_string_init(locale, strlen(locale), false);
+ obj->style = icu_style;
+ obj->capitalization_context = icu_capitalization_context;
+ if (number_formatter != nullptr) {
+ obj->number_formatter = Z_OBJ_P(number_formatter);
+ GC_ADDREF(obj->number_formatter);
+ obj->number_formatter_version = number_formatter_obj->configuration_version;
+ }
+}
+
+template<typename Formatter>
+static zend_string *reldateformatter_format_result(
+ IntlRelativeDateTimeFormatter_object *obj,
+ Formatter &&formatter,
+ const char *failure_message,
+ bool capitalize = false
+)
+{
+ UErrorCode status = U_ZERO_ERROR;
+ UnicodeString result;
+ formatter(result, status);
+ if (U_FAILURE(status)) {
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, failure_message);
+ return nullptr;
+ }
+
+ icu::BreakIterator *sentence_iterator = RELDATEFORMATTER_OBJECT(obj)->sentence_iterator.get();
+ if (capitalize && sentence_iterator != nullptr && !result.isEmpty() && u_islower(result.char32At(0))) {
+ result.toTitle(sentence_iterator, RELDATEFORMATTER_OBJECT(obj)->locale,
+ U_TITLECASE_NO_LOWERCASE | U_TITLECASE_NO_BREAK_ADJUSTMENT);
+ }
+ if (result.isBogus()) {
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), U_MEMORY_ALLOCATION_ERROR, failure_message);
+ return nullptr;
+ }
+
+ zend_string *utf8_result = intl_charFromString(result, &status);
+ if (utf8_result == nullptr) {
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, "Failed to convert result to UTF-8");
+ }
+
+ return utf8_result;
+}
+
+static void reldateformatter_format(INTERNAL_FUNCTION_PARAMETERS, bool numeric)
+{
+ zval *offset;
+ zend_object *unit_object;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_NUMBER(offset)
+ Z_PARAM_OBJ_OF_CLASS(unit_object, reldateformatter_unit_ce)
+ ZEND_PARSE_PARAMETERS_END();
+
+ IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+ intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+ if (RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+ zend_throw_error(NULL, "Found unconstructed IntlRelativeDateTimeFormatter");
+ RETURN_THROWS();
+ }
+
+ if (!reldateformatter_refresh_number_formatter(obj)) {
+ RETURN_FALSE;
+ }
+
+ const double numeric_offset = zval_get_double(offset);
+ const URelativeDateTimeUnit unit = reldateformatter_unit_from_enum(unit_object);
+ zend_string *result;
+ if (numeric) {
+ result = reldateformatter_format_result(obj,
+ [obj, numeric_offset, unit](UnicodeString &result, UErrorCode &status) {
+ RELDATEFORMATTER_OBJECT(obj)->formatter->formatNumeric(numeric_offset, unit, result, status);
+ },
+ "Failed to format relative date/time numerically", true);
+ } else {
+ result = reldateformatter_format_result(obj,
+ [obj, numeric_offset, unit](UnicodeString &result, UErrorCode &status) {
+ RELDATEFORMATTER_OBJECT(obj)->formatter->format(numeric_offset, unit, result, status);
+ },
+ "Failed to format relative date/time", true);
+ }
+
+ if (result == nullptr) {
+ RETURN_FALSE;
+ }
+ RETURN_STR(result);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, format)
+{
+ reldateformatter_format(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, formatNumeric)
+{
+ reldateformatter_format(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, combineDateAndTime)
+{
+ zend_string *relative_date;
+ zend_string *time;
+
+ ZEND_PARSE_PARAMETERS_START(2, 2)
+ Z_PARAM_STR(relative_date)
+ Z_PARAM_STR(time)
+ ZEND_PARSE_PARAMETERS_END();
+
+ IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+ intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+ if (RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+ zend_throw_error(NULL, "Found unconstructed IntlRelativeDateTimeFormatter");
+ RETURN_THROWS();
+ }
+
+ UErrorCode status = U_ZERO_ERROR;
+ UnicodeString relative_date_utf16;
+ UnicodeString time_utf16;
+ intl_stringFromChar(
+ relative_date_utf16, ZSTR_VAL(relative_date), ZSTR_LEN(relative_date), &status);
+ if (U_FAILURE(status)) {
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+ "Failed to convert relative date to UTF-16");
+ RETURN_FALSE;
+ }
+
+ intl_stringFromChar(time_utf16, ZSTR_VAL(time), ZSTR_LEN(time), &status);
+ if (U_FAILURE(status)) {
+ intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+ "Failed to convert time to UTF-16");
+ RETURN_FALSE;
+ }
+
+ zend_string *result = reldateformatter_format_result(obj,
+ [obj, &relative_date_utf16, &time_utf16](UnicodeString &result, UErrorCode &status) {
+ RELDATEFORMATTER_OBJECT(obj)->formatter->combineDateAndTime(
+ relative_date_utf16, time_utf16, result, status);
+ },
+ "Failed to combine relative date and time");
+
+ if (result == nullptr) {
+ RETURN_FALSE;
+ }
+ RETURN_STR(result);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, getErrorCode)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+ RETURN_LONG(intl_error_get_code(RELDATEFORMATTER_ERROR_P(obj)));
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, getErrorMessage)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+ RETURN_STR(intl_error_get_message(RELDATEFORMATTER_ERROR_P(obj)));
+}
+
+void reldateformatter_register_class(void)
+{
+ reldateformatter_style_ce = register_class_IntlRelativeDateTimeFormatterStyle();
+ reldateformatter_capitalization_ce = register_class_IntlRelativeDateTimeFormatterCapitalization();
+ reldateformatter_unit_ce = register_class_IntlRelativeDateTimeFormatterUnit();
+
+ IntlRelativeDateTimeFormatter_ce_ptr = register_class_IntlRelativeDateTimeFormatter();
+ IntlRelativeDateTimeFormatter_ce_ptr->create_object = reldateformatter_create_object;
+ IntlRelativeDateTimeFormatter_ce_ptr->default_object_handlers = &reldateformatter_handlers;
+
+ memcpy(&reldateformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+ reldateformatter_handlers.offset = offsetof(IntlRelativeDateTimeFormatter_object, zo);
+ reldateformatter_handlers.free_obj = reldateformatter_free_object;
+ reldateformatter_handlers.clone_obj = nullptr;
+ reldateformatter_handlers.get_gc = reldateformatter_get_gc;
+}
diff --git a/ext/intl/reldateformatter/reldateformatter_class.h b/ext/intl/reldateformatter/reldateformatter_class.h
new file mode 100644
index 00000000000..cd2ddc6e32e
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter_class.h
@@ -0,0 +1,56 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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: Weilin Du <weilindu@php.net> |
+ +----------------------------------------------------------------------+
+ */
+
+#ifndef RELDATEFORMATTER_CLASS_H
+#define RELDATEFORMATTER_CLASS_H
+
+#include <php.h>
+#include <unicode/ureldatefmt.h>
+
+#include "../intl_error.h"
+
+typedef struct reldateformatter_data reldateformatter_data;
+
+typedef struct {
+ intl_error error;
+ reldateformatter_data *formatter;
+ zend_object *number_formatter;
+ uint64_t number_formatter_version;
+ zend_string *locale;
+ UDateRelativeDateTimeFormatterStyle style;
+ UDisplayContext capitalization_context;
+ zend_object zo;
+} IntlRelativeDateTimeFormatter_object;
+
+#define php_intl_reldateformatter_fetch_object(obj) \
+ ZEND_CONTAINER_OF(obj, IntlRelativeDateTimeFormatter_object, zo)
+#define Z_INTL_RELDATEFORMATTER_P(zv) \
+ php_intl_reldateformatter_fetch_object(Z_OBJ_P(zv))
+
+#define RELDATEFORMATTER_ERROR(obj) ((obj)->error)
+#define RELDATEFORMATTER_ERROR_P(obj) (&RELDATEFORMATTER_ERROR(obj))
+#define RELDATEFORMATTER_OBJECT(obj) ((obj)->formatter)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void reldateformatter_register_class(void);
+extern zend_class_entry *IntlRelativeDateTimeFormatter_ce_ptr;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* RELDATEFORMATTER_CLASS_H */
diff --git a/ext/intl/reldateformatter/reldateformatter_decl.h b/ext/intl/reldateformatter/reldateformatter_decl.h
new file mode 100644
index 00000000000..5031a0e6fdd
Binary files /dev/null and b/ext/intl/reldateformatter/reldateformatter_decl.h differ
diff --git a/ext/intl/tests/reldateformatter/formatting/format.inc b/ext/intl/tests/reldateformatter/formatting/format.inc
new file mode 100644
index 00000000000..69a531537f7
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/format.inc
@@ -0,0 +1,106 @@
+<?php
+
+const RELATIVE_DATE_TIME_UNITS = [
+ 'year' => IntlRelativeDateTimeFormatterUnit::Year,
+ 'quarter' => IntlRelativeDateTimeFormatterUnit::Quarter,
+ 'month' => IntlRelativeDateTimeFormatterUnit::Month,
+ 'week' => IntlRelativeDateTimeFormatterUnit::Week,
+ 'day' => IntlRelativeDateTimeFormatterUnit::Day,
+ 'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+ 'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+ 'second' => IntlRelativeDateTimeFormatterUnit::Second,
+ 'sunday' => IntlRelativeDateTimeFormatterUnit::Sunday,
+ 'monday' => IntlRelativeDateTimeFormatterUnit::Monday,
+ 'tuesday' => IntlRelativeDateTimeFormatterUnit::Tuesday,
+ 'wednesday' => IntlRelativeDateTimeFormatterUnit::Wednesday,
+ 'thursday' => IntlRelativeDateTimeFormatterUnit::Thursday,
+ 'friday' => IntlRelativeDateTimeFormatterUnit::Friday,
+ 'saturday' => IntlRelativeDateTimeFormatterUnit::Saturday,
+];
+
+const RELATIVE_DATE_TIME_OFFSETS = [
+ -3, -2, -1.75, -1.5, -1, -0.5,
+ 0, 0.0, -0.0,
+ 0.5, 1, 1.5, 1.75, 2, 3,
+];
+
+/**
+ * Offsets may be a list shared by all units, or a map from unit names to lists.
+ * A map selects only the units it contains, preserving the shared unit order.
+ */
+function printRelativeDateTimeTable(
+ IntlRelativeDateTimeFormatter $formatter,
+ array $offsets = RELATIVE_DATE_TIME_OFFSETS,
+ array $units = RELATIVE_DATE_TIME_UNITS,
+): void {
+
+ $rows = [];
+ $headers = ['offset', 'format', 'formatNumeric'];
+
+ $widths = array_map('grapheme_strlen', $headers);
+
+ $sharedOffsets = array_is_list($offsets);
+
+ foreach ($units as $name => $unit) {
+
+ if (!$sharedOffsets && !array_key_exists($name, $offsets)) {
+ continue;
+ }
+
+ foreach ($sharedOffsets ? $offsets : $offsets[$name] as $offset) {
+
+ // NaN must not trigger float to string warning.
+ $offsetLabel = is_nan($offset) ? 'NAN' : (string) $offset;
+
+ $row = [
+ $offsetLabel,
+ (string) $formatter->format($offset, $unit),
+ (string) $formatter->formatNumeric($offset, $unit),
+ ];
+
+ foreach ($row as $column => $value) {
+ $widths[$column] = max($widths[$column], grapheme_strlen($value));
+ }
+
+ $rows[$name][] = $row;
+ }
+ }
+
+ $printRow = static function (array $row) use ($widths): void {
+
+ foreach ($row as $column => $value) {
+
+ // unpadded final column to avoid trailing whitespace.
+ if ($column === array_key_last($row)) {
+ continue;
+ }
+
+ $space = str_repeat(' ', $widths[$column] - grapheme_strlen($value));
+
+ $row[$column] = $column === 0
+ ? $space . $value
+ : $value . $space;
+ }
+
+ echo implode(' ', $row), "\n";
+ };
+
+ $fullWidth = array_sum($widths) + 2 * (count($widths) - 1);
+ $separator = array_map(static fn (int $width): string => str_repeat('-', $width), $widths);
+
+ foreach ($rows as $name => $unitRows) {
+
+ if ($name !== array_key_first($rows)) {
+ echo "\n";
+ }
+
+ echo '=== ', $name, ' ', str_repeat('=', $fullWidth - grapheme_strlen($name) - 5), "\n\n";
+
+ $printRow($headers);
+ $printRow($separator);
+
+ foreach ($unitRows as $row) {
+ $printRow($row);
+ }
+ }
+}
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt
new file mode 100644
index 00000000000..68034c915c9
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - large offsets - grouping and values beyond integer range
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+ new IntlRelativeDateTimeFormatter('en_US'),
+ offsets: [-1e20, -1e6, 1e6, 1e20],
+ units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+?>
+--EXPECT--
+=== second ===============================================================================
+
+ offset format formatNumeric
+-------- --------------------------------------- ---------------------------------------
+-1.0E+20 100,000,000,000,000,000,000 seconds ago 100,000,000,000,000,000,000 seconds ago
+-1000000 1,000,000 seconds ago 1,000,000 seconds ago
+ 1000000 in 1,000,000 seconds in 1,000,000 seconds
+ 1.0E+20 in 100,000,000,000,000,000,000 seconds in 100,000,000,000,000,000,000 seconds
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt
new file mode 100644
index 00000000000..574ada3e0ae
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - offsets near zero - relative and numeric output
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+ new IntlRelativeDateTimeFormatter('en_US'),
+ offsets: [-0.001, 0.001],
+ units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+?>
+--EXPECT--
+=== second ======================
+
+offset format formatNumeric
+------ ------ -----------------
+-0.001 now 0.001 seconds ago
+ 0.001 now in 0.001 seconds
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt
new file mode 100644
index 00000000000..34170b21079
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - non-finite offsets - infinities and NaN
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+printRelativeDateTimeTable(
+ $formatter,
+ offsets: [-INF, INF, NAN],
+ units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+var_dump($formatter->getErrorCode());
+var_dump($formatter->getErrorMessage());
+
+?>
+--EXPECT--
+=== second ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -INF ∞ seconds ago ∞ seconds ago
+ INF in ∞ seconds in ∞ seconds
+ NAN in NaN seconds in NaN seconds
+int(0)
+string(12) "U_ZERO_ERROR"
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt
new file mode 100644
index 00000000000..4c13ba5d333
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - long style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+ die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Long);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =======================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 years ago 3 years ago
+ -2 2 years ago 2 years ago
+ -1.75 1.75 years ago 1.75 years ago
+ -1.5 1.5 years ago 1.5 years ago
+ -1 last year 1 year ago
+ -0.5 0.5 years ago 0.5 years ago
+ 0 this year in 0 years
+ 0 this year in 0 years
+ -0 this year 0 years ago
+ 0.5 in 0.5 years in 0.5 years
+ 1 next year in 1 year
+ 1.5 in 1.5 years in 1.5 years
+ 1.75 in 1.75 years in 1.75 years
+ 2 in 2 years in 2 years
+ 3 in 3 years in 3 years
+
+=== quarter ====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 quarters ago 3 quarters ago
+ -2 2 quarters ago 2 quarters ago
+ -1.75 1.75 quarters ago 1.75 quarters ago
+ -1.5 1.5 quarters ago 1.5 quarters ago
+ -1 last quarter 1 quarter ago
+ -0.5 0.5 quarters ago 0.5 quarters ago
+ 0 this quarter in 0 quarters
+ 0 this quarter in 0 quarters
+ -0 this quarter 0 quarters ago
+ 0.5 in 0.5 quarters in 0.5 quarters
+ 1 next quarter in 1 quarter
+ 1.5 in 1.5 quarters in 1.5 quarters
+ 1.75 in 1.75 quarters in 1.75 quarters
+ 2 in 2 quarters in 2 quarters
+ 3 in 3 quarters in 3 quarters
+
+=== month ======================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 months ago 3 months ago
+ -2 2 months ago 2 months ago
+ -1.75 1.75 months ago 1.75 months ago
+ -1.5 1.5 months ago 1.5 months ago
+ -1 last month 1 month ago
+ -0.5 0.5 months ago 0.5 months ago
+ 0 this month in 0 months
+ 0 this month in 0 months
+ -0 this month 0 months ago
+ 0.5 in 0.5 months in 0.5 months
+ 1 next month in 1 month
+ 1.5 in 1.5 months in 1.5 months
+ 1.75 in 1.75 months in 1.75 months
+ 2 in 2 months in 2 months
+ 3 in 3 months in 3 months
+
+=== week =======================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 weeks ago 3 weeks ago
+ -2 2 weeks ago 2 weeks ago
+ -1.75 1.75 weeks ago 1.75 weeks ago
+ -1.5 1.5 weeks ago 1.5 weeks ago
+ -1 last week 1 week ago
+ -0.5 0.5 weeks ago 0.5 weeks ago
+ 0 this week in 0 weeks
+ 0 this week in 0 weeks
+ -0 this week 0 weeks ago
+ 0.5 in 0.5 weeks in 0.5 weeks
+ 1 next week in 1 week
+ 1.5 in 1.5 weeks in 1.5 weeks
+ 1.75 in 1.75 weeks in 1.75 weeks
+ 2 in 2 weeks in 2 weeks
+ 3 in 3 weeks in 3 weeks
+
+=== day ========================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 days ago 3 days ago
+ -2 2 days ago 2 days ago
+ -1.75 1.75 days ago 1.75 days ago
+ -1.5 1.5 days ago 1.5 days ago
+ -1 yesterday 1 day ago
+ -0.5 0.5 days ago 0.5 days ago
+ 0 today in 0 days
+ 0 today in 0 days
+ -0 today 0 days ago
+ 0.5 in 0.5 days in 0.5 days
+ 1 tomorrow in 1 day
+ 1.5 in 1.5 days in 1.5 days
+ 1.75 in 1.75 days in 1.75 days
+ 2 in 2 days in 2 days
+ 3 in 3 days in 3 days
+
+=== hour =======================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 hours ago 3 hours ago
+ -2 2 hours ago 2 hours ago
+ -1.75 1.75 hours ago 1.75 hours ago
+ -1.5 1.5 hours ago 1.5 hours ago
+ -1 1 hour ago 1 hour ago
+ -0.5 0.5 hours ago 0.5 hours ago
+ 0 this hour in 0 hours
+ 0 this hour in 0 hours
+ -0 this hour 0 hours ago
+ 0.5 in 0.5 hours in 0.5 hours
+ 1 in 1 hour in 1 hour
+ 1.5 in 1.5 hours in 1.5 hours
+ 1.75 in 1.75 hours in 1.75 hours
+ 2 in 2 hours in 2 hours
+ 3 in 3 hours in 3 hours
+
+=== minute =====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 minutes ago 3 minutes ago
+ -2 2 minutes ago 2 minutes ago
+ -1.75 1.75 minutes ago 1.75 minutes ago
+ -1.5 1.5 minutes ago 1.5 minutes ago
+ -1 1 minute ago 1 minute ago
+ -0.5 0.5 minutes ago 0.5 minutes ago
+ 0 this minute in 0 minutes
+ 0 this minute in 0 minutes
+ -0 this minute 0 minutes ago
+ 0.5 in 0.5 minutes in 0.5 minutes
+ 1 in 1 minute in 1 minute
+ 1.5 in 1.5 minutes in 1.5 minutes
+ 1.75 in 1.75 minutes in 1.75 minutes
+ 2 in 2 minutes in 2 minutes
+ 3 in 3 minutes in 3 minutes
+
+=== second =====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 seconds ago 3 seconds ago
+ -2 2 seconds ago 2 seconds ago
+ -1.75 1.75 seconds ago 1.75 seconds ago
+ -1.5 1.5 seconds ago 1.5 seconds ago
+ -1 1 second ago 1 second ago
+ -0.5 0.5 seconds ago 0.5 seconds ago
+ 0 now in 0 seconds
+ 0 now in 0 seconds
+ -0 now 0 seconds ago
+ 0.5 in 0.5 seconds in 0.5 seconds
+ 1 in 1 second in 1 second
+ 1.5 in 1.5 seconds in 1.5 seconds
+ 1.75 in 1.75 seconds in 1.75 seconds
+ 2 in 2 seconds in 2 seconds
+ 3 in 3 seconds in 3 seconds
+
+=== sunday =====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Sundays ago 3 Sundays ago
+ -2 2 Sundays ago 2 Sundays ago
+ -1.75 1.75 Sundays ago 1.75 Sundays ago
+ -1.5 1.5 Sundays ago 1.5 Sundays ago
+ -1 last Sunday 1 Sunday ago
+ -0.5 0.5 Sundays ago 0.5 Sundays ago
+ 0 this Sunday in 0 Sundays
+ 0 this Sunday in 0 Sundays
+ -0 this Sunday 0 Sundays ago
+ 0.5 in 0.5 Sundays in 0.5 Sundays
+ 1 next Sunday in 1 Sunday
+ 1.5 in 1.5 Sundays in 1.5 Sundays
+ 1.75 in 1.75 Sundays in 1.75 Sundays
+ 2 in 2 Sundays in 2 Sundays
+ 3 in 3 Sundays in 3 Sundays
+
+=== monday =====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Mondays ago 3 Mondays ago
+ -2 2 Mondays ago 2 Mondays ago
+ -1.75 1.75 Mondays ago 1.75 Mondays ago
+ -1.5 1.5 Mondays ago 1.5 Mondays ago
+ -1 last Monday 1 Monday ago
+ -0.5 0.5 Mondays ago 0.5 Mondays ago
+ 0 this Monday in 0 Mondays
+ 0 this Monday in 0 Mondays
+ -0 this Monday 0 Mondays ago
+ 0.5 in 0.5 Mondays in 0.5 Mondays
+ 1 next Monday in 1 Monday
+ 1.5 in 1.5 Mondays in 1.5 Mondays
+ 1.75 in 1.75 Mondays in 1.75 Mondays
+ 2 in 2 Mondays in 2 Mondays
+ 3 in 3 Mondays in 3 Mondays
+
+=== tuesday ====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Tuesdays ago 3 Tuesdays ago
+ -2 2 Tuesdays ago 2 Tuesdays ago
+ -1.75 1.75 Tuesdays ago 1.75 Tuesdays ago
+ -1.5 1.5 Tuesdays ago 1.5 Tuesdays ago
+ -1 last Tuesday 1 Tuesday ago
+ -0.5 0.5 Tuesdays ago 0.5 Tuesdays ago
+ 0 this Tuesday in 0 Tuesdays
+ 0 this Tuesday in 0 Tuesdays
+ -0 this Tuesday 0 Tuesdays ago
+ 0.5 in 0.5 Tuesdays in 0.5 Tuesdays
+ 1 next Tuesday in 1 Tuesday
+ 1.5 in 1.5 Tuesdays in 1.5 Tuesdays
+ 1.75 in 1.75 Tuesdays in 1.75 Tuesdays
+ 2 in 2 Tuesdays in 2 Tuesdays
+ 3 in 3 Tuesdays in 3 Tuesdays
+
+=== wednesday ==================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Wednesdays ago 3 Wednesdays ago
+ -2 2 Wednesdays ago 2 Wednesdays ago
+ -1.75 1.75 Wednesdays ago 1.75 Wednesdays ago
+ -1.5 1.5 Wednesdays ago 1.5 Wednesdays ago
+ -1 last Wednesday 1 Wednesday ago
+ -0.5 0.5 Wednesdays ago 0.5 Wednesdays ago
+ 0 this Wednesday in 0 Wednesdays
+ 0 this Wednesday in 0 Wednesdays
+ -0 this Wednesday 0 Wednesdays ago
+ 0.5 in 0.5 Wednesdays in 0.5 Wednesdays
+ 1 next Wednesday in 1 Wednesday
+ 1.5 in 1.5 Wednesdays in 1.5 Wednesdays
+ 1.75 in 1.75 Wednesdays in 1.75 Wednesdays
+ 2 in 2 Wednesdays in 2 Wednesdays
+ 3 in 3 Wednesdays in 3 Wednesdays
+
+=== thursday ===================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Thursdays ago 3 Thursdays ago
+ -2 2 Thursdays ago 2 Thursdays ago
+ -1.75 1.75 Thursdays ago 1.75 Thursdays ago
+ -1.5 1.5 Thursdays ago 1.5 Thursdays ago
+ -1 last Thursday 1 Thursday ago
+ -0.5 0.5 Thursdays ago 0.5 Thursdays ago
+ 0 this Thursday in 0 Thursdays
+ 0 this Thursday in 0 Thursdays
+ -0 this Thursday 0 Thursdays ago
+ 0.5 in 0.5 Thursdays in 0.5 Thursdays
+ 1 next Thursday in 1 Thursday
+ 1.5 in 1.5 Thursdays in 1.5 Thursdays
+ 1.75 in 1.75 Thursdays in 1.75 Thursdays
+ 2 in 2 Thursdays in 2 Thursdays
+ 3 in 3 Thursdays in 3 Thursdays
+
+=== friday =====================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Fridays ago 3 Fridays ago
+ -2 2 Fridays ago 2 Fridays ago
+ -1.75 1.75 Fridays ago 1.75 Fridays ago
+ -1.5 1.5 Fridays ago 1.5 Fridays ago
+ -1 last Friday 1 Friday ago
+ -0.5 0.5 Fridays ago 0.5 Fridays ago
+ 0 this Friday in 0 Fridays
+ 0 this Friday in 0 Fridays
+ -0 this Friday 0 Fridays ago
+ 0.5 in 0.5 Fridays in 0.5 Fridays
+ 1 next Friday in 1 Friday
+ 1.5 in 1.5 Fridays in 1.5 Fridays
+ 1.75 in 1.75 Fridays in 1.75 Fridays
+ 2 in 2 Fridays in 2 Fridays
+ 3 in 3 Fridays in 3 Fridays
+
+=== saturday ===================================
+
+offset format formatNumeric
+------ ------------------- -------------------
+ -3 3 Saturdays ago 3 Saturdays ago
+ -2 2 Saturdays ago 2 Saturdays ago
+ -1.75 1.75 Saturdays ago 1.75 Saturdays ago
+ -1.5 1.5 Saturdays ago 1.5 Saturdays ago
+ -1 last Saturday 1 Saturday ago
+ -0.5 0.5 Saturdays ago 0.5 Saturdays ago
+ 0 this Saturday in 0 Saturdays
+ 0 this Saturday in 0 Saturdays
+ -0 this Saturday 0 Saturdays ago
+ 0.5 in 0.5 Saturdays in 0.5 Saturdays
+ 1 next Saturday in 1 Saturday
+ 1.5 in 1.5 Saturdays in 1.5 Saturdays
+ 1.75 in 1.75 Saturdays in 1.75 Saturdays
+ 2 in 2 Saturdays in 2 Saturdays
+ 3 in 3 Saturdays in 3 Saturdays
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..e72819bf56e
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - long style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+ new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Long),
+ offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+ units: [
+ 'day' => IntlRelativeDateTimeFormatterUnit::Day,
+ 'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+ 'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+ ],
+);
+
+?>
+--EXPECT--
+=== day ==================================
+
+offset format formatNumeric
+------ ---------------- ----------------
+ -2.75 vor 2,75 Tagen vor 2,75 Tagen
+ -1.75 vor 1,75 Tagen vor 1,75 Tagen
+ -1.5 vor 1,5 Tagen vor 1,5 Tagen
+ 1.5 in 1,5 Tagen in 1,5 Tagen
+ 1.75 in 1,75 Tagen in 1,75 Tagen
+ 2.75 in 2,75 Tagen in 2,75 Tagen
+
+=== hour =================================
+
+offset format formatNumeric
+------ ---------------- ----------------
+ -2.75 vor 2,75 Stunden vor 2,75 Stunden
+ -1.75 vor 1,75 Stunden vor 1,75 Stunden
+ -1.5 vor 1,5 Stunden vor 1,5 Stunden
+ 1.5 in 1,5 Stunden in 1,5 Stunden
+ 1.75 in 1,75 Stunden in 1,75 Stunden
+ 2.75 in 2,75 Stunden in 2,75 Stunden
+
+=== minute ===============================
+
+offset format formatNumeric
+------ ---------------- ----------------
+ -2.75 vor 2,75 Minuten vor 2,75 Minuten
+ -1.75 vor 1,75 Minuten vor 1,75 Minuten
+ -1.5 vor 1,5 Minuten vor 1,5 Minuten
+ 1.5 in 1,5 Minuten in 1,5 Minuten
+ 1.75 in 1,75 Minuten in 1,75 Minuten
+ 2.75 in 2,75 Minuten in 2,75 Minuten
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt
new file mode 100644
index 00000000000..3d3793ccca2
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - narrow style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+ die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Narrow);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =========================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3y ago 3y ago
+ -2 2y ago 2y ago
+ -1.75 1.75y ago 1.75y ago
+ -1.5 1.5y ago 1.5y ago
+ -1 last yr. 1y ago
+ -0.5 0.5y ago 0.5y ago
+ 0 this yr. in 0y
+ 0 this yr. in 0y
+ -0 this yr. 0y ago
+ 0.5 in 0.5y in 0.5y
+ 1 next yr. in 1y
+ 1.5 in 1.5y in 1.5y
+ 1.75 in 1.75y in 1.75y
+ 2 in 2y in 2y
+ 3 in 3y in 3y
+
+=== quarter ======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3q ago 3q ago
+ -2 2q ago 2q ago
+ -1.75 1.75q ago 1.75q ago
+ -1.5 1.5q ago 1.5q ago
+ -1 last qtr. 1q ago
+ -0.5 0.5q ago 0.5q ago
+ 0 this qtr. in 0q
+ 0 this qtr. in 0q
+ -0 this qtr. 0q ago
+ 0.5 in 0.5q in 0.5q
+ 1 next qtr. in 1q
+ 1.5 in 1.5q in 1.5q
+ 1.75 in 1.75q in 1.75q
+ 2 in 2q in 2q
+ 3 in 3q in 3q
+
+=== month ========================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3mo ago 3mo ago
+ -2 2mo ago 2mo ago
+ -1.75 1.75mo ago 1.75mo ago
+ -1.5 1.5mo ago 1.5mo ago
+ -1 last mo. 1mo ago
+ -0.5 0.5mo ago 0.5mo ago
+ 0 this mo. in 0mo
+ 0 this mo. in 0mo
+ -0 this mo. 0mo ago
+ 0.5 in 0.5mo in 0.5mo
+ 1 next mo. in 1mo
+ 1.5 in 1.5mo in 1.5mo
+ 1.75 in 1.75mo in 1.75mo
+ 2 in 2mo in 2mo
+ 3 in 3mo in 3mo
+
+=== week =========================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3w ago 3w ago
+ -2 2w ago 2w ago
+ -1.75 1.75w ago 1.75w ago
+ -1.5 1.5w ago 1.5w ago
+ -1 last wk. 1w ago
+ -0.5 0.5w ago 0.5w ago
+ 0 this wk. in 0w
+ 0 this wk. in 0w
+ -0 this wk. 0w ago
+ 0.5 in 0.5w in 0.5w
+ 1 next wk. in 1w
+ 1.5 in 1.5w in 1.5w
+ 1.75 in 1.75w in 1.75w
+ 2 in 2w in 2w
+ 3 in 3w in 3w
+
+=== day ==========================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3d ago 3d ago
+ -2 2d ago 2d ago
+ -1.75 1.75d ago 1.75d ago
+ -1.5 1.5d ago 1.5d ago
+ -1 yesterday 1d ago
+ -0.5 0.5d ago 0.5d ago
+ 0 today in 0d
+ 0 today in 0d
+ -0 today 0d ago
+ 0.5 in 0.5d in 0.5d
+ 1 tomorrow in 1d
+ 1.5 in 1.5d in 1.5d
+ 1.75 in 1.75d in 1.75d
+ 2 in 2d in 2d
+ 3 in 3d in 3d
+
+=== hour =========================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3h ago 3h ago
+ -2 2h ago 2h ago
+ -1.75 1.75h ago 1.75h ago
+ -1.5 1.5h ago 1.5h ago
+ -1 1h ago 1h ago
+ -0.5 0.5h ago 0.5h ago
+ 0 this hour in 0h
+ 0 this hour in 0h
+ -0 this hour 0h ago
+ 0.5 in 0.5h in 0.5h
+ 1 in 1h in 1h
+ 1.5 in 1.5h in 1.5h
+ 1.75 in 1.75h in 1.75h
+ 2 in 2h in 2h
+ 3 in 3h in 3h
+
+=== minute =======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3m ago 3m ago
+ -2 2m ago 2m ago
+ -1.75 1.75m ago 1.75m ago
+ -1.5 1.5m ago 1.5m ago
+ -1 1m ago 1m ago
+ -0.5 0.5m ago 0.5m ago
+ 0 this minute in 0m
+ 0 this minute in 0m
+ -0 this minute 0m ago
+ 0.5 in 0.5m in 0.5m
+ 1 in 1m in 1m
+ 1.5 in 1.5m in 1.5m
+ 1.75 in 1.75m in 1.75m
+ 2 in 2m in 2m
+ 3 in 3m in 3m
+
+=== second =======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3s ago 3s ago
+ -2 2s ago 2s ago
+ -1.75 1.75s ago 1.75s ago
+ -1.5 1.5s ago 1.5s ago
+ -1 1s ago 1s ago
+ -0.5 0.5s ago 0.5s ago
+ 0 now in 0s
+ 0 now in 0s
+ -0 now 0s ago
+ 0.5 in 0.5s in 0.5s
+ 1 in 1s in 1s
+ 1.5 in 1.5s in 1.5s
+ 1.75 in 1.75s in 1.75s
+ 2 in 2s in 2s
+ 3 in 3s in 3s
+
+=== sunday =======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 Su ago 3 Su ago
+ -2 2 Su ago 2 Su ago
+ -1.75 1.75 Su ago 1.75 Su ago
+ -1.5 1.5 Su ago 1.5 Su ago
+ -1 last Su 1 Su ago
+ -0.5 0.5 Su ago 0.5 Su ago
+ 0 this Su in 0 Su
+ 0 this Su in 0 Su
+ -0 this Su 0 Su ago
+ 0.5 in 0.5 Su in 0.5 Su
+ 1 next Su in 1 Su
+ 1.5 in 1.5 Su in 1.5 Su
+ 1.75 in 1.75 Su in 1.75 Su
+ 2 in 2 Su in 2 Su
+ 3 in 3 Su in 3 Su
+
+=== monday =======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 M ago 3 M ago
+ -2 2 M ago 2 M ago
+ -1.75 1.75 M ago 1.75 M ago
+ -1.5 1.5 M ago 1.5 M ago
+ -1 last M 1 M ago
+ -0.5 0.5 M ago 0.5 M ago
+ 0 this M in 0 M
+ 0 this M in 0 M
+ -0 this M 0 M ago
+ 0.5 in 0.5 M in 0.5 M
+ 1 next M in 1 M
+ 1.5 in 1.5 M in 1.5 M
+ 1.75 in 1.75 M in 1.75 M
+ 2 in 2 M in 2 M
+ 3 in 3 M in 3 M
+
+=== tuesday ======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 Tu ago 3 Tu ago
+ -2 2 Tu ago 2 Tu ago
+ -1.75 1.75 Tu ago 1.75 Tu ago
+ -1.5 1.5 Tu ago 1.5 Tu ago
+ -1 last Tu 1 Tu ago
+ -0.5 0.5 Tu ago 0.5 Tu ago
+ 0 this Tu in 0 Tu
+ 0 this Tu in 0 Tu
+ -0 this Tu 0 Tu ago
+ 0.5 in 0.5 Tu in 0.5 Tu
+ 1 next Tu in 1 Tu
+ 1.5 in 1.5 Tu in 1.5 Tu
+ 1.75 in 1.75 Tu in 1.75 Tu
+ 2 in 2 Tu in 2 Tu
+ 3 in 3 Tu in 3 Tu
+
+=== wednesday ====================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 W ago 3 W ago
+ -2 2 W ago 2 W ago
+ -1.75 1.75 W ago 1.75 W ago
+ -1.5 1.5 W ago 1.5 W ago
+ -1 last W 1 W ago
+ -0.5 0.5 W ago 0.5 W ago
+ 0 this W in 0 W
+ 0 this W in 0 W
+ -0 this W 0 W ago
+ 0.5 in 0.5 W in 0.5 W
+ 1 next W in 1 W
+ 1.5 in 1.5 W in 1.5 W
+ 1.75 in 1.75 W in 1.75 W
+ 2 in 2 W in 2 W
+ 3 in 3 W in 3 W
+
+=== thursday =====================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 Th ago 3 Th ago
+ -2 2 Th ago 2 Th ago
+ -1.75 1.75 Th ago 1.75 Th ago
+ -1.5 1.5 Th ago 1.5 Th ago
+ -1 last Th 1 Th ago
+ -0.5 0.5 Th ago 0.5 Th ago
+ 0 this Th in 0 Th
+ 0 this Th in 0 Th
+ -0 this Th 0 Th ago
+ 0.5 in 0.5 Th in 0.5 Th
+ 1 next Th in 1 Th
+ 1.5 in 1.5 Th in 1.5 Th
+ 1.75 in 1.75 Th in 1.75 Th
+ 2 in 2 Th in 2 Th
+ 3 in 3 Th in 3 Th
+
+=== friday =======================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 F ago 3 F ago
+ -2 2 F ago 2 F ago
+ -1.75 1.75 F ago 1.75 F ago
+ -1.5 1.5 F ago 1.5 F ago
+ -1 last F 1 F ago
+ -0.5 0.5 F ago 0.5 F ago
+ 0 this F in 0 F
+ 0 this F in 0 F
+ -0 this F 0 F ago
+ 0.5 in 0.5 F in 0.5 F
+ 1 next F in 1 F
+ 1.5 in 1.5 F in 1.5 F
+ 1.75 in 1.75 F in 1.75 F
+ 2 in 2 F in 2 F
+ 3 in 3 F in 3 F
+
+=== saturday =====================
+
+offset format formatNumeric
+------ ----------- -------------
+ -3 3 Sa ago 3 Sa ago
+ -2 2 Sa ago 2 Sa ago
+ -1.75 1.75 Sa ago 1.75 Sa ago
+ -1.5 1.5 Sa ago 1.5 Sa ago
+ -1 last Sa 1 Sa ago
+ -0.5 0.5 Sa ago 0.5 Sa ago
+ 0 this Sa in 0 Sa
+ 0 this Sa in 0 Sa
+ -0 this Sa 0 Sa ago
+ 0.5 in 0.5 Sa in 0.5 Sa
+ 1 next Sa in 1 Sa
+ 1.5 in 1.5 Sa in 1.5 Sa
+ 1.75 in 1.75 Sa in 1.75 Sa
+ 2 in 2 Sa in 2 Sa
+ 3 in 3 Sa in 3 Sa
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..fff2bd67395
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - narrow style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+ new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Narrow),
+ offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+ units: [
+ 'day' => IntlRelativeDateTimeFormatterUnit::Day,
+ 'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+ 'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+ ],
+);
+
+?>
+--EXPECT--
+=== day ==============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 Tagen vor 2,75 Tagen
+ -1.75 vor 1,75 Tagen vor 1,75 Tagen
+ -1.5 vor 1,5 Tagen vor 1,5 Tagen
+ 1.5 in 1,5 Tagen in 1,5 Tagen
+ 1.75 in 1,75 Tagen in 1,75 Tagen
+ 2.75 in 2,75 Tagen in 2,75 Tagen
+
+=== hour =============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 Std. vor 2,75 Std.
+ -1.75 vor 1,75 Std. vor 1,75 Std.
+ -1.5 vor 1,5 Std. vor 1,5 Std.
+ 1.5 in 1,5 Std. in 1,5 Std.
+ 1.75 in 1,75 Std. in 1,75 Std.
+ 2.75 in 2,75 Std. in 2,75 Std.
+
+=== minute ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 m vor 2,75 m
+ -1.75 vor 1,75 m vor 1,75 m
+ -1.5 vor 1,5 m vor 1,5 m
+ 1.5 in 1,5 m in 1,5 m
+ 1.75 in 1,75 m in 1,75 m
+ 2.75 in 2,75 m in 2,75 m
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt
new file mode 100644
index 00000000000..0873fe0cb1f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - short style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+ die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Short);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 yr. ago 3 yr. ago
+ -2 2 yr. ago 2 yr. ago
+ -1.75 1.75 yr. ago 1.75 yr. ago
+ -1.5 1.5 yr. ago 1.5 yr. ago
+ -1 last yr. 1 yr. ago
+ -0.5 0.5 yr. ago 0.5 yr. ago
+ 0 this yr. in 0 yr.
+ 0 this yr. in 0 yr.
+ -0 this yr. 0 yr. ago
+ 0.5 in 0.5 yr. in 0.5 yr.
+ 1 next yr. in 1 yr.
+ 1.5 in 1.5 yr. in 1.5 yr.
+ 1.75 in 1.75 yr. in 1.75 yr.
+ 2 in 2 yr. in 2 yr.
+ 3 in 3 yr. in 3 yr.
+
+=== quarter ==========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 qtrs. ago 3 qtrs. ago
+ -2 2 qtrs. ago 2 qtrs. ago
+ -1.75 1.75 qtrs. ago 1.75 qtrs. ago
+ -1.5 1.5 qtrs. ago 1.5 qtrs. ago
+ -1 last qtr. 1 qtr. ago
+ -0.5 0.5 qtrs. ago 0.5 qtrs. ago
+ 0 this qtr. in 0 qtrs.
+ 0 this qtr. in 0 qtrs.
+ -0 this qtr. 0 qtrs. ago
+ 0.5 in 0.5 qtrs. in 0.5 qtrs.
+ 1 next qtr. in 1 qtr.
+ 1.5 in 1.5 qtrs. in 1.5 qtrs.
+ 1.75 in 1.75 qtrs. in 1.75 qtrs.
+ 2 in 2 qtrs. in 2 qtrs.
+ 3 in 3 qtrs. in 3 qtrs.
+
+=== month ============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 mo. ago 3 mo. ago
+ -2 2 mo. ago 2 mo. ago
+ -1.75 1.75 mo. ago 1.75 mo. ago
+ -1.5 1.5 mo. ago 1.5 mo. ago
+ -1 last mo. 1 mo. ago
+ -0.5 0.5 mo. ago 0.5 mo. ago
+ 0 this mo. in 0 mo.
+ 0 this mo. in 0 mo.
+ -0 this mo. 0 mo. ago
+ 0.5 in 0.5 mo. in 0.5 mo.
+ 1 next mo. in 1 mo.
+ 1.5 in 1.5 mo. in 1.5 mo.
+ 1.75 in 1.75 mo. in 1.75 mo.
+ 2 in 2 mo. in 2 mo.
+ 3 in 3 mo. in 3 mo.
+
+=== week =============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 wk. ago 3 wk. ago
+ -2 2 wk. ago 2 wk. ago
+ -1.75 1.75 wk. ago 1.75 wk. ago
+ -1.5 1.5 wk. ago 1.5 wk. ago
+ -1 last wk. 1 wk. ago
+ -0.5 0.5 wk. ago 0.5 wk. ago
+ 0 this wk. in 0 wk.
+ 0 this wk. in 0 wk.
+ -0 this wk. 0 wk. ago
+ 0.5 in 0.5 wk. in 0.5 wk.
+ 1 next wk. in 1 wk.
+ 1.5 in 1.5 wk. in 1.5 wk.
+ 1.75 in 1.75 wk. in 1.75 wk.
+ 2 in 2 wk. in 2 wk.
+ 3 in 3 wk. in 3 wk.
+
+=== day ==============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 days ago 3 days ago
+ -2 2 days ago 2 days ago
+ -1.75 1.75 days ago 1.75 days ago
+ -1.5 1.5 days ago 1.5 days ago
+ -1 yesterday 1 day ago
+ -0.5 0.5 days ago 0.5 days ago
+ 0 today in 0 days
+ 0 today in 0 days
+ -0 today 0 days ago
+ 0.5 in 0.5 days in 0.5 days
+ 1 tomorrow in 1 day
+ 1.5 in 1.5 days in 1.5 days
+ 1.75 in 1.75 days in 1.75 days
+ 2 in 2 days in 2 days
+ 3 in 3 days in 3 days
+
+=== hour =============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 hr. ago 3 hr. ago
+ -2 2 hr. ago 2 hr. ago
+ -1.75 1.75 hr. ago 1.75 hr. ago
+ -1.5 1.5 hr. ago 1.5 hr. ago
+ -1 1 hr. ago 1 hr. ago
+ -0.5 0.5 hr. ago 0.5 hr. ago
+ 0 this hour in 0 hr.
+ 0 this hour in 0 hr.
+ -0 this hour 0 hr. ago
+ 0.5 in 0.5 hr. in 0.5 hr.
+ 1 in 1 hr. in 1 hr.
+ 1.5 in 1.5 hr. in 1.5 hr.
+ 1.75 in 1.75 hr. in 1.75 hr.
+ 2 in 2 hr. in 2 hr.
+ 3 in 3 hr. in 3 hr.
+
+=== minute ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 min. ago 3 min. ago
+ -2 2 min. ago 2 min. ago
+ -1.75 1.75 min. ago 1.75 min. ago
+ -1.5 1.5 min. ago 1.5 min. ago
+ -1 1 min. ago 1 min. ago
+ -0.5 0.5 min. ago 0.5 min. ago
+ 0 this minute in 0 min.
+ 0 this minute in 0 min.
+ -0 this minute 0 min. ago
+ 0.5 in 0.5 min. in 0.5 min.
+ 1 in 1 min. in 1 min.
+ 1.5 in 1.5 min. in 1.5 min.
+ 1.75 in 1.75 min. in 1.75 min.
+ 2 in 2 min. in 2 min.
+ 3 in 3 min. in 3 min.
+
+=== second ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 sec. ago 3 sec. ago
+ -2 2 sec. ago 2 sec. ago
+ -1.75 1.75 sec. ago 1.75 sec. ago
+ -1.5 1.5 sec. ago 1.5 sec. ago
+ -1 1 sec. ago 1 sec. ago
+ -0.5 0.5 sec. ago 0.5 sec. ago
+ 0 now in 0 sec.
+ 0 now in 0 sec.
+ -0 now 0 sec. ago
+ 0.5 in 0.5 sec. in 0.5 sec.
+ 1 in 1 sec. in 1 sec.
+ 1.5 in 1.5 sec. in 1.5 sec.
+ 1.75 in 1.75 sec. in 1.75 sec.
+ 2 in 2 sec. in 2 sec.
+ 3 in 3 sec. in 3 sec.
+
+=== sunday ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Sun. ago 3 Sun. ago
+ -2 2 Sun. ago 2 Sun. ago
+ -1.75 1.75 Sun. ago 1.75 Sun. ago
+ -1.5 1.5 Sun. ago 1.5 Sun. ago
+ -1 last Sun. 1 Sun. ago
+ -0.5 0.5 Sun. ago 0.5 Sun. ago
+ 0 this Sun. in 0 Sun.
+ 0 this Sun. in 0 Sun.
+ -0 this Sun. 0 Sun. ago
+ 0.5 in 0.5 Sun. in 0.5 Sun.
+ 1 next Sun. in 1 Sun.
+ 1.5 in 1.5 Sun. in 1.5 Sun.
+ 1.75 in 1.75 Sun. in 1.75 Sun.
+ 2 in 2 Sun. in 2 Sun.
+ 3 in 3 Sun. in 3 Sun.
+
+=== monday ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Mon. ago 3 Mon. ago
+ -2 2 Mon. ago 2 Mon. ago
+ -1.75 1.75 Mon. ago 1.75 Mon. ago
+ -1.5 1.5 Mon. ago 1.5 Mon. ago
+ -1 last Mon. 1 Mon. ago
+ -0.5 0.5 Mon. ago 0.5 Mon. ago
+ 0 this Mon. in 0 Mon.
+ 0 this Mon. in 0 Mon.
+ -0 this Mon. 0 Mon. ago
+ 0.5 in 0.5 Mon. in 0.5 Mon.
+ 1 next Mon. in 1 Mon.
+ 1.5 in 1.5 Mon. in 1.5 Mon.
+ 1.75 in 1.75 Mon. in 1.75 Mon.
+ 2 in 2 Mon. in 2 Mon.
+ 3 in 3 Mon. in 3 Mon.
+
+=== tuesday ==========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Tue. ago 3 Tue. ago
+ -2 2 Tue. ago 2 Tue. ago
+ -1.75 1.75 Tue. ago 1.75 Tue. ago
+ -1.5 1.5 Tue. ago 1.5 Tue. ago
+ -1 last Tue. 1 Tue. ago
+ -0.5 0.5 Tue. ago 0.5 Tue. ago
+ 0 this Tue. in 0 Tue.
+ 0 this Tue. in 0 Tue.
+ -0 this Tue. 0 Tue. ago
+ 0.5 in 0.5 Tue. in 0.5 Tue.
+ 1 next Tue. in 1 Tue.
+ 1.5 in 1.5 Tue. in 1.5 Tue.
+ 1.75 in 1.75 Tue. in 1.75 Tue.
+ 2 in 2 Tue. in 2 Tue.
+ 3 in 3 Tue. in 3 Tue.
+
+=== wednesday ========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Wed. ago 3 Wed. ago
+ -2 2 Wed. ago 2 Wed. ago
+ -1.75 1.75 Wed. ago 1.75 Wed. ago
+ -1.5 1.5 Wed. ago 1.5 Wed. ago
+ -1 last Wed. 1 Wed. ago
+ -0.5 0.5 Wed. ago 0.5 Wed. ago
+ 0 this Wed. in 0 Wed.
+ 0 this Wed. in 0 Wed.
+ -0 this Wed. 0 Wed. ago
+ 0.5 in 0.5 Wed. in 0.5 Wed.
+ 1 next Wed. in 1 Wed.
+ 1.5 in 1.5 Wed. in 1.5 Wed.
+ 1.75 in 1.75 Wed. in 1.75 Wed.
+ 2 in 2 Wed. in 2 Wed.
+ 3 in 3 Wed. in 3 Wed.
+
+=== thursday =========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Thu. ago 3 Thu. ago
+ -2 2 Thu. ago 2 Thu. ago
+ -1.75 1.75 Thu. ago 1.75 Thu. ago
+ -1.5 1.5 Thu. ago 1.5 Thu. ago
+ -1 last Thu. 1 Thu. ago
+ -0.5 0.5 Thu. ago 0.5 Thu. ago
+ 0 this Thu. in 0 Thu.
+ 0 this Thu. in 0 Thu.
+ -0 this Thu. 0 Thu. ago
+ 0.5 in 0.5 Thu. in 0.5 Thu.
+ 1 next Thu. in 1 Thu.
+ 1.5 in 1.5 Thu. in 1.5 Thu.
+ 1.75 in 1.75 Thu. in 1.75 Thu.
+ 2 in 2 Thu. in 2 Thu.
+ 3 in 3 Thu. in 3 Thu.
+
+=== friday ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Fri. ago 3 Fri. ago
+ -2 2 Fri. ago 2 Fri. ago
+ -1.75 1.75 Fri. ago 1.75 Fri. ago
+ -1.5 1.5 Fri. ago 1.5 Fri. ago
+ -1 last Fri. 1 Fri. ago
+ -0.5 0.5 Fri. ago 0.5 Fri. ago
+ 0 this Fri. in 0 Fri.
+ 0 this Fri. in 0 Fri.
+ -0 this Fri. 0 Fri. ago
+ 0.5 in 0.5 Fri. in 0.5 Fri.
+ 1 next Fri. in 1 Fri.
+ 1.5 in 1.5 Fri. in 1.5 Fri.
+ 1.75 in 1.75 Fri. in 1.75 Fri.
+ 2 in 2 Fri. in 2 Fri.
+ 3 in 3 Fri. in 3 Fri.
+
+=== saturday =========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -3 3 Sat. ago 3 Sat. ago
+ -2 2 Sat. ago 2 Sat. ago
+ -1.75 1.75 Sat. ago 1.75 Sat. ago
+ -1.5 1.5 Sat. ago 1.5 Sat. ago
+ -1 last Sat. 1 Sat. ago
+ -0.5 0.5 Sat. ago 0.5 Sat. ago
+ 0 this Sat. in 0 Sat.
+ 0 this Sat. in 0 Sat.
+ -0 this Sat. 0 Sat. ago
+ 0.5 in 0.5 Sat. in 0.5 Sat.
+ 1 next Sat. in 1 Sat.
+ 1.5 in 1.5 Sat. in 1.5 Sat.
+ 1.75 in 1.75 Sat. in 1.75 Sat.
+ 2 in 2 Sat. in 2 Sat.
+ 3 in 3 Sat. in 3 Sat.
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..a6575980a9f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - short style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+ new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Short),
+ offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+ units: [
+ 'day' => IntlRelativeDateTimeFormatterUnit::Day,
+ 'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+ 'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+ ],
+);
+
+?>
+--EXPECT--
+=== day ==============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 Tagen vor 2,75 Tagen
+ -1.75 vor 1,75 Tagen vor 1,75 Tagen
+ -1.5 vor 1,5 Tagen vor 1,5 Tagen
+ 1.5 in 1,5 Tagen in 1,5 Tagen
+ 1.75 in 1,75 Tagen in 1,75 Tagen
+ 2.75 in 2,75 Tagen in 2,75 Tagen
+
+=== hour =============================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 Std. vor 2,75 Std.
+ -1.75 vor 1,75 Std. vor 1,75 Std.
+ -1.5 vor 1,5 Std. vor 1,5 Std.
+ 1.5 in 1,5 Std. in 1,5 Std.
+ 1.75 in 1,75 Std. in 1,75 Std.
+ 2.75 in 2,75 Std. in 2,75 Std.
+
+=== minute ===========================
+
+offset format formatNumeric
+------ -------------- --------------
+ -2.75 vor 2,75 Min. vor 2,75 Min.
+ -1.75 vor 1,75 Min. vor 1,75 Min.
+ -1.5 vor 1,5 Min. vor 1,5 Min.
+ 1.5 in 1,5 Min. in 1,5 Min.
+ 1.75 in 1,75 Min. in 1,75 Min.
+ 2.75 in 2,75 Min. in 2,75 Min.
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_api.phpt b/ext/intl/tests/reldateformatter/reldateformatter_api.phpt
new file mode 100644
index 00000000000..8de80e95b0e
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_api.phpt
@@ -0,0 +1,72 @@
+--TEST--
+IntlRelativeDateTimeFormatter API surface
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$reflection = new ReflectionClass(IntlRelativeDateTimeFormatter::class);
+var_dump($reflection->isFinal());
+var_dump($reflection->isInternal());
+var_dump($reflection->isInstantiable());
+var_dump($reflection->getConstants());
+
+foreach ([
+ IntlRelativeDateTimeFormatterStyle::class,
+ IntlRelativeDateTimeFormatterCapitalization::class,
+ IntlRelativeDateTimeFormatterUnit::class,
+] as $class) {
+ $enum = new ReflectionEnum($class);
+ echo $enum->getName(), "\n";
+ var_dump($enum->isInternal(), $enum->isBacked());
+ echo implode(', ', array_column($class::cases(), 'name')), "\n";
+}
+
+$constructor = $reflection->getConstructor();
+foreach ($constructor->getParameters() as $parameter) {
+ echo $parameter->getName(), ': ', $parameter->getType(), "\n";
+ $default = $parameter->getDefaultValue();
+ echo 'default: ', $default instanceof UnitEnum ? $default::class . '::' . $default->name : 'null', "\n";
+}
+
+foreach (['format', 'formatNumeric'] as $name) {
+ $method = $reflection->getMethod($name);
+ echo $name, ': ', $method->getReturnType(), "\n";
+ foreach ($method->getParameters() as $parameter) {
+ echo $parameter->getName(), ': ', $parameter->getType(), "\n";
+ }
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+array(0) {
+}
+IntlRelativeDateTimeFormatterStyle
+bool(true)
+bool(false)
+Long, Short, Narrow
+IntlRelativeDateTimeFormatterCapitalization
+bool(true)
+bool(false)
+None, MiddleOfSentence, BeginningOfSentence, UiListAndMenu, Standalone
+IntlRelativeDateTimeFormatterUnit
+bool(true)
+bool(false)
+Year, Quarter, Month, Week, Day, Hour, Minute, Second, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
+locale: ?string
+default: null
+style: IntlRelativeDateTimeFormatterStyle
+default: IntlRelativeDateTimeFormatterStyle::Long
+capitalizationContext: IntlRelativeDateTimeFormatterCapitalization
+default: IntlRelativeDateTimeFormatterCapitalization::None
+numberFormatter: ?NumberFormatter
+default: null
+format: string|false
+offset: int|float
+unit: IntlRelativeDateTimeFormatterUnit
+formatNumeric: string|false
+offset: int|float
+unit: IntlRelativeDateTimeFormatterUnit
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt b/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt
new file mode 100644
index 00000000000..77647737086
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt
@@ -0,0 +1,40 @@
+--TEST--
+IntlRelativeDateTimeFormatter basic formatting
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(0, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Sunday), "\n";
+
+echo $formatter->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
+
+$relativeDate = $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day);
+echo $formatter->combineDateAndTime($relativeDate, '3:45 PM'), "\n";
+
+var_dump($formatter->getErrorCode());
+var_dump($formatter->getErrorMessage());
+
+?>
+--EXPECTF--
+yesterday
+today
+tomorrow
+last week
+in 2 weeks
+last Sunday
+1 day ago
+in 1 day
+in 1.5 hours
+yesterday%s3:45 PM
+int(0)
+string(12) "U_ZERO_ERROR"
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt b/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt
new file mode 100644
index 00000000000..c5187cc819b
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt
@@ -0,0 +1,46 @@
+--TEST--
+IntlRelativeDateTimeFormatter capitalization enums and NumberFormatter refresh
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+foreach (IntlRelativeDateTimeFormatterCapitalization::cases() as $context) {
+ $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
+ $formatter = new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Narrow,
+ $context,
+ $numberFormatter,
+ );
+ echo $context->name, ': ', $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+ echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+ $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1);
+ echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+ echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+}
+
+?>
+--EXPECTF--
+None: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+MiddleOfSentence: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+BeginningOfSentence: Yesterday
+In 2%r(?:d| days)%r
+Yesterday
+In 2.0%r(?:d| days)%r
+UiListAndMenu: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+Standalone: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt b/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt
new file mode 100644
index 00000000000..f68424df3e0
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt
@@ -0,0 +1,37 @@
+--TEST--
+IntlRelativeDateTimeFormatter sentence capitalization uses the locale and preserves existing case
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('tr', NumberFormatter::DECIMAL);
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, 'istanbul ABC ');
+$formatter = new IntlRelativeDateTimeFormatter(
+ 'tr',
+ IntlRelativeDateTimeFormatterStyle::Long,
+ IntlRelativeDateTimeFormatterCapitalization::BeginningOfSentence,
+ $numberFormatter,
+);
+
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, 'izmir XYZ ');
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+// Combining strings must not apply sentence capitalization.
+echo $formatter->combineDateAndTime('istanbul', '12:00'), "\n";
+
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, '');
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECT--
+Dün
+İstanbul ABC 3 gün sonra
+İstanbul ABC 2 gün sonra
+İzmir XYZ 2 gün sonra
+istanbul 12:00
+2 gün sonra
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt b/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt
new file mode 100644
index 00000000000..1dc23bd0349
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt
@@ -0,0 +1,35 @@
+--TEST--
+IntlRelativeDateTimeFormatter synchronizes and resets ICU error state
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+
+var_dump($formatter->combineDateAndTime("\x80", '3:45 PM'));
+var_dump($formatter->getErrorCode() === U_INVALID_CHAR_FOUND);
+var_dump(intl_get_error_code() === $formatter->getErrorCode());
+var_dump(str_contains($formatter->getErrorMessage(), 'Failed to convert relative date to UTF-16'));
+
+var_dump($formatter->format(1, IntlRelativeDateTimeFormatterUnit::Day));
+var_dump($formatter->getErrorCode());
+var_dump(intl_get_error_code());
+
+ini_set('intl.use_exceptions', '1');
+try {
+ $formatter->combineDateAndTime('today', "\x80");
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+string(8) "tomorrow"
+int(0)
+int(0)
+IntlException: IntlRelativeDateTimeFormatter::combineDateAndTime(): Failed to convert time to UTF-16
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt b/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt
new file mode 100644
index 00000000000..18c5dfb7552
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt
@@ -0,0 +1,38 @@
+--TEST--
+IntlRelativeDateTimeFormatter argument validation and object state errors
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+function showThrowable(Closure $callback): void {
+ try {
+ $callback();
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+ }
+}
+
+showThrowable(fn() => new IntlRelativeDateTimeFormatter('f'));
+showThrowable(fn() => new IntlRelativeDateTimeFormatter(str_repeat('a', 157)));
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+showThrowable(fn() => $formatter->__construct());
+showThrowable(fn() => clone $formatter);
+
+$unconstructedNumberFormatter = (new ReflectionClass(NumberFormatter::class))
+ ->newInstanceWithoutConstructor();
+showThrowable(fn() => new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Long,
+ IntlRelativeDateTimeFormatterCapitalization::None,
+ $unconstructedNumberFormatter,
+));
+
+?>
+--EXPECT--
+ValueError: IntlRelativeDateTimeFormatter::__construct(): Argument #1 ($locale) "f" is invalid
+ValueError: IntlRelativeDateTimeFormatter::__construct(): Argument #1 ($locale) must be less than or equal to 156 characters
+Error: IntlRelativeDateTimeFormatter object is already constructed
+Error: Trying to clone an uncloneable object of class IntlRelativeDateTimeFormatter
+Error: Found unconstructed NumberFormatter
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt b/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt
new file mode 100644
index 00000000000..e7ccedb3ef1
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt
@@ -0,0 +1,35 @@
+--TEST--
+IntlRelativeDateTimeFormatter returns false on number formatting failure
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (version_compare(INTL_ICU_VERSION, '62.1') < 0) die('skip ICU >= 62.1 required for number formatting errors'); ?>
+--FILE--
+<?php
+
+function createFailingFormatter(): IntlRelativeDateTimeFormatter
+{
+ $numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+ $numberFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
+ // ICU's UNUM_ROUND_UNNECESSARY is not exposed as a NumberFormatter constant.
+ $numberFormatter->setAttribute(NumberFormatter::ROUNDING_MODE, 7);
+
+ return new IntlRelativeDateTimeFormatter('en_US', numberFormatter: $numberFormatter);
+}
+
+foreach (['format', 'formatNumeric'] as $method) {
+ $formatter = createFailingFormatter();
+
+ var_dump($formatter->$method(1.5, IntlRelativeDateTimeFormatterUnit::Second));
+ var_dump(intl_error_name($formatter->getErrorCode()) === 'U_FORMAT_INEXACT_ERROR');
+ var_dump(intl_get_error_code() === $formatter->getErrorCode());
+}
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(true)
+bool(true)
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt b/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt
new file mode 100644
index 00000000000..89eb79f6fec
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt
@@ -0,0 +1,45 @@
+--TEST--
+IntlRelativeDateTimeFormatter locales, styles, and capitalization context
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$french = new IntlRelativeDateTimeFormatter('fr_FR');
+echo $french->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $french->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
+
+$chinese = new IntlRelativeDateTimeFormatter('zh_CN');
+echo $chinese->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Month), "\n";
+
+$beginning = new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Long,
+ IntlRelativeDateTimeFormatterCapitalization::BeginningOfSentence,
+);
+echo $beginning->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+$short = new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Short,
+);
+$narrow = new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Narrow,
+);
+echo $short->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $narrow->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+Locale::setDefault('en_US');
+$defaultLocale = new IntlRelativeDateTimeFormatter();
+echo $defaultLocale->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECTF--
+il y a 1 jour
+dans 1,5 heure
+3个月后
+Yesterday
+in 3 days
+in 3%r(?:d| days)%r
+tomorrow
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt
new file mode 100644
index 00000000000..d51fdbcda1f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt
@@ -0,0 +1,34 @@
+--TEST--
+IntlRelativeDateTimeFormatter observes changes to a custom NumberFormatter
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+$numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1);
+$formatter = new IntlRelativeDateTimeFormatter(
+ 'en_US',
+ IntlRelativeDateTimeFormatterStyle::Long,
+ IntlRelativeDateTimeFormatterCapitalization::None,
+ $numberFormatter,
+);
+
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+
+$numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
+
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+
+unset($numberFormatter);
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECT--
+in 2.0 days
+in 2.0 weeks
+in 2 days
+in 2 weeks
+in 2 days
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt
new file mode 100644
index 00000000000..64abfeaa4f1
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt
@@ -0,0 +1,29 @@
+--TEST--
+IntlRelativeDateTimeFormatter cycle management with a custom NumberFormatter
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+class TestNumberFormatter extends NumberFormatter
+{
+ public ?IntlRelativeDateTimeFormatter $formatter = null;
+}
+
+function collectFormatterCycle(): void
+{
+ $numberFormatter = new TestNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter = new IntlRelativeDateTimeFormatter('en', numberFormatter: $numberFormatter);
+ $numberFormatter->formatter = $formatter;
+
+ unset($numberFormatter, $formatter);
+ var_dump(gc_collect_cycles());
+}
+
+collectFormatterCycle();
+collectFormatterCycle();
+
+?>
+--EXPECT--
+int(2)
+int(2)
diff --git a/ext/standard/credits_ext.h b/ext/standard/credits_ext.h
index 45f8ce89c8d..08b0ac5b023 100644
--- a/ext/standard/credits_ext.h
+++ b/ext/standard/credits_ext.h
@@ -31,7 +31,7 @@ CREDIT_LINE("GetText", "Alex Plotnick");
CREDIT_LINE("GNU GMP support", "Stanislav Malyshev");
CREDIT_LINE("Iconv", "Rui Hirokawa, Stig Bakken, Moriyoshi Koizumi");
CREDIT_LINE("Input Filter", "Rasmus Lerdorf, Derick Rethans, Pierre-Alain Joye, Ilia Alshanetsky");
-CREDIT_LINE("Internationalization", "Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar");
+CREDIT_LINE("Internationalization", "Ed Batutis, David Carlier, Weilin Du, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar");
CREDIT_LINE("JSON", "Jakub Zelenka, Omar Kilani, Scott MacVicar");
CREDIT_LINE("LDAP", "Amitay Isaacs, Eric Warnke, Rasmus Lerdorf, Gerrit Thomson, Stig Venaas");
CREDIT_LINE("LIBXML", "Christian Stocker, Rob Richards, Marcus Boerger, Wez Furlong, Shane Caraveo");