Commit | Line | Data |
---|---|---|
e0750e84 AIL |
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 | |
496b85f9 | 198 | res[self.__formatDayD4(month - 1, day)] = self.__solarTerm[i] |
e0750e84 AIL |
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 | } |