"""PERIOD property type from :rfc:`5545`."""
from datetime import date, datetime, timedelta
from typing import Any, ClassVar
from icalendar.compatibility import Self
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.timezone import tzp
from icalendar.tools import is_datetime, normalize_pytz
from .base import TimeBase
from .datetime import vDatetime
from .duration import vDuration
[docs]
class vPeriod(TimeBase):
"""Period of Time
Value Name:
PERIOD
Purpose:
This value type is used to identify values that contain a
precise period of time.
Format Definition:
This value type is defined by the following notation:
.. code-block:: text
period = period-explicit / period-start
period-explicit = date-time "/" date-time
; [ISO.8601.2004] complete representation basic format for a
; period of time consisting of a start and end. The start MUST
; be before the end.
period-start = date-time "/" dur-value
; [ISO.8601.2004] complete representation basic format for a
; period of time consisting of a start and positive duration
; of time.
Description:
If the property permits, multiple "period" values are
specified by a COMMA-separated list of values. There are two
forms of a period of time. First, a period of time is identified
by its start and its end. This format is based on the
[ISO.8601.2004] complete representation, basic format for "DATE-
TIME" start of the period, followed by a SOLIDUS character
followed by the "DATE-TIME" of the end of the period. The start
of the period MUST be before the end of the period. Second, a
period of time can also be defined by a start and a positive
duration of time. The format is based on the [ISO.8601.2004]
complete representation, basic format for the "DATE-TIME" start of
the period, followed by a SOLIDUS character, followed by the
[ISO.8601.2004] basic format for "DURATION" of the period.
Example:
The period starting at 18:00:00 UTC, on January 1, 1997 and
ending at 07:00:00 UTC on January 2, 1997 would be:
.. code-block:: text
19970101T180000Z/19970102T070000Z
The period start at 18:00:00 on January 1, 1997 and lasting 5 hours
and 30 minutes would be:
.. code-block:: text
19970101T180000Z/PT5H30M
.. code-block:: pycon
>>> from icalendar.prop import vPeriod
>>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z')
>>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M')
"""
default_value: ClassVar[str] = "PERIOD"
params: Parameters
by_duration: bool
start: datetime
end: datetime
duration: timedelta
def __init__(
self,
per: tuple[datetime, datetime | timedelta],
params: dict[str, Any] | None = None,
):
start, end_or_duration = per
if not (isinstance(start, (datetime, date))):
raise TypeError("Start value MUST be a datetime or date instance")
if not (isinstance(end_or_duration, (datetime, date, timedelta))):
raise TypeError(
"end_or_duration MUST be a datetime, date or timedelta instance"
)
by_duration = isinstance(end_or_duration, timedelta)
if by_duration:
duration = end_or_duration
end = normalize_pytz(start + duration)
else:
end = end_or_duration
duration = normalize_pytz(end - start)
if start > end:
raise ValueError("Start time is greater than end time")
self.params = Parameters(params or {"value": "PERIOD"})
# set the timezone identifier
# does not support different timezones for start and end
self.params.update_tzid_from(start)
self.start = start
self.end = end
self.by_duration = by_duration
self.duration = duration
[docs]
def overlaps(self, other):
if self.start > other.start:
return other.overlaps(self)
return self.start <= other.start < self.end
[docs]
def to_ical(self):
if self.by_duration:
return (
vDatetime(self.start).to_ical()
+ b"/"
+ vDuration(self.duration).to_ical()
)
return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical()
[docs]
@staticmethod
def from_ical(ical, timezone=None):
from icalendar.prop.dt.types import vDDDTypes
try:
start, end_or_duration = ical.split("/")
start = vDDDTypes.from_ical(start, timezone=timezone)
end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone)
except Exception as e:
raise ValueError(f"Expected period format, got: {ical}") from e
return (start, end_or_duration)
def __repr__(self):
p = (self.start, self.duration) if self.by_duration else (self.start, self.end)
return f"vPeriod({p!r})"
@property
def dt(self):
"""Make this cooperate with the other vDDDTypes."""
return (self.start, (self.duration if self.by_duration else self.end))
from icalendar.param import FBTYPE
[docs]
@classmethod
def examples(cls) -> list[Self]:
"""Examples of vPeriod."""
return [
vPeriod((datetime(2025, 11, 10, 16, 35), timedelta(hours=1, minutes=30))),
vPeriod((datetime(2025, 11, 10, 16, 35), datetime(2025, 11, 10, 18, 5))),
]
from icalendar.param import VALUE
[docs]
def to_jcal(self, name: str) -> list:
"""The jCal representation of this property according to :rfc:`7265`."""
value = [vDatetime(self.start).to_jcal(name)[-1]]
if self.by_duration:
value.append(vDuration(self.duration).to_jcal(name)[-1])
else:
value.append(vDatetime(self.end).to_jcal(name)[-1])
return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
[docs]
@classmethod
def parse_jcal_value(
cls, jcal: str | list
) -> tuple[datetime, datetime] | tuple[datetime, timedelta]:
"""Parse a jCal value.
Raises:
~error.JCalParsingError: If the period is not a list with exactly two items,
or it can't parse a date-time or duration.
"""
if isinstance(jcal, str) and "/" in jcal:
# only occurs in the example of RFC7265, Section B.2.2.
jcal = jcal.split("/")
if not isinstance(jcal, list) or len(jcal) != 2:
raise JCalParsingError(
"A period must be a list with exactly 2 items.", cls, value=jcal
)
with JCalParsingError.reraise_with_path_added(0):
start = vDatetime.parse_jcal_value(jcal[0])
with JCalParsingError.reraise_with_path_added(1):
JCalParsingError.validate_value_type(jcal[1], str, cls)
if jcal[1].startswith(("P", "-P", "+P")):
end_or_duration = vDuration.parse_jcal_value(jcal[1])
else:
try:
end_or_duration = vDatetime.parse_jcal_value(jcal[1])
except JCalParsingError as e:
raise JCalParsingError(
"Cannot parse date-time or duration.",
cls,
value=jcal[1],
) from e
return start, end_or_duration
[docs]
@classmethod
def from_jcal(cls, jcal_property: list) -> Self:
"""Parse jCal from :rfc:`7265`.
Parameters:
jcal_property: The jCal property to parse.
Raises:
~error.JCalParsingError: If the provided jCal is invalid.
"""
JCalParsingError.validate_property(jcal_property, cls)
with JCalParsingError.reraise_with_path_added(3):
start, end_or_duration = cls.parse_jcal_value(jcal_property[3])
params = Parameters.from_jcal_property(jcal_property)
tzid = params.tzid
if tzid:
start = tzp.localize(start, tzid)
if is_datetime(end_or_duration):
end_or_duration = tzp.localize(end_or_duration, tzid)
return cls((start, end_or_duration), params=params)
__all__ = ["vPeriod"]