Skip to content
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

Support values::AbstractDict #25

Merged
merged 3 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
fail-fast: false
matrix:
version:
- '0.7'
- '1.0'
- '1'
- 'nightly'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.jl.cov
*.jl.*.cov
*.jl.mem
Manifest.toml
7 changes: 4 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
name = "IndirectArrays"
uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959"
version = "0.5.1"
version = "1.0.0"

[deps]

[compat]
FixedPointNumbers = "0.5, 0.6, 0.7"
julia = "0.7, 1"
julia = "1"

[extras]
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "MappedArrays", "Colors", "FixedPointNumbers"]
test = ["Test", "MappedArrays", "Colors", "FixedPointNumbers", "OrderedCollections"]
54 changes: 37 additions & 17 deletions src/IndirectArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,42 @@ export IndirectArray
IndirectArray(index, values)

creates an array `A` where the values are looked up in the value table,
`values`, using the `index`. Concretely, `A[i,j] =
values[index[i,j]]`.
`values`, using the `index`. Concretely, `A[i...] =
values[index[i...]]`.

`values` can be an `AbstractVector` (useful when the values in `index` are
contiguous) or an `AbstractDict` (useful when they are not).
When there are not very many such distinct values, "frozen" LittleDicts from
[OrderedCollections](https://github.com/JuliaCollections/OrderedCollections.jl)
are recommended for performance.
"""
struct IndirectArray{T,N,A<:AbstractArray{<:Integer,N},V<:AbstractVector{T}} <: AbstractArray{T,N}
struct IndirectArray{T,N,I,A<:AbstractArray{I,N},V<:Union{AbstractVector{T},AbstractDict{I,T}}} <: AbstractArray{T,N}
index::A
values::V

@inline function IndirectArray{T,N,A,V}(index, values) where {T,N,A,V}
@inline function IndirectArray{T,N,I,A,V}(index, values) where {T,N,I,A,V}
# The typical logic for testing bounds and then using
# @inbounds will not check whether index is inbounds for
# values. So we had better check this on construction.
@boundscheck checkbounds(values, index)
new{T,N,A,V}(index, values)
if isa(values, AbstractVector)
I <: Integer || error("with a vector `values`, the index must have integer eltype")
@boundscheck checkbounds(values, index)
else
@boundscheck begin
uindex = unique(index)
all(idx -> haskey(values, idx), index) || Base.throw_boundserror(index, values)
end
end
new{T,N,I,A,V}(index, values)
end
end
const IndirectArrayVec{T,N,I,A,V<:AbstractVector} = IndirectArray{T,N,I,A,V}

Base.@propagate_inbounds IndirectArray(index::AbstractArray{<:Integer,N}, values::AbstractVector{T}) where {T,N} =
IndirectArray{T,N,typeof(index),typeof(values)}(index, values)
IndirectArray{T,N,eltype(index),typeof(index),typeof(values)}(index, values)
Base.@propagate_inbounds IndirectArray(index::AbstractArray{I,N}, values::AbstractDict{I,T}) where {I,T,N} =
IndirectArray{T,N,I,typeof(index),typeof(values)}(index, values)

function IndirectArray{T}(A::AbstractArray) where {T}
values = unique(A)
index = convert(Array{T}, indexin(A, values))
Expand All @@ -32,30 +51,31 @@ IndirectArray(A::AbstractArray) = IndirectArray{UInt8}(A)

Base.size(A::IndirectArray) = size(A.index)
Base.axes(A::IndirectArray) = axes(A.index)
Base.IndexStyle(::Type{IndirectArray{T,N,A,V}}) where {T,N,A,V} = IndexStyle(A)
Base.IndexStyle(::Type{IndirectArray{T,N,I,A,V}}) where {T,N,I,A,V} = IndexStyle(A)

Base.copy(A::IndirectArray) = IndirectArray(copy(A.index), copy(A.values))
function Base.copy(A::IndirectArray)
@inbounds ret = IndirectArray(copy(A.index), copy(A.values))
return ret
end

@inline function Base.getindex(A::IndirectArray, i::Int)
@boundscheck checkbounds(A.index, i)
@inbounds idx = A.index[i]
@boundscheck checkbounds(A.values, idx)
@inbounds ret = A.values[idx]
ret
end

@inline function Base.getindex(A::IndirectArray{T,N}, I::Vararg{Int,N}) where {T,N}
@boundscheck checkbounds(A.index, I...)
@inbounds idx = A.index[I...]
@boundscheck checkbounds(A.values, idx)
@inbounds ret = A.values[idx]
ret
end

@inline function Base.setindex!(A::IndirectArray, x, i::Int)
@inline function Base.setindex!(A::IndirectArrayVec, x, i::Int)
@boundscheck checkbounds(A.index, i)
idx = findfirst(isequal(x), A.values)
if idx == nothing
if idx === nothing
push!(A.values, x)
A.index[i] = length(A.values)
else
Expand All @@ -64,9 +84,9 @@ end
return A
end

@inline function Base.push!(A::IndirectArray{T,1} where T, x)
@inline function Base.push!(A::IndirectArrayVec{T,1} where T, x)
idx = findfirst(isequal(x), A.values)
if idx == nothing
if idx === nothing
push!(A.values, x)
push!(A.index, length(A.values))
else
Expand All @@ -75,7 +95,7 @@ end
return A
end

function Base.append!(A::IndirectArray{T,1}, B::IndirectArray{T,1}) where T
function Base.append!(A::IndirectArrayVec{T,1}, B::IndirectArray{T,1}) where T
if A.values == B.values
append!(A.index, B.index)
else # pretty inefficient but let's get something going
Expand All @@ -86,7 +106,7 @@ function Base.append!(A::IndirectArray{T,1}, B::IndirectArray{T,1}) where T
return A
end

function Base.append!(A::IndirectArray{<:Any,1}, B::AbstractVector)
function Base.append!(A::IndirectArrayVec{<:Any,1}, B::AbstractVector)
for b in B
push!(A, b)
end
Expand Down
113 changes: 64 additions & 49 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,53 +1,68 @@
using IndirectArrays, MappedArrays
using IndirectArrays, MappedArrays, OrderedCollections
using Test, FixedPointNumbers, Colors

colors = [RGB(1,0,0) RGB(0,1,0);
RGB(0,0,1) RGB(1,0,0)]
index0 = [1 3;
2 1]
for indexT in (Int8, Int16, UInt8, UInt16)
A = IndirectArray{indexT}(colors)
@test eltype(A) == RGB{N0f8}
@test size(A) == (2,2)
@test ndims(A) == 2
@test A[1,1] === A[1] === RGB(1,0,0)
@test A[2,1] === A[2] === RGB(0,0,1)
@test A[1,2] === A[3] === RGB(0,1,0)
@test A[2,2] === A[4] === RGB(1,0,0)
@test isa(eachindex(A), AbstractUnitRange)
@test A.index == index0
end
x = IndirectArray(colors[:])
@test x == IndirectArray{UInt8}(colors[:])
xc = copy(x)
@test x == xc
xc[2], xc[3] = RGB(0,1,0), RGB(0,1,1)
@test xc == IndirectArray([RGB(1,0,0), RGB(0,1,0), RGB(0,1,1), RGB(1,0,0)])
@test append!(x, x) == IndirectArray([colors[:]; colors[:]])
@test append!(x, IndirectArray([RGB(1,0,0), RGB(1,1,0)])) ==
IndirectArray([colors[:]; colors[:]; [RGB(1,0,0), RGB(1,1,0)]])
# Append with non-IndirectArray
@test append!(IndirectArray(colors[:]), IndirectArray(colors[:])) ==
append!(IndirectArray(colors[:]), colors[:])
@testset "values::AbstractVector" begin
colors = [RGB(1,0,0) RGB(0,1,0);
RGB(0,0,1) RGB(1,0,0)]
index0 = [1 3;
2 1]
for indexT in (Int8, Int16, UInt8, UInt16)
A = IndirectArray{indexT}(colors)
@test eltype(A) == RGB{N0f8}
@test size(A) == (2,2)
@test ndims(A) == 2
@test A[1,1] === A[1] === RGB(1,0,0)
@test A[2,1] === A[2] === RGB(0,0,1)
@test A[1,2] === A[3] === RGB(0,1,0)
@test A[2,2] === A[4] === RGB(1,0,0)
@test isa(eachindex(A), AbstractUnitRange)
@test A.index == index0
end
x = IndirectArray(colors[:])
@test x == IndirectArray{UInt8}(colors[:])
xc = copy(x)
@test x == xc
xc[2], xc[3] = RGB(0,1,0), RGB(0,1,1)
@test xc == IndirectArray([RGB(1,0,0), RGB(0,1,0), RGB(0,1,1), RGB(1,0,0)])
@test append!(x, x) == IndirectArray([colors[:]; colors[:]])
@test append!(x, IndirectArray([RGB(1,0,0), RGB(1,1,0)])) ==
IndirectArray([colors[:]; colors[:]; [RGB(1,0,0), RGB(1,1,0)]])
# Append with non-IndirectArray
@test append!(IndirectArray(colors[:]), IndirectArray(colors[:])) ==
append!(IndirectArray(colors[:]), colors[:])

# Bounds checking upon construction
index_ob = copy(index0)
index_ob[1] = 5 # out-of-bounds
unsafe_ia(idx, vals) = (@inbounds ret = IndirectArray(idx, vals); ret)
safe_ia(idx, vals) = (ret = IndirectArray(idx, vals); ret)
@test_throws BoundsError safe_ia(index_ob, colors[1:3])
# This requires inlining, which means it fails on Travis since we turn
# off inlining for better coverage stats
# B = unsafe_ia(index_ob, colors)
# @test_throws BoundsError B[1]
# @test B[2] == RGB(0,0,1)

# Bounds checking upon construction
index_ob = copy(index0)
index_ob[1] = 5 # out-of-bounds
unsafe_ia(idx, vals) = (@inbounds ret = IndirectArray(idx, vals); ret)
safe_ia(idx, vals) = (ret = IndirectArray(idx, vals); ret)
@test_throws BoundsError safe_ia(index_ob, colors[1:3])
# This requires inlining, which means it fails on Travis since we turn
# off inlining for better coverage stats
# B = unsafe_ia(index_ob, colors)
# @test_throws BoundsError B[1]
# @test B[2] == RGB(0,0,1)
# Non-Arrays
a = [0.1 0.4;
0.33 1.0]
f(x) = round(Int, 99*x) + 1 # maps 0-1 to 1-100
m = mappedarray(f, a)
cmap = colormap("RdBu", 100)
img = IndirectArray(m, cmap)
@test img == [cmap[11] cmap[41];
cmap[34] cmap[100]]
end

# Non-Arrays
a = [0.1 0.4;
0.33 1.0]
f(x) = round(Int, 99*x) + 1 # maps 0-1 to 1-100
m = mappedarray(f, a)
cmap = colormap("RdBu", 100)
img = IndirectArray(m, cmap)
@test img == [cmap[11] cmap[41];
cmap[34] cmap[100]]
@testset "values::AbstractDict" begin
# With Dicts
a = ['a' 'b';
'q' 'j']
v = freeze(Dict('a' => "apple", 'b' => "book", 'q' => "quit", 'j' => "jolly"))
A = IndirectArray(a, v)
@test A[1,1] == "apple"
@test A[2,1] == "quit"
@test A[1,2] == "book"
@test A[2,2] == "jolly"
@test_throws BoundsError IndirectArray(a, Dict('a' => "apple", 'b' => "book"))
end