diff --git a/docs/src/intset.md b/docs/src/intset.md index 3057dbf1..be2b3509 100644 --- a/docs/src/intset.md +++ b/docs/src/intset.md @@ -1,6 +1,6 @@ # DataStructures.IntSet -`DataStructures.IntSet` is a drop-in replacement for the Base `BitSet` +`DataStructures.IntSet` (`<: AbstractSet{Int}`) is a drop-in replacement for the Base `BitSet` type. It efficiently stores dense collections of small non-negative `Int`s as a sorted set. The constructor `IntSet([itr])` constructs a sorted set of the integers generated by the given iterable object, or an diff --git a/docs/src/sparse_int_set.md b/docs/src/sparse_int_set.md index 452318b6..ddf13b0c 100644 --- a/docs/src/sparse_int_set.md +++ b/docs/src/sparse_int_set.md @@ -1,5 +1,7 @@ # DataStructures.SparseIntSet +`SparseIntSet` is an `AbstractSet{Int}` with a packed representation. + Implementation of a __Sparse Integer Set__, for background see [Sparse Sets](https://www.computist.xyz/2018/06/sparse-sets.html). Only positive non-zero `Int`s are allowed inside the set. The idea is to have one **packed** `Vector` storing all the `Int`s contained in the set as to allow for fast iteration, and a sparse, paged **reverse** `Vector` with the position of a particular `Int` inside the **packed** `Vector`. This allows for very fast iteration, insertion and deletion of indices. diff --git a/src/int_set.jl b/src/int_set.jl index ba1c6f5d..65d10ef9 100644 --- a/src/int_set.jl +++ b/src/int_set.jl @@ -1,6 +1,6 @@ # This file was a part of Julia. License is MIT: http://julialang.org/license -mutable struct IntSet +mutable struct IntSet <: AbstractSet{Int} bits::BitVector inverse::Bool IntSet() = new(falses(256), false) @@ -8,6 +8,9 @@ end IntSet(itr) = union!(IntSet(), itr) Base.empty(s::IntSet) = IntSet() +Base.emptymutable(::IntSet, ::Type{Int}=Int) = IntSet() +Base.copymutable(s::IntSet) = copy(s) +Base.filter!(f, s::IntSet) = Base.unsafe_filter!(f, s) Base.copy(s1::IntSet) = copy!(IntSet(), s1) function Base.copy!(to::IntSet, from::IntSet) resize!(to.bits, length(from.bits)) @@ -133,6 +136,7 @@ end Base.symdiff(s::IntSet, ns) = symdiff!(copy(s), ns) Base.symdiff!(s::IntSet, ns) = (for n in ns; symdiff!(s, n); end; s) +Base.symdiff!(s::IntSet, ns::AbstractSet) = (for n in ns; symdiff!(s, n); end; s) function Base.symdiff!(s::IntSet, n::Integer) 0 <= n < typemax(Int) || throw(ArgumentError(_intset_bounds_err_msg)) val = (n in s) ⊻ !s.inverse diff --git a/src/sparse_int_set.jl b/src/sparse_int_set.jl index c7b1001f..9eae634b 100644 --- a/src/sparse_int_set.jl +++ b/src/sparse_int_set.jl @@ -2,7 +2,7 @@ const INT_PER_PAGE = div(ccall(:jl_getpagesize, Clong, ()), sizeof(Int)) # we use this to mark pages not in use, it must never be written to. const NULL_INT_PAGE = Vector{Int}() -mutable struct SparseIntSet +mutable struct SparseIntSet <: AbstractSet{Int} packed ::Vector{Int} reverse::Vector{Vector{Int}} counters::Vector{Int} # counts the number of real elements in each page of reverse. @@ -15,6 +15,9 @@ SparseIntSet(indices) = union!(SparseIntSet(), indices) Base.eltype(::Type{SparseIntSet}) = Int Base.empty(::SparseIntSet) = SparseIntSet() +Base.emptymutable(::SparseIntSet, ::Type{Int}=Int) = SparseIntSet() +Base.copymutable(s::SparseIntSet) = copy(s) +Base.filter!(f, s::SparseIntSet) = Base.unsafe_filter!(f, s) function Base.empty!(s::SparseIntSet) empty!(s.packed) @@ -143,6 +146,7 @@ end return in(id, s) ? (@inbounds pop!(s, id)) : default end Base.popfirst!(s::SparseIntSet) = pop!(s, first(s)) +Base.delete!(s::SparseIntSet, id::Integer) = (pop!(s, id, nothing); s) @inline Base.iterate(set::SparseIntSet, args...) = iterate(set.packed, args...) @@ -170,6 +174,7 @@ Base.intersect!(s1::SparseIntSet, ss...) = intersect!(s1, intersect(ss...)) #Is there a more performant way to do this? Base.intersect!(s1::SparseIntSet, ns) = copy!(s1, intersect(s1, ns)) +Base.intersect!(s1::SparseIntSet, ns::AbstractSet) = copy!(s1, intersect(s1, ns)) Base.setdiff(s::SparseIntSet, ns) = setdiff!(copy(s), ns) function Base.setdiff!(s::SparseIntSet, ns) diff --git a/test/test_int_set.jl b/test/test_int_set.jl index 6606e794..e02a12a0 100644 --- a/test/test_int_set.jl +++ b/test/test_int_set.jl @@ -313,4 +313,65 @@ import DataStructures: IntSet show(IOBuffer(), complement(IntSet())) end + + @testset "AbstractSet" begin + @test IntSet([1, 2]) isa AbstractSet{Int} + @test IntSet() isa AbstractSet{Int} + + # Generic AbstractSet algorithms dispatch on IntSet + @test issetequal(IntSet([1, 2]), IntSet([2, 1])) + @test isdisjoint(IntSet([1]), IntSet([2])) + @test !isdisjoint(IntSet([1, 2]), IntSet([2, 3])) + @test IntSet([1]) ⊆ Set([1, 2]) + @test Set([1]) ⊆ IntSet([1, 2]) + + # Cross-type equality becomes available via AbstractSet fallbacks + @test IntSet([1, 2]) == Set([1, 2]) + @test IntSet([1, 2]) == SparseIntSet([1, 2]) + + # Derived operations stay within the type rather than degrading to Set + @test filter(isodd, IntSet([1, 2, 3])) isa IntSet + @test filter(isodd, IntSet([1, 2, 3])) == IntSet([1, 3]) + @test filter(_ -> false, IntSet([1, 2])) isa IntSet + @test isempty(filter(_ -> false, IntSet([1, 2]))) + @test setdiff(IntSet([1, 2, 3]), [2]) isa IntSet + @test setdiff(IntSet([1, 2, 3]), [2]) == IntSet([1, 3]) + @test setdiff(IntSet([1, 2, 3]), IntSet([2])) isa IntSet + + # filter! / intersect! with AbstractSet (Base uses filter!(in(s2), s)) + s = IntSet([1, 2, 3]) + filter!(isodd, s) + @test s == IntSet([1, 3]) + s = IntSet([1, 2, 3]) + intersect!(s, Set([2, 3, 4])) + @test s == IntSet([2, 3]) + s = IntSet([1]) + union!(s, Set([2])) + @test s == IntSet([1, 2]) + + # Same-type equality and ordering still win on specificity + @test IntSet([1]) == IntSet([1]) + @test IntSet([1]) < IntSet([1, 2]) + @test IntSet([1, 2]) <= IntSet([1, 2]) + + # symdiff! with an AbstractSet argument resolves unambiguously + @test symdiff!(IntSet([1, 2, 3]), Set([2, 4])) == IntSet([1, 3, 4]) + + # Complement remains IntSet <: AbstractSet + c = complement(IntSet([0, 1, 2])) + @test c isa IntSet + @test c isa AbstractSet{Int} + @test 3 in c + @test !(1 in c) + + # emptymutable / copymutable keep type + @test Base.emptymutable(IntSet([1])) isa IntSet + s = IntSet([1, 2]) + cpy = Base.copymutable(s) + push!(cpy, 9) + @test 9 in cpy + @test !(9 in s) + end + + end # @testset IntSet diff --git a/test/test_sparse_int_set.jl b/test/test_sparse_int_set.jl index b794a2cf..d64bb6de 100644 --- a/test/test_sparse_int_set.jl +++ b/test/test_sparse_int_set.jl @@ -1,5 +1,5 @@ using DataStructures, Test -import DataStructures: SparseIntSet +import DataStructures: SparseIntSet, IntSet @testset "SparseIntSet" begin @testset "Construction, collect" begin @@ -189,4 +189,49 @@ import DataStructures: SparseIntSet @test zip() isa Iterators.Zip # issue 621 end + + @testset "AbstractSet" begin + @test SparseIntSet([1, 2]) isa AbstractSet{Int} + @test SparseIntSet() isa AbstractSet{Int} + + @test issetequal(SparseIntSet([1, 2]), SparseIntSet([2, 1])) + @test isdisjoint(SparseIntSet([1]), SparseIntSet([2])) + @test !isdisjoint(SparseIntSet([1, 2]), SparseIntSet([2, 3])) + @test SparseIntSet([1]) ⊆ Set([1, 2]) + @test Set([1]) ⊆ SparseIntSet([1, 2]) + + @test SparseIntSet([1, 2]) == Set([1, 2]) + @test SparseIntSet([1, 2]) == IntSet([1, 2]) + + @test filter(isodd, SparseIntSet([1, 2, 3])) isa SparseIntSet + @test issetequal(filter(isodd, SparseIntSet([1, 2, 3])), SparseIntSet([1, 3])) + @test filter(_ -> false, SparseIntSet([1, 2])) isa SparseIntSet + @test isempty(filter(_ -> false, SparseIntSet([1, 2]))) + @test setdiff(SparseIntSet([1, 2, 3]), [2]) isa SparseIntSet + @test issetequal(setdiff(SparseIntSet([1, 2, 3]), [2]), SparseIntSet([1, 3])) + + s = SparseIntSet([1, 2, 3]) + filter!(isodd, s) + @test issetequal(s, SparseIntSet([1, 3])) + @test issetequal(intersect!(SparseIntSet([1, 2, 3]), Set([2, 3, 4])), SparseIntSet([2, 3])) + s = SparseIntSet([1]) + union!(s, Set([2])) + @test issetequal(s, SparseIntSet([1, 2])) + + @test SparseIntSet([1]) == SparseIntSet([1]) + @test SparseIntSet([1]) < SparseIntSet([1, 2]) + @test SparseIntSet([1, 2]) <= SparseIntSet([1, 2]) + + # No native symdiff; AbstractSet fallback must still work + @test issetequal(symdiff(SparseIntSet([1, 2]), Set([2, 3])), Set([1, 3])) + + @test Base.emptymutable(SparseIntSet([1])) isa SparseIntSet + s = SparseIntSet([1, 2]) + cpy = Base.copymutable(s) + push!(cpy, 9) + @test 9 in cpy + @test !(9 in s) + end + + end