| 1 | import csv |
| 2 | import datetime |
| 3 | import os |
| 4 | |
| 5 | |
| 6 | class LunarCalendar: |
| 7 | """ |
| 8 | LunarCalendar by Adrian I. Lam (2019), |
| 9 | a Python 3 port of the JavaScript LunarCalendar by zzyss86, |
| 10 | available at <https://github.com/zzyss86/LunarCalendar>. |
| 11 | |
| 12 | This is only a partial port. I have ported |
| 13 | LunarCalendar.solarToLunar(year, month, day) and |
| 14 | LunarCalendar.lunarToSolar(year, month, day) only, |
| 15 | and have ignored everything related to holidays. |
| 16 | |
| 17 | Since the original author did not specify any license, I will |
| 18 | also not license this script. This script comes with no warranties, |
| 19 | expressed or implied. In particular, the warranties of merchantability, |
| 20 | fitness for a particular purpose and noninfringement are disclaimed. |
| 21 | """ |
| 22 | |
| 23 | def __formatDayD4(self, month, day): |
| 24 | month = month + 1 |
| 25 | month = '0' + str(month) if month < 10 else str(month) |
| 26 | day = '0' + str(day) if day < 10 else str(day) |
| 27 | return 'd' + month + day |
| 28 | |
| 29 | __minYear = 1890 |
| 30 | __maxYear = 2100 |
| 31 | |
| 32 | __heavenlyStems = ['甲', '乙', '丙', '丁', '戊', |
| 33 | '己', '庚', '辛', '壬', '癸'] # 天干 |
| 34 | __earthlyBranches = ['子', '丑', '寅', '卯', '辰', '巳', |
| 35 | '午', '未', '申', '酉', '戌', '亥'] # 地支 |
| 36 | __zodiac = ['鼠', '牛', '虎', '兔', '龍', '蛇', |
| 37 | '馬', '羊', '猴', '雞', '狗', '豬'] # 對應地支十二生肖 |
| 38 | __solarTerm = ['小寒', '大寒', '立春', '雨水', '驚蟄', '春分', |
| 39 | '清明', '穀雨', '立夏', '小滿', '芒種', '夏至', |
| 40 | '小暑', '大暑', '立秋', '處暑', '白露', '秋分', |
| 41 | '寒露', '霜降', '立冬', '小雪', '大雪', '冬至'] # 二十四節氣 |
| 42 | __monthCn = ['正', '二', '三', '四', '五', '六', |
| 43 | '七', '八', '九', '十', '十一', '十二'] |
| 44 | __dateCn = ['初一', '初二', '初三', '初四', '初五', '初六', |
| 45 | '初七', '初八', '初九', '初十', '十一', '十二', |
| 46 | '十三', '十四', '十五', '十六', '十七', '十八', |
| 47 | '十九', '二十', '廿一', '廿二', '廿三', '廿四', |
| 48 | '廿五', '廿六', '廿七', '廿八', '廿九', '三十', '卅一'] |
| 49 | |
| 50 | # 1890 - 2100 年的農曆數據 |
| 51 | # 數據格式:[0,2,9,21936] |
| 52 | # [閏月所在月,0為沒有閏月; |
| 53 | # *正月初一對應公曆月; |
| 54 | # *正月初一對應公曆日; |
| 55 | # *農曆每月的天數的數組(需轉換為二進制,得到每月大小, |
| 56 | # 0=小月(29日),1=大月(30日));] |
| 57 | __lunarInfo = [] |
| 58 | __dir = os.path.dirname(os.path.realpath(__file__)) |
| 59 | with open(os.path.join(__dir, 'lunarInfo.csv')) as __lunarInfoCSV: |
| 60 | for __row in csv.reader(__lunarInfoCSV): |
| 61 | __lunarInfo.append([int(x) for x in __row]) |
| 62 | |
| 63 | # 二十四節氣數據,節氣點時間(單位是分鐘) |
| 64 | # 從0小寒起算 |
| 65 | __termInfo = [0, 21208, 42467, 63836, 85337, 107014, 128867, 150921, |
| 66 | 173149, 195551, 218072, 240693, 263343, 285989, 308563, |
| 67 | 331033, 353350, 375494, 397447, 419210, 440795, 462224, |
| 68 | 483532, 504758] |
| 69 | |
| 70 | def __init__(self): |
| 71 | self.__cache = {} |
| 72 | |
| 73 | def __getLunarLeapYear(self, year): |
| 74 | """ |
| 75 | 判斷農曆年閏月數 |
| 76 | @param {Number} year 農曆年 |
| 77 | return 閏月數(月份從1開始) |
| 78 | """ |
| 79 | yearData = self.__lunarInfo[year - self.__minYear] |
| 80 | return yearData[0] |
| 81 | |
| 82 | def __getLunarYearDays(self, year): |
| 83 | """ |
| 84 | 獲取農曆年份一年的每月的天數及一年的總天數 |
| 85 | @param {Number} year 農曆年 |
| 86 | """ |
| 87 | yearData = self.__lunarInfo[year - self.__minYear] |
| 88 | leapMonth = yearData[0] |
| 89 | monthData = yearData[3] |
| 90 | |
| 91 | monthDataArr = [] |
| 92 | for i in range(15, -1, -1): |
| 93 | monthDataArr.append((monthData & (1 << i)) >> i) |
| 94 | |
| 95 | numMonthsInYear = 13 if leapMonth else 12 |
| 96 | yearDays = 0 |
| 97 | monthDays = [] |
| 98 | for i in range(numMonthsInYear): |
| 99 | if monthDataArr[i] == 0: |
| 100 | yearDays += 29 |
| 101 | monthDays.append(29) |
| 102 | else: |
| 103 | yearDays += 30 |
| 104 | monthDays.append(30) |
| 105 | |
| 106 | return { |
| 107 | 'yearDays': yearDays, |
| 108 | 'monthDays': monthDays |
| 109 | } |
| 110 | |
| 111 | def __getLunarDateByBetween(self, year, between): |
| 112 | """ |
| 113 | 通過間隔天數查找農曆日期 |
| 114 | @param {Number} year,between 農曆年,間隔天數 |
| 115 | """ |
| 116 | lunarYearDays = self.__getLunarYearDays(year) |
| 117 | end = between if between > 0 else lunarYearDays['yearDays'] + between |
| 118 | monthDays = lunarYearDays['monthDays'] |
| 119 | tempDays = 0 |
| 120 | month = 0 |
| 121 | for i in range(len(monthDays)): |
| 122 | tempDays += monthDays[i] |
| 123 | if tempDays > end: |
| 124 | month = i |
| 125 | tempDays = tempDays - monthDays[i] |
| 126 | break |
| 127 | |
| 128 | return [year, month, end - tempDays + 1] |
| 129 | |
| 130 | def __getLunarByBetween(self, year, month, day): |
| 131 | """ |
| 132 | 根據距離正月初一的天數計算農曆日期 |
| 133 | @param {Number} year 公曆年,月,日 |
| 134 | """ |
| 135 | yearData = self.__lunarInfo[year - self.__minYear] |
| 136 | zenMonth = yearData[1] |
| 137 | zenDay = yearData[2] |
| 138 | between = self.__getDaysBetweenSolar(year, zenMonth - 1, zenDay, |
| 139 | year, month, day) |
| 140 | if between == 0: |
| 141 | return [year, 0, 1] |
| 142 | else: |
| 143 | lunarYear = year if between > 0 else year - 1 |
| 144 | return self.__getLunarDateByBetween(lunarYear, between) |
| 145 | |
| 146 | def __getDaysBetweenSolar(self, year, month, day, year1, month1, day1): |
| 147 | """ |
| 148 | 兩個公曆日期之間的天數 |
| 149 | """ |
| 150 | # https://stackoverflow.com/a/151211 |
| 151 | d0 = datetime.date(year, month + 1, day) |
| 152 | d1 = datetime.date(year1, month1 + 1, day1) |
| 153 | delta = d1 - d0 |
| 154 | return delta.days |
| 155 | |
| 156 | def __getDaysBetweenZheng(self, year, month, day): |
| 157 | """ |
| 158 | 計算農曆日期離正月初一有多少天 |
| 159 | @param {Number} year,month,day 農年,月(0-12,有閏月),日 |
| 160 | """ |
| 161 | lunarYearDays = self.__getLunarYearDays(year) |
| 162 | monthDays = lunarYearDays['monthDays'] |
| 163 | days = 0 |
| 164 | for i in range(len(monthDays)): |
| 165 | if i < month: |
| 166 | days += monthDays[i] |
| 167 | else: |
| 168 | break |
| 169 | return days + day - 1 |
| 170 | |
| 171 | def __getTerm(self, y, n): |
| 172 | """ |
| 173 | 某年的第n個節氣為幾日 |
| 174 | 31556925974.7為地球公轉週期,是毫秒 |
| 175 | 1890年的正小寒點:01-05 16:02:31,1890年為基準點 |
| 176 | @param {Number} y 公曆年 |
| 177 | @param {Number} n 第幾個節氣,從0小寒起算 |
| 178 | 由於農曆24節氣交節時刻採用近似算法,可能存在少量誤差(30分鐘內) |
| 179 | """ |
| 180 | offsetms = 31556925974.7 * (y - 1890) + self.__termInfo[n] * 60000 |
| 181 | offDate = datetime.datetime( |
| 182 | 1890, 1, 5, 16, 2, 31, |
| 183 | tzinfo=datetime.timezone.utc |
| 184 | ) + datetime.timedelta(milliseconds=offsetms) |
| 185 | return offDate.day |
| 186 | |
| 187 | def __getYearTerm(self, year): |
| 188 | """ |
| 189 | 獲取公曆年一年的二十四節氣 |
| 190 | 返回key:日期,value:節氣中文名 |
| 191 | """ |
| 192 | res = {} |
| 193 | month = 0 |
| 194 | for i in range(24): |
| 195 | day = self.__getTerm(year, i) |
| 196 | if i % 2 == 0: |
| 197 | month += 1 |
| 198 | res[self.__formatDayD4(month - 1, day)] = self.__solarTerm[i] |
| 199 | return res |
| 200 | |
| 201 | def __getYearZodiac(self, year): |
| 202 | """ |
| 203 | 獲取生肖 |
| 204 | @param {Number} year 干支所在年(默認以立春前的公曆年作為基數) |
| 205 | """ |
| 206 | num = year - 1890 + 25 # 參考干支紀年的計算,生肖對應地支 |
| 207 | return self.__zodiac[num % 12] |
| 208 | |
| 209 | def __cyclical(self, num): |
| 210 | """ |
| 211 | 計算天干地支 |
| 212 | @param {Number} num 60進制中的位置(把60個天干地支,當成一個60進制的數) |
| 213 | """ |
| 214 | return ( |
| 215 | self.__heavenlyStems[num % 10] + |
| 216 | self.__earthlyBranches[num % 12] |
| 217 | ) |
| 218 | |
| 219 | def __getLunarYearName(self, year, offset=0): |
| 220 | """ |
| 221 | 獲取干支紀年 |
| 222 | @param {Number} year 干支所在年 |
| 223 | @param {Number} offset 偏移量,默認為0,便於查詢一個年跨兩個干支紀年(以立春為分界線) |
| 224 | """ |
| 225 | # 1890年1月小寒(小寒一般是1月5或6日)以前為己丑年,在60進制中排25 |
| 226 | return self.__cyclical(year - 1890 + 25 + offset) |
| 227 | |
| 228 | def __getLunarMonthName(self, year, month, offset=0): |
| 229 | """ |
| 230 | 獲取干支紀月 |
| 231 | @param {Number} year,month 公曆年,干支所在月 |
| 232 | @param {Number} offset 偏移量,默認為0,便於查詢一個月跨兩個干支紀月(有立春的2月) |
| 233 | """ |
| 234 | # 1890年1月小寒以前為丙子月,在60進制中排12 |
| 235 | return self.__cyclical((year - 1890) * 12 + month + 12 + offset) |
| 236 | |
| 237 | def __getLunarDayName(self, year, month, day): |
| 238 | """ |
| 239 | 獲取干支紀日 |
| 240 | @param {Number} year,month,day 公曆年,月,日 |
| 241 | """ |
| 242 | # 當日與1890/1/1 相差天數 |
| 243 | # 1890/1/1與1970/1/1 相差29219日, 1890/1/1 日柱為壬午日(60進制18) |
| 244 | dayCyclical = ( |
| 245 | datetime.date(year, month + 1, day) + |
| 246 | datetime.timedelta(days=29219+18) - |
| 247 | datetime.date(1970, 1, 1) |
| 248 | ) |
| 249 | |
| 250 | return self.__cyclical(dayCyclical.days) |
| 251 | |
| 252 | def __getSolarMonthDays(self, year, month): |
| 253 | """ |
| 254 | 獲取公曆月份的天數 |
| 255 | @param {Number} year 公曆年 |
| 256 | @param {Number} month 公曆月 |
| 257 | """ |
| 258 | monthDays = [31, 29 if self.__isLeapYear(year) else 28, |
| 259 | 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] |
| 260 | return monthDays[month] |
| 261 | |
| 262 | def __isLeapYear(self, year): |
| 263 | """ |
| 264 | 判斷公曆年是否是閏年 |
| 265 | @param {Number} year 公曆年 |
| 266 | """ |
| 267 | return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 |
| 268 | |
| 269 | def __formatDate(self, year=None, month=None, day=None, _minYear=None): |
| 270 | """ |
| 271 | 統一日期輸入參數(輸入月份從1開始,內部月份統一從0開始) |
| 272 | """ |
| 273 | now = datetime.datetime.now() |
| 274 | year = int(year) if year is not None else now.year |
| 275 | month = int(month) - 1 if month is not None else now.month - 1 |
| 276 | day = int(day) if day is not None else now.day |
| 277 | if year < (_minYear if _minYear is not None |
| 278 | else self.__minYear + 1) or year > self.__maxYear: |
| 279 | return { |
| 280 | 'error': 100, |
| 281 | 'msg': 'Year out of range' |
| 282 | } |
| 283 | return { |
| 284 | 'year': year, |
| 285 | 'month': month, |
| 286 | 'day': day |
| 287 | } |
| 288 | |
| 289 | def lunarToSolar(self, _year, _month, _day): |
| 290 | """ |
| 291 | 將農曆轉換為公曆 |
| 292 | @param {Number} year,month,day 農曆年,月(1-13,有閏月),日 |
| 293 | year: Gregorian year |
| 294 | """ |
| 295 | inputDate = self.__formatDate(_year, _month, _day) |
| 296 | try: |
| 297 | inputDate['error'] |
| 298 | except KeyError: |
| 299 | pass |
| 300 | else: |
| 301 | return inputDate |
| 302 | year = inputDate['year'] |
| 303 | month = inputDate['month'] |
| 304 | day = inputDate['day'] |
| 305 | |
| 306 | between = self.__getDaysBetweenZheng(year, month, day) # 離正月初一的天數 |
| 307 | yearData = self.__lunarInfo[year - self.__minYear] |
| 308 | zenMonth = yearData[1] |
| 309 | zenDay = yearData[2] |
| 310 | |
| 311 | offDate = ( |
| 312 | datetime.date(year, zenMonth, zenDay) + |
| 313 | datetime.timedelta(days=between) |
| 314 | ) |
| 315 | return { |
| 316 | 'year': offDate.year, |
| 317 | 'month': offDate.month, |
| 318 | 'day': offDate.day |
| 319 | } |
| 320 | |
| 321 | def solarToLunar(self, _year, _month, _day): |
| 322 | """ |
| 323 | Converts Gregorian date to lunar calendar. |
| 324 | _year: integer between 1891 and 2100 inclusive. |
| 325 | _month, _day: integer, 1-indexed. |
| 326 | """ |
| 327 | inputDate = self.__formatDate(_year, _month, _day, self.__minYear) |
| 328 | try: |
| 329 | inputDate['error'] |
| 330 | except KeyError: |
| 331 | pass |
| 332 | else: |
| 333 | return inputDate |
| 334 | |
| 335 | year = inputDate['year'] |
| 336 | month = inputDate['month'] |
| 337 | day = inputDate['day'] |
| 338 | |
| 339 | try: |
| 340 | termList = self.__cache[year] # 二十四節氣 |
| 341 | except KeyError: |
| 342 | self.__cache[year] = self.__getYearTerm(year) |
| 343 | termList = self.__cache[year] |
| 344 | |
| 345 | term2 = [k for (k, v) in termList.items() if v == '立春'][0] |
| 346 | term2 = int(term2[-1]) |
| 347 | |
| 348 | firstTerm = self.__getTerm(year, month * 2) |
| 349 | GanZhiYear = ( |
| 350 | year + 1 if month > 1 or month == 1 and day >= term2 |
| 351 | else year |
| 352 | ) |
| 353 | GanZhiMonth = month + 1 if day >= firstTerm else month |
| 354 | |
| 355 | lunarDate = self.__getLunarByBetween(year, month, day) |
| 356 | lunarLeapMonth = self.__getLunarLeapYear(lunarDate[0]) |
| 357 | |
| 358 | if lunarLeapMonth > 0 and lunarLeapMonth == lunarDate[1]: |
| 359 | lunarMonthName = '閏' + self.__monthCn[lunarDate[1] - 1] + '月' |
| 360 | elif lunarLeapMonth > 0 and lunarDate[1] > lunarLeapMonth: |
| 361 | lunarMonthName = self.__monthCn[lunarDate[1] - 1] + '月' |
| 362 | else: |
| 363 | lunarMonthName = self.__monthCn[lunarDate[1]] + '月' |
| 364 | |
| 365 | try: |
| 366 | term = termList[self.__formatDayD4(month, day)] |
| 367 | except KeyError: |
| 368 | term = None |
| 369 | return { |
| 370 | 'zodiac': self.__getYearZodiac(GanZhiYear), |
| 371 | 'GanZhiYear': self.__getLunarYearName(GanZhiYear), |
| 372 | 'GanZhiMonth': self.__getLunarMonthName(year, GanZhiMonth), |
| 373 | 'GanZhiDay': self.__getLunarDayName(year, month, day), |
| 374 | 'term': term, |
| 375 | 'lunarYear': lunarDate[0], |
| 376 | 'lunarMonth': lunarDate[1] + 1, |
| 377 | 'lunarDay': lunarDate[2], |
| 378 | 'lunarMonthName': lunarMonthName, |
| 379 | 'lunarDayName': self.__dateCn[lunarDate[2] - 1], |
| 380 | 'lunarLeapMonth': lunarLeapMonth |
| 381 | } |