ladybird/Libraries/LibUnicode/Calendars/AdjustedEraCalendar.h
Timothy Flynn 34d7a8fa69 LibUnicode: Handle ICU vs ECMA-402 era formatting discrepancies
ICU's Islamic calendar implementations always set ERA=0, even for dates
before the Hijra (622 CE), using negative year values instead. However,
the CLDR defines two eras: "Anno Hegirae" (era 0) and "Before Hijrah"
(era 1). ECMA-402 expects distinct era names in formatToParts output.
Similarly, ICU's Coptic calendar has an empty CLDR era 0 name, causing
the era parts to be omitted entirely from formatted output.

This patch adds another icu::Calendar subclass to handle these cases.
2026-03-13 14:43:45 -04:00

60 lines
2.4 KiB
C++

/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullOwnPtr.h>
#include <unicode/calendar.h>
namespace Unicode {
// Custom icu::Calendar subclass to fix era handling for calendars where ICU's default behavior does not match ECMA-402
// requirements.
//
// Coptic calendar (SingleEra mode): ICU's Coptic calendar defines two eras (0 and 1), but the CLDR era 0 name is an
// empty string, causing formatToParts to omit the era entirely for pre-epoch dates. ECMA-402 treats Coptic as a
// single-era calendar where all dates use the same era ("Anno Martyrum") and the year can be negative. This subclass
// forces a fixed era value for all dates.
//
// Islamic calendars (DualEra mode): ICU's Islamic calendar implementations always set ERA=0 and allow the year to go
// negative for dates before the Hijra (622 CE). However, ICU's CLDR data defines two eras (era 0 "Anno Hegirae" and
// era 1 "Before Hijrah"), and ECMA-402 expects pre-epoch dates to use a distinct era. This subclass maps positive
// years to ERA=0 (AH) and non-positive years to ERA=1 (BH) with a positive year value.
class AdjustedEraCalendar final : public icu::Calendar {
public:
enum class EraMode : u8 {
SingleEra,
DualEra,
};
AdjustedEraCalendar(NonnullOwnPtr<icu::Calendar> base_calendar, icu::Locale const&, UErrorCode&, EraMode);
~AdjustedEraCalendar() override;
char const* getType() const override { return m_base_calendar->getType(); }
AdjustedEraCalendar* clone() const override;
UClassID getDynamicClassID() const override;
static UClassID getStaticClassID();
private:
AdjustedEraCalendar(AdjustedEraCalendar const&);
void handleComputeFields(int32_t julian_day, UErrorCode&) override;
int64_t handleComputeMonthStart(int32_t extended_year, int32_t month, UBool use_month, UErrorCode&) const override;
int32_t handleGetExtendedYear(UErrorCode&) override;
int32_t handleGetLimit(UCalendarDateFields field, ELimitType limit_type) const override;
UBool haveDefaultCentury() const override { return m_base_calendar->haveDefaultCentury(); }
UDate defaultCenturyStart() const override { return m_base_calendar->defaultCenturyStart(); }
int32_t defaultCenturyStartYear() const override { return m_base_calendar->defaultCenturyStartYear(); }
NonnullOwnPtr<icu::Calendar> m_base_calendar;
EraMode m_era_mode;
};
}