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

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

73 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)?)?$" 

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:: text 

67 

68 P15DT5H0M20S 

69 

70 A duration of 7 weeks would be: 

71 

72 .. code-block:: text 

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__(self, td: timedelta | str, /, params: dict[str, Any] | None = None): 

91 if isinstance(td, str): 

92 td = vDuration.from_ical(td) 

93 if not isinstance(td, timedelta): 

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

95 self.td = td 

96 self.params = Parameters(params) 

97 

98 def to_ical(self): 

99 sign = "" 

100 td = self.td 

101 if td.days < 0: 

102 sign = "-" 

103 td = -td 

104 timepart = "" 

105 if td.seconds: 

106 timepart = "T" 

107 hours = td.seconds // 3600 

108 minutes = td.seconds % 3600 // 60 

109 seconds = td.seconds % 60 

110 if hours: 

111 timepart += f"{hours}H" 

112 if minutes or (hours and seconds): 

113 timepart += f"{minutes}M" 

114 if seconds: 

115 timepart += f"{seconds}S" 

116 if td.days == 0 and timepart: 

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

118 return ( 

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

120 + b"P" 

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

122 + b"D" 

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

124 ) 

125 

126 @staticmethod 

127 def from_ical(ical): 

128 match = DURATION_REGEX.match(ical) 

129 if not match: 

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

131 

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

133 value = timedelta( 

134 weeks=int(weeks or 0), 

135 days=int(days or 0), 

136 hours=int(hours or 0), 

137 minutes=int(minutes or 0), 

138 seconds=int(seconds or 0), 

139 ) 

140 

141 if sign == "-": 

142 value = -value 

143 

144 return value 

145 

146 @property 

147 def dt(self) -> timedelta: 

148 """The time delta for compatibility.""" 

149 return self.td 

150 

151 @classmethod 

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

153 """Examples of vDuration.""" 

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

155 

156 from icalendar.param import VALUE 

157 

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

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

160 return [ 

161 name, 

162 self.params.to_jcal(), 

163 self.VALUE.lower(), 

164 self.to_ical().decode(), 

165 ] 

166 

167 @classmethod 

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

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

170 

171 Raises: 

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

173 JCalParsingError.validate_value_type(jcal, str, cls) 

174 try: 

175 return cls.from_ical(jcal) 

176 except (ValueError, InvalidCalendar) as e: 

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

178 

179 @classmethod 

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

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

182 

183 Parameters: 

184 jcal_property: The jCal property to parse. 

185 

186 Raises: 

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

188 """ 

189 JCalParsingError.validate_property(jcal_property, cls) 

190 with JCalParsingError.reraise_with_path_added(3): 

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

192 return cls( 

193 duration, 

194 Parameters.from_jcal_property(jcal_property), 

195 ) 

196 

197 

198__all__ = ["vDuration"]