- Moved common code in rrule and rruleset to a new rrulebase class.
- Improved caching system.
- Added __getitem__(), __contains__(), and count() in common code.
- Removed old predicate compiling code.
- Now rules that find no instances until MAXYEAR return an empty
  set instead of raising an error.
- New testcases.
diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index 1cebb8e..57d537f 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -7,9 +7,11 @@
 __author__ = "Gustavo Niemeyer <niemeyer@conectiva.com>"
 __license__ = "PSF License"
 
+import itertools
 import datetime
 import calendar
 import thread
+import sys
 
 __all__ = ["rrule", "rruleset", "rrulestr",
            "FREQ_YEARLY", "FREQ_MONTHLY", "FREQ_WEEKLY", "FREQ_DAILY",
@@ -75,20 +77,156 @@
 
 MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
 
-class rrule:
+class rrulebase:
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = thread.allocate_lock()
+            self._cache_gen  = self._iter()
+            self._cache_complete = False
+        else:
+            self._cache = None
+            self._cache_complete = False
+        self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(gen.next())
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxint,
+                                             item.step or 1))
+        elif item > 0:
+            gen = iter(self)
+            for i in range(item):
+                res = gen.next()
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+        return False
+
+    # __len__() introduces a large performance penality.
+    def count(self):
+        if self._len is None:
+            for x in self: pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def between(self, after, before, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    if i > before:
+                        break
+                    l.append(i)
+        else:
+            for i in gen:
+                if not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    if i >= before:
+                        break
+                    l.append(i)
+        return l
+
+class rrule(rrulebase):
     def __init__(self, freq, dtstart=None,
                  interval=1, wkst=None, count=None, until=None, bysetpos=None,
                  bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
                  byweekno=None, byweekday=None,
                  byhour=None, byminute=None, bysecond=None,
                  cache=False):
+        rrulebase.__init__(self, cache)
         global easter
-        if cache:
-            self._cache = []
-            self._cache_lock = thread.allocate_lock()
-            self._cache_gen  = self._iter()
-        else:
-            self._cache = None
         if not dtstart:
             dtstart = datetime.datetime.now().replace(microsecond=0)
         elif not isinstance(dtstart, datetime.datetime):
@@ -244,54 +382,6 @@
             self._timeset.sort()
             self._timeset = tuple(self._timeset)
 
-        #self._compile_pred()
-    
-    def _compile_pred(self):
-        predlist = []
-        if self._byweekday:
-            predlist.append("(ii.wdaymask[i] not in self._byweekday)")
-        if self._bymonth:
-            predlist.append("(ii.mmask[i] not in self._bymonth)")
-        if self._bymonthday and self._bynmonthday:
-            predlist.append("(ii.mdaymask[i] not in self._bymonthday and"
-                            " ii.nmdaymask[i] not in self._bynmonthday)")
-        elif self._bymonthday:
-            predlist.append("(ii.mdaymask[i] not in self._bymonthday)")
-        elif self._bynmonthday:
-            predlist.append("(ii.nmdaymask[i] not in self._bynmonthday)")
-        if self._byweekno:
-            predlist.append("(not ii.wnomask[i])")
-        if self._byyearday:
-            predlist.append("(i+1 not in self._byyearday)")
-        if self._bynweekday and self._freq in (FREQ_YEARLY, FREQ_MONTHLY):
-            predlist.append("(not ii.nwdaymask[i])")
-        if self._byeaster:
-            predlist.append("(ii.eastermask[i])")
-        if predlist:
-            self._pred = compile(" or ".join(predlist), "<string>", "eval")
-        else:
-            self._pred = compile("False", "<string>", "eval")
-
-    def _iter_cached(self):
-        i = 0
-        while True:
-            if i == len(self._cache):
-                self._cache_lock.acquire()
-                try:
-                    try:
-                        for j in range(10):
-                            self._cache.append(self._cache_gen.next())
-                    except StopIteration:
-                        self._cache.append(None)
-                finally:
-                    self._cache_lock.release()
-            item = self._cache[i]
-            if item is None:
-                return
-            else:
-                yield item
-            i += 1
-
     def _iter(self):
         year, month, day, hour, minute, second, weekday, yearday, _ = \
             self._dtstart.timetuple()
@@ -340,6 +430,7 @@
             else:
                 timeset = gettimeset(hour, minute, second)
 
+        total = 0
         count = self._count
         while True:
             # Get dayset with the right frequency
@@ -348,7 +439,6 @@
             # Do the "hard" work ;-)
             filtered = False
             for i in dayset[start:end]:
-                #if eval(self._pred):
                 if ((bymonth and ii.mmask[i] not in bymonth) or
                     (byweekno and not ii.wnomask[i]) or
                     (byyearday and (i%ii.yearlen)+1 not in byyearday) or
@@ -383,12 +473,15 @@
                 poslist.sort()
                 for res in poslist:
                     if until and res > until:
+                        self._len = total
                         return
                     elif res >= self._dtstart:
+                        total += 1
                         yield res
                         if count:
                             count -= 1
                             if not count:
+                                self._len = total
                                 return
             else:
                 for i in dayset[start:end]:
@@ -397,18 +490,24 @@
                         for time in timeset:
                             res = datetime.datetime.combine(date, time)
                             if until and res > until:
+                                self._len = total
                                 return
                             elif res >= self._dtstart:
+                                total += 1
                                 yield res
                                 if count:
                                     count -= 1
                                     if not count:
+                                        self._len = total
                                         return
 
             # Handle frequency and interval
             fixday = False
             if freq == FREQ_YEARLY:
                 year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
                 ii.rebuild(year, month)
             elif freq == FREQ_MONTHLY:
                 month += interval
@@ -419,6 +518,9 @@
                     if month == 0:
                         month = 12
                         year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
                 ii.rebuild(year, month)
             elif freq == FREQ_WEEKLY:
                 if wkst > weekday:
@@ -499,65 +601,12 @@
                         if month == 13:
                             month = 1
                             year += 1
+                            if year > datetime.MAXYEAR:
+                                self._len = total
+                                return
                         daysinmonth = calendar.monthrange(year, month)[1]
                     ii.rebuild(year, month)
 
-    def __iter__(self):
-        if self._cache is None:
-            return self._iter()
-        else:
-            return self._iter_cached()
-            
-    def before(self, dt, inc=False):
-        last = None
-        if inc:
-            for i in self:
-                if i > dt:
-                    break
-                last = i
-        else:
-            for i in self:
-                if i >= dt:
-                    break
-                last = i
-        return last
-
-    def after(self, dt, inc=False):
-        if inc:
-            for i in self:
-                if i >= dt:
-                    return i
-        else:
-            for i in self:
-                if i > dt:
-                    return i
-        return None
-
-    def between(self, after, before, inc=False):
-        started = False
-        l = []
-        if inc:
-            for i in self:
-                if not started:
-                    if i >= after:
-                        started = True
-                        l.append(i)
-                else:
-                    if i > before:
-                        break
-                    l.append(i)
-        else:
-            for i in self:
-                if not started:
-                    if i > after:
-                        started = True
-                        l.append(i)
-                else:
-                    if i >= before:
-                        break
-                    l.append(i)
-        return l
-
 class _iterinfo(object):
     __slots__ = ["rrule", "lastyear", "lastmonth",
                  "yearlen", "yearordinal", "yearweekday",
@@ -752,7 +801,7 @@
                 tzinfo=self.rrule._tzinfo),)
 
 
-class rruleset:
+class rruleset(rrulebase):
 
     class _genitem:
         def __init__(self, genlist, gen):
@@ -774,18 +823,12 @@
             return cmp(self.dt, other.dt)
 
     def __init__(self, cache=False):
+        rrulebase.__init__(self, cache)
         self._rrule = []
         self._rdate = []
         self._exrule = []
         self._exdate = []
 
-        if cache:
-            self._cache = []
-            self._cache_lock = thread.allocate_lock()
-            self._cache_gen  = self._iter()
-        else:
-            self._cache = None
-
     def rrule(self, rrule):
         self._rrule.append(rrule)
     
@@ -798,26 +841,6 @@
     def exdate(self, exdate):
         self._exdate.append(exdate)
 
-    def _iter_cached(self):
-        i = 0
-        while True:
-            if i == len(self._cache):
-                self._cache_lock.acquire()
-                try:
-                    try:
-                        for i in range(10):
-                            self._cache.append(self._cache_gen.next())
-                    except StopIteration:
-                        self._cache.append(None)
-                finally:
-                    self._cache_lock.release()
-            item = self._cache[i]
-            if item is None:
-                return
-            else:
-                yield item
-            i += 1
-
     def _iter(self):
         rlist = []
         self._genitem(rlist, iter(self._rdate).next)
@@ -830,6 +853,7 @@
             self._genitem(exlist, gen)
         exlist.sort()
         lastdt = None
+        total = 0
         while rlist:
             ritem = rlist[0]
             if not lastdt or lastdt != ritem.dt:
@@ -837,67 +861,12 @@
                     exlist[0].next()
                     exlist.sort()
                 if not exlist or ritem != exlist[0]:
+                    total += 1
                     yield ritem.dt
                 lastdt = ritem.dt
             ritem.next()
             rlist.sort()
-
-    def __iter__(self):
-        if self._cache is None:
-            return self._iter()
-        else:
-            return self._iter_cached()
-
-    def before(self, dt, inc=False):
-        last = None
-        if inc:
-            for i in self:
-                if i > dt:
-                    break
-                last = i
-        else:
-            for i in self:
-                if i >= dt:
-                    break
-                last = i
-        return last
-
-    def after(self, dt, inc=False):
-        if inc:
-            for i in self:
-                if i >= dt:
-                    return i
-        else:
-            for i in self:
-                if i > dt:
-                    return i
-        return None
-
-    def between(self, after, before, inc=False):
-        started = False
-        l = []
-        if inc:
-            for i in self:
-                if not started:
-                    if i >= after:
-                        started = True
-                        l.append(i)
-                else:
-                    if i > before:
-                        break
-                    l.append(i)
-        else:
-            for i in self:
-                if not started:
-                    if i > after:
-                        started = True
-                        l.append(i)
-                else:
-                    if i >= before:
-                        break
-                    l.append(i)
-        return l
-
+        self._len = total
 
 class _rrulestr:
 
diff --git a/test.py b/test.py
index 74b9820..f2f6fd0 100644
--- a/test.py
+++ b/test.py
@@ -2287,6 +2287,115 @@
                           datetime(1997, 9, 3, 9, 0),
                           datetime(1997, 9, 4, 9, 0)])
 
+    def testMaxYear(self):
+        self.assertEqual(list(rrule(FREQ_YEARLY,
+                              count=3,
+                              bymonth=2,
+                              bymonthday=31,
+                              dtstart=parse("99970902T090000"))),
+                         [])
+
+    def testGetItem(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               count=3,
+                               dtstart=parse("19970902T090000"))[0],
+                         datetime(1997, 9, 2, 9, 0))
+
+    def testGetItemNeg(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               count=3,
+                               dtstart=parse("19970902T090000"))[-1],
+                         datetime(1997, 9, 4, 9, 0))
+
+    def testGetItemSlice(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               #count=3,
+                               dtstart=parse("19970902T090000"))[1:2],
+                         [datetime(1997, 9, 3, 9, 0)])
+
+    def testGetItemSliceEmpty(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               count=3,
+                               dtstart=parse("19970902T090000"))[:],
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0)])
+
+    def testGetItemSliceStep(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               count=3,
+                               dtstart=parse("19970902T090000"))[::-2],
+                         [datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 2, 9, 0)])
+
+    def testCount(self):
+        self.assertEqual(rrule(FREQ_DAILY,
+                               count=3,
+                               dtstart=parse("19970902T090000")).count(),
+                         3)
+
+    def testCachePre(self):
+        rr = rrule(FREQ_DAILY, count=15, cache=True,
+                   dtstart=parse("19970902T090000"))
+        self.assertEqual(list(rr),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testCachePost(self):
+        rr = rrule(FREQ_DAILY, count=15, cache=True,
+                   dtstart=parse("19970902T090000"))
+        for x in rr: pass
+        self.assertEqual(list(rr),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
+    def testCachePostInternal(self):
+        rr = rrule(FREQ_DAILY, count=15, cache=True,
+                   dtstart=parse("19970902T090000"))
+        for x in rr: pass
+        self.assertEqual(rr._cache,
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 3, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 5, 9, 0),
+                          datetime(1997, 9, 6, 9, 0),
+                          datetime(1997, 9, 7, 9, 0),
+                          datetime(1997, 9, 8, 9, 0),
+                          datetime(1997, 9, 9, 9, 0),
+                          datetime(1997, 9, 10, 9, 0),
+                          datetime(1997, 9, 11, 9, 0),
+                          datetime(1997, 9, 12, 9, 0),
+                          datetime(1997, 9, 13, 9, 0),
+                          datetime(1997, 9, 14, 9, 0),
+                          datetime(1997, 9, 15, 9, 0),
+                          datetime(1997, 9, 16, 9, 0)])
+
     def testSet(self):
         set = rruleset()
         set.rrule(rrule(FREQ_YEARLY, count=2, byweekday=TU,
@@ -2363,6 +2472,49 @@
                           datetime(1997, 9, 9, 9, 0),
                           datetime(1997, 9, 16, 9, 0)])
 
+    def testSetCount(self):
+        set = rruleset()
+        set.rrule(rrule(FREQ_YEARLY, count=6, byweekday=(TU,TH),
+                        dtstart=parse("19970902T090000")))
+        set.exrule(rrule(FREQ_YEARLY, count=3, byweekday=TH,
+                        dtstart=parse("19970902T090000")))
+        self.assertEqual(set.count(), 3)
+
+    def testSetCachePre(self):
+        set = rruleset()
+        set.rrule(rrule(FREQ_YEARLY, count=2, byweekday=TU,
+                        dtstart=parse("19970902T090000")))
+        set.rrule(rrule(FREQ_YEARLY, count=1, byweekday=TH,
+                        dtstart=parse("19970902T090000")))
+        self.assertEqual(list(set),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetCachePost(self):
+        set = rruleset(cache=True)
+        set.rrule(rrule(FREQ_YEARLY, count=2, byweekday=TU,
+                        dtstart=parse("19970902T090000")))
+        set.rrule(rrule(FREQ_YEARLY, count=1, byweekday=TH,
+                        dtstart=parse("19970902T090000")))
+        for x in set: pass
+        self.assertEqual(list(set),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
+    def testSetCachePostInternal(self):
+        set = rruleset(cache=True)
+        set.rrule(rrule(FREQ_YEARLY, count=2, byweekday=TU,
+                        dtstart=parse("19970902T090000")))
+        set.rrule(rrule(FREQ_YEARLY, count=1, byweekday=TH,
+                        dtstart=parse("19970902T090000")))
+        for x in set: pass
+        self.assertEqual(list(set._cache),
+                         [datetime(1997, 9, 2, 9, 0),
+                          datetime(1997, 9, 4, 9, 0),
+                          datetime(1997, 9, 9, 9, 0)])
+
     def testStr(self):
         self.assertEqual(list(rrulestr(
                               "DTSTART:19970902T090000\n"