Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/dt/duration.py: 82%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

76 statements  

1"""DURATION property type from :rfc:`5545`.""" 

2 

3import re 

4from datetime import timedelta 

5from typing import Any, ClassVar 

6 

7from icalendar.compatibility import Self 

8from icalendar.error import InvalidCalendar, JCalParsingError 

9from icalendar.parser import Parameters 

10 

11from .base import TimeBase 

12 

13DURATION_REGEX = re.compile( 

14 r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?\Z" 

15) 

16 

17 

18class vDuration(TimeBase): 

19 """Duration 

20 

21 Value Name: 

22 DURATION 

23 

24 Purpose: 

25 This value type is used to identify properties that contain 

26 a duration of time. 

27 

28 Format Definition: 

29 This value type is defined by the following notation: 

30 

31 .. code-block:: text 

32 

33 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) 

34 

35 dur-date = dur-day [dur-time] 

36 dur-time = "T" (dur-hour / dur-minute / dur-second) 

37 dur-week = 1*DIGIT "W" 

38 dur-hour = 1*DIGIT "H" [dur-minute] 

39 dur-minute = 1*DIGIT "M" [dur-second] 

40 dur-second = 1*DIGIT "S" 

41 dur-day = 1*DIGIT "D" 

42 

43 Description: 

44 If the property permits, multiple "duration" values are 

45 specified by a COMMA-separated list of values. The format is 

46 based on the [ISO.8601.2004] complete representation basic format 

47 with designators for the duration of time. The format can 

48 represent nominal durations (weeks and days) and accurate 

49 durations (hours, minutes, and seconds). Note that unlike 

50 [ISO.8601.2004], this value type doesn't support the "Y" and "M" 

51 designators to specify durations in terms of years and months. 

52 The duration of a week or a day depends on its position in the 

53 calendar. In the case of discontinuities in the time scale, such 

54 as the change from standard time to daylight time and back, the 

55 computation of the exact duration requires the subtraction or 

56 addition of the change of duration of the discontinuity. Leap 

57 seconds MUST NOT be considered when computing an exact duration. 

58 When computing an exact duration, the greatest order time 

59 components MUST be added first, that is, the number of days MUST 

60 be added first, followed by the number of hours, number of 

61 minutes, and number of seconds. 

62 

63 Example: 

64 A duration of 15 days, 5 hours, and 20 seconds would be: 

65 

66 .. code-block:: ics 

67 

68 P15DT5H0M20S 

69 

70 A duration of 7 weeks would be: 

71 

72 .. code-block:: ics 

73 

74 P7W 

75 

76 .. code-block:: pycon 

77 

78 >>> from icalendar.prop import vDuration 

79 >>> duration = vDuration.from_ical('P15DT5H0M20S') 

80 >>> duration 

81 datetime.timedelta(days=15, seconds=18020) 

82 >>> duration = vDuration.from_ical('P7W') 

83 >>> duration 

84 datetime.timedelta(days=49) 

85 """ 

86 

87 default_value: ClassVar[str] = "DURATION" 

88 params: Parameters 

89 

90 def __init__( 

91 self, td: timedelta | str, /, params: dict[str, Any] | None = None 

92 ) -> None: 

93 if isinstance(td, str): 

94 td = vDuration.from_ical(td) 

95 if not isinstance(td, timedelta): 

96 raise TypeError("Value MUST be a timedelta instance") 

97 self.td = td 

98 self.params = Parameters(params) 

99 

100 def to_ical(self): 

101 sign = "" 

102 td = self.td 

103 if td.days < 0: 

104 sign = "-" 

105 td = -td 

106 timepart = "" 

107 if td.seconds: 

108 timepart = "T" 

109 hours = td.seconds // 3600 

110 minutes = td.seconds % 3600 // 60 

111 seconds = td.seconds % 60 

112 if hours: 

113 timepart += f"{hours}H" 

114 if minutes or (hours and seconds): 

115 timepart += f"{minutes}M" 

116 if seconds: 

117 timepart += f"{seconds}S" 

118 if td.days == 0 and timepart: 

119 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8") 

120 return ( 

121 str(sign).encode("utf-8") 

122 + b"P" 

123 + str(abs(td.days)).encode("utf-8") 

124 + b"D" 

125 + str(timepart).encode("utf-8") 

126 ) 

127 

128 @staticmethod 

129 def from_ical(ical): 

130 match = DURATION_REGEX.match(ical) 

131 if not match: 

132 raise InvalidCalendar(f"Invalid iCalendar duration: {ical}") 

133 

134 sign, weeks, days, hours, minutes, seconds = match.groups() 

135 try: 

136 value = timedelta( 

137 weeks=int(weeks or 0), 

138 days=int(days or 0), 

139 hours=int(hours or 0), 

140 minutes=int(minutes or 0), 

141 seconds=int(seconds or 0), 

142 ) 

143 except OverflowError as e: 

144 # ``timedelta`` rejects values that are too large for its C 

145 # implementation. Raise the same error as other invalid durations 

146 # instead of leaking ``OverflowError`` to callers. 

147 raise InvalidCalendar(f"Impractical iCalendar duration: {ical}") from e 

148 

149 if sign == "-": 

150 value = -value 

151 

152 return value 

153 

154 @property 

155 def dt(self) -> timedelta: 

156 """The time delta for compatibility.""" 

157 return self.td 

158 

159 @classmethod 

160 def examples(cls) -> list[Self]: 

161 """Examples of vDuration.""" 

162 return [cls(timedelta(1, 99))] 

163 

164 from icalendar.param import VALUE 

165 

166 def to_jcal(self, name: str) -> list: 

167 """The jCal representation of this property according to :rfc:`7265`.""" 

168 return [ 

169 name, 

170 self.params.to_jcal(), 

171 self.VALUE.lower(), 

172 self.to_ical().decode(), 

173 ] 

174 

175 @classmethod 

176 def parse_jcal_value(cls, jcal: str) -> timedelta: 

177 """Parse a jCal string to a :class:`datetime.timedelta`. 

178 

179 Raises: 

180 ~error.JCalParsingError: If it can't parse a duration.""" 

181 JCalParsingError.validate_value_type(jcal, str, cls) 

182 try: 

183 return cls.from_ical(jcal) 

184 except (ValueError, InvalidCalendar) as e: 

185 raise JCalParsingError("Cannot parse duration.", cls, value=jcal) from e 

186 

187 @classmethod 

188 def from_jcal(cls, jcal_property: list) -> Self: 

189 """Parse jCal from :rfc:`7265`. 

190 

191 Parameters: 

192 jcal_property: The jCal property to parse. 

193 

194 Raises: 

195 ~error.JCalParsingError: If the provided jCal is invalid. 

196 """ 

197 JCalParsingError.validate_property(jcal_property, cls) 

198 with JCalParsingError.reraise_with_path_added(3): 

199 duration = cls.parse_jcal_value(jcal_property[3]) 

200 return cls( 

201 duration, 

202 Parameters.from_jcal_property(jcal_property), 

203 ) 

204 

205 

206__all__ = ["vDuration"]