Skip to content

Commit a2d73a3

Browse files
authored
Merge pull request #158 from SymbolicML/MilesCranmer/issue157
feat: add explicit handling of ranges
2 parents 8b1eb46 + 1b10c41 commit a2d73a3

File tree

2 files changed

+131
-6
lines changed

2 files changed

+131
-6
lines changed

src/utils.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k)
149149
return dimension_names(T1) == dimension_names(T2)
150150
end
151151

152+
# Multiplying ranges with units
153+
Base.TwicePrecision{T}(x::T) where {T<:AbstractQuantity} = Base.TwicePrecision{typeof(x)}(x, zero(x))
154+
# TODO: Note that to get RealQuantity working, we have to overload many other functions,
155+
# which is why we skip it.
156+
157+
# Avoid https://github.com/JuliaLang/julia/issues/56610
158+
for T1 in (AbstractQuantity{<:Real}, Real),
159+
T2 in (AbstractQuantity{<:Real}, Real),
160+
T3 in (AbstractQuantity{<:Real}, Real)
161+
162+
T1 === T2 === T3 === Real && continue
163+
164+
@eval function Base.:(:)(start::$T1, step::$T2, stop::$T3)
165+
dimension(start) == dimension(step) || throw(DimensionError(start, step))
166+
dimension(start) == dimension(stop) || throw(DimensionError(start, stop))
167+
return range(start, stop, length=length(ustrip(start):ustrip(step):ustrip(stop)))
168+
end
169+
170+
if T3 === Real && !(T1 === T2 === Real)
171+
@eval function Base.:(:)(start::$T1, stop::$T2)
172+
if !iszero(dimension(start)) || !iszero(dimension(stop))
173+
error("When creating a range over dimensionful quantities, you must specify a step.")
174+
end
175+
return start:1:stop
176+
end
177+
end
178+
end
179+
152180
# Compatibility with `.*`
153181
Base.size(q::UnionAbstractQuantity) = size(ustrip(q))
154182
Base.length(q::UnionAbstractQuantity) = length(ustrip(q))

test/unittests.jl

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,6 @@ Base.round(::Type{T}, x::SimpleRatio) where {T} = round(T, x.num // x.den)
198198

199199
end
200200

201-
@testset "Ranges" begin
202-
x = [xi for xi in 0.0u"km/s":0.1u"km/s":1.0u"km/s"]
203-
@test x[2] == 0.1u"km/s"
204-
@test x[end] == 1.0u"km/s"
205-
end
206-
207201
@testset "Complex numbers" begin
208202
x = (0.5 + 0.6im) * u"km/s"
209203
@test string(x) == "(500.0 + 600.0im) m s⁻¹"
@@ -358,6 +352,109 @@ end
358352
@test ustrip(x') == ustrip(x)'
359353
end
360354

355+
@testset "Ranges" begin
356+
@testset "Ranges from units" begin
357+
x = [xi for xi in 0.0u"km/s":0.1u"km/s":1.0u"km/s"]
358+
@test x[2] == 0.1u"km/s"
359+
@test x[end] == 1.0u"km/s"
360+
361+
# https://github.com/JuliaLang/julia/issues/56610
362+
c = collect(1u"inch":0.25u"inch":4u"inch")
363+
@test c[1] == 1u"inch"
364+
@test c[end] <= 4u"inch"
365+
366+
# Test dimensionless quantities
367+
x = collect(1u"1":2u"1":5u"1")
368+
@test x == [1, 3, 5] .* u"1"
369+
@test eltype(x) <: Quantity
370+
371+
# Test error for missing step
372+
@test_throws "must specify a step" 1u"km":2u"km"
373+
@test_throws "must specify a step" 1us"km":2us"km"
374+
375+
# However, for backwards compatibility, dimensionless ranges are allowed:
376+
x = collect(1u"1":5u"1")
377+
@test x == [1, 2, 3, 4, 5]
378+
@test eltype(x) <: Quantity{Float64}
379+
380+
# Test errors for incompatible units
381+
@test_throws DimensionError 1u"km":1u"s":5u"km"
382+
@test_throws DimensionError 1u"km":1u"m":5u"s"
383+
@test_throws DimensionError 1u"km":1u"km/s":5u"km"
384+
385+
# Same for symbolic units!
386+
@test_throws DimensionError 1us"km":1us"m":5us"inch"
387+
@test length(1u"inch":1u"m":5u"km") == 5000
388+
389+
# Test with symbolic units
390+
x = collect(1us"inch":0.25us"inch":4us"inch")
391+
@test x[1] == 1us"inch"
392+
@test x[2] == 1.25us"inch"
393+
@test x[end] == 4us"inch"
394+
end
395+
396+
@testset "Multiplying ranges with units" begin
397+
# Test multiplying ranges with units
398+
x = (1:0.25:4)u"inch"
399+
@test x isa StepRangeLen
400+
@test first(x) == 1u"inch"
401+
@test x[2] == 1.25u"inch"
402+
@test last(x) == 4u"inch"
403+
@test length(x) == 13
404+
405+
# Integer range (but real-valued unit)
406+
x = (1:4)u"inch"
407+
@test x isa StepRangeLen
408+
@test first(x) == 1u"inch"
409+
@test x[2] == 2u"inch"
410+
@test last(x) == 4u"inch"
411+
@test length(x) == 4
412+
@test collect(x)[3] == 3u"inch"
413+
414+
# Test with floating point range
415+
x = (1.0:0.5:3.0)u"m"
416+
@test x isa StepRangeLen
417+
@test first(x) == 1.0u"m"
418+
@test x[2] == 1.5u"m"
419+
@test last(x) == 3.0u"m"
420+
@test length(x) == 5
421+
@test collect(x)[3] == 2.0u"m"
422+
423+
x = (0:0.1:1)u"m"
424+
@test length(x) == 11
425+
@test collect(x)[3] == 0.2u"m"
426+
427+
# Test with symbolic units
428+
x = (1:0.25:4)us"inch"
429+
@test x isa StepRangeLen{<:Quantity{Float64,<:SymbolicDimensions}}
430+
@test first(x) == us"inch"
431+
@test x[2] == 1.25us"inch"
432+
@test last(x) == 4us"inch"
433+
@test length(x) == 13
434+
435+
# Test that symbolic units preserve their symbolic nature
436+
x = (0:0.1:1)us"km/h"
437+
@test x isa AbstractRange
438+
@test first(x) == 0us"km/h"
439+
@test x[2] == 0.1us"km/h"
440+
@test last(x) == 1us"km/h"
441+
@test length(x) == 11
442+
443+
# Similarly, integers should stay integers:
444+
x = (1:4)us"inch"
445+
@test_skip x isa StepRangeLen{<:Quantity{Int64,<:SymbolicDimensions}}
446+
@test first(x) == us"inch"
447+
@test x[2] == 2us"inch"
448+
@test last(x) == 4us"inch"
449+
@test length(x) == 4
450+
451+
# With RealQuantity:
452+
@test_skip (1.0:4.0) * RealQuantity(u"inch") isa StepRangeLen{<:RealQuantity{Float64,<:SymbolicDimensions}}
453+
# TODO: This is not available as TwicePrecision interacts with Real in a way
454+
# that demands many other functions to be defined.
455+
end
456+
end
457+
361458
@testset "Alternate dimension construction" begin
362459
z = Quantity(-52, length=1) * Dimensions(mass=2)
363460
z2 = Dimensions(mass=2) * Quantity(-52, length=1)

0 commit comments

Comments
 (0)