Skip to content

Commit 162bd0d

Browse files
authored
Implemented findall(in(interval), x::AbstractRange), fixes #52 (#63)
1 parent 9c7cc1c commit 162bd0d

File tree

5 files changed

+219
-1
lines changed

5 files changed

+219
-1
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ julia = "0.7, 1"
1313

1414
[extras]
1515
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
16+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
17+
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
1618

1719
[targets]
18-
test = ["Test"]
20+
test = ["Test", "Random", "OffsetArrays"]

src/IntervalSets.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,6 @@ convert(::Type{ClosedInterval}, x::Number) = x..x
272272
convert(::Type{ClosedInterval{T}}, x::Number) where T =
273273
convert(AbstractInterval{T}, convert(AbstractInterval, x))
274274

275+
include("findall.jl")
275276

276277
end # module

src/findall.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
findall(in(interval), x::AbstractRange)
3+
4+
Return all indices `i` for which `x[i] ∈ interval`, specialized for
5+
the case where `x` is a range, which enables constant-time complexity.
6+
7+
# Examples
8+
9+
```jldoctest
10+
julia> x = range(0,stop=3,length=10)
11+
0.0:0.3333333333333333:3.0
12+
13+
julia> collect(x)'
14+
1×10 LinearAlgebra.Adjoint{Float64,Array{Float64,1}}:
15+
0.0 0.333333 0.666667 1.0 1.33333 1.66667 2.0 2.33333 2.66667 3.0
16+
17+
julia> findall(in(1..6), x)
18+
4:10
19+
```
20+
21+
It also works for decreasing ranges:
22+
```jldoctest
23+
julia> y = 8:-0.5:0
24+
8.0:-0.5:0.0
25+
26+
julia> collect(y)'
27+
1×17 LinearAlgebra.Adjoint{Float64,Array{Float64,1}}:
28+
8.0 7.5 7.0 6.5 6.0 5.5 5.0 4.5 4.0 3.5 3.0 2.5 2.0 1.5 1.0 0.5 0.0
29+
30+
julia> findall(in(1..6), y)
31+
5:15
32+
33+
julia> findall(in(Interval{:open,:closed}(1,6)), y) # (1,6], does not include 1
34+
5:14
35+
```
36+
"""
37+
function Base.findall(interval_d::Base.Fix2{typeof(in),Interval{L,R,T}}, x::AbstractRange) where {L,R,T}
38+
isempty(x) && return 1:0
39+
40+
interval = interval_d.x
41+
il, ir = firstindex(x), lastindex(x)
42+
δx = step(x)
43+
a,b = if δx < 0
44+
rev = findall(in(interval), reverse(x))
45+
isempty(rev) && return rev
46+
47+
a = (il+ir)-last(rev)
48+
b = (il+ir)-first(rev)
49+
50+
a,b
51+
else
52+
lx, rx = first(x), last(x)
53+
l = max(leftendpoint(interval), lx-1)
54+
r = min(rightendpoint(interval), rx+1)
55+
56+
(l > rx || r < lx) && return 1:0
57+
58+
a = il + max(0, round(Int, cld(l-lx, δx)))
59+
a += (a ir && (x[a] == l && L == :open || x[a] < l))
60+
61+
b = min(ir, round(Int, cld(r-lx, δx)) + il)
62+
b -= (b il && (x[b] == r && R == :open || x[b] > r))
63+
64+
a,b
65+
end
66+
# Reversing a range could change sign of values close to zero (cf
67+
# sign of the smallest element in x and reverse(x), where x =
68+
# range(BigFloat(-0.5),stop=BigFloat(1.0),length=10)), or more
69+
# generally push elements in or out of the interval (as can cld),
70+
# so we need to check once again.
71+
a += +(a < ir && x[a] interval) - (il < a && x[a-1] interval)
72+
b += -(il < b && x[b] interval) + (b < ir && x[b+1] interval)
73+
74+
a:b
75+
end
76+
77+
# We overload Base._findin to avoid an ambiguity that arises with
78+
# Base.findall(interval_d::Base.Fix2{typeof(in),Interval{L,R,T}}, x::AbstractArray)
79+
function Base._findin(a::Union{AbstractArray, Tuple}, b::Interval)
80+
ind = Vector{eltype(keys(a))}()
81+
@inbounds for (i,ai) in pairs(a)
82+
ai in b && push!(ind, i)
83+
end
84+
ind
85+
end

test/findall.jl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using OffsetArrays
2+
3+
# Helper function to test that findall(in(interval), x) works. By
4+
# default, a reference is generated using the general algorithm,
5+
# linear in complexity, by generating a vector with the same contents
6+
# as x.
7+
function assert_in_interval(x, interval,
8+
expected=findall(v -> v interval, x))
9+
10+
result = :(findall(in($interval), $x))
11+
expr = :($result == $expected || isempty($result) && isempty($expected))
12+
if !(@eval $expr)
13+
println("Looking for elements of $x$interval, got $(@eval $result), expected $expected")
14+
length(x) < 30 && println(" x = ", collect(pairs(x)), "\n")
15+
end
16+
@eval @test $expr
17+
end
18+
19+
@testset "Interval coverage" begin
20+
@testset "Basic tests" begin
21+
let x = range(0, stop=1, length=21)
22+
Random.seed!(321)
23+
@testset "$kind" for (kind,end_points) in [
24+
("Two intervals", [(0.0, 0.5), (0.25,0.5)]),
25+
("Three intervals", [(0, 1/3), (1/3, 2/3), (2/3, 1)]),
26+
("Random intervals", [minmax(rand(),rand()) for i = 1:2]),
27+
("Interval containing one point", [(0.4619303378979984,0.5450937144417902)]),
28+
("Interval containing no points", [(0.9072957410215778,0.9082803807133988)])
29+
]
30+
@testset "L=$L" for L=[:closed,:open]
31+
@testset "R=$R" for R=[:closed,:open]
32+
for (a,b) in end_points
33+
interval = Interval{L,R}(a, b)
34+
@testset "Reversed: $reversed" for reversed in [false, true]
35+
assert_in_interval(reversed ? reverse(x) : x, interval)
36+
end
37+
end
38+
end
39+
end
40+
end
41+
42+
@testset "Open interval" begin
43+
assert_in_interval(x, OpenInterval(0.2,0.4), 6:8)
44+
end
45+
end
46+
end
47+
48+
@testset "Partially covered intervals" begin
49+
@testset "$T" for T in (Float32,Float64,BigFloat)
50+
@testset "$name, x = $x" for (name,x) in [
51+
("Outside left",range(T(-1),stop=T(-0.5),length=10)),
52+
("Touching left",range(T(-1),stop=T(0),length=10)),
53+
("Touching left-ϵ",range(T(-1),stop=T(0)-eps(T),length=10)),
54+
("Touching left+ϵ",range(T(-1),stop=T(0)+eps(T),length=10)),
55+
56+
("Outside right",range(T(1.5),stop=T(2),length=10)),
57+
("Touching right",range(T(1),stop=T(2),length=10)),
58+
("Touching right-ϵ",range(T(1)-eps(T),stop=T(2),length=10)),
59+
("Touching right+ϵ",range(T(1)+eps(T),stop=T(2),length=10)),
60+
61+
("Other right",range(T(0.5),stop=T(1),length=10)),
62+
("Other right-ϵ",range(T(0.5)-eps(T(0.5)),stop=T(1),length=10)),
63+
("Other right+ϵ",range(T(0.5)+eps(T(0.5)),stop=T(1),length=10)),
64+
65+
("Complete", range(T(0),stop=T(1),length=10)),
66+
("Complete-ϵ", range(eps(T),stop=T(1)-eps(T),length=10)),
67+
("Complete+ϵ", range(-eps(T),stop=T(1)+eps(T),length=10)),
68+
69+
("Left partial", range(T(-0.5),stop=T(0.6),length=10)),
70+
("Left", range(T(-0.5),stop=T(1.0),length=10)),
71+
("Right partial", range(T(0.5),stop=T(1.6),length=10)),
72+
("Right", range(T(0),stop=T(1.6),length=10))]
73+
@testset "L=$L" for L=[:closed,:open]
74+
@testset "R=$R" for R=[:closed,:open]
75+
@testset "Reversed: $reversed" for reversed in [false, true]
76+
for (a,b) in [(T(0.0),T(0.5)),(T(0.5),T(1.0))]
77+
interval = Interval{L,R}(a, b)
78+
assert_in_interval(reversed ? reverse(x) : x, interval)
79+
end
80+
end
81+
end
82+
end
83+
end
84+
end
85+
end
86+
87+
@testset "Large intervals" begin
88+
@test findall(in(4..Inf), 2:2:10) == 2:5
89+
@test findall(in(4..1e20), 2:2:10) == 2:5
90+
@test isempty(findall(in(-Inf..(-1e20)), 2:2:10))
91+
end
92+
93+
@testset "Reverse intervals" begin
94+
for x in [1:10, 1:3:10, 2:3:11, -1:9, -2:0.5:5]
95+
for lo in -3:4, hi in 5:13
96+
for L in [:closed, :open], R in [:closed, :open]
97+
interval = Interval{L,R}(lo,hi)
98+
assert_in_interval(x, interval)
99+
assert_in_interval(reverse(x), interval)
100+
end
101+
end
102+
end
103+
end
104+
105+
@testset "Arrays" begin
106+
@test findall(in(1..6), collect(0:7)) == 2:7
107+
@test findall(in(1..6), reshape(1:16, 4, 4)) ==
108+
vcat([CartesianIndex(i,1) for i = 1:4], CartesianIndex(1,2), CartesianIndex(2,2))
109+
end
110+
111+
@testset "Empty ranges and intervals" begin
112+
# Range empty
113+
@test isempty(findall(in(1..6), 1:0))
114+
# Interval empty
115+
@test isempty(findall(in(Interval{:closed,:open}(1.0..1.0)),
116+
0.0:0.02040816326530612:1.0))
117+
end
118+
119+
@testset "Offset arrays" begin
120+
for (x,interval) in [(OffsetArray(ones(10), -5), -1..1),
121+
(OffsetArray(1:5, -3), 2..4),
122+
(OffsetArray(5:-1:1, -5), 2..4)]
123+
assert_in_interval(x, interval)
124+
assert_in_interval(reverse(x), interval)
125+
end
126+
end
127+
end

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ using Test
33
using Dates
44
using Statistics
55
import Statistics: mean
6+
using Random
67

78
import IntervalSets: Domain, endpoints, closedendpoints, TypedEndpointsInterval
89

@@ -706,4 +707,6 @@ struct IncompleteInterval <: AbstractInterval{Int} end
706707
@test_throws ErrorException endpoints(I)
707708
@test_throws ErrorException closedendpoints(I)
708709
end
710+
711+
include("findall.jl")
709712
end

0 commit comments

Comments
 (0)