solar_to_lunar.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. from odps.udf import annotate
  4. FMT_STR_MAP = {
  5. "yyyyMMdd": "%Y%m%d",
  6. "yyyy-MM-dd": "%Y-%m-%d",
  7. "yyyy年MM月dd日": "%Y年%m月%d日"
  8. }
  9. DEFAULT_OUTPUT_MODE = "default"
  10. CN_OUTPUT_MODE = "cn"
  11. class LunarDate(object):
  12. def __init__(self, year, month, day, is_leap=False):
  13. self.year = year
  14. self.month = month
  15. self.day = day
  16. self.is_leap = is_leap
  17. class InputDateParser(object):
  18. """第一部分: 解析输入日期。"""
  19. def __init__(self, fmt_map=None):
  20. self._fmt_map = fmt_map or FMT_STR_MAP
  21. def parse(self, date_str, fmt_str):
  22. py_fmt_str = self._fmt_map.get(fmt_str)
  23. if py_fmt_str is None:
  24. raise ValueError("unsupported input format {fmt_str}".format(fmt_str=fmt_str))
  25. dt = datetime.datetime.strptime(date_str, py_fmt_str)
  26. return dt.year, dt.month, dt.day
  27. class LunarFormatterBase(object):
  28. """第三部分: 根据 fmt_str 和 output 输出最终结果。"""
  29. LUNAR_MONTH_NAME = [
  30. "", "正月", "二月", "三月", "四月", "五月", "六月",
  31. "七月", "八月", "九月", "十月", "冬月", "腊月"
  32. ]
  33. LUNAR_DAY_NAME = [
  34. "", "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
  35. "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
  36. "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
  37. ]
  38. DIGIT_MAP = {
  39. '0': '〇',
  40. '1': '一',
  41. '2': '二',
  42. '3': '三',
  43. '4': '四',
  44. '5': '五',
  45. '6': '六',
  46. '7': '七',
  47. '8': '八',
  48. '9': '九'
  49. }
  50. @staticmethod
  51. def _apply_leap_prefix(value, is_leap):
  52. return "闰" + str(value) if is_leap else str(value)
  53. def _year_to_cnt(self, year):
  54. return ''.join(self.DIGIT_MAP[ch] for ch in str(year))
  55. def _to_cn_number(self, value):
  56. return self._year_to_cnt(value)
  57. @staticmethod
  58. def _to_arabic_number(value, width=None):
  59. # width 为空时只做普通字符串转换;需要固定宽度时由 formatter 传入。
  60. value_str = str(value)
  61. if width is not None:
  62. return value_str.zfill(width)
  63. return value_str
  64. def format(self, lunar_date):
  65. raise NotImplementedError
  66. class YyyyMMddDefaultFormatter(LunarFormatterBase):
  67. def format(self, lunar_date):
  68. ly = self._to_arabic_number(lunar_date.year)
  69. lm = self._apply_leap_prefix(self._to_arabic_number(lunar_date.month, 2), lunar_date.is_leap)
  70. ld = self._to_arabic_number(lunar_date.day, 2)
  71. return "{ly}{lm}{ld}".format(ly=ly, lm=lm, ld=ld)
  72. class YyyyMMddCnFormatter(LunarFormatterBase):
  73. def format(self, lunar_date):
  74. ly = self._to_cn_number(lunar_date.year)
  75. lm = self._apply_leap_prefix(self._to_cn_number(lunar_date.month), lunar_date.is_leap)
  76. ld = self._to_cn_number(lunar_date.day)
  77. return "{ly}{lm}{ld}".format(ly=ly, lm=lm, ld=ld)
  78. class YyyyDashMmDashDdDefaultFormatter(LunarFormatterBase):
  79. def format(self, lunar_date):
  80. ly = self._to_arabic_number(lunar_date.year)
  81. lm = self._apply_leap_prefix(self._to_arabic_number(lunar_date.month, 2), lunar_date.is_leap)
  82. ld = self._to_arabic_number(lunar_date.day, 2)
  83. return "{ly}-{lm}-{ld}".format(ly=ly, lm=lm, ld=ld)
  84. class YyyyDashMmDashDdCnFormatter(LunarFormatterBase):
  85. def format(self, lunar_date):
  86. ly = self._to_cn_number(lunar_date.year)
  87. lm = self._apply_leap_prefix(self._to_cn_number(lunar_date.month), lunar_date.is_leap)
  88. ld = self._to_cn_number(lunar_date.day)
  89. return "{ly}-{lm}-{ld}".format(ly=ly, lm=lm, ld=ld)
  90. class YyyyCnFormatter(LunarFormatterBase):
  91. def format(self, lunar_date):
  92. ly = self._year_to_cnt(lunar_date.year)
  93. lm = self.LUNAR_MONTH_NAME[lunar_date.month]
  94. ld = self.LUNAR_DAY_NAME[lunar_date.day]
  95. lm = self._apply_leap_prefix(lm, lunar_date.is_leap)
  96. return "{ly}年{lm}{ld}日".format(ly=ly, lm=lm, ld=ld)
  97. class LunarFormatterRegistry(object):
  98. """按 fmt_str 选择输出格式化策略。"""
  99. _registry = {
  100. "yyyyMMdd": {
  101. DEFAULT_OUTPUT_MODE: YyyyMMddDefaultFormatter(),
  102. CN_OUTPUT_MODE: YyyyMMddCnFormatter()
  103. },
  104. "yyyy-MM-dd": {
  105. DEFAULT_OUTPUT_MODE: YyyyDashMmDashDdDefaultFormatter(),
  106. CN_OUTPUT_MODE: YyyyDashMmDashDdCnFormatter()
  107. },
  108. "yyyy年MM月dd日": {
  109. DEFAULT_OUTPUT_MODE: YyyyCnFormatter()
  110. }
  111. }
  112. SUPPORTED_FMT_STRS = frozenset(_registry.keys())
  113. @classmethod
  114. def get(cls, fmt_str, output_mode):
  115. if fmt_str not in cls.SUPPORTED_FMT_STRS:
  116. return None
  117. mode_registry = cls._registry.get(fmt_str, {})
  118. return mode_registry.get(output_mode)
  119. class SolarToLunarConverter(object):
  120. # 农历数据(1900-2100)
  121. LUNAR_INFO = [
  122. 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554,
  123. 0x056a0, 0x09ad0, 0x055d2, 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250,
  124. 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, 0x04970,
  125. 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570,
  126. 0x052f2, 0x04970, 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0,
  127. 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, 0x0d4a0, 0x1d8a6,
  128. 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950,
  129. 0x0b557, 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573,
  130. 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, 0x0aea6, 0x0ab50, 0x04b60,
  131. 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
  132. 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558,
  133. 0x0b540, 0x0b5a0, 0x195a6, 0x095b0, 0x049b0, 0x0a974, 0x0a4b0,
  134. 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, 0x04af5,
  135. 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60,
  136. 0x096d5, 0x092e0, 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552,
  137. 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, 0x0a950, 0x0b4a0,
  138. 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0,
  139. 0x0a930, 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6,
  140. 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, 0x05aa0, 0x076a3, 0x096d0,
  141. 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
  142. 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50,
  143. 0x1b255, 0x06d20, 0x0ada0
  144. ]
  145. def __init__(self):
  146. self._input_parser = InputDateParser()
  147. @staticmethod
  148. def _leap_month(year):
  149. return SolarToLunarConverter.LUNAR_INFO[year - 1900] & 0xf
  150. @staticmethod
  151. def _leap_days(year):
  152. if SolarToLunarConverter._leap_month(year):
  153. return 30 if (SolarToLunarConverter.LUNAR_INFO[year - 1900] & 0x10000) else 29
  154. return 0
  155. @staticmethod
  156. def _month_days(year, month):
  157. return 30 if (SolarToLunarConverter.LUNAR_INFO[year - 1900] & (0x10000 >> month)) else 29
  158. @staticmethod
  159. def _year_days(year):
  160. sum_days = 348
  161. i = 0x8000
  162. while i > 0x8:
  163. if SolarToLunarConverter.LUNAR_INFO[year - 1900] & i:
  164. sum_days += 1
  165. i >>= 1
  166. return sum_days + SolarToLunarConverter._leap_days(year)
  167. @staticmethod
  168. def _days_since_base_date(year, month, day):
  169. base = datetime.date(1900, 1, 31)
  170. obj = datetime.date(year, month, day)
  171. return (obj - base).days
  172. def _find_lunar_year_and_offset(self, offset):
  173. """先把公历偏移量落到具体的农历年份里。"""
  174. lunar_year = 1900
  175. while lunar_year < 2100:
  176. year_days = self._year_days(lunar_year)
  177. if offset < year_days:
  178. return lunar_year, offset
  179. offset -= year_days
  180. lunar_year += 1
  181. raise ValueError("date is out of supported lunar range")
  182. def _iter_lunar_month_schedule(self, year):
  183. """按顺序列出该农历年的月份,闰月会在对应普通月后额外插入一次。"""
  184. leap = self._leap_month(year)
  185. for lunar_month in range(1, 13):
  186. yield lunar_month, False, self._month_days(year, lunar_month)
  187. if leap == lunar_month:
  188. yield lunar_month, True, self._leap_days(year)
  189. def _find_lunar_month_day(self, year, offset):
  190. """在指定农历年内,根据剩余偏移量定位到月和日。"""
  191. for lunar_month, is_leap, month_days in self._iter_lunar_month_schedule(year):
  192. if offset < month_days:
  193. return lunar_month, offset + 1, is_leap
  194. offset -= month_days
  195. raise ValueError("date is out of supported lunar range")
  196. def solar_to_lunar_core(self, year, month, day):
  197. """把公历日期转换成农历日期。"""
  198. offset = self._days_since_base_date(year, month, day)
  199. if offset < 0:
  200. raise ValueError("date is out of supported lunar range")
  201. lunar_year, lunar_offset = self._find_lunar_year_and_offset(offset)
  202. lunar_month, lunar_day, is_leap = self._find_lunar_month_day(lunar_year, lunar_offset)
  203. return lunar_year, lunar_month, lunar_day, is_leap
  204. def convert(self, date_str, fmt_str="yyyy-MM-dd", output=DEFAULT_OUTPUT_MODE):
  205. if not date_str:
  206. return None
  207. output = DEFAULT_OUTPUT_MODE if output in (None, "") else output
  208. y, m, d = self._input_parser.parse(date_str, fmt_str)
  209. lunar_year, lunar_month, lunar_day, is_leap = self.solar_to_lunar_core(y, m, d)
  210. lunar_date = LunarDate(lunar_year, lunar_month, lunar_day, is_leap)
  211. formatter = LunarFormatterRegistry.get(fmt_str, output)
  212. if formatter is None:
  213. raise ValueError(
  214. "unsupported output mode {output} for format {fmt_str}".format(
  215. output=output,
  216. fmt_str=fmt_str
  217. )
  218. )
  219. return formatter.format(lunar_date)
  220. @annotate("*->string")
  221. class solar_to_lunar(object):
  222. def __init__(self):
  223. self._converter = SolarToLunarConverter()
  224. def evaluate(self, date_str, fmt_str="yyyy-MM-dd", output=DEFAULT_OUTPUT_MODE):
  225. try:
  226. return self._converter.convert(date_str, fmt_str, output)
  227. except ValueError as e:
  228. return "ERROR: {type}: {e}".format(type=type(e).__name__, e=str(e))
  229. except Exception as e:
  230. return "ERROR: {type}: {e}".format(type=type(e).__name__, e=str(e))