866 lines
27 KiB
C#
866 lines
27 KiB
C#
|
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
|||
|
|
//
|
|||
|
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
|||
|
|
//
|
|||
|
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
|||
|
|
|
|||
|
|
namespace Admin.NET.Core;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历辅助工具类
|
|||
|
|
/// </summary>
|
|||
|
|
/// <remarks>
|
|||
|
|
/// 提供公历与农历互转、天干地支、生肖、节气、农历节日等功能,
|
|||
|
|
/// 支持1900年至2100年的农历计算
|
|||
|
|
/// </remarks>
|
|||
|
|
public static class LunarCalendarHelper
|
|||
|
|
{
|
|||
|
|
#region 常量定义
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历数据起始年份
|
|||
|
|
/// </summary>
|
|||
|
|
private const int MinYear = 1900;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历数据结束年份
|
|||
|
|
/// </summary>
|
|||
|
|
private const int MaxYear = 2100;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历基准日期 (1900年1月31日为农历1900年正月初一)
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly DateTime BaseDate = new(1900, 1, 31);
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 天干数组
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] Tiangan = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 地支数组
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] Dizhi = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 生肖数组
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] Zodiac = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历月份名称
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] LunarMonths = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"];
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历日期名称
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] LunarDays =
|
|||
|
|
[
|
|||
|
|
"初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
|
|||
|
|
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
|
|||
|
|
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 二十四节气名称
|
|||
|
|
/// </summary>
|
|||
|
|
private static readonly string[] SolarTerms =
|
|||
|
|
[
|
|||
|
|
"立春", "雨水", "惊蛰", "春分", "清明", "谷雨", "立夏", "小满", "芒种", "夏至", "小暑", "大暑",
|
|||
|
|
"立秋", "处暑", "白露", "秋分", "寒露", "霜降", "立冬", "小雪", "大雪", "冬至", "小寒", "大寒"
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
#endregion 常量定义
|
|||
|
|
|
|||
|
|
#region 农历数据表
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历年份数据 (1900-2100年)
|
|||
|
|
/// 每个数值的低12位表示12个月的大小月(1为大月30天,0为小月29天)
|
|||
|
|
/// 第13位表示闰月的大小月
|
|||
|
|
/// 第14-17位表示闰月月份(0表示无闰月)
|
|||
|
|
/// </summary>
|
|||
|
|
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 公开方法 - 公历转农历
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 将公历日期转换为农历日期
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">公历日期</param>
|
|||
|
|
/// <returns>农历日期信息</returns>
|
|||
|
|
/// <exception cref="ArgumentOutOfRangeException">日期超出支持范围时抛出</exception>
|
|||
|
|
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
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 将农历日期转换为公历日期
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="lunarYear">农历年</param>
|
|||
|
|
/// <param name="lunarMonth">农历月</param>
|
|||
|
|
/// <param name="lunarDay">农历日</param>
|
|||
|
|
/// <param name="isLeapMonth">是否闰月</param>
|
|||
|
|
/// <returns>公历日期</returns>
|
|||
|
|
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 公开方法 - 天干地支与生肖
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定年份的生肖
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">年份(农历年)</param>
|
|||
|
|
/// <returns>生肖名称</returns>
|
|||
|
|
public static string GetZodiac(int year)
|
|||
|
|
{
|
|||
|
|
var index = (year - 1900) % 12;
|
|||
|
|
return Zodiac[index];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定年份的天干地支
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">年份(农历年)</param>
|
|||
|
|
/// <returns>天干地支组合</returns>
|
|||
|
|
public static string GetTianganDizhi(int year)
|
|||
|
|
{
|
|||
|
|
var tianganIndex = (year - 1900) % 10;
|
|||
|
|
var dizhiIndex = (year - 1900) % 12;
|
|||
|
|
return Tiangan[tianganIndex] + Dizhi[dizhiIndex];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定公历日期的天干地支
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">公历日期</param>
|
|||
|
|
/// <returns>日期天干地支</returns>
|
|||
|
|
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 公开方法 - 节气计算
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定年份的所有节气日期
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">公历年份</param>
|
|||
|
|
/// <returns>节气日期列表</returns>
|
|||
|
|
public static List<SolarTerm> GetSolarTerms(int year)
|
|||
|
|
{
|
|||
|
|
var solarTerms = new List<SolarTerm>();
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定日期所属的节气
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">公历日期</param>
|
|||
|
|
/// <returns>节气信息,如果不是节气日则返回null</returns>
|
|||
|
|
public static SolarTerm? GetSolarTerm(DateTime date)
|
|||
|
|
{
|
|||
|
|
var solarTerms = GetSolarTerms(date.Year);
|
|||
|
|
return solarTerms.FirstOrDefault(st => st.Date.Date == date.Date);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 判断指定日期是否为节气
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">公历日期</param>
|
|||
|
|
/// <returns>是否为节气日</returns>
|
|||
|
|
public static bool IsSolarTerm(DateTime date)
|
|||
|
|
{
|
|||
|
|
return GetSolarTerm(date) != null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion 公开方法 - 节气计算
|
|||
|
|
|
|||
|
|
#region 公开方法 - 农历节日
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定农历日期的传统节日名称
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="lunarMonth">农历月</param>
|
|||
|
|
/// <param name="lunarDay">农历日</param>
|
|||
|
|
/// <param name="isLeapMonth">是否闰月</param>
|
|||
|
|
/// <returns>节日名称,如果不是节日则返回null</returns>
|
|||
|
|
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
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取指定公历日期的传统节日名称
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">公历日期</param>
|
|||
|
|
/// <returns>节日名称,如果不是节日则返回null</returns>
|
|||
|
|
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 公开方法 - 工具方法
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历年份的中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <returns>中文年份名称</returns>
|
|||
|
|
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 + "年";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历月份的中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="month">农历月份</param>
|
|||
|
|
/// <param name="isLeapMonth">是否闰月</param>
|
|||
|
|
/// <returns>中文月份名称</returns>
|
|||
|
|
public static string GetLunarMonthName(int month, bool isLeapMonth = false)
|
|||
|
|
{
|
|||
|
|
var monthName = LunarMonths[month - 1];
|
|||
|
|
return isLeapMonth ? "闰" + monthName : monthName;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历日期的中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="day">农历日期</param>
|
|||
|
|
/// <returns>中文日期名称</returns>
|
|||
|
|
public static string GetLunarDayName(int day)
|
|||
|
|
{
|
|||
|
|
return day is >= 1 and <= 30 ? LunarDays[day - 1] : day.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 判断指定农历年份是否有闰月
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <returns>是否有闰月</returns>
|
|||
|
|
public static bool HasLeapMonth(int year)
|
|||
|
|
{
|
|||
|
|
return GetLeapMonth(year) > 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历年份的总天数
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <returns>总天数</returns>
|
|||
|
|
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 私有方法
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历年份的闰月月份
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <returns>闰月月份,0表示无闰月</returns>
|
|||
|
|
private static int GetLeapMonth(int year)
|
|||
|
|
{
|
|||
|
|
return year is < MinYear or > MaxYear ? 0 : (LunarYearData[year - MinYear] & 0xf0000) >> 16;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历月份的天数
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <param name="month">农历月份</param>
|
|||
|
|
/// <returns>月份天数</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取农历年份闰月的天数
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">农历年份</param>
|
|||
|
|
/// <returns>闰月天数</returns>
|
|||
|
|
private static int GetLeapMonthDays(int year)
|
|||
|
|
{
|
|||
|
|
return !HasLeapMonth(year) ? 0 : (LunarYearData[year - MinYear] & 0x10000) != 0 ? 30 : 29;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 计算指定年份第n个节气的日期(基于太阳黄经的真实算法)
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">公历年份</param>
|
|||
|
|
/// <param name="termIndex">节气索引(0-23)</param>
|
|||
|
|
/// <returns>节气日期</returns>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 计算儒略日数
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="date">日期</param>
|
|||
|
|
/// <returns>儒略日数</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 将儒略日数转换为DateTime
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="julianDay">儒略日数</param>
|
|||
|
|
/// <returns>日期时间</returns>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 计算太阳黄经(简化版VSOP87算法)
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="julianDay">儒略日数</param>
|
|||
|
|
/// <returns>太阳黄经(度)</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取节气的估算日期(从年初开始的天数)
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">年份</param>
|
|||
|
|
/// <param name="termIndex">节气索引</param>
|
|||
|
|
/// <returns>估算天数</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 二分法搜索节气精确时刻
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="startJd">搜索开始的儒略日</param>
|
|||
|
|
/// <param name="endJd">搜索结束的儒略日</param>
|
|||
|
|
/// <param name="targetLongitude">目标黄经</param>
|
|||
|
|
/// <returns>精确的儒略日数</returns>
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 计算两个角度之间的差值(考虑360度循环)
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="angle1">角度1</param>
|
|||
|
|
/// <param name="angle2">角度2</param>
|
|||
|
|
/// <returns>角度差</returns>
|
|||
|
|
private static double GetAngleDifference(double angle1, double angle2)
|
|||
|
|
{
|
|||
|
|
var diff = angle1 - angle2;
|
|||
|
|
while (diff > 180)
|
|||
|
|
{
|
|||
|
|
diff -= 360;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
while (diff < -180)
|
|||
|
|
{
|
|||
|
|
diff += 360;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return diff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 判断是否为闰年
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="year">年份</param>
|
|||
|
|
/// <returns>是否为闰年</returns>
|
|||
|
|
private static bool IsLeapYear(int year)
|
|||
|
|
{
|
|||
|
|
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion 私有方法
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历日期信息
|
|||
|
|
/// </summary>
|
|||
|
|
public class LunarDate
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历年份
|
|||
|
|
/// </summary>
|
|||
|
|
public int Year { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历月份
|
|||
|
|
/// </summary>
|
|||
|
|
public int Month { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历日期
|
|||
|
|
/// </summary>
|
|||
|
|
public int Day { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 是否闰月
|
|||
|
|
/// </summary>
|
|||
|
|
public bool IsLeapMonth { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历年份中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
public string YearName { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历月份中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
public string MonthName { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历日期中文名称
|
|||
|
|
/// </summary>
|
|||
|
|
public string DayName { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 生肖
|
|||
|
|
/// </summary>
|
|||
|
|
public string Zodiac { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 天干地支
|
|||
|
|
/// </summary>
|
|||
|
|
public string TianganDizhi { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 对应的公历日期
|
|||
|
|
/// </summary>
|
|||
|
|
public DateTime SolarDate { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历节日名称
|
|||
|
|
/// </summary>
|
|||
|
|
public string? Festival => LunarCalendarHelper.GetLunarFestival(Month, Day, IsLeapMonth);
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 农历日期的完整中文表示
|
|||
|
|
/// </summary>
|
|||
|
|
public string FullName => $"{YearName}{MonthName}{DayName}";
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 转换为字符串表示
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>格式化的农历日期</returns>
|
|||
|
|
public override string ToString()
|
|||
|
|
{
|
|||
|
|
var festival = Festival;
|
|||
|
|
var festivalText = !string.IsNullOrEmpty(festival) ? $" ({festival})" : "";
|
|||
|
|
return $"{FullName} {Zodiac}年 {TianganDizhi}{festivalText}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 节气信息
|
|||
|
|
/// </summary>
|
|||
|
|
public class SolarTerm
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 节气名称
|
|||
|
|
/// </summary>
|
|||
|
|
public string Name { get; set; } = string.Empty;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 节气日期
|
|||
|
|
/// </summary>
|
|||
|
|
public DateTime Date { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 节气序号(1-24)
|
|||
|
|
/// </summary>
|
|||
|
|
public int Order { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 转换为字符串表示
|
|||
|
|
/// </summary>
|
|||
|
|
/// <returns>格式化的节气信息</returns>
|
|||
|
|
public override string ToString()
|
|||
|
|
{
|
|||
|
|
return $"{Name} ({Date:yyyy年MM月dd日})";
|
|||
|
|
}
|
|||
|
|
}
|