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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 05:31 +0900
1"""日本の元号と西暦の変換ユーティリティ
3このモジュールは日本の元号(明治、大正、昭和、平成、令和)と
4西暦の相互変換機能を提供します。
5"""
7from datetime import date
8from typing import Dict, Optional, Tuple
9import re
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}
22# 略称から元号名へのマッピング
23ABBREV_TO_ERA: Dict[str, str] = {
24 "M": "明治",
25 "T": "大正",
26 "S": "昭和",
27 "H": "平成",
28 "R": "令和",
29}
32class EraConversionError(ValueError):
33 """元号変換エラー"""
34 pass
37def parse_japanese_date(input_str: str) -> date:
38 """元号形式の日付を西暦dateオブジェクトに変換
40 Args:
41 input_str: 元号形式の日付文字列
42 例: "令和5年10月3日", "R5.10.3", "R5/10/3"
44 Returns:
45 date: 西暦のdateオブジェクト
47 Raises:
48 EraConversionError: 変換できない形式の場合
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)
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)
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))
80 if abbrev not in ABBREV_TO_ERA:
81 raise EraConversionError(f"不明な元号略称: {abbrev}")
83 era_name = ABBREV_TO_ERA[abbrev]
84 return _convert_era_to_date(era_name, era_year, month, day)
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
98 raise EraConversionError(
99 f"サポートされていない日付形式: {input_str}\n"
100 f"サポート形式: 令和5年10月3日, R5.10.3, R5/10/3, 2023-10-03"
101 )
104def _normalize_numbers(text: str) -> str:
105 """全角数字を半角数字に変換"""
106 zen_to_han = str.maketrans("0123456789", "0123456789")
107 return text.translate(zen_to_han)
110def _convert_era_to_date(era_name: str, era_year: int, month: int, day: int) -> date:
111 """元号と年月日を西暦dateに変換
113 Args:
114 era_name: 元号名(例: "令和")
115 era_year: 元号の年(例: 5)
116 month: 月
117 day: 日
119 Returns:
120 date: 西暦のdateオブジェクト
122 Raises:
123 EraConversionError: 無効な元号や日付の場合
124 """
125 if era_name not in ERA_MAP:
126 raise EraConversionError(f"不明な元号: {era_name}")
128 abbrev, start_year, start_month, start_day, end_year, end_month, end_day = ERA_MAP[era_name]
130 # 元号元年は1年として扱う
131 if era_year < 1:
132 raise EraConversionError(f"{era_name}の年は1以上である必要があります: {era_year}")
134 # 西暦年を計算
135 western_year = start_year + era_year - 1
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
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 )
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 )
158 return result_date
161def format_japanese_date(target_date: date, format_type: str = "long") -> str:
162 """西暦dateを元号形式の文字列に変換
164 Args:
165 target_date: 西暦のdateオブジェクト
166 format_type: 出力形式
167 - "long": 令和5年10月3日
168 - "short": R5.10.3
169 - "slash": R5/10/3
171 Returns:
172 str: 元号形式の日付文字列
174 Raises:
175 EraConversionError: 明治以前の日付の場合
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)
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)
199 raise EraConversionError(f"明治以前の日付は変換できません: {target_date}")
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}")
214def get_era_name(target_date: date) -> str:
215 """指定された日付の元号名を取得
217 Args:
218 target_date: 西暦のdateオブジェクト
220 Returns:
221 str: 元号名(例: "令和")
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)
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
237 raise EraConversionError(f"明治以前の日付は対応していません: {target_date}")