Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

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

4the recurrence rules documented in the 

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

6including support for caching of results. 

7""" 

8import itertools 

9import datetime 

10import calendar 

11import re 

12import sys 

13 

14try: 

15 from math import gcd 

16except ImportError: 

17 from fractions import gcd 

18 

19from six import advance_iterator, integer_types 

20from six.moves import _thread, range 

21import heapq 

22 

23from ._common import weekday as weekdaybase 

24 

25# For warning about deprecation of until and count 

26from warnings import warn 

27 

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

29 "YEARLY", "MONTHLY", "WEEKLY", "DAILY", 

30 "HOURLY", "MINUTELY", "SECONDLY", 

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

32 

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

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

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

36M365MASK = list(M366MASK) 

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

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

39MDAY365MASK = list(MDAY366MASK) 

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

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

42NMDAY365MASK = list(NMDAY366MASK) 

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

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

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

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

47MDAY365MASK = tuple(MDAY365MASK) 

48M365MASK = tuple(M365MASK) 

49 

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

51 

52(YEARLY, 

53 MONTHLY, 

54 WEEKLY, 

55 DAILY, 

56 HOURLY, 

57 MINUTELY, 

58 SECONDLY) = list(range(7)) 

59 

60# Imported on demand. 

61easter = None 

62parser = None 

63 

64 

65class weekday(weekdaybase): 

66 """ 

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

68 """ 

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

70 if n == 0: 

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

72 

73 super(weekday, self).__init__(wkday, n) 

74 

75 

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

77 

78 

79def _invalidates_cache(f): 

80 """ 

81 Decorator for rruleset methods which may invalidate the 

82 cached length. 

83 """ 

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

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

86 self._invalidate_cache() 

87 return rv 

88 

89 return inner_func 

90 

91 

92class rrulebase(object): 

93 def __init__(self, cache=False): 

94 if cache: 

95 self._cache = [] 

96 self._cache_lock = _thread.allocate_lock() 

97 self._invalidate_cache() 

98 else: 

99 self._cache = None 

100 self._cache_complete = False 

101 self._len = None 

102 

103 def __iter__(self): 

104 if self._cache_complete: 

105 return iter(self._cache) 

106 elif self._cache is None: 

107 return self._iter() 

108 else: 

109 return self._iter_cached() 

110 

111 def _invalidate_cache(self): 

112 if self._cache is not None: 

113 self._cache = [] 

114 self._cache_complete = False 

115 self._cache_gen = self._iter() 

116 

117 if self._cache_lock.locked(): 

118 self._cache_lock.release() 

119 

120 self._len = None 

121 

122 def _iter_cached(self): 

123 i = 0 

124 gen = self._cache_gen 

125 cache = self._cache 

126 acquire = self._cache_lock.acquire 

127 release = self._cache_lock.release 

128 while gen: 

129 if i == len(cache): 

130 acquire() 

131 if self._cache_complete: 

132 break 

133 try: 

134 for j in range(10): 

135 cache.append(advance_iterator(gen)) 

136 except StopIteration: 

137 self._cache_gen = gen = None 

138 self._cache_complete = True 

139 break 

140 release() 

141 yield cache[i] 

142 i += 1 

143 while i < self._len: 

144 yield cache[i] 

145 i += 1 

146 

147 def __getitem__(self, item): 

148 if self._cache_complete: 

149 return self._cache[item] 

150 elif isinstance(item, slice): 

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

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

153 else: 

154 return list(itertools.islice(self, 

155 item.start or 0, 

156 item.stop or sys.maxsize, 

157 item.step or 1)) 

158 elif item >= 0: 

159 gen = iter(self) 

160 try: 

161 for i in range(item+1): 

162 res = advance_iterator(gen) 

163 except StopIteration: 

164 raise IndexError 

165 return res 

166 else: 

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

168 

169 def __contains__(self, item): 

170 if self._cache_complete: 

171 return item in self._cache 

172 else: 

173 for i in self: 

174 if i == item: 

175 return True 

176 elif i > item: 

177 return False 

178 return False 

179 

180 # __len__() introduces a large performance penalty. 

181 def count(self): 

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

183 trough the whole recurrence, if this hasn't been done before. """ 

184 if self._len is None: 

185 for x in self: 

186 pass 

187 return self._len 

188 

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

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

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

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

193 if self._cache_complete: 

194 gen = self._cache 

195 else: 

196 gen = self 

197 last = None 

198 if inc: 

199 for i in gen: 

200 if i > dt: 

201 break 

202 last = i 

203 else: 

204 for i in gen: 

205 if i >= dt: 

206 break 

207 last = i 

208 return last 

209 

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

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

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

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

214 if self._cache_complete: 

215 gen = self._cache 

216 else: 

217 gen = self 

218 if inc: 

219 for i in gen: 

220 if i >= dt: 

221 return i 

222 else: 

223 for i in gen: 

224 if i > dt: 

225 return i 

226 return None 

227 

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

229 """ 

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

231 datetime instance, equivalent to `after`. 

232 

233 :param dt: 

234 The datetime at which to start generating recurrences. 

235 

236 :param count: 

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

238 dates are generated until the recurrence rule is exhausted. 

239 

240 :param inc: 

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

242 included in the output. 

243 

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

245 """ 

246 

247 if self._cache_complete: 

248 gen = self._cache 

249 else: 

250 gen = self 

251 

252 # Select the comparison function 

253 if inc: 

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

255 else: 

256 comp = lambda dc, dtc: dc > dtc 

257 

258 # Generate dates 

259 n = 0 

260 for d in gen: 

261 if comp(d, dt): 

262 if count is not None: 

263 n += 1 

264 if n > count: 

265 break 

266 

267 yield d 

268 

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

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

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

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

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

274 if self._cache_complete: 

275 gen = self._cache 

276 else: 

277 gen = self 

278 started = False 

279 l = [] 

280 if inc: 

281 for i in gen: 

282 if i > before: 

283 break 

284 elif not started: 

285 if i >= after: 

286 started = True 

287 l.append(i) 

288 else: 

289 l.append(i) 

290 else: 

291 for i in gen: 

292 if i >= before: 

293 break 

294 elif not started: 

295 if i > after: 

296 started = True 

297 l.append(i) 

298 else: 

299 l.append(i) 

300 return l 

301 

302 

303class rrule(rrulebase): 

304 """ 

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

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

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

308 

309 rrule(freq) 

310 

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

312 or SECONDLY. 

313 

314 .. note:: 

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

316 and times are ignored rather than coerced: 

317 

318 Recurrence rules may generate recurrence instances with an invalid 

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

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

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

322 counted as part of the recurrence set. 

323 

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

325 start date occurs at the end of the month: 

326 

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

328 >>> from datetime import datetime 

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

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

331 ... # doctest: +NORMALIZE_WHITESPACE 

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

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

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

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

336 

337 Additionally, it supports the following keyword arguments: 

338 

339 :param dtstart: 

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

341 missing parameters in the final recurrence instances will also be 

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

343 instead. 

344 :param interval: 

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

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

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

348 :param wkst: 

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

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

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

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

353 calendar.setfirstweekday(). 

354 :param count: 

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

356 

357 .. note:: 

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

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

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

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

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

363 :param until: 

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

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

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

367 ``until`` parameter. 

368 

369 .. note:: 

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

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

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

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

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

375 :param bysetpos: 

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

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

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

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

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

381 result in the last work day of every month. 

382 :param bymonth: 

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

384 meaning the months to apply the recurrence to. 

385 :param bymonthday: 

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

387 meaning the month days to apply the recurrence to. 

388 :param byyearday: 

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

390 meaning the year days to apply the recurrence to. 

391 :param byeaster: 

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

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

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

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

396 :param byweekno: 

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

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

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

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

401 :param byweekday: 

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

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

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

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

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

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

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

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

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

411 avoid the ambiguity of that keyword. 

412 :param byhour: 

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

414 meaning the hours to apply the recurrence to. 

415 :param byminute: 

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

417 meaning the minutes to apply the recurrence to. 

418 :param bysecond: 

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

420 meaning the seconds to apply the recurrence to. 

421 :param cache: 

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

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

424 times, enabling caching will improve the performance considerably. 

425 """ 

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

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

428 bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 

429 byweekno=None, byweekday=None, 

430 byhour=None, byminute=None, bysecond=None, 

431 cache=False): 

432 super(rrule, self).__init__(cache) 

433 global easter 

434 if not dtstart: 

435 if until and until.tzinfo: 

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

437 else: 

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

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

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

441 else: 

442 dtstart = dtstart.replace(microsecond=0) 

443 self._dtstart = dtstart 

444 self._tzinfo = dtstart.tzinfo 

445 self._freq = freq 

446 self._interval = interval 

447 self._count = count 

448 

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

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

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

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

453 self._original_rule = {} 

454 

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

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

457 self._until = until 

458 

459 if self._dtstart and self._until: 

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

461 # According to RFC5545 Section 3.3.10: 

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

463 # 

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

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

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

467 # > UTC time. 

468 raise ValueError( 

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

470 'is timezone-aware' 

471 ) 

472 

473 if count is not None and until: 

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

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

476 "raise an error.", DeprecationWarning) 

477 

478 if wkst is None: 

479 self._wkst = calendar.firstweekday() 

480 elif isinstance(wkst, integer_types): 

481 self._wkst = wkst 

482 else: 

483 self._wkst = wkst.weekday 

484 

485 if bysetpos is None: 

486 self._bysetpos = None 

487 elif isinstance(bysetpos, integer_types): 

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

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

490 "or between -366 and -1") 

491 self._bysetpos = (bysetpos,) 

492 else: 

493 self._bysetpos = tuple(bysetpos) 

494 for pos in self._bysetpos: 

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

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

497 "or between -366 and -1") 

498 

499 if self._bysetpos: 

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

501 

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

503 byweekday is None and byeaster is None): 

504 if freq == YEARLY: 

505 if bymonth is None: 

506 bymonth = dtstart.month 

507 self._original_rule['bymonth'] = None 

508 bymonthday = dtstart.day 

509 self._original_rule['bymonthday'] = None 

510 elif freq == MONTHLY: 

511 bymonthday = dtstart.day 

512 self._original_rule['bymonthday'] = None 

513 elif freq == WEEKLY: 

514 byweekday = dtstart.weekday() 

515 self._original_rule['byweekday'] = None 

516 

517 # bymonth 

518 if bymonth is None: 

519 self._bymonth = None 

520 else: 

521 if isinstance(bymonth, integer_types): 

522 bymonth = (bymonth,) 

523 

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

525 

526 if 'bymonth' not in self._original_rule: 

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

528 

529 # byyearday 

530 if byyearday is None: 

531 self._byyearday = None 

532 else: 

533 if isinstance(byyearday, integer_types): 

534 byyearday = (byyearday,) 

535 

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

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

538 

539 # byeaster 

540 if byeaster is not None: 

541 if not easter: 

542 from dateutil import easter 

543 if isinstance(byeaster, integer_types): 

544 self._byeaster = (byeaster,) 

545 else: 

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

547 

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

549 else: 

550 self._byeaster = None 

551 

552 # bymonthday 

553 if bymonthday is None: 

554 self._bymonthday = () 

555 self._bynmonthday = () 

556 else: 

557 if isinstance(bymonthday, integer_types): 

558 bymonthday = (bymonthday,) 

559 

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

561 

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

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

564 

565 # Storing positive numbers first, then negative numbers 

566 if 'bymonthday' not in self._original_rule: 

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

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

569 

570 # byweekno 

571 if byweekno is None: 

572 self._byweekno = None 

573 else: 

574 if isinstance(byweekno, integer_types): 

575 byweekno = (byweekno,) 

576 

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

578 

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

580 

581 # byweekday / bynweekday 

582 if byweekday is None: 

583 self._byweekday = None 

584 self._bynweekday = None 

585 else: 

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

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

588 # byweekday set. 

589 if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): 

590 byweekday = (byweekday,) 

591 

592 self._byweekday = set() 

593 self._bynweekday = set() 

594 for wday in byweekday: 

595 if isinstance(wday, integer_types): 

596 self._byweekday.add(wday) 

597 elif not wday.n or freq > MONTHLY: 

598 self._byweekday.add(wday.weekday) 

599 else: 

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

601 

602 if not self._byweekday: 

603 self._byweekday = None 

604 elif not self._bynweekday: 

605 self._bynweekday = None 

606 

607 if self._byweekday is not None: 

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

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

610 else: 

611 orig_byweekday = () 

612 

613 if self._bynweekday is not None: 

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

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

616 else: 

617 orig_bynweekday = () 

618 

619 if 'byweekday' not in self._original_rule: 

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

621 orig_byweekday, orig_bynweekday)) 

622 

623 # byhour 

624 if byhour is None: 

625 if freq < HOURLY: 

626 self._byhour = {dtstart.hour} 

627 else: 

628 self._byhour = None 

629 else: 

630 if isinstance(byhour, integer_types): 

631 byhour = (byhour,) 

632 

633 if freq == HOURLY: 

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

635 byxxx=byhour, 

636 base=24) 

637 else: 

638 self._byhour = set(byhour) 

639 

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

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

642 

643 # byminute 

644 if byminute is None: 

645 if freq < MINUTELY: 

646 self._byminute = {dtstart.minute} 

647 else: 

648 self._byminute = None 

649 else: 

650 if isinstance(byminute, integer_types): 

651 byminute = (byminute,) 

652 

653 if freq == MINUTELY: 

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

655 byxxx=byminute, 

656 base=60) 

657 else: 

658 self._byminute = set(byminute) 

659 

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

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

662 

663 # bysecond 

664 if bysecond is None: 

665 if freq < SECONDLY: 

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

667 else: 

668 self._bysecond = None 

669 else: 

670 if isinstance(bysecond, integer_types): 

671 bysecond = (bysecond,) 

672 

673 self._bysecond = set(bysecond) 

674 

675 if freq == SECONDLY: 

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

677 byxxx=bysecond, 

678 base=60) 

679 else: 

680 self._bysecond = set(bysecond) 

681 

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

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

684 

685 if self._freq >= HOURLY: 

686 self._timeset = None 

687 else: 

688 self._timeset = [] 

689 for hour in self._byhour: 

690 for minute in self._byminute: 

691 for second in self._bysecond: 

692 self._timeset.append( 

693 datetime.time(hour, minute, second, 

694 tzinfo=self._tzinfo)) 

695 self._timeset.sort() 

696 self._timeset = tuple(self._timeset) 

697 

698 def __str__(self): 

699 """ 

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

701 This is mostly compatible with RFC5545, except for the 

702 dateutil-specific extension BYEASTER. 

703 """ 

704 

705 output = [] 

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

707 if self._dtstart: 

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

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

710 

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

712 if self._interval != 1: 

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

714 

715 if self._wkst: 

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

717 

718 if self._count is not None: 

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

720 

721 if self._until: 

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

723 

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

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

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

727 original_rule = dict(self._original_rule) 

728 wday_strings = [] 

729 for wday in original_rule['byweekday']: 

730 if wday.n: 

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

732 n=wday.n, 

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

734 else: 

735 wday_strings.append(repr(wday)) 

736 

737 original_rule['byweekday'] = wday_strings 

738 else: 

739 original_rule = self._original_rule 

740 

741 partfmt = '{name}={vals}' 

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

743 ('BYMONTH', 'bymonth'), 

744 ('BYMONTHDAY', 'bymonthday'), 

745 ('BYYEARDAY', 'byyearday'), 

746 ('BYWEEKNO', 'byweekno'), 

747 ('BYDAY', 'byweekday'), 

748 ('BYHOUR', 'byhour'), 

749 ('BYMINUTE', 'byminute'), 

750 ('BYSECOND', 'bysecond'), 

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

752 value = original_rule.get(key) 

753 if value: 

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

755 for v in value)))) 

756 

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

758 return '\n'.join(output) 

759 

760 def replace(self, **kwargs): 

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

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

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

764 "count": self._count, 

765 "dtstart": self._dtstart, 

766 "freq": self._freq, 

767 "until": self._until, 

768 "wkst": self._wkst, 

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

770 new_kwargs.update(self._original_rule) 

771 new_kwargs.update(kwargs) 

772 return rrule(**new_kwargs) 

773 

774 def _iter(self): 

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

776 self._dtstart.timetuple() 

777 

778 # Some local variables to speed things up a bit 

779 freq = self._freq 

780 interval = self._interval 

781 wkst = self._wkst 

782 until = self._until 

783 bymonth = self._bymonth 

784 byweekno = self._byweekno 

785 byyearday = self._byyearday 

786 byweekday = self._byweekday 

787 byeaster = self._byeaster 

788 bymonthday = self._bymonthday 

789 bynmonthday = self._bynmonthday 

790 bysetpos = self._bysetpos 

791 byhour = self._byhour 

792 byminute = self._byminute 

793 bysecond = self._bysecond 

794 

795 ii = _iterinfo(self) 

796 ii.rebuild(year, month) 

797 

798 getdayset = {YEARLY: ii.ydayset, 

799 MONTHLY: ii.mdayset, 

800 WEEKLY: ii.wdayset, 

801 DAILY: ii.ddayset, 

802 HOURLY: ii.ddayset, 

803 MINUTELY: ii.ddayset, 

804 SECONDLY: ii.ddayset}[freq] 

805 

806 if freq < HOURLY: 

807 timeset = self._timeset 

808 else: 

809 gettimeset = {HOURLY: ii.htimeset, 

810 MINUTELY: ii.mtimeset, 

811 SECONDLY: ii.stimeset}[freq] 

812 if ((freq >= HOURLY and 

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

814 (freq >= MINUTELY and 

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

816 (freq >= SECONDLY and 

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

818 timeset = () 

819 else: 

820 timeset = gettimeset(hour, minute, second) 

821 

822 total = 0 

823 count = self._count 

824 while True: 

825 # Get dayset with the right frequency 

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

827 

828 # Do the "hard" work ;-) 

829 filtered = False 

830 for i in dayset[start:end]: 

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

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

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

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

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

836 ((bymonthday or bynmonthday) and 

837 ii.mdaymask[i] not in bymonthday and 

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

839 (byyearday and 

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

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

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

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

844 dayset[i] = None 

845 filtered = True 

846 

847 # Output results 

848 if bysetpos and timeset: 

849 poslist = [] 

850 for pos in bysetpos: 

851 if pos < 0: 

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

853 else: 

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

855 try: 

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

857 if x is not None][daypos] 

858 time = timeset[timepos] 

859 except IndexError: 

860 pass 

861 else: 

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

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

864 if res not in poslist: 

865 poslist.append(res) 

866 poslist.sort() 

867 for res in poslist: 

868 if until and res > until: 

869 self._len = total 

870 return 

871 elif res >= self._dtstart: 

872 if count is not None: 

873 count -= 1 

874 if count < 0: 

875 self._len = total 

876 return 

877 total += 1 

878 yield res 

879 else: 

880 for i in dayset[start:end]: 

881 if i is not None: 

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

883 for time in timeset: 

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

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 

895 total += 1 

896 yield res 

897 

898 # Handle frequency and interval 

899 fixday = False 

900 if freq == YEARLY: 

901 year += interval 

902 if year > datetime.MAXYEAR: 

903 self._len = total 

904 return 

905 ii.rebuild(year, month) 

906 elif freq == MONTHLY: 

907 month += interval 

908 if month > 12: 

909 div, mod = divmod(month, 12) 

910 month = mod 

911 year += div 

912 if month == 0: 

913 month = 12 

914 year -= 1 

915 if year > datetime.MAXYEAR: 

916 self._len = total 

917 return 

918 ii.rebuild(year, month) 

919 elif freq == WEEKLY: 

920 if wkst > weekday: 

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

922 else: 

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

924 weekday = wkst 

925 fixday = True 

926 elif freq == DAILY: 

927 day += interval 

928 fixday = True 

929 elif freq == HOURLY: 

930 if filtered: 

931 # Jump to one iteration before next day 

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

933 

934 if byhour: 

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

936 byxxx=self._byhour, 

937 base=24) 

938 else: 

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

940 

941 if ndays: 

942 day += ndays 

943 fixday = True 

944 

945 timeset = gettimeset(hour, minute, second) 

946 elif freq == MINUTELY: 

947 if filtered: 

948 # Jump to one iteration before next day 

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

950 

951 valid = False 

952 rep_rate = (24*60) 

953 for j in range(rep_rate // gcd(interval, rep_rate)): 

954 if byminute: 

955 nhours, minute = \ 

956 self.__mod_distance(value=minute, 

957 byxxx=self._byminute, 

958 base=60) 

959 else: 

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

961 

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

963 if div: 

964 day += div 

965 fixday = True 

966 filtered = False 

967 

968 if not byhour or hour in byhour: 

969 valid = True 

970 break 

971 

972 if not valid: 

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

974 'byhour resulting in empty rule.') 

975 

976 timeset = gettimeset(hour, minute, second) 

977 elif freq == SECONDLY: 

978 if filtered: 

979 # Jump to one iteration before next day 

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

981 // interval) * interval) 

982 

983 rep_rate = (24 * 3600) 

984 valid = False 

985 for j in range(0, rep_rate // gcd(interval, rep_rate)): 

986 if bysecond: 

987 nminutes, second = \ 

988 self.__mod_distance(value=second, 

989 byxxx=self._bysecond, 

990 base=60) 

991 else: 

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

993 

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

995 if div: 

996 hour += div 

997 div, hour = divmod(hour, 24) 

998 if div: 

999 day += div 

1000 fixday = True 

1001 

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

1003 (not byminute or minute in byminute) and 

1004 (not bysecond or second in bysecond)): 

1005 valid = True 

1006 break 

1007 

1008 if not valid: 

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

1010 'byhour and byminute resulting in empty' + 

1011 ' rule.') 

1012 

1013 timeset = gettimeset(hour, minute, second) 

1014 

1015 if fixday and day > 28: 

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

1017 if day > daysinmonth: 

1018 while day > daysinmonth: 

1019 day -= daysinmonth 

1020 month += 1 

1021 if month == 13: 

1022 month = 1 

1023 year += 1 

1024 if year > datetime.MAXYEAR: 

1025 self._len = total 

1026 return 

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

1028 ii.rebuild(year, month) 

1029 

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

1031 """ 

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

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

1034 specifications which cannot be reached given some starting conditions. 

1035 

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

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

1038 ending position is not coprime with the greatest common denominator 

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

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

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

1042 coprime. 

1043 

1044 :param start: 

1045 Specifies the starting position. 

1046 :param byxxx: 

1047 An iterable containing the list of allowed values. 

1048 :param base: 

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

1050 24 hours, 60 minutes). 

1051 

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

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

1054 speed up later lookups. 

1055 

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

1057 results in an empty rrule. 

1058 """ 

1059 

1060 cset = set() 

1061 

1062 # Support a single byxxx value. 

1063 if isinstance(byxxx, integer_types): 

1064 byxxx = (byxxx, ) 

1065 

1066 for num in byxxx: 

1067 i_gcd = gcd(self._interval, base) 

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

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

1070 cset.add(num) 

1071 

1072 if len(cset) == 0: 

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

1074 

1075 return cset 

1076 

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

1078 """ 

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

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

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

1082 

1083 :param value: 

1084 The old value of the component. 

1085 :param byxxx: 

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

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

1088 valid rule is present. 

1089 :param base: 

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

1091 24 hours, 60 minutes). 

1092 

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

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

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

1096 

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

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

1099 value in `byxxx` is found. 

1100 """ 

1101 accumulator = 0 

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

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

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

1105 accumulator += div 

1106 if value in byxxx: 

1107 return (accumulator, value) 

1108 

1109 

1110class _iterinfo(object): 

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

1112 "yearlen", "nextyearlen", "yearordinal", "yearweekday", 

1113 "mmask", "mrange", "mdaymask", "nmdaymask", 

1114 "wdaymask", "wnomask", "nwdaymask", "eastermask"] 

1115 

1116 def __init__(self, rrule): 

1117 for attr in self.__slots__: 

1118 setattr(self, attr, None) 

1119 self.rrule = rrule 

1120 

1121 def rebuild(self, year, month): 

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

1123 rr = self.rrule 

1124 if year != self.lastyear: 

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

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

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

1128 self.yearordinal = firstyday.toordinal() 

1129 self.yearweekday = firstyday.weekday() 

1130 

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

1132 if self.yearlen == 365: 

1133 self.mmask = M365MASK 

1134 self.mdaymask = MDAY365MASK 

1135 self.nmdaymask = NMDAY365MASK 

1136 self.wdaymask = WDAYMASK[wday:] 

1137 self.mrange = M365RANGE 

1138 else: 

1139 self.mmask = M366MASK 

1140 self.mdaymask = MDAY366MASK 

1141 self.nmdaymask = NMDAY366MASK 

1142 self.wdaymask = WDAYMASK[wday:] 

1143 self.mrange = M366RANGE 

1144 

1145 if not rr._byweekno: 

1146 self.wnomask = None 

1147 else: 

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

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

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

1151 if no1wkst >= 4: 

1152 no1wkst = 0 

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

1154 # from last year. 

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

1156 else: 

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

1158 # left in last year. 

1159 wyearlen = self.yearlen-no1wkst 

1160 div, mod = divmod(wyearlen, 7) 

1161 numweeks = div+mod//4 

1162 for n in rr._byweekno: 

1163 if n < 0: 

1164 n += numweeks+1 

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

1166 continue 

1167 if n > 1: 

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

1169 if no1wkst != firstwkst: 

1170 i -= 7-firstwkst 

1171 else: 

1172 i = no1wkst 

1173 for j in range(7): 

1174 self.wnomask[i] = 1 

1175 i += 1 

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

1177 break 

1178 if 1 in rr._byweekno: 

1179 # Check week number 1 of next year as well 

1180 # TODO: Check -numweeks for next year. 

1181 i = no1wkst+numweeks*7 

1182 if no1wkst != firstwkst: 

1183 i -= 7-firstwkst 

1184 if i < self.yearlen: 

1185 # If week starts in next year, we 

1186 # don't care about it. 

1187 for j in range(7): 

1188 self.wnomask[i] = 1 

1189 i += 1 

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

1191 break 

1192 if no1wkst: 

1193 # Check last week number of last year as 

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

1195 # started on week start, or week number 1 

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

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

1198 # this year. 

1199 if -1 not in rr._byweekno: 

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

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

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

1203 if lno1wkst >= 4: 

1204 lno1wkst = 0 

1205 lnumweeks = 52+(lyearlen + 

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

1207 else: 

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

1209 else: 

1210 lnumweeks = -1 

1211 if lnumweeks in rr._byweekno: 

1212 for i in range(no1wkst): 

1213 self.wnomask[i] = 1 

1214 

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

1216 year != self.lastyear)): 

1217 ranges = [] 

1218 if rr._freq == YEARLY: 

1219 if rr._bymonth: 

1220 for month in rr._bymonth: 

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

1222 else: 

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

1224 elif rr._freq == MONTHLY: 

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

1226 if ranges: 

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

1228 # care about cross-year weekly periods. 

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

1230 for first, last in ranges: 

1231 last -= 1 

1232 for wday, n in rr._bynweekday: 

1233 if n < 0: 

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

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

1236 else: 

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

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

1239 if first <= i <= last: 

1240 self.nwdaymask[i] = 1 

1241 

1242 if rr._byeaster: 

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

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

1245 for offset in rr._byeaster: 

1246 self.eastermask[eyday+offset] = 1 

1247 

1248 self.lastyear = year 

1249 self.lastmonth = month 

1250 

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

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

1253 

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

1255 dset = [None]*self.yearlen 

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

1257 for i in range(start, end): 

1258 dset[i] = i 

1259 return dset, start, end 

1260 

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

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

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

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

1265 start = i 

1266 for j in range(7): 

1267 dset[i] = i 

1268 i += 1 

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

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

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

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

1273 break 

1274 return dset, start, i 

1275 

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

1277 dset = [None] * self.yearlen 

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

1279 dset[i] = i 

1280 return dset, i, i + 1 

1281 

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

1283 tset = [] 

1284 rr = self.rrule 

1285 for minute in rr._byminute: 

1286 for second in rr._bysecond: 

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

1288 tzinfo=rr._tzinfo)) 

1289 tset.sort() 

1290 return tset 

1291 

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

1293 tset = [] 

1294 rr = self.rrule 

1295 for second in rr._bysecond: 

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

1297 tset.sort() 

1298 return tset 

1299 

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

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

1302 tzinfo=self.rrule._tzinfo),) 

1303 

1304 

1305class rruleset(rrulebase): 

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

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

1308 constructor takes the following keyword arguments: 

1309 

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

1311 performance of multiple queries considerably. """ 

1312 

1313 class _genitem(object): 

1314 def __init__(self, genlist, gen): 

1315 try: 

1316 self.dt = advance_iterator(gen) 

1317 genlist.append(self) 

1318 except StopIteration: 

1319 pass 

1320 self.genlist = genlist 

1321 self.gen = gen 

1322 

1323 def __next__(self): 

1324 try: 

1325 self.dt = advance_iterator(self.gen) 

1326 except StopIteration: 

1327 if self.genlist[0] is self: 

1328 heapq.heappop(self.genlist) 

1329 else: 

1330 self.genlist.remove(self) 

1331 heapq.heapify(self.genlist) 

1332 

1333 next = __next__ 

1334 

1335 def __lt__(self, other): 

1336 return self.dt < other.dt 

1337 

1338 def __gt__(self, other): 

1339 return self.dt > other.dt 

1340 

1341 def __eq__(self, other): 

1342 return self.dt == other.dt 

1343 

1344 def __ne__(self, other): 

1345 return self.dt != other.dt 

1346 

1347 def __init__(self, cache=False): 

1348 super(rruleset, self).__init__(cache) 

1349 self._rrule = [] 

1350 self._rdate = [] 

1351 self._exrule = [] 

1352 self._exdate = [] 

1353 

1354 @_invalidates_cache 

1355 def rrule(self, rrule): 

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

1357 generation. """ 

1358 self._rrule.append(rrule) 

1359 

1360 @_invalidates_cache 

1361 def rdate(self, rdate): 

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

1363 set generation. """ 

1364 self._rdate.append(rdate) 

1365 

1366 @_invalidates_cache 

1367 def exrule(self, exrule): 

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

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

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

1371 """ 

1372 self._exrule.append(exrule) 

1373 

1374 @_invalidates_cache 

1375 def exdate(self, exdate): 

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

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

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

1379 self._exdate.append(exdate) 

1380 

1381 def _iter(self): 

1382 rlist = [] 

1383 self._rdate.sort() 

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

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

1386 self._genitem(rlist, gen) 

1387 exlist = [] 

1388 self._exdate.sort() 

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

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

1391 self._genitem(exlist, gen) 

1392 lastdt = None 

1393 total = 0 

1394 heapq.heapify(rlist) 

1395 heapq.heapify(exlist) 

1396 while rlist: 

1397 ritem = rlist[0] 

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

1399 while exlist and exlist[0] < ritem: 

1400 exitem = exlist[0] 

1401 advance_iterator(exitem) 

1402 if exlist and exlist[0] is exitem: 

1403 heapq.heapreplace(exlist, exitem) 

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

1405 total += 1 

1406 yield ritem.dt 

1407 lastdt = ritem.dt 

1408 advance_iterator(ritem) 

1409 if rlist and rlist[0] is ritem: 

1410 heapq.heapreplace(rlist, ritem) 

1411 self._len = total 

1412 

1413 

1414 

1415 

1416class _rrulestr(object): 

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

1418 recurrence rules. 

1419 

1420 :param s: 

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

1422 

1423 :param dtstart: 

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

1425 rule string. 

1426 

1427 :param cache: 

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

1429 performance of multiple queries considerably. 

1430 

1431 :param unfold: 

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

1433 than one line and should be joined before processing. 

1434 

1435 :param forceset: 

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

1437 be returned. 

1438 

1439 :param compatible: 

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

1441 

1442 :param ignoretz: 

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

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

1445 

1446 :param tzids: 

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

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

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

1450 

1451 :param tzinfos: 

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

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

1454 information. 

1455 

1456 :return: 

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

1458 :class:`dateutil.rrule.rrule` 

1459 """ 

1460 

1461 _freq_map = {"YEARLY": YEARLY, 

1462 "MONTHLY": MONTHLY, 

1463 "WEEKLY": WEEKLY, 

1464 "DAILY": DAILY, 

1465 "HOURLY": HOURLY, 

1466 "MINUTELY": MINUTELY, 

1467 "SECONDLY": SECONDLY} 

1468 

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

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

1471 

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

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

1474 

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

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

1477 

1478 _handle_INTERVAL = _handle_int 

1479 _handle_COUNT = _handle_int 

1480 _handle_BYSETPOS = _handle_int_list 

1481 _handle_BYMONTH = _handle_int_list 

1482 _handle_BYMONTHDAY = _handle_int_list 

1483 _handle_BYYEARDAY = _handle_int_list 

1484 _handle_BYEASTER = _handle_int_list 

1485 _handle_BYWEEKNO = _handle_int_list 

1486 _handle_BYHOUR = _handle_int_list 

1487 _handle_BYMINUTE = _handle_int_list 

1488 _handle_BYSECOND = _handle_int_list 

1489 

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

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

1492 

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

1494 global parser 

1495 if not parser: 

1496 from dateutil import parser 

1497 try: 

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

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

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

1501 except ValueError: 

1502 raise ValueError("invalid until date") 

1503 

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

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

1506 

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

1508 """ 

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

1510 """ 

1511 l = [] 

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

1513 if '(' in wday: 

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

1515 splt = wday.split('(') 

1516 w = splt[0] 

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

1518 elif len(wday): 

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

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

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

1522 break 

1523 n = wday[:i] or None 

1524 w = wday[i:] 

1525 if n: 

1526 n = int(n) 

1527 else: 

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

1529 

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

1531 rrkwargs["byweekday"] = l 

1532 

1533 _handle_BYDAY = _handle_BYWEEKDAY 

1534 

1535 def _parse_rfc_rrule(self, line, 

1536 dtstart=None, 

1537 cache=False, 

1538 ignoretz=False, 

1539 tzinfos=None): 

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

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

1542 if name != "RRULE": 

1543 raise ValueError("unknown parameter name") 

1544 else: 

1545 value = line 

1546 rrkwargs = {} 

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

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

1549 name = name.upper() 

1550 value = value.upper() 

1551 try: 

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

1553 ignoretz=ignoretz, 

1554 tzinfos=tzinfos) 

1555 except AttributeError: 

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

1557 except (KeyError, ValueError): 

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

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

1560 

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

1562 ignoretz, tzids, tzinfos): 

1563 global parser 

1564 if not parser: 

1565 from dateutil import parser 

1566 

1567 datevals = [] 

1568 value_found = False 

1569 TZID = None 

1570 

1571 for parm in parms: 

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

1573 try: 

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

1575 except KeyError: 

1576 continue 

1577 if tzids is None: 

1578 from . import tz 

1579 tzlookup = tz.gettz 

1580 elif callable(tzids): 

1581 tzlookup = tzids 

1582 else: 

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

1584 if tzlookup is None: 

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

1586 'not %s' % tzids) 

1587 raise ValueError(msg) 

1588 

1589 TZID = tzlookup(tzkey) 

1590 continue 

1591 

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

1593 # only once. 

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

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

1596 else: 

1597 if value_found: 

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

1599 raise ValueError(msg) 

1600 value_found = True 

1601 

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

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

1604 if TZID is not None: 

1605 if date.tzinfo is None: 

1606 date = date.replace(tzinfo=TZID) 

1607 else: 

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

1609 datevals.append(date) 

1610 

1611 return datevals 

1612 

1613 def _parse_rfc(self, s, 

1614 dtstart=None, 

1615 cache=False, 

1616 unfold=False, 

1617 forceset=False, 

1618 compatible=False, 

1619 ignoretz=False, 

1620 tzids=None, 

1621 tzinfos=None): 

1622 global parser 

1623 if compatible: 

1624 forceset = True 

1625 unfold = True 

1626 

1627 TZID_NAMES = dict(map( 

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

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

1630 )) 

1631 s = s.upper() 

1632 if not s.strip(): 

1633 raise ValueError("empty string") 

1634 if unfold: 

1635 lines = s.splitlines() 

1636 i = 0 

1637 while i < len(lines): 

1638 line = lines[i].rstrip() 

1639 if not line: 

1640 del lines[i] 

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

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

1643 del lines[i] 

1644 else: 

1645 i += 1 

1646 else: 

1647 lines = s.split() 

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

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

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

1651 dtstart=dtstart, ignoretz=ignoretz, 

1652 tzinfos=tzinfos) 

1653 else: 

1654 rrulevals = [] 

1655 rdatevals = [] 

1656 exrulevals = [] 

1657 exdatevals = [] 

1658 for line in lines: 

1659 if not line: 

1660 continue 

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

1662 name = "RRULE" 

1663 value = line 

1664 else: 

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

1666 parms = name.split(';') 

1667 if not parms: 

1668 raise ValueError("empty property name") 

1669 name = parms[0] 

1670 parms = parms[1:] 

1671 if name == "RRULE": 

1672 for parm in parms: 

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

1674 rrulevals.append(value) 

1675 elif name == "RDATE": 

1676 for parm in parms: 

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

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

1679 rdatevals.append(value) 

1680 elif name == "EXRULE": 

1681 for parm in parms: 

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

1683 exrulevals.append(value) 

1684 elif name == "EXDATE": 

1685 exdatevals.extend( 

1686 self._parse_date_value(value, parms, 

1687 TZID_NAMES, ignoretz, 

1688 tzids, tzinfos) 

1689 ) 

1690 elif name == "DTSTART": 

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

1692 ignoretz, tzids, tzinfos) 

1693 if len(dtvals) != 1: 

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

1695 value) 

1696 dtstart = dtvals[0] 

1697 else: 

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

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

1700 or exrulevals or exdatevals): 

1701 if not parser and (rdatevals or exdatevals): 

1702 from dateutil import parser 

1703 rset = rruleset(cache=cache) 

1704 for value in rrulevals: 

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

1706 ignoretz=ignoretz, 

1707 tzinfos=tzinfos)) 

1708 for value in rdatevals: 

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

1710 rset.rdate(parser.parse(datestr, 

1711 ignoretz=ignoretz, 

1712 tzinfos=tzinfos)) 

1713 for value in exrulevals: 

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

1715 ignoretz=ignoretz, 

1716 tzinfos=tzinfos)) 

1717 for value in exdatevals: 

1718 rset.exdate(value) 

1719 if compatible and dtstart: 

1720 rset.rdate(dtstart) 

1721 return rset 

1722 else: 

1723 return self._parse_rfc_rrule(rrulevals[0], 

1724 dtstart=dtstart, 

1725 cache=cache, 

1726 ignoretz=ignoretz, 

1727 tzinfos=tzinfos) 

1728 

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

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

1731 

1732 

1733rrulestr = _rrulestr() 

1734 

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