Initial commit
authorAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sat, 1 Jun 2019 09:07:19 +0000 (10:07 +0100)
committerAdrian Iain Lam <adrianiainlam@users.noreply.github.com>
Sat, 1 Jun 2019 09:14:56 +0000 (10:14 +0100)
LunarCalendar.py [new file with mode: 0644]
lunarInfo.csv [new file with mode: 0644]

diff --git a/LunarCalendar.py b/LunarCalendar.py
new file mode 100644 (file)
index 0000000..1ff374e
--- /dev/null
@@ -0,0 +1,381 @@
+import csv
+import datetime
+import os
+
+
+class LunarCalendar:
+    """
+    LunarCalendar by Adrian I. Lam (2019),
+    a Python 3 port of the JavaScript LunarCalendar by zzyss86,
+    available at <https://github.com/zzyss86/LunarCalendar>.
+
+    This is only a partial port. I have ported
+    LunarCalendar.solarToLunar(year, month, day) and
+    LunarCalendar.lunarToSolar(year, month, day) only,
+    and have ignored everything related to holidays.
+
+    Since the original author did not specify any license, I will
+    also not license this script. This script comes with no warranties,
+    expressed or implied. In particular, the warranties of merchantability,
+    fitness for a particular purpose and noninfringement are disclaimed.
+    """
+
+    def __formatDayD4(self, month, day):
+        month = month + 1
+        month = '0' + str(month) if month < 10 else str(month)
+        day = '0' + str(day) if day < 10 else str(day)
+        return 'd' + month + day
+
+    __minYear = 1890
+    __maxYear = 2100
+
+    __heavenlyStems = ['甲', '乙', '丙', '丁', '戊',
+                       '己', '庚', '辛', '壬', '癸']  # 天干
+    __earthlyBranches = ['子', '丑', '寅', '卯', '辰', '巳',
+                         '午', '未', '申', '酉', '戌', '亥']  # 地支
+    __zodiac = ['鼠', '牛', '虎', '兔', '龍', '蛇',
+                '馬', '羊', '猴', '雞', '狗', '豬']  # 對應地支十二生肖
+    __solarTerm = ['小寒', '大寒', '立春', '雨水', '驚蟄', '春分',
+                   '清明', '穀雨', '立夏', '小滿', '芒種', '夏至',
+                   '小暑', '大暑', '立秋', '處暑', '白露', '秋分',
+                   '寒露', '霜降', '立冬', '小雪', '大雪', '冬至']  # 二十四節氣
+    __monthCn = ['正', '二', '三', '四', '五', '六',
+                 '七', '八', '九', '十', '十一', '十二']
+    __dateCn = ['初一', '初二', '初三', '初四', '初五', '初六',
+                '初七', '初八', '初九', '初十', '十一', '十二',
+                '十三', '十四', '十五', '十六', '十七', '十八',
+                '十九', '二十', '廿一', '廿二', '廿三', '廿四',
+                '廿五', '廿六', '廿七', '廿八', '廿九', '三十', '卅一']
+
+    # 1890 - 2100 年的農曆數據
+    # 數據格式:[0,2,9,21936]
+    # [閏月所在月,0為沒有閏月;
+    #  *正月初一對應公曆月;
+    #  *正月初一對應公曆日;
+    #  *農曆每月的天數的數組(需轉換為二進制,得到每月大小,
+    #                        0=小月(29日),1=大月(30日));]
+    __lunarInfo = []
+    __dir = os.path.dirname(os.path.realpath(__file__))
+    with open(os.path.join(__dir, 'lunarInfo.csv')) as __lunarInfoCSV:
+        for __row in csv.reader(__lunarInfoCSV):
+            __lunarInfo.append([int(x) for x in __row])
+
+    # 二十四節氣數據,節氣點時間(單位是分鐘)
+    # 從0小寒起算
+    __termInfo = [0, 21208, 42467, 63836, 85337, 107014, 128867, 150921,
+                  173149, 195551, 218072, 240693, 263343, 285989, 308563,
+                  331033, 353350, 375494, 397447, 419210, 440795, 462224,
+                  483532, 504758]
+
+    def __init__(self):
+        self.__cache = {}
+
+    def __getLunarLeapYear(self, year):
+        """
+        判斷農曆年閏月數
+        @param {Number} year 農曆年
+        return 閏月數(月份從1開始)
+        """
+        yearData = self.__lunarInfo[year - self.__minYear]
+        return yearData[0]
+
+    def __getLunarYearDays(self, year):
+        """
+        獲取農曆年份一年的每月的天數及一年的總天數
+        @param {Number} year 農曆年
+        """
+        yearData = self.__lunarInfo[year - self.__minYear]
+        leapMonth = yearData[0]
+        monthData = yearData[3]
+
+        monthDataArr = []
+        for i in range(15, -1, -1):
+            monthDataArr.append((monthData & (1 << i)) >> i)
+
+        numMonthsInYear = 13 if leapMonth else 12
+        yearDays = 0
+        monthDays = []
+        for i in range(numMonthsInYear):
+            if monthDataArr[i] == 0:
+                yearDays += 29
+                monthDays.append(29)
+            else:
+                yearDays += 30
+                monthDays.append(30)
+
+        return {
+            'yearDays': yearDays,
+            'monthDays': monthDays
+        }
+
+    def __getLunarDateByBetween(self, year, between):
+        """
+        通過間隔天數查找農曆日期
+        @param {Number} year,between 農曆年,間隔天數
+        """
+        lunarYearDays = self.__getLunarYearDays(year)
+        end = between if between > 0 else lunarYearDays['yearDays'] + between
+        monthDays = lunarYearDays['monthDays']
+        tempDays = 0
+        month = 0
+        for i in range(len(monthDays)):
+            tempDays += monthDays[i]
+            if tempDays > end:
+                month = i
+                tempDays = tempDays - monthDays[i]
+                break
+
+        return [year, month, end - tempDays + 1]
+
+    def __getLunarByBetween(self, year, month, day):
+        """
+        根據距離正月初一的天數計算農曆日期
+        @param {Number} year 公曆年,月,日
+        """
+        yearData = self.__lunarInfo[year - self.__minYear]
+        zenMonth = yearData[1]
+        zenDay = yearData[2]
+        between = self.__getDaysBetweenSolar(year, zenMonth - 1, zenDay,
+                                             year, month, day)
+        if between == 0:
+            return [year, 0, 1]
+        else:
+            lunarYear = year if between > 0 else year - 1
+            return self.__getLunarDateByBetween(lunarYear, between)
+
+    def __getDaysBetweenSolar(self, year, month, day, year1, month1, day1):
+        """
+        兩個公曆日期之間的天數
+        """
+        # https://stackoverflow.com/a/151211
+        d0 = datetime.date(year, month + 1, day)
+        d1 = datetime.date(year1, month1 + 1, day1)
+        delta = d1 - d0
+        return delta.days
+
+    def __getDaysBetweenZheng(self, year, month, day):
+        """
+        計算農曆日期離正月初一有多少天
+        @param {Number} year,month,day 農年,月(0-12,有閏月),日
+        """
+        lunarYearDays = self.__getLunarYearDays(year)
+        monthDays = lunarYearDays['monthDays']
+        days = 0
+        for i in range(len(monthDays)):
+            if i < month:
+                days += monthDays[i]
+            else:
+                break
+        return days + day - 1
+
+    def __getTerm(self, y, n):
+        """
+        某年的第n個節氣為幾日
+        31556925974.7為地球公轉週期,是毫秒
+        1890年的正小寒點:01-05 16:02:31,1890年為基準點
+        @param {Number} y 公曆年
+        @param {Number} n 第幾個節氣,從0小寒起算
+        由於農曆24節氣交節時刻採用近似算法,可能存在少量誤差(30分鐘內)
+        """
+        offsetms = 31556925974.7 * (y - 1890) + self.__termInfo[n] * 60000
+        offDate = datetime.datetime(
+            1890, 1, 5, 16, 2, 31,
+            tzinfo=datetime.timezone.utc
+        ) + datetime.timedelta(milliseconds=offsetms)
+        return offDate.day
+
+    def __getYearTerm(self, year):
+        """
+        獲取公曆年一年的二十四節氣
+        返回key:日期,value:節氣中文名
+        """
+        res = {}
+        month = 0
+        for i in range(24):
+            day = self.__getTerm(year, i)
+            if i % 2 == 0:
+                month += 1
+                res[self.__formatDayD4(month - 1, day)] = self.__solarTerm[i]
+        return res
+
+    def __getYearZodiac(self, year):
+        """
+        獲取生肖
+        @param {Number} year 干支所在年(默認以立春前的公曆年作為基數)
+        """
+        num = year - 1890 + 25  # 參考干支紀年的計算,生肖對應地支
+        return self.__zodiac[num % 12]
+
+    def __cyclical(self, num):
+        """
+        計算天干地支
+        @param {Number} num 60進制中的位置(把60個天干地支,當成一個60進制的數)
+        """
+        return (
+            self.__heavenlyStems[num % 10] +
+            self.__earthlyBranches[num % 12]
+        )
+
+    def __getLunarYearName(self, year, offset=0):
+        """
+        獲取干支紀年
+        @param {Number} year 干支所在年
+        @param {Number} offset 偏移量,默認為0,便於查詢一個年跨兩個干支紀年(以立春為分界線)
+        """
+        # 1890年1月小寒(小寒一般是1月5或6日)以前為己丑年,在60進制中排25
+        return self.__cyclical(year - 1890 + 25 + offset)
+
+    def __getLunarMonthName(self, year, month, offset=0):
+        """
+        獲取干支紀月
+        @param {Number} year,month 公曆年,干支所在月
+        @param {Number} offset 偏移量,默認為0,便於查詢一個月跨兩個干支紀月(有立春的2月)
+        """
+        # 1890年1月小寒以前為丙子月,在60進制中排12
+        return self.__cyclical((year - 1890) * 12 + month + 12 + offset)
+
+    def __getLunarDayName(self, year, month, day):
+        """
+        獲取干支紀日
+        @param {Number} year,month,day 公曆年,月,日
+        """
+        # 當日與189​​0/1/1 相差天數
+        # 1890/1/1與1970/1/1 相差29219日, 1890/1/1 日柱為壬午日(60進制18)
+        dayCyclical = (
+            datetime.date(year, month + 1, day) +
+            datetime.timedelta(days=29219+18) -
+            datetime.date(1970, 1, 1)
+        )
+
+        return self.__cyclical(dayCyclical.days)
+
+    def __getSolarMonthDays(self, year, month):
+        """
+        獲取公曆月份的天數
+        @param {Number} year 公曆年
+        @param {Number} month 公曆月
+        """
+        monthDays = [31, 29 if self.__isLeapYear(year) else 28,
+                     31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+        return monthDays[month]
+
+    def __isLeapYear(self, year):
+        """
+        判斷公曆年是否是閏年
+        @param {Number} year 公曆年
+        """
+        return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
+
+    def __formatDate(self, year=None, month=None, day=None, _minYear=None):
+        """
+        統一日期輸入參數(輸入月份從1開始,內部月份統一從0開始)
+        """
+        now = datetime.datetime.now()
+        year = int(year) if year is not None else now.year
+        month = int(month) - 1 if month is not None else now.month - 1
+        day = int(day) if day is not None else now.day
+        if year < (_minYear if _minYear is not None
+                   else self.__minYear + 1) or year > self.__maxYear:
+            return {
+                'error': 100,
+                'msg': 'Year out of range'
+            }
+        return {
+            'year': year,
+            'month': month,
+            'day': day
+        }
+
+    def lunarToSolar(self, _year, _month, _day):
+        """
+        將農曆轉換為公曆
+        @param {Number} year,month,day 農曆年,月(1-13,有閏月),日
+        year: Gregorian year
+        """
+        inputDate = self.__formatDate(_year, _month, _day)
+        try:
+            inputDate['error']
+        except KeyError:
+            pass
+        else:
+            return inputDate
+        year = inputDate['year']
+        month = inputDate['month']
+        day = inputDate['day']
+
+        between = self.__getDaysBetweenZheng(year, month, day)  # 離正月初一的天數
+        yearData = self.__lunarInfo[year - self.__minYear]
+        zenMonth = yearData[1]
+        zenDay = yearData[2]
+
+        offDate = (
+            datetime.date(year, zenMonth, zenDay) +
+            datetime.timedelta(days=between)
+        )
+        return {
+            'year': offDate.year,
+            'month': offDate.month,
+            'day': offDate.day
+        }
+
+    def solarToLunar(self, _year, _month, _day):
+        """
+        Converts Gregorian date to lunar calendar.
+        _year: integer between 1891 and 2100 inclusive.
+        _month, _day: integer, 1-indexed.
+        """
+        inputDate = self.__formatDate(_year, _month, _day, self.__minYear)
+        try:
+            inputDate['error']
+        except KeyError:
+            pass
+        else:
+            return inputDate
+
+        year = inputDate['year']
+        month = inputDate['month']
+        day = inputDate['day']
+
+        try:
+            termList = self.__cache[year]  # 二十四節氣
+        except KeyError:
+            self.__cache[year] = self.__getYearTerm(year)
+            termList = self.__cache[year]
+
+        term2 = [k for (k, v) in termList.items() if v == '立春'][0]
+        term2 = int(term2[-1])
+
+        firstTerm = self.__getTerm(year, month * 2)
+        GanZhiYear = (
+            year + 1 if month > 1 or month == 1 and day >= term2
+            else year
+        )
+        GanZhiMonth = month + 1 if day >= firstTerm else month
+
+        lunarDate = self.__getLunarByBetween(year, month, day)
+        lunarLeapMonth = self.__getLunarLeapYear(lunarDate[0])
+
+        if lunarLeapMonth > 0 and lunarLeapMonth == lunarDate[1]:
+            lunarMonthName = '閏' + self.__monthCn[lunarDate[1] - 1] + '月'
+        elif lunarLeapMonth > 0 and lunarDate[1] > lunarLeapMonth:
+            lunarMonthName = self.__monthCn[lunarDate[1] - 1] + '月'
+        else:
+            lunarMonthName = self.__monthCn[lunarDate[1]] + '月'
+
+        try:
+            term = termList[self.__formatDayD4(month, day)]
+        except KeyError:
+            term = None
+        return {
+            'zodiac': self.__getYearZodiac(GanZhiYear),
+            'GanZhiYear': self.__getLunarYearName(GanZhiYear),
+            'GanZhiMonth': self.__getLunarMonthName(year, GanZhiMonth),
+            'GanZhiDay': self.__getLunarDayName(year, month, day),
+            'term': term,
+            'lunarYear': lunarDate[0],
+            'lunarMonth': lunarDate[1] + 1,
+            'lunarDay': lunarDate[2],
+            'lunarMonthName': lunarMonthName,
+            'lunarDayName': self.__dateCn[lunarDate[2] - 1],
+            'lunarLeapMonth': lunarLeapMonth
+        }
diff --git a/lunarInfo.csv b/lunarInfo.csv
new file mode 100644 (file)
index 0000000..6b82f59
--- /dev/null
@@ -0,0 +1,211 @@
+2,1,21,22184\r
+0,2,9,21936\r
+6,1,30,9656\r
+0,2,17,9584\r
+0,2,6,21168\r
+5,1,26,43344\r
+0,2,13,59728\r
+0,2,2,27296\r
+3,1,22,44368\r
+0,2,10,43856\r
+8,1,30,19304\r
+0,2,19,19168\r
+0,2,8,42352\r
+5,1,29,21096\r
+0,2,16,53856\r
+0,2,4,55632\r
+4,1,25,27304\r
+0,2,13,22176\r
+0,2,2,39632\r
+2,1,22,19176\r
+0,2,10,19168\r
+6,1,30,42200\r
+0,2,18,42192\r
+0,2,6,53840\r
+5,1,26,54568\r
+0,2,14,46400\r
+0,2,3,54944\r
+2,1,23,38608\r
+0,2,11,38320\r
+7,2,1,18872\r
+0,2,20,18800\r
+0,2,8,42160\r
+5,1,28,45656\r
+0,2,16,27216\r
+0,2,5,27968\r
+4,1,24,44456\r
+0,2,13,11104\r
+0,2,2,38256\r
+2,1,23,18808\r
+0,2,10,18800\r
+6,1,30,25776\r
+0,2,17,54432\r
+0,2,6,59984\r
+5,1,26,27976\r
+0,2,14,23248\r
+0,2,4,11104\r
+3,1,24,37744\r
+0,2,11,37600\r
+7,1,31,51560\r
+0,2,19,51536\r
+0,2,8,54432\r
+6,1,27,55888\r
+0,2,15,46416\r
+0,2,5,22176\r
+4,1,25,43736\r
+0,2,13,9680\r
+0,2,2,37584\r
+2,1,22,51544\r
+0,2,10,43344\r
+7,1,29,46248\r
+0,2,17,27808\r
+0,2,6,46416\r
+5,1,27,21928\r
+0,2,14,19872\r
+0,2,3,42416\r
+3,1,24,21176\r
+0,2,12,21168\r
+8,1,31,43344\r
+0,2,18,59728\r
+0,2,8,27296\r
+6,1,28,44368\r
+0,2,15,43856\r
+0,2,5,19296\r
+4,1,25,42352\r
+0,2,13,42352\r
+0,2,2,21088\r
+3,1,21,59696\r
+0,2,9,55632\r
+7,1,30,23208\r
+0,2,17,22176\r
+0,2,6,38608\r
+5,1,27,19176\r
+0,2,15,19152\r
+0,2,3,42192\r
+4,1,23,53864\r
+0,2,11,53840\r
+8,1,31,54568\r
+0,2,18,46400\r
+0,2,7,46752\r
+6,1,28,38608\r
+0,2,16,38320\r
+0,2,5,18864\r
+4,1,25,42168\r
+0,2,13,42160\r
+10,2,2,45656\r
+0,2,20,27216\r
+0,2,9,27968\r
+6,1,29,44448\r
+0,2,17,43872\r
+0,2,6,38256\r
+5,1,27,18808\r
+0,2,15,18800\r
+0,2,4,25776\r
+3,1,23,27216\r
+0,2,10,59984\r
+8,1,31,27432\r
+0,2,19,23232\r
+0,2,7,43872\r
+5,1,28,37736\r
+0,2,16,37600\r
+0,2,5,51552\r
+4,1,24,54440\r
+0,2,12,54432\r
+0,2,1,55888\r
+2,1,22,23208\r
+0,2,9,22176\r
+7,1,29,43736\r
+0,2,18,9680\r
+0,2,7,37584\r
+5,1,26,51544\r
+0,2,14,43344\r
+0,2,3,46240\r
+4,1,23,46416\r
+0,2,10,44368\r
+9,1,31,21928\r
+0,2,19,19360\r
+0,2,8,42416\r
+6,1,28,21176\r
+0,2,16,21168\r
+0,2,5,43312\r
+4,1,25,29864\r
+0,2,12,27296\r
+0,2,1,44368\r
+2,1,22,19880\r
+0,2,10,19296\r
+6,1,29,42352\r
+0,2,17,42208\r
+0,2,6,53856\r
+5,1,26,59696\r
+0,2,13,54576\r
+0,2,3,23200\r
+3,1,23,27472\r
+0,2,11,38608\r
+11,1,31,19176\r
+0,2,19,19152\r
+0,2,8,42192\r
+6,1,28,53848\r
+0,2,15,53840\r
+0,2,4,54560\r
+5,1,24,55968\r
+0,2,12,46496\r
+0,2,1,22224\r
+2,1,22,19160\r
+0,2,10,18864\r
+7,1,30,42168\r
+0,2,17,42160\r
+0,2,6,43600\r
+5,1,26,46376\r
+0,2,14,27936\r
+0,2,2,44448\r
+3,1,23,21936\r
+0,2,11,37744\r
+8,2,1,18808\r
+0,2,19,18800\r
+0,2,8,25776\r
+6,1,28,27216\r
+0,2,15,59984\r
+0,2,4,27424\r
+4,1,24,43872\r
+0,2,12,43744\r
+0,2,2,37600\r
+3,1,21,51568\r
+0,2,9,51552\r
+7,1,29,54440\r
+0,2,17,54432\r
+0,2,5,55888\r
+5,1,26,23208\r
+0,2,14,22176\r
+0,2,3,42704\r
+4,1,23,21224\r
+0,2,11,21200\r
+8,1,31,43352\r
+0,2,19,43344\r
+0,2,7,46240\r
+6,1,27,46416\r
+0,2,15,44368\r
+0,2,5,21920\r
+4,1,24,42448\r
+0,2,12,42416\r
+0,2,2,21168\r
+3,1,22,43320\r
+0,2,9,26928\r
+7,1,29,29336\r
+0,2,17,27296\r
+0,2,6,44368\r
+5,1,26,19880\r
+0,2,14,19296\r
+0,2,3,42352\r
+4,1,24,21104\r
+0,2,10,53856\r
+8,1,30,59696\r
+0,2,18,54560\r
+0,2,7,55968\r
+6,1,27,27472\r
+0,2,15,22224\r
+0,2,5,19168\r
+4,1,25,42216\r
+0,2,12,42192\r
+0,2,1,53584\r
+2,1,21,55592\r
+0,2,9,54560\r