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
This commit is contained in:
André Bargull
2025-04-23 11:23:29 +00:00
parent 8e0c960614
commit c943966835
8 changed files with 187 additions and 81 deletions

View File

@@ -574,9 +574,6 @@ TEST(IntlDateTimeFormat, SetStartTimeIfGregorian)
auto timeZone = Some(MakeStringSpan(u"UTC")); 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 // 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. // before the default change date, in this case January 1, 1582.
constexpr double FirstJanuary1582 = -12244089600000.0; constexpr double FirstJanuary1582 = -12244089600000.0;
@@ -597,38 +594,25 @@ TEST(IntlDateTimeFormat, SetStartTimeIfGregorian)
MakeStringSpan(locale), style, gen.get(), timeZone) MakeStringSpan(locale), style, gen.get(), timeZone)
.unwrap(); .unwrap();
const char* Dec22_1581;
const char* Jan01_1582; const char* Jan01_1582;
const char* Jan01_1583; const char* Jan01_1583;
if (locale == "en-US-u-ca-iso8601"sv) { if (locale == "en-US-u-ca-iso8601"sv) {
Dec22_1581 = "1581 December 22";
Jan01_1582 = "1582 January 1"; Jan01_1582 = "1582 January 1";
Jan01_1583 = "1583 January 1"; Jan01_1583 = "1583 January 1";
} else { } else {
Dec22_1581 = "December 22, 1581";
Jan01_1582 = "January 1, 1582"; Jan01_1582 = "January 1, 1582";
Jan01_1583 = "January 1, 1583"; Jan01_1583 = "January 1, 1583";
} }
TestBuffer<char> buffer; TestBuffer<char> buffer;
// Before the default Gregorian change date, so interpreted in the Julian // Before the default Gregorian change date, but not interpreted in the
// calendar, which is December 22, 1581. // Julian calendar, which is December 22, 1581. Instead interpreted in
dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap(); // proleptic Gregorian calendar at January 1, 1582.
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.
dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap(); dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap();
ASSERT_TRUE(buffer.verboseMatches(Jan01_1582)); 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(); dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap();
ASSERT_TRUE(buffer.verboseMatches(Jan01_1583)); ASSERT_TRUE(buffer.verboseMatches(Jan01_1583));
} }

View File

@@ -2,12 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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 "DateTimeFormatUtils.h"
#include "ScopedICUObject.h" #include "ScopedICUObject.h"
#include "mozilla/intl/Calendar.h" #include "mozilla/intl/Calendar.h"
#include "mozilla/intl/DateIntervalFormat.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 { namespace mozilla::intl {
@@ -63,7 +69,23 @@ Result<UniquePtr<DateIntervalFormat>, ICUError> DateIntervalFormat::TryCreate(
return Err(ToICUError(status)); return Err(ToICUError(status));
} }
return UniquePtr<DateIntervalFormat>(new DateIntervalFormat(dif)); auto result = UniquePtr<DateIntervalFormat>(new DateIntervalFormat(dif));
#if !MOZ_SYSTEM_ICU
auto* dtif = reinterpret_cast<icu::DateIntervalFormat*>(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() { DateIntervalFormat::~DateIntervalFormat() {
@@ -133,6 +155,50 @@ ICUResult DateIntervalFormat::TryFormatDateTime(
return Ok(); 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( ICUResult DateIntervalFormat::TryFormattedToParts(
const AutoFormattedDateInterval& aFormatted, const AutoFormattedDateInterval& aFormatted,
DateTimePartVector& aParts) const { DateTimePartVector& aParts) const {

View File

@@ -17,6 +17,7 @@
namespace mozilla::intl { namespace mozilla::intl {
class Calendar; class Calendar;
class DateTimeFormat;
using AutoFormattedDateInterval = using AutoFormattedDateInterval =
AutoFormattedResult<UFormattedDateInterval, udtitvfmt_openResult, AutoFormattedResult<UFormattedDateInterval, udtitvfmt_openResult,
@@ -79,6 +80,21 @@ class DateIntervalFormat final {
AutoFormattedDateInterval& aFormatted, AutoFormattedDateInterval& aFormatted,
bool* aPracticallyEqual) const; bool* aPracticallyEqual) const;
/**
* Format a date-time range between two Unix epoch times in milliseconds.
*
* The result will be stored in aFormatted, caller can use
* AutoFormattedDateInterval::ToSpan() to get the formatted string, or pass
* the aFormatted to TryFormattedToParts to get the parts vector.
*
* aPracticallyEqual will be set to true if the date times of the two
* Unix epoch times are equal.
*/
ICUResult TryFormatDateTime(double aStart, double aEnd,
const DateTimeFormat* aDateTimeFormat,
AutoFormattedDateInterval& aFormatted,
bool* aPracticallyEqual) const;
/** /**
* Convert the formatted DateIntervalFormat into several parts. * Convert the formatted DateIntervalFormat into several parts.
* *

View File

@@ -279,6 +279,8 @@ Result<UniquePtr<DateTimeFormat>, ICUError> DateTimeFormat::TryCreateFromStyle(
return Err(ToICUError(status)); return Err(ToICUError(status));
} }
MOZ_TRY(ApplyCalendarOverride(dateFormat));
auto df = UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); auto df = UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat));
if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) { if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) {
@@ -564,11 +566,12 @@ DateTimeFormat::TryCreateFromPattern(
UDateFormat* dateFormat = udat_open( UDateFormat* dateFormat = udat_open(
UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength, UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength,
aPattern.data(), static_cast<int32_t>(aPattern.size()), &status); aPattern.data(), static_cast<int32_t>(aPattern.size()), &status);
if (U_FAILURE(status)) { if (U_FAILURE(status)) {
return Err(ToICUError(status)); return Err(ToICUError(status));
} }
MOZ_TRY(ApplyCalendarOverride(dateFormat));
// The DateTimeFormat wrapper will control the life cycle of the ICU // The DateTimeFormat wrapper will control the life cycle of the ICU
// dateFormat object. // dateFormat object.
return UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat)); return UniquePtr<DateTimeFormat>(new DateTimeFormat(dateFormat));
@@ -612,13 +615,6 @@ ICUResult DateTimeFormat::CacheSkeleton(Span<const char16_t> aSkeleton) {
return Err(ICUError::OutOfMemory); return Err(ICUError::OutOfMemory);
} }
void DateTimeFormat::SetStartTimeIfGregorian(double aTime) {
UErrorCode status = U_ZERO_ERROR;
UCalendar* cal = const_cast<UCalendar*>(udat_getCalendar(mDateFormat));
ucal_setGregorianChange(cal, aTime, &status);
// An error here means the calendar is not Gregorian, and can be ignored.
}
/* static */ /* static */
Result<UniquePtr<Calendar>, ICUError> DateTimeFormat::CloneCalendar( Result<UniquePtr<Calendar>, ICUError> DateTimeFormat::CloneCalendar(
double aUnixEpoch) const { double aUnixEpoch) const {

View File

@@ -477,12 +477,6 @@ class DateTimeFormat final {
} }
return Ok(); 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. * Determines the resolved components for the current DateTimeFormat.

View File

@@ -3,8 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/Assertions.h" #include "mozilla/Assertions.h"
#include "mozilla/Try.h"
#include "DateTimeFormatUtils.h" #include "DateTimeFormatUtils.h"
#include "mozilla/intl/ICU4CGlue.h"
#include <cstring>
#if !MOZ_SYSTEM_ICU
# include "unicode/datefmt.h"
# include "unicode/gregocal.h"
#endif
namespace mozilla::intl { namespace mozilla::intl {
@@ -101,4 +110,74 @@ DateTimePartType ConvertUFormatFieldToPartType(UDateFormatField fieldName) {
return DateTimePartType::Unknown; 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<Ok, ICUError> 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<Ok, ICUError> ApplyCalendarOverride(UDateFormat* aDateFormat) {
#if !MOZ_SYSTEM_ICU
icu::DateFormat* df = reinterpret_cast<icu::DateFormat*>(aDateFormat);
const icu::Calendar* calendar = df->getCalendar();
const char* type = calendar->getType();
if (IsGregorianLikeCalendar(type)) {
auto* gregorian = static_cast<const icu::GregorianCalendar*>(calendar);
MOZ_TRY(
SetGregorianChangeDate(const_cast<icu::GregorianCalendar*>(gregorian)));
}
#else
UErrorCode status = U_ZERO_ERROR;
UCalendar* cal = const_cast<UCalendar*>(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<UniquePtr<icu::Calendar>, ICUError> CreateCalendarOverride(
const icu::Calendar* calendar) {
const char* type = calendar->getType();
if (IsGregorianLikeCalendar(type)) {
UniquePtr<icu::GregorianCalendar> gregorian(
static_cast<const icu::GregorianCalendar*>(calendar)->clone());
if (!gregorian) {
return Err(ICUError::OutOfMemory);
}
MOZ_TRY(SetGregorianChangeDate(gregorian.get()));
return UniquePtr<icu::Calendar>{gregorian.release()};
}
return UniquePtr<icu::Calendar>{};
}
#endif
} // namespace mozilla::intl } // namespace mozilla::intl

View File

@@ -5,10 +5,24 @@
#define intl_components_DateTimeFormatUtils_h_ #define intl_components_DateTimeFormatUtils_h_
#include "unicode/udat.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/DateTimePart.h"
#include "mozilla/intl/ICUError.h"
namespace mozilla::intl { namespace mozilla::intl {
DateTimePartType ConvertUFormatFieldToPartType(UDateFormatField fieldName); DateTimePartType ConvertUFormatFieldToPartType(UDateFormatField fieldName);
Result<Ok, ICUError> ApplyCalendarOverride(UDateFormat* aDateFormat);
#if !MOZ_SYSTEM_ICU
Result<UniquePtr<icu::Calendar>, ICUError> CreateCalendarOverride(
const icu::Calendar* calendar);
#endif
} // namespace mozilla::intl } // namespace mozilla::intl
#endif #endif

View File

@@ -1638,10 +1638,6 @@ static mozilla::intl::DateTimeFormat* NewDateTimeFormat(
df = dfResult.unwrap(); df = dfResult.unwrap();
} }
// ECMAScript requires the Gregorian calendar to be used from the beginning
// of ECMAScript time.
df->SetStartTimeIfGregorian(StartOfTime);
return df.release(); return df.release();
} }
@@ -2578,51 +2574,12 @@ static bool PartitionDateTimeRangePattern(
MOZ_ASSERT(x.isValid()); MOZ_ASSERT(x.isValid());
MOZ_ASSERT(y.isValid()); MOZ_ASSERT(y.isValid());
// We can't access the calendar used by UDateIntervalFormat to change it to a auto result =
// proleptic Gregorian calendar. Instead we need to call a different formatter dif->TryFormatDateTime(x.toDouble(), y.toDouble(), df, formatted, equal);
// 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);
}
if (result.isErr()) { if (result.isErr()) {
intl::ReportInternalError(cx, result.unwrapErr()); intl::ReportInternalError(cx, result.unwrapErr());
return false; return false;
} }
return true; return true;
} }