"""UTC-Offset property type from :rfc:`5545`."""
import re
from datetime import timedelta
from typing import Any, ClassVar
from xml.etree.ElementTree import Element, SubElement
from icalendar.compatibility import Self
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.parser.xcal.base import XCalParser
from icalendar.parser.xcal.match import XCalRegexMatcher
from icalendar.parser.xcal.wrapper import from_xcal_wrapper
UTC_OFFSET_JCAL_REGEX = re.compile(
r"^(?P<sign>[+-])?(?P<hours>\d\d):(?P<minutes>\d\d)(?::(?P<seconds>\d\d))?\Z"
)
XCAL_UTC_OFFSET_REGEX = XCalRegexMatcher(
UTC_OFFSET_JCAL_REGEX,
"Expected utc-offset format https://datatracker.ietf.org/doc/html/rfc6321#section-3.6.14",
)
[docs]
class vUTCOffset:
"""UTC Offset
Value Name:
UTC-OFFSET
Purpose:
This value type is used to identify properties that contain
an offset from UTC to local time.
Format Definition:
This value type is defined by the following notation:
.. code-block:: text
utc-offset = time-numzone
time-numzone = ("+" / "-") time-hour time-minute [time-second]
Description:
The PLUS SIGN character MUST be specified for positive
UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST
be specified for negative UTC offsets (i.e., behind of UTC). The
value of "-0000" and "-000000" are not allowed. The time-second,
if present, MUST NOT be 60; if absent, it defaults to zero.
Example:
The following UTC offsets are given for standard time for
New York (five hours behind UTC) and Geneva (one hour ahead of
UTC):
.. code-block:: ics
-0500
+0100
.. code-block:: pycon
>>> from icalendar.prop import vUTCOffset
>>> utc_offset = vUTCOffset.from_ical('-0500')
>>> utc_offset
datetime.timedelta(days=-1, seconds=68400)
>>> utc_offset = vUTCOffset.from_ical('+0100')
>>> utc_offset
datetime.timedelta(seconds=3600)
"""
default_value: ClassVar[str] = "UTC-OFFSET"
params: Parameters
ignore_exceptions = False # if True, and we cannot parse this
# component, we will silently ignore
# it, rather than let the exception
# propagate upwards
def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None) -> None:
if not isinstance(td, timedelta):
raise TypeError("Offset value MUST be a timedelta instance")
self.td = td
self.params = Parameters(params)
[docs]
def to_ical(self) -> str:
"""Return the ical representation."""
return self.format("")
[docs]
@classmethod
def from_ical(cls, ical):
if isinstance(ical, cls):
return ical.td
try:
sign, hours, minutes, seconds = (
ical[0:1],
int(ical[1:3]),
int(ical[3:5]),
int(ical[5:7] or 0),
)
offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
except Exception as e:
raise ValueError(f"Expected UTC offset, got: {ical}") from e
if not cls.ignore_exceptions and offset >= timedelta(hours=24):
raise ValueError(f"Offset must be less than 24 hours, was {ical}")
if sign == "-":
return -offset
return offset
def __eq__(self, other):
if not isinstance(other, vUTCOffset):
return False
return self.td == other.td
def __hash__(self):
return hash(self.td)
def __repr__(self):
return f"vUTCOffset({self.td!r})"
[docs]
@classmethod
def examples(cls) -> list[Self]:
"""Examples of vUTCOffset."""
return [
cls(timedelta(hours=3)),
cls(timedelta(0)),
]
from icalendar.param import VALUE
[docs]
def to_jcal(self, name: str) -> list:
"""The jCal representation of this property according to :rfc:`7265`."""
return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")]
[docs]
@classmethod
def from_jcal(cls, jcal_property: list) -> Self:
"""Parse jCal from :rfc:`7265`.
Parameters:
jcal_property: The jCal property to parse.
Raises:
~icalendar.error.JCalParsingError: If the provided jCal is invalid.
"""
JCalParsingError.validate_property(jcal_property, cls)
match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3])
if match is None:
raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.")
negative = match.group("sign") == "-"
hours = int(match.group("hours"))
minutes = int(match.group("minutes"))
seconds = int(match.group("seconds") or 0)
t = timedelta(hours=hours, minutes=minutes, seconds=seconds)
if negative:
t = -t
return cls(t, Parameters.from_jcal_property(jcal_property))
[docs]
def to_xcal(self, element: Element) -> None:
"""The xCal representation of this property according to :rfc:`6321`."""
self.params.to_xcal(element)
element = SubElement(element, self.default_value.lower())
element.text = self.format(":")
[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.
"""
element = parser.parse_tag(cls.default_value)
sign, hour, minute, second = XCAL_UTC_OFFSET_REGEX.groups(element)
dt = timedelta(hours=int(hour), minutes=int(minute), seconds=int(second or "0"))
if sign == "-":
dt = -dt
return cls(dt, params=params)
__all__ = ["vUTCOffset"]