"""BYDAY and WKST value type of RECUR from :rfc:`5545`."""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Any
from xml.etree.ElementTree import Element, SubElement
from icalendar.caselessdict import CaselessDict
from icalendar.error import JCalParsingError, XCalParsingError
from icalendar.parser import Parameters
from icalendar.parser.xcal.wrapper import from_xcal_wrapper
from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode
if TYPE_CHECKING:
from icalendar.compatibility import Self
from icalendar.parser.xcal.base import XCalParser
# Use ``[0-9]`` to explicitly specify that only ASCII digits should be
# matched, instead of ``\d``. In Python, ``\d`` matches a digit zero through
# nine in any script except ideographic scripts, and is equivalent to
# ``\p{Nd}``. For example, the Arabic-Indic "١٢" would match. Here, a
# malformed ``ordwk`` would get parsed into a valid ``relative`` number.
# RFC 5545, section 3.3.10, allows only ASCII characters per its definition
# of ``DIGIT``.
WEEKDAY_RULE = re.compile(
r"(?P<signal>[+-]?)(?P<relative>[0-9]{0,2})(?P<weekday>[\w]{2})\Z"
)
[docs]
class vWeekday(str):
"""Either a ``weekday`` or a ``weekdaynum``.
.. code-block:: pycon
>>> from icalendar import vWeekday
>>> vWeekday("MO") # Simple weekday
'MO'
>>> vWeekday("2FR").relative # Second friday
2
>>> vWeekday("2FR").weekday
'FR'
>>> vWeekday("-1SU").relative # Last Sunday
-1
Definition from :rfc:`5545#section-3.3.10`:
.. code-block:: text
weekdaynum = [[plus / minus] ordwk] weekday
plus = "+"
minus = "-"
ordwk = 1*2DIGIT ;1 to 53
weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
;FRIDAY, and SATURDAY days of the week.
"""
params: Parameters
__slots__ = ("params", "relative", "weekday")
week_days = CaselessDict(
{
"SU": 0,
"MO": 1,
"TU": 2,
"WE": 3,
"TH": 4,
"FR": 5,
"SA": 6,
}
)
def __new__(
cls,
value,
encoding=DEFAULT_ENCODING,
/,
params: dict[str, Any] | None = None,
):
value = to_unicode(value, encoding=encoding)
self = super().__new__(cls, value)
match = WEEKDAY_RULE.match(self)
if match is None:
raise ValueError(f"Expected weekday abbreviation, got: {self}")
match = match.groupdict()
sign = match["signal"]
weekday = match["weekday"]
relative = match["relative"]
if weekday not in vWeekday.week_days or sign not in "+-":
raise ValueError(f"Expected weekday abbreviation, got: {self}")
self.weekday = weekday or None
self.relative = (relative and int(relative)) or None
if sign == "-" and self.relative:
self.relative *= -1
self.params = Parameters(params)
return self
[docs]
def to_ical(self) -> bytes:
return self.encode(DEFAULT_ENCODING).upper()
[docs]
@classmethod
def from_ical(cls, ical):
try:
return cls(ical.upper())
except Exception as e:
raise ValueError(f"Expected weekday abbreviation, got: {ical}") from e
@property
def ical_value(self) -> str:
"""Returns the weekday value as a string, for example, ``MO``, ``+2TH``, or ``-1SU``.
See Also:
:rfc:`5545#section-3.3.10` for the ``BYDAY`` rule grammar.
"""
return str(self)
[docs]
@classmethod
def parse_jcal_value(cls, value: Any) -> Self:
"""Parse a jCal value for vWeekday.
Raises:
~icalendar.error.JCalParsingError: If the value is not a valid weekday.
"""
JCalParsingError.validate_value_type(value, str, cls)
try:
return cls(value)
except ValueError as e:
raise JCalParsingError(
"The value must be a valid weekday.", cls, value=value
) from e
[docs]
@classmethod
@from_xcal_wrapper
def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
"""Parse xCal from :rfc:`6321`.
Parameters:
xml: The XML to parse or a parser.
Raises:
~error.XCalParsingError: If the provided xCal is invalid.
"""
child = parser.parse_tag()
text = child.get_xsd_token()
try:
return cls(text.upper(), params=params)
except ValueError as e:
raise XCalParsingError(
"Expected weekday https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10",
text,
child,
) from e
[docs]
def to_xcal(self, element: Element) -> None:
"""Add the xCal representation of this property according to :rfc:`6321`."""
self.params.to_xcal(element)
weekday_element = SubElement(element, "byday")
weekday_element.text = self.ical_value
__all__ = ["vWeekday"]