// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
///
/// 农历辅助工具类
///
///
/// 提供公历与农历互转、天干地支、生肖、节气、农历节日等功能,
/// 支持1900年至2100年的农历计算
///
public static class LunarCalendarHelper
{
#region 常量定义
///
/// 农历数据起始年份
///
private const int MinYear = 1900;
///
/// 农历数据结束年份
///
private const int MaxYear = 2100;
///
/// 农历基准日期 (1900年1月31日为农历1900年正月初一)
///
private static readonly DateTime BaseDate = new(1900, 1, 31);
///
/// 天干数组
///
private static readonly string[] Tiangan = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
///
/// 地支数组
///
private static readonly string[] Dizhi = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
///
/// 生肖数组
///
private static readonly string[] Zodiac = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
///
/// 农历月份名称
///
private static readonly string[] LunarMonths = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"];
///
/// 农历日期名称
///
private static readonly string[] LunarDays =
[
"初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
];
///
/// 二十四节气名称
///
private static readonly string[] SolarTerms =
[
"立春", "雨水", "惊蛰", "春分", "清明", "谷雨", "立夏", "小满", "芒种", "夏至", "小暑", "大暑",
"立秋", "处暑", "白露", "秋分", "寒露", "霜降", "立冬", "小雪", "大雪", "冬至", "小寒", "大寒"
];
#endregion 常量定义
#region 农历数据表
///
/// 农历年份数据 (1900-2100年)
/// 每个数值的低12位表示12个月的大小月(1为大月30天,0为小月29天)
/// 第13位表示闰月的大小月
/// 第14-17位表示闰月月份(0表示无闰月)
///
private static readonly int[] LunarYearData =
[
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, 0x0a5d0, 0x14573, 0x052d0, 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, 0x055c0, 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,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
0x0d520
];
#endregion 农历数据表
#region 公开方法 - 公历转农历
///
/// 将公历日期转换为农历日期
///
/// 公历日期
/// 农历日期信息
/// 日期超出支持范围时抛出
public static LunarDate ConvertToLunar(DateTime date)
{
if (date.Year is < MinYear or > MaxYear)
{
throw new ArgumentOutOfRangeException(nameof(date), $"仅支持{MinYear}年至{MaxYear}年的日期转换");
}
var daysDiff = (date - BaseDate).Days;
var lunarYear = MinYear;
// 计算农历年份
while (lunarYear < MaxYear)
{
var daysInYear = GetLunarYearDays(lunarYear);
if (daysDiff < daysInYear)
{
break;
}
daysDiff -= daysInYear;
lunarYear++;
}
// 计算农历月份和日期
var lunarMonth = 1;
var isLeapMonth = false;
var leapMonth = GetLeapMonth(lunarYear);
while (lunarMonth <= 12)
{
var daysInMonth = GetLunarMonthDays(lunarYear, lunarMonth);
if (daysDiff < daysInMonth)
{
break;
}
daysDiff -= daysInMonth;
// 检查闰月
if (lunarMonth == leapMonth && !isLeapMonth)
{
isLeapMonth = true;
daysInMonth = GetLeapMonthDays(lunarYear);
if (daysDiff < daysInMonth)
{
break;
}
daysDiff -= daysInMonth;
isLeapMonth = false;
}
lunarMonth++;
}
var lunarDay = daysDiff + 1;
return new LunarDate
{
Year = lunarYear,
Month = lunarMonth,
Day = lunarDay,
IsLeapMonth = isLeapMonth,
YearName = GetLunarYearName(lunarYear),
MonthName = GetLunarMonthName(lunarMonth, isLeapMonth),
DayName = GetLunarDayName(lunarDay),
Zodiac = GetZodiac(lunarYear),
TianganDizhi = GetTianganDizhi(lunarYear),
SolarDate = date
};
}
///
/// 将农历日期转换为公历日期
///
/// 农历年
/// 农历月
/// 农历日
/// 是否闰月
/// 公历日期
public static DateTime ConvertToSolar(int lunarYear, int lunarMonth, int lunarDay, bool isLeapMonth = false)
{
if (lunarYear is < MinYear or > MaxYear)
{
throw new ArgumentOutOfRangeException(nameof(lunarYear), $"仅支持{MinYear}年至{MaxYear}年的农历年份");
}
var totalDays = 0;
// 计算从基准年到目标年的总天数
for (var year = MinYear; year < lunarYear; year++)
{
totalDays += GetLunarYearDays(year);
}
// 计算目标年中到目标月的天数
var leapMonth = GetLeapMonth(lunarYear);
for (var month = 1; month < lunarMonth; month++)
{
totalDays += GetLunarMonthDays(lunarYear, month);
if (month == leapMonth)
{
totalDays += GetLeapMonthDays(lunarYear);
}
}
// 如果是闰月,还需要加上正常月的天数
if (isLeapMonth && lunarMonth == leapMonth)
{
totalDays += GetLunarMonthDays(lunarYear, lunarMonth);
}
// 加上目标日的天数
totalDays += lunarDay - 1;
return BaseDate.AddDays(totalDays);
}
#endregion 公开方法 - 公历转农历
#region 公开方法 - 天干地支与生肖
///
/// 获取指定年份的生肖
///
/// 年份(农历年)
/// 生肖名称
public static string GetZodiac(int year)
{
var index = (year - 1900) % 12;
return Zodiac[index];
}
///
/// 获取指定年份的天干地支
///
/// 年份(农历年)
/// 天干地支组合
public static string GetTianganDizhi(int year)
{
var tianganIndex = (year - 1900) % 10;
var dizhiIndex = (year - 1900) % 12;
return Tiangan[tianganIndex] + Dizhi[dizhiIndex];
}
///
/// 获取指定公历日期的天干地支
///
/// 公历日期
/// 日期天干地支
public static string GetDayTianganDizhi(DateTime date)
{
// 以1900年1月1日为甲子日计算
var baseDay = new DateTime(1900, 1, 1);
var daysDiff = (date - baseDay).Days;
var tianganIndex = (daysDiff + 6) % 10; // 1900年1月1日为甲子日,甲为第0位
var dizhiIndex = (daysDiff + 6) % 12;
return Tiangan[tianganIndex] + Dizhi[dizhiIndex];
}
#endregion 公开方法 - 天干地支与生肖
#region 公开方法 - 节气计算
///
/// 获取指定年份的所有节气日期
///
/// 公历年份
/// 节气日期列表
public static List GetSolarTerms(int year)
{
var solarTerms = new List();
for (var i = 0; i < 24; i++)
{
var date = GetSolarTermDate(year, i);
solarTerms.Add(new SolarTerm
{
Name = SolarTerms[i],
Date = date,
Order = i + 1
});
}
return solarTerms;
}
///
/// 获取指定日期所属的节气
///
/// 公历日期
/// 节气信息,如果不是节气日则返回null
public static SolarTerm? GetSolarTerm(DateTime date)
{
var solarTerms = GetSolarTerms(date.Year);
return solarTerms.FirstOrDefault(st => st.Date.Date == date.Date);
}
///
/// 判断指定日期是否为节气
///
/// 公历日期
/// 是否为节气日
public static bool IsSolarTerm(DateTime date)
{
return GetSolarTerm(date) != null;
}
#endregion 公开方法 - 节气计算
#region 公开方法 - 农历节日
///
/// 获取指定农历日期的传统节日名称
///
/// 农历月
/// 农历日
/// 是否闰月
/// 节日名称,如果不是节日则返回null
public static string? GetLunarFestival(int lunarMonth, int lunarDay, bool isLeapMonth = false)
{
if (isLeapMonth)
{
return null; // 闰月一般不过传统节日
}
return (lunarMonth, lunarDay) switch
{
(1, 1) => "春节",
(1, 15) => "元宵节",
(2, 2) => "龙抬头",
(5, 5) => "端午节",
(7, 7) => "七夕节",
(7, 15) => "中元节",
(8, 15) => "中秋节",
(9, 9) => "重阳节",
(10, 1) => "寒衣节",
(10, 15) => "下元节",
(12, 8) => "腊八节",
(12, 23) => "小年",
(12, 24) => "小年",
(12, 30) => "除夕",
(12, 29) => GetLunarMonthDays(DateTime.Now.Year, 12) == 29 ? "除夕" : null,
_ => null
};
}
///
/// 获取指定公历日期的传统节日名称
///
/// 公历日期
/// 节日名称,如果不是节日则返回null
public static string? GetSolarFestival(DateTime date)
{
return (date.Month, date.Day) switch
{
(1, 1) => "元旦",
(2, 14) => "情人节",
(3, 8) => "妇女节",
(3, 12) => "植树节",
(4, 1) => "愚人节",
(5, 1) => "劳动节",
(5, 4) => "青年节",
(6, 1) => "儿童节",
(7, 1) => "建党节",
(8, 1) => "建军节",
(9, 10) => "教师节",
(10, 1) => "国庆节",
(12, 25) => "圣诞节",
_ => null
};
}
#endregion 公开方法 - 农历节日
#region 公开方法 - 工具方法
///
/// 获取农历年份的中文名称
///
/// 农历年份
/// 中文年份名称
public static string GetLunarYearName(int year)
{
var yearStr = year.ToString();
var chineseNumbers = new[] { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" };
var result = "";
foreach (var digit in yearStr)
{
result += chineseNumbers[digit - '0'];
}
return result + "年";
}
///
/// 获取农历月份的中文名称
///
/// 农历月份
/// 是否闰月
/// 中文月份名称
public static string GetLunarMonthName(int month, bool isLeapMonth = false)
{
var monthName = LunarMonths[month - 1];
return isLeapMonth ? "闰" + monthName : monthName;
}
///
/// 获取农历日期的中文名称
///
/// 农历日期
/// 中文日期名称
public static string GetLunarDayName(int day)
{
return day is >= 1 and <= 30 ? LunarDays[day - 1] : day.ToString();
}
///
/// 判断指定农历年份是否有闰月
///
/// 农历年份
/// 是否有闰月
public static bool HasLeapMonth(int year)
{
return GetLeapMonth(year) > 0;
}
///
/// 获取农历年份的总天数
///
/// 农历年份
/// 总天数
public static int GetLunarYearDays(int year)
{
var days = 0;
for (var month = 1; month <= 12; month++)
{
days += GetLunarMonthDays(year, month);
}
// 如果有闰月,加上闰月的天数
if (HasLeapMonth(year))
{
days += GetLeapMonthDays(year);
}
return days;
}
#endregion 公开方法 - 工具方法
#region 私有方法
///
/// 获取农历年份的闰月月份
///
/// 农历年份
/// 闰月月份,0表示无闰月
private static int GetLeapMonth(int year)
{
return year is < MinYear or > MaxYear ? 0 : (LunarYearData[year - MinYear] & 0xf0000) >> 16;
}
///
/// 获取农历月份的天数
///
/// 农历年份
/// 农历月份
/// 月份天数
private static int GetLunarMonthDays(int year, int month)
{
if (year is < MinYear or > MaxYear)
{
return 29;
}
var monthData = LunarYearData[year - MinYear] & 0xfff;
return (monthData & (1 << (12 - month))) != 0 ? 30 : 29;
}
///
/// 获取农历年份闰月的天数
///
/// 农历年份
/// 闰月天数
private static int GetLeapMonthDays(int year)
{
return !HasLeapMonth(year) ? 0 : (LunarYearData[year - MinYear] & 0x10000) != 0 ? 30 : 29;
}
///
/// 计算指定年份第n个节气的日期(基于太阳黄经的真实算法)
///
/// 公历年份
/// 节气索引(0-23)
/// 节气日期
private static DateTime GetSolarTermDate(int year, int termIndex)
{
// 每个节气对应的太阳黄经度数
var solarLongitudes = new double[]
{
315, 330, 345, 0, 15, 30, 45, 60, 75, 90, 105, 120,
135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285, 300
};
var targetLongitude = solarLongitudes[termIndex];
// 计算当年1月1日的儒略日数
var jan1 = new DateTime(year, 1, 1);
var julianDay = GetJulianDay(jan1);
// 估算节气可能的日期范围
var estimatedDay = GetEstimatedSolarTermDay(year, termIndex);
var searchStart = julianDay + estimatedDay - 15;
var searchEnd = julianDay + estimatedDay + 15;
// 二分法搜索精确的节气时刻
var result = BinarySearchSolarTerm(searchStart, searchEnd, targetLongitude);
return JulianDayToDateTime(result);
}
///
/// 计算儒略日数
///
/// 日期
/// 儒略日数
private static double GetJulianDay(DateTime date)
{
var year = date.Year;
var month = date.Month;
var day = date.Day + (date.Hour / 24.0) + (date.Minute / 1440.0) + (date.Second / 86400.0);
if (month <= 2)
{
year -= 1;
month += 12;
}
var a = year / 100;
var b = 2 - a + (a / 4);
return Math.Floor(365.25 * (year + 4716)) + Math.Floor(30.6001 * (month + 1)) + day + b - 1524.5;
}
///
/// 将儒略日数转换为DateTime
///
/// 儒略日数
/// 日期时间
private static DateTime JulianDayToDateTime(double julianDay)
{
var z = Math.Floor(julianDay + 0.5);
var f = julianDay + 0.5 - z;
double a;
if (z < 2299161)
{
a = z;
}
else
{
var alpha = Math.Floor((z - 1867216.25) / 36524.25);
a = z + 1 + alpha - Math.Floor(alpha / 4);
}
var b = a + 1524;
var c = Math.Floor((b - 122.1) / 365.25);
var d = Math.Floor(365.25 * c);
var e = Math.Floor((b - d) / 30.6001);
var day = b - d - Math.Floor(30.6001 * e) + f;
var month = e < 14 ? e - 1 : e - 13;
var year = month > 2 ? c - 4716 : c - 4715;
var wholeDays = Math.Floor(day);
var fractionalDay = day - wholeDays;
var hours = fractionalDay * 24;
var wholeHours = Math.Floor(hours);
var fractionalHours = hours - wholeHours;
var minutes = fractionalHours * 60;
var wholeMinutes = Math.Floor(minutes);
var fractionalMinutes = minutes - wholeMinutes;
var seconds = Math.Floor(fractionalMinutes * 60);
return new DateTime((int)year, (int)month, (int)wholeDays, (int)wholeHours, (int)wholeMinutes, (int)seconds);
}
///
/// 计算太阳黄经(简化版VSOP87算法)
///
/// 儒略日数
/// 太阳黄经(度)
private static double CalculateSolarLongitude(double julianDay)
{
// 儒略世纪数
var t = (julianDay - 2451545.0) / 36525.0;
// 太阳的平黄经
var l0 = 280.46646 + (36000.76983 * t) + (0.0003032 * t * t);
// 太阳的平近点角
var m = 357.52911 + (35999.05029 * t) - (0.0001537 * t * t);
// 转换为弧度
var mRad = m * Math.PI / 180.0;
// 黄经修正项(主要项)
var c = ((1.914602 - (0.004817 * t) - (0.000014 * t * t)) * Math.Sin(mRad)) +
((0.019993 - (0.000101 * t)) * Math.Sin(2 * mRad)) +
(0.000289 * Math.Sin(3 * mRad));
// 真黄经
var lambda = l0 + c;
// 章动修正(简化)
var omega = 125.04452 - (1934.136261 * t);
var omegaRad = omega * Math.PI / 180.0;
var deltaPsi = -17.20 * Math.Sin(omegaRad) / 3600.0;
lambda += deltaPsi;
// 确保角度在0-360度范围内
lambda %= 360.0;
if (lambda < 0)
{
lambda += 360.0;
}
return lambda;
}
///
/// 获取节气的估算日期(从年初开始的天数)
///
/// 年份
/// 节气索引
/// 估算天数
private static int GetEstimatedSolarTermDay(int year, int termIndex)
{
// 基于统计平均值的估算表(从1月1日开始的天数)
var estimatedDays = new[]
{
4, 19, 35, 51, 66, 81, 96, 112, 128, 144, 160, 176,
192, 208, 224, 240, 256, 272, 288, 304, 320, 336, 352, 3
};
var baseDay = estimatedDays[termIndex];
// 对于小寒,如果是下一年的,需要调整
if (termIndex == 23 && baseDay < 10)
{
baseDay += 365;
if (IsLeapYear(year))
{
baseDay += 1;
}
}
return baseDay;
}
///
/// 二分法搜索节气精确时刻
///
/// 搜索开始的儒略日
/// 搜索结束的儒略日
/// 目标黄经
/// 精确的儒略日数
private static double BinarySearchSolarTerm(double startJd, double endJd, double targetLongitude)
{
const double Precision = 1.0 / 86400.0; // 1秒的精度
const int MaxIterations = 50;
var iterations = 0;
while (endJd - startJd > Precision && iterations < MaxIterations)
{
var midJd = (startJd + endJd) / 2.0;
var longitude = CalculateSolarLongitude(midJd);
// 处理角度跨越0度的情况
var diff = GetAngleDifference(longitude, targetLongitude);
if (Math.Abs(diff) < 0.01) // 0.01度的精度
{
return midJd;
}
// 判断太阳是否还未到达目标黄经
if (diff > 0)
{
endJd = midJd;
}
else
{
startJd = midJd;
}
iterations++;
}
return (startJd + endJd) / 2.0;
}
///
/// 计算两个角度之间的差值(考虑360度循环)
///
/// 角度1
/// 角度2
/// 角度差
private static double GetAngleDifference(double angle1, double angle2)
{
var diff = angle1 - angle2;
while (diff > 180)
{
diff -= 360;
}
while (diff < -180)
{
diff += 360;
}
return diff;
}
///
/// 判断是否为闰年
///
/// 年份
/// 是否为闰年
private static bool IsLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
#endregion 私有方法
}
///
/// 农历日期信息
///
public class LunarDate
{
///
/// 农历年份
///
public int Year { get; set; }
///
/// 农历月份
///
public int Month { get; set; }
///
/// 农历日期
///
public int Day { get; set; }
///
/// 是否闰月
///
public bool IsLeapMonth { get; set; }
///
/// 农历年份中文名称
///
public string YearName { get; set; } = string.Empty;
///
/// 农历月份中文名称
///
public string MonthName { get; set; } = string.Empty;
///
/// 农历日期中文名称
///
public string DayName { get; set; } = string.Empty;
///
/// 生肖
///
public string Zodiac { get; set; } = string.Empty;
///
/// 天干地支
///
public string TianganDizhi { get; set; } = string.Empty;
///
/// 对应的公历日期
///
public DateTime SolarDate { get; set; }
///
/// 农历节日名称
///
public string? Festival => LunarCalendarHelper.GetLunarFestival(Month, Day, IsLeapMonth);
///
/// 农历日期的完整中文表示
///
public string FullName => $"{YearName}{MonthName}{DayName}";
///
/// 转换为字符串表示
///
/// 格式化的农历日期
public override string ToString()
{
var festival = Festival;
var festivalText = !string.IsNullOrEmpty(festival) ? $" ({festival})" : "";
return $"{FullName} {Zodiac}年 {TianganDizhi}{festivalText}";
}
}
///
/// 节气信息
///
public class SolarTerm
{
///
/// 节气名称
///
public string Name { get; set; } = string.Empty;
///
/// 节气日期
///
public DateTime Date { get; set; }
///
/// 节气序号(1-24)
///
public int Order { get; set; }
///
/// 转换为字符串表示
///
/// 格式化的节气信息
public override string ToString()
{
return $"{Name} ({Date:yyyy年MM月dd日})";
}
}