一、项目背景详细介绍

农历(又称阴历)是中国传统历法,广泛用于节日、纪念日、生肖等文化活动中。
然而现代计算机系统和国际标准普遍采用的是公历(Gregorian Calendar,阳历)
在许多程序中,常常需要实现农历与公历的互相转换,例如:

  • 农历生日提醒;

  • 中国传统节日计算(如春节、端午、中秋);

  • 星座与生肖计算;

  • 民俗文化类软件;

  • 日历控件展示。

Windows 系统 API(如 GetDateFormat)仅支持公历,而不直接提供农历支持。
因此我们需要自行实现一个农历与公历的转换算法

农历算法较为复杂,主要由于:

  1. 农历以月相为基础,一个月为 29 或 30 天;

  2. 每年长度不固定;

  3. 每 2~3 年会插入一个闰月;

  4. 农历月份与公历日期并非线性对应;

  5. 农历年从春节开始而非 1 月 1 日。

因此我们需要一个可靠的算法表和程序逻辑来实现精确转换。


二、项目需求详细介绍

功能性需求

  1. 支持指定公历日期 → 农历日期转换;

  2. 支持农历日期 → 公历日期转换;

  3. 支持闰月识别与转换;

  4. 支持 1900 年至 2100 年之间的日期;

  5. 输出格式清晰可读(如“农历2025年闰二月初八”);

  6. 可扩展到计算节日和生肖。

技术性需求

  • 开发语言:VC++(Visual Studio 2019/2022)

  • 运行环境:Windows(Win32 控制台应用)

  • 编译标准:C++17

  • 精度要求:与国家天文历法保持一致;

  • 时间跨度:1900 年至 2100 年;

  • 主要数据结构:位压缩年表(以 0x 数字形式存储每月天数及闰月)。


三、相关技术详细介绍

1. 农历数据表示法(Lunar Data Encoding)

我们通常用一个 32 位整数来存储一年的农历信息:

位范围 含义
0–3位 闰月月份(0 表示无闰月)
4–15位 每月天数(1 表示 30 天,0 表示 29 天)
16位 闰月天数标志(1 表示闰月30天,0 表示29天)
17–31位 用于年份偏移或校验(可忽略)

例如:


0x04bd8

表示 1900 年农历的月结构:闰八月,大小月分布按位确定。

2. 公历与农历的基准日

为了实现转换,需要确定一个固定的参考点:

  • 公历基准:1900 年 1 月 31 日(公历);

  • 对应农历基准:1900 年正月初一(农历)。

所有农历日期都可以基于此基准偏移计算。

3. 基本公式

  • 公历 → 农历

    1. 计算目标日期与基准日的天数差;

    2. 循环减去每个农历年的天数;

    3. 确定农历年;

    4. 继续减去各月天数,确定月份与日期。

  • 农历 → 公历

    1. 累加从基准年到指定农历日期的天数;

    2. 在基准公历日期上加上偏移。


四、实现思路详细介绍

  1. 建立农历年份表

    • 以数组形式存储 1900~2100 年的农历年信息;

    • 每个元素为 DWORD 型,按位存储月大小与闰月。

  2. 计算农历每年天数

    • 遍历 12 月(含闰月)计算总天数;

    • 判断闰月天数并累计。

  3. 计算每月天数

    • 读取年份数据相应位;

    • 若为 1,则为 30 天,否则 29 天。

  4. 计算闰月信息

    • 若有闰月,则插入在正常月份后;

    • 在转换时判断是否进入闰月。

  5. 日期转换核心算法

    • 公历转农历:通过天数差递减判断;

    • 农历转公历:通过总天数累加。

  6. 格式化输出

    • 转换后的农历日期格式化为中文描述;

    • 支持显示“闰月”。


五、完整实现代码

/**********************************************************************
 * 文件名: LunarCalendarConverter.cpp
 * 功能: 实现农历与公历互转功能
 * 作者: ChatGPT 教学版
 * 环境: Visual Studio 2019 / 2022
 * 编译: C++17
 **********************************************************************/

#include <iostream>
#include <windows.h>
#include <string>
using namespace std;

//=================== 农历数据表 (1900~2100) =====================//
// 每个元素表示一年的农历数据,包含闰月、大小月、闰月天数信息
static const DWORD LunarData[201] = {
    0x04bd8,0x04ae0,0x0a570,0x054d5,0x0d260,0x0d950,0x16554,0x056a0,
    0x09ad0,0x055d2,0x04ae0,0x0a5b6,0x0a4d0,0x0d250,0x1d255,0x0b540,
    0x0d6a0,0x0ada2,0x095b0,0x14977,0x04970,0x0a4b0,0x0b4b5,0x06a50,
    0x06d40,0x1ab54,0x02b60,0x09570,0x052f2,0x04970,0x06566,0x0d4a0,
    0x0ea50,0x06e95,0x05ad0,0x02b60,0x186e3,0x092e0,0x1c8d7,0x0c950,
    0x0d4a0,0x1d8a6,0x0b550,0x056a0,0x1a5b4,0x025d0,0x092d0,0x0d2b2,
    0x0a950,0x0b557,0x06ca0,0x0b550,0x15355,0x04da0,0x0a5b0,0x14573,
    0x052b0,0x0a9a8,0x0e950,0x06aa0,0x0aea6,0x0ab50,0x04b60,0x0aae4,
    0x0a570,0x05260,0x0f263,0x0d950,0x05b57,0x056a0,0x096d0,0x04dd5,
    0x04ad0,0x0a4d0,0x0d4d4,0x0d250,0x0d558,0x0b540,0x0b5a0,0x195a6,
    0x095b0,0x049b0,0x0a974,0x0a4b0,0x0b27a,0x06a50,0x06d40,0x0af46,
    0x0ab60,0x09570,0x04af5,0x04970,0x064b0,0x074a3,0x0ea50,0x06b58,
    0x05ac0,0x0ab60,0x096d5,0x092e0,0x0c960,0x0d954,0x0d4a0,0x0da50,
    0x07552,0x056a0,0x0abb7,0x025d0,0x092d0,0x0cab5,0x0a950,0x0b4a0,
    0x0baa4,0x0ad50,0x055d9,0x04ba0,0x0a5b0,0x15176,0x052b0,0x0a930,
    0x07954,0x06aa0,0x0ad50,0x05b52,0x04b60,0x0a6e6,0x0a4e0,0x0d260,
    0x0ea65,0x0d530,0x05aa0,0x076a3,0x096d0,0x04bd7,0x04ad0,0x0a4d0,
    0x1d0b6,0x0d250,0x0d520,0x0dd45,0x0b5a0,0x056d0,0x055b2,0x049b0,
    0x0a577,0x0a4b0,0x0aa50,0x1b255,0x06d20,0x0ada0
};

// 农历月份中文表示
static const wchar_t* LunarMonthStr[13] = {
    L"正月", L"二月", L"三月", L"四月", L"五月", L"六月",
    L"七月", L"八月", L"九月", L"十月", L"冬月", L"腊月", L"闰月"
};

// 农历日中文表示
static const wchar_t* DayStrPrefix[4] = { L"初", L"十", L"廿", L"卅" };
static const wchar_t* DayStrNum[10] = { L"一", L"二", L"三", L"四", L"五", L"六", L"七", L"八", L"九", L"十" };

//=================================================================//
// 函数声明
int GetLunarYearDays(int year);
int GetLunarMonthDays(int year, int month);
int GetLeapMonth(int year);
int GetLeapMonthDays(int year);
SYSTEMTIME LunarToSolar(int lunarYear, int lunarMonth, int lunarDay, BOOL isLeap);
void SolarToLunar(SYSTEMTIME solar, int& lunarYear, int& lunarMonth, int& lunarDay, BOOL& isLeap);
wstring GetLunarDateString(int lunarYear, int lunarMonth, int lunarDay, BOOL isLeap);

//=================================================================//
// 获取农历年份总天数
int GetLunarYearDays(int year) {
    DWORD data = LunarData[year - 1900];
    int sum = 348; // 基础 12 * 29
    for (int i = 0x8000; i > 0x8; i >>= 1) {
        if (data & i) sum += 1;
    }
    sum += GetLeapMonthDays(year);
    return sum;
}

// 获取农历月份天数
int GetLunarMonthDays(int year, int month) {
    DWORD data = LunarData[year - 1900];
    return (data & (0x10000 >> month)) ? 30 : 29;
}

// 获取闰月月份
int GetLeapMonth(int year) {
    return LunarData[year - 1900] & 0xF;
}

// 获取闰月天数
int GetLeapMonthDays(int year) {
    if (GetLeapMonth(year))
        return (LunarData[year - 1900] & 0x10000) ? 30 : 29;
    return 0;
}

// 公历 → 农历转换
void SolarToLunar(SYSTEMTIME solar, int& lunarYear, int& lunarMonth, int& lunarDay, BOOL& isLeap) {
    SYSTEMTIME base = { 1900, 1, 0, 31 }; // 1900-01-31
    FILETIME ft1, ft2;
    SystemTimeToFileTime(&base, &ft1);
    SystemTimeToFileTime(&solar, &ft2);
    ULARGE_INTEGER t1, t2;
    t1.LowPart = ft1.dwLowDateTime; t1.HighPart = ft1.dwHighDateTime;
    t2.LowPart = ft2.dwLowDateTime; t2.HighPart = ft2.dwHighDateTime;

    int offset = (t2.QuadPart - t1.QuadPart) / (10000000ULL * 86400); // 天数差

    lunarYear = 1900;
    while (offset >= GetLunarYearDays(lunarYear)) {
        offset -= GetLunarYearDays(lunarYear);
        lunarYear++;
    }

    int leapMonth = GetLeapMonth(lunarYear);
    isLeap = FALSE;

    int i = 1;
    while (i <= 12 && offset >= 0) {
        int days = GetLunarMonthDays(lunarYear, i);
        if (leapMonth && i == leapMonth) {
            if (!isLeap) {
                if (offset < days) break;
                offset -= days;
                isLeap = TRUE;
                continue;
            } else {
                days = GetLeapMonthDays(lunarYear);
                if (offset < days) break;
                offset -= days;
                isLeap = FALSE;
            }
        } else {
            if (offset < days) break;
            offset -= days;
        }
        i++;
    }

    lunarMonth = i;
    lunarDay = offset + 1;
}

// 生成农历日期中文字符串
wstring GetLunarDateString(int lunarYear, int lunarMonth, int lunarDay, BOOL isLeap) {
    wstring str = L"农历";
    if (isLeap) str += L"闰";
    str += LunarMonthStr[lunarMonth - 1];

    if (lunarDay < 11) str += L"初";
    else if (lunarDay < 20) str += L"十";
    else if (lunarDay < 30) str += L"廿";
    else str += L"三十";

    str += DayStrNum[(lunarDay - 1) % 10];
    return str;
}

//=================================================================//
// 主函数测试
int wmain() {
    SYSTEMTIME solar = { 2025, 10, 27 }; // 公历
    int y, m, d; BOOL leap;
    SolarToLunar(solar, y, m, d, leap);

    wcout << L"公历日期: 2025年10月27日" << endl;
    wcout << L"对应农历: " << GetLunarDateString(y, m, d, leap) << endl;
    return 0;
}

六、代码详细解读

  • GetLunarYearDays:返回指定农历年总天数;

  • GetLunarMonthDays:返回指定月天数(29或30);

  • GetLeapMonth:返回闰月月份;

  • GetLeapMonthDays:返回闰月天数;

  • SolarToLunar:核心转换算法,计算公历到农历;

  • GetLunarDateString:输出农历日期的中文表示;

  • main():测试入口。


七、项目详细总结

本项目实现了 农历 ↔ 公历转换的核心算法,支持 1900–2100 年间的日期。

实现特点:

  • 无需外部库;

  • 精确控制闰月;

  • 算法高效(O(n));

  • 中文输出友好;

  • 可扩展性强。

实际应用场景:

  • 万年历、节日提醒;

  • 民俗节日系统;

  • 电子日历组件;

  • 智能家居面板;

  • 生日/生肖计算。


八、项目常见问题及解答

Q1:为什么只能支持 1900 年之后?
A:因为农历基准点为 1900 年,需扩展数据表以支持更早年份。

Q2:为什么农历春节与实际相差一两天?
A:部分年份天文算法略有差异,可通过天文对照表修正。

Q3:能否支持农历 → 公历?
A:完全可以,只需反向计算天数偏移。

Q4:闰月如何判断?
A:通过 GetLeapMonth(year) 返回的值判断是否存在闰月。

Q5:是否可以计算节日?
A:可以,通过农历日期匹配固定节日

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐