diff --git a/NEWS.md b/NEWS.md index 7793ba31b7f24..dc532ea43526b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -40,6 +40,7 @@ Standard library changes ------------------------ * `count` and `findall` now accept an `AbstractChar` argument to search for a character in a string ([#38675]). +* `range` now supports `start` as an optional keyword argument ([#38041]). * `islowercase` and `isuppercase` are now compliant with the Unicode lower/uppercase categories ([#38574]). * `iseven` and `isodd` functions now support non-`Integer` numeric types ([#38976]). * `escape_string` can now receive a collection of characters in the keyword diff --git a/base/range.jl b/base/range.jl index 63f4c1aed02e2..aec23e852589b 100644 --- a/base/range.jl +++ b/base/range.jl @@ -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) + +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 diff --git a/base/twiceprecision.jl b/base/twiceprecision.jl index 1490a0624c7d6..e7a2e5041f4ef 100644 --- a/base/twiceprecision.jl +++ b/base/twiceprecision.jl @@ -427,7 +427,7 @@ end step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T<:AbstractFloat} = T(r.step) step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T} = T(r.step) -function _range(a::T, st::T, ::Nothing, len::Integer) where T<:Union{Float16,Float32,Float64} +function range_start_step_length(a::T, st::T, len::Integer) where T<:Union{Float16,Float32,Float64} start_n, start_d = rat(a) step_n, step_d = rat(st) if start_d != 0 && step_d != 0 && @@ -591,7 +591,7 @@ end ## LinRange # For Float16, Float32, and Float64, this returns a StepRangeLen -function _range(start::T, ::Nothing, stop::T, len::Integer) where {T<:IEEEFloat} +function range_start_stop_length(start::T, stop::T, len::Integer) where {T<:IEEEFloat} len < 2 && return _linspace1(T, start, stop, len) if start == stop return steprangelen_hp(T, start, zero(T), 0, len, 1) diff --git a/test/ranges.jl b/test/ranges.jl index 6fcfc18b50529..b9f77f211193d 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -1,5 +1,25 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +@testset "range construction" begin + @testset "range(;kw...)" begin + @test_throws ArgumentError range(start=1, step=1, stop=2, length=10) + @test_throws ArgumentError range(start=1, step=1, stop=10, length=11) + + r = 3.0:2:11 + @test r == range(start=first(r), step=step(r), stop=last(r) ) + @test r == range(start=first(r), step=step(r), length=length(r)) + @test r == range(start=first(r), stop=last(r), length=length(r)) + @test r == range( step=step(r), stop=last(r), length=length(r)) + + r = 4:9 + @test r === range(start=first(r), stop=last(r) ) + @test r === range(start=first(r), length=length(r)) + # the next one uses ==, because it changes the eltype + @test r == range(start=first(r), stop=last(r), length=length(r)) + @test r === range( stop=last(r), length=length(r)) + end +end + using Dates, Random isdefined(Main, :PhysQuantities) || @eval Main include("testhelpers/PhysQuantities.jl") using .Main.PhysQuantities