Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dateutil/rrule.py: 32%

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

997 statements  

1""" 

2The rrule module offers a small, complete, and very fast, implementation of 

3the recurrence rules documented in the 

4`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, 

5including support for caching of results. 

6""" 

7 

8import _thread 

9import calendar 

10import datetime 

11import heapq 

12import itertools 

13import math 

14import re 

15import sys 

16from functools import wraps 

17# For warning about deprecation of until and count 

18from warnings import warn 

19 

20from ._common import weekday as weekdaybase 

21 

22__all__ = ["rrule", "rruleset", "rrulestr", 

23 "YEARLY", "MONTHLY", "WEEKLY", "DAILY", 

24 "HOURLY", "MINUTELY", "SECONDLY", 

25 "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 

26 

27# Every mask is 7 days longer to handle cross-year weekly periods. 

28M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + 

29 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) 

30M365MASK = list(M366MASK) 

31M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) 

32MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 

33MDAY365MASK = list(MDAY366MASK) 

34M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) 

35NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 

36NMDAY365MASK = list(NMDAY366MASK) 

37M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) 

38M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) 

39WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 

40del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] 

41MDAY365MASK = tuple(MDAY365MASK) 

42M365MASK = tuple(M365MASK) 

43 

44FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] 

45 

46(YEARLY, 

47 MONTHLY, 

48 WEEKLY, 

49 DAILY, 

50 HOURLY, 

51 MINUTELY, 

52 SECONDLY) = list(range(7)) 

53 

54# Imported on demand. 

55easter = None 

56parser = None 

57 

58 

59class weekday(weekdaybase): 

60 """ 

61 This version of weekday does not allow n = 0. 

62 """ 

63 def __init__(self, wkday, n=None): 

64 if n == 0: 

65 raise ValueError("Can't create weekday with n==0") 

66 

67 super().__init__(wkday, n) 

68 

69 

70MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) 

71 

72 

73def _invalidates_cache(f): 

74 """ 

75 Decorator for rruleset methods which may invalidate the 

76 cached length. 

77 """ 

78 @wraps(f) 

79 def inner_func(self, *args, **kwargs): 

80 rv = f(self, *args, **kwargs) 

81 self._invalidate_cache() 

82 return rv 

83 

84 return inner_func 

85 

86 

87class rrulebase: 

88 def __init__(self, cache=False): 

89 if cache: 

90 self._cache = [] 

91 self._cache_lock = _thread.allocate_lock() 

92 self._invalidate_cache() 

93 else: 

94 self._cache = None 

95 self._cache_complete = False 

96 self._len = None 

97 

98 def __reduce_ex__(self, protocol): 

99 rv = super().__reduce_ex__(protocol) 

100 state, slots = rv[2], None 

101 if isinstance(state, tuple): 

102 state, slots = state 

103 if self._cache is not None: 

104 state = dict(state) 

105 state.pop("_cache_lock", None) 

106 state.pop("_cache_gen", None) 

107 state["_cache"] = [] 

108 if slots is not None: 

109 state = (state, slots) 

110 return rv[:2] + (state,) + rv[3:] 

111 

112 def __setstate__(self, state): 

113 if isinstance(state, tuple): 

114 state, slots = state 

115 for name, value in slots.items(): 

116 setattr(self, name, value) 

117 self.__dict__.update(state) 

118 if self._cache is not None: 

119 self._cache_lock = _thread.allocate_lock() 

120 self._invalidate_cache() 

121 

122 def __iter__(self): 

123 if self._cache_complete: 

124 return iter(self._cache) 

125 elif self._cache is None: 

126 return self._iter() 

127 else: 

128 return self._iter_cached() 

129 

130 def _invalidate_cache(self): 

131 if self._cache is not None: 

132 self._cache = [] 

133 self._cache_complete = False 

134 self._cache_gen = self._iter() 

135 

136 if self._cache_lock.locked(): 

137 self._cache_lock.release() 

138 

139 self._len = None 

140 

141 def _iter_cached(self): 

142 i = 0 

143 gen = self._cache_gen 

144 cache = self._cache 

145 lock = self._cache_lock 

146 while gen: 

147 if i == len(cache): 

148 with lock: 

149 if self._cache_complete: 

150 break 

151 try: 

152 for j in range(10): 

153 cache.append(next(gen)) 

154 except StopIteration: 

155 self._cache_gen = gen = None 

156 self._cache_complete = True 

157 break 

158 yield cache[i] 

159 i += 1 

160 while i < self._len: 

161 yield cache[i] 

162 i += 1 

163 

164 def __getitem__(self, item): 

165 if self._cache_complete: 

166 return self._cache[item] 

167 elif isinstance(item, slice): 

168 if item.step and item.step < 0: 

169 return list(iter(self))[item] 

170 else: 

171 return list(itertools.islice(self, 

172 item.start or 0, 

173 item.stop or sys.maxsize, 

174 item.step or 1)) 

175 elif item >= 0: 

176 gen = iter(self) 

177 try: 

178 for i in range(item+1): 

179 res = next(gen) 

180 except StopIteration: 

181 raise IndexError 

182 return res 

183 else: 

184 return list(iter(self))[item] 

185 

186 def __contains__(self, item): 

187 if self._cache_complete: 

188 return item in self._cache 

189 else: 

190 for i in self: 

191 if i == item: 

192 return True 

193 elif i > item: 

194 return False 

195 return False 

196 

197 # __len__() introduces a large performance penalty. 

198 def count(self): 

199 """ Returns the number of recurrences in this set. It will have go 

200 through the whole recurrence, if this hasn't been done before. """ 

201 if self._len is None: 

202 for x in self: 

203 pass 

204 return self._len 

205 

206 def before(self, dt, inc=False): 

207 """ Returns the last recurrence before the given datetime instance. The 

208 inc keyword defines what happens if dt is an occurrence. With 

209 inc=True, if dt itself is an occurrence, it will be returned. """ 

210 if self._cache_complete: 

211 gen = self._cache 

212 else: 

213 gen = self 

214 last = None 

215 if inc: 

216 for i in gen: 

217 if i > dt: 

218 break 

219 last = i 

220 else: 

221 for i in gen: 

222 if i >= dt: 

223 break 

224 last = i 

225 return last 

226 

227 def after(self, dt, inc=False): 

228 """ Returns the first recurrence after the given datetime instance. The 

229 inc keyword defines what happens if dt is an occurrence. With 

230 inc=True, if dt itself is an occurrence, it will be returned. """ 

231 if self._cache_complete: 

232 gen = self._cache 

233 else: 

234 gen = self 

235 if inc: 

236 for i in gen: 

237 if i >= dt: 

238 return i 

239 else: 

240 for i in gen: 

241 if i > dt: 

242 return i 

243 return None 

244 

245 def xafter(self, dt, count=None, inc=False): 

246 """ 

247 Generator which yields up to `count` recurrences after the given 

248 datetime instance, equivalent to `after`. 

249 

250 :param dt: 

251 The datetime at which to start generating recurrences. 

252 

253 :param count: 

254 The maximum number of recurrences to generate. If `None` (default), 

255 dates are generated until the recurrence rule is exhausted. 

256 

257 :param inc: 

258 If `dt` is an instance of the rule and `inc` is `True`, it is 

259 included in the output. 

260 

261 :yields: Yields a sequence of `datetime` objects. 

262 """ 

263 

264 if self._cache_complete: 

265 gen = self._cache 

266 else: 

267 gen = self 

268 

269 # Select the comparison function 

270 if inc: 

271 comp = lambda dc, dtc: dc >= dtc 

272 else: 

273 comp = lambda dc, dtc: dc > dtc 

274 

275 # Generate dates 

276 n = 0 

277 for d in gen: 

278 if comp(d, dt): 

279 if count is not None: 

280 n += 1 

281 if n > count: 

282 break 

283 

284 yield d 

285 

286 def between(self, after, before, inc=False, count=1): 

287 """ Returns all the occurrences of the rrule between after and before. 

288 The inc keyword defines what happens if after and/or before are 

289 themselves occurrences. With inc=True, they will be included in the 

290 list, if they are found in the recurrence set. """ 

291 if self._cache_complete: 

292 gen = self._cache 

293 else: 

294 gen = self 

295 started = False 

296 l = [] 

297 if inc: 

298 for i in gen: 

299 if i > before: 

300 break 

301 elif not started: 

302 if i >= after: 

303 started = True 

304 l.append(i) 

305 else: 

306 l.append(i) 

307 else: 

308 for i in gen: 

309 if i >= before: 

310 break 

311 elif not started: 

312 if i > after: 

313 started = True 

314 l.append(i) 

315 else: 

316 l.append(i) 

317 return l 

318 

319 

320class rrule(rrulebase): 

321 """ 

322 That's the base of the rrule operation. It accepts all the keywords 

323 defined in the RFC as its constructor parameters (except byday, 

324 which was renamed to byweekday) and more. The constructor prototype is:: 

325 

326 rrule(freq) 

327 

328 Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, 

329 or SECONDLY. 

330 

331 .. note:: 

332 Per RFC section 3.3.10, recurrence instances falling on invalid dates 

333 and times are ignored rather than coerced: 

334 

335 Recurrence rules may generate recurrence instances with an invalid 

336 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM 

337 on a day where the local time is moved forward by an hour at 1:00 

338 AM). Such recurrence instances MUST be ignored and MUST NOT be 

339 counted as part of the recurrence set. 

340 

341 This can lead to possibly surprising behavior when, for example, the 

342 start date occurs at the end of the month: 

343 

344 >>> from dateutil.rrule import rrule, MONTHLY 

345 >>> from datetime import datetime 

346 >>> start_date = datetime(2014, 12, 31) 

347 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) 

348 ... # doctest: +NORMALIZE_WHITESPACE 

349 [datetime.datetime(2014, 12, 31, 0, 0), 

350 datetime.datetime(2015, 1, 31, 0, 0), 

351 datetime.datetime(2015, 3, 31, 0, 0), 

352 datetime.datetime(2015, 5, 31, 0, 0)] 

353 

354 Additionally, it supports the following keyword arguments: 

355 

356 :param dtstart: 

357 The recurrence start. Besides being the base for the recurrence, 

358 missing parameters in the final recurrence instances will also be 

359 extracted from this date. If not given, datetime.now() will be used 

360 instead. 

361 :param interval: 

362 The interval between each freq iteration. For example, when using 

363 YEARLY, an interval of 2 means once every two years, but with HOURLY, 

364 it means once every two hours. The default interval is 1. 

365 :param wkst: 

366 The week start day. Must be one of the MO, TU, WE constants, or an 

367 integer, specifying the first day of the week. This will affect 

368 recurrences based on weekly periods. The default week start is got 

369 from calendar.firstweekday(), and may be modified by 

370 calendar.setfirstweekday(). 

371 :param count: 

372 If given, this determines how many occurrences will be generated. 

373 

374 .. note:: 

375 As of version 2.5.0, the use of the keyword ``until`` in conjunction 

376 with ``count`` is deprecated, to make sure ``dateutil`` is fully 

377 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ 

378 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` 

379 **must not** occur in the same call to ``rrule``. 

380 :param until: 

381 If given, this must be a datetime instance specifying the upper-bound 

382 limit of the recurrence. The last recurrence in the rule is the greatest 

383 datetime that is less than or equal to the value specified in the 

384 ``until`` parameter. 

385 

386 .. note:: 

387 As of version 2.5.0, the use of the keyword ``until`` in conjunction 

388 with ``count`` is deprecated, to make sure ``dateutil`` is fully 

389 compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/ 

390 html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count`` 

391 **must not** occur in the same call to ``rrule``. 

392 :param bysetpos: 

393 If given, it must be either an integer, or a sequence of integers, 

394 positive or negative. Each given integer will specify an occurrence 

395 number, corresponding to the nth occurrence of the rule inside the 

396 frequency period. For example, a bysetpos of -1 if combined with a 

397 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will 

398 result in the last work day of every month. 

399 :param bymonth: 

400 If given, it must be either an integer, or a sequence of integers, 

401 meaning the months to apply the recurrence to. 

402 :param bymonthday: 

403 If given, it must be either an integer, or a sequence of integers, 

404 meaning the month days to apply the recurrence to. 

405 :param byyearday: 

406 If given, it must be either an integer, or a sequence of integers, 

407 meaning the year days to apply the recurrence to. 

408 :param byeaster: 

409 If given, it must be either an integer, or a sequence of integers, 

410 positive or negative. Each integer will define an offset from the 

411 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter 

412 Sunday itself. This is an extension to the RFC specification. 

413 :param byweekno: 

414 If given, it must be either an integer, or a sequence of integers, 

415 meaning the week numbers to apply the recurrence to. Week numbers 

416 have the meaning described in ISO8601, that is, the first week of 

417 the year is that containing at least four days of the new year. 

418 :param byweekday: 

419 If given, it must be either an integer (0 == MO), a sequence of 

420 integers, one of the weekday constants (MO, TU, etc), or a sequence 

421 of these constants. When given, these variables will define the 

422 weekdays where the recurrence will be applied. It's also possible to 

423 use an argument n for the weekday instances, which will mean the nth 

424 occurrence of this weekday in the period. For example, with MONTHLY, 

425 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the 

426 first friday of the month where the recurrence happens. Notice that in 

427 the RFC documentation, this is specified as BYDAY, but was renamed to 

428 avoid the ambiguity of that keyword. 

429 :param byhour: 

430 If given, it must be either an integer, or a sequence of integers, 

431 meaning the hours to apply the recurrence to. 

432 :param byminute: 

433 If given, it must be either an integer, or a sequence of integers, 

434 meaning the minutes to apply the recurrence to. 

435 :param bysecond: 

436 If given, it must be either an integer, or a sequence of integers, 

437 meaning the seconds to apply the recurrence to. 

438 :param cache: 

439 If given, it must be a boolean value specifying to enable or disable 

440 caching of results. If you will use the same rrule instance multiple 

441 times, enabling caching will improve the performance considerably. 

442 """ 

443 def __init__(self, freq, dtstart=None, 

444 interval=1, wkst=None, count=None, until=None, bysetpos=None, 

445 bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 

446 byweekno=None, byweekday=None, 

447 byhour=None, byminute=None, bysecond=None, 

448 cache=False): 

449 super().__init__(cache) 

450 global easter 

451 if not dtstart: 

452 if until and until.tzinfo: 

453 dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) 

454 else: 

455 dtstart = datetime.datetime.now().replace(microsecond=0) 

456 elif not isinstance(dtstart, datetime.datetime): 

457 dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) 

458 else: 

459 dtstart = dtstart.replace(microsecond=0) 

460 self._dtstart = dtstart 

461 self._tzinfo = dtstart.tzinfo 

462 self._freq = freq 

463 self._interval = interval 

464 self._count = count 

465 

466 # Cache the original byxxx rules, if they are provided, as the _byxxx 

467 # attributes do not necessarily map to the inputs, and this can be 

468 # a problem in generating the strings. Only store things if they've 

469 # been supplied (the string retrieval will just use .get()) 

470 self._original_rule = {} 

471 

472 if until and not isinstance(until, datetime.datetime): 

473 until = datetime.datetime.fromordinal(until.toordinal()) 

474 self._until = until 

475 

476 if self._dtstart and self._until: 

477 if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): 

478 # According to RFC5545 Section 3.3.10: 

479 # https://tools.ietf.org/html/rfc5545#section-3.3.10 

480 # 

481 # > If the "DTSTART" property is specified as a date with UTC 

482 # > time or a date with local time and time zone reference, 

483 # > then the UNTIL rule part MUST be specified as a date with 

484 # > UTC time. 

485 raise ValueError( 

486 'RRULE UNTIL values must be specified in UTC when DTSTART ' 

487 'is timezone-aware' 

488 ) 

489 

490 if count is not None and until: 

491 warn("Using both 'count' and 'until' is inconsistent with RFC 5545" 

492 " and has been deprecated in dateutil. Future versions will " 

493 "raise an error.", DeprecationWarning) 

494 

495 if wkst is None: 

496 self._wkst = calendar.firstweekday() 

497 elif isinstance(wkst, int): 

498 self._wkst = wkst 

499 else: 

500 self._wkst = wkst.weekday 

501 

502 if bysetpos is None: 

503 self._bysetpos = None 

504 elif isinstance(bysetpos, int): 

505 if bysetpos == 0 or not (-366 <= bysetpos <= 366): 

506 raise ValueError("bysetpos must be between 1 and 366, " 

507 "or between -366 and -1") 

508 self._bysetpos = (bysetpos,) 

509 else: 

510 self._bysetpos = tuple(bysetpos) 

511 for pos in self._bysetpos: 

512 if pos == 0 or not (-366 <= pos <= 366): 

513 raise ValueError("bysetpos must be between 1 and 366, " 

514 "or between -366 and -1") 

515 

516 if self._bysetpos: 

517 self._original_rule['bysetpos'] = self._bysetpos 

518 

519 if (byweekno is None and byyearday is None and bymonthday is None and 

520 byweekday is None and byeaster is None): 

521 if freq == YEARLY: 

522 if bymonth is None: 

523 bymonth = dtstart.month 

524 self._original_rule['bymonth'] = None 

525 bymonthday = dtstart.day 

526 self._original_rule['bymonthday'] = None 

527 elif freq == MONTHLY: 

528 bymonthday = dtstart.day 

529 self._original_rule['bymonthday'] = None 

530 elif freq == WEEKLY: 

531 byweekday = dtstart.weekday() 

532 self._original_rule['byweekday'] = None 

533 

534 # bymonth 

535 if bymonth is None: 

536 self._bymonth = None 

537 else: 

538 if isinstance(bymonth, int): 

539 bymonth = (bymonth,) 

540 

541 self._bymonth = tuple(sorted(set(bymonth))) 

542 

543 if 'bymonth' not in self._original_rule: 

544 self._original_rule['bymonth'] = self._bymonth 

545 

546 # byyearday 

547 if byyearday is None: 

548 self._byyearday = None 

549 else: 

550 if isinstance(byyearday, int): 

551 byyearday = (byyearday,) 

552 

553 self._byyearday = tuple(sorted(set(byyearday))) 

554 self._original_rule['byyearday'] = self._byyearday 

555 

556 # byeaster 

557 if byeaster is not None: 

558 if not easter: 

559 from dateutil import easter 

560 if isinstance(byeaster, int): 

561 self._byeaster = (byeaster,) 

562 else: 

563 self._byeaster = tuple(sorted(byeaster)) 

564 

565 self._original_rule['byeaster'] = self._byeaster 

566 else: 

567 self._byeaster = None 

568 

569 # bymonthday 

570 if bymonthday is None: 

571 self._bymonthday = () 

572 self._bynmonthday = () 

573 else: 

574 if isinstance(bymonthday, int): 

575 bymonthday = (bymonthday,) 

576 

577 bymonthday = set(bymonthday) # Ensure it's unique 

578 

579 self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) 

580 self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) 

581 

582 # Storing positive numbers first, then negative numbers 

583 if 'bymonthday' not in self._original_rule: 

584 self._original_rule['bymonthday'] = tuple( 

585 itertools.chain(self._bymonthday, self._bynmonthday)) 

586 

587 # byweekno 

588 if byweekno is None: 

589 self._byweekno = None 

590 else: 

591 if isinstance(byweekno, int): 

592 byweekno = (byweekno,) 

593 

594 self._byweekno = tuple(sorted(set(byweekno))) 

595 

596 self._original_rule['byweekno'] = self._byweekno 

597 

598 # byweekday / bynweekday 

599 if byweekday is None: 

600 self._byweekday = None 

601 self._bynweekday = None 

602 else: 

603 # If it's one of the valid non-sequence types, convert to a 

604 # single-element sequence before the iterator that builds the 

605 # byweekday set. 

606 if isinstance(byweekday, int) or hasattr(byweekday, "n"): 

607 byweekday = (byweekday,) 

608 

609 self._byweekday = set() 

610 self._bynweekday = set() 

611 for wday in byweekday: 

612 if isinstance(wday, int): 

613 self._byweekday.add(wday) 

614 elif not wday.n or freq > MONTHLY: 

615 self._byweekday.add(wday.weekday) 

616 else: 

617 self._bynweekday.add((wday.weekday, wday.n)) 

618 

619 if not self._byweekday: 

620 self._byweekday = None 

621 elif not self._bynweekday: 

622 self._bynweekday = None 

623 

624 if self._byweekday is not None: 

625 self._byweekday = tuple(sorted(self._byweekday)) 

626 orig_byweekday = [weekday(x) for x in self._byweekday] 

627 else: 

628 orig_byweekday = () 

629 

630 if self._bynweekday is not None: 

631 self._bynweekday = tuple(sorted(self._bynweekday)) 

632 orig_bynweekday = [weekday(*x) for x in self._bynweekday] 

633 else: 

634 orig_bynweekday = () 

635 

636 if 'byweekday' not in self._original_rule: 

637 self._original_rule['byweekday'] = tuple(itertools.chain( 

638 orig_byweekday, orig_bynweekday)) 

639 

640 # byhour 

641 if byhour is None: 

642 if freq < HOURLY: 

643 self._byhour = {dtstart.hour} 

644 else: 

645 self._byhour = None 

646 else: 

647 if isinstance(byhour, int): 

648 byhour = (byhour,) 

649 

650 if freq == HOURLY: 

651 self._byhour = self.__construct_byset(start=dtstart.hour, 

652 byxxx=byhour, 

653 base=24) 

654 else: 

655 self._byhour = set(byhour) 

656 

657 self._byhour = tuple(sorted(self._byhour)) 

658 self._original_rule['byhour'] = self._byhour 

659 

660 # byminute 

661 if byminute is None: 

662 if freq < MINUTELY: 

663 self._byminute = {dtstart.minute} 

664 else: 

665 self._byminute = None 

666 else: 

667 if isinstance(byminute, int): 

668 byminute = (byminute,) 

669 

670 if freq == MINUTELY: 

671 self._byminute = self.__construct_byset(start=dtstart.minute, 

672 byxxx=byminute, 

673 base=60) 

674 else: 

675 self._byminute = set(byminute) 

676 

677 self._byminute = tuple(sorted(self._byminute)) 

678 self._original_rule['byminute'] = self._byminute 

679 

680 # bysecond 

681 if bysecond is None: 

682 if freq < SECONDLY: 

683 self._bysecond = ((dtstart.second,)) 

684 else: 

685 self._bysecond = None 

686 else: 

687 if isinstance(bysecond, int): 

688 bysecond = (bysecond,) 

689 

690 self._bysecond = set(bysecond) 

691 

692 if freq == SECONDLY: 

693 self._bysecond = self.__construct_byset(start=dtstart.second, 

694 byxxx=bysecond, 

695 base=60) 

696 else: 

697 self._bysecond = set(bysecond) 

698 

699 self._bysecond = tuple(sorted(self._bysecond)) 

700 self._original_rule['bysecond'] = self._bysecond 

701 

702 if self._freq >= HOURLY: 

703 self._timeset = None 

704 else: 

705 self._timeset = [] 

706 for hour in self._byhour: 

707 for minute in self._byminute: 

708 for second in self._bysecond: 

709 self._timeset.append( 

710 datetime.time(hour, minute, second, 

711 tzinfo=self._tzinfo)) 

712 self._timeset.sort() 

713 self._timeset = tuple(self._timeset) 

714 

715 def __str__(self): 

716 """ 

717 Output a string that would generate this RRULE if passed to rrulestr. 

718 This is mostly compatible with RFC5545, except for the 

719 dateutil-specific extension BYEASTER. 

720 """ 

721 

722 output = [] 

723 h, m, s = [None] * 3 

724 if self._dtstart: 

725 output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) 

726 h, m, s = self._dtstart.timetuple()[3:6] 

727 

728 parts = ['FREQ=' + FREQNAMES[self._freq]] 

729 if self._interval != 1: 

730 parts.append('INTERVAL=' + str(self._interval)) 

731 

732 if self._wkst: 

733 parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) 

734 

735 if self._count is not None: 

736 parts.append('COUNT=' + str(self._count)) 

737 

738 if self._until: 

739 parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) 

740 

741 if self._original_rule.get('byweekday') is not None: 

742 # The str() method on weekday objects doesn't generate 

743 # RFC5545-compliant strings, so we should modify that. 

744 original_rule = dict(self._original_rule) 

745 wday_strings = [] 

746 for wday in original_rule['byweekday']: 

747 if wday.n: 

748 wday_strings.append('{n:+d}{wday}'.format( 

749 n=wday.n, 

750 wday=repr(wday)[0:2])) 

751 else: 

752 wday_strings.append(repr(wday)) 

753 

754 original_rule['byweekday'] = wday_strings 

755 else: 

756 original_rule = self._original_rule 

757 

758 partfmt = '{name}={vals}' 

759 for name, key in [('BYSETPOS', 'bysetpos'), 

760 ('BYMONTH', 'bymonth'), 

761 ('BYMONTHDAY', 'bymonthday'), 

762 ('BYYEARDAY', 'byyearday'), 

763 ('BYWEEKNO', 'byweekno'), 

764 ('BYDAY', 'byweekday'), 

765 ('BYHOUR', 'byhour'), 

766 ('BYMINUTE', 'byminute'), 

767 ('BYSECOND', 'bysecond'), 

768 ('BYEASTER', 'byeaster')]: 

769 value = original_rule.get(key) 

770 if value: 

771 parts.append(partfmt.format(name=name, vals=(','.join(str(v) 

772 for v in value)))) 

773 

774 output.append('RRULE:' + ';'.join(parts)) 

775 return '\n'.join(output) 

776 

777 def replace(self, **kwargs): 

778 """Return new rrule with same attributes except for those attributes given new 

779 values by whichever keyword arguments are specified.""" 

780 new_kwargs = {"interval": self._interval, 

781 "count": self._count, 

782 "dtstart": self._dtstart, 

783 "freq": self._freq, 

784 "until": self._until, 

785 "wkst": self._wkst, 

786 "cache": False if self._cache is None else True } 

787 new_kwargs.update(self._original_rule) 

788 new_kwargs.update(kwargs) 

789 return rrule(**new_kwargs) 

790 

791 def _iter(self): 

792 year, month, day, hour, minute, second, weekday, yearday, _ = \ 

793 self._dtstart.timetuple() 

794 

795 # Some local variables to speed things up a bit 

796 freq = self._freq 

797 interval = self._interval 

798 wkst = self._wkst 

799 until = self._until 

800 bymonth = self._bymonth 

801 byweekno = self._byweekno 

802 byyearday = self._byyearday 

803 byweekday = self._byweekday 

804 byeaster = self._byeaster 

805 bymonthday = self._bymonthday 

806 bynmonthday = self._bynmonthday 

807 bysetpos = self._bysetpos 

808 byhour = self._byhour 

809 byminute = self._byminute 

810 bysecond = self._bysecond 

811 

812 ii = _iterinfo(self) 

813 ii.rebuild(year, month) 

814 

815 getdayset = {YEARLY: ii.ydayset, 

816 MONTHLY: ii.mdayset, 

817 WEEKLY: ii.wdayset, 

818 DAILY: ii.ddayset, 

819 HOURLY: ii.ddayset, 

820 MINUTELY: ii.ddayset, 

821 SECONDLY: ii.ddayset}[freq] 

822 

823 if freq < HOURLY: 

824 timeset = self._timeset 

825 else: 

826 gettimeset = {HOURLY: ii.htimeset, 

827 MINUTELY: ii.mtimeset, 

828 SECONDLY: ii.stimeset}[freq] 

829 if ((freq >= HOURLY and 

830 self._byhour and hour not in self._byhour) or 

831 (freq >= MINUTELY and 

832 self._byminute and minute not in self._byminute) or 

833 (freq >= SECONDLY and 

834 self._bysecond and second not in self._bysecond)): 

835 timeset = () 

836 else: 

837 timeset = gettimeset(hour, minute, second) 

838 

839 total = 0 

840 count = self._count 

841 while True: 

842 # Get dayset with the right frequency 

843 dayset, start, end = getdayset(year, month, day) 

844 

845 # Do the "hard" work ;-) 

846 filtered = False 

847 for i in dayset[start:end]: 

848 if ((bymonth and ii.mmask[i] not in bymonth) or 

849 (byweekno and not ii.wnomask[i]) or 

850 (byweekday and ii.wdaymask[i] not in byweekday) or 

851 (ii.nwdaymask and not ii.nwdaymask[i]) or 

852 (byeaster and not ii.eastermask[i]) or 

853 ((bymonthday or bynmonthday) and 

854 ii.mdaymask[i] not in bymonthday and 

855 ii.nmdaymask[i] not in bynmonthday) or 

856 (byyearday and 

857 ((i < ii.yearlen and i+1 not in byyearday and 

858 -ii.yearlen+i not in byyearday) or 

859 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and 

860 -ii.nextyearlen+i-ii.yearlen not in byyearday)))): 

861 dayset[i] = None 

862 filtered = True 

863 

864 # Output results 

865 if bysetpos and timeset: 

866 poslist = [] 

867 for pos in bysetpos: 

868 if pos < 0: 

869 daypos, timepos = divmod(pos, len(timeset)) 

870 else: 

871 daypos, timepos = divmod(pos-1, len(timeset)) 

872 try: 

873 i = [x for x in dayset[start:end] 

874 if x is not None][daypos] 

875 time = timeset[timepos] 

876 except IndexError: 

877 pass 

878 else: 

879 date = datetime.date.fromordinal(ii.yearordinal+i) 

880 res = datetime.datetime.combine(date, time) 

881 if res not in poslist: 

882 poslist.append(res) 

883 poslist.sort() 

884 for res in poslist: 

885 if until and res > until: 

886 self._len = total 

887 return 

888 elif res >= self._dtstart: 

889 if count is not None: 

890 count -= 1 

891 if count < 0: 

892 self._len = total 

893 return 

894 total += 1 

895 yield res 

896 else: 

897 for i in dayset[start:end]: 

898 if i is not None: 

899 date = datetime.date.fromordinal(ii.yearordinal + i) 

900 for time in timeset: 

901 res = datetime.datetime.combine(date, time) 

902 if until and res > until: 

903 self._len = total 

904 return 

905 elif res >= self._dtstart: 

906 if count is not None: 

907 count -= 1 

908 if count < 0: 

909 self._len = total 

910 return 

911 

912 total += 1 

913 yield res 

914 

915 # Handle frequency and interval 

916 fixday = False 

917 if freq == YEARLY: 

918 year += interval 

919 if year > datetime.MAXYEAR: 

920 self._len = total 

921 return 

922 ii.rebuild(year, month) 

923 elif freq == MONTHLY: 

924 month += interval 

925 if month > 12: 

926 div, mod = divmod(month, 12) 

927 month = mod 

928 year += div 

929 if month == 0: 

930 month = 12 

931 year -= 1 

932 if year > datetime.MAXYEAR: 

933 self._len = total 

934 return 

935 ii.rebuild(year, month) 

936 elif freq == WEEKLY: 

937 if wkst > weekday: 

938 day += -(weekday+1+(6-wkst))+self._interval*7 

939 else: 

940 day += -(weekday-wkst)+self._interval*7 

941 weekday = wkst 

942 fixday = True 

943 elif freq == DAILY: 

944 day += interval 

945 fixday = True 

946 elif freq == HOURLY: 

947 if filtered: 

948 # Jump to one iteration before next day 

949 hour += ((23-hour)//interval)*interval 

950 

951 if byhour: 

952 ndays, hour = self.__mod_distance(value=hour, 

953 byxxx=self._byhour, 

954 base=24) 

955 else: 

956 ndays, hour = divmod(hour+interval, 24) 

957 

958 if ndays: 

959 day += ndays 

960 fixday = True 

961 

962 timeset = gettimeset(hour, minute, second) 

963 elif freq == MINUTELY: 

964 if filtered: 

965 # Jump to one iteration before next day 

966 minute += ((1439-(hour*60+minute))//interval)*interval 

967 

968 valid = False 

969 rep_rate = (24*60) 

970 for j in range(rep_rate // math.gcd(interval, rep_rate)): 

971 if byminute: 

972 nhours, minute = \ 

973 self.__mod_distance(value=minute, 

974 byxxx=self._byminute, 

975 base=60) 

976 else: 

977 nhours, minute = divmod(minute+interval, 60) 

978 

979 div, hour = divmod(hour+nhours, 24) 

980 if div: 

981 day += div 

982 fixday = True 

983 filtered = False 

984 

985 if not byhour or hour in byhour: 

986 valid = True 

987 break 

988 

989 if not valid: 

990 raise ValueError('Invalid combination of interval and ' + 

991 'byhour resulting in empty rule.') 

992 

993 timeset = gettimeset(hour, minute, second) 

994 elif freq == SECONDLY: 

995 if filtered: 

996 # Jump to one iteration before next day 

997 second += (((86399 - (hour * 3600 + minute * 60 + second)) 

998 // interval) * interval) 

999 

1000 rep_rate = (24 * 3600) 

1001 valid = False 

1002 for j in range(0, rep_rate // math.gcd(interval, rep_rate)): 

1003 if bysecond: 

1004 nminutes, second = \ 

1005 self.__mod_distance(value=second, 

1006 byxxx=self._bysecond, 

1007 base=60) 

1008 else: 

1009 nminutes, second = divmod(second+interval, 60) 

1010 

1011 div, minute = divmod(minute+nminutes, 60) 

1012 if div: 

1013 hour += div 

1014 div, hour = divmod(hour, 24) 

1015 if div: 

1016 day += div 

1017 fixday = True 

1018 

1019 if ((not byhour or hour in byhour) and 

1020 (not byminute or minute in byminute) and 

1021 (not bysecond or second in bysecond)): 

1022 valid = True 

1023 break 

1024 

1025 if not valid: 

1026 raise ValueError('Invalid combination of interval, ' + 

1027 'byhour and byminute resulting in empty' + 

1028 ' rule.') 

1029 

1030 timeset = gettimeset(hour, minute, second) 

1031 

1032 if fixday and day > 28: 

1033 daysinmonth = calendar.monthrange(year, month)[1] 

1034 if day > daysinmonth: 

1035 while day > daysinmonth: 

1036 day -= daysinmonth 

1037 month += 1 

1038 if month == 13: 

1039 month = 1 

1040 year += 1 

1041 if year > datetime.MAXYEAR: 

1042 self._len = total 

1043 return 

1044 daysinmonth = calendar.monthrange(year, month)[1] 

1045 ii.rebuild(year, month) 

1046 

1047 def __construct_byset(self, start, byxxx, base): 

1048 """ 

1049 If a `BYXXX` sequence is passed to the constructor at the same level as 

1050 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some 

1051 specifications which cannot be reached given some starting conditions. 

1052 

1053 This occurs whenever the interval is not coprime with the base of a 

1054 given unit and the difference between the starting position and the 

1055 ending position is not coprime with the greatest common denominator 

1056 between the interval and the base. For example, with a FREQ of hourly 

1057 starting at 17:00 and an interval of 4, the only valid values for 

1058 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not 

1059 coprime. 

1060 

1061 :param start: 

1062 Specifies the starting position. 

1063 :param byxxx: 

1064 An iterable containing the list of allowed values. 

1065 :param base: 

1066 The largest allowable value for the specified frequency (e.g. 

1067 24 hours, 60 minutes). 

1068 

1069 This does not preserve the type of the iterable, returning a set, since 

1070 the values should be unique and the order is irrelevant, this will 

1071 speed up later lookups. 

1072 

1073 In the event of an empty set, raises a :exception:`ValueError`, as this 

1074 results in an empty rrule. 

1075 """ 

1076 

1077 cset = set() 

1078 

1079 # Support a single byxxx value. 

1080 if isinstance(byxxx, int): 

1081 byxxx = (byxxx, ) 

1082 

1083 for num in byxxx: 

1084 i_gcd = math.gcd(self._interval, base) 

1085 # Use divmod rather than % because we need to wrap negative nums. 

1086 if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: 

1087 cset.add(num) 

1088 

1089 if len(cset) == 0: 

1090 raise ValueError("Invalid rrule byxxx generates an empty set.") 

1091 

1092 return cset 

1093 

1094 def __mod_distance(self, value, byxxx, base): 

1095 """ 

1096 Calculates the next value in a sequence where the `FREQ` parameter is 

1097 specified along with a `BYXXX` parameter at the same "level" 

1098 (e.g. `HOURLY` specified with `BYHOUR`). 

1099 

1100 :param value: 

1101 The old value of the component. 

1102 :param byxxx: 

1103 The `BYXXX` set, which should have been generated by 

1104 `rrule._construct_byset`, or something else which checks that a 

1105 valid rule is present. 

1106 :param base: 

1107 The largest allowable value for the specified frequency (e.g. 

1108 24 hours, 60 minutes). 

1109 

1110 If a valid value is not found after `base` iterations (the maximum 

1111 number before the sequence would start to repeat), this raises a 

1112 :exception:`ValueError`, as no valid values were found. 

1113 

1114 This returns a tuple of `divmod(n*interval, base)`, where `n` is the 

1115 smallest number of `interval` repetitions until the next specified 

1116 value in `byxxx` is found. 

1117 """ 

1118 accumulator = 0 

1119 for ii in range(1, base + 1): 

1120 # Using divmod() over % to account for negative intervals 

1121 div, value = divmod(value + self._interval, base) 

1122 accumulator += div 

1123 if value in byxxx: 

1124 return (accumulator, value) 

1125 

1126 

1127class _iterinfo: 

1128 __slots__ = ["rrule", "lastyear", "lastmonth", 

1129 "yearlen", "nextyearlen", "yearordinal", "yearweekday", 

1130 "mmask", "mrange", "mdaymask", "nmdaymask", 

1131 "wdaymask", "wnomask", "nwdaymask", "eastermask"] 

1132 

1133 def __init__(self, rrule): 

1134 for attr in self.__slots__: 

1135 setattr(self, attr, None) 

1136 self.rrule = rrule 

1137 

1138 def rebuild(self, year, month): 

1139 # Every mask is 7 days longer to handle cross-year weekly periods. 

1140 rr = self.rrule 

1141 if year != self.lastyear: 

1142 self.yearlen = 365 + calendar.isleap(year) 

1143 self.nextyearlen = 365 + calendar.isleap(year + 1) 

1144 firstyday = datetime.date(year, 1, 1) 

1145 self.yearordinal = firstyday.toordinal() 

1146 self.yearweekday = firstyday.weekday() 

1147 

1148 wday = datetime.date(year, 1, 1).weekday() 

1149 if self.yearlen == 365: 

1150 self.mmask = M365MASK 

1151 self.mdaymask = MDAY365MASK 

1152 self.nmdaymask = NMDAY365MASK 

1153 self.wdaymask = WDAYMASK[wday:] 

1154 self.mrange = M365RANGE 

1155 else: 

1156 self.mmask = M366MASK 

1157 self.mdaymask = MDAY366MASK 

1158 self.nmdaymask = NMDAY366MASK 

1159 self.wdaymask = WDAYMASK[wday:] 

1160 self.mrange = M366RANGE 

1161 

1162 if not rr._byweekno: 

1163 self.wnomask = None 

1164 else: 

1165 self.wnomask = [0]*(self.yearlen+7) 

1166 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) 

1167 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 

1168 if no1wkst >= 4: 

1169 no1wkst = 0 

1170 # Number of days in the year, plus the days we got 

1171 # from last year. 

1172 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 

1173 else: 

1174 # Number of days in the year, minus the days we 

1175 # left in last year. 

1176 wyearlen = self.yearlen-no1wkst 

1177 div, mod = divmod(wyearlen, 7) 

1178 numweeks = div+mod//4 

1179 for n in rr._byweekno: 

1180 if n < 0: 

1181 n += numweeks+1 

1182 if not (0 < n <= numweeks): 

1183 continue 

1184 if n > 1: 

1185 i = no1wkst+(n-1)*7 

1186 if no1wkst != firstwkst: 

1187 i -= 7-firstwkst 

1188 else: 

1189 i = no1wkst 

1190 for j in range(7): 

1191 self.wnomask[i] = 1 

1192 i += 1 

1193 if self.wdaymask[i] == rr._wkst: 

1194 break 

1195 if 1 in rr._byweekno: 

1196 # Check week number 1 of next year as well 

1197 # TODO: Check -numweeks for next year. 

1198 i = no1wkst+numweeks*7 

1199 if no1wkst != firstwkst: 

1200 i -= 7-firstwkst 

1201 if i < self.yearlen: 

1202 # If week starts in next year, we 

1203 # don't care about it. 

1204 for j in range(7): 

1205 self.wnomask[i] = 1 

1206 i += 1 

1207 if self.wdaymask[i] == rr._wkst: 

1208 break 

1209 if no1wkst: 

1210 # Check last week number of last year as 

1211 # well. If no1wkst is 0, either the year 

1212 # started on week start, or week number 1 

1213 # got days from last year, so there are no 

1214 # days from last year's last week number in 

1215 # this year. 

1216 if -1 not in rr._byweekno: 

1217 lyearweekday = datetime.date(year-1, 1, 1).weekday() 

1218 lno1wkst = (7-lyearweekday+rr._wkst) % 7 

1219 lyearlen = 365+calendar.isleap(year-1) 

1220 if lno1wkst >= 4: 

1221 lno1wkst = 0 

1222 lnumweeks = 52+(lyearlen + 

1223 (lyearweekday-rr._wkst) % 7) % 7//4 

1224 else: 

1225 lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 

1226 else: 

1227 lnumweeks = -1 

1228 if lnumweeks in rr._byweekno: 

1229 for i in range(no1wkst): 

1230 self.wnomask[i] = 1 

1231 

1232 if (rr._bynweekday and (month != self.lastmonth or 

1233 year != self.lastyear)): 

1234 ranges = [] 

1235 if rr._freq == YEARLY: 

1236 if rr._bymonth: 

1237 for month in rr._bymonth: 

1238 ranges.append(self.mrange[month-1:month+1]) 

1239 else: 

1240 ranges = [(0, self.yearlen)] 

1241 elif rr._freq == MONTHLY: 

1242 ranges = [self.mrange[month-1:month+1]] 

1243 if ranges: 

1244 # Weekly frequency won't get here, so we may not 

1245 # care about cross-year weekly periods. 

1246 self.nwdaymask = [0]*self.yearlen 

1247 for first, last in ranges: 

1248 last -= 1 

1249 for wday, n in rr._bynweekday: 

1250 if n < 0: 

1251 i = last+(n+1)*7 

1252 i -= (self.wdaymask[i]-wday) % 7 

1253 else: 

1254 i = first+(n-1)*7 

1255 i += (7-self.wdaymask[i]+wday) % 7 

1256 if first <= i <= last: 

1257 self.nwdaymask[i] = 1 

1258 

1259 if rr._byeaster: 

1260 self.eastermask = [0]*(self.yearlen+7) 

1261 eyday = easter.easter(year).toordinal()-self.yearordinal 

1262 for offset in rr._byeaster: 

1263 self.eastermask[eyday+offset] = 1 

1264 

1265 self.lastyear = year 

1266 self.lastmonth = month 

1267 

1268 def ydayset(self, year, month, day): 

1269 return list(range(self.yearlen)), 0, self.yearlen 

1270 

1271 def mdayset(self, year, month, day): 

1272 dset = [None]*self.yearlen 

1273 start, end = self.mrange[month-1:month+1] 

1274 for i in range(start, end): 

1275 dset[i] = i 

1276 return dset, start, end 

1277 

1278 def wdayset(self, year, month, day): 

1279 # We need to handle cross-year weeks here. 

1280 dset = [None]*(self.yearlen+7) 

1281 i = datetime.date(year, month, day).toordinal()-self.yearordinal 

1282 start = i 

1283 for j in range(7): 

1284 dset[i] = i 

1285 i += 1 

1286 # if (not (0 <= i < self.yearlen) or 

1287 # self.wdaymask[i] == self.rrule._wkst): 

1288 # This will cross the year boundary, if necessary. 

1289 if self.wdaymask[i] == self.rrule._wkst: 

1290 break 

1291 return dset, start, i 

1292 

1293 def ddayset(self, year, month, day): 

1294 dset = [None] * self.yearlen 

1295 i = datetime.date(year, month, day).toordinal() - self.yearordinal 

1296 dset[i] = i 

1297 return dset, i, i + 1 

1298 

1299 def htimeset(self, hour, minute, second): 

1300 tset = [] 

1301 rr = self.rrule 

1302 for minute in rr._byminute: 

1303 for second in rr._bysecond: 

1304 tset.append(datetime.time(hour, minute, second, 

1305 tzinfo=rr._tzinfo)) 

1306 tset.sort() 

1307 return tset 

1308 

1309 def mtimeset(self, hour, minute, second): 

1310 tset = [] 

1311 rr = self.rrule 

1312 for second in rr._bysecond: 

1313 tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) 

1314 tset.sort() 

1315 return tset 

1316 

1317 def stimeset(self, hour, minute, second): 

1318 return (datetime.time(hour, minute, second, 

1319 tzinfo=self.rrule._tzinfo),) 

1320 

1321 

1322class rruleset(rrulebase): 

1323 """ The rruleset type allows more complex recurrence setups, mixing 

1324 multiple rules, dates, exclusion rules, and exclusion dates. The type 

1325 constructor takes the following keyword arguments: 

1326 

1327 :param cache: If True, caching of results will be enabled, improving 

1328 performance of multiple queries considerably. """ 

1329 

1330 class _genitem: 

1331 def __init__(self, genlist, gen): 

1332 try: 

1333 self.dt = next(gen) 

1334 genlist.append(self) 

1335 except StopIteration: 

1336 pass 

1337 self.genlist = genlist 

1338 self.gen = gen 

1339 

1340 def __next__(self): 

1341 try: 

1342 self.dt = next(self.gen) 

1343 except StopIteration: 

1344 if self.genlist[0] is self: 

1345 heapq.heappop(self.genlist) 

1346 else: 

1347 self.genlist.remove(self) 

1348 heapq.heapify(self.genlist) 

1349 

1350 

1351 def __lt__(self, other): 

1352 return self.dt < other.dt 

1353 

1354 def __gt__(self, other): 

1355 return self.dt > other.dt 

1356 

1357 def __eq__(self, other): 

1358 return self.dt == other.dt 

1359 

1360 def __ne__(self, other): 

1361 return self.dt != other.dt 

1362 

1363 def __init__(self, cache=False): 

1364 super().__init__(cache) 

1365 self._rrule = [] 

1366 self._rdate = [] 

1367 self._exrule = [] 

1368 self._exdate = [] 

1369 

1370 @_invalidates_cache 

1371 def rrule(self, rrule): 

1372 """ Include the given :py:class:`rrule` instance in the recurrence set 

1373 generation. """ 

1374 self._rrule.append(rrule) 

1375 

1376 @_invalidates_cache 

1377 def rdate(self, rdate): 

1378 """ Include the given :py:class:`datetime` instance in the recurrence 

1379 set generation. """ 

1380 self._rdate.append(rdate) 

1381 

1382 @_invalidates_cache 

1383 def exrule(self, exrule): 

1384 """ Include the given rrule instance in the recurrence set exclusion 

1385 list. Dates which are part of the given recurrence rules will not 

1386 be generated, even if some inclusive rrule or rdate matches them. 

1387 """ 

1388 self._exrule.append(exrule) 

1389 

1390 @_invalidates_cache 

1391 def exdate(self, exdate): 

1392 """ Include the given datetime instance in the recurrence set 

1393 exclusion list. Dates included that way will not be generated, 

1394 even if some inclusive rrule or rdate matches them. """ 

1395 self._exdate.append(exdate) 

1396 

1397 def _iter(self): 

1398 rlist = [] 

1399 self._rdate.sort() 

1400 self._genitem(rlist, iter(self._rdate)) 

1401 for gen in [iter(x) for x in self._rrule]: 

1402 self._genitem(rlist, gen) 

1403 exlist = [] 

1404 self._exdate.sort() 

1405 self._genitem(exlist, iter(self._exdate)) 

1406 for gen in [iter(x) for x in self._exrule]: 

1407 self._genitem(exlist, gen) 

1408 lastdt = None 

1409 total = 0 

1410 heapq.heapify(rlist) 

1411 heapq.heapify(exlist) 

1412 while rlist: 

1413 ritem = rlist[0] 

1414 if not lastdt or lastdt != ritem.dt: 

1415 while exlist and exlist[0] < ritem: 

1416 exitem = exlist[0] 

1417 next(exitem) 

1418 if exlist and exlist[0] is exitem: 

1419 heapq.heapreplace(exlist, exitem) 

1420 if not exlist or ritem != exlist[0]: 

1421 total += 1 

1422 yield ritem.dt 

1423 lastdt = ritem.dt 

1424 next(ritem) 

1425 if rlist and rlist[0] is ritem: 

1426 heapq.heapreplace(rlist, ritem) 

1427 self._len = total 

1428 

1429 

1430class _rrulestr: 

1431 """ Parses a string representation of a recurrence rule or set of 

1432 recurrence rules. 

1433 

1434 :param s: 

1435 Required, a string defining one or more recurrence rules. 

1436 

1437 :param dtstart: 

1438 If given, used as the default recurrence start if not specified in the 

1439 rule string. 

1440 

1441 :param cache: 

1442 If set ``True`` caching of results will be enabled, improving 

1443 performance of multiple queries considerably. 

1444 

1445 :param unfold: 

1446 If set ``True`` indicates that a rule string is split over more 

1447 than one line and should be joined before processing. 

1448 

1449 :param forceset: 

1450 If set ``True`` forces a :class:`dateutil.rrule.rruleset` to 

1451 be returned. 

1452 

1453 :param compatible: 

1454 If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. 

1455 

1456 :param ignoretz: 

1457 If set ``True``, time zones in parsed strings are ignored and a naive 

1458 :class:`datetime.datetime` object is returned. 

1459 

1460 :param tzids: 

1461 If given, a callable or mapping used to retrieve a 

1462 :class:`datetime.tzinfo` from a string representation. 

1463 Defaults to :func:`dateutil.tz.gettz`. 

1464 

1465 :param tzinfos: 

1466 Additional time zone names / aliases which may be present in a string 

1467 representation. See :func:`dateutil.parser.parse` for more 

1468 information. 

1469 

1470 :return: 

1471 Returns a :class:`dateutil.rrule.rruleset` or 

1472 :class:`dateutil.rrule.rrule` 

1473 """ 

1474 

1475 _freq_map = {"YEARLY": YEARLY, 

1476 "MONTHLY": MONTHLY, 

1477 "WEEKLY": WEEKLY, 

1478 "DAILY": DAILY, 

1479 "HOURLY": HOURLY, 

1480 "MINUTELY": MINUTELY, 

1481 "SECONDLY": SECONDLY} 

1482 

1483 _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, 

1484 "FR": 4, "SA": 5, "SU": 6} 

1485 

1486 def _handle_int(self, rrkwargs, name, value, **kwargs): 

1487 rrkwargs[name.lower()] = int(value) 

1488 

1489 def _handle_int_list(self, rrkwargs, name, value, **kwargs): 

1490 rrkwargs[name.lower()] = [int(x) for x in value.split(',')] 

1491 

1492 _handle_INTERVAL = _handle_int 

1493 _handle_COUNT = _handle_int 

1494 _handle_BYSETPOS = _handle_int_list 

1495 _handle_BYMONTH = _handle_int_list 

1496 _handle_BYMONTHDAY = _handle_int_list 

1497 _handle_BYYEARDAY = _handle_int_list 

1498 _handle_BYEASTER = _handle_int_list 

1499 _handle_BYWEEKNO = _handle_int_list 

1500 _handle_BYHOUR = _handle_int_list 

1501 _handle_BYMINUTE = _handle_int_list 

1502 _handle_BYSECOND = _handle_int_list 

1503 

1504 def _handle_FREQ(self, rrkwargs, name, value, **kwargs): 

1505 rrkwargs["freq"] = self._freq_map[value] 

1506 

1507 def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): 

1508 global parser 

1509 if not parser: 

1510 from dateutil import parser 

1511 try: 

1512 rrkwargs["until"] = parser.parse(value, 

1513 ignoretz=kwargs.get("ignoretz"), 

1514 tzinfos=kwargs.get("tzinfos")) 

1515 except ValueError: 

1516 raise ValueError("invalid until date") 

1517 

1518 def _handle_WKST(self, rrkwargs, name, value, **kwargs): 

1519 rrkwargs["wkst"] = self._weekday_map[value] 

1520 

1521 def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): 

1522 """ 

1523 Two ways to specify this: +1MO or MO(+1) 

1524 """ 

1525 l = [] 

1526 for wday in value.split(','): 

1527 if '(' in wday: 

1528 # If it's of the form TH(+1), etc. 

1529 splt = wday.split('(') 

1530 w = splt[0] 

1531 n = int(splt[1][:-1]) 

1532 elif len(wday): 

1533 # If it's of the form +1MO 

1534 for i in range(len(wday)): 

1535 if wday[i] not in '+-0123456789': 

1536 break 

1537 n = wday[:i] or None 

1538 w = wday[i:] 

1539 if n: 

1540 n = int(n) 

1541 else: 

1542 raise ValueError("Invalid (empty) BYDAY specification.") 

1543 

1544 l.append(weekdays[self._weekday_map[w]](n)) 

1545 rrkwargs["byweekday"] = l 

1546 

1547 _handle_BYDAY = _handle_BYWEEKDAY 

1548 

1549 def _parse_rfc_rrule(self, line, 

1550 dtstart=None, 

1551 cache=False, 

1552 ignoretz=False, 

1553 tzinfos=None): 

1554 if line.find(':') != -1: 

1555 name, value = line.split(':') 

1556 if name != "RRULE": 

1557 raise ValueError("unknown parameter name") 

1558 else: 

1559 value = line 

1560 rrkwargs = {} 

1561 for pair in value.split(';'): 

1562 name, value = pair.split('=') 

1563 name = name.upper() 

1564 value = value.upper() 

1565 try: 

1566 getattr(self, "_handle_"+name)(rrkwargs, name, value, 

1567 ignoretz=ignoretz, 

1568 tzinfos=tzinfos) 

1569 except AttributeError: 

1570 raise ValueError("unknown parameter '%s'" % name) 

1571 except (KeyError, ValueError): 

1572 raise ValueError("invalid '%s': %s" % (name, value)) 

1573 return rrule(dtstart=dtstart, cache=cache, **rrkwargs) 

1574 

1575 def _parse_date_value(self, date_value, parms, rule_tzids, 

1576 ignoretz, tzids, tzinfos): 

1577 global parser 

1578 if not parser: 

1579 from dateutil import parser 

1580 

1581 datevals = [] 

1582 value_found = False 

1583 TZID = None 

1584 

1585 for parm in parms: 

1586 if parm.startswith("TZID="): 

1587 try: 

1588 tzkey = rule_tzids[parm.split('TZID=')[-1]] 

1589 except KeyError: 

1590 continue 

1591 if tzids is None: 

1592 from . import tz 

1593 tzlookup = tz.gettz 

1594 elif callable(tzids): 

1595 tzlookup = tzids 

1596 else: 

1597 tzlookup = getattr(tzids, 'get', None) 

1598 if tzlookup is None: 

1599 msg = ('tzids must be a callable, mapping, or None, ' 

1600 'not %s' % tzids) 

1601 raise ValueError(msg) 

1602 

1603 TZID = tzlookup(tzkey) 

1604 continue 

1605 

1606 # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found 

1607 # only once. 

1608 if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: 

1609 raise ValueError("unsupported parm: " + parm) 

1610 else: 

1611 if value_found: 

1612 msg = ("Duplicate value parameter found in: " + parm) 

1613 raise ValueError(msg) 

1614 value_found = True 

1615 

1616 for datestr in date_value.split(','): 

1617 date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) 

1618 if TZID is not None: 

1619 if date.tzinfo is None: 

1620 date = date.replace(tzinfo=TZID) 

1621 else: 

1622 raise ValueError('DTSTART/EXDATE specifies multiple timezone') 

1623 datevals.append(date) 

1624 

1625 return datevals 

1626 

1627 def _parse_rfc(self, s, 

1628 dtstart=None, 

1629 cache=False, 

1630 unfold=False, 

1631 forceset=False, 

1632 compatible=False, 

1633 ignoretz=False, 

1634 tzids=None, 

1635 tzinfos=None): 

1636 global parser 

1637 if compatible: 

1638 forceset = True 

1639 unfold = True 

1640 

1641 TZID_NAMES = dict(map( 

1642 lambda x: (x.upper(), x), 

1643 re.findall('TZID=(?P<name>[^:]+):', s) 

1644 )) 

1645 s = s.upper() 

1646 if not s.strip(): 

1647 raise ValueError("empty string") 

1648 if unfold: 

1649 lines = s.splitlines() 

1650 i = 0 

1651 while i < len(lines): 

1652 line = lines[i].rstrip() 

1653 if not line: 

1654 del lines[i] 

1655 elif i > 0 and line[0] == " ": 

1656 lines[i-1] += line[1:] 

1657 del lines[i] 

1658 else: 

1659 i += 1 

1660 else: 

1661 lines = s.split() 

1662 if (not forceset and len(lines) == 1 and (s.find(':') == -1 or 

1663 s.startswith('RRULE:'))): 

1664 return self._parse_rfc_rrule(lines[0], cache=cache, 

1665 dtstart=dtstart, ignoretz=ignoretz, 

1666 tzinfos=tzinfos) 

1667 else: 

1668 rrulevals = [] 

1669 rdatevals = [] 

1670 exrulevals = [] 

1671 exdatevals = [] 

1672 for line in lines: 

1673 if not line: 

1674 continue 

1675 if line.find(':') == -1: 

1676 name = "RRULE" 

1677 value = line 

1678 else: 

1679 name, value = line.split(':', 1) 

1680 parms = name.split(';') 

1681 if not parms: 

1682 raise ValueError("empty property name") 

1683 name = parms[0] 

1684 parms = parms[1:] 

1685 if name == "RRULE": 

1686 for parm in parms: 

1687 if parm.startswith("X-"): 

1688 # Experimental random vendor features; safe to ignore 

1689 continue 

1690 raise ValueError("unsupported RRULE parm: " + parm) 

1691 rrulevals.append(value) 

1692 elif name == "RDATE": 

1693 for parm in parms: 

1694 if parm != "VALUE=DATE-TIME": 

1695 raise ValueError("unsupported RDATE parm: "+parm) 

1696 rdatevals.append(value) 

1697 elif name == "EXRULE": 

1698 for parm in parms: 

1699 raise ValueError("unsupported EXRULE parm: "+parm) 

1700 exrulevals.append(value) 

1701 elif name == "EXDATE": 

1702 exdatevals.extend( 

1703 self._parse_date_value(value, parms, 

1704 TZID_NAMES, ignoretz, 

1705 tzids, tzinfos) 

1706 ) 

1707 elif name == "DTSTART": 

1708 dtvals = self._parse_date_value(value, parms, TZID_NAMES, 

1709 ignoretz, tzids, tzinfos) 

1710 if len(dtvals) != 1: 

1711 raise ValueError("Multiple DTSTART values specified:" + 

1712 value) 

1713 dtstart = dtvals[0] 

1714 else: 

1715 raise ValueError("unsupported property: "+name) 

1716 if (forceset or len(rrulevals) > 1 or rdatevals 

1717 or exrulevals or exdatevals): 

1718 if not parser and (rdatevals or exdatevals): 

1719 from dateutil import parser 

1720 rset = rruleset(cache=cache) 

1721 for value in rrulevals: 

1722 rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, 

1723 ignoretz=ignoretz, 

1724 tzinfos=tzinfos)) 

1725 for value in rdatevals: 

1726 for datestr in value.split(','): 

1727 rset.rdate(parser.parse(datestr, 

1728 ignoretz=ignoretz, 

1729 tzinfos=tzinfos)) 

1730 for value in exrulevals: 

1731 rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, 

1732 ignoretz=ignoretz, 

1733 tzinfos=tzinfos)) 

1734 for value in exdatevals: 

1735 rset.exdate(value) 

1736 if compatible and dtstart: 

1737 rset.rdate(dtstart) 

1738 return rset 

1739 else: 

1740 return self._parse_rfc_rrule(rrulevals[0], 

1741 dtstart=dtstart, 

1742 cache=cache, 

1743 ignoretz=ignoretz, 

1744 tzinfos=tzinfos) 

1745 

1746 def __call__(self, s, **kwargs): 

1747 return self._parse_rfc(s, **kwargs) 

1748 

1749 

1750rrulestr = _rrulestr() 

1751 

1752# vim:ts=4:sw=4:et