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