From c943966835a5e373003d1ff8e2479b601d8992ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Wed, 23 Apr 2025 11:23:29 +0000 Subject: [PATCH] Bug 1954138 - Part 2: Set Gregorian change date when constructing formatters. r=dminor Always reset the Gregorian change date, because all uses either require the Gregorian change date to be set to `-8.64e15` or don't care about dates far into the past. The slow `DateIntervalFormat::TryFormatCalendar` is now only used when `MOZ_SYSTEM_ICU` is defined. And also resets the Gregorian change date for "buddhist", "japanese", and "roc" to get consistent results when compared to the ICU4X-based Temporal code. Differential Revision: https://phabricator.services.mozilla.com/D241644 --- intl/components/gtest/TestDateTimeFormat.cpp | 24 +----- intl/components/src/DateIntervalFormat.cpp | 70 ++++++++++++++++- intl/components/src/DateIntervalFormat.h | 16 ++++ intl/components/src/DateTimeFormat.cpp | 12 +-- intl/components/src/DateTimeFormat.h | 6 -- intl/components/src/DateTimeFormatUtils.cpp | 79 ++++++++++++++++++++ intl/components/src/DateTimeFormatUtils.h | 14 ++++ js/src/builtin/intl/DateTimeFormat.cpp | 47 +----------- 8 files changed, 187 insertions(+), 81 deletions(-) diff --git a/intl/components/gtest/TestDateTimeFormat.cpp b/intl/components/gtest/TestDateTimeFormat.cpp index 4a0b0788ed3d..3a7846522a8b 100644 --- a/intl/components/gtest/TestDateTimeFormat.cpp +++ b/intl/components/gtest/TestDateTimeFormat.cpp @@ -574,9 +574,6 @@ TEST(IntlDateTimeFormat, SetStartTimeIfGregorian) auto timeZone = Some(MakeStringSpan(u"UTC")); - // Beginning of ECMAScript time. - constexpr double StartOfTime = -8.64e15; - // Gregorian change date defaults to October 15, 1582 in ICU. Test with a date // before the default change date, in this case January 1, 1582. constexpr double FirstJanuary1582 = -12244089600000.0; @@ -597,38 +594,25 @@ TEST(IntlDateTimeFormat, SetStartTimeIfGregorian) MakeStringSpan(locale), style, gen.get(), timeZone) .unwrap(); - const char* Dec22_1581; const char* Jan01_1582; const char* Jan01_1583; if (locale == "en-US-u-ca-iso8601"sv) { - Dec22_1581 = "1581 December 22"; Jan01_1582 = "1582 January 1"; Jan01_1583 = "1583 January 1"; } else { - Dec22_1581 = "December 22, 1581"; Jan01_1582 = "January 1, 1582"; Jan01_1583 = "January 1, 1583"; } TestBuffer buffer; - // Before the default Gregorian change date, so interpreted in the Julian - // calendar, which is December 22, 1581. - dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap(); - ASSERT_TRUE(buffer.verboseMatches(Dec22_1581)); - - // After default Gregorian change date, so January 1, 1583. - dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap(); - ASSERT_TRUE(buffer.verboseMatches(Jan01_1583)); - - // Adjust the start time to use a proleptic Gregorian calendar. - dtFormat->SetStartTimeIfGregorian(StartOfTime); - - // Now interpreted in proleptic Gregorian calendar at January 1, 1582. + // Before the default Gregorian change date, but not interpreted in the + // Julian calendar, which is December 22, 1581. Instead interpreted in + // proleptic Gregorian calendar at January 1, 1582. dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap(); ASSERT_TRUE(buffer.verboseMatches(Jan01_1582)); - // Still January 1, 1583. + // After default Gregorian change date, so January 1, 1583. dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap(); ASSERT_TRUE(buffer.verboseMatches(Jan01_1583)); } diff --git a/intl/components/src/DateIntervalFormat.cpp b/intl/components/src/DateIntervalFormat.cpp index 0097668f8bc8..c09b34338974 100644 --- a/intl/components/src/DateIntervalFormat.cpp +++ b/intl/components/src/DateIntervalFormat.cpp @@ -2,12 +2,18 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "DateTimeFormat.h" // for DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES #include "DateTimeFormatUtils.h" #include "ScopedICUObject.h" #include "mozilla/intl/Calendar.h" #include "mozilla/intl/DateIntervalFormat.h" +#include "mozilla/intl/DateTimeFormat.h" + +#if !MOZ_SYSTEM_ICU +# include "unicode/calendar.h" +# include "unicode/datefmt.h" +# include "unicode/dtitvfmt.h" +#endif namespace mozilla::intl { @@ -63,7 +69,23 @@ Result, ICUError> DateIntervalFormat::TryCreate( return Err(ToICUError(status)); } - return UniquePtr(new DateIntervalFormat(dif)); + auto result = UniquePtr(new DateIntervalFormat(dif)); + +#if !MOZ_SYSTEM_ICU + auto* dtif = reinterpret_cast(dif); + const icu::Calendar* calendar = dtif->getDateFormat()->getCalendar(); + + auto replacement = CreateCalendarOverride(calendar); + if (replacement.isErr()) { + return replacement.propagateErr(); + } + + if (auto newCalendar = replacement.unwrap()) { + dtif->adoptCalendar(newCalendar.release()); + } +#endif + + return result; } DateIntervalFormat::~DateIntervalFormat() { @@ -133,6 +155,50 @@ ICUResult DateIntervalFormat::TryFormatDateTime( return Ok(); } +ICUResult DateIntervalFormat::TryFormatDateTime( + double aStart, double aEnd, const DateTimeFormat* aDateTimeFormat, + AutoFormattedDateInterval& aFormatted, bool* aPracticallyEqual) const { +#if MOZ_SYSTEM_ICU + // We can't access the calendar used by UDateIntervalFormat to change it to a + // proleptic Gregorian calendar. Instead we need to call a different formatter + // function which accepts UCalendar instead of UDate. + // But creating new UCalendar objects for each call is slow, so when we can + // ensure that the input dates are later than the Gregorian change date, + // directly call the formatter functions taking UDate. + + constexpr int32_t msPerDay = 24 * 60 * 60 * 1000; + + // The Gregorian change date "1582-10-15T00:00:00.000Z". + constexpr double GregorianChangeDate = -12219292800000.0; + + // Add a full day to account for time zone offsets. + constexpr double GregorianChangeDatePlusOneDay = + GregorianChangeDate + msPerDay; + + if (aStart < GregorianChangeDatePlusOneDay || + aEnd < GregorianChangeDatePlusOneDay) { + // Create calendar objects for the start and end date by cloning the date + // formatter calendar. The date formatter calendar already has the correct + // time zone set and was changed to use a proleptic Gregorian calendar. + auto startCal = aDateTimeFormat->CloneCalendar(aStart); + if (startCal.isErr()) { + return startCal.propagateErr(); + } + + auto endCal = aDateTimeFormat->CloneCalendar(aEnd); + if (endCal.isErr()) { + return endCal.propagateErr(); + } + + return TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(), aFormatted, + aPracticallyEqual); + } +#endif + + // The common fast path which doesn't require creating calendar objects. + return TryFormatDateTime(aStart, aEnd, aFormatted, aPracticallyEqual); +} + ICUResult DateIntervalFormat::TryFormattedToParts( const AutoFormattedDateInterval& aFormatted, DateTimePartVector& aParts) const { diff --git a/intl/components/src/DateIntervalFormat.h b/intl/components/src/DateIntervalFormat.h index c4dbce807a7d..aac9be9e46f5 100644 --- a/intl/components/src/DateIntervalFormat.h +++ b/intl/components/src/DateIntervalFormat.h @@ -17,6 +17,7 @@ namespace mozilla::intl { class Calendar; +class DateTimeFormat; using AutoFormattedDateInterval = AutoFormattedResult, ICUError> DateTimeFormat::TryCreateFromStyle( return Err(ToICUError(status)); } + MOZ_TRY(ApplyCalendarOverride(dateFormat)); + auto df = UniquePtr(new DateTimeFormat(dateFormat)); if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) { @@ -564,11 +566,12 @@ DateTimeFormat::TryCreateFromPattern( UDateFormat* dateFormat = udat_open( UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength, aPattern.data(), static_cast(aPattern.size()), &status); - if (U_FAILURE(status)) { return Err(ToICUError(status)); } + MOZ_TRY(ApplyCalendarOverride(dateFormat)); + // The DateTimeFormat wrapper will control the life cycle of the ICU // dateFormat object. return UniquePtr(new DateTimeFormat(dateFormat)); @@ -612,13 +615,6 @@ ICUResult DateTimeFormat::CacheSkeleton(Span aSkeleton) { return Err(ICUError::OutOfMemory); } -void DateTimeFormat::SetStartTimeIfGregorian(double aTime) { - UErrorCode status = U_ZERO_ERROR; - UCalendar* cal = const_cast(udat_getCalendar(mDateFormat)); - ucal_setGregorianChange(cal, aTime, &status); - // An error here means the calendar is not Gregorian, and can be ignored. -} - /* static */ Result, ICUError> DateTimeFormat::CloneCalendar( double aUnixEpoch) const { diff --git a/intl/components/src/DateTimeFormat.h b/intl/components/src/DateTimeFormat.h index 661ca6dfd1d9..4dda4a9006cc 100644 --- a/intl/components/src/DateTimeFormat.h +++ b/intl/components/src/DateTimeFormat.h @@ -477,12 +477,6 @@ class DateTimeFormat final { } return Ok(); } - /** - * Set the start time of the Gregorian calendar. This is useful for - * ensuring the consistent use of a proleptic Gregorian calendar for ECMA-402. - * https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar - */ - void SetStartTimeIfGregorian(double aTime); /** * Determines the resolved components for the current DateTimeFormat. diff --git a/intl/components/src/DateTimeFormatUtils.cpp b/intl/components/src/DateTimeFormatUtils.cpp index fd0649461e91..3ecddebf268e 100644 --- a/intl/components/src/DateTimeFormatUtils.cpp +++ b/intl/components/src/DateTimeFormatUtils.cpp @@ -3,8 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/Assertions.h" +#include "mozilla/Try.h" #include "DateTimeFormatUtils.h" +#include "mozilla/intl/ICU4CGlue.h" + +#include + +#if !MOZ_SYSTEM_ICU +# include "unicode/datefmt.h" +# include "unicode/gregocal.h" +#endif namespace mozilla::intl { @@ -101,4 +110,74 @@ DateTimePartType ConvertUFormatFieldToPartType(UDateFormatField fieldName) { return DateTimePartType::Unknown; } +// Start of ECMAScript time. +static constexpr double StartOfTime = -8.64e15; + +#if !MOZ_SYSTEM_ICU +static bool IsGregorianLikeCalendar(const char* type) { + return std::strcmp(type, "gregorian") == 0 || + std::strcmp(type, "iso8601") == 0 || + std::strcmp(type, "buddhist") == 0 || + std::strcmp(type, "japanese") == 0 || std::strcmp(type, "roc") == 0; +} + +/** + * Set the start time of the Gregorian calendar. This is useful for + * ensuring the consistent use of a proleptic Gregorian calendar for ECMA-402. + * https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar + */ +static Result SetGregorianChangeDate( + icu::GregorianCalendar* gregorian) { + UErrorCode status = U_ZERO_ERROR; + gregorian->setGregorianChange(StartOfTime, status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + return Ok{}; +} +#endif + +Result ApplyCalendarOverride(UDateFormat* aDateFormat) { +#if !MOZ_SYSTEM_ICU + icu::DateFormat* df = reinterpret_cast(aDateFormat); + const icu::Calendar* calendar = df->getCalendar(); + + const char* type = calendar->getType(); + + if (IsGregorianLikeCalendar(type)) { + auto* gregorian = static_cast(calendar); + MOZ_TRY( + SetGregorianChangeDate(const_cast(gregorian))); + } +#else + UErrorCode status = U_ZERO_ERROR; + UCalendar* cal = const_cast(udat_getCalendar(aDateFormat)); + ucal_setGregorianChange(cal, StartOfTime, &status); + // An error here means the calendar is not Gregorian, and can be ignored. +#endif + + return Ok{}; +} + +#if !MOZ_SYSTEM_ICU +Result, ICUError> CreateCalendarOverride( + const icu::Calendar* calendar) { + const char* type = calendar->getType(); + + if (IsGregorianLikeCalendar(type)) { + UniquePtr gregorian( + static_cast(calendar)->clone()); + if (!gregorian) { + return Err(ICUError::OutOfMemory); + } + + MOZ_TRY(SetGregorianChangeDate(gregorian.get())); + + return UniquePtr{gregorian.release()}; + } + + return UniquePtr{}; +} +#endif + } // namespace mozilla::intl diff --git a/intl/components/src/DateTimeFormatUtils.h b/intl/components/src/DateTimeFormatUtils.h index 89187b9871e5..51555b8f98e9 100644 --- a/intl/components/src/DateTimeFormatUtils.h +++ b/intl/components/src/DateTimeFormatUtils.h @@ -5,10 +5,24 @@ #define intl_components_DateTimeFormatUtils_h_ #include "unicode/udat.h" +#if !MOZ_SYSTEM_ICU +# include "unicode/calendar.h" +#endif + +#include "mozilla/Result.h" +#include "mozilla/UniquePtr.h" #include "mozilla/intl/DateTimePart.h" +#include "mozilla/intl/ICUError.h" namespace mozilla::intl { DateTimePartType ConvertUFormatFieldToPartType(UDateFormatField fieldName); + +Result ApplyCalendarOverride(UDateFormat* aDateFormat); + +#if !MOZ_SYSTEM_ICU +Result, ICUError> CreateCalendarOverride( + const icu::Calendar* calendar); +#endif } // namespace mozilla::intl #endif diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index d163e0bad31c..e27b9f081847 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -1638,10 +1638,6 @@ static mozilla::intl::DateTimeFormat* NewDateTimeFormat( df = dfResult.unwrap(); } - // ECMAScript requires the Gregorian calendar to be used from the beginning - // of ECMAScript time. - df->SetStartTimeIfGregorian(StartOfTime); - return df.release(); } @@ -2578,51 +2574,12 @@ static bool PartitionDateTimeRangePattern( MOZ_ASSERT(x.isValid()); MOZ_ASSERT(y.isValid()); - // We can't access the calendar used by UDateIntervalFormat to change it to a - // proleptic Gregorian calendar. Instead we need to call a different formatter - // function which accepts UCalendar instead of UDate. - // But creating new UCalendar objects for each call is slow, so when we can - // ensure that the input dates are later than the Gregorian change date, - // directly call the formatter functions taking UDate. - - // The Gregorian change date "1582-10-15T00:00:00.000Z". - constexpr double GregorianChangeDate = -12219292800000.0; - - // Add a full day to account for time zone offsets. - constexpr double GregorianChangeDatePlusOneDay = - GregorianChangeDate + msPerDay; - - mozilla::intl::ICUResult result = Ok(); - if (x.toDouble() < GregorianChangeDatePlusOneDay || - y.toDouble() < GregorianChangeDatePlusOneDay) { - // Create calendar objects for the start and end date by cloning the date - // formatter calendar. The date formatter calendar already has the correct - // time zone set and was changed to use a proleptic Gregorian calendar. - auto startCal = df->CloneCalendar(x.toDouble()); - if (startCal.isErr()) { - intl::ReportInternalError(cx, startCal.unwrapErr()); - return false; - } - - auto endCal = df->CloneCalendar(y.toDouble()); - if (endCal.isErr()) { - intl::ReportInternalError(cx, endCal.unwrapErr()); - return false; - } - - result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(), - formatted, equal); - } else { - // The common fast path which doesn't require creating calendar objects. - result = - dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal); - } - + auto result = + dif->TryFormatDateTime(x.toDouble(), y.toDouble(), df, formatted, equal); if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } - return true; }