diff --git a/icu4c/source/i18n/messageformat2_formattable.cpp b/icu4c/source/i18n/messageformat2_formattable.cpp index 4e2df49aecc..0bbecaf25d7 100644 --- a/icu4c/source/i18n/messageformat2_formattable.cpp +++ b/icu4c/source/i18n/messageformat2_formattable.cpp @@ -11,6 +11,8 @@ #include "unicode/messageformat2_formattable.h" #include "unicode/smpdtfmt.h" +#include "messageformat2_allocation.h" +#include "messageformat2_function_registry_internal.h" #include "messageformat2_macros.h" #include "limits.h" @@ -37,7 +39,6 @@ namespace message2 { Formattable::Formattable(const Formattable& other) { contents = other.contents; - holdsDate = other.holdsDate; } Formattable Formattable::forDecimal(std::string_view number, UErrorCode &status) { @@ -55,7 +56,7 @@ namespace message2 { UFormattableType Formattable::getType() const { if (std::holds_alternative(contents)) { - return holdsDate ? UFMT_DATE : UFMT_DOUBLE; + return UFMT_DOUBLE; } if (std::holds_alternative(contents)) { return UFMT_INT64; @@ -76,6 +77,9 @@ namespace message2 { } } } + if (isDate()) { + return UFMT_DATE; + } if (std::holds_alternative(contents)) { return UFMT_OBJECT; } @@ -225,14 +229,6 @@ namespace message2 { return df.orphan(); } - void formatDateWithDefaults(const Locale& locale, UDate date, UnicodeString& result, UErrorCode& errorCode) { - CHECK_ERROR(errorCode); - - LocalPointer df(defaultDateTimeInstance(locale, errorCode)); - CHECK_ERROR(errorCode); - df->format(date, result, 0, errorCode); - } - // Called when output is required and the contents are an unevaluated `Formattable`; // formats the source `Formattable` to a string with defaults, if it can be // formatted with a default formatter @@ -261,9 +257,9 @@ namespace message2 { switch (type) { case UFMT_DATE: { UnicodeString result; - UDate d = toFormat.getDate(status); + const DateInfo* dateInfo = toFormat.getDate(status); U_ASSERT(U_SUCCESS(status)); - formatDateWithDefaults(locale, d, result, status); + formatDateWithDefaults(locale, *dateInfo, result, status); return FormattedPlaceholder(input, FormattedValue(std::move(result))); } case UFMT_DOUBLE: { diff --git a/icu4c/source/i18n/messageformat2_function_registry.cpp b/icu4c/source/i18n/messageformat2_function_registry.cpp index d0e6bf62235..3e924ce9780 100644 --- a/icu4c/source/i18n/messageformat2_function_registry.cpp +++ b/icu4c/source/i18n/messageformat2_function_registry.cpp @@ -17,6 +17,7 @@ #include "unicode/messageformat2_data_model_names.h" #include "unicode/messageformat2_function_registry.h" #include "unicode/normalizer2.h" +#include "unicode/simpletz.h" #include "unicode/smpdtfmt.h" #include "charstr.h" #include "double-conversion.h" @@ -24,7 +25,9 @@ #include "messageformat2_function_registry_internal.h" #include "messageformat2_macros.h" #include "hash.h" +#include "mutex.h" #include "number_types.h" +#include "ucln_in.h" #include "uvector.h" // U_ASSERT // The C99 standard suggested that C++ implementations not define PRId64 etc. constants @@ -1136,6 +1139,213 @@ Formatter* StandardFunctions::DateTimeFactory::createFormatter(const Locale& loc return result; } +// DateFormat parsers that are shared across threads +static DateFormat* dateParser = nullptr; +static DateFormat* dateTimeParser = nullptr; +static DateFormat* dateTimeUTCParser = nullptr; +static DateFormat* dateTimeZoneParser = nullptr; +static icu::UInitOnce gMF2DateParsersInitOnce {}; + +// Clean up shared DateFormat objects +static UBool mf2_date_parsers_cleanup() { + if (dateParser != nullptr) { + delete dateParser; + dateParser = nullptr; + } + if (dateTimeParser != nullptr) { + delete dateTimeParser; + dateTimeParser = nullptr; + } + if (dateTimeUTCParser != nullptr) { + delete dateTimeUTCParser; + dateTimeUTCParser = nullptr; + } + if (dateTimeZoneParser != nullptr) { + delete dateTimeZoneParser; + dateTimeZoneParser = nullptr; + } + return true; +} + +// Initialize DateFormat objects used for parsing date literals +static void initDateParsersOnce(UErrorCode& errorCode) { + U_ASSERT(dateParser == nullptr); + U_ASSERT(dateTimeParser == nullptr); + U_ASSERT(dateTimeUTCParser == nullptr); + U_ASSERT(dateTimeZoneParser == nullptr); + + // Handles ISO 8601 date + dateParser = new SimpleDateFormat(UnicodeString("YYYY-MM-dd"), errorCode); + // Handles ISO 8601 datetime without time zone + dateTimeParser = new SimpleDateFormat(UnicodeString("YYYY-MM-dd'T'HH:mm:ss"), errorCode); + // Handles ISO 8601 datetime with 'Z' to denote UTC + dateTimeUTCParser = new SimpleDateFormat(UnicodeString("YYYY-MM-dd'T'HH:mm:ssZ"), errorCode); + // Handles ISO 8601 datetime with timezone offset; 'zzzz' denotes timezone offset + dateTimeZoneParser = new SimpleDateFormat(UnicodeString("YYYY-MM-dd'T'HH:mm:sszzzz"), errorCode); + + if (!dateParser || !dateTimeParser || !dateTimeUTCParser || !dateTimeZoneParser) { + errorCode = U_MEMORY_ALLOCATION_ERROR; + mf2_date_parsers_cleanup(); + return; + } + ucln_i18n_registerCleanup(UCLN_I18N_MF2_DATE_PARSERS, mf2_date_parsers_cleanup); +} + +// Lazily initialize DateFormat objects used for parsing date literals +static void initDateParsers(UErrorCode& errorCode) { + CHECK_ERROR(errorCode); + + umtx_initOnce(gMF2DateParsersInitOnce, &initDateParsersOnce, errorCode); +} + +// From https://github.com/unicode-org/message-format-wg/blob/main/spec/registry.md#date-and-time-operands : +// "A date/time literal value is a non-empty string consisting of an ISO 8601 date, or +// an ISO 8601 datetime optionally followed by a timezone offset." +UDate StandardFunctions::DateTime::tryPatterns(const UnicodeString& sourceStr, + UErrorCode& errorCode) const { + if (U_FAILURE(errorCode)) { + return 0; + } + // Handle ISO 8601 datetime (tryTimeZonePatterns() handles the case + // where a timezone offset follows) + if (sourceStr.length() > 10) { + return dateTimeParser->parse(sourceStr, errorCode); + } + // Handle ISO 8601 date + return dateParser->parse(sourceStr, errorCode); +} + +// See comment on tryPatterns() for spec reference +UDate StandardFunctions::DateTime::tryTimeZonePatterns(const UnicodeString& sourceStr, + UErrorCode& errorCode) const { + if (U_FAILURE(errorCode)) { + return 0; + } + int32_t len = sourceStr.length(); + if (len > 0 && sourceStr[len] == 'Z') { + return dateTimeUTCParser->parse(sourceStr, errorCode); + } + return dateTimeZoneParser->parse(sourceStr, errorCode); +} + +static TimeZone* createTimeZone(const DateInfo& dateInfo, UErrorCode& errorCode) { + NULL_ON_ERROR(errorCode); + + TimeZone* tz; + if (dateInfo.zoneId.isEmpty()) { + // Floating time value -- use default time zone + tz = TimeZone::createDefault(); + } else { + tz = TimeZone::createTimeZone(dateInfo.zoneId); + } + if (tz == nullptr) { + errorCode = U_MEMORY_ALLOCATION_ERROR; + } + return tz; +} + +// Returns true iff `sourceStr` ends in an offset like +03:30 or -06:00 +// (This function is just used to determine whether to call tryPatterns() +// or tryTimeZonePatterns(); tryTimeZonePatterns() checks fully that the +// string matches the expected format) +static bool hasTzOffset(const UnicodeString& sourceStr) { + int32_t len = sourceStr.length(); + + if (len <= 6) { + return false; + } + return ((sourceStr[len - 6] == PLUS || sourceStr[len - 6] == HYPHEN) + && sourceStr[len - 3] == COLON); +} + +// Note: `calendar` option to :datetime not implemented yet; +// Gregorian calendar is assumed +DateInfo StandardFunctions::DateTime::createDateInfoFromString(const UnicodeString& sourceStr, + UErrorCode& errorCode) const { + if (U_FAILURE(errorCode)) { + return {}; + } + + UDate absoluteDate; + + // Check if the string has a time zone part + int32_t indexOfZ = sourceStr.indexOf('Z'); + int32_t indexOfPlus = sourceStr.lastIndexOf('+'); + int32_t indexOfMinus = sourceStr.lastIndexOf('-'); + int32_t indexOfSign = indexOfPlus > -1 ? indexOfPlus : indexOfMinus; + bool isTzOffset = hasTzOffset(sourceStr); + bool isGMT = indexOfZ > 0; + UnicodeString offsetPart; + bool hasTimeZone = isTzOffset || isGMT; + + if (!hasTimeZone) { + // No time zone; parse the date and time + absoluteDate = tryPatterns(sourceStr, errorCode); + if (U_FAILURE(errorCode)) { + return {}; + } + } else { + // Try to split into time zone and non-time-zone parts + UnicodeString dateTimePart; + if (isGMT) { + dateTimePart = sourceStr.tempSubString(0, indexOfZ); + } else { + dateTimePart = sourceStr.tempSubString(0, indexOfSign); + } + // Parse the date from the date/time part + tryPatterns(dateTimePart, errorCode); + // Failure -- can't parse this string + if (U_FAILURE(errorCode)) { + return {}; + } + // Success -- now parse the time zone part + if (isGMT) { + dateTimePart += UnicodeString("GMT"); + absoluteDate = tryTimeZonePatterns(dateTimePart, errorCode); + if (U_FAILURE(errorCode)) { + return {}; + } + } else { + // Try to parse time zone in offset format: [+-]nn:nn + absoluteDate = tryTimeZonePatterns(sourceStr, errorCode); + if (U_FAILURE(errorCode)) { + return {}; + } + offsetPart = sourceStr.tempSubString(indexOfSign, sourceStr.length()); + } + } + + // If the time zone was provided, get its canonical ID, + // in order to return it in the DateInfo + UnicodeString canonicalID; + if (hasTimeZone) { + UnicodeString tzID("GMT"); + if (!isGMT) { + tzID += offsetPart; + } + TimeZone::getCanonicalID(tzID, canonicalID, errorCode); + if (U_FAILURE(errorCode)) { + return {}; + } + } + + return { absoluteDate, canonicalID }; +} + +void formatDateWithDefaults(const Locale& locale, + const DateInfo& dateInfo, + UnicodeString& result, + UErrorCode& errorCode) { + CHECK_ERROR(errorCode); + + LocalPointer df(defaultDateTimeInstance(locale, errorCode)); + CHECK_ERROR(errorCode); + + df->adoptTimeZone(createTimeZone(dateInfo, errorCode)); + CHECK_ERROR(errorCode); + df->format(dateInfo.date, result, nullptr, errorCode); +} + FormattedPlaceholder StandardFunctions::DateTime::format(FormattedPlaceholder&& toFormat, FunctionOptions&& opts, UErrorCode& errorCode) const { @@ -1322,44 +1532,41 @@ FormattedPlaceholder StandardFunctions::DateTime::format(FormattedPlaceholder&& const Formattable& source = toFormat.asFormattable(); switch (source.getType()) { case UFMT_STRING: { + // Lazily initialize date parsers used for parsing date literals + initDateParsers(errorCode); + if (U_FAILURE(errorCode)) { + return {}; + } + const UnicodeString& sourceStr = source.getString(errorCode); U_ASSERT(U_SUCCESS(errorCode)); - // Pattern for ISO 8601 format - datetime - UnicodeString pattern("YYYY-MM-dd'T'HH:mm:ss"); - LocalPointer dateParser(new SimpleDateFormat(pattern, errorCode)); + + DateInfo dateInfo = createDateInfoFromString(sourceStr, errorCode); if (U_FAILURE(errorCode)) { - errorCode = U_MF_FORMATTING_ERROR; - } else { - // Parse the date - UDate d = dateParser->parse(sourceStr, errorCode); - if (U_FAILURE(errorCode)) { - // Pattern for ISO 8601 format - date - UnicodeString pattern("YYYY-MM-dd"); - errorCode = U_ZERO_ERROR; - dateParser.adoptInstead(new SimpleDateFormat(pattern, errorCode)); - if (U_FAILURE(errorCode)) { - errorCode = U_MF_FORMATTING_ERROR; - } else { - d = dateParser->parse(sourceStr, errorCode); - if (U_FAILURE(errorCode)) { - errorCode = U_MF_OPERAND_MISMATCH_ERROR; - } - } - } - // Use the parsed date as the source value - // in the returned FormattedPlaceholder; this is necessary - // so the date can be re-formatted - toFormat = FormattedPlaceholder(message2::Formattable::forDate(d), - toFormat.getFallback()); - df->format(d, result, 0, errorCode); + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + return {}; } + df->adoptTimeZone(createTimeZone(dateInfo, errorCode)); + + // Use the parsed date as the source value + // in the returned FormattedPlaceholder; this is necessary + // so the date can be re-formatted + df->format(dateInfo.date, result, 0, errorCode); + toFormat = FormattedPlaceholder(message2::Formattable(std::move(dateInfo)), + toFormat.getFallback()); break; } case UFMT_DATE: { - df->format(source.asICUFormattable(errorCode), result, 0, errorCode); - if (U_FAILURE(errorCode)) { - if (errorCode == U_ILLEGAL_ARGUMENT_ERROR) { - errorCode = U_MF_OPERAND_MISMATCH_ERROR; + const DateInfo* dateInfo = source.getDate(errorCode); + if (U_SUCCESS(errorCode)) { + // If U_SUCCESS(errorCode), then source.getDate() returned + // a non-null pointer + df->adoptTimeZone(createTimeZone(*dateInfo, errorCode)); + df->format(dateInfo->date, result, 0, errorCode); + if (U_FAILURE(errorCode)) { + if (errorCode == U_ILLEGAL_ARGUMENT_ERROR) { + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + } } } break; diff --git a/icu4c/source/i18n/messageformat2_function_registry_internal.h b/icu4c/source/i18n/messageformat2_function_registry_internal.h index aef41ce383c..25d2a467502 100644 --- a/icu4c/source/i18n/messageformat2_function_registry_internal.h +++ b/icu4c/source/i18n/messageformat2_function_registry_internal.h @@ -125,9 +125,15 @@ static constexpr std::u16string_view YEAR = u"year"; const Locale& locale; const DateTimeFactory::DateTimeType type; friend class DateTimeFactory; - DateTime(const Locale& l, DateTimeFactory::DateTimeType t) : locale(l), type(t) {} + DateTime(const Locale& l, DateTimeFactory::DateTimeType t) + : locale(l), type(t) {} const LocalPointer icuFormatter; + // Methods for parsing date literals + UDate tryPatterns(const UnicodeString&, UErrorCode&) const; + UDate tryTimeZonePatterns(const UnicodeString&, UErrorCode&) const; + DateInfo createDateInfoFromString(const UnicodeString&, UErrorCode&) const; + /* Looks up an option by name, first checking `opts`, then the cached options in `toFormat` if applicable, and finally using a default @@ -322,7 +328,7 @@ static constexpr std::u16string_view YEAR = u"year"; }; - extern void formatDateWithDefaults(const Locale& locale, UDate date, UnicodeString&, UErrorCode& errorCode); + extern void formatDateWithDefaults(const Locale& locale, const DateInfo& date, UnicodeString&, UErrorCode& errorCode); extern number::FormattedNumber formatNumberWithDefaults(const Locale& locale, double toFormat, UErrorCode& errorCode); extern number::FormattedNumber formatNumberWithDefaults(const Locale& locale, int32_t toFormat, UErrorCode& errorCode); extern number::FormattedNumber formatNumberWithDefaults(const Locale& locale, int64_t toFormat, UErrorCode& errorCode); diff --git a/icu4c/source/i18n/tznames.cpp b/icu4c/source/i18n/tznames.cpp index 48e043a3db2..87fc40bfdfd 100644 --- a/icu4c/source/i18n/tznames.cpp +++ b/icu4c/source/i18n/tznames.cpp @@ -72,8 +72,12 @@ static UBool U_CALLCONV timeZoneNames_cleanup() static void U_CALLCONV deleteTimeZoneNamesCacheEntry(void *obj) { icu::TimeZoneNamesCacheEntry *entry = (icu::TimeZoneNamesCacheEntry*)obj; - delete (icu::TimeZoneNamesImpl*) entry->names; - uprv_free(entry); + if (entry->refCount <= 1) { + delete (icu::TimeZoneNamesImpl*) entry->names; + uprv_free(entry); + } else { + entry->refCount--; + } } U_CDECL_END @@ -175,7 +179,9 @@ TimeZoneNamesDelegate::TimeZoneNamesDelegate(const Locale& locale, UErrorCode& s status = U_MEMORY_ALLOCATION_ERROR; } else { cacheEntry->names = tznames; - cacheEntry->refCount = 1; + // The initial refCount is 2 because the entry is referenced both + // by this TimeZoneDelegate and by the gTimeZoneNamesCache + cacheEntry->refCount = 2; cacheEntry->lastAccess = static_cast(uprv_getUTCtime()); uhash_put(gTimeZoneNamesCache, newKey, cacheEntry, &status); @@ -209,9 +215,13 @@ TimeZoneNamesDelegate::~TimeZoneNamesDelegate() { umtx_lock(&gTimeZoneNamesLock); { if (fTZnamesCacheEntry) { - U_ASSERT(fTZnamesCacheEntry->refCount > 0); - // Just decrement the reference count - fTZnamesCacheEntry->refCount--; + if (fTZnamesCacheEntry->refCount <= 1) { + delete fTZnamesCacheEntry->names; + uprv_free(fTZnamesCacheEntry); + } else { + // Just decrement the reference count + fTZnamesCacheEntry->refCount--; + } } } umtx_unlock(&gTimeZoneNamesLock); diff --git a/icu4c/source/i18n/ucln_in.h b/icu4c/source/i18n/ucln_in.h index 74868891c83..722a1aaf569 100644 --- a/icu4c/source/i18n/ucln_in.h +++ b/icu4c/source/i18n/ucln_in.h @@ -64,6 +64,7 @@ typedef enum ECleanupI18NType { UCLN_I18N_LIST_FORMATTER, UCLN_I18N_NUMSYS, UCLN_I18N_MF2_UNISETS, + UCLN_I18N_MF2_DATE_PARSERS, UCLN_I18N_COUNT /* This must be last */ } ECleanupI18NType; diff --git a/icu4c/source/i18n/unicode/messageformat2_formattable.h b/icu4c/source/i18n/unicode/messageformat2_formattable.h index 90648bf1d3a..c3dee723656 100644 --- a/icu4c/source/i18n/unicode/messageformat2_formattable.h +++ b/icu4c/source/i18n/unicode/messageformat2_formattable.h @@ -17,6 +17,7 @@ #include "unicode/chariter.h" #include "unicode/numberformatter.h" #include "unicode/messageformat2_data_model_names.h" +#include "unicode/smpdtfmt.h" #ifndef U_HIDE_DEPRECATED_API @@ -66,6 +67,34 @@ namespace message2 { virtual ~FormattableObject(); }; // class FormattableObject + /** + * The `DateInfo` struct represents all the information needed to + * format a date with a time zone. It includes an absolute date and a time zone name, + * as well as a calendar name. The calendar name is not currently used. + * + * @internal ICU 76 technology preview + * @deprecated This API is for technology preview only. + */ + struct U_I18N_API DateInfo { + /** + * Date in UTC + * + * @internal ICU 76 technology preview + * @deprecated This API is for technology preview only. + */ + UDate date; + /** + * IANA time zone name; "UTC" if UTC; empty string if value is floating + * The time zone is required in order to format the date/time value + * (its offset is added to/subtracted from the datestamp in order to + * produce the formatted date). + * + * @internal ICU 76 technology preview + * @deprecated This API is for technology preview only. + */ + UnicodeString zoneId; + }; + class Formattable; } // namespace message2 @@ -84,6 +113,7 @@ template class U_I18N_API std::_Variant_storage_>; #endif @@ -92,6 +122,7 @@ template class U_I18N_API std::variant; #endif @@ -100,6 +131,7 @@ template class U_I18N_API std::variant(&contents); + return std::get_if(&contents); } status = U_ILLEGAL_ARGUMENT_ERROR; } - return 0; + return nullptr; } /** @@ -301,7 +335,6 @@ namespace message2 { using std::swap; swap(f1.contents, f2.contents); - swap(f1.holdsDate, f2.holdsDate); } /** * Copy constructor. @@ -353,18 +386,15 @@ namespace message2 { */ Formattable(int64_t i) : contents(i) {} /** - * Date factory method. + * Date constructor. * - * @param d A UDate value to wrap as a Formattable. + * @param d A DateInfo struct representing a date, + * to wrap as a Formattable. + * Passed by move * @internal ICU 75 technology preview * @deprecated This API is for technology preview only. */ - static Formattable forDate(UDate d) { - Formattable f; - f.contents = d; - f.holdsDate = true; - return f; - } + Formattable(DateInfo&& d) : contents(std::move(d)) {} /** * Creates a Formattable object of an appropriate numeric type from a * a decimal number in string form. The Formattable will retain the @@ -424,16 +454,16 @@ namespace message2 { int64_t, UnicodeString, icu::Formattable, // represents a Decimal + DateInfo, const FormattableObject*, std::pair> contents; - bool holdsDate = false; // otherwise, we get type errors about UDate being a duplicate type UnicodeString bogusString; // :(((( UBool isDecimal() const { return std::holds_alternative(contents); } UBool isDate() const { - return std::holds_alternative(contents) && holdsDate; + return std::holds_alternative(contents); } }; // class Formattable diff --git a/icu4c/source/test/intltest/messageformat2test.cpp b/icu4c/source/test/intltest/messageformat2test.cpp index 5317d2c3585..531d7181d51 100644 --- a/icu4c/source/test/intltest/messageformat2test.cpp +++ b/icu4c/source/test/intltest/messageformat2test.cpp @@ -8,7 +8,7 @@ #if !UCONFIG_NO_MF2 -#include "unicode/calendar.h" +#include "unicode/gregocal.h" #include "messageformat2test.h" using namespace icu::message2; @@ -158,13 +158,14 @@ void TestMessageFormat2::testAPISimple() { .setLocale(locale) .build(errorCode); - Calendar* cal(Calendar::createInstance(errorCode)); + GregorianCalendar cal(errorCode); // Sunday, October 28, 2136 8:39:12 AM PST - cal->set(2136, Calendar::OCTOBER, 28, 8, 39, 12); - UDate date = cal->getTime(errorCode); + cal.set(2136, Calendar::OCTOBER, 28, 8, 39, 12); argsBuilder.clear(); - argsBuilder["today"] = message2::Formattable::forDate(date); + DateInfo dateInfo = { cal.getTime(errorCode), + "Pacific Standard Time" }; + argsBuilder["today"] = message2::Formattable(std::move(dateInfo)); args = MessageArguments(argsBuilder, errorCode); result = mf.formatToString(args, errorCode); assertEquals("testAPI", "Today is Sunday, October 28, 2136.", result); @@ -187,8 +188,6 @@ void TestMessageFormat2::testAPISimple() { .build(errorCode); result = mf.formatToString(args, errorCode); assertEquals("testAPI", "Maria added 12 photos to her album.", result); - - delete cal; } // Design doc example, with more details diff --git a/icu4c/source/test/intltest/messageformat2test_utils.h b/icu4c/source/test/intltest/messageformat2test_utils.h index 7d6f813ff59..47e96022851 100644 --- a/icu4c/source/test/intltest/messageformat2test_utils.h +++ b/icu4c/source/test/intltest/messageformat2test_utils.h @@ -113,7 +113,12 @@ class TestCase : public UMemory { return *this; } Builder& setDateArgument(const UnicodeString& k, UDate date) { - arguments[k] = Formattable::forDate(date); + // This ignores time zones; the data-driven tests represent date/time values + // as a datestamp, so this code suffices to handle those. + // Date/time literal strings would be handled using `setArgument()` with a string + // argument. + DateInfo dateInfo = { date, {} }; // No time zone or calendar name + arguments[k] = Formattable(std::move(dateInfo)); return *this; } Builder& setDecimalArgument(const UnicodeString& k, std::string_view decimal, UErrorCode& errorCode) { diff --git a/icu4c/source/test/testdata/message2/icu4j/icu-test-functions.json b/icu4c/source/test/testdata/message2/icu4j/icu-test-functions.json index 6d78ffe4f04..21f917da6e5 100644 --- a/icu4c/source/test/testdata/message2/icu4j/icu-test-functions.json +++ b/icu4c/source/test/testdata/message2/icu4j/icu-test-functions.json @@ -108,13 +108,11 @@ }, { "src": "Expires at {|2024-07-02T19:23:45Z| :datetime timeStyle=long}", - "exp": "Expires at 7:23:45 PM GMT", - "ignoreTest": "ICU-22754 Time zones not working yet (bug)" + "exp": "Expires at 7:23:45 PM GMT" }, { "src": "Expires at {|2024-07-02T19:23:45+03:30| :datetime timeStyle=full}", - "exp": "Expires at 7:23:45 PM GMT+03:30", - "ignoreTest": "ICU-22754 Time zones not working yet (bug)" + "exp": "Expires at 7:23:45 PM GMT+03:30" } ], "Chaining" : [ diff --git a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/message2/more-functions.json b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/message2/more-functions.json index b34803635ce..b785d9988db 100644 --- a/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/message2/more-functions.json +++ b/icu4j/main/core/src/test/resources/com/ibm/icu/dev/test/message2/more-functions.json @@ -113,5 +113,29 @@ "locale": "ro", "params": [{ "name": "val", "value": {"decimal": "1234567890123456789.987654321"} }] } + ], + "datetime_extra": [ + { + "srcs": [".local $d = {|2024-07-02T19:23:45Z| :datetime timeStyle=long}\n", + "{{Expires at {$d} or {$d :datetime timeStyle=full}}}"], + "exp": "Expires at 7:23:45\u202FPM GMT or 7:23:45\u202FPM Greenwich Mean Time", + "comment": "This checks that the return value of :datetime preserves the time zone offset if specified" + }, + { + "srcs": [".local $d = {|2024-07-02T19:23:45+03:30| :datetime timeStyle=long}\n", + "{{Expires at {$d} or {$d :datetime timeStyle=full}}}"], + "exp": "Expires at 7:23:45\u202FPM GMT+3:30 or 7:23:45\u202FPM GMT+03:30", + "comment": "This checks that the return value of :datetime preserves the time zone offset if specified" + }, + { + "src": "{$d :datetime timeStyle=full}", + "exp": "7:23:45\u202FPM GMT+03:30", + "params": {"d": "2024-07-02T19:23:45+03:30"}, + "comment": "This checks that an argument can be a date literal string." + }, + { + "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long} / {|2006-01-03| :datetime dateStyle=medium}", + "exp": "January 2, 2006 / Jan 3, 2006" + } ] }