1"""BYWEEKDAY, BYDAY, and WKST value type of RECUR from :rfc:`5545`."""
2
3import re
4from typing import Any
5
6from icalendar.caselessdict import CaselessDict
7from icalendar.compatibility import Self
8from icalendar.error import JCalParsingError
9from icalendar.parser import Parameters
10from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode
11
12WEEKDAY_RULE = re.compile(
13 r"(?P<signal>[+-]?)(?P<relative>[\d]{0,2})(?P<weekday>[\w]{2})$"
14)
15
16
17class vWeekday(str):
18 """Either a ``weekday`` or a ``weekdaynum``.
19
20 .. code-block:: pycon
21
22 >>> from icalendar import vWeekday
23 >>> vWeekday("MO") # Simple weekday
24 'MO'
25 >>> vWeekday("2FR").relative # Second friday
26 2
27 >>> vWeekday("2FR").weekday
28 'FR'
29 >>> vWeekday("-1SU").relative # Last Sunday
30 -1
31
32 Definition from :rfc:`5545#section-3.3.10`:
33
34 .. code-block:: text
35
36 weekdaynum = [[plus / minus] ordwk] weekday
37 plus = "+"
38 minus = "-"
39 ordwk = 1*2DIGIT ;1 to 53
40 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
41 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
42 ;FRIDAY, and SATURDAY days of the week.
43
44 """
45
46 params: Parameters
47 __slots__ = ("params", "relative", "weekday")
48
49 week_days = CaselessDict(
50 {
51 "SU": 0,
52 "MO": 1,
53 "TU": 2,
54 "WE": 3,
55 "TH": 4,
56 "FR": 5,
57 "SA": 6,
58 }
59 )
60
61 def __new__(
62 cls,
63 value,
64 encoding=DEFAULT_ENCODING,
65 /,
66 params: dict[str, Any] | None = None,
67 ):
68 value = to_unicode(value, encoding=encoding)
69 self = super().__new__(cls, value)
70 match = WEEKDAY_RULE.match(self)
71 if match is None:
72 raise ValueError(f"Expected weekday abbreviation, got: {self}")
73 match = match.groupdict()
74 sign = match["signal"]
75 weekday = match["weekday"]
76 relative = match["relative"]
77 if weekday not in vWeekday.week_days or sign not in "+-":
78 raise ValueError(f"Expected weekday abbreviation, got: {self}")
79 self.weekday = weekday or None
80 self.relative = (relative and int(relative)) or None
81 if sign == "-" and self.relative:
82 self.relative *= -1
83 self.params = Parameters(params)
84 return self
85
86 def to_ical(self):
87 return self.encode(DEFAULT_ENCODING).upper()
88
89 @classmethod
90 def from_ical(cls, ical):
91 try:
92 return cls(ical.upper())
93 except Exception as e:
94 raise ValueError(f"Expected weekday abbreviation, got: {ical}") from e
95
96 @classmethod
97 def parse_jcal_value(cls, value: Any) -> Self:
98 """Parse a jCal value for vWeekday.
99
100 Raises:
101 ~error.JCalParsingError: If the value is not a valid weekday.
102 """
103 JCalParsingError.validate_value_type(value, str, cls)
104 try:
105 return cls(value)
106 except ValueError as e:
107 raise JCalParsingError(
108 "The value must be a valid weekday.", cls, value=value
109 ) from e
110
111
112__all__ = ["vWeekday"]