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())で初期化
  • 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
235    @property
236    def dt(self) -> datetime:
237        """datetime型を返すプロパティ"""
238        return self._dt

datetime型を返すプロパティ

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

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