1"""BYMONTH value type of RECUR from :rfc:`5545` and :rfc:`7529`."""
2
3from typing import Any
4
5from icalendar.compatibility import Self
6from icalendar.error import JCalParsingError
7from icalendar.parser import Parameters
8
9
10class vMonth(int):
11 """The number of the month for recurrence.
12
13 In :rfc:`5545`, this is just an int.
14 In :rfc:`7529`, this can be followed by `L` to indicate a leap month.
15
16 .. code-block:: pycon
17
18 >>> from icalendar import vMonth
19 >>> vMonth(1) # first month January
20 vMonth('1')
21 >>> vMonth("5L") # leap month in Hebrew calendar
22 vMonth('5L')
23 >>> vMonth(1).leap
24 False
25 >>> vMonth("5L").leap
26 True
27
28 Definition from RFC:
29
30 .. code-block:: text
31
32 type-bymonth = element bymonth {
33 xsd:positiveInteger |
34 xsd:string
35 }
36 """
37
38 params: Parameters
39
40 def __new__(cls, month: str | int, /, params: dict[str, Any] | None = None):
41 if isinstance(month, vMonth):
42 return cls(month.to_ical().decode())
43 if isinstance(month, str):
44 # ``str.isdigit`` is True for non-ASCII digits (e.g. "١٢") that
45 # ``int`` then either accepts as a different value or rejects, so
46 # gate on ASCII digits to match what ``int`` parses below.
47 if month.isascii() and month.isdigit():
48 month_index = int(month)
49 leap = False
50 else:
51 digits = month[:-1]
52 if (
53 not month
54 or month[-1] != "L"
55 or not (digits.isascii() and digits.isdigit())
56 ):
57 raise ValueError(f"Invalid month: {month!r}")
58 month_index = int(digits)
59 leap = True
60 else:
61 leap = False
62 month_index = int(month)
63 self = super().__new__(cls, month_index)
64 self.leap = leap
65 self.params = Parameters(params)
66 return self
67
68 def to_ical(self) -> bytes:
69 """The ical representation."""
70 return str(self).encode("utf-8")
71
72 @classmethod
73 def from_ical(cls, ical: str):
74 return cls(ical)
75
76 @property
77 def leap(self) -> bool:
78 """Whether this is a leap month."""
79 return self._leap
80
81 @leap.setter
82 def leap(self, value: bool) -> None:
83 self._leap = value
84
85 def __repr__(self) -> str:
86 """repr(self)"""
87 return f"{self.__class__.__name__}({str(self)!r})"
88
89 def __str__(self) -> str:
90 """str(self)"""
91 return f"{int(self)}{'L' if self.leap else ''}"
92
93 @classmethod
94 def parse_jcal_value(cls, value: Any) -> Self:
95 """Parse a jCal value for vMonth.
96
97 Raises:
98 ~error.JCalParsingError: If the value is not a valid month.
99 """
100 JCalParsingError.validate_value_type(value, (str, int), cls)
101 try:
102 return cls(value)
103 except ValueError as e:
104 raise JCalParsingError(
105 "The value must be a string or an integer.", cls, value=value
106 ) from e
107
108
109__all__ = ["vMonth"]