C#农历日期时间类(包括天干地支,节气)

  之前写了个小倒计时工具,用到了农历日期的转换,后来想想直接封装个类得了,以后用起来也方便,像国人的生日大多也都是农历来算的。于是研究了下农历的规则,结合了网上的一些算法,封装了下面的类(ChineseDateTime)。主要用到的是.Net类库中的农历类System.Globalization.ChineseLunisolarCalendar(公元纪年与中国传统农历纪年之间的相互转换),日期范围在1901-02-19到2101-01-28 23:59:59.999之间,也够日常使用了。不多说了,直接上代码:

using System;
using System.Globalization;
using System.Text;
using System.Linq;
using System.Linq.Expressions;

namespace Ich.Timer
{
    /// <summary>
    /// ChineseDateTime
    /// 一日有十二时辰,一时辰有四刻,一刻有三盏茶,一盏茶有两柱香
    /// 一柱香有五分,一分有六弹指,一弹指有十刹那,一刹那为一念
    /// </summary>
    public class ChineseDateTime
    {
        #region ====== 内部常量 ======
        private readonly ChineseLunisolarCalendar _chineseDateTime;
        private readonly DateTime _dateTime;
        private readonly int _serialMonth;

        private static readonly string[] _chineseNumber = { "〇", "一", "二", "三", "四", "五", "六", "七", "八", "九" };
        private static readonly string[] _chineseMonth =
        {
            "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"
        };
        private static readonly string[] _chineseDay =
        {
            "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
            "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
            "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
        };
        private static readonly string[] _chineseWeek =
        {
            "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"
        };

        private static readonly string[] _celestialStem = { "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" };
        private static readonly string[] _terrestrialBranch = { "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" };
        private static readonly string[] _chineseZodiac = { "鼠", "牛", "虎", "免", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪" };

        private static readonly string[] _solarTerm =
        {
            "小寒", "大寒", "立春", "雨水", "惊蛰", "春分",
            "清明", "谷雨", "立夏", "小满", "芒种", "夏至",
            "小暑", "大暑", "立秋", "处暑", "白露", "秋分",
            "寒露", "霜降", "立冬", "小雪", "大雪", "冬至"
        };
        private static readonly int[] _solarTermInfo = {
            0, 21208, 42467, 63836, 85337, 107014, 128867, 150921, 173149, 195551, 218072, 240693, 263343, 285989,
            308563, 331033, 353350, 375494, 397447, 419210, 440795, 462224, 483532, 504758
        };
        #endregion
        
        #region ======= 构建日期 ======

        public ChineseDateTime(DateTime dateTime)
        {
            _chineseDateTime = new ChineseLunisolarCalendar();
            if (dateTime < _chineseDateTime.MinSupportedDateTime || dateTime > _chineseDateTime.MaxSupportedDateTime)
            {
                throw new ArgumentOutOfRangeException(
                    $"参数日期不在有效的范围内:只支持{_chineseDateTime.MinSupportedDateTime.ToShortTimeString()}到{_chineseDateTime.MaxSupportedDateTime}");
            }

            Year = _chineseDateTime.GetYear(dateTime);
            Month = _chineseDateTime.GetMonth(dateTime);
            Day = _chineseDateTime.GetDayOfMonth(dateTime);
            IsLeep = _chineseDateTime.IsLeapMonth(Year, Month);
            _dateTime = dateTime;
            _serialMonth = Month;
            var leepMonth = _chineseDateTime.GetLeapMonth(Year);
            if (leepMonth > 0 && leepMonth <= Month) Month--;
        }
        
        /// <summary>
        /// 参数为农历的年月日及是否润月
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <param name="day"></param>
        /// <param name="isLeap"></param>
        public ChineseDateTime(int year, int month, int day, bool isLeap = false)
            : this(year, month, day, 0, 0, 0, isLeap)
        {

        }

        public ChineseDateTime(int year, int month, int day, int hour, int minute, int second, bool isLeap = false)
            : this(year, month, day, hour, minute, second, 0, isLeap)
        {

        }

        public ChineseDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, bool isLeap = false)
        {
            _chineseDateTime = new ChineseLunisolarCalendar();
            if (year < _chineseDateTime.MinSupportedDateTime.Year || year >= _chineseDateTime.MaxSupportedDateTime.Year)
            {
                throw new ArgumentOutOfRangeException(
                    $"参数年份不在有效的范围内,只支持{_chineseDateTime.MinSupportedDateTime.Year}到{_chineseDateTime.MaxSupportedDateTime.Year - 1}");
            }

            if (month < 1 || month > 12) throw new ArgumentOutOfRangeException($"月份只支持1-12");
            IsLeep = isLeap;
            var leepMonth = _chineseDateTime.GetLeapMonth(year);
            if (leepMonth - 1 != month)
                IsLeep = false;
            _serialMonth = month;
            if (leepMonth > 0 && (month == leepMonth - 1 && isLeap || month > leepMonth - 1))
                _serialMonth = month + 1;

            if (_chineseDateTime.GetDaysInMonth(year, _serialMonth) < day || day < 1)
                throw new ArgumentOutOfRangeException($"指定的月份天数,不在有效的范围内");

            Year = year;
            Month = month;
            Day = day;
            _dateTime = _chineseDateTime.ToDateTime(Year, _serialMonth, Day, hour, minute, second, millisecond);
        }

        public static ChineseDateTime Now => new ChineseDateTime(DateTime.Now);

        #endregion

        #region ====== 年月日润属性 ======
        public int Year { get; }
        public int Month { get; }
        public int Day { get; }
        
        /// <summary>
        /// 是否为润月
        /// </summary>
        public bool IsLeep { get; }
        #endregion
        
        #region ====== 输出常规日期 ======
        /// <summary>
        /// 转换为公历
        /// </summary>
        /// <returns></returns>
        public DateTime ToDateTime()
        {
            return _chineseDateTime.ToDateTime(Year, _serialMonth, Day, _dateTime.Hour,
                _dateTime.Minute,
                _dateTime.Second, _dateTime.Millisecond);
        }
        
        /// <summary>
        /// 短日期(农历)
        /// </summary>
        /// <returns></returns>
        public string ToShortDateString()
        {
            return $"{Year}-{GetLeap(false)}{Month}-{Day}";
        }
      
        /// <summary>
        /// 长日期(农历)
        /// </summary>
        /// <returns></returns>
        public string ToLongDateString()
        {
            return $"{Year}年{GetLeap()}{Month}月-{Day}日";
        }
        
        public new string ToString()
        {
            return $"{Year}-{GetLeap(false)}{Month}-{Day} {_dateTime.Hour}:{_dateTime.Minute}:{_dateTime.Second}";
        }
        #endregion

        #region ====== 输出中文日期及星期 ======
        public string ToChineseString()
        {
            return ToChineseString("yMd");
        }

        public string ToChineseString(string format)
        {
            var year = GetYear();
            var month = GetMonth();
            var day = GetDay();

            var date = new StringBuilder();
            foreach (var item in format.ToCharArray())
            {
                switch (item)
                {
                    case 'y':
                        date.Append($"{year}年");
                        break;
                    case 'M':
                        date.Append($"{month}月");
                        break;
                    case 'd':
                        date.Append($"{day}");
                        break;
                    default:
                        date.Append(item);
                        break;
                }
            }
            var def = $"{year}年{month}月{day}";
            var result = date.ToString();
            return string.IsNullOrEmpty(result) ? def : result;
        }

        public string ChineseWeek => _chineseWeek[(int)_dateTime.DayOfWeek];
        #endregion
        
        #region ====== 输出天干地支生肖 ======

        public string ToChineseEraString()
        {
            return ToChineseEraString("yMdHm");
        }

        public string ToChineseEraString(string format)
        {
            var year = GetEraYear();
            var month = GetEraMonth();
            var day = GetEraDay();
            var hour = GetEraHour();
            var minute = GetEraMinute();

            var date = new StringBuilder();
            foreach (var item in format.ToCharArray())
            {
                switch (item)
                {
                    case 'y':
                        date.Append($"{year}年");
                        break;
                    case 'M':
                        date.Append($"{month}月");
                        break;
                    case 'd':
                        date.Append($"{day}日");
                        break;
                    case 'H':
                        date.Append($"{hour}时");
                        break;
                    case 'm':
                        date.Append($"{minute}刻");
                        break;
                    default:
                        date.Append(item);
                        break;
                }
            }
            var def = $"{year}年{month}月{day}日{hour}时";
            var result = date.ToString();
            return string.IsNullOrEmpty(result) ? def : result;
        }

        public string ChineseZodiac => _chineseZodiac[(Year - 4) % 12];
        #endregion

        #region ====== 辅助方法(Chinese) ======
        private string GetYear()
        {
            var yearArray = Array.ConvertAll(Year.ToString().ToCharArray(), x => int.Parse(x.ToString()));
            var year = new StringBuilder();
            foreach (var item in yearArray)
                year.Append(_chineseNumber[item]);
            return year.ToString();
        }

        private string GetMonth()
        {
            return $"{GetLeap()}{_chineseMonth[Month - 1]}";
        }

        private string GetDay()
        {
            return _chineseDay[Day - 1];
        }

        private string GetLeap(bool isChinese = true)
        {
            return IsLeep ? isChinese ? "润" : "L" : "";
        }
        #endregion

        #region ====== 输助方法(天干地支)======
        //年采用的头尾法,月采用的是节令法,主流日历基本上都这种结合,如百度的日历  

        private string GetEraYear()
        {
            var sexagenaryYear = _chineseDateTime.GetSexagenaryYear(_dateTime);
            var stemIndex = _chineseDateTime.GetCelestialStem(sexagenaryYear) - 1;
            var branchIndex = _chineseDateTime.GetTerrestrialBranch(sexagenaryYear) - 1;
            return $"{_celestialStem[stemIndex]}{_terrestrialBranch[branchIndex]}";
        }

        private string GetEraMonth()
        {
            #region ====== 节令法 ======
            var solarIndex = SolarTermFunc((x, y) => x <= y, out var dt);
            solarIndex = solarIndex == -1 ? 23 : solarIndex;
            var currentIndex = (int)Math.Floor(solarIndex / (decimal)2);

            //天干         
            var solarMonth = currentIndex == 0 ? 11 : currentIndex - 1; //计算天干序(月份)
            var sexagenaryYear = _chineseDateTime.GetSexagenaryYear(_dateTime);
            var stemYear = _chineseDateTime.GetCelestialStem(sexagenaryYear) - 1;
            if (solarMonth == 0) //立春时,春节前后的不同处理
            {
                var year = _chineseDateTime.GetYear(dt);
                var month = _chineseDateTime.GetMonth(dt);
                stemYear = year == Year && month != 1 ? stemYear + 1 : stemYear;
            }
            if (solarMonth == 11) //立春在春节后,对前一节气春节前后不同处理
            {
                var year = _chineseDateTime.GetYear(dt);
                stemYear = year != Year ? stemYear - 1 : stemYear;
            }
            int stemIndex;
            switch (stemYear)
            {
                case 0:
                case 5:
                    stemIndex = 3;
                    break;
                case 1:
                case 6:
                    stemIndex = 5;
                    break;
                case 2:
                case 7:
                    stemIndex = 7;
                    break;
                case 3:
                case 8:
                    stemIndex = 9;
                    break;
                default:
                    stemIndex = 1;
                    break;
            }
            //天干序
            stemIndex = (stemIndex - 1 + solarMonth) % 10;

            //地支序
            var branchIndex = currentIndex >= 11 ? currentIndex - 11 : currentIndex + 1;

            return $"{_celestialStem[stemIndex]}{_terrestrialBranch[branchIndex]}";

            #endregion

            #region ====== 头尾法 ======
            //这里算法要容易些,原理和节令法一样,只需取农历整年整月即可。未贴上来
            #endregion
        }
        
        private string GetEraDay()
        {
            var ts = _dateTime - new DateTime(1901, 2, 15);
            var offset = ts.Days;
            var sexagenaryDay = offset % 60;
            return $"{_celestialStem[sexagenaryDay % 10]}{_terrestrialBranch[sexagenaryDay % 12]}";
        }

        private string GetEraHour()
        {
            var hourIndex = (int)Math.Floor((_dateTime.Hour + 1) / (decimal)2);
            hourIndex = hourIndex == 12 ? 0 : hourIndex;
            return _terrestrialBranch[hourIndex];
        }

        private string GetEraMinute()
        {
            var realMinute = (_dateTime.Hour % 2 == 0 ? 60 : 0) + _dateTime.Minute;
            return $"{_chineseNumber[(int)Math.Floor(realMinute / (decimal)30) + 1]}";
        }
        #endregion
        
        #region ====== 24节气 ======
        /// <summary>
        /// 当前节气,没有则返回空
        /// </summary>
        public string SolarTerm
        {
            get
            {
                var i = SolarTermFunc((x, y) => x == y, out var dt);
                return i == -1 ? "" : _solarTerm[i];
            }
        }
        
        /// <summary>
        /// 上一个节气
        /// </summary>
        public string SolarTermPrev
        {
            get
            {
                var i = SolarTermFunc((x, y) => x < y, out var dt);
                return i == -1 ? "" : _solarTerm[i];
            }
        }
        
        /// <summary>
        /// 下一个节气
        /// </summary>
        public string SolarTermNext
        {
            get
            {
                var i = SolarTermFunc((x, y) => x > y, out var dt);
                return i == -1 ? "" : $"{_solarTerm[i]}";
            }
        }
        
        /// <summary>
        /// 节气计算(当前年),返回指定条件的节气序及日期(公历)
        /// </summary>
        /// <param name="func"></param>
        /// <param name="dateTime"></param>
        /// <returns>-1时即没找到</returns>
        private int SolarTermFunc(Expression<Func<int, int, bool>> func, out DateTime dateTime)
        {
            var baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0); //#1/6/1900 2:05:00 AM#
            var year = _dateTime.Year;
            int[] solar = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
            var expressionType = func.Body.NodeType;
            if (expressionType != ExpressionType.LessThan && expressionType != ExpressionType.LessThanOrEqual &&
                expressionType != ExpressionType.GreaterThan && expressionType != ExpressionType.GreaterThanOrEqual &&
                expressionType != ExpressionType.Equal)
            {
                throw new NotSupportedException("不受支持的操作符");
            }

            if (expressionType == ExpressionType.LessThan || expressionType == ExpressionType.LessThanOrEqual)
            {
                solar = solar.OrderByDescending(x => x).ToArray();
            }
            foreach (var item in solar)
            {
                var num = 525948.76 * (year - 1900) + _solarTermInfo[item - 1];
                var newDate = baseDateAndTime.AddMinutes(num); //按分钟计算
                if (func.Compile()(newDate.DayOfYear, _dateTime.DayOfYear))
                {
                    dateTime = newDate;
                    return item - 1;
                }
            }
            dateTime = _chineseDateTime.MinSupportedDateTime;
            return -1;
        }
        #endregion
    }
}

类行数长了点哈😎。

农历类的基本使用

1、当前日期实例: var cdt = ChineseDateTime.Now;
2、输出中文年月日:cdt.ToChineseString(); 结果:二〇一八年六月初九
  支持简易参数"yMd",可分别单独传。
  如:cdt.ToChineseString("yM"); 结果:二〇一八年六月
3、输出天干地支:cdt.ToChineseEraString(); 结果:戊戌年己未月甲寅日午时二刻
  支持简易参数"yMdHm",传法同中文日期
4、取得星期和生肖:
  星期:cdt.ChineseWeek; 结果:星期六
  生肖:cdt.ChineseZodiac; 结果:狗
5、24节气,这里主要用于节令法干支的月份计算,顺便可以输出一下。
  上一个节气:cdt.SolarTermPrev; 结果:小暑。(同年没有则为空值,非null)

其它一些说明

侧重点写了些基础使用的值,至于输出的结果,可根据自己的需求,灵活调整。
主要用到ChineseLunisolarCalendar类的如下方法:
1、GetYear(DateTime dateTime) 返回指定日期中的农历年份
2、GetMonth(DateTime dateTime)返回指定日期中的农历月份。(1-13)有润月则顺延
3、GetDayOfMonth(DateTime dateTime) 返回指定日期中的农历日
4、IsLeapMonth(int year,int month) 确定指定年份中的指定的月份是否为润月(农历)
5、GetLeapMonth(int year)计算指定年份的闰月。无返回0,如润6月,则返回 7
6、GetSexagenaryYear(DateTime dateTime)计算甲子 (60 年) 循环,对应于指定日期的年份
7、GetCelestialStem(int sexagenaryYear)计算甲子 (60 年) 循环中的指定年份天干
8、GetTerrestrialBranch(int sexagenaryYear)计算甲子 (60 年) 循环中的指定年份的地支

尾声

不足之处多多指教  --快乐泥巴

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容