66from itertools import islice as islicef
77
88from .fup import fupdate
9+ from .it import first , lastn , butlastn
10+ from .misc import CountingIterator
911
1012def islice (iterable ):
11- """Use itertools.islice with slice syntax.
13+ """Use itertools.islice with slice syntax, with some bonus features .
1214
1315 Usage::
1416
1517 islice(iterable)[idx_or_slice]
1618
17- The slicing variant calls ``itertools.islice`` with the corresponding
18- slicing parameters.
19+ For convenience:
1920
20- As a convenience feature: a single index is interpreted as a length-1 islice
21- starting at that index. The slice is then immediately evaluated and the item
22- is returned.
21+ - Negative ``start``, ``stop`` are supported. **CAUTION**: using a negative
22+ start or stop will force the iterable, because that is the only way to
23+ know its length.
24+
25+ - A single index (negative also allowed) is interpreted as a length-1
26+ islice starting at that index. The slice is then immediately evaluated
27+ and the item is returned.
28+
29+ Once negative indices have been handled, the slicing variant calls
30+ ``itertools.islice`` with the corresponding slicing parameters.
2331
2432 Examples::
2533
@@ -37,9 +45,10 @@ def islice(iterable):
3745 assert tuple(islice(odds)[:5]) == (1, 3, 5, 7, 9)
3846 assert tuple(islice(odds)[:5]) == (11, 13, 15, 17, 19) # five more
3947
40- **CAUTION**: Keep in mind ``itertools.islice`` does not support negative
41- indexing for any of ``start``, ``stop`` or ``step``, and that the slicing
42- process consumes elements from the iterable.
48+ **CAUTION**: Keep in mind the slicing process consumes elements from the
49+ iterable.
50+
51+ **CAUTION**: ``step``, if present, must be positive.
4352 """
4453 # manually curry to take indices later, but expect them in subscript syntax to support slicing
4554 class islice1 :
@@ -48,10 +57,50 @@ def __getitem__(self, k):
4857 if isinstance (k , tuple ):
4958 raise TypeError ("multidimensional indexing not supported, got {}" .format (k ))
5059 if isinstance (k , slice ):
51- return islicef (iterable , k .start , k .stop , k .step )
52- return tuple (islicef (iterable , k , k + 1 ))[0 ]
60+ start , stop , step = k .start , k .stop , k .step
61+ it = iter (iterable )
62+ # One or both of start and stop may be negative or None.
63+ # Step must be positive; filter first, then slice normally.
64+ #
65+ # A general iterable doesn't know its length (might not even be
66+ # knowable; it may be a generator), so if we get a negative
67+ # start or stop, the only way to find the correct position is
68+ # to force the iterable until it ends (if ever).
69+ if start and start < 0 and stop and stop < 0 :
70+ it = butlastn (- stop , lastn (- start , iterable ))
71+ start = stop = None
72+ elif start and start < 0 :
73+ n , start = - start , None
74+ if not stop :
75+ it = lastn (n , iterable )
76+ else : # stop and stop > 0:
77+ # to adjust stop, we must know how many items are dropped
78+ cit = CountingIterator (iterable )
79+ it = tuple (lastn (n , cit )) # force to actually count (note lastn stores only <= n items)
80+ n_dropped = max (0 , cit .count - n ) # max needed if start is past the start of iterable
81+ stop -= n_dropped
82+ assert stop >= 0
83+ elif stop and stop < 0 :
84+ it = butlastn (- stop , iterable )
85+ stop = None
86+ return islicef (it , start , stop , step )
87+ if k < 0 :
88+ return first (lastn (- k , iterable ))
89+ return first (islicef (iterable , k , k + 1 ))
5390 return islice1 ()
5491
92+ # Basic idea, no negative index support:
93+ # def islice(iterable):
94+ # class islice1:
95+ # """Subscript me to perform the slicing."""
96+ # def __getitem__(self, k):
97+ # if isinstance(k, tuple):
98+ # raise TypeError("multidimensional indexing not supported, got {}".format(k))
99+ # if isinstance(k, slice):
100+ # return islicef(iterable, k.start, k.stop, k.step)
101+ # return first(islicef(iterable, k, k + 1))
102+ # return islice1()
103+
55104def fup (seq ):
56105 """Functionally update a sequence.
57106
0 commit comments