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

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

2

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

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

5"""

6

7 from datetime import date

8 from typing import Dict, Optional, Tuple

9 import re

10

11

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

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

14 ERA_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# 略称から元号名へのマッピング

23 ABBREV_TO_ERA: Dict[str, str] = {

24 "M": "明治",

25 "T": "大正",

26 "S": "昭和",

27 "H": "平成",

28 "R": "令和",

29}

30

31

32 class EraConversionError(ValueError):

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

34 pass

35

36

37 def 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

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

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

106 zen_to_han = str.maketrans("０１２３４５６７８９", "0123456789")

107 return text.translate(zen_to_han)

108

109

110 def _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

161 def 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

202 def _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

214 def 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}")