Make Python slice sane (positive/forward + no Nones + no negative indices + within bounds)

When implementing classes in Python that shall be sliceable with standard Python syntax (i.e. negative indices, stepping, etc.) it sometimes can be helpful to convert the slice into a "sane, forward slice" to determine the elements of the slice. How can one write such a function in a concise / elegant form?

With a "sane, forward slice" I mean a slice that is equivalent to the initial slice in the sense that the resulting elements are the same, but has a positive step, no negative indices, and also no indices that are larger than the object's length.

Examples (assuming an array length of 10):

slice(None,   4,None)  ->  slice( 0, 4, 1)
slice(  -7,  12,   1)  ->  slice( 3,10, 1)
slice(None,None,  -1)  ->  slice( 0,10, 1)
slice(   7,   3,  -2)  ->  slice( 5, 8, 2)
slice(   9,   1,  -3)  ->  slice( 3,10, 3)

It is not extremely difficult to write a functions that performs such a conversion, but I was not able to write it concise. Especially determining the start index when converting a "backwards slice" into an equivalent "forward slice" seems to be quite cumbersome.

Working example:

def to_sane_slice(s, N):
    step = s.step if s.step is not None else 1
    if step == 0:
        ValueError('Slice step cannot be 0!')

    # get first index
    first = s.start
    if first is None:
        first = 0 if step > 0 else N-1
    elif first < 0:
        first = N+first
        if first < 0:
            if step < 0:
                return slice(0,0,1)
            first = 0
    elif first >= N:
        if step > 0:
            return slice(0,0,1)
        first = N-1
    # get stop index
    stop = s.stop
    if stop is None:
        stop = N if step > 0 else -1
    elif stop < 0:
        stop = max(-1, N+stop)

    # check for etmpy slices
    if (stop-first)*step <= 0:
        return slice(0,0,1)

    if step > 0:
        return slice(first, min(stop,N), step)
    elif step == -1:
        return slice(stop+1, first+1, -step)
    else:
        # calculate the new start -- does not have to be next to old stop, since
        # the stepping might lead elsewhere
        step = -step
        dist = first - max(stop,-1)
        last = first - dist / step * step
        if dist % step == 0:
            last += step
        return slice(last, first+1, step)

.
.
.

Edit (final function):

With the use of slice.indices() to which @doublep kindly pointed me it becomes:

def to_sane_slice(s, N):
    # get rid of None's, overly large indices, and negative indices (except -1 for
    # backward slices that go down to first element)
    start, stop, step = s.indices(N)

    # get number of steps & remaining
    n, r = divmod(stop - start, step)
    if n < 0 or (n==0 and r==0):
        return slice(0,0,1)
    if r != 0: # its a "stop" index, not an last index
        n += 1

    if step < 0:
        start, stop, step = start+(n-1)*step, start-step, -step
    else: # step > 0, step == 0 is not allowed
        stop = start+n*step
    stop = min(stop, N)

    return slice(start, stop, step)

Upvotes: 1

Views: 540

Answers (1)

user319799
user319799

Reputation:

Use slice.indices() method.

S.indices(len) -> (start, stop, stride)

Assuming a sequence of length len, calculate the start and stop indices, and the stride length of the extended slice described by S. Out of bounds indices are clipped in a manner consistent with the handling of normal slices.

Upvotes: 3

Related Questions