Initial commit
[LunarCalendarPy.git] / LunarCalendar.py
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 # 當日與189​​0/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 }