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]]

日付範囲変換キーワード用辞書

keyword: list[str]
range: Callable[[datetime.datetime], list[datetime.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())で初期化
  • 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']
DelimiterStyle: TypeAlias = Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None]
dt: datetime.datetime
243    @property
244    def dt(self) -> datetime:
245        """datetime型を返すプロパティ"""
246        return self._dt

datetime型を返すプロパティ

def set( self, value: Union[str, float, datetime.datetime, ExtendedDatetime]) -> None:
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: 変換文字列

def range(self, value: str | list) -> ExtendedDatetimeList:
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']
DelimiterStyle: TypeAlias = Literal['slash', '/', 'hyphen', '-', 'ja', 'number', 'num', None]
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]: 生成した辞書