Skip to content

Add Korean Lunar Calendar (음력) support#2375

Open
dsblank wants to merge 11 commits into
gramps-project:masterfrom
dsblank:feature/korean-lunar-calendar
Open

Add Korean Lunar Calendar (음력) support#2375
dsblank wants to merge 11 commits into
gramps-project:masterfrom
dsblank:feature/korean-lunar-calendar

Conversation

@dsblank

@dsblank dsblank commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds the Korean Lunar calendar (음력) as calendar type 8, following the same pattern as the Chinese Lunar calendar added in #2354.

The Korean and Chinese lunar calendars share identical astronomical rules and produce the same dates. The differences are display names only:

  • Traditional Korean month names: 정월 (1st) … 십이월 (12th)
  • Leap months prefixed with 윤 (e.g. 윤사월 for intercalary 4th month)
  • 60-year 간지 (干支) sexagenary cycle in Korean characters: 갑자, 을축, … 계해

New files

File Purpose
gramps/gen/datehandler/_date_ko.py Korean parser and displayer, registered for ko/ko_KR locales
gramps/gen/lib/test/korean_calendar_test.py SDN round-trip, 간지 name, and base handler tests
gramps/gen/datehandler/test/date_ko_test.py Korean parser and displayer tests

Modified files

  • gcalendar.pykorean_lunar_sdn, korean_lunar_ymd, korean_ganji_year
  • date.pyCAL_KOREAN_LUNAR = 8, updated CALENDARS, converter lists and name lists
  • _datestrings.py — "Korean Lunar" calendar name; romanised month names (Jeongwol … Sibiwol) for non-Korean locales
  • _datedisplay.pykorean_lunar attribute; _display_korean_lunar base method
  • _dateparser.pykorean_lunar_to_int class variable; _parse_korean_lunar method
  • datehandler/__init__.py — imports _date_ko
  • gui/editors/editdate.py — adds CAL_KOREAN_LUNAR to month-name map
  • po/POTFILES.skip — excludes _date_ko.py (no translatable strings)

Dependencies

This PR is built on top of #2354 (Chinese Lunar Calendar). Once #2354 is merged to master this branch will be rebased and the diff will show only the Korean-specific changes.

Test plan

  • 49 new unit tests pass (korean_calendar_test.py, date_ko_test.py)
  • Seollal (Korean New Year) anchor dates verified for 2022–2025
  • 간지 year names verified: 갑자 (1984), 갑진 (2024), 을사 (2025)
  • Leap-month round-trips verified (윤4월 2020 = May 23 Gregorian)
  • Korean dates identical to Chinese dates (same astronomical base)
  • Black formatting passes

🤖 Generated with Claude Code

Date Display: US (English) vs KO (Korean)

Description US (English) KO (Korean)
Year only 1949 1949
Month + Year 2000-03 2000년3월
Full date 2000-03-15 2000년3월15일
Before before 2000 2000 이전
After after 1949 1949 이후
About about 1850 약 1850
Before (full date) before 1900-06-15 1900년6월15일 이전
After (full date) after 1800-01-01 1800년1월1일 이후
Estimated estimated 1800 estimated 1800
Calculated calculated 1776-06 calculated 1776년6월
Est. + Before estimated before 1900 estimated 1900 이전
Calc. + After calculated after 1800 calculated 1800 이후
Span (from/to) 1900 - 1910 1900에서 1910까지
Span (full dates) 1905-03-01 - 1912-11-30 1905년3월1일에서 1912년11월30일까지
Range (between/and) 1900 - 1910 1900에서 1910사이
Julian 1582-02-10 (Julian) 1582년2월10일 (율리우스력)
Hebrew 5780-01-01 (Hebrew) 5780년1월1일 (히브리력)
Islamic 1400-01 (Islamic) 1400년1월 (이슬람력)
French Rep. 10-03-01 (French Republican) 10년3월1일 (프랑스 혁명력)
Persian 1400-01 (Persian) 1400년1월 (페르시안력)
Swedish 1712-03 (Swedish) 1712년3월 (스웨덴 달력)
Korean Lunar (full) 2000-01-15 (Korean Lunar) 2000년 정월 15일 (Korean Lunar)
Korean Lunar (year) 2000 (Korean Lunar) 2000년 (Korean Lunar)
Korean Lunar (month) 2023-05 (Korean Lunar) 2023년 오월 (Korean Lunar)
Before (Korean Lunar) before 1900 (Korean Lunar) 1900년 이전 (Korean Lunar)
After (Korean Lunar) after 1900 (Korean Lunar) 1900년 이후 (Korean Lunar)
간지 (full) 2000-01-15 (Korean Lunar) 경진년 정월 15일 (Korean Lunar)
간지 (year) 2000 (Korean Lunar) 경진년 (Korean Lunar)
Text only sometime in the 1800s sometime in the 1800s

dsblank and others added 6 commits June 10, 2026 20:42
Adds a new CAL_CHINESE_LUNAR (value 7) calendar type alongside the
existing Gregorian, Julian, Hebrew, French Republican, Persian, Islamic,
and Swedish calendars. Conversion between Chinese Lunar dates and the
internal SDN (Serial Date Number) representation is implemented in
gcalendar.py using a compact year-info table (17 bits per year, covering
1900-2099) derived from the lunardate package (GPL-2, Fung F. Lee,
Ricky Yeung, LI Daobing). Leap months are encoded as month + 100
(e.g. month 104 = intercalary 4th month). Month names use pinyin
romanisation (Zhengyue...Shier'yue) so they can be translated for any
locale. Parsing accepts both pinyin names and YYYY-MM[-DD] numeric
notation with full leap-month support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Gramps runs under a Simplified (zh_CN) or Traditional (zh_TW)
Chinese locale, Chinese Lunar calendar dates are now displayed as
native 年/月/日 strings (e.g. 1976年八月8日) instead of pinyin
romanisation. Leap months use the correct prefix — 闰 for Simplified,
閏 for Traditional (e.g. 1976年闰八月8日 for month 108).

The parsers now recognise Chinese character month names (正月, 二月 ...
十二月) and their leap forms as input, in addition to the pinyin names
already supported by the base English parser. Calendar keywords 农历 /
阴历 / 旧历 (Simplified) and 農曆 / 陰曆 / 舊曆 (Traditional) are also
accepted as the Chinese Lunar calendar specifier. Base DateParser gains
dedicated _cltext/_cltext2 regexes built from chinese_lunar_to_int so
that month-name parsing works for any locale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces chinese_sexagenary_year(year) in gcalendar.py, which maps
any Gregorian-aligned Chinese year to its 干支 name (e.g. 1984 → 甲子,
2024 → 甲辰) using the standard (year - 4) % 10 / % 12 formula with the
ten Heavenly Stems (天干) and twelve Earthly Branches (地支).

The zh_CN and zh_TW date displayers gain a third format option
"干支年格式" (index 2). When selected, Chinese Lunar dates render as
e.g. 甲子年八月8日 or 甲辰年闰八月8日 instead of a numeric year.

Three new unit tests cover: known year values (1984 甲子, 2024 甲辰,
1900 庚子, 1949 己丑, 2025 乙巳), the 60-year cycle property, and
that all 60 stem-branch combinations appear exactly once per cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the base _display_chinese_lunar, passing a remapped date_val
(month 108 → 8) to _display_calendar caused the ISO string to show
month 08 instead of 108, breaking parser round-trips. Fixed by falling
back to display_iso whenever the month is a leap month (> 100), which
preserves the raw encoding for all non-zh-locale formats. Locale-specific
handlers (zh_CN, zh_TW) override this method and render leap months
natively (闰八月 / 閏八月) so they are unaffected.

Also adds Chinese Lunar months 1-12 and the known 1976 leap 8th month
(108) to the non-Gregorian calendar loop in date_test.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 200-entry (1900-2099) SDN lookup table with a 9600-entry
table covering years 400-9999, generated from the tyme4py library
(MIT licence, https://github.com/6tail/tyme4py).

The table stores the same 17-bit encoding per year (12 regular month
sizes, leap month index, leap month size).  The SDN anchor is derived
by working backward from the verified reference point Lunar 1600/1/1 =
Gregorian 1600-02-14, so the 1600-9999 range is verifiably exact.
Years 400-1599 are also accurate; years 1-399 are excluded because
tyme4py's astronomical reconstruction for that period uses a different
leap-month schedule than the historically recorded Han-dynasty calendar,
introducing ~59 days of systematic drift.

The generation script scripts/gen_chinese_lunar_table.py can be run to
regenerate the table from tyme4py if the upstream library is updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the Korean Lunar calendar as calendar type 8, following the same
pattern as the Chinese Lunar calendar.  The Korean and Chinese lunar
calendars share identical astronomical rules and dates; the only
differences are the display names (Korean month names and 간지 year
names).

New files:
- gramps/gen/datehandler/_date_ko.py — Korean parser and displayer,
  registered for ko/ko_KR locales.  Month names use traditional Korean
  (정월…십이월), leap months are prefixed with 윤, and format 2 shows the
  60-year 간지 (干支) sexagenary cycle (갑자…계해).
- gramps/gen/lib/test/korean_calendar_test.py — SDN round-trip, 간지
  name, and base handler tests.
- gramps/gen/datehandler/test/date_ko_test.py — Korean parser and
  displayer tests.

Modified files: gcalendar.py (korean_lunar_sdn, korean_lunar_ymd,
korean_ganji_year), date.py (CAL_KOREAN_LUNAR = 8), _datestrings.py
(Korean Lunar calendar name and romanised month names), _datedisplay.py
(_display_korean_lunar), _dateparser.py (korean_lunar_to_int,
_parse_korean_lunar), __init__.py, editdate.py, po/POTFILES.skip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five bugs caused 1854 test failures for lang='ko':

- calendar_to_int used short names ("율리우스") while the translations
  produce the full calendar names with the 력 suffix ("율리우스력",
  "히브리력", "이슬람력", "프랑스 혁명력", "페르시안력", "스웨덴 달력").
  Added both forms so either spelling is accepted.
- All modifier and quality msgstr in ko.po were empty, causing display
  to fall back to English ("before ", "after ", "about ", "from ",
  "to ", "estimated ", "calculated ").  Added English fallbacks to
  modifier_to_int and quality_to_int so the parser accepts them.
- The span regex used "부터...까지" but the Korean span display
  format is "<start>에서 <stop>까지".  Replaced with an infix
  "에서...까지" pattern using named groups start/stop.
- The range regex used "사이...와" but the Korean range display format
  is "<start>에서 <stop>사이".  Replaced with an infix "에서...사이"
  pattern.
- The ko.po span and range format templates put {date_quality} at the
  end after 까지/사이, so the parser could never strip quality before
  parsing the span.  Moved {date_quality} to the front of both
  templates to match how every other locale orders the fields.
- Collapsed a 3-line _klmon_str assignment to one line to satisfy
  Black's line-length formatting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dsblank dsblank marked this pull request as ready for review June 12, 2026 18:57
dsblank and others added 4 commits June 12, 2026 12:04
Fill in po/ko.po modifier msgstr values (이전/이후/약/부터/까지) and
quality msgstr values (추정/계산) so the date displayer outputs Korean
rather than falling back to English. Round-trip parsing is preserved
because DateParserKO.modifier_to_int and quality_to_int already list
these Korean keywords.
Po file changes should be submitted through Weblate, not via pull
requests directly to the repository.
…abel.

The Korean locale translates the span display format as
"{nonstd_calendar_and_ny}{date_start}에서 {date_stop}까지 {date_quality}",
placing the (calendar) label at the start and the quality word at the end.
After strip() the leading whitespace was gone, so the old _cal regex
"(.*)\s+\(cal\)(.*)" (requiring \s+ before the opening parenthesis) never
matched, causing the entire date to be stored as text-only.

Two regex fixes in _dateparser.init_strings():

* _cal, _calny, _calny_iso, _ny, _ny_iso: change (.*)\s+ to (.*?)\s* so
  that a "(CalendarName)" at position 0 (no leading whitespace) is
  recognised.  Using non-greedy (.*?) keeps the leftmost-match behaviour
  identical to the old greedy approach for the common case where the
  calendar label appears at the end.

* _qual: extend the pattern from "(.* ?)QUAL\s+(.+)" to
  "(.* ?)QUAL(?:\s+(.+)|\s*$)" so quality words that appear at the very
  end of the string (nothing after them) are also stripped.

match_quality updated to handle the new optional group(3) (None when
quality is at the end).

These changes resolve 1344 CI failures in
DateHandlerTest.test_span for lang='ko'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Korean, temporal modifiers meaning "before" and "after" follow the
date: "2000년 이전" (before 2000), "1949년 이후" (after 2000). Previously
both were in modifier_to_int (prefix), so "2000년 이전" was treated as
unrecognised text and the display fell back to English "before"/"after".

- Move 이전/이후 from modifier_to_int to modifier_after_to_int in
  DateParserKO. English fallbacks (before/after) remain in
  modifier_to_int so stored dates with English text still parse.
- Override _modifier_after regex in init_strings to use \s* so that
  both "2000년 이전" (space) and "2000년이전" (no space) are accepted.
- Add __init__ to DateDisplayKO to patch _mod_str with leading-space
  postfix strings for 이전/이후 and prefix "약 " for MOD_ABOUT, and to
  restore korean_lunar Hangul month names overwritten by DateDisplay.__init__.
- Update test_modifier_ijeon/ihu to check modifier_after_to_int.
- Add TestKoreanModifierParsing covering parser, display word order,
  English fallbacks, and round-trips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant