1"""BYWEEKDAY, BYDAY, and WKST value type of RECUR from :rfc:`5545`."""
2
3from __future__ import annotations
4
5import re
6from typing import TYPE_CHECKING, Any
7from xml.etree.ElementTree import Element, SubElement
8
9from icalendar.caselessdict import CaselessDict
10from icalendar.error import JCalParsingError, XCalParsingError
11from icalendar.parser import Parameters
12from icalendar.parser.xcal.wrapper import from_xcal_wrapper
13from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode
14
15if TYPE_CHECKING:
16 from icalendar.compatibility import Self
17 from icalendar.parser.xcal.base import XCalParser
18
19# Use ``[0-9]`` to explicitly specify that only ASCII digits should be
20# matched, instead of ``\d``. In Python, ``\d`` matches a digit zero through
21# nine in any script except ideographic scripts, and is equivalent to
22# ``\p{Nd}``. For example, the Arabic-Indic "١٢" would match. Here, a
23# malformed ``ordwk`` would get parsed into a valid ``relative`` number.
24# RFC 5545, section 3.3.10, allows only ASCII characters per its definition
25# of ``DIGIT``.
26WEEKDAY_RULE = re.compile(
27 r"(?P<signal>[+-]?)(?P<relative>[0-9]{0,2})(?P<weekday>[\w]{2})\Z"
28)
29
30
31class vWeekday(str):
32 """Either a ``weekday`` or a ``weekdaynum``.
33
34 .. code-block:: pycon
35
36 >>> from icalendar import vWeekday
37 >>> vWeekday("MO") # Simple weekday
38 'MO'
39 >>> vWeekday("2FR").relative # Second friday
40 2
41 >>> vWeekday("2FR").weekday
42 'FR'
43 >>> vWeekday("-1SU").relative # Last Sunday
44 -1
45
46 Definition from :rfc:`5545#section-3.3.10`:
47
48 .. code-block:: text
49
50 weekdaynum = [[plus / minus] ordwk] weekday
51 plus = "+"
52 minus = "-"
53 ordwk = 1*2DIGIT ;1 to 53
54 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
55 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
56 ;FRIDAY, and SATURDAY days of the week.
57
58 """
59
60 params: Parameters
61 __slots__ = ("params", "relative", "weekday")
62
63 week_days = CaselessDict(
64 {
65 "SU": 0,
66 "MO": 1,
67 "TU": 2,
68 "WE": 3,
69 "TH": 4,
70 "FR": 5,
71 "SA": 6,
72 }
73 )
74
75 def __new__(
76 cls,
77 value,
78 encoding=DEFAULT_ENCODING,
79 /,
80 params: dict[str, Any] | None = None,
81 ):
82 value = to_unicode(value, encoding=encoding)
83 self = super().__new__(cls, value)
84 match = WEEKDAY_RULE.match(self)
85 if match is None:
86 raise ValueError(f"Expected weekday abbreviation, got: {self}")
87 match = match.groupdict()
88 sign = match["signal"]
89 weekday = match["weekday"]
90 relative = match["relative"]
91 if weekday not in vWeekday.week_days or sign not in "+-":
92 raise ValueError(f"Expected weekday abbreviation, got: {self}")
93 self.weekday = weekday or None
94 self.relative = (relative and int(relative)) or None
95 if sign == "-" and self.relative:
96 self.relative *= -1
97 self.params = Parameters(params)
98 return self
99
100 def to_ical(self):
101 return self.encode(DEFAULT_ENCODING).upper()
102
103 @classmethod
104 def from_ical(cls, ical):
105 try:
106 return cls(ical.upper())
107 except Exception as e:
108 raise ValueError(f"Expected weekday abbreviation, got: {ical}") from e
109
110 @property
111 def ical_value(self) -> str:
112 """Returns the weekday value as a string, for example, ``MO``, ``+2TH``, or ``-1SU``.
113
114 See Also:
115
116 :rfc:`5545#section-3.3.10` for the ``BYDAY`` rule grammar.
117 """
118 return str(self)
119
120 @classmethod
121 def parse_jcal_value(cls, value: Any) -> Self:
122 """Parse a jCal value for vWeekday.
123
124 Raises:
125 ~error.JCalParsingError: If the value is not a valid weekday.
126 """
127 JCalParsingError.validate_value_type(value, str, cls)
128 try:
129 return cls(value)
130 except ValueError as e:
131 raise JCalParsingError(
132 "The value must be a valid weekday.", cls, value=value
133 ) from e
134
135 @classmethod
136 @from_xcal_wrapper
137 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
138 """Parse xCal from :rfc:`6321`.
139
140 Parameters:
141 xml: The XML to parse or a parser.
142
143 Raises:
144 ~error.XCalParsingError: If the provided xCal is invalid.
145 """
146 child = parser.parse_tag()
147 text = child.get_xsd_token()
148 try:
149 return cls(text.upper(), params=params)
150 except ValueError as e:
151 raise XCalParsingError(
152 "Expected weekday https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10",
153 text,
154 child,
155 ) from e
156
157 def to_xcal(self, element: Element) -> None:
158 """Add the xCal representation of this property according to :rfc:`6321`."""
159 self.params.to_xcal(element)
160 weekday_element = SubElement(element, "byday")
161 weekday_element.text = self.ical_value
162
163
164__all__ = ["vWeekday"]