cls.timekit
timekit - datetime 拡張ユーティリティ
ExtendedDatetime
: 柔軟な初期化と書式変換ができる datetime 拡張クラスExtendedDatetimeList
: ExtendedDatetimeを要素とする日付リストを扱う補助クラス
Examples:
>>> from cls.timekit import ExtendedDatetime >>> t = ExtendedDatetime("2025-04-19 12:34:56") >>> t.format("ymdhm") '2025/04/19 12:34'
>>> t.set("2025-05-01 00:00:00") >>> t.format("sql") '2025-05-01 00:00:00.000000'
>>> from dateutil.relativedelta import relativedelta >>> t2 = t + relativedelta(days=93) >>> t2.format("ymd") '2025/08/02'
>>> t + {"days": 1, "months": 2} 2025-07-02 00:00:00.000000
>>> ExtendedDatetime.range("今月").format("ymdhm") ['2025/04/01 00:00', '2025/04/30 23:59']
>>> ExtendedDatetime.range("今月").dict_format("ymd", "ja") {'start': '2025年04月01日', 'end': '2025年04月30日'}
>>> ExtendedDatetime("2025-01-01 01:23:45", hours=-12).range("今年") [2024-01-01 00:00:00.000000, 2024-12-31 23:59:59.999999]
1""" 2timekit - datetime 拡張ユーティリティ 3 4- `ExtendedDatetime`: 柔軟な初期化と書式変換ができる datetime 拡張クラス 5- `ExtendedDatetimeList`: ExtendedDatetimeを要素とする日付リストを扱う補助クラス 6 7Examples: 8 >>> from cls.timekit import ExtendedDatetime 9 >>> t = ExtendedDatetime("2025-04-19 12:34:56") 10 >>> t.format("ymdhm") 11 '2025/04/19 12:34' 12 13 >>> t.set("2025-05-01 00:00:00") 14 >>> t.format("sql") 15 '2025-05-01 00:00:00.000000' 16 17 >>> from dateutil.relativedelta import relativedelta 18 >>> t2 = t + relativedelta(days=93) 19 >>> t2.format("ymd") 20 '2025/08/02' 21 22 >>> t + {"days": 1, "months": 2} 23 2025-07-02 00:00:00.000000 24 25 >>> ExtendedDatetime.range("今月").format("ymdhm") 26 ['2025/04/01 00:00', '2025/04/30 23:59'] 27 28 >>> ExtendedDatetime.range("今月").dict_format("ymd", "ja") 29 {'start': '2025年04月01日', 'end': '2025年04月30日'} 30 31 >>> ExtendedDatetime("2025-01-01 01:23:45", hours=-12).range("今年") 32 [2024-01-01 00:00:00.000000, 2024-12-31 23:59:59.999999] 33""" 34 35from datetime import datetime 36from functools import total_ordering 37from typing import List, Literal, TypeAlias, Union, cast 38 39from dateutil.relativedelta import relativedelta 40 41from cls.types import DateRangeSpec 42from libs.data import lookup 43 44FormatType: TypeAlias = Literal[ 45 "ts", "y", "ym", "ymd", "ymdhm", "ymdhms", "hm", "hms", "sql", "ext", 46] 47"""フォーマット変換で指定する種類 48- **ts**: タイムスタンプ 49- **y**: %Y 50- **ym**: %Y/%m 51- **ymd**: %Y/%m/%d 52- **ymdhm**: %Y/%m/%d %H:%M 53- **ymdhms**: %Y/%m/%d %H:%M:%S 54- **hm**: %H:%M:%S 55- **hms**: %H:%M 56- **sql**: SQLite用フォーマット(%Y-%m-%d %H:%M:%S.%f) 57- **ext**: ファイル拡張子用(%Y%m%d-%H%M%S) 58""" 59 60DelimiterStyle: TypeAlias = Literal[ 61 "slash", "/", "hyphen", "-", "ja", "number", "num", None 62] 63"""区切り記号 64- **slash** | **/**: スラッシュ(ex: %Y/%m/%d) 65- **hyphen** | **-**: ハイフン(ex: %Y-%m-%d) 66- **number** | **num**: 無し (ex: %Y%m%d) 67- **ja**: Japanese Style (ex: %Y%年m%月d日) 68- **None**: 未指定 69""" 70 71DATE_RANGE_MAP: dict[str, DateRangeSpec] = { 72 "today": { 73 "keyword": ["今日", "本日", "当日"], 74 "range": lambda x: [ 75 x.replace(hour=0, minute=0, second=0, microsecond=0), 76 x.replace(hour=23, minute=59, second=59, microsecond=999999), 77 ], 78 }, 79 "yesterday": { 80 "keyword": ["昨日"], 81 "range": lambda _: [ 82 datetime.now() + relativedelta(days=-1, hour=0, minute=0, second=0, microsecond=0), 83 datetime.now() + relativedelta(days=-1, hour=23, minute=59, second=59, microsecond=999999), 84 ], 85 }, 86 "this_month": { 87 "keyword": ["今月"], 88 "range": lambda x: [ 89 x + relativedelta(day=1, hour=0, minute=0, second=0, microsecond=0), 90 x + relativedelta(day=31, hour=23, minute=59, second=59, microsecond=999999), 91 ], 92 }, 93 "last_month": { 94 "keyword": ["先月", "昨月"], 95 "range": lambda x: [ 96 x + relativedelta(months=-1, day=1, hour=0, minute=0, second=0, microsecond=0), 97 x + relativedelta(months=-1, day=31, hour=23, minute=59, second=59, microsecond=999999), 98 ], 99 }, 100 "two_months_ago": { 101 "keyword": ["先々月"], 102 "range": lambda x: [ 103 x + relativedelta(months=-2, day=1, hour=0, minute=0, second=0, microsecond=0), 104 x + relativedelta(months=-2, day=31, hour=23, minute=59, second=59, microsecond=999999), 105 ], 106 }, 107 "this_year": { 108 "keyword": ["今年"], 109 "range": lambda x: [ 110 x + relativedelta(month=1, day=1, hour=0, minute=0, second=0, microsecond=0), 111 x + relativedelta(month=12, day=31, hour=23, minute=59, second=59, microsecond=999999), 112 ], 113 }, 114 "last_year": { 115 "keyword": ["去年", "昨年"], 116 "range": lambda x: [ 117 x + relativedelta(years=-1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0), 118 x + relativedelta(years=-1, month=12, day=31, hour=23, minute=59, second=59, microsecond=999999), 119 ], 120 }, 121 "year_before_last": { 122 "keyword": ["一昨年"], 123 "range": lambda x: [ 124 x + relativedelta(years=-2, month=1, day=1, hour=0, minute=0, second=0, microsecond=0), 125 x + relativedelta(years=-2, month=12, day=31, hour=23, minute=59, second=59, microsecond=999999), 126 ], 127 }, 128 "first_day": { 129 "keyword": ["最初"], 130 "range": lambda _: [ 131 lookup.db.first_record() + relativedelta(days=-1), 132 ], 133 }, 134 "last_day": { 135 "keyword": ["最後"], 136 "range": lambda _: [ 137 datetime.now() + relativedelta(days=1, hour=23, minute=59, second=59, microsecond=999999) 138 ], 139 }, 140 "all": { 141 "keyword": ["全部"], 142 "range": lambda _: [ 143 lookup.db.first_record() + relativedelta(days=-1), 144 datetime.now() + relativedelta(days=1, hour=23, minute=59, second=59, microsecond=999999) 145 ], 146 }, 147} 148"""キーワードと日付範囲のマッピングリスト""" 149 150 151@total_ordering 152class ExtendedDatetime: 153 """datetime拡張クラス""" 154 155 _dt: datetime 156 """操作対象""" 157 158 # 型アノテーション用定数 159 AcceptedType: TypeAlias = Union[str, float, datetime, "ExtendedDatetime"] 160 """引数として受け付ける型 161 - **str**: 日付文字列(ISO形式など) 162 - **float**: UNIXタイムスタンプ 163 - **datetime** / **ExtendedDatetime**: オブジェクトをそのまま利用 164 """ 165 166 FormatType: TypeAlias = FormatType 167 DelimiterStyle: TypeAlias = DelimiterStyle 168 169 def __init__(self, value: AcceptedType | None = None, **relativedelta_kwargs): 170 """ExtendedDatetimeの初期化 171 172 Args: 173 value (AcceptedType | None, optional): 引数 174 - None: 現在時刻(`datetime.now()`)で初期化 175 relativedelta_kwargs (dict): 初期化時にrelativedelta()に渡す引数 176 """ 177 178 self._dt = self.convert(value) if value else datetime.now() 179 if relativedelta_kwargs: 180 self._dt += relativedelta(**relativedelta_kwargs) 181 182 def __str__(self) -> str: 183 return self.format("sql") 184 185 def __repr__(self) -> str: 186 return self.format("sql") 187 188 def __eq__(self, other): 189 if isinstance(other, ExtendedDatetime): 190 return self.dt == other.dt 191 if isinstance(other, datetime): 192 return self.dt == other 193 return NotImplemented 194 195 def __lt__(self, other): 196 if isinstance(other, ExtendedDatetime): 197 return self.dt < other.dt 198 if isinstance(other, datetime): 199 return self.dt < other 200 return NotImplemented 201 202 def __add__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 203 if isinstance(other, dict): 204 delta = relativedelta(**other) 205 elif isinstance(other, relativedelta): 206 delta = other 207 else: 208 raise TypeError("Expected dict or relativedelta") 209 210 return ExtendedDatetime(self._dt + delta) 211 212 def __sub__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 213 if isinstance(other, dict): 214 delta = relativedelta(**other) 215 elif isinstance(other, relativedelta): 216 delta = other 217 else: 218 raise TypeError("Expected dict or relativedelta") 219 220 return ExtendedDatetime(self._dt - delta) 221 222 def __radd__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 223 return self.__add__(other) 224 225 def __rsub__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 226 return self.__sub__(other) 227 228 def __hash__(self): 229 return hash(self.dt) 230 231 def __getattr__(self, name): 232 return getattr(self._dt, name) 233 234 @property 235 def dt(self) -> datetime: 236 """datetime型を返すプロパティ""" 237 return self._dt 238 239 def set(self, value: AcceptedType) -> None: 240 """渡された値をdatetime型に変換して保持 241 242 Args: 243 value (AcceptedType): 入力値 244 """ 245 246 self._dt = self.convert(value) 247 248 def format(self, fmt: FormatType, delimiter: DelimiterStyle = None) -> str: 249 """フォーマット変換 250 251 Args: 252 fmt (FormatType): 変換形式 253 delimiter (DelimiterStyle): 区切り 254 255 Raises: 256 ValueError: 受け付けない変換形式 257 258 Returns: 259 str: 変換文字列 260 """ 261 262 ret: str 263 match fmt: 264 case "ts": 265 ret = str(self._dt.timestamp()) 266 case "y": 267 match delimiter: 268 case "ja": 269 ret = self._dt.strftime("%Y年") 270 case _: 271 ret = self._dt.strftime("%Y") 272 case "ym": 273 match delimiter: 274 case "slash" | "/": 275 ret = self._dt.strftime("%Y/%m") 276 case "hyphen" | "-": 277 ret = self._dt.strftime("%Y-%m") 278 case "ja": 279 ret = self._dt.strftime("%Y年%m月") 280 case "number" | "num": 281 ret = self._dt.strftime("%Y%m") 282 case _: 283 ret = self._dt.strftime("%Y/%m") 284 case "ymd": 285 match delimiter: 286 case "slash" | "/": 287 ret = self._dt.strftime("%Y/%m/%d") 288 case "hyphen" | "-": 289 ret = self._dt.strftime("%Y-%m-%d") 290 case "ja": 291 ret = self._dt.strftime("%Y年%m月%d日") 292 case "number" | "num": 293 ret = self._dt.strftime("%Y%m%d") 294 case _: 295 ret = self._dt.strftime("%Y/%m/%d") 296 case "ymdhm": 297 match delimiter: 298 case "slash" | "/": 299 ret = self._dt.strftime("%Y/%m/%d %H:%M") 300 case "hyphen" | "-": 301 ret = self._dt.strftime("%Y-%m-%d %H:%M") 302 case "ja": 303 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分") 304 case "number" | "num": 305 ret = self._dt.strftime("%Y%m%d%H%M") 306 case _: 307 ret = self._dt.strftime("%Y/%m/%d %H:%M") 308 case "ymdhms": 309 match delimiter: 310 case "slash" | "/": 311 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 312 case "hyphen" | "-": 313 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S") 314 case "ja": 315 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分%S秒") 316 case "number" | "num": 317 ret = self._dt.strftime("%Y%m%d%H%M%S") 318 case _: 319 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 320 case "sql": 321 match delimiter: 322 case "slash" | "/": 323 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S.%f") 324 case "hyphen" | "-": 325 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 326 case "number" | "num": 327 ret = self._dt.strftime("%Y%m%d%H%M%S%f") 328 case _: 329 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 330 case "ext": 331 ret = self._dt.strftime("%Y%m%d-%H%M%S") 332 case _: 333 raise ValueError(f"Unknown format: {fmt}") 334 335 return ret 336 337 def range(self, value: str | list) -> "ExtendedDatetimeList": 338 """キーワードが示す範囲をリストで返す 339 340 Args: 341 value (str | list): 範囲取得キーワード 342 - str: スペース区切りで分割してリスト化 343 - list: スペース区切りで再分割 344 345 Returns: 346 ExtendedDatetimeList: 日付リスト 347 """ 348 349 if isinstance(value, str): 350 check_list = value.split() 351 else: 352 check_list = sum([str(x).split() for x in value], []) # 平坦化 353 354 ret: list[datetime] = [] 355 for word in check_list: 356 for _, range_map in DATE_RANGE_MAP.items(): 357 if word in cast(list, range_map["keyword"]): 358 ret.extend(range_map["range"](self._dt)) 359 break 360 else: 361 try: 362 try_time = self.convert(str(word)) 363 ret.append(try_time.replace(hour=0, minute=0, second=0, microsecond=0)) 364 ret.append(try_time.replace(hour=23, minute=59, second=59, microsecond=999999)) 365 except ValueError: 366 pass 367 368 continue 369 370 return ExtendedDatetimeList([ExtendedDatetime(x) for x in ret]) 371 372 @classmethod 373 def valid_keywords(cls) -> list[str]: 374 """有効なキーワード一覧 375 376 Returns: 377 list[str]: キーワード一覧 378 """ 379 380 ret: list = [] 381 for _, range_map in DATE_RANGE_MAP.items(): 382 ret.extend(cast(list, range_map["keyword"])) 383 384 return ret 385 386 @classmethod 387 def print_range(cls) -> str: 388 """指定可能キーワードで取得できる範囲の一覧 389 390 Returns: 391 str: 出力メッセージ 392 """ 393 394 base_instance = cls() 395 ret: str = "" 396 397 for _, val in DATE_RANGE_MAP.items(): 398 for label in val["keyword"]: 399 scope = " ~ ".join(base_instance.range(label).format("ymd")) 400 ret += f"{label}: {scope}\n" 401 402 return ret.strip() 403 404 @staticmethod 405 def convert(value: AcceptedType) -> datetime: 406 """引数の型を判定してdatetimeへ変換 407 408 Args: 409 value (AcceptedType): 変換対象 410 411 Raises: 412 TypeError: str型が変換できない場合 413 414 Returns: 415 datetime: 変換した型 416 """ 417 418 if isinstance(value, ExtendedDatetime): 419 return value.dt 420 if isinstance(value, datetime): 421 return value 422 if isinstance(value, float): 423 return datetime.fromtimestamp(value) 424 if isinstance(value, str): 425 try: 426 return datetime.fromisoformat(value) 427 except ValueError: 428 return datetime.strptime(value, "%Y/%m/%d %H:%M") 429 430 raise TypeError("Unsupported type for datetime conversion") 431 432 433class ExtendedDatetimeList(list): 434 """ExtendedDatetimeを要素とする日付リストを扱う補助クラス""" 435 436 FormatType: TypeAlias = FormatType 437 DelimiterStyle: TypeAlias = DelimiterStyle 438 439 def __add__(self, other): 440 if isinstance(other, dict): 441 return ExtendedDatetimeList([dt + other for dt in self]) 442 return NotImplemented 443 444 def __sub__(self, other): 445 if isinstance(other, dict): 446 return ExtendedDatetimeList([dt - other for dt in self]) 447 return NotImplemented 448 449 @property 450 def start(self) -> ExtendedDatetime | None: 451 """最小日付を返す。空ならNone。""" 452 return (min(self) if self else None) 453 454 @property 455 def end(self) -> ExtendedDatetime | None: 456 """最大日付を返す。空ならNone。""" 457 return (max(self) if self else None) 458 459 @property 460 def period(self) -> List[ExtendedDatetime | None]: 461 """最小値と最大値をリストで返す""" 462 min_dt = min(self) if self else None 463 max_dt = max(self) if self else None 464 465 return [min_dt, max_dt] 466 467 def format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> list[str]: 468 """全要素にformatを適用した文字列リストを返す 469 470 Args: 471 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 472 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 473 474 Returns: 475 list[str]: 生成したリスト 476 """ 477 478 return [dt.format(fmt, delimiter) for dt in self if isinstance(dt, ExtendedDatetime)] 479 480 def dict_format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> dict[str, str]: 481 """全要素にformatを適用し、最小日付と最大日付を辞書で返す 482 483 Args: 484 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 485 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 486 487 Returns: 488 dict[str, str]: 生成した辞書 489 """ 490 491 date_range = [dt for dt in self if isinstance(dt, ExtendedDatetime)] 492 493 if not date_range: 494 return {} 495 496 return ({"start": min(date_range).format(fmt, delimiter), "end": max(date_range).format(fmt, delimiter)})
FormatType: TypeAlias =
Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext']
フォーマット変換で指定する種類
- ts: タイムスタンプ
- y: %Y
- ym: %Y/%m
- ymd: %Y/%m/%d
- ymdhm: %Y/%m/%d %H:%M
- ymdhms: %Y/%m/%d %H:%M:%S
- hm: %H:%M:%S
- hms: %H:%M
- sql: SQLite用フォーマット(%Y-%m-%d %H:%M:%S.%f)
- ext: ファイル拡張子用(%Y%m%d-%H%M%S)
DelimiterStyle: TypeAlias =
Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None]
区切り記号
- slash | /: スラッシュ(ex: %Y/%m/%d)
- hyphen | -: ハイフン(ex: %Y-%m-%d)
- number | num: 無し (ex: %Y%m%d)
- ja: Japanese Style (ex: %Y%年m%月d日)
- None: 未指定
DATE_RANGE_MAP: dict[str, cls.types.DateRangeSpec] =
{'today': {'keyword': ['今日', '本日', '当日'], 'range': <function <lambda>>}, 'yesterday': {'keyword': ['昨日'], 'range': <function <lambda>>}, 'this_month': {'keyword': ['今月'], 'range': <function <lambda>>}, 'last_month': {'keyword': ['先月', '昨月'], 'range': <function <lambda>>}, 'two_months_ago': {'keyword': ['先々月'], 'range': <function <lambda>>}, 'this_year': {'keyword': ['今年'], 'range': <function <lambda>>}, 'last_year': {'keyword': ['去年', '昨年'], 'range': <function <lambda>>}, 'year_before_last': {'keyword': ['一昨年'], 'range': <function <lambda>>}, 'first_day': {'keyword': ['最初'], 'range': <function <lambda>>}, 'last_day': {'keyword': ['最後'], 'range': <function <lambda>>}, 'all': {'keyword': ['全部'], 'range': <function <lambda>>}}
キーワードと日付範囲のマッピングリスト
@total_ordering
class
ExtendedDatetime:
152@total_ordering 153class ExtendedDatetime: 154 """datetime拡張クラス""" 155 156 _dt: datetime 157 """操作対象""" 158 159 # 型アノテーション用定数 160 AcceptedType: TypeAlias = Union[str, float, datetime, "ExtendedDatetime"] 161 """引数として受け付ける型 162 - **str**: 日付文字列(ISO形式など) 163 - **float**: UNIXタイムスタンプ 164 - **datetime** / **ExtendedDatetime**: オブジェクトをそのまま利用 165 """ 166 167 FormatType: TypeAlias = FormatType 168 DelimiterStyle: TypeAlias = DelimiterStyle 169 170 def __init__(self, value: AcceptedType | None = None, **relativedelta_kwargs): 171 """ExtendedDatetimeの初期化 172 173 Args: 174 value (AcceptedType | None, optional): 引数 175 - None: 現在時刻(`datetime.now()`)で初期化 176 relativedelta_kwargs (dict): 初期化時にrelativedelta()に渡す引数 177 """ 178 179 self._dt = self.convert(value) if value else datetime.now() 180 if relativedelta_kwargs: 181 self._dt += relativedelta(**relativedelta_kwargs) 182 183 def __str__(self) -> str: 184 return self.format("sql") 185 186 def __repr__(self) -> str: 187 return self.format("sql") 188 189 def __eq__(self, other): 190 if isinstance(other, ExtendedDatetime): 191 return self.dt == other.dt 192 if isinstance(other, datetime): 193 return self.dt == other 194 return NotImplemented 195 196 def __lt__(self, other): 197 if isinstance(other, ExtendedDatetime): 198 return self.dt < other.dt 199 if isinstance(other, datetime): 200 return self.dt < other 201 return NotImplemented 202 203 def __add__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 204 if isinstance(other, dict): 205 delta = relativedelta(**other) 206 elif isinstance(other, relativedelta): 207 delta = other 208 else: 209 raise TypeError("Expected dict or relativedelta") 210 211 return ExtendedDatetime(self._dt + delta) 212 213 def __sub__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 214 if isinstance(other, dict): 215 delta = relativedelta(**other) 216 elif isinstance(other, relativedelta): 217 delta = other 218 else: 219 raise TypeError("Expected dict or relativedelta") 220 221 return ExtendedDatetime(self._dt - delta) 222 223 def __radd__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 224 return self.__add__(other) 225 226 def __rsub__(self, other: Union[relativedelta, dict]) -> "ExtendedDatetime": 227 return self.__sub__(other) 228 229 def __hash__(self): 230 return hash(self.dt) 231 232 def __getattr__(self, name): 233 return getattr(self._dt, name) 234 235 @property 236 def dt(self) -> datetime: 237 """datetime型を返すプロパティ""" 238 return self._dt 239 240 def set(self, value: AcceptedType) -> None: 241 """渡された値をdatetime型に変換して保持 242 243 Args: 244 value (AcceptedType): 入力値 245 """ 246 247 self._dt = self.convert(value) 248 249 def format(self, fmt: FormatType, delimiter: DelimiterStyle = None) -> str: 250 """フォーマット変換 251 252 Args: 253 fmt (FormatType): 変換形式 254 delimiter (DelimiterStyle): 区切り 255 256 Raises: 257 ValueError: 受け付けない変換形式 258 259 Returns: 260 str: 変換文字列 261 """ 262 263 ret: str 264 match fmt: 265 case "ts": 266 ret = str(self._dt.timestamp()) 267 case "y": 268 match delimiter: 269 case "ja": 270 ret = self._dt.strftime("%Y年") 271 case _: 272 ret = self._dt.strftime("%Y") 273 case "ym": 274 match delimiter: 275 case "slash" | "/": 276 ret = self._dt.strftime("%Y/%m") 277 case "hyphen" | "-": 278 ret = self._dt.strftime("%Y-%m") 279 case "ja": 280 ret = self._dt.strftime("%Y年%m月") 281 case "number" | "num": 282 ret = self._dt.strftime("%Y%m") 283 case _: 284 ret = self._dt.strftime("%Y/%m") 285 case "ymd": 286 match delimiter: 287 case "slash" | "/": 288 ret = self._dt.strftime("%Y/%m/%d") 289 case "hyphen" | "-": 290 ret = self._dt.strftime("%Y-%m-%d") 291 case "ja": 292 ret = self._dt.strftime("%Y年%m月%d日") 293 case "number" | "num": 294 ret = self._dt.strftime("%Y%m%d") 295 case _: 296 ret = self._dt.strftime("%Y/%m/%d") 297 case "ymdhm": 298 match delimiter: 299 case "slash" | "/": 300 ret = self._dt.strftime("%Y/%m/%d %H:%M") 301 case "hyphen" | "-": 302 ret = self._dt.strftime("%Y-%m-%d %H:%M") 303 case "ja": 304 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分") 305 case "number" | "num": 306 ret = self._dt.strftime("%Y%m%d%H%M") 307 case _: 308 ret = self._dt.strftime("%Y/%m/%d %H:%M") 309 case "ymdhms": 310 match delimiter: 311 case "slash" | "/": 312 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 313 case "hyphen" | "-": 314 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S") 315 case "ja": 316 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分%S秒") 317 case "number" | "num": 318 ret = self._dt.strftime("%Y%m%d%H%M%S") 319 case _: 320 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 321 case "sql": 322 match delimiter: 323 case "slash" | "/": 324 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S.%f") 325 case "hyphen" | "-": 326 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 327 case "number" | "num": 328 ret = self._dt.strftime("%Y%m%d%H%M%S%f") 329 case _: 330 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 331 case "ext": 332 ret = self._dt.strftime("%Y%m%d-%H%M%S") 333 case _: 334 raise ValueError(f"Unknown format: {fmt}") 335 336 return ret 337 338 def range(self, value: str | list) -> "ExtendedDatetimeList": 339 """キーワードが示す範囲をリストで返す 340 341 Args: 342 value (str | list): 範囲取得キーワード 343 - str: スペース区切りで分割してリスト化 344 - list: スペース区切りで再分割 345 346 Returns: 347 ExtendedDatetimeList: 日付リスト 348 """ 349 350 if isinstance(value, str): 351 check_list = value.split() 352 else: 353 check_list = sum([str(x).split() for x in value], []) # 平坦化 354 355 ret: list[datetime] = [] 356 for word in check_list: 357 for _, range_map in DATE_RANGE_MAP.items(): 358 if word in cast(list, range_map["keyword"]): 359 ret.extend(range_map["range"](self._dt)) 360 break 361 else: 362 try: 363 try_time = self.convert(str(word)) 364 ret.append(try_time.replace(hour=0, minute=0, second=0, microsecond=0)) 365 ret.append(try_time.replace(hour=23, minute=59, second=59, microsecond=999999)) 366 except ValueError: 367 pass 368 369 continue 370 371 return ExtendedDatetimeList([ExtendedDatetime(x) for x in ret]) 372 373 @classmethod 374 def valid_keywords(cls) -> list[str]: 375 """有効なキーワード一覧 376 377 Returns: 378 list[str]: キーワード一覧 379 """ 380 381 ret: list = [] 382 for _, range_map in DATE_RANGE_MAP.items(): 383 ret.extend(cast(list, range_map["keyword"])) 384 385 return ret 386 387 @classmethod 388 def print_range(cls) -> str: 389 """指定可能キーワードで取得できる範囲の一覧 390 391 Returns: 392 str: 出力メッセージ 393 """ 394 395 base_instance = cls() 396 ret: str = "" 397 398 for _, val in DATE_RANGE_MAP.items(): 399 for label in val["keyword"]: 400 scope = " ~ ".join(base_instance.range(label).format("ymd")) 401 ret += f"{label}: {scope}\n" 402 403 return ret.strip() 404 405 @staticmethod 406 def convert(value: AcceptedType) -> datetime: 407 """引数の型を判定してdatetimeへ変換 408 409 Args: 410 value (AcceptedType): 変換対象 411 412 Raises: 413 TypeError: str型が変換できない場合 414 415 Returns: 416 datetime: 変換した型 417 """ 418 419 if isinstance(value, ExtendedDatetime): 420 return value.dt 421 if isinstance(value, datetime): 422 return value 423 if isinstance(value, float): 424 return datetime.fromtimestamp(value) 425 if isinstance(value, str): 426 try: 427 return datetime.fromisoformat(value) 428 except ValueError: 429 return datetime.strptime(value, "%Y/%m/%d %H:%M") 430 431 raise TypeError("Unsupported type for datetime conversion")
datetime拡張クラス
ExtendedDatetime( value: Union[str, float, datetime.datetime, ExtendedDatetime, NoneType] = None, **relativedelta_kwargs)
170 def __init__(self, value: AcceptedType | None = None, **relativedelta_kwargs): 171 """ExtendedDatetimeの初期化 172 173 Args: 174 value (AcceptedType | None, optional): 引数 175 - None: 現在時刻(`datetime.now()`)で初期化 176 relativedelta_kwargs (dict): 初期化時にrelativedelta()に渡す引数 177 """ 178 179 self._dt = self.convert(value) if value else datetime.now() 180 if relativedelta_kwargs: 181 self._dt += relativedelta(**relativedelta_kwargs)
ExtendedDatetimeの初期化
Arguments:
- value (AcceptedType | None, optional): 引数
- None: 現在時刻(
datetime.now()
)で初期化
- None: 現在時刻(
- relativedelta_kwargs (dict): 初期化時にrelativedelta()に渡す引数
AcceptedType: TypeAlias =
Union[str, float, datetime.datetime, ForwardRef('ExtendedDatetime')]
引数として受け付ける型
- str: 日付文字列(ISO形式など)
- float: UNIXタイムスタンプ
- datetime / ExtendedDatetime: オブジェクトをそのまま利用
FormatType: TypeAlias =
Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext']
240 def set(self, value: AcceptedType) -> None: 241 """渡された値をdatetime型に変換して保持 242 243 Args: 244 value (AcceptedType): 入力値 245 """ 246 247 self._dt = self.convert(value)
渡された値をdatetime型に変換して保持
Arguments:
- value (AcceptedType): 入力値
def
format( self, fmt: Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext'], delimiter: Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None] = None) -> str:
249 def format(self, fmt: FormatType, delimiter: DelimiterStyle = None) -> str: 250 """フォーマット変換 251 252 Args: 253 fmt (FormatType): 変換形式 254 delimiter (DelimiterStyle): 区切り 255 256 Raises: 257 ValueError: 受け付けない変換形式 258 259 Returns: 260 str: 変換文字列 261 """ 262 263 ret: str 264 match fmt: 265 case "ts": 266 ret = str(self._dt.timestamp()) 267 case "y": 268 match delimiter: 269 case "ja": 270 ret = self._dt.strftime("%Y年") 271 case _: 272 ret = self._dt.strftime("%Y") 273 case "ym": 274 match delimiter: 275 case "slash" | "/": 276 ret = self._dt.strftime("%Y/%m") 277 case "hyphen" | "-": 278 ret = self._dt.strftime("%Y-%m") 279 case "ja": 280 ret = self._dt.strftime("%Y年%m月") 281 case "number" | "num": 282 ret = self._dt.strftime("%Y%m") 283 case _: 284 ret = self._dt.strftime("%Y/%m") 285 case "ymd": 286 match delimiter: 287 case "slash" | "/": 288 ret = self._dt.strftime("%Y/%m/%d") 289 case "hyphen" | "-": 290 ret = self._dt.strftime("%Y-%m-%d") 291 case "ja": 292 ret = self._dt.strftime("%Y年%m月%d日") 293 case "number" | "num": 294 ret = self._dt.strftime("%Y%m%d") 295 case _: 296 ret = self._dt.strftime("%Y/%m/%d") 297 case "ymdhm": 298 match delimiter: 299 case "slash" | "/": 300 ret = self._dt.strftime("%Y/%m/%d %H:%M") 301 case "hyphen" | "-": 302 ret = self._dt.strftime("%Y-%m-%d %H:%M") 303 case "ja": 304 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分") 305 case "number" | "num": 306 ret = self._dt.strftime("%Y%m%d%H%M") 307 case _: 308 ret = self._dt.strftime("%Y/%m/%d %H:%M") 309 case "ymdhms": 310 match delimiter: 311 case "slash" | "/": 312 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 313 case "hyphen" | "-": 314 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S") 315 case "ja": 316 ret = self._dt.strftime("%Y年%m月%d日 %H時%M分%S秒") 317 case "number" | "num": 318 ret = self._dt.strftime("%Y%m%d%H%M%S") 319 case _: 320 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S") 321 case "sql": 322 match delimiter: 323 case "slash" | "/": 324 ret = self._dt.strftime("%Y/%m/%d %H:%M:%S.%f") 325 case "hyphen" | "-": 326 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 327 case "number" | "num": 328 ret = self._dt.strftime("%Y%m%d%H%M%S%f") 329 case _: 330 ret = self._dt.strftime("%Y-%m-%d %H:%M:%S.%f") 331 case "ext": 332 ret = self._dt.strftime("%Y%m%d-%H%M%S") 333 case _: 334 raise ValueError(f"Unknown format: {fmt}") 335 336 return ret
フォーマット変換
Arguments:
- fmt (FormatType): 変換形式
- delimiter (DelimiterStyle): 区切り
Raises:
- ValueError: 受け付けない変換形式
Returns:
str: 変換文字列
338 def range(self, value: str | list) -> "ExtendedDatetimeList": 339 """キーワードが示す範囲をリストで返す 340 341 Args: 342 value (str | list): 範囲取得キーワード 343 - str: スペース区切りで分割してリスト化 344 - list: スペース区切りで再分割 345 346 Returns: 347 ExtendedDatetimeList: 日付リスト 348 """ 349 350 if isinstance(value, str): 351 check_list = value.split() 352 else: 353 check_list = sum([str(x).split() for x in value], []) # 平坦化 354 355 ret: list[datetime] = [] 356 for word in check_list: 357 for _, range_map in DATE_RANGE_MAP.items(): 358 if word in cast(list, range_map["keyword"]): 359 ret.extend(range_map["range"](self._dt)) 360 break 361 else: 362 try: 363 try_time = self.convert(str(word)) 364 ret.append(try_time.replace(hour=0, minute=0, second=0, microsecond=0)) 365 ret.append(try_time.replace(hour=23, minute=59, second=59, microsecond=999999)) 366 except ValueError: 367 pass 368 369 continue 370 371 return ExtendedDatetimeList([ExtendedDatetime(x) for x in ret])
キーワードが示す範囲をリストで返す
Arguments:
- value (str | list): 範囲取得キーワード
- str: スペース区切りで分割してリスト化
- list: スペース区切りで再分割
Returns:
ExtendedDatetimeList: 日付リスト
@classmethod
def
valid_keywords(cls) -> list[str]:
373 @classmethod 374 def valid_keywords(cls) -> list[str]: 375 """有効なキーワード一覧 376 377 Returns: 378 list[str]: キーワード一覧 379 """ 380 381 ret: list = [] 382 for _, range_map in DATE_RANGE_MAP.items(): 383 ret.extend(cast(list, range_map["keyword"])) 384 385 return ret
有効なキーワード一覧
Returns:
list[str]: キーワード一覧
@classmethod
def
print_range(cls) -> str:
387 @classmethod 388 def print_range(cls) -> str: 389 """指定可能キーワードで取得できる範囲の一覧 390 391 Returns: 392 str: 出力メッセージ 393 """ 394 395 base_instance = cls() 396 ret: str = "" 397 398 for _, val in DATE_RANGE_MAP.items(): 399 for label in val["keyword"]: 400 scope = " ~ ".join(base_instance.range(label).format("ymd")) 401 ret += f"{label}: {scope}\n" 402 403 return ret.strip()
指定可能キーワードで取得できる範囲の一覧
Returns:
str: 出力メッセージ
@staticmethod
def
convert( value: Union[str, float, datetime.datetime, ExtendedDatetime]) -> datetime.datetime:
405 @staticmethod 406 def convert(value: AcceptedType) -> datetime: 407 """引数の型を判定してdatetimeへ変換 408 409 Args: 410 value (AcceptedType): 変換対象 411 412 Raises: 413 TypeError: str型が変換できない場合 414 415 Returns: 416 datetime: 変換した型 417 """ 418 419 if isinstance(value, ExtendedDatetime): 420 return value.dt 421 if isinstance(value, datetime): 422 return value 423 if isinstance(value, float): 424 return datetime.fromtimestamp(value) 425 if isinstance(value, str): 426 try: 427 return datetime.fromisoformat(value) 428 except ValueError: 429 return datetime.strptime(value, "%Y/%m/%d %H:%M") 430 431 raise TypeError("Unsupported type for datetime conversion")
引数の型を判定してdatetimeへ変換
Arguments:
- value (AcceptedType): 変換対象
Raises:
- TypeError: str型が変換できない場合
Returns:
datetime: 変換した型
class
ExtendedDatetimeList(builtins.list):
434class ExtendedDatetimeList(list): 435 """ExtendedDatetimeを要素とする日付リストを扱う補助クラス""" 436 437 FormatType: TypeAlias = FormatType 438 DelimiterStyle: TypeAlias = DelimiterStyle 439 440 def __add__(self, other): 441 if isinstance(other, dict): 442 return ExtendedDatetimeList([dt + other for dt in self]) 443 return NotImplemented 444 445 def __sub__(self, other): 446 if isinstance(other, dict): 447 return ExtendedDatetimeList([dt - other for dt in self]) 448 return NotImplemented 449 450 @property 451 def start(self) -> ExtendedDatetime | None: 452 """最小日付を返す。空ならNone。""" 453 return (min(self) if self else None) 454 455 @property 456 def end(self) -> ExtendedDatetime | None: 457 """最大日付を返す。空ならNone。""" 458 return (max(self) if self else None) 459 460 @property 461 def period(self) -> List[ExtendedDatetime | None]: 462 """最小値と最大値をリストで返す""" 463 min_dt = min(self) if self else None 464 max_dt = max(self) if self else None 465 466 return [min_dt, max_dt] 467 468 def format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> list[str]: 469 """全要素にformatを適用した文字列リストを返す 470 471 Args: 472 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 473 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 474 475 Returns: 476 list[str]: 生成したリスト 477 """ 478 479 return [dt.format(fmt, delimiter) for dt in self if isinstance(dt, ExtendedDatetime)] 480 481 def dict_format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> dict[str, str]: 482 """全要素にformatを適用し、最小日付と最大日付を辞書で返す 483 484 Args: 485 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 486 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 487 488 Returns: 489 dict[str, str]: 生成した辞書 490 """ 491 492 date_range = [dt for dt in self if isinstance(dt, ExtendedDatetime)] 493 494 if not date_range: 495 return {} 496 497 return ({"start": min(date_range).format(fmt, delimiter), "end": max(date_range).format(fmt, delimiter)})
ExtendedDatetimeを要素とする日付リストを扱う補助クラス
FormatType: TypeAlias =
Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext']
start: ExtendedDatetime | None
450 @property 451 def start(self) -> ExtendedDatetime | None: 452 """最小日付を返す。空ならNone。""" 453 return (min(self) if self else None)
最小日付を返す。空ならNone。
end: ExtendedDatetime | None
455 @property 456 def end(self) -> ExtendedDatetime | None: 457 """最大日付を返す。空ならNone。""" 458 return (max(self) if self else None)
最大日付を返す。空ならNone。
period: List[ExtendedDatetime | None]
460 @property 461 def period(self) -> List[ExtendedDatetime | None]: 462 """最小値と最大値をリストで返す""" 463 min_dt = min(self) if self else None 464 max_dt = max(self) if self else None 465 466 return [min_dt, max_dt]
最小値と最大値をリストで返す
def
format( self, fmt: Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext'] = 'sql', delimiter: Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None] = None) -> list[str]:
468 def format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> list[str]: 469 """全要素にformatを適用した文字列リストを返す 470 471 Args: 472 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 473 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 474 475 Returns: 476 list[str]: 生成したリスト 477 """ 478 479 return [dt.format(fmt, delimiter) for dt in self if isinstance(dt, ExtendedDatetime)]
全要素にformatを適用した文字列リストを返す
Arguments:
- fmt (FormatType, optional): フォーマット変換. Defaults to "sql".
- delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None.
Returns:
list[str]: 生成したリスト
def
dict_format( self, fmt: Literal['ts', 'y', 'ym', 'ymd', 'ymdhm', 'ymdhms', 'hm', 'hms', 'sql', 'ext'] = 'sql', delimiter: Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None] = None) -> dict[str, str]:
481 def dict_format(self, fmt: FormatType = "sql", delimiter: DelimiterStyle = None) -> dict[str, str]: 482 """全要素にformatを適用し、最小日付と最大日付を辞書で返す 483 484 Args: 485 fmt (FormatType, optional): フォーマット変換. Defaults to "sql". 486 delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None. 487 488 Returns: 489 dict[str, str]: 生成した辞書 490 """ 491 492 date_range = [dt for dt in self if isinstance(dt, ExtendedDatetime)] 493 494 if not date_range: 495 return {} 496 497 return ({"start": min(date_range).format(fmt, delimiter), "end": max(date_range).format(fmt, delimiter)})
全要素にformatを適用し、最小日付と最大日付を辞書で返す
Arguments:
- fmt (FormatType, optional): フォーマット変換. Defaults to "sql".
- delimiter (DelimiterStyle, optional): 区切り記号指定. Defaults to None.
Returns:
dict[str, str]: 生成した辞書