Skip to content

Commit 9ab1afa

Browse files
committed
Add negative start, stop support for unpythonic.slicing.islice
Resolves #25.
1 parent e2225e4 commit 9ab1afa

File tree

2 files changed

+88
-11
lines changed

2 files changed

+88
-11
lines changed

unpythonic/slicing.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@
66
from itertools import islice as islicef
77

88
from .fup import fupdate
9+
from .it import first, lastn, butlastn
10+
from .misc import CountingIterator
911

1012
def 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+
55104
def fup(seq):
56105
"""Functionally update a sequence.
57106

unpythonic/test/test_slicing.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ def test():
3030
assert tuple(islice(odds)[:5]) == (1, 3, 5, 7, 9)
3131
assert tuple(islice(odds)[:5]) == (11, 13, 15, 17, 19) # five more
3232

33+
# negative start, stop in islice
34+
#
35+
# !! step must be positive !!
36+
#
37+
# CAUTION: will force the iterable (at the latest at the time when the
38+
# first item is read from it), since that's the only way to know where it
39+
# ends, if at all.
40+
assert tuple(islice(range(10))[-3:]) == (7, 8, 9) # start < 0, no stop
41+
assert tuple(islice(range(10))[-3:9]) == (7, 8) # start < 0, stop > 0, before end
42+
assert tuple(islice(range(10))[-2:10]) == (8, 9) # start < 0, stop > 0, at end
43+
assert tuple(islice(range(10))[-2:20]) == (8, 9) # start < 0, stop > 0, beyond end
44+
45+
assert tuple(islice(range(10))[:-8]) == (0, 1) # no start, stop < 0
46+
assert tuple(islice(range(10))[6:-2]) == (6, 7) # start > 0, stop < 0
47+
assert tuple(islice(range(10))[10:-2]) == () # start > 0, at end, stop < 0
48+
assert tuple(islice(range(10))[20:-2]) == () # start > 0, beyond end, stop < 0
49+
50+
assert tuple(islice(range(10))[-8:-4]) == (2, 3, 4, 5) # start < 0, stop < 0
51+
52+
assert tuple(islice(range(10))[-6::2]) == (4, 6, 8) # step
53+
assert tuple(islice(range(10))[:-2:2]) == (0, 2, 4, 6) # step
54+
assert tuple(islice(range(10))[-8:-2:3]) == (2, 5) # step
55+
56+
# edge cases, should behave like list does
57+
assert tuple(islice(range(10))[:-20]) == () # stop < 0, past the start of the iterable
58+
assert tuple(islice(range(10))[-20:]) == tuple(range(10)) # start < 0, past the start of the iterable
59+
assert tuple(islice(range(10))[-20:5]) == tuple(range(5)) # same, but with stop > 0
60+
3361
print("All tests PASSED")
3462

3563
if __name__ == '__main__':

0 commit comments

Comments
 (0)