Skip to content

[HK] Add Hong Kong national holiday calendar #257

Description

@davejoyce

Module(s) Affected

holiday-calendar-apac

Problem / Motivation

No Hong Kong holiday calendar exists in this library today. Hong Kong's statutory holidays are gazetted under Cap. 149 (General Holidays Ordinance) and include a mix of Western (colonial-era) and Chinese lunar-calendar holidays, plus an unusual asymmetric weekend in-lieu rule not currently modeled by any existing DateRoll in this codebase.

Proposed Solution

Create HolidayCalendarServiceHK (code HK) as the national/general statutory holiday calendar, following the ISO 3166-1 alpha-2 convention. This is the base calendar that XHKG (#258) builds on top of.

Holiday list

  • New Year's Day (Jan 1, fixed)
  • Lunar New Year (3-day cluster: eve + Day 1 + Day 2) — reuses org.holiday.calendar.observance.lunar.ChineseNewYearDay, widening its dayNumber guard to allow 0 for "eve"; see Open Questions below for an alternative
  • Ching Ming Festival (solar term ~Apr 4-5)
  • Good Friday and Easter Monday — needs independent verification against Cap. 149 / HK Labour Department gazette before merging; do not copy-paste from Singapore's precedent (SG only has Good Friday, no Easter Monday) or assume UK-style Easter Monday-only. HK's actual ordinance is understood to include Good Friday, the day following Good Friday, and Easter Monday as three separate entries — confirm exact count and names against the primary source.
  • Labour Day (May 1, fixed)
  • Buddha's Birthday (4th lunar month, 8th day) — needs a new algorithmic observance class, BuddhasBirthday in observance.lunar (Time4J-backed, EastAsianMonth.valueOf(4), day 8) — distinct from the existing Singapore VesakDay (different day-of-month, different data source: CSV/gazette table vs. algorithmic)
  • Tuen Ng Festival / Dragon Boat Festival (5th lunar month, 5th day)
  • HKSAR Establishment Day (Jul 1, fixed)
  • Day following Mid-Autumn Festival (8th lunar month, 15th day, +1 day) — needs a day-offset variant of MidAutumnFestival
  • National Day (Oct 1, fixed)
  • Chung Yeung Festival / Double Ninth Festival (9th lunar month, 9th day)
  • Christmas Day (Dec 25, fixed) and the first weekday after Christmas Day

Weekend roll — asymmetric in-lieu rule

Hong Kong's Employment Ordinance in-lieu rule is asymmetric, unlike anything currently in DateRolls:

  • Falls on Sunday → substitute on the next day that is not itself a weekend day or another statutory/substitute holiday (iteratively skip forward)
  • Falls on Saturdayno substitute at all (statute treats Saturday as a working day; some employers grant a courtesy Monday, but this is not the statutory rule and should not be modeled as such)
  • Falls on a weekday → observed as-is

New class: org.holiday.calendar.observance.hk.HKDateRolls.sundayToNextNonHolidayWeekday(List<Holiday> allHolidays) — a holiday-set-aware DateRoll closure. Note: this is genuinely new mechanics, not a straightforward reuse of UKDateRolls.fixedHolidayRoll — that method is a hardcoded 3-holiday switch with flat day offsets, not a generic iterating list-scan. Budget this as new development.

Cross-year boundary question — RESOLVED (structural proof, not just empirical absence). A researcher checked 15 years of actual HK Government gazetted "General Holidays" schedules (2012–2027) plus the definitional bounds of Lunar New Year, and confirmed the skip-forward walk can never cross the Dec 31/Jan 1 boundary:

  • HK's only late-December holidays are Dec 25 (Christmas Day) and Dec 26 (day following Christmas Day) — nothing else falls between Dec 27–31.
  • The worst-case chain is Dec 25 landing on Sunday with Dec 26 (Monday, itself a holiday) forcing the walk to continue — and it always terminates at Dec 27, since Dec 27 is never itself a holiday. Confirmed in every weekend-collision year found (2016, 2021, 2022, 2027 — all resolved to Dec 27, the latest possible substitute date observed or derivable).
  • The related "Lunar New Year's Eve could itself fall in the previous year" concern (if LNY Day 1 landed on Jan 1/2) is also structurally impossible: Chinese New Year is defined to fall between Jan 21 and Feb 20 every year (second new moon after the winter solstice), so it can never land on Jan 1 or 2.

Conclusion: HKDateRolls.sundayToNextNonHolidayWeekday is correctly scoped to same-calendar-year-only. Document this structural proof in the class Javadoc (not just as an implementation limitation, but as a proven invariant of HK's actual holiday set) so a future reader doesn't mistake the same-year scoping for an oversight.

Scope

  • Verify Good Friday/Easter Monday/day-after-Good-Friday holiday list against Cap. 149 primary source
  • Create org.holiday.calendar.observance.lunar.BuddhasBirthday (new, East Asian 4th-month/8th-day convention)
  • Add day-offset variant for Mid-Autumn day-after (either widen MidAutumnFestival or add a sibling class)
  • Widen ChineseNewYearDay's dayNumber guard to allow 0, update Javadoc to document the new "eve" semantics, OR add a sibling ChineseNewYearEve class instead (recommended — avoids changing the documented contract of a class two already-shipped calendars, CN/CNY, depend on)
  • Create org.holiday.calendar.observance.hk.HKDateRolls with the asymmetric Sunday/Saturday roll, scoped to same-calendar-year-only per the resolved structural proof above (document the proof in the class Javadoc, don't just silently assume it)
  • Create package-private HKHolidays shared holiday-list factory in org.holiday.calendar.impl (mirrors IsraelHolidays)
  • Create HolidayCalendarServiceHK
  • Register in holiday-calendar-apac/src/main/java/module-info.java (provides entry; export new observance.hk package)

Testing

  • HolidayCalendarServiceHKTest — metadata, count, golden-master floating-holiday dates for 2024-2026 (Lunar New Year, Ching Ming, Buddha's Birthday, Tuen Ng, Mid-Autumn day-after, Chung Yeung), factory discovery, chronological ordering, name definitions
  • Explicit named (not just parametrized) negative tests: testTuenNg2025SaturdayNoRoll() (May 31, 2025 stays put, no in-lieu), testMidAutumnDayAfter2026SaturdayNoRoll() (Sep 26, 2026 stays put), testChingMing2026SundayRollsToMonday() (positive contrast: Apr 5, 2026 Sunday → Apr 6 Monday)
  • testChristmasCluster2026() — Dec 25, 2026 (Fri, no roll) + day-after Dec 26 (Sat) — assert no phantom in-lieu entry
  • testChristmasSubstituteNeverExceedsDec27(int year) — parametrized over the confirmed historical/derivable weekend-collision years (2016, 2021, 2022, 2027) — assert the substitute always lands on Dec 27 and never later, as the concrete regression guard for the resolved structural proof
  • HKDateRollsTest — direct unit tests of the roll function in isolation: single-hop skip, multi-hop skip (immediately-following Monday also a holiday), Saturday-never-rolls. Include testCrossYearBoundaryInputIsUnreachableForHK() (or similar) as a defensive unit test documenting that a cross-year-spanning holiday list, while mechanically constructible as an input to the isolated function, never actually arises from HK's real holiday set per the resolved structural proof — keep this test's docstring explicit that it's defensive, not a live production scenario
  • BuddhasBirthdayTest — golden-master dates (2024 May 15, 2025 May 5, 2026 May 24 raw/pre-roll), null-safety, and an explicit testDiffersFromVesakDay() sanity check proving this isn't an accidental alias of the existing Singapore class
  • Cross-check test for the Mid-Autumn day-after variant: assert it always equals MidAutumnFestival().apply(year).plusDays(1), not just independently-hardcoded dates
  • Add HK to HolidayCalendar30YearIT's allCalendarCodes() for the 4 generic checks (range completeness, min holiday count, no nulls, chronological order)
  • Add a bespoke 30-year IT section re-deriving expected Saturday-no-roll/Sunday-rolls behavior for all floating holidays across 2026-2055, including an assertion that no Christmas-cluster substitute ever falls after Dec 27 or crosses into the following year — this section is no longer gated as a TODO, since the cross-year-boundary question is resolved

Verification

  • mvn -pl holiday-calendar-apac clean install
  • mvn -pl tests clean verify
  • factory.create("HK").dataValidThrough() returns OptionalInt.empty() (fully algorithmic, no year ceiling)

Related

Alternatives Considered

Considered modeling the Saturday-no-roll case by simply omitting rollable(true) on Saturday-adjacent holidays — rejected because rollability must be evaluated per-instance-year (a holiday can fall on Saturday in one year and Sunday in another), not statically per-holiday-definition.

Additional Context

Research package flagged Ching Ming's exact date and the Saturday-in-lieu "courtesy Monday" government/employer practice (as distinct from statute) as open verification items — see parent issue notes.

Cross-year boundary resolution (2026 follow-up research): verified against 15 years (2012–2027) of HK Government gazetted "General Holidays" schedules. Every weekend-collision year for the Dec 25/26 cluster (2016, 2021, 2022, 2027) resolved to a Dec 27 substitute, never later — and no other HK holiday falls between Dec 27–31 to extend the chain further, so a cross-year walk is structurally impossible, not merely unobserved. Lunar New Year's Eve crossing into the previous year is similarly impossible since Chinese New Year is defined to fall between Jan 21–Feb 20 every year.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestnew-marketRequest for creation of a holiday calendar for a new "market" (central bank / market / nation)

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions