Skip to content

Commit 97bd48c

Browse files
jw3126mcabbottjohnnychen94mkittimbauman
authored
Improve range: refactor, support start as an optional kwarg, clearer docs and error messages (#38041)
Mathematically a range is uniquely determined by three out of four of start, step, stop, length. Furthermore if one assumes step=1 any combination of two others macthematically suffices to specify a range. With this PR the range function reflects this. Any combination of three (two non step) arguments will be accepted. Co-authored-by: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Co-authored-by: Johnny Chen <johnnychen94@hotmail.com> Co-authored-by: Mark Kittisopikul <mkitti@users.noreply.github.com> Co-authored-by: Matt Bauman <mbauman@juliacomputing.com>
1 parent c1f41ad commit 97bd48c

File tree

4 files changed

+154
-54
lines changed

4 files changed

+154
-54
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Standard library changes
4040
------------------------
4141

4242
* `count` and `findall` now accept an `AbstractChar` argument to search for a character in a string ([#38675]).
43+
* `range` now supports `start` as an optional keyword argument ([#38041]).
4344
* `islowercase` and `isuppercase` are now compliant with the Unicode lower/uppercase categories ([#38574]).
4445
* `iseven` and `isodd` functions now support non-`Integer` numeric types ([#38976]).
4546
* `escape_string` can now receive a collection of characters in the keyword

base/range.jl

Lines changed: 131 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,16 @@ function _colon(start::T, step, stop::T) where T
4747
end
4848

4949
"""
50-
range(start[, stop]; length, stop, step=1)
50+
range(start, stop; length, step)
51+
range(start; length, stop, step)
52+
range(;start, length, stop, step)
5153
52-
Given a starting value, construct a range either by length or from `start` to `stop`,
53-
optionally with a given step (defaults to 1, a [`UnitRange`](@ref)).
54-
One of `length` or `stop` is required. If `length`, `stop`, and `step` are all specified, they must agree.
55-
56-
If `length` and `stop` are provided and `step` is not, the step size will be computed
57-
automatically such that there are `length` linearly spaced elements in the range.
58-
59-
If `step` and `stop` are provided and `length` is not, the overall range length will be computed
60-
automatically such that the elements are `step` spaced.
61-
62-
Special care is taken to ensure intermediate values are computed rationally.
63-
To avoid this induced overhead, see the [`LinRange`](@ref) constructor.
64-
65-
`stop` may be specified as either a positional or keyword argument.
66-
67-
!!! compat "Julia 1.1"
68-
`stop` as a positional argument requires at least Julia 1.1.
54+
Construct a specialized array with evenly spaced elements and optimized storage (an [`AbstractRange`](@ref)) from the arguments.
55+
Mathematically a range is uniquely determined by any three of `start`, `step`, `stop` and `length`.
56+
Valid invocations of range are:
57+
* Call `range` with any three of `start`, `step`, `stop`, `length`.
58+
* Call `range` with two of `start`, `stop`, `length`. In this case `step` will be assumed
59+
to be one. If both arguments are Integers, a [`UnitRange`](@ref) will be returned.
6960
7061
# Examples
7162
```jldoctest
@@ -86,51 +77,139 @@ julia> range(1, 10, length=101)
8677
8778
julia> range(1, 100, step=5)
8879
1:5:96
80+
81+
julia> range(stop=10, length=5)
82+
6:10
83+
84+
julia> range(stop=10, step=1, length=5)
85+
6:1:10
86+
87+
julia> range(start=1, step=1, stop=10)
88+
1:1:10
8989
```
90+
If `length` is not specified and `stop - start` is not an integer multiple of `step`, a range that ends before `stop` will be produced.
91+
```jldoctest
92+
julia> range(1, 3.5, step=2)
93+
1.0:2.0:3.0
94+
```
95+
96+
Special care is taken to ensure intermediate values are computed rationally.
97+
To avoid this induced overhead, see the [`LinRange`](@ref) constructor.
98+
99+
Both `start` and `stop` may be specified as either a positional or keyword arguments.
100+
If both are specified as positional arguments, one of `step` or `length` must also be provided.
101+
102+
!!! compat "Julia 1.1"
103+
`stop` as a positional argument requires at least Julia 1.1.
104+
105+
!!! compat "Julia 1.7"
106+
`start` as a keyword argument requires at least Julia 1.7.
90107
"""
91-
range(start; length::Union{Integer,Nothing}=nothing, stop=nothing, step=nothing) =
108+
function range end
109+
110+
range(start; stop=nothing, length::Union{Integer,Nothing}=nothing, step=nothing) =
92111
_range(start, step, stop, length)
93112

94-
range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing) =
95-
_range2(start, step, stop, length)
113+
function range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing)
114+
# For code clarity, the user must pass step or length
115+
# See https://github.com/JuliaLang/julia/pull/28708#issuecomment-420034562
116+
if step === length === nothing
117+
msg = """
118+
Neither `step` nor `length` was provided. To fix this do one of the following:
119+
* Pass one of them
120+
* Use `$(start):$(stop)`
121+
* Use `range($start, stop=$stop)`
122+
"""
123+
throw(ArgumentError(msg))
124+
end
125+
_range(start, step, stop, length)
126+
end
96127

97-
_range2(start, ::Nothing, stop, ::Nothing) =
98-
throw(ArgumentError("At least one of `length` or `step` must be specified"))
128+
range(;start=nothing, stop=nothing, length::Union{Integer, Nothing}=nothing, step=nothing) =
129+
_range(start, step, stop, length)
99130

100-
_range2(start, step, stop, length) = _range(start, step, stop, length)
131+
_range(start::Nothing, step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
132+
_range(start::Nothing, step::Nothing, stop::Nothing, len::Any ) = range_error(start, step, stop, len)
133+
_range(start::Nothing, step::Nothing, stop::Any , len::Nothing) = range_error(start, step, stop, len)
134+
_range(start::Nothing, step::Nothing, stop::Any , len::Any ) = range_stop_length(stop, len)
135+
_range(start::Nothing, step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
136+
_range(start::Nothing, step::Any , stop::Nothing, len::Any ) = range_error(start, step, stop, len)
137+
_range(start::Nothing, step::Any , stop::Any , len::Nothing) = range_error(start, step, stop, len)
138+
_range(start::Nothing, step::Any , stop::Any , len::Any ) = range_step_stop_length(step, stop, len)
139+
_range(start::Any , step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
140+
_range(start::Any , step::Nothing, stop::Nothing, len::Any ) = range_start_length(start, len)
141+
_range(start::Any , step::Nothing, stop::Any , len::Nothing) = range_start_stop(start, stop)
142+
_range(start::Any , step::Nothing, stop::Any , len::Any ) = range_start_stop_length(start, stop, len)
143+
_range(start::Any , step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
144+
_range(start::Any , step::Any , stop::Nothing, len::Any ) = range_start_step_length(start, step, len)
145+
_range(start::Any , step::Any , stop::Any , len::Nothing) = range_start_step_stop(start, step, stop)
146+
_range(start::Any , step::Any , stop::Any , len::Any ) = range_error(start, step, stop, len)
147+
148+
range_stop_length(stop, length) = (stop-length+1):stop
149+
150+
range_step_stop_length(step, stop, length) = reverse(range_start_step_length(stop, -step, length))
151+
152+
range_start_length(a::Real, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1))
153+
range_start_length(a::AbstractFloat, len::Integer) = range_start_step_length(a, oftype(a, 1), len)
154+
range_start_length(a, len::Integer) = range_start_step_length(a, oftype(a-a, 1), len)
155+
156+
range_start_stop(start, stop) = start:stop
157+
158+
function range_start_step_length(a::AbstractFloat, step::AbstractFloat, len::Integer)
159+
range_start_step_length(promote(a, step)..., len)
160+
end
101161

102-
# Range from start to stop: range(a, [step=s,] stop=b), no length
103-
_range(start, step, stop, ::Nothing) = (:)(start, step, stop)
104-
_range(start, ::Nothing, stop, ::Nothing) = (:)(start, stop)
162+
function range_start_step_length(a::Real, step::AbstractFloat, len::Integer)
163+
range_start_step_length(float(a), step, len)
164+
end
105165

106-
# Range of a given length: range(a, [step=s,] length=l), no stop
107-
_range(a::Real, ::Nothing, ::Nothing, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1))
108-
_range(a::AbstractFloat, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a, 1), nothing, len)
109-
_range(a::AbstractFloat, st::AbstractFloat, ::Nothing, len::Integer) = _range(promote(a, st)..., nothing, len)
110-
_range(a::Real, st::AbstractFloat, ::Nothing, len::Integer) = _range(float(a), st, nothing, len)
111-
_range(a::AbstractFloat, st::Real, ::Nothing, len::Integer) = _range(a, float(st), nothing, len)
112-
_range(a, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a-a, 1), nothing, len)
166+
function range_start_step_length(a::AbstractFloat, step::Real, len::Integer)
167+
range_start_step_length(a, float(step), len)
168+
end
113169

114-
_range(a::T, step::T, ::Nothing, len::Integer) where {T <: AbstractFloat} =
170+
function range_start_step_length(a::T, step::T, len::Integer) where {T <: AbstractFloat}
115171
_rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len)
116-
_range(a::T, step, ::Nothing, len::Integer) where {T} =
172+
end
173+
174+
function range_start_step_length(a::T, step, len::Integer) where {T}
117175
_rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len)
176+
end
177+
118178
_rangestyle(::Ordered, ::ArithmeticWraps, a::T, step::S, len::Integer) where {T,S} =
119179
StepRange{typeof(a+zero(step)),S}(a, step, a+step*(len-1))
120180
_rangestyle(::Any, ::Any, a::T, step::S, len::Integer) where {T,S} =
121181
StepRangeLen{typeof(a+zero(step)),T,S}(a, step, len)
122182

123-
# Malformed calls
124-
_range(start, step, ::Nothing, ::Nothing) = # range(a, step=s)
125-
throw(ArgumentError("At least one of `length` or `stop` must be specified"))
126-
_range(start, ::Nothing, ::Nothing, ::Nothing) = # range(a)
127-
throw(ArgumentError("At least one of `length` or `stop` must be specified"))
128-
_range(::Nothing, ::Nothing, ::Nothing, ::Nothing) = # range(nothing)
129-
throw(ArgumentError("At least one of `length` or `stop` must be specified"))
130-
_range(start::Real, step::Real, stop::Real, length::Integer) = # range(a, step=s, stop=b, length=l)
131-
throw(ArgumentError("Too many arguments specified; try passing only one of `stop` or `length`"))
132-
_range(::Nothing, ::Nothing, ::Nothing, ::Integer) = # range(nothing, length=l)
133-
throw(ArgumentError("Can't start a range at `nothing`"))
183+
range_start_step_stop(start, step, stop) = start:step:stop
184+
185+
function range_error(start, step, stop, length)
186+
hasstart = start !== nothing
187+
hasstep = step !== nothing
188+
hasstop = stop !== nothing
189+
haslength = start !== nothing
190+
191+
hint = if hasstart && hasstep && hasstop && haslength
192+
"Try specifying only three arguments"
193+
elseif !hasstop && !haslength
194+
"At least one of `length` or `stop` must be specified."
195+
elseif !hasstep && !haslength
196+
"At least one of `length` or `step` must be specified."
197+
elseif !hasstart && !hasstop
198+
"At least one of `start` or `stop` must be specified."
199+
else
200+
"Try specifying more arguments."
201+
end
202+
203+
msg = """
204+
Cannot construct range from arguments:
205+
start = $start
206+
step = $step
207+
stop = $stop
208+
length = $length
209+
$hint
210+
"""
211+
throw(ArgumentError(msg))
212+
end
134213

135214
## 1-dimensional ranges ##
136215

@@ -434,13 +513,13 @@ function LinRange(start, stop, len::Integer)
434513
LinRange{T}(start, stop, len)
435514
end
436515

437-
function _range(start::T, ::Nothing, stop::S, len::Integer) where {T,S}
516+
function range_start_stop_length(start::T, stop::S, len::Integer) where {T,S}
438517
a, b = promote(start, stop)
439-
_range(a, nothing, b, len)
518+
range_start_stop_length(a, b, len)
440519
end
441-
_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len)
442-
_range(start::T, ::Nothing, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len)
443-
_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Integer} =
520+
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len)
521+
range_start_stop_length(start::T, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len)
522+
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Integer} =
444523
_linspace(float(T), start, stop, len)
445524
## for Float16, Float32, and Float64 we hit twiceprecision.jl to lift to higher precision StepRangeLen
446525
# for all other types we fall back to a plain old LinRange

base/twiceprecision.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ end
427427
step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T<:AbstractFloat} = T(r.step)
428428
step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T} = T(r.step)
429429

430-
function _range(a::T, st::T, ::Nothing, len::Integer) where T<:Union{Float16,Float32,Float64}
430+
function range_start_step_length(a::T, st::T, len::Integer) where T<:Union{Float16,Float32,Float64}
431431
start_n, start_d = rat(a)
432432
step_n, step_d = rat(st)
433433
if start_d != 0 && step_d != 0 &&
@@ -591,7 +591,7 @@ end
591591
## LinRange
592592

593593
# For Float16, Float32, and Float64, this returns a StepRangeLen
594-
function _range(start::T, ::Nothing, stop::T, len::Integer) where {T<:IEEEFloat}
594+
function range_start_stop_length(start::T, stop::T, len::Integer) where {T<:IEEEFloat}
595595
len < 2 && return _linspace1(T, start, stop, len)
596596
if start == stop
597597
return steprangelen_hp(T, start, zero(T), 0, len, 1)

test/ranges.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3+
@testset "range construction" begin
4+
@testset "range(;kw...)" begin
5+
@test_throws ArgumentError range(start=1, step=1, stop=2, length=10)
6+
@test_throws ArgumentError range(start=1, step=1, stop=10, length=11)
7+
8+
r = 3.0:2:11
9+
@test r == range(start=first(r), step=step(r), stop=last(r) )
10+
@test r == range(start=first(r), step=step(r), length=length(r))
11+
@test r == range(start=first(r), stop=last(r), length=length(r))
12+
@test r == range( step=step(r), stop=last(r), length=length(r))
13+
14+
r = 4:9
15+
@test r === range(start=first(r), stop=last(r) )
16+
@test r === range(start=first(r), length=length(r))
17+
# the next one uses ==, because it changes the eltype
18+
@test r == range(start=first(r), stop=last(r), length=length(r))
19+
@test r === range( stop=last(r), length=length(r))
20+
end
21+
end
22+
323
using Dates, Random
424
isdefined(Main, :PhysQuantities) || @eval Main include("testhelpers/PhysQuantities.jl")
525
using .Main.PhysQuantities

0 commit comments

Comments
 (0)