-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve range: refactor, support start as an optional kwarg, clearer docs and error messages #38041
Changes from all commits
62e629f
208eaf0
8d5c3ba
a5ef905
50734fd
3796650
cc25cf3
8c327e9
519ef36
87e8faf
7712b4c
083a4f6
1097661
ba32fef
6a0e25e
7500cdd
0c5fc36
307d59b
65898ed
01f6ccc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,25 +47,16 @@ function _colon(start::T, step, stop::T) where T | |
end | ||
|
||
""" | ||
range(start[, stop]; length, stop, step=1) | ||
range(start, stop; length, step) | ||
range(start; length, stop, step) | ||
range(;start, length, stop, step) | ||
|
||
Given a starting value, construct a range either by length or from `start` to `stop`, | ||
optionally with a given step (defaults to 1, a [`UnitRange`](@ref)). | ||
One of `length` or `stop` is required. If `length`, `stop`, and `step` are all specified, they must agree. | ||
|
||
If `length` and `stop` are provided and `step` is not, the step size will be computed | ||
automatically such that there are `length` linearly spaced elements in the range. | ||
|
||
If `step` and `stop` are provided and `length` is not, the overall range length will be computed | ||
automatically such that the elements are `step` spaced. | ||
|
||
Special care is taken to ensure intermediate values are computed rationally. | ||
To avoid this induced overhead, see the [`LinRange`](@ref) constructor. | ||
|
||
`stop` may be specified as either a positional or keyword argument. | ||
|
||
!!! compat "Julia 1.1" | ||
`stop` as a positional argument requires at least Julia 1.1. | ||
Construct a specialized array with evenly spaced elements and optimized storage (an [`AbstractRange`](@ref)) from the arguments. | ||
Mathematically a range is uniquely determined by any three of `start`, `step`, `stop` and `length`. | ||
Valid invocations of range are: | ||
* Call `range` with any three of `start`, `step`, `stop`, `length`. | ||
* Call `range` with two of `start`, `stop`, `length`. In this case `step` will be assumed | ||
to be one. If both arguments are Integers, a [`UnitRange`](@ref) will be returned. | ||
|
||
# Examples | ||
```jldoctest | ||
|
@@ -86,51 +77,139 @@ julia> range(1, 10, length=101) | |
|
||
julia> range(1, 100, step=5) | ||
1:5:96 | ||
|
||
julia> range(stop=10, length=5) | ||
6:10 | ||
|
||
julia> range(stop=10, step=1, length=5) | ||
6:1:10 | ||
|
||
julia> range(start=1, step=1, stop=10) | ||
1:1:10 | ||
``` | ||
If `length` is not specified and `stop - start` is not an integer multiple of `step`, a range that ends before `stop` will be produced. | ||
```jldoctest | ||
julia> range(1, 3.5, step=2) | ||
1.0:2.0:3.0 | ||
``` | ||
|
||
Special care is taken to ensure intermediate values are computed rationally. | ||
To avoid this induced overhead, see the [`LinRange`](@ref) constructor. | ||
|
||
Both `start` and `stop` may be specified as either a positional or keyword arguments. | ||
If both are specified as positional arguments, one of `step` or `length` must also be provided. | ||
|
||
!!! compat "Julia 1.1" | ||
`stop` as a positional argument requires at least Julia 1.1. | ||
|
||
!!! compat "Julia 1.7" | ||
`start` as a keyword argument requires at least Julia 1.7. | ||
""" | ||
range(start; length::Union{Integer,Nothing}=nothing, stop=nothing, step=nothing) = | ||
function range end | ||
|
||
range(start; stop=nothing, length::Union{Integer,Nothing}=nothing, step=nothing) = | ||
_range(start, step, stop, length) | ||
|
||
range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing) = | ||
_range2(start, step, stop, length) | ||
function range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing) | ||
# For code clarity, the user must pass step or length | ||
# See https://github.com/JuliaLang/julia/pull/28708#issuecomment-420034562 | ||
if step === length === nothing | ||
msg = """ | ||
Neither `step` nor `length` was provided. To fix this do one of the following: | ||
* Pass one of them | ||
* Use `$(start):$(stop)` | ||
* Use `range($start, stop=$stop)` | ||
""" | ||
throw(ArgumentError(msg)) | ||
end | ||
_range(start, step, stop, length) | ||
end | ||
|
||
_range2(start, ::Nothing, stop, ::Nothing) = | ||
throw(ArgumentError("At least one of `length` or `step` must be specified")) | ||
range(;start=nothing, stop=nothing, length::Union{Integer, Nothing}=nothing, step=nothing) = | ||
_range(start, step, stop, length) | ||
|
||
_range2(start, step, stop, length) = _range(start, step, stop, length) | ||
_range(start::Nothing, step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Nothing, stop::Nothing, len::Any ) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Nothing, stop::Any , len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Nothing, stop::Any , len::Any ) = range_stop_length(stop, len) | ||
_range(start::Nothing, step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Any , stop::Nothing, len::Any ) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Any , stop::Any , len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Nothing, step::Any , stop::Any , len::Any ) = range_step_stop_length(step, stop, len) | ||
_range(start::Any , step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Any , step::Nothing, stop::Nothing, len::Any ) = range_start_length(start, len) | ||
_range(start::Any , step::Nothing, stop::Any , len::Nothing) = range_start_stop(start, stop) | ||
_range(start::Any , step::Nothing, stop::Any , len::Any ) = range_start_stop_length(start, stop, len) | ||
_range(start::Any , step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len) | ||
_range(start::Any , step::Any , stop::Nothing, len::Any ) = range_start_step_length(start, step, len) | ||
_range(start::Any , step::Any , stop::Any , len::Nothing) = range_start_step_stop(start, step, stop) | ||
_range(start::Any , step::Any , stop::Any , len::Any ) = range_error(start, step, stop, len) | ||
Comment on lines
+131
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it looks cool 😛 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could use some comments. I think How about something like this: # range( )
_range(start::Nothing, step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
# range(; length )
_range(start::Nothing, step::Nothing, stop::Nothing, len::Any ) = range_error(start, step, stop, len)
# range(; stop )
_range(start::Nothing, step::Nothing, stop::Any , len::Nothing) = range_error(start, step, stop, len)
# range(; stop, length )
_range(start::Nothing, step::Nothing, stop::Any , len::Any ) = range_stop_length(stop, len) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks very self-documenting to my eyes with a clear pattern. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is navigable to me, but I need to parse it to find the correct line. I'm happy with how it is though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I put some thought into how to write down this dispatch mechanic. I also considered using |
||
|
||
range_stop_length(stop, length) = (stop-length+1):stop | ||
|
||
range_step_stop_length(step, stop, length) = reverse(range_start_step_length(stop, -step, length)) | ||
|
||
range_start_length(a::Real, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1)) | ||
range_start_length(a::AbstractFloat, len::Integer) = range_start_step_length(a, oftype(a, 1), len) | ||
range_start_length(a, len::Integer) = range_start_step_length(a, oftype(a-a, 1), len) | ||
|
||
range_start_stop(start, stop) = start:stop | ||
|
||
function range_start_step_length(a::AbstractFloat, step::AbstractFloat, len::Integer) | ||
range_start_step_length(promote(a, step)..., len) | ||
end | ||
|
||
# Range from start to stop: range(a, [step=s,] stop=b), no length | ||
_range(start, step, stop, ::Nothing) = (:)(start, step, stop) | ||
_range(start, ::Nothing, stop, ::Nothing) = (:)(start, stop) | ||
function range_start_step_length(a::Real, step::AbstractFloat, len::Integer) | ||
range_start_step_length(float(a), step, len) | ||
end | ||
|
||
# Range of a given length: range(a, [step=s,] length=l), no stop | ||
_range(a::Real, ::Nothing, ::Nothing, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1)) | ||
_range(a::AbstractFloat, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a, 1), nothing, len) | ||
_range(a::AbstractFloat, st::AbstractFloat, ::Nothing, len::Integer) = _range(promote(a, st)..., nothing, len) | ||
_range(a::Real, st::AbstractFloat, ::Nothing, len::Integer) = _range(float(a), st, nothing, len) | ||
_range(a::AbstractFloat, st::Real, ::Nothing, len::Integer) = _range(a, float(st), nothing, len) | ||
_range(a, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a-a, 1), nothing, len) | ||
function range_start_step_length(a::AbstractFloat, step::Real, len::Integer) | ||
range_start_step_length(a, float(step), len) | ||
end | ||
|
||
_range(a::T, step::T, ::Nothing, len::Integer) where {T <: AbstractFloat} = | ||
function range_start_step_length(a::T, step::T, len::Integer) where {T <: AbstractFloat} | ||
_rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len) | ||
_range(a::T, step, ::Nothing, len::Integer) where {T} = | ||
end | ||
|
||
function range_start_step_length(a::T, step, len::Integer) where {T} | ||
_rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len) | ||
end | ||
|
||
_rangestyle(::Ordered, ::ArithmeticWraps, a::T, step::S, len::Integer) where {T,S} = | ||
StepRange{typeof(a+zero(step)),S}(a, step, a+step*(len-1)) | ||
_rangestyle(::Any, ::Any, a::T, step::S, len::Integer) where {T,S} = | ||
StepRangeLen{typeof(a+zero(step)),T,S}(a, step, len) | ||
|
||
# Malformed calls | ||
_range(start, step, ::Nothing, ::Nothing) = # range(a, step=s) | ||
throw(ArgumentError("At least one of `length` or `stop` must be specified")) | ||
_range(start, ::Nothing, ::Nothing, ::Nothing) = # range(a) | ||
throw(ArgumentError("At least one of `length` or `stop` must be specified")) | ||
_range(::Nothing, ::Nothing, ::Nothing, ::Nothing) = # range(nothing) | ||
throw(ArgumentError("At least one of `length` or `stop` must be specified")) | ||
_range(start::Real, step::Real, stop::Real, length::Integer) = # range(a, step=s, stop=b, length=l) | ||
throw(ArgumentError("Too many arguments specified; try passing only one of `stop` or `length`")) | ||
_range(::Nothing, ::Nothing, ::Nothing, ::Integer) = # range(nothing, length=l) | ||
throw(ArgumentError("Can't start a range at `nothing`")) | ||
range_start_step_stop(start, step, stop) = start:step:stop | ||
|
||
function range_error(start, step, stop, length) | ||
hasstart = start !== nothing | ||
hasstep = step !== nothing | ||
hasstop = stop !== nothing | ||
haslength = start !== nothing | ||
|
||
hint = if hasstart && hasstep && hasstop && haslength | ||
"Try specifying only three arguments" | ||
elseif !hasstop && !haslength | ||
"At least one of `length` or `stop` must be specified." | ||
elseif !hasstep && !haslength | ||
"At least one of `length` or `step` must be specified." | ||
elseif !hasstart && !hasstop | ||
"At least one of `start` or `stop` must be specified." | ||
else | ||
"Try specifying more arguments." | ||
end | ||
|
||
msg = """ | ||
Cannot construct range from arguments: | ||
start = $start | ||
step = $step | ||
stop = $stop | ||
length = $length | ||
$hint | ||
""" | ||
throw(ArgumentError(msg)) | ||
end | ||
|
||
## 1-dimensional ranges ## | ||
|
||
|
@@ -432,13 +511,13 @@ function LinRange(start, stop, len::Integer) | |
LinRange{T}(start, stop, len) | ||
end | ||
|
||
function _range(start::T, ::Nothing, stop::S, len::Integer) where {T,S} | ||
function range_start_stop_length(start::T, stop::S, len::Integer) where {T,S} | ||
a, b = promote(start, stop) | ||
_range(a, nothing, b, len) | ||
range_start_stop_length(a, b, len) | ||
end | ||
_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len) | ||
_range(start::T, ::Nothing, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len) | ||
_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Integer} = | ||
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len) | ||
range_start_stop_length(start::T, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len) | ||
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Integer} = | ||
_linspace(float(T), start, stop, len) | ||
## for Float16, Float32, and Float64 we hit twiceprecision.jl to lift to higher precision StepRangeLen | ||
# for all other types we fall back to a plain old LinRange | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 as a default start value sounds like a good idea. Should be a separate PR however. I really don t want to delay this one further.