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