Appearance
Lunisolar Calendar (農曆)
stembranch computes the Chinese lunisolar calendar from first principles using astronomical new moon calculations and solar term interpolation.
How It Works
The Chinese calendar (農曆) is lunisolar — lunar months reconciled with the solar year:
- Months start at each new moon (朔日)
- Year boundaries are aligned with the solar cycle via intercalary (leap) months
- Month numbering is determined by which major solar term (中氣) falls within each lunar month
Step 1: New Moon Calculation
New moon moments are computed using Meeus Ch. 49 (Jean Meeus, Astronomical Algorithms):
- Estimate the lunation number k from the target date
- Compute the approximate new moon JDE using a polynomial in k
- Apply 14 planetary correction terms and 25 additional periodic terms
- Convert from TT to UT via ΔT
Step 2: Solar Term Mapping
The 12 中氣 (major solar terms at 30° intervals) determine month numbers:
| 中氣 | Longitude | Month Number |
|---|---|---|
| 雨水 | 330° | 1 (正月) |
| 春分 | 0° | 2 |
| 穀雨 | 30° | 3 |
| 小滿 | 60° | 4 |
| 夏至 | 90° | 5 |
| 大暑 | 120° | 6 |
| 處暑 | 150° | 7 |
| 秋分 | 180° | 8 |
| 霜降 | 210° | 9 |
| 小雪 | 240° | 10 |
| 冬至 | 270° | 11 |
| 大寒 | 300° | 12 |
A lunar month is assigned the number of the 中氣 that falls within it.
Step 3: Intercalary Month Detection
When a lunar month contains no 中氣, it becomes an intercalary (leap) month (閏月). It takes the number of the preceding month with a 閏 prefix.
This is the No-Zhongqi Rule (無中氣置閏法), the standard rule used since the Qing dynasty Shíxiàn calendar (時憲曆, 1645).
The 2033 Problem
In some years, multiple lunar months lack a 中氣, creating ambiguity about which month is intercalary. The most notable case is 2033, where the standard algorithm would place the leap month incorrectly without the Winter Solstice Rule (冬至必在十一月): the month containing 冬至 must be month 11.
stembranch handles this correctly — validated against Hong Kong Observatory data.
API
typescript
import { gregorianToLunisolar } from 'stem-branch';
// Gregorian → lunisolar (農曆) date
gregorianToLunisolar(new Date(2024, 1, 10));
// → { year: 2024, month: 1, day: 1, isLeapMonth: false }