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-22 12:01:54 +00:00
parent 3e9d035acd
commit 472da3bb00
8 changed files with 187 additions and 81 deletions

View File

@@ -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<char> 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));
}

View File

@@ -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<UniquePtr<DateIntervalFormat>, ICUError> DateIntervalFormat::TryCreate(
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() {
@@ -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 {

View File

@@ -17,6 +17,7 @@
namespace mozilla::intl {
class Calendar;
class DateTimeFormat;
using AutoFormattedDateInterval =
AutoFormattedResult<UFormattedDateInterval, udtitvfmt_openResult,
@@ -79,6 +80,21 @@ class DateIntervalFormat final {
AutoFormattedDateInterval& aFormatted,
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.
*

View File

@@ -279,6 +279,8 @@ Result<UniquePtr<DateTimeFormat>, ICUError> DateTimeFormat::TryCreateFromStyle(
return Err(ToICUError(status));
}
MOZ_TRY(ApplyCalendarOverride(dateFormat));
auto df = UniquePtr<DateTimeFormat>(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<int32_t>(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<DateTimeFormat>(new DateTimeFormat(dateFormat));
@@ -612,13 +615,6 @@ ICUResult DateTimeFormat::CacheSkeleton(Span<const char16_t> aSkeleton) {
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 */
Result<UniquePtr<Calendar>, ICUError> DateTimeFormat::CloneCalendar(
double aUnixEpoch) const {

View File

@@ -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.

View File

@@ -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 <cstring>
#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<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

View File

@@ -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<Ok, ICUError> ApplyCalendarOverride(UDateFormat* aDateFormat);
#if !MOZ_SYSTEM_ICU
Result<UniquePtr<icu::Calendar>, ICUError> CreateCalendarOverride(
const icu::Calendar* calendar);
#endif
} // namespace mozilla::intl
#endif

View File

@@ -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;
}