Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/js/src/builtin/temporal/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 123 kB image not shown  

Quelle  Calendar.cpp   Sprache: C

 
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * 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 "builtin/temporal/Calendar.h"

#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/Casting.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/EnumSet.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/intl/ICU4XGeckoDataProvider.h"
#include "mozilla/intl/Locale.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Maybe.h"
#include "mozilla/Result.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/Span.h"
#include "mozilla/TextUtils.h"
#include "mozilla/UniquePtr.h"

#include <algorithm>
#include <array>
#include <cmath>
#include <initializer_list>
#include <iterator>
#include <stddef.h>
#include <stdint.h>
#include <utility>

#include "diplomat_runtime.h"
#include "ICU4XAnyCalendarKind.h"
#include "ICU4XCalendar.h"
#include "ICU4XDate.h"
#include "ICU4XError.h"
#include "ICU4XIsoDate.h"
#include "ICU4XIsoWeekday.h"
#include "ICU4XWeekCalculator.h"
#include "ICU4XWeekRelativeUnit.h"

#include "jsnum.h"
#include "jstypes.h"
#include "NamespaceImports.h"

#include "builtin/temporal/CalendarFields.h"
#include "builtin/temporal/Crash.h"
#include "builtin/temporal/Duration.h"
#include "builtin/temporal/Era.h"
#include "builtin/temporal/MonthCode.h"
#include "builtin/temporal/PlainDate.h"
#include "builtin/temporal/PlainDateTime.h"
#include "builtin/temporal/PlainMonthDay.h"
#include "builtin/temporal/PlainTime.h"
#include "builtin/temporal/PlainYearMonth.h"
#include "builtin/temporal/Temporal.h"
#include "builtin/temporal/TemporalParser.h"
#include "builtin/temporal/TemporalRoundingMode.h"
#include "builtin/temporal/TemporalTypes.h"
#include "builtin/temporal/TemporalUnit.h"
#include "builtin/temporal/ZonedDateTime.h"
#include "gc/Barrier.h"
#include "gc/GCEnum.h"
#include "js/AllocPolicy.h"
#include "js/ErrorReport.h"
#include "js/friend/ErrorMessages.h"
#include "js/Printer.h"
#include "js/RootingAPI.h"
#include "js/TracingAPI.h"
#include "js/Value.h"
#include "js/Vector.h"
#include "util/Text.h"
#include "vm/BytecodeUtil.h"
#include "vm/Compartment.h"
#include "vm/JSAtomState.h"
#include "vm/JSContext.h"
#include "vm/StringType.h"

#include "vm/Compartment-inl.h"
#include "vm/JSContext-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/ObjectOperations-inl.h"

using namespace js;
using namespace js::temporal;

void js::temporal::CalendarValue::trace(JSTracer* trc) {
  TraceRoot(trc, &value_, "CalendarValue::value");
}

bool js::temporal::WrapCalendarValue(JSContext* cx,
                                     MutableHandle<JS::Value> calendar) {
  MOZ_ASSERT(calendar.isInt32());
  return cx->compartment()->wrap(cx, calendar);
}

/**
 * IsISOLeapYear ( year )
 */

static constexpr bool IsISOLeapYear(int32_t year) {
  // Steps 1-5.
  return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
}

/**
 * ISODaysInYear ( year )
 */

static int32_t ISODaysInYear(int32_t year) {
  // Steps 1-3.
  return IsISOLeapYear(year) ? 366 : 365;
}

/**
 * ISODaysInMonth ( year, month )
 */

static constexpr int32_t ISODaysInMonth(int32_t year, int32_t month) {
  MOZ_ASSERT(1 <= month && month <= 12);

  constexpr uint8_t daysInMonth[2][13] = {
      {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
      {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};

  // Steps 1-4.
  return daysInMonth[IsISOLeapYear(year)][month];
}

/**
 * ISODaysInMonth ( year, month )
 */

int32_t js::temporal::ISODaysInMonth(int32_t year, int32_t month) {
  return ::ISODaysInMonth(year, month);
}

/**
 * 21.4.1.6 Week Day
 *
 * Compute the week day from |day| without first expanding |day| into a full
 * date through |MakeDate(day, 0)|:
 *
 *   WeekDay(MakeDate(day, 0))
 * = WeekDay(day × msPerDay + 0)
 * = WeekDay(day × msPerDay)
 * = ��(ℝ(Day(day × msPerDay) + 4��) modulo 7)
 * = ��(ℝ(��(floor(ℝ((day × msPerDay) / msPerDay))) + 4��) modulo 7)
 * = ��(ℝ(��(floor(ℝ(day))) + 4��) modulo 7)
 * = ��(ℝ(��(day) + 4��) modulo 7)
 */

static int32_t WeekDay(int32_t day) {
  int32_t result = (day + 4) % 7;
  if (result < 0) {
    result += 7;
  }
  return result;
}

/**
 * ISODayOfWeek ( isoDate )
 */

static int32_t ISODayOfWeek(const ISODate& isoDate) {
  MOZ_ASSERT(ISODateWithinLimits(isoDate));

  // Step 1.
  int32_t day = MakeDay(isoDate);

  // Step 2.
  int32_t dayOfWeek = WeekDay(day);

  // Steps 3-4.
  return dayOfWeek != 0 ? dayOfWeek : 7;
}

static constexpr auto FirstDayOfMonth(int32_t year) {
  // The following array contains the day of year for the first day of each
  // month, where index 0 is January, and day 0 is January 1.
  std::array<int32_t, 13> days = {};
  for (int32_t month = 1; month <= 12; ++month) {
    days[month] = days[month - 1] + ::ISODaysInMonth(year, month);
  }
  return days;
}

/**
 * ISODayOfYear ( isoDate )
 */

static int32_t ISODayOfYear(const ISODate& isoDate) {
  MOZ_ASSERT(ISODateWithinLimits(isoDate));

  const auto& [year, month, day] = isoDate;

  // First day of month arrays for non-leap and leap years.
  constexpr decltype(FirstDayOfMonth(0)) firstDayOfMonth[2] = {
      FirstDayOfMonth(1), FirstDayOfMonth(0)};

  // Steps 1-2.
  //
  // Instead of first computing the date and then using DayWithinYear to map the
  // date to the day within the year, directly lookup the first day of the month
  // and then add the additional days.
  return firstDayOfMonth[IsISOLeapYear(year)][month - 1] + day;
}

static int32_t FloorDiv(int32_t dividend, int32_t divisor) {
  MOZ_ASSERT(divisor > 0);

  int32_t quotient = dividend / divisor;
  int32_t remainder = dividend % divisor;
  if (remainder < 0) {
    quotient -= 1;
  }
  return quotient;
}

/**
 * 21.4.1.3 Year Number, DayFromYear
 */

static int32_t DayFromYear(int32_t year) {
  return 365 * (year - 1970) + FloorDiv(year - 1969, 4) -
         FloorDiv(year - 1901, 100) + FloorDiv(year - 1601, 400);
}

/**
 * 21.4.1.11 MakeTime ( hour, min, sec, ms )
 */

static int64_t MakeTime(const Time& time) {
  MOZ_ASSERT(IsValidTime(time));

  // Step 1 (Not applicable).

  // Step 2.
  int64_t h = time.hour;

  // Step 3.
  int64_t m = time.minute;

  // Step 4.
  int64_t s = time.second;

  // Step 5.
  int64_t milli = time.millisecond;

  // Steps 6-7.
  return h * ToMilliseconds(TemporalUnit::Hour) +
         m * ToMilliseconds(TemporalUnit::Minute) +
         s * ToMilliseconds(TemporalUnit::Second) + milli;
}

/**
 * 21.4.1.12 MakeDay ( year, month, date )
 */

int32_t js::temporal::MakeDay(const ISODate& date) {
  MOZ_ASSERT(ISODateWithinLimits(date));

  return DayFromYear(date.year) + ISODayOfYear(date) - 1;
}

/**
 * 21.4.1.13 MakeDate ( day, time )
 */

int64_t js::temporal::MakeDate(const ISODateTime& dateTime) {
  MOZ_ASSERT(ISODateTimeWithinLimits(dateTime));

  // Step 1 (Not applicable).

  // Steps 2-3.
  int64_t tv = MakeDay(dateTime.date) * ToMilliseconds(TemporalUnit::Day) +
               MakeTime(dateTime.time);

  // Step 4.
  return tv;
}

struct YearWeek final {
  int32_t year = 0;
  int32_t week = 0;
};

/**
 * ISOWeekOfYear ( isoDate )
 */

static YearWeek ISOWeekOfYear(const ISODate& isoDate) {
  MOZ_ASSERT(ISODateWithinLimits(isoDate));

  // Step 1.
  int32_t year = isoDate.year;

  // Step 2-7. (Not applicable in our implementation.)

  // Steps 8-9.
  int32_t dayOfYear = ISODayOfYear(isoDate);
  int32_t dayOfWeek = ISODayOfWeek(isoDate);

  // Step 10.
  int32_t week = (10 + dayOfYear - dayOfWeek) / 7;
  MOZ_ASSERT(0 <= week && week <= 53);

  // An ISO year has 53 weeks if the year starts on a Thursday or if it's a
  // leap year which starts on a Wednesday.
  auto isLongYear = [](int32_t year) {
    int32_t startOfYear = ISODayOfWeek({year, 1, 1});
    return startOfYear == 4 || (startOfYear == 3 && IsISOLeapYear(year));
  };

  // Step 11.
  //
  // Part of last year's last week, which is either week 52 or week 53.
  if (week == 0) {
    return {year - 1, 52 + int32_t(isLongYear(year - 1))};
  }

  // Step 12.
  //
  // Part of next year's first week if the current year isn't a long year.
  if (week == 53 && !isLongYear(year)) {
    return {year + 1, 1};
  }

  // Step 13.
  return {year, week};
}

/**
 * ToTemporalCalendarIdentifier ( calendarSlotValue )
 */

std::string_view js::temporal::CalendarIdentifier(CalendarId calendarId) {
  switch (calendarId) {
    case CalendarId::ISO8601:
      return "iso8601";
    case CalendarId::Buddhist:
      return "buddhist";
    case CalendarId::Chinese:
      return "chinese";
    case CalendarId::Coptic:
      return "coptic";
    case CalendarId::Dangi:
      return "dangi";
    case CalendarId::Ethiopian:
      return "ethiopic";
    case CalendarId::EthiopianAmeteAlem:
      return "ethioaa";
    case CalendarId::Gregorian:
      return "gregory";
    case CalendarId::Hebrew:
      return "hebrew";
    case CalendarId::Indian:
      return "indian";
    case CalendarId::Islamic:
      return "islamic";
    case CalendarId::IslamicCivil:
      return "islamic-civil";
    case CalendarId::IslamicRGSA:
      return "islamic-rgsa";
    case CalendarId::IslamicTabular:
      return "islamic-tbla";
    case CalendarId::IslamicUmmAlQura:
      return "islamic-umalqura";
    case CalendarId::Japanese:
      return "japanese";
    case CalendarId::Persian:
      return "persian";
    case CalendarId::ROC:
      return "roc";
  }
  MOZ_CRASH("invalid calendar id");
}

class MOZ_STACK_CLASS AsciiLowerCaseChars final {
  static constexpr size_t InlineCapacity = 24;

  Vector<char, InlineCapacity> chars_;

 public:
  explicit AsciiLowerCaseChars(JSContext* cx) : chars_(cx) {}

  operator mozilla::Span<const char>() const {
    return mozilla::Span<const char>{chars_};
  }

  [[nodiscard]] bool init(JSLinearString* str) {
    MOZ_ASSERT(StringIsAscii(str));

    if (!chars_.resize(str->length())) {
      return false;
    }

    CopyChars(reinterpret_cast<JS::Latin1Char*>(chars_.begin()), *str);

    mozilla::intl::AsciiToLowerCase(chars_.begin(), chars_.length(),
                                    chars_.begin());

    return true;
  }
};

/**
 * CanonicalizeCalendar ( id )
 */

bool js::temporal::CanonicalizeCalendar(JSContext* cx, Handle<JSString*> id,
                                        MutableHandle<CalendarValue> result) {
  Rooted<JSLinearString*> linear(cx, id->ensureLinear(cx));
  if (!linear) {
    return false;
  }

  // Steps 1-3.
  do {
    if (!StringIsAscii(linear) || linear->empty()) {
      break;
    }

    AsciiLowerCaseChars lowerCaseChars(cx);
    if (!lowerCaseChars.init(linear)) {
      return false;
    }
    mozilla::Span<const char> id = lowerCaseChars;

    // Reject invalid types before trying to resolve aliases.
    if (mozilla::intl::LocaleParser::CanParseUnicodeExtensionType(id).isErr()) {
      break;
    }

    // Resolve calendar aliases.
    static constexpr auto key = mozilla::MakeStringSpan("ca");
    if (const char* replacement =
            mozilla::intl::Locale::ReplaceUnicodeExtensionType(key, id)) {
      id = mozilla::MakeStringSpan(replacement);
    }

    // Step 1.
    static constexpr auto& calendars = AvailableCalendars();

    // Steps 2-3.
    for (auto identifier : calendars) {
      if (id == mozilla::Span{CalendarIdentifier(identifier)}) {
        result.set(CalendarValue(identifier));
        return true;
      }
    }
  } while (false);

  if (auto chars = QuoteString(cx, linear)) {
    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                             JSMSG_TEMPORAL_CALENDAR_INVALID_ID, chars.get());
  }
  return false;
}

template <typename T, typename... Ts>
static bool ToTemporalCalendar(JSContext* cx, Handle<JSObject*> object,
                               MutableHandle<CalendarValue> result) {
  if (auto* unwrapped = object->maybeUnwrapIf<T>()) {
    result.set(unwrapped->calendar());
    return result.wrap(cx);
  }

  if constexpr (sizeof...(Ts) > 0) {
    return ToTemporalCalendar<Ts...>(cx, object, result);
  }

  result.set(CalendarValue());
  return true;
}

/**
 * ToTemporalCalendarSlotValue ( temporalCalendarLike )
 */

bool js::temporal::ToTemporalCalendar(JSContext* cx,
                                      Handle<Value> temporalCalendarLike,
                                      MutableHandle<CalendarValue> result) {
  // Step 1.
  if (temporalCalendarLike.isObject()) {
    Rooted<JSObject*> obj(cx, &temporalCalendarLike.toObject());

    // Step 1.a.
    Rooted<CalendarValue> calendar(cx);
    if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject,
                              PlainMonthDayObject, PlainYearMonthObject,
                              ZonedDateTimeObject>(cx, obj, &calendar)) {
      return false;
    }
    if (calendar) {
      result.set(calendar);
      return true;
    }
  }

  // Step 2.
  if (!temporalCalendarLike.isString()) {
    ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK,
                     temporalCalendarLike, nullptr, "not a string");
    return false;
  }
  Rooted<JSString*> str(cx, temporalCalendarLike.toString());

  // Step 3.
  Rooted<JSLinearString*> id(cx, ParseTemporalCalendarString(cx, str));
  if (!id) {
    return false;
  }

  // Step 4.
  return CanonicalizeCalendar(cx, id, result);
}

/**
 * GetTemporalCalendarSlotValueWithISODefault ( item )
 */

bool js::temporal::GetTemporalCalendarWithISODefault(
    JSContext* cx, Handle<JSObject*> item,
    MutableHandle<CalendarValue> result) {
  // Step 1.
  Rooted<CalendarValue> calendar(cx);
  if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject,
                            PlainMonthDayObject, PlainYearMonthObject,
                            ZonedDateTimeObject>(cx, item, &calendar)) {
    return false;
  }
  if (calendar) {
    result.set(calendar);
    return true;
  }

  // Step 2.
  Rooted<Value> calendarValue(cx);
  if (!GetProperty(cx, item, item, cx->names().calendar, &calendarValue)) {
    return false;
  }

  // Step 3.
  if (calendarValue.isUndefined()) {
    result.set(CalendarValue(CalendarId::ISO8601));
    return true;
  }

  // Step 4.
  return ToTemporalCalendar(cx, calendarValue, result);
}

static auto ToAnyCalendarKind(CalendarId id) {
  switch (id) {
    case CalendarId::ISO8601:
      return capi::ICU4XAnyCalendarKind_Iso;
    case CalendarId::Buddhist:
      return capi::ICU4XAnyCalendarKind_Buddhist;
    case CalendarId::Chinese:
      return capi::ICU4XAnyCalendarKind_Chinese;
    case CalendarId::Coptic:
      return capi::ICU4XAnyCalendarKind_Coptic;
    case CalendarId::Dangi:
      return capi::ICU4XAnyCalendarKind_Dangi;
    case CalendarId::Ethiopian:
      return capi::ICU4XAnyCalendarKind_Ethiopian;
    case CalendarId::EthiopianAmeteAlem:
      return capi::ICU4XAnyCalendarKind_EthiopianAmeteAlem;
    case CalendarId::Gregorian:
      return capi::ICU4XAnyCalendarKind_Gregorian;
    case CalendarId::Hebrew:
      return capi::ICU4XAnyCalendarKind_Hebrew;
    case CalendarId::Indian:
      return capi::ICU4XAnyCalendarKind_Indian;
    case CalendarId::IslamicCivil:
      return capi::ICU4XAnyCalendarKind_IslamicCivil;
    case CalendarId::Islamic:
      return capi::ICU4XAnyCalendarKind_IslamicObservational;
    case CalendarId::IslamicRGSA:
      // ICU4X doesn't support a separate islamic-rgsa calendar, so we use the
      // observational calendar instead. This also matches ICU4C.
      return capi::ICU4XAnyCalendarKind_IslamicObservational;
    case CalendarId::IslamicTabular:
      return capi::ICU4XAnyCalendarKind_IslamicTabular;
    case CalendarId::IslamicUmmAlQura:
      return capi::ICU4XAnyCalendarKind_IslamicUmmAlQura;
    case CalendarId::Japanese:
      return capi::ICU4XAnyCalendarKind_Japanese;
    case CalendarId::Persian:
      return capi::ICU4XAnyCalendarKind_Persian;
    case CalendarId::ROC:
      return capi::ICU4XAnyCalendarKind_Roc;
  }
  MOZ_CRASH("invalid calendar id");
}

class ICU4XCalendarDeleter {
 public:
  void operator()(capi::ICU4XCalendar* ptr) {
    capi::ICU4XCalendar_destroy(ptr);
  }
};

using UniqueICU4XCalendar =
    mozilla::UniquePtr<capi::ICU4XCalendar, ICU4XCalendarDeleter>;

static UniqueICU4XCalendar CreateICU4XCalendar(JSContext* cx, CalendarId id) {
  auto result = capi::ICU4XCalendar_create_for_kind(
      mozilla::intl::GetDataProvider(), ToAnyCalendarKind(id));
  if (!result.is_ok) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
    return nullptr;
  }
  return UniqueICU4XCalendar{result.ok};
}

static uint32_t MaximumISOYear(CalendarId calendarId) {
  switch (calendarId) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
    case CalendarId::Gregorian:
    case CalendarId::Hebrew:
    case CalendarId::Indian:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::Japanese:
    case CalendarId::Persian:
    case CalendarId::ROC: {
      // Passing values near INT32_{MIN,MAX} triggers ICU4X assertions, so we
      // have to handle large input years early.
      return 300'000;
    }

    case CalendarId::Chinese:
    case CalendarId::Dangi: {
      // Lower limit for these calendars to avoid running into ICU4X assertions.
      //
      // https://github.com/unicode-org/icu4x/issues/4917
      return 10'000;
    }

    case CalendarId::Islamic:
    case CalendarId::IslamicRGSA:
    case CalendarId::IslamicUmmAlQura: {
      // Lower limit for these calendars to avoid running into ICU4X assertions.
      //
      // https://github.com/unicode-org/icu4x/issues/4917
      return 5'000;
    }
  }
  MOZ_CRASH("invalid calendar");
}

static uint32_t MaximumCalendarYear(CalendarId calendarId) {
  switch (calendarId) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
    case CalendarId::Gregorian:
    case CalendarId::Hebrew:
    case CalendarId::Indian:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::Japanese:
    case CalendarId::Persian:
    case CalendarId::ROC: {
      // Passing values near INT32_{MIN,MAX} triggers ICU4X assertions, so we
      // have to handle large input years early.
      return 300'000;
    }

    case CalendarId::Chinese:
    case CalendarId::Dangi: {
      // Lower limit for these calendars to avoid running into ICU4X assertions.
      //
      // https://github.com/unicode-org/icu4x/issues/4917
      return 10'000;
    }

    case CalendarId::Islamic:
    case CalendarId::IslamicRGSA:
    case CalendarId::IslamicUmmAlQura: {
      // Lower limit for these calendars to avoid running into ICU4X assertions.
      //
      // https://github.com/unicode-org/icu4x/issues/4917
      return 5'000;
    }
  }
  MOZ_CRASH("invalid calendar");
}

static void ReportCalendarFieldOverflow(JSContext* cx, const char* name,
                                        double num) {
  ToCStringBuf numCbuf;
  const char* numStr = NumberToCString(&numCbuf, num);

  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                            JSMSG_TEMPORAL_CALENDAR_OVERFLOW_FIELD, name,
                            numStr);
}

class ICU4XDateDeleter {
 public:
  void operator()(capi::ICU4XDate* ptr) { capi::ICU4XDate_destroy(ptr); }
};

using UniqueICU4XDate = mozilla::UniquePtr<capi::ICU4XDate, ICU4XDateDeleter>;

static UniqueICU4XDate CreateICU4XDate(JSContext* cx, const ISODate& date,
                                       CalendarId calendarId,
                                       const capi::ICU4XCalendar* calendar) {
  if (mozilla::Abs(date.year) > MaximumISOYear(calendarId)) {
    ReportCalendarFieldOverflow(cx, "year", date.year);
    return nullptr;
  }

  auto result = capi::ICU4XDate_create_from_iso_in_calendar(
      date.year, date.month, date.day, calendar);
  if (!result.is_ok) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
    return nullptr;
  }
  return UniqueICU4XDate{result.ok};
}

class ICU4XIsoDateDeleter {
 public:
  void operator()(capi::ICU4XIsoDate* ptr) { capi::ICU4XIsoDate_destroy(ptr); }
};

using UniqueICU4XIsoDate =
    mozilla::UniquePtr<capi::ICU4XIsoDate, ICU4XIsoDateDeleter>;

class ICU4XWeekCalculatorDeleter {
 public:
  void operator()(capi::ICU4XWeekCalculator* ptr) {
    capi::ICU4XWeekCalculator_destroy(ptr);
  }
};

using UniqueICU4XWeekCalculator =
    mozilla::UniquePtr<capi::ICU4XWeekCalculator, ICU4XWeekCalculatorDeleter>;

static UniqueICU4XWeekCalculator CreateICU4WeekCalculator(JSContext* cx,
                                                          CalendarId calendar) {
  MOZ_ASSERT(calendar == CalendarId::Gregorian);

  auto firstWeekday = capi::ICU4XIsoWeekday_Monday;
  uint8_t minWeekDays = 1;

  auto* result =
      capi::ICU4XWeekCalculator_create_from_first_day_of_week_and_min_week_days(
          firstWeekday, minWeekDays);
  return UniqueICU4XWeekCalculator{result};
}

// Define IMPLEMENTS_DR2126 if DR2126 is implemented.
//
// https://cplusplus.github.io/CWG/issues/2126.html
#if defined(__clang__)
#  if (__clang_major__ >= 12)
#    define IMPLEMENTS_DR2126
#  endif
#else
#  define IMPLEMENTS_DR2126
#endif

#ifdef IMPLEMENTS_DR2126
static constexpr size_t EraNameMaxLength() {
  size_t length = 0;
  for (auto calendar : AvailableCalendars()) {
    for (auto era : CalendarEras(calendar)) {
      for (auto name : CalendarEraNames(calendar, era)) {
        length = std::max(length, name.length());
      }
    }
  }
  return length;
}
#endif

static mozilla::Maybe<EraCode> EraForString(CalendarId calendar,
                                            JSLinearString* string) {
  MOZ_ASSERT(CalendarEraRelevant(calendar));

  // Note: Assigning MaxLength to EraNameMaxLength() breaks the CDT indexer.
  constexpr size_t MaxLength = 24;
#ifdef IMPLEMENTS_DR2126
  static_assert(MaxLength >= EraNameMaxLength(),
                "Storage size is at least as large as the largest known era");
#endif

  if (string->length() > MaxLength || !StringIsAscii(string)) {
    return mozilla::Nothing();
  }

  char chars[MaxLength] = {};
  CopyChars(reinterpret_cast<JS::Latin1Char*>(chars), *string);

  auto stringView = std::string_view{chars, string->length()};

  for (auto era : CalendarEras(calendar)) {
    for (auto name : CalendarEraNames(calendar, era)) {
      if (name == stringView) {
        return mozilla::Some(era);
      }
    }
  }
  return mozilla::Nothing();
}

static constexpr std::string_view IcuEraName(CalendarId calendar, EraCode era) {
  switch (calendar) {
    // https://docs.rs/icu/latest/icu/calendar/iso/struct.Iso.html#era-codes
    case CalendarId::ISO8601: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "default";
    }

    // https://docs.rs/icu/latest/icu/calendar/buddhist/struct.Buddhist.html#era-codes
    case CalendarId::Buddhist: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "be";
    }

    // https://docs.rs/icu/latest/icu/calendar/chinese/struct.Chinese.html#year-and-era-codes
    case CalendarId::Chinese: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "chinese";
    }

    // https://docs.rs/icu/latest/icu/calendar/coptic/struct.Coptic.html#era-codes
    case CalendarId::Coptic: {
      MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse);
      return era == EraCode::Standard ? "ad" : "bd";
    }

    // https://docs.rs/icu/latest/icu/calendar/dangi/struct.Dangi.html#era-codes
    case CalendarId::Dangi: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "dangi";
    }

    // https://docs.rs/icu/latest/icu/calendar/ethiopian/struct.Ethiopian.html#era-codes
    case CalendarId::Ethiopian: {
      MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse);
      return era == EraCode::Standard ? "incar" : "pre-incar";
    }

    // https://docs.rs/icu/latest/icu/calendar/ethiopian/struct.Ethiopian.html#era-codes
    case CalendarId::EthiopianAmeteAlem: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "mundi";
    }

    // https://docs.rs/icu/latest/icu/calendar/gregorian/struct.Gregorian.html#era-codes
    case CalendarId::Gregorian: {
      MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse);
      return era == EraCode::Standard ? "ce" : "bce";
    }

    // https://docs.rs/icu/latest/icu/calendar/hebrew/struct.Hebrew.html
    case CalendarId::Hebrew: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "am";
    }

    // https://docs.rs/icu/latest/icu/calendar/indian/struct.Indian.html#era-codes
    case CalendarId::Indian: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "saka";
    }

    // https://docs.rs/icu/latest/icu/calendar/islamic/struct.IslamicCivil.html#era-codes
    // https://docs.rs/icu/latest/icu/calendar/islamic/struct.IslamicObservational.html#era-codes
    // https://docs.rs/icu/latest/icu/calendar/islamic/struct.IslamicTabular.html#era-codes
    // https://docs.rs/icu/latest/icu/calendar/islamic/struct.IslamicUmmAlQura.html#era-codes
    // https://docs.rs/icu/latest/icu/calendar/persian/struct.Persian.html#era-codes
    case CalendarId::Islamic:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicRGSA:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
    case CalendarId::Persian: {
      MOZ_ASSERT(era == EraCode::Standard);
      return "ah";
    }

    // https://docs.rs/icu/latest/icu/calendar/japanese/struct.Japanese.html#era-codes
    case CalendarId::Japanese: {
      switch (era) {
        case EraCode::Standard:
          return "ce";
        case EraCode::Inverse:
          return "bce";
        case EraCode::Meiji:
          return "meiji";
        case EraCode::Taisho:
          return "taisho";
        case EraCode::Showa:
          return "showa";
        case EraCode::Heisei:
          return "heisei";
        case EraCode::Reiwa:
          return "reiwa";
      }
      break;
    }

    // https://docs.rs/icu/latest/icu/calendar/roc/struct.Roc.html#era-codes
    case CalendarId::ROC: {
      MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse);
      return era == EraCode::Standard ? "roc" : "roc-inverse";
    }
  }
  JS_CONSTEXPR_CRASH("invalid era");
}

enum class CalendarError {
  // Catch-all kind for all other error types.
  Generic,

  // https://docs.rs/icu/latest/icu/calendar/enum.Error.html#variant.Overflow
  Overflow,

  // https://docs.rs/icu/latest/icu/calendar/enum.Error.html#variant.Underflow
  Underflow,

  // https://docs.rs/icu/latest/icu/calendar/enum.Error.html#variant.OutOfRange
  OutOfRange,

  // https://docs.rs/icu/latest/icu/calendar/enum.Error.html#variant.UnknownEra
  UnknownEra,

  // https://docs.rs/icu/latest/icu/calendar/enum.Error.html#variant.UnknownMonthCode
  UnknownMonthCode,
};

#ifdef DEBUG
static auto CalendarErasAsEnumSet(CalendarId calendarId) {
  // `mozilla::EnumSet<EraCode>(CalendarEras(calendarId))` doesn't work in old
  // GCC versions, so add all era codes manually to the enum set.
  mozilla::EnumSet<EraCode> eras{};
  for (auto era : CalendarEras(calendarId)) {
    eras += era;
  }
  return eras;
}
#endif

static mozilla::Result<UniqueICU4XDate, CalendarError> CreateDateFromCodes(
    CalendarId calendarId, const capi::ICU4XCalendar* calendar, EraYear eraYear,
    MonthCode monthCode, int32_t day) {
  MOZ_ASSERT(calendarId != CalendarId::ISO8601);
  MOZ_ASSERT(capi::ICU4XCalendar_kind(calendar) ==
             ToAnyCalendarKind(calendarId));
  MOZ_ASSERT(CalendarErasAsEnumSet(calendarId).contains(eraYear.era));
  MOZ_ASSERT_IF(CalendarEraRelevant(calendarId), eraYear.year > 0);
  MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId));
  MOZ_ASSERT(CalendarMonthCodes(calendarId).contains(monthCode));
  MOZ_ASSERT(day > 0);
  MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second);

  auto era = IcuEraName(calendarId, eraYear.era);
  auto monthCodeView = std::string_view{monthCode};
  auto date = capi::ICU4XDate_create_from_codes_in_calendar(
      era.data(), era.length(), eraYear.year, monthCodeView.data(),
      monthCodeView.length(), day, calendar);
  if (date.is_ok) {
    return UniqueICU4XDate{date.ok};
  }

  // Map possible calendar errors.
  //
  // Calendar error codes which can't happen for `create_from_codes_in_calendar`
  // are mapped to `CalendarError::Generic`.
  switch (date.err) {
    case capi::ICU4XError_CalendarOverflowError:
      return mozilla::Err(CalendarError::Overflow);
    case capi::ICU4XError_CalendarUnderflowError:
      return mozilla::Err(CalendarError::Underflow);
    case capi::ICU4XError_CalendarOutOfRangeError:
      return mozilla::Err(CalendarError::OutOfRange);
    case capi::ICU4XError_CalendarUnknownEraError:
      return mozilla::Err(CalendarError::UnknownEra);
    case capi::ICU4XError_CalendarUnknownMonthCodeError:
      return mozilla::Err(CalendarError::UnknownMonthCode);
    default:
      return mozilla::Err(CalendarError::Generic);
  }
}

/**
 * Return the first year (gannen) of a Japanese era.
 */

static bool FirstYearOfJapaneseEra(JSContext* cx, CalendarId calendarId,
                                   const capi::ICU4XCalendar* calendar,
                                   EraCode era, int32_t* result) {
  MOZ_ASSERT(calendarId == CalendarId::Japanese);
  MOZ_ASSERT(!CalendarEraStartsAtYearBoundary(calendarId, era));

  // All supported Japanese eras last at least one year, so December 31 is
  // guaranteed to be in the first year of the era.
  auto dateResult =
      CreateDateFromCodes(calendarId, calendar, {era, 1}, MonthCode{12}, 31);
  if (dateResult.isErr()) {
    MOZ_ASSERT(dateResult.inspectErr() == CalendarError::Generic,
               "unexpected non-generic calendar error");

    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
    return false;
  }

  auto date = dateResult.unwrap();
  UniqueICU4XIsoDate isoDate{capi::ICU4XDate_to_iso(date.get())};

  int32_t isoYear = capi::ICU4XIsoDate_year(isoDate.get());
  MOZ_ASSERT(isoYear > 0, "unexpected era start before 1 CE");

  *result = isoYear;
  return true;
}

/**
 * Return the equivalent common era year for a Japanese era year.
 */

static bool JapaneseEraYearToCommonEraYear(JSContext* cx, CalendarId calendarId,
                                           const capi::ICU4XCalendar* calendar,
                                           EraYear eraYear, EraYear* result) {
  int32_t firstYearOfEra;
  if (!FirstYearOfJapaneseEra(cx, calendarId, calendar, eraYear.era,
                              &firstYearOfEra)) {
    return false;
  }

  // Map non-positive era years to years before the first era year:
  //
  //  1 Reiwa =  2019 CE
  //  0 Reiwa -> 2018 CE
  // -1 Reiwa -> 2017 CE
  // etc.
  //
  // Map too large era years to the next era:
  //
  // Heisei 31 =  2019 CE
  // Heisei 32 -> 2020 CE
  // ...

  int32_t year = (firstYearOfEra - 1) + eraYear.year;
  if (year > 0) {
    *result = {EraCode::Standard, year};
    return true;
  }
  *result = {EraCode::Inverse, int32_t(mozilla::Abs(year) + 1)};
  return true;
}

static UniqueICU4XDate CreateDateFromCodes(JSContext* cx, CalendarId calendarId,
                                           const capi::ICU4XCalendar* calendar,
                                           EraYear eraYear, MonthCode monthCode,
                                           int32_t day,
                                           TemporalOverflow overflow) {
  MOZ_ASSERT(CalendarMonthCodes(calendarId).contains(monthCode));
  MOZ_ASSERT(day > 0);
  MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second);

  // Constrain day to the maximum possible day for the input month.
  //
  // Special cases like February 29 in leap years of the Gregorian calendar are
  // handled below.
  int32_t daysInMonth = CalendarDaysInMonth(calendarId, monthCode).second;
  if (overflow == TemporalOverflow::Constrain) {
    day = std::min(day, daysInMonth);
  } else {
    MOZ_ASSERT(overflow == TemporalOverflow::Reject);

    if (day > daysInMonth) {
      ReportCalendarFieldOverflow(cx, "day", day);
      return nullptr;
    }
  }

  // ICU4X doesn't support large dates, so we have to handle this case early.
  if (mozilla::Abs(eraYear.year) > MaximumCalendarYear(calendarId)) {
    ReportCalendarFieldOverflow(cx, "year", eraYear.year);
    return nullptr;
  }

  auto result =
      CreateDateFromCodes(calendarId, calendar, eraYear, monthCode, day);
  if (result.isOk()) {
    return result.unwrap();
  }

  switch (result.inspectErr()) {
    case CalendarError::UnknownMonthCode: {
      // We've asserted above that |monthCode| is valid for this calendar, so
      // any unknown month code must be for a leap month which doesn't happen in
      // the current year.
      MOZ_ASSERT(CalendarHasLeapMonths(calendarId));
      MOZ_ASSERT(monthCode.isLeapMonth());

      if (overflow == TemporalOverflow::Reject) {
        // Ensure the month code is null-terminated.
        char code[5] = {};
        auto monthCodeView = std::string_view{monthCode};
        monthCodeView.copy(code, monthCodeView.length());

        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                                 JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE,
                                 code);
        return nullptr;
      }

      // Retry as non-leap month when we're allowed to constrain.
      //
      // CalendarDateToISO ( calendar, fields, overflow )
      //
      // If the month is a leap month that doesn't exist in the year, pick
      // another date according to the cultural conventions of that calendar's
      // users. Usually this will result in the same day in the month before or
      // after where that month would normally fall in a leap year.
      //
      // Hebrew calendar:
      // Replace Adar I (M05L) with Adar (M06).
      //
      // Chinese/Dangi calendar:
      // Pick the next month, for example M03L -> M04, except for M12L, because
      // we don't to switch over to the next year.

      int32_t nonLeapMonth = std::min(monthCode.ordinal() + 1, 12);
      auto nonLeapMonthCode = MonthCode{nonLeapMonth};
      return CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                 nonLeapMonthCode, day, overflow);
    }

    case CalendarError::Overflow: {
      // ICU4X throws an overflow error when:
      // 1. month > monthsInYear(year), or
      // 2. days > daysInMonthOf(year, month).
      //
      // Case 1 can't happen for month-codes, so it doesn't apply here.
      // Case 2 can only happen when |day| is larger than the minimum number
      // of days in the month.
      MOZ_ASSERT(day > CalendarDaysInMonth(calendarId, monthCode).first);

      if (overflow == TemporalOverflow::Reject) {
        ReportCalendarFieldOverflow(cx, "day", day);
        return nullptr;
      }

      auto firstDayOfMonth = CreateDateFromCodes(
          cx, calendarId, calendar, eraYear, monthCode, 1, overflow);
      if (!firstDayOfMonth) {
        return nullptr;
      }

      int32_t daysInMonth =
          capi::ICU4XDate_days_in_month(firstDayOfMonth.get());
      MOZ_ASSERT(day > daysInMonth);
      return CreateDateFromCodes(cx, calendarId, calendar, eraYear, monthCode,
                                 daysInMonth, overflow);
    }

    case CalendarError::OutOfRange: {
      // ICU4X throws an out-of-range error if:
      // 1. Non-positive era years are given.
      // 2. Dates are before/after the requested named Japanese era.
      //
      // Case 1 doesn't happen for us, because we always pass strictly positive
      // era years, so this error must be for case 2.
      MOZ_ASSERT(calendarId == CalendarId::Japanese);
      MOZ_ASSERT(!CalendarEraStartsAtYearBoundary(calendarId, eraYear.era));

      EraYear commonEraYear;
      if (!JapaneseEraYearToCommonEraYear(cx, calendarId, calendar, eraYear,
                                          &commonEraYear)) {
        return nullptr;
      }
      return CreateDateFromCodes(cx, calendarId, calendar, commonEraYear,
                                 monthCode, day, overflow);
    }

    case CalendarError::Underflow:
    case CalendarError::UnknownEra:
      MOZ_ASSERT(false"unexpected calendar error");
      break;

    case CalendarError::Generic:
      break;
  }

  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                            JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
  return nullptr;
}

static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendarId,
                                      const capi::ICU4XCalendar* calendar,
                                      EraYear eraYear, int32_t month,
                                      int32_t day, TemporalOverflow overflow) {
  MOZ_ASSERT(calendarId != CalendarId::ISO8601);
  MOZ_ASSERT(month > 0);
  MOZ_ASSERT(day > 0);
  MOZ_ASSERT(month <= CalendarMonthsPerYear(calendarId));
  MOZ_ASSERT(day <= CalendarDaysInMonth(calendarId).second);

  switch (calendarId) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
    case CalendarId::Gregorian:
    case CalendarId::Indian:
    case CalendarId::Islamic:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicRGSA:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
    case CalendarId::Japanese:
    case CalendarId::Persian:
    case CalendarId::ROC: {
      MOZ_ASSERT(!CalendarHasLeapMonths(calendarId));

      // Use the month-code corresponding to the ordinal month number for
      // calendar systems without leap months.
      auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                      MonthCode{month}, day, overflow);
      if (!date) {
        return nullptr;
      }
      MOZ_ASSERT_IF(
          CalendarEraStartsAtYearBoundary(calendarId),
          capi::ICU4XDate_ordinal_month(date.get()) == uint32_t(month));
      return date;
    }

    case CalendarId::Dangi:
    case CalendarId::Chinese: {
      static_assert(CalendarHasLeapMonths(CalendarId::Chinese));
      static_assert(CalendarMonthsPerYear(CalendarId::Chinese) == 13);
      static_assert(CalendarHasLeapMonths(CalendarId::Dangi));
      static_assert(CalendarMonthsPerYear(CalendarId::Dangi) == 13);

      MOZ_ASSERT(1 <= month && month <= 13);

      // Create date with month number replaced by month-code.
      auto monthCode = MonthCode{std::min(month, 12)};
      auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                      monthCode, day, overflow);
      if (!date) {
        return nullptr;
      }

      // If the ordinal month of |date| matches the input month, no additional
      // changes are necessary and we can directly return |date|.
      int32_t ordinal = capi::ICU4XDate_ordinal_month(date.get());
      if (ordinal == month) {
        return date;
      }

      // Otherwise we need to handle three cases:
      // 1. The input year contains a leap month and we need to adjust the
      //    month-code.
      // 2. The thirteenth month of a year without leap months was requested.
      // 3. The thirteenth month of a year with leap months was requested.
      if (ordinal > month) {
        MOZ_ASSERT(1 < month && month <= 12);

        // This case can only happen in leap years.
        MOZ_ASSERT(capi::ICU4XDate_months_in_year(date.get()) == 13);

        // Leap months can occur after any month in the Chinese calendar.
        //
        // Example when the fourth month is a leap month between M03 and M04.
        //
        // Month code:     M01  M02  M03  M03L  M04  M05  M06 ...
        // Ordinal month:  1    2    3    4     5    6    7

        // The month can be off by exactly one.
        MOZ_ASSERT((ordinal - month) == 1);

        // First try the case when the previous month isn't a leap month. This
        // case can only occur when |month > 2|, because otherwise we know that
        // "M01L" is the correct answer.
        if (month > 2) {
          auto previousMonthCode = MonthCode{month - 1};
          date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                     previousMonthCode, day, overflow);
          if (!date) {
            return nullptr;
          }

          int32_t ordinal = capi::ICU4XDate_ordinal_month(date.get());
          if (ordinal == month) {
            return date;
          }
        }

        // Fall-through when the previous month is a leap month.
      } else {
        MOZ_ASSERT(month == 13);
        MOZ_ASSERT(ordinal == 12);

        // Years with leap months contain thirteen months.
        if (capi::ICU4XDate_months_in_year(date.get()) != 13) {
          if (overflow == TemporalOverflow::Reject) {
            ReportCalendarFieldOverflow(cx, "month", month);
            return nullptr;
          }
          return date;
        }

        // Fall-through to return leap month "M12L" at the end of the year.
      }

      // Finally handle the case when the previous month is a leap month.
      auto leapMonthCode = MonthCode{month - 1, /* isLeapMonth= */ true};
      date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                 leapMonthCode, day, overflow);
      if (!date) {
        return nullptr;
      }
      MOZ_ASSERT(capi::ICU4XDate_ordinal_month(date.get()) == uint32_t(month),
                 "unexpected ordinal month");
      return date;
    }

    case CalendarId::Hebrew: {
      static_assert(CalendarHasLeapMonths(CalendarId::Hebrew));
      static_assert(CalendarMonthsPerYear(CalendarId::Hebrew) == 13);

      MOZ_ASSERT(1 <= month && month <= 13);

      // Create date with month number replaced by month-code.
      auto monthCode = MonthCode{std::min(month, 12)};
      auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                      monthCode, day, overflow);
      if (!date) {
        return nullptr;
      }

      // If the ordinal month of |date| matches the input month, no additional
      // changes are necessary and we can directly return |date|.
      int32_t ordinal = capi::ICU4XDate_ordinal_month(date.get());
      if (ordinal == month) {
        return date;
      }

      // Otherwise we need to handle two cases:
      // 1. The input year contains a leap month and we need to adjust the
      //    month-code.
      // 2. The thirteenth month of a year without leap months was requested.
      if (ordinal > month) {
        MOZ_ASSERT(1 < month && month <= 12);

        // This case can only happen in leap years.
        MOZ_ASSERT(capi::ICU4XDate_months_in_year(date.get()) == 13);

        // Leap months can occur between M05 and M06 in the Hebrew calendar.
        //
        // Month code:     M01  M02  M03  M04  M05  M05L  M06 ...
        // Ordinal month:  1    2    3    4    5    6     7

        // The month can be off by exactly one.
        MOZ_ASSERT((ordinal - month) == 1);
      } else {
        MOZ_ASSERT(month == 13);
        MOZ_ASSERT(ordinal == 12);

        if (overflow == TemporalOverflow::Reject) {
          ReportCalendarFieldOverflow(cx, "month", month);
          return nullptr;
        }
        return date;
      }

      // The previous month is the leap month Adar I iff |month| is six.
      bool isLeapMonth = month == 6;
      auto previousMonthCode = MonthCode{month - 1, isLeapMonth};
      date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
                                 previousMonthCode, day, overflow);
      if (!date) {
        return nullptr;
      }
      MOZ_ASSERT(capi::ICU4XDate_ordinal_month(date.get()) == uint32_t(month),
                 "unexpected ordinal month");
      return date;
    }
  }
  MOZ_CRASH("invalid calendar id");
}

#ifdef IMPLEMENTS_DR2126
static constexpr size_t ICUEraNameMaxLength() {
  size_t length = 0;
  for (auto calendar : AvailableCalendars()) {
    for (auto era : CalendarEras(calendar)) {
      auto name = IcuEraName(calendar, era);
      length = std::max(length, name.length());
    }
  }
  return length;
}
#endif

/**
 * Retrieve the era code from |date| and then map the returned ICU4X era code to
 * the corresponding |EraCode| member.
 */

static bool CalendarDateEra(JSContext* cx, CalendarId calendar,
                            const capi::ICU4XDate* date, EraCode* result) {
  MOZ_ASSERT(calendar != CalendarId::ISO8601);

  // Note: Assigning MaxLength to ICUEraNameMaxLength() breaks the CDT indexer.
  constexpr size_t MaxLength = 15;
#ifdef IMPLEMENTS_DR2126
  static_assert(MaxLength >= ICUEraNameMaxLength(),
                "Storage size is at least as large as the largest known era");
#endif

  // Storage for the largest known era string and the terminating NUL-character.
  char buf[MaxLength + 1] = {};
  auto writable = capi::diplomat_simple_writeable(buf, std::size(buf));

  if (!capi::ICU4XDate_era(date, &writable).is_ok) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
    return false;
  }
  MOZ_ASSERT(writable.buf == buf, "unexpected buffer relocation");

  auto dateEra = std::string_view{writable.buf, writable.len};

  // Map to era name to era code.
  for (auto era : CalendarEras(calendar)) {
    if (IcuEraName(calendar, era) == dateEra) {
      *result = era;
      return true;
    }
  }

  // Invalid/Unknown era name.
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                            JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
  return false;
}

/**
 * Return the extended (non-era) year from |date|.
 */

static bool CalendarDateYear(JSContext* cx, CalendarId calendar,
                             const capi::ICU4XDate* date, int32_t* result) {
  MOZ_ASSERT(calendar != CalendarId::ISO8601);

  // FIXME: ICU4X doesn't yet support CalendarDateYear, so we need to manually
  // adjust the era year to determine the non-era year.
  //
  // https://github.com/unicode-org/icu4x/issues/3962

  if (!CalendarEraRelevant(calendar)) {
    int32_t year = capi::ICU4XDate_year_in_era(date);
    *result = year;
    return true;
  }

  if (calendar != CalendarId::Japanese) {
    MOZ_ASSERT(CalendarEras(calendar).size() == 2);

    int32_t year = capi::ICU4XDate_year_in_era(date);
    MOZ_ASSERT(year > 0, "era years are strictly positive in ICU4X");

    EraCode era;
    if (!CalendarDateEra(cx, calendar, date, &era)) {
      return false;
    }

    // Map from era year to extended year.
    //
    // For example in the Gregorian calendar:
    //
    // ----------------------------
    // | Era Year | Extended Year |
    // | 2 CE     |  2            |
    // | 1 CE     |  1            |
    // | 1 BCE    |  0            |
    // | 2 BCE    | -1            |
    // ----------------------------
    if (era == EraCode::Inverse) {
      year = -(year - 1);
    } else {
      MOZ_ASSERT(era == EraCode::Standard);
    }

    *result = year;
    return true;
  }

  // Japanese uses a proleptic Gregorian calendar, so we can use the ISO year.
  UniqueICU4XIsoDate isoDate{capi::ICU4XDate_to_iso(date)};
  int32_t isoYear = capi::ICU4XIsoDate_year(isoDate.get());

  *result = isoYear;
  return true;
}

/**
 * Retrieve the month code from |date| and then map the returned ICU4X month
 * code to the corresponding |MonthCode| member.
 */

static bool CalendarDateMonthCode(JSContext* cx, CalendarId calendar,
                                  const capi::ICU4XDate* date,
                                  MonthCode* result) {
  MOZ_ASSERT(calendar != CalendarId::ISO8601);

  // Valid month codes are "M01".."M13" and "M01L".."M12L".
  constexpr size_t MaxLength =
      std::string_view{MonthCode::maxLeapMonth()}.length();
  static_assert(
      MaxLength > std::string_view{MonthCode::maxNonLeapMonth()}.length(),
      "string representation of max-leap month is larger");

  // Storage for the largest valid month code and the terminating NUL-character.
  char buf[MaxLength + 1] = {};
  auto writable = capi::diplomat_simple_writeable(buf, std::size(buf));

  if (!capi::ICU4XDate_month_code(date, &writable).is_ok) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INTERNAL_ERROR);
    return false;
  }
  MOZ_ASSERT(writable.buf == buf, "unexpected buffer relocation");

  auto view = std::string_view{writable.buf, writable.len};

  MOZ_ASSERT(view.length() >= 3);
  MOZ_ASSERT(view[0] == 'M');
  MOZ_ASSERT(mozilla::IsAsciiDigit(view[1]));
  MOZ_ASSERT(mozilla::IsAsciiDigit(view[2]));
  MOZ_ASSERT_IF(view.length() > 3, view[3] == 'L');

  int32_t ordinal =
      AsciiDigitToNumber(view[1]) * 10 + AsciiDigitToNumber(view[2]);
  bool isLeapMonth = view.length() > 3;
  auto monthCode = MonthCode{ordinal, isLeapMonth};

  static constexpr auto IrregularAdarII =
      MonthCode{6, /* isLeapMonth = */ true};
  static constexpr auto RegularAdarII = MonthCode{6};

  // Handle the irregular month code "M06L" for Adar II in leap years.
  //
  // https://docs.rs/icu/latest/icu/calendar/hebrew/struct.Hebrew.html#month-codes
  if (calendar == CalendarId::Hebrew && monthCode == IrregularAdarII) {
    monthCode = RegularAdarII;
  }

  // The month code must be valid for this calendar.
  MOZ_ASSERT(CalendarMonthCodes(calendar).contains(monthCode));

  *result = monthCode;
  return true;
}

class MonthCodeString {
  // Zero-terminated month code string.
  char str_[4 + 1];

 public:
  explicit MonthCodeString(MonthCodeField field) {
    str_[0] = 'M';
    str_[1] = char('0' + (field.ordinal() / 10));
    str_[2] = char('0' + (field.ordinal() % 10));
    str_[3] = field.isLeapMonth() ? 'L' : '\0';
    str_[4] = '\0';
  }

  const char* toCString() const { return str_; }
};

/**
 * CalendarResolveFields ( calendar, fields, type )
 */

static bool ISOCalendarResolveMonth(JSContext* cx,
                                    Handle<CalendarFields> fields,
                                    double* result) {
  double month = fields.month();
  MOZ_ASSERT_IF(fields.has(CalendarField::Month),
                IsInteger(month) && month > 0);

  // CalendarResolveFields, steps 1.e.
  if (!fields.has(CalendarField::MonthCode)) {
    MOZ_ASSERT(fields.has(CalendarField::Month));

    *result = month;
    return true;
  }

  auto monthCode = fields.monthCode();

  // CalendarResolveFields, steps 1.f-k.
  int32_t ordinal = monthCode.ordinal();
  if (ordinal < 1 || ordinal > 12 || monthCode.isLeapMonth()) {
    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                             JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE,
                             MonthCodeString{monthCode}.toCString());
    return false;
  }

  // CalendarResolveFields, steps 1.l-m.
  if (fields.has(CalendarField::Month) && month != ordinal) {
    ToCStringBuf cbuf;
    const char* monthStr = NumberToCString(&cbuf, month);

    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                             JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_MONTHCODE,
                             MonthCodeString{monthCode}.toCString(), monthStr);
    return false;
  }

  // CalendarResolveFields, steps 1.n.
  *result = ordinal;
  return true;
}

struct EraYears {
  // Year starting from the calendar epoch.
  mozilla::Maybe<EraYear> fromEpoch;

  // Year starting from a specific calendar era.
  mozilla::Maybe<EraYear> fromEra;
};

static bool CalendarEraYear(JSContext* cx, CalendarId calendarId,
                            EraYear eraYear, EraYear* result) {
  MOZ_ASSERT(CalendarEraRelevant(calendarId));
  MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId));

  if (eraYear.year > 0) {
    *result = eraYear;
    return true;
  }

  switch (eraYear.era) {
    case EraCode::Standard: {
      // Map non-positive era years as follows:
      //
      //  0 CE -> 1 BCE
      // -1 CE -> 2 BCE
      // etc.
      *result = {EraCode::Inverse, int32_t(mozilla::Abs(eraYear.year) + 1)};
      return true;
    }

    case EraCode::Inverse: {
      // Map non-positive era years as follows:
      //
      //  0 BCE -> 1 CE
      // -1 BCE -> 2 CE
      // etc.
      *result = {EraCode::Standard, int32_t(mozilla::Abs(eraYear.year) + 1)};
      return true;
    }

    case EraCode::Meiji:
    case EraCode::Taisho:
    case EraCode::Showa:
    case EraCode::Heisei:
    case EraCode::Reiwa: {
      MOZ_ASSERT(calendarId == CalendarId::Japanese);

      auto cal = CreateICU4XCalendar(cx, calendarId);
      if (!cal) {
        return false;
      }
      return JapaneseEraYearToCommonEraYear(cx, calendarId, cal.get(), eraYear,
                                            result);
    }
  }
  MOZ_CRASH("invalid era id");
}

/**
 * CalendarResolveFields ( calendar, fields, type )
 * CalendarDateToISO ( calendar, fields, overflow )
 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow )
 *
 * Extract `year` and `eraYear` from |fields| and perform some initial
 * validation to ensure the values are valid for the requested calendar.
 */

static bool CalendarFieldYear(JSContext* cx, CalendarId calendar,
                              Handle<CalendarFields> fields, EraYears* result) {
  MOZ_ASSERT(fields.has(CalendarField::Year) ||
             fields.has(CalendarField::EraYear));

  // |eraYear| is to be ignored when not relevant for |calendar| per
  // CalendarResolveFields.
  bool hasRelevantEra =
      fields.has(CalendarField::Era) && CalendarEraRelevant(calendar);
  MOZ_ASSERT_IF(fields.has(CalendarField::Era), CalendarEraRelevant(calendar));

  // Case 1: |year| field is present.
  mozilla::Maybe<EraYear> fromEpoch;
  if (fields.has(CalendarField::Year)) {
    double year = fields.year();
    MOZ_ASSERT(IsInteger(year));

    int32_t intYear;
    if (!mozilla::NumberEqualsInt32(year, &intYear) ||
        mozilla::Abs(intYear) > MaximumCalendarYear(calendar)) {
      ReportCalendarFieldOverflow(cx, "year", year);
      return false;
    }

    fromEpoch = mozilla::Some(CalendarEraYear(calendar, intYear));
  } else {
    MOZ_ASSERT(hasRelevantEra);
  }

  // Case 2: |era| and |eraYear| fields are present and relevant for |calendar|.
  mozilla::Maybe<EraYear> fromEra;
  if (hasRelevantEra) {
    MOZ_ASSERT(fields.has(CalendarField::Era));
    MOZ_ASSERT(fields.has(CalendarField::EraYear));

    auto era = fields.era();
    MOZ_ASSERT(era);

    double eraYear = fields.eraYear();
    MOZ_ASSERT(IsInteger(eraYear));

    auto* linearEra = era->ensureLinear(cx);
    if (!linearEra) {
      return false;
    }

    // Ensure the requested era is valid for |calendar|.
    auto eraCode = EraForString(calendar, linearEra);
    if (!eraCode) {
      if (auto code = QuoteString(cx, era)) {
        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                                 JSMSG_TEMPORAL_CALENDAR_INVALID_ERA,
                                 code.get());
      }
      return false;
    }

    int32_t intEraYear;
    if (!mozilla::NumberEqualsInt32(eraYear, &intEraYear) ||
        mozilla::Abs(intEraYear) > MaximumCalendarYear(calendar)) {
      ReportCalendarFieldOverflow(cx, "eraYear", eraYear);
      return false;
    }

    EraYear eraAndYear;
    if (!CalendarEraYear(cx, calendar, {*eraCode, intEraYear}, &eraAndYear)) {
      return false;
    }
    fromEra = mozilla::Some(eraAndYear);
  }

  *result = {fromEpoch, fromEra};
  return true;
}

struct Month {
  // Month code.
  MonthCode code;

  // Ordinal month number.
  int32_t ordinal = 0;
};

/**
 * CalendarResolveFields ( calendar, fields, type )
 * CalendarDateToISO ( calendar, fields, overflow )
 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow )
 *
 * Extract `month` and `monthCode` from |fields| and perform some initial
 * validation to ensure the values are valid for the requested calendar.
 */

static bool CalendarFieldMonth(JSContext* cx, CalendarId calendar,
                               Handle<CalendarFields> fields,
                               TemporalOverflow overflow, Month* result) {
  MOZ_ASSERT(fields.has(CalendarField::Month) ||
             fields.has(CalendarField::MonthCode));

  // Case 1: |month| field is present.
  int32_t intMonth = 0;
  if (fields.has(CalendarField::Month)) {
    double month = fields.month();
    MOZ_ASSERT(IsInteger(month) && month > 0);

    if (!mozilla::NumberEqualsInt32(month, &intMonth)) {
      intMonth = 0;
    }

    const int32_t monthsPerYear = CalendarMonthsPerYear(calendar);
    if (intMonth < 1 || intMonth > monthsPerYear) {
      if (overflow == TemporalOverflow::Reject) {
        ReportCalendarFieldOverflow(cx, "month", month);
        return false;
      }
      MOZ_ASSERT(overflow == TemporalOverflow::Constrain);

      intMonth = monthsPerYear;
    }

    MOZ_ASSERT(intMonth > 0);
  }

  // Case 2: |monthCode| field is present.
  MonthCode fromMonthCode;
  if (fields.has(CalendarField::MonthCode)) {
    auto monthCode = fields.monthCode();
    int32_t ordinal = monthCode.ordinal();
    bool isLeapMonth = monthCode.isLeapMonth();

    constexpr int32_t minMonth = MonthCode{1}.ordinal();
    constexpr int32_t maxNonLeapMonth = MonthCode::maxNonLeapMonth().ordinal();
    constexpr int32_t maxLeapMonth = MonthCode::maxLeapMonth().ordinal();

    // Minimum month number is 1. Maximum month is 12 (or 13 when the calendar
    // uses epagomenal months).
    const int32_t maxMonth = isLeapMonth ? maxLeapMonth : maxNonLeapMonth;
    if (minMonth <= ordinal && ordinal <= maxMonth) {
      fromMonthCode = MonthCode{ordinal, isLeapMonth};
    }

    // Ensure the month code is valid for this calendar.
    const auto& monthCodes = CalendarMonthCodes(calendar);
    if (!monthCodes.contains(fromMonthCode)) {
      JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                               JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE,
                               MonthCodeString{monthCode}.toCString());
      return false;
    }
  }

  *result = {fromMonthCode, intMonth};
  return true;
}

/**
 * CalendarResolveFields ( calendar, fields, type )
 * CalendarDateToISO ( calendar, fields, overflow )
 * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow )
 *
 * Extract `day` from |fields| and perform some initial validation to ensure the
 * value is valid for the requested calendar.
 */

static bool CalendarFieldDay(JSContext* cx, CalendarId calendar,
                             Handle<CalendarFields> fields,
                             TemporalOverflow overflow, int32_t* result) {
  MOZ_ASSERT(fields.has(CalendarField::Day));

  double day = fields.day();
  MOZ_ASSERT(IsInteger(day) && day > 0);

  int32_t intDay;
  if (!mozilla::NumberEqualsInt32(day, &intDay)) {
    intDay = 0;
  }

  // Constrain to a valid day value in this calendar.
  int32_t daysPerMonth = CalendarDaysInMonth(calendar).second;
  if (intDay < 1 || intDay > daysPerMonth) {
    if (overflow == TemporalOverflow::Reject) {
      ReportCalendarFieldOverflow(cx, "day", day);
      return false;
    }
    MOZ_ASSERT(overflow == TemporalOverflow::Constrain);

    intDay = daysPerMonth;
  }

  *result = intDay;
  return true;
}

/**
 * CalendarResolveFields ( calendar, fields, type )
 *
 * > The operation throws a TypeError exception if the properties of fields are
 * > internally inconsistent within the calendar [...]. For example:
 * >
 * > [...] The values for "era" and "eraYear" do not together identify the same
 * > year as the value for "year".
 */

static bool CalendarFieldEraYearMatchesYear(JSContext* cx, CalendarId calendar,
                                            Handle<CalendarFields> fields,
                                            const capi::ICU4XDate* date) {
  MOZ_ASSERT(fields.has(CalendarField::EraYear));
  MOZ_ASSERT(fields.has(CalendarField::Year));

  double year = fields.year();
  MOZ_ASSERT(IsInteger(year));

  int32_t intYear;
  MOZ_ALWAYS_TRUE(mozilla::NumberEqualsInt32(year, &intYear));

  int32_t yearFromEraYear;
  if (!CalendarDateYear(cx, calendar, date, &yearFromEraYear)) {
    return false;
  }

  // The user requested year must match the actual (extended/epoch) year.
  if (intYear != yearFromEraYear) {
    ToCStringBuf yearCbuf;
    const char* yearStr = NumberToCString(&yearCbuf, intYear);

    ToCStringBuf fromEraCbuf;
    const char* fromEraStr = NumberToCString(&fromEraCbuf, yearFromEraYear);

    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_YEAR,
                              yearStr, fromEraStr);
    return false;
  }
  return true;
}

/**
 * CalendarResolveFields ( calendar, fields, type )
 *
 * > The operation throws a TypeError exception if the properties of fields are
 * > internally inconsistent within the calendar [...]. For example:
 * >
 * > If "month" and "monthCode" in the calendar [...] do not identify the same
 * > month.
 */

static bool CalendarFieldMonthCodeMatchesMonth(JSContext* cx,
                                               Handle<CalendarFields> fields,
                                               const capi::ICU4XDate* date,
                                               int32_t month) {
  int32_t ordinal = capi::ICU4XDate_ordinal_month(date);

  // The user requested month must match the actual ordinal month.
  if (month != ordinal) {
    ToCStringBuf cbuf;
    const char* monthStr = NumberToCString(&cbuf, fields.month());

    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                             JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE_MONTHCODE,
                             MonthCodeString{fields.monthCode()}.toCString(),
                             monthStr);
    return false;
  }
  return true;
}

static ISODate ToISODate(const capi::ICU4XDate* date) {
  UniqueICU4XIsoDate isoDate{capi::ICU4XDate_to_iso(date)};

  int32_t isoYear = capi::ICU4XIsoDate_year(isoDate.get());

  int32_t isoMonth = capi::ICU4XIsoDate_month(isoDate.get());
  MOZ_ASSERT(1 <= isoMonth && isoMonth <= 12);

  int32_t isoDay = capi::ICU4XIsoDate_day_of_month(isoDate.get());

  // TODO: Workaround for <https://github.com/unicode-org/icu4x/issues/5070>.
  if (isoDay == 0) {
    MOZ_ASSERT(capi::ICU4XCalendar_kind(capi::ICU4XDate_calendar(date)) ==
               capi::ICU4XAnyCalendarKind_Indian);
    isoDay = 31;
    isoMonth = 12;
    isoYear -= 1;
  }

  MOZ_ASSERT(1 <= isoDay && isoDay <= ::ISODaysInMonth(isoYear, isoMonth));

  return {isoYear, isoMonth, isoDay};
}

static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendar,
                                      const capi::ICU4XCalendar* cal,
                                      const EraYears& eraYears,
                                      const Month& month, int32_t day,
                                      Handle<CalendarFields> fields,
                                      TemporalOverflow overflow) {
  // Use |eraYear| if present, so we can more easily check for consistent
  // |year| and |eraYear| fields.
  auto eraYear = eraYears.fromEra ? *eraYears.fromEra : *eraYears.fromEpoch;

  UniqueICU4XDate date;
  if (month.code != MonthCode{}) {
    date = CreateDateFromCodes(cx, calendar, cal, eraYear, month.code, day,
                               overflow);
  } else {
    date = CreateDateFrom(cx, calendar, cal, eraYear, month.ordinal, day,
                          overflow);
  }
  if (!date) {
    return nullptr;
  }

  // |year| and |eraYear| must be consistent.
  if (eraYears.fromEpoch && eraYears.fromEra) {
    if (!CalendarFieldEraYearMatchesYear(cx, calendar, fields, date.get())) {
      return nullptr;
    }
  }

  // |month| and |monthCode| must be consistent.
  if (month.code != MonthCode{} && month.ordinal > 0) {
    if (!CalendarFieldMonthCodeMatchesMonth(cx, fields, date.get(),
                                            month.ordinal)) {
      return nullptr;
    }
  }

  return date;
}

/**
 * RegulateISODate ( year, month, day, overflow )
 */

static bool RegulateISODate(JSContext* cx, int32_t year, double month,
                            double day, TemporalOverflow overflow,
                            ISODate* result) {
  MOZ_ASSERT(IsInteger(month));
  MOZ_ASSERT(IsInteger(day));

  // Step 1.
  if (overflow == TemporalOverflow::Constrain) {
    // Step 1.a.
    int32_t m = int32_t(std::clamp(month, 1.0, 12.0));

    // Step 1.b.
    double daysInMonth = double(::ISODaysInMonth(year, m));

    // Step 1.c.
    int32_t d = int32_t(std::clamp(day, 1.0, daysInMonth));

    // Step 3. (Inlined call to CreateISODateRecord.)
    *result = {year, m, d};
    return true;
  }

  // Step 2.a.
  MOZ_ASSERT(overflow == TemporalOverflow::Reject);

  // Step 2.b.
  if (!ThrowIfInvalidISODate(cx, year, month, day)) {
    return false;
  }

  // Step 3. (Inlined call to CreateISODateRecord.)
  *result = {year, int32_t(month), int32_t(day)};
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=86 H=97 G=91

¤ Dauer der Verarbeitung: 0.35 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.