一
为什么春节的日期每年都在变化?
这是因为这些中国传统节日并不是基于公历确定的,而是根据中国传统历法中的农历来推算。一个简单的判断方法是,只需要确定立春当日对应的农历日期即可。若立春位于当月农历的 1 至 15 日之间,则这个农历月就对应春节至元宵所在的正月;若立春位于当月农历十五之后,则下一个农历月的初一才是春节。以 2023 年为例,立春时间为公历 2 月 4 日,这一天对应农历十四,因此这个农历月就被视作正月,本月农历初一也就是正月初一。
至于节气的具体日期如何测算,这本身是一项相对复杂的天文计算,通常由中国科学院紫金山天文台等机构进行测算并向社会公布。
一种简单的计算方法如下。
近似公式:
\[D = \lfloor 0.2422Y + C\rfloor-\lfloor\frac{Y}{4}\rfloor\]其中:
D:节气所在公历日Y:年份后两位C:该节气对应常数
对 2000–2099,可取:
\[L = \lfloor\frac{Y}{4}\rfloor\]对 1901–1999,可取:
\[L = \lfloor\frac{Y - 1}{4}\rfloor\]常数对照表如下:
| 节气 | 月份 | 20世纪C值 | 21世纪C值 |
|---|---|---|---|
| 小寒 | 1 | 6.11 | 5.4055 |
| 大寒 | 1 | 20.84 | 20.12 |
| 立春 | 2 | 4.6295 | 3.87 |
| 雨水 | 2 | 19.4599 | 18.73 |
| 惊蛰 | 3 | 6.3826 | 5.63 |
| 春分 | 3 | 21.4155 | 20.646 |
| 清明 | 4 | 5.59 | 4.81 |
| 谷雨 | 4 | 20.888 | 20.1 |
| 立夏 | 5 | 6.318 | 5.52 |
| 小满 | 5 | 21.86 | 21.04 |
| 芒种 | 6 | 6.5 | 5.678 |
| 夏至 | 6 | 22.20 | 21.37 |
| 小暑 | 7 | 7.928 | 7.108 |
| 大暑 | 7 | 23.65 | 22.83 |
| 立秋 | 8 | 8.35 | 7.5 |
| 处暑 | 8 | 23.95 | 23.13 |
| 白露 | 9 | 8.44 | 7.646 |
| 秋分 | 9 | 23.822 | 23.042 |
| 寒露 | 10 | 9.098 | 8.318 |
| 霜降 | 10 | 24.218 | 23.438 |
| 立冬 | 11 | 8.218 | 7.438 |
| 小雪 | 11 | 23.08 | 22.36 |
| 大雪 | 12 | 7.9 | 7.18 |
| 冬至 | 12 | 22.60 | 21.94 |
Python Code
from math import floor
SOLAR_TERMS = [
("小寒", 1, 6.11, 5.4055),
("大寒", 1, 20.84, 20.12),
("立春", 2, 4.6295, 3.87),
("雨水", 2, 19.4599, 18.73),
("惊蛰", 3, 6.3826, 5.63),
("春分", 3, 21.4155, 20.646),
("清明", 4, 5.59, 4.81),
("谷雨", 4, 20.888, 20.1),
("立夏", 5, 6.318, 5.52),
("小满", 5, 21.86, 21.04),
("芒种", 6, 6.5, 5.678),
("夏至", 6, 22.20, 21.37),
("小暑", 7, 7.928, 7.108),
("大暑", 7, 23.65, 22.83),
("立秋", 8, 8.35, 7.5),
("处暑", 8, 23.95, 23.13),
("白露", 9, 8.44, 7.646),
("秋分", 9, 23.822, 23.042),
("寒露", 10, 9.098, 8.318),
("霜降", 10, 24.218, 23.438),
("立冬", 11, 8.218, 7.438),
("小雪", 11, 23.08, 22.36),
("大雪", 12, 7.9, 7.18),
("冬至", 12, 22.60, 21.94),
]
TERM_EXCEPTIONS = {
# "小寒": {1982: +1, 2019: -1},
# "立春": {2021: -1},
# "雨水": {2026: -1},
}
def leap_correction(year: int) -> int:
y = year % 100
if 1901 <= year <= 1999:
return floor((y - 1) / 4)
elif 2000 <= year <= 2099:
return floor(y / 4)
else:
raise ValueError
def get_c_value(year: int, c20: float, c21: float) -> float:
if 1901 <= year <= 1999:
return c20
elif 2000 <= year <= 2099:
return c21
else:
raise ValueError
def solar_term_day(year: int, name: str, month: int, c20: float, c21: float) -> int:
y = year % 100
c = get_c_value(year, c20, c21)
l = leap_correction(year)
day = floor(y * 0.2422 + c) - l
day += TERM_EXCEPTIONS.get(name, {}).get(year, 0)
return day
def list_solar_terms(year: int):
result = []
for name, month, c20, c21 in SOLAR_TERMS:
day = solar_term_day(year, name, month, c20, c21)
result.append((name, month, day))
return result
if __name__ == "__main__":
year = 2026
print(f"{year} 年二十四节气")
for name, month, day in list_solar_terms(year):
print(f"{name}: {year}-{month:02d}-{day:02d}")
二
民间常把农历称作阴历,因为它确实与月相盈亏密切相关;但严格来说,农历并不是纯粹的阴历,而是一种阴阳合历。与基于太阳周年运动的阳历不同,它并不单纯根据地球绕太阳公转的轨迹来计算。更准确地说,农历中的年与太阳相关,而月份和日期则由朔望月决定。朔望月指的是月亮连续两次合朔之间的时间。在古代,人们将朔定义为全黑的新月,在天文学上,它对应月亮黄经与太阳黄经相同的时刻;此时月亮位于地球和太阳之间,在地球上只能看见月亮的暗面。而望则对应满月,是从地球观测时月亮被太阳完全照亮的状态,此时太阳与月亮的黄经相差 180 度。一个朔望月大约是 29.53 天,因此一年十二个朔望月约为 $29.53 \times 12 = 354.36$ 天,比公历年要少若干天。为了弥补这一差距,农历采用置闰的方式进行调整,其基本规则就是「十九年七闰」,即在 19 年中加入 7 个闰月。这样一来,19 年中农历的实际天数约为 $29.53 \times (12 \times 19 + 7) = 6939.55$,与公历的 $365.24 \times 19 = 6939.56$ 已经非常接近了。
再说说回归年和恒星年。回归年是指从地球上观察,太阳再次回到黄道上同一位置所经历的时间,具体约为 365 天 5 小时 48 分 46 秒。恒星年则是地球真正的公转周期,也就是太阳在天球上返回相对于恒星同一位置所经历的时间,约为 365 天 6 小时 9 分 10 秒。农历将一个回归年大致等分为 24 份,这便是二十四节气,每一段大约为 15.2 天。
一个有趣的小知识:由于岁差的影响,极星也会发生变化。现在的北极星是位于小熊座的勾陈一,而两千和四千年后分别会变为仙王座的两颗星,在14000年后又会变为位于天琴座的织女星,在25000年后勾陈一会再次成为北极星。
三
星空和时间是浪漫与想象的集合,在广袤的未知中蕴藏着无限的可能。宏大的星图卷轴在每一个夜空中徐徐展开,足以令人陶醉其中而无法自拔。面对浩瀚的星宇,我们自己只是历史长河中的一瞬,是有一天终会被忘记的尘埃。所以我们过去做了什么并不重要,我们将来如何被记住也不重要,唯一重要的,就是此时此刻。
2026年4月更新,原写于2023年9月30日。