Getting a closed interval is awkward. Let's say you want an array of the 
integers from a to b, inclusive at both ends. e.g. for a=10 b=15, you want:

[10, 11, 12, 13, 14, 15]

Neither linspace nor arange makes this easy.

arange(a, b) # wrong, it only goes up to 14
arange(a, b+1) # right, but awkward. you have to remember to add one
linspace(a, b, b-a) # wrong, will produce fractions
linspace(a, b, b-a, endpoint=False) # wrong, only goes up to 14
linspace(a, b, b-a+1) # right, awkward. also it gives you floats by default

Both ways have the potential for an off-by-one error. I forget the right way to 
do it and have to look it up, pretty much every time.

It has been proposed before to add an step parameter to linspace or an endpoint 
parameter to arange: https://github.com/numpy/numpy/issues/630 but it was 
rejected on the grounds that it's too hard / impossible to make it work 
properly with floats.

But that's not a problem if the function is restricted to only give integers in 
the first place. There's a few ways this can be done; one is to make a function 
"crange" (closed range):

crange(10, 15) # 10, 11, 12, 13, 14, 15

Like arange it would take a step parameter, defaulting to 1. Unlike arange, the 
start, stop, and step parameters must all be integers.

This raises the question of what to do when the step size doesn't evenly divide 
the range. What should crange(10, 15, 3) return? There's no clear answer. There 
are three constraints we might be interested in:

1: for 0 <= i < len(array)-1: array[i] + step == array[i+1]
2: for x in array: start <= x <= stop
3: array[0] == start, array[-1] == stop

But for crange(10, 15, 3) we can only choose two:

[10, 13, 15] # violates 1, satisfies 2 and 3
[10, 13] # violates 2, satisfies 1 and 3
[10, 13, 16] # violates 3, satisfies 1 and 2

Depending on the situation you might plausibly want any one of these. 
Furthermore, there is the symmetric set of possibilities where we fix the stop 
bound, and step backwards from it, letting the start bound vary:

[10, 12, 15] # violates 1, satisfies 2 and 3
[9, 12, 15] # violates 2, satisfies 1 and 3
[12, 15] # violates 3, satisfies 1 and 2

There are two axes of behaviour here: which bound we should "anchor", and what 
to do with the other bound. We can "anchor" either the start or the stop, and 
with the other bound we have three choices: "open", "closed", or "exact". Since 
"crange" now seems wrongly named, call it "intrange":

def intrange(start: int, stop: int, step: int = 1, bound: str = "open", anchor: 
str = "start", dtype=None): ...

Would behave like:

intrange(10, 15, 3)
[10, 13] # same as arange
intrange(10, 15, 3, bound="closed")
[10, 13, 16]
intrange(10, 15, 3, bound="exact")
[10, 13, 15]
intrange(10, 15, 3, bound="open", anchor="stop")
[12, 15]
intrange(10, 15, 3, bound="closed", anchor="stop")
[9, 12, 15]
intrange(10, 15, 3, bound="exact", anchor="stop")
[10, 12, 15]

Anyway, the two most common kinds of integer range you'd want to make are easy 
.. mostly you just want step=1, inclusive at the start, and either exclusive or 
inclusive at the end.

intrange(10, 15)
[10, 11, 12, 13, 14] # same as arange
intrange(10, 15, bound="closed")
[10, 11, 12, 13, 14, 15] # easy to remember, intuitive, no off-by-one-error

>From there, you can make inclusive floating point ranges by scaling:

intrange(10, 15, bound="closed") / 10
[1.0, 1.1, 1.2, 1.3, 1.4, 1.5]

This way, we don't have to worry about floating point imprecision screwing up 
the bounds calculation.

Sketch implementation:

def intrange(start, stop, step=1, bound="open", anchor="start"):
    match (anchor, bound):
        case ("start", "open"):
            return np.arange(start, stop, step)
        case ("start", "closed"):
            return np.arange(start, stop + step, step)
        case ("start", "exact"):
            result = np.arange(start, stop + step, step)
            result[-1] = stop
            return result
        case ("stop", "open"):
            return np.flip(np.arange(stop, start, -step))
        case ("stop", "closed"):
            return np.flip(np.arange(stop, start - step, -step))
        case ("stop", "exact"):
            result = np.flip(np.arange(stop, start - step, -step))
            result[0] = start
            return result
        case _:
            assert False
_______________________________________________
NumPy-Discussion mailing list -- numpy-discussion@python.org
To unsubscribe send an email to numpy-discussion-le...@python.org
https://mail.python.org/mailman3/lists/numpy-discussion.python.org/
Member address: arch...@mail-archive.com

Reply via email to