Coverage for src/inheritance_calculator_core/utils/era_converter.py: 0%

91 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-17 05:31 +0900

1"""日本の元号と西暦の変換ユーティリティ 

2 

3このモジュールは日本の元号(明治、大正、昭和、平成、令和)と 

4西暦の相互変換機能を提供します。 

5""" 

6 

7from datetime import date 

8from typing import Dict, Optional, Tuple 

9import re 

10 

11 

12# 元号マッピングデータ 

13# Format: 元号名 -> (略称, 開始年, 開始月, 開始日, 終了年, 終了月, 終了日) 

14ERA_MAP: Dict[str, Tuple[str, int, int, int, Optional[int], Optional[int], Optional[int]]] = { 

15 "明治": ("M", 1868, 1, 25, 1912, 7, 30), 

16 "大正": ("T", 1912, 7, 30, 1926, 12, 25), 

17 "昭和": ("S", 1926, 12, 25, 1989, 1, 7), 

18 "平成": ("H", 1989, 1, 8, 2019, 4, 30), 

19 "令和": ("R", 2019, 5, 1, None, None, None), 

20} 

21 

22# 略称から元号名へのマッピング 

23ABBREV_TO_ERA: Dict[str, str] = { 

24 "M": "明治", 

25 "T": "大正", 

26 "S": "昭和", 

27 "H": "平成", 

28 "R": "令和", 

29} 

30 

31 

32class EraConversionError(ValueError): 

33 """元号変換エラー""" 

34 pass 

35 

36 

37def parse_japanese_date(input_str: str) -> date: 

38 """元号形式の日付を西暦dateオブジェクトに変換 

39 

40 Args: 

41 input_str: 元号形式の日付文字列 

42 例: "令和5年10月3日", "R5.10.3", "R5/10/3" 

43 

44 Returns: 

45 date: 西暦のdateオブジェクト 

46 

47 Raises: 

48 EraConversionError: 変換できない形式の場合 

49 

50 Examples: 

51 >>> parse_japanese_date("令和5年10月3日") 

52 datetime.date(2023, 10, 3) 

53 >>> parse_japanese_date("R5.10.3") 

54 datetime.date(2023, 10, 3) 

55 >>> parse_japanese_date("H31/4/30") 

56 datetime.date(2019, 4, 30) 

57 """ 

58 # 全角数字を半角に変換 

59 input_str = _normalize_numbers(input_str) 

60 

61 # パターン1: 令和5年10月3日 

62 pattern1 = r"^([明大昭平令][治正和成和])(\d{1,2})年(\d{1,2})月(\d{1,2})日$" 

63 match = re.match(pattern1, input_str) 

64 if match: 

65 era_name = match.group(1) 

66 era_year = int(match.group(2)) 

67 month = int(match.group(3)) 

68 day = int(match.group(4)) 

69 return _convert_era_to_date(era_name, era_year, month, day) 

70 

71 # パターン2: R5.10.3 または R5/10/3 

72 pattern2 = r"^([MTSHR])(\d{1,2})[./](\d{1,2})[./](\d{1,2})$" 

73 match = re.match(pattern2, input_str) 

74 if match: 

75 abbrev = match.group(1) 

76 era_year = int(match.group(2)) 

77 month = int(match.group(3)) 

78 day = int(match.group(4)) 

79 

80 if abbrev not in ABBREV_TO_ERA: 

81 raise EraConversionError(f"不明な元号略称: {abbrev}") 

82 

83 era_name = ABBREV_TO_ERA[abbrev] 

84 return _convert_era_to_date(era_name, era_year, month, day) 

85 

86 # パターン3: 西暦形式(YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD) 

87 pattern3 = r"^(\d{4})[\-/.](\d{1,2})[\-/.](\d{1,2})$" 

88 match = re.match(pattern3, input_str) 

89 if match: 

90 year = int(match.group(1)) 

91 month = int(match.group(2)) 

92 day = int(match.group(3)) 

93 try: 

94 return date(year, month, day) 

95 except ValueError as e: 

96 raise EraConversionError(f"無効な日付: {input_str}") from e 

97 

98 raise EraConversionError( 

99 f"サポートされていない日付形式: {input_str}\n" 

100 f"サポート形式: 令和5年10月3日, R5.10.3, R5/10/3, 2023-10-03" 

101 ) 

102 

103 

104def _normalize_numbers(text: str) -> str: 

105 """全角数字を半角数字に変換""" 

106 zen_to_han = str.maketrans("0123456789", "0123456789") 

107 return text.translate(zen_to_han) 

108 

109 

110def _convert_era_to_date(era_name: str, era_year: int, month: int, day: int) -> date: 

111 """元号と年月日を西暦dateに変換 

112 

113 Args: 

114 era_name: 元号名(例: "令和") 

115 era_year: 元号の年(例: 5) 

116 month: 月 

117 day: 日 

118 

119 Returns: 

120 date: 西暦のdateオブジェクト 

121 

122 Raises: 

123 EraConversionError: 無効な元号や日付の場合 

124 """ 

125 if era_name not in ERA_MAP: 

126 raise EraConversionError(f"不明な元号: {era_name}") 

127 

128 abbrev, start_year, start_month, start_day, end_year, end_month, end_day = ERA_MAP[era_name] 

129 

130 # 元号元年は1年として扱う 

131 if era_year < 1: 

132 raise EraConversionError(f"{era_name}の年は1以上である必要があります: {era_year}") 

133 

134 # 西暦年を計算 

135 western_year = start_year + era_year - 1 

136 

137 # 日付を作成 

138 try: 

139 result_date = date(western_year, month, day) 

140 except ValueError as e: 

141 raise EraConversionError(f"無効な日付: {era_name}{era_year}{month}{day}") from e 

142 

143 # 元号の開始日より前でないかチェック 

144 era_start = date(start_year, start_month, start_day) 

145 if result_date < era_start: 

146 raise EraConversionError( 

147 f"{era_name}{era_year}{month}{day}日は{era_name}の開始日({era_start})より前です" 

148 ) 

149 

150 # 元号の終了日より後でないかチェック(令和は終了日なし) 

151 if end_year is not None and end_month is not None and end_day is not None: 

152 era_end = date(end_year, end_month, end_day) 

153 if result_date > era_end: 

154 raise EraConversionError( 

155 f"{era_name}{era_year}{month}{day}日は{era_name}の終了日({era_end})より後です" 

156 ) 

157 

158 return result_date 

159 

160 

161def format_japanese_date(target_date: date, format_type: str = "long") -> str: 

162 """西暦dateを元号形式の文字列に変換 

163 

164 Args: 

165 target_date: 西暦のdateオブジェクト 

166 format_type: 出力形式 

167 - "long": 令和5年10月3日 

168 - "short": R5.10.3 

169 - "slash": R5/10/3 

170 

171 Returns: 

172 str: 元号形式の日付文字列 

173 

174 Raises: 

175 EraConversionError: 明治以前の日付の場合 

176 

177 Examples: 

178 >>> format_japanese_date(date(2023, 10, 3), "long") 

179 '令和5年10月3日' 

180 >>> format_japanese_date(date(2023, 10, 3), "short") 

181 'R5.10.3' 

182 """ 

183 # 該当する元号を検索 

184 for era_name, (abbrev, start_year, start_month, start_day, end_year, end_month, end_day) in ERA_MAP.items(): 

185 era_start = date(start_year, start_month, start_day) 

186 

187 # 終了日の判定 

188 if end_year is not None and end_month is not None and end_day is not None: 

189 era_end = date(end_year, end_month, end_day) 

190 if era_start <= target_date <= era_end: 

191 era_year = target_date.year - start_year + 1 

192 return _format_era_string(era_name, abbrev, era_year, target_date.month, target_date.day, format_type) 

193 else: 

194 # 令和(終了日なし) 

195 if target_date >= era_start: 

196 era_year = target_date.year - start_year + 1 

197 return _format_era_string(era_name, abbrev, era_year, target_date.month, target_date.day, format_type) 

198 

199 raise EraConversionError(f"明治以前の日付は変換できません: {target_date}") 

200 

201 

202def _format_era_string(era_name: str, abbrev: str, era_year: int, month: int, day: int, format_type: str) -> str: 

203 """元号文字列をフォーマット""" 

204 if format_type == "long": 

205 return f"{era_name}{era_year}{month}{day}" 

206 elif format_type == "short": 

207 return f"{abbrev}{era_year}.{month}.{day}" 

208 elif format_type == "slash": 

209 return f"{abbrev}{era_year}/{month}/{day}" 

210 else: 

211 raise ValueError(f"不明なフォーマット: {format_type}") 

212 

213 

214def get_era_name(target_date: date) -> str: 

215 """指定された日付の元号名を取得 

216 

217 Args: 

218 target_date: 西暦のdateオブジェクト 

219 

220 Returns: 

221 str: 元号名(例: "令和") 

222 

223 Raises: 

224 EraConversionError: 明治以前の日付の場合 

225 """ 

226 for era_name, (abbrev, start_year, start_month, start_day, end_year, end_month, end_day) in ERA_MAP.items(): 

227 era_start = date(start_year, start_month, start_day) 

228 

229 if end_year is not None and end_month is not None and end_day is not None: 

230 era_end = date(end_year, end_month, end_day) 

231 if era_start <= target_date <= era_end: 

232 return era_name 

233 else: 

234 if target_date >= era_start: 

235 return era_name 

236 

237 raise EraConversionError(f"明治以前の日付は対応していません: {target_date}")