From e6fd04e7c240aadff7fc0aae530a89eb1e4c11e5 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Thu, 10 Aug 2023 01:04:12 -0400 Subject: [PATCH 01/15] Follow-up on #95 --- src/OrderedCollections.jl | 3 +- src/dict_support.jl | 5 + src/little_set.jl | 130 +++++++++++++++++++ src/ordered_set.jl | 2 - test/runtests.jl | 1 + test/test_little_set.jl | 257 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 src/little_set.jl create mode 100644 test/test_little_set.jl diff --git a/src/OrderedCollections.jl b/src/OrderedCollections.jl index 47e1128..02b0e58 100644 --- a/src/OrderedCollections.jl +++ b/src/OrderedCollections.jl @@ -17,12 +17,13 @@ module OrderedCollections valtype, lastindex, nextind, copymutable, emptymutable, dict_with_eltype - export OrderedDict, OrderedSet, LittleDict + export OrderedDict, OrderedSet, LittleDict, LittleSet export freeze include("dict_support.jl") include("ordered_dict.jl") include("little_dict.jl") + include("little_set.jl") include("ordered_set.jl") include("dict_sorting.jl") diff --git a/src/dict_support.jl b/src/dict_support.jl index b44f514..2ab397f 100644 --- a/src/dict_support.jl +++ b/src/dict_support.jl @@ -4,3 +4,8 @@ # so they are redefined here. _tablesz(x::Integer) = x < 16 ? 16 : one(x)<<((sizeof(x)<<3)-leading_zeros(x-1)) hashindex(key, sz) = (reinterpret(Int,(hash(key))) & (sz-1)) + 1 + +const ERROR_INDEX = typemin(Int) + +const orderedset_seed = UInt === UInt64 ? 0x2114638a942a91a5 : 0xd86bdbf1 + diff --git a/src/little_set.jl b/src/little_set.jl new file mode 100644 index 0000000..da7c5f5 --- /dev/null +++ b/src/little_set.jl @@ -0,0 +1,130 @@ + +struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} + data::D + + LittleSet{T, D}(data) where {T,D} = new{T, D}(data) + function LittleSet{T}(data::AbstractVector) where {T} + if eltype(data) == T + return new{T, typeof(data)}(data) + else + d = convert(AbstractVector{T}, data) + return new{T, typeof(d)}(d) + end + end + LittleSet{T}(data::Tuple{Vararg{T}}) where {T} = new{T, typeof(data)}(data) + LittleSet{T}(itr) where {T} = union!(LittleSet{T}(), itr) + LittleSet{T}() where {T} = new{T, Vector{T}}(Vector{T}()) + LittleSet() = LittleSet{Any}() + function LittleSet(itr) + IET = Base.IteratorEltype(itr) + if isa(IET, Base.HasEltype) + LittleSet{eltype(itr)}(itr) + else + T = Base.@default_eltype(itr) + if (Base.isconcretetype(T) || T === Union{}) + return LittleSet{T}(itr) + else + return Base.grow_to!(LittleSet{T}(), itr) + end + end + end +end + +# `data` should not be directly accessed +Base.propertynames(::LittleSet) = () +function Base.propertynames(::LittleSet, ::Symbol) + throw(ArgumentError("LittleSet does not support public access to it's fields.")) +end + +const FrozenLittleSet{T} = LittleSet{T, <:Tuple} +const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} + +Base.Tuple(s::FrozenLittleSet) = getfield(s, :data) +Base.Tuple(s::UnfrozenLittleDict) = Tuple(getfield(s, :data)) + +# find the index position of `key` +function find_key_index(key, s::LittleSet) + data = getfield(s, :data) + for i in eachindex(data) + isequal(@inbounds(data[i]), key) && return i + end + return ERROR_INDEX +end + +freeze(s::AbstractSet{T}) where {T} = LittleSet{T}(Tuple(s)) + +function hash(s::LittleSet, h::UInt) + hash(getfield(s, :data), hash(orderedset_seed, h)) +end + +Base.length(s::LittleSet) = length(getfield(s, :data)) + +Base.isempty(s::LittleSet) = isempty(getfield(s, :data)) + +Base.empty(s::LittleSet{T}) where {T} = LittleSet{T}(empty(getfield(s, :data))) + +Base.iterate(s::LittleSet, state=1) = iterate(getfield(s, :data), state) + +Base.in(x, s::LittleSet) = in(x, getfield(s, :data)) + +# since `Base.copy` is a shallow copy on collections, an immutable collection like `Tuple` the same +Base.copy(s::FrozenLittleSet) = s +Base.copy(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(copy(getfield(s, :data))) + +function Base.sizehint!(s::UnfrozenLittleSet, sz) + sizehint!(getfield(s, :data), sz) + return s +end + +Base.filter!(f, s::UnfrozenLittleSet) = filter!(f, getfield(s, :data)) + +function Base.push!(s::UnfrozenLittleSet, val) + data = getfield(s, :data) + if !in(val, data) + push!(data, val) + end + return s +end + +Base.pop!(s::UnfrozenLittleSet) = pop!(getfield(s, :data)) +function Base.pop!(s::UnfrozenLittleSet, key) + index = find_key_index(key, s) + index === ERROR_INDEX && throw(KeyError(key)) + deleteat!(getfield(s, :data), index) + key +end +function Base.pop!(s::UnfrozenLittleSet, key, default) + index = find_key_index(key, s) + if index === ERROR_INDEX + return default + else + deleteat!(getfield(s, :data), index) + return key + end +end + + +Base.empty!(s::UnfrozenLittleSet) = (empty!(getfield(s, :data)); s) + +function Base.delete!(s::UnfrozenLittleSet, key) + index = find_key_index(key, s) + if index !== ERROR_INDEX + deleteat!(getfield(s, :data), index) + end + return s +end + +function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} + LittleSet{T}(sort(getfield(s, :data); ks...)) +end +# this is a temporary hack to get around the lack of `sort` available for tuples +function Base.sort(s::FrozenLittleSet{T}; ks...) where {T} + LittleSet{T}(sort(T[getfield(s, :data)...]; ks...)) +end + +function Base.sort!(s::UnfrozenLittleSet; ks...) + sort!(getfield(s, :data); ks...) + return s +end + + diff --git a/src/ordered_set.jl b/src/ordered_set.jl index b7f329a..f71d293 100644 --- a/src/ordered_set.jl +++ b/src/ordered_set.jl @@ -2,7 +2,6 @@ # This was largely copied and modified from Base - struct OrderedSet{T} <: AbstractSet{T} dict::OrderedDict{T,Nothing} @@ -72,7 +71,6 @@ function filter!(f::Function, s::OrderedSet) return s end -const orderedset_seed = UInt === UInt64 ? 0x2114638a942a91a5 : 0xd86bdbf1 function hash(s::OrderedSet, h::UInt) h = hash(orderedset_seed, h) s.dict.ndel > 0 && rehash!(s.dict) diff --git a/test/runtests.jl b/test/runtests.jl index 07a9879..27ad299 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,7 @@ using Random, Serialization tests = [ "little_dict", + "little_set", "ordered_dict", "ordered_set", ] diff --git a/test/test_little_set.jl b/test/test_little_set.jl new file mode 100644 index 0000000..c3c8ac3 --- /dev/null +++ b/test/test_little_set.jl @@ -0,0 +1,257 @@ +using OrderedCollections, Test + +@testset "LittleSet" begin + + @testset "Constructors" begin + @test isa(LittleSet{Int}(keys(LittleDict{Int,Float64}(Vector{Int}(), Vector{Float64}()))), LittleSet{Int}) + @test isa(LittleSet{Int}(keys(LittleDict([(1,2.0)]))), LittleSet{Int}) + @test isa(LittleSet(), LittleSet{Any}) + @test isa(LittleSet([1,2,3]), LittleSet{Int}) + @test isa(LittleSet{Int}([3]), LittleSet{Int}) + data_in = (1, "banana", ()) + s = LittleSet(data_in) + data_out = collect(s) + @test isa(data_out, Array{Any,1}) + @test tuple(data_out...) === data_in + @test tuple(data_in...) === tuple(s...) + @test length(data_out) == length(data_in) + end + + @testset "hash" begin + s1 = LittleSet{String}(["bar", "foo"]) + s2 = LittleSet{String}(["foo", "bar"]) + s3 = LittleSet{String}(["baz"]) + @test hash(s1) != hash(s2) + @test hash(s1) != hash(s3) + end + + @testset "isequal" begin + @test isequal(LittleSet(), LittleSet()) + @test !isequal(LittleSet(), LittleSet([1])) + @test isequal(LittleSet{Any}(Any[1,2]), LittleSet{Int}([1,2])) + @test !isequal(LittleSet{Any}(Any[1,2]), LittleSet{Int}([1,2,3])) + + @test isequal(LittleSet{Int}(), LittleSet{AbstractString}()) + @test !isequal(LittleSet{Int}(), LittleSet{AbstractString}([""])) + @test !isequal(LittleSet{AbstractString}(), LittleSet{Int}([0])) + @test !isequal(LittleSet{Int}([1]), LittleSet{AbstractString}()) + @test isequal(LittleSet{Any}([1,2,3]), LittleSet{Int}([1,2,3])) + @test isequal(LittleSet{Int}([1,2,3]), LittleSet{Any}([1,2,3])) + @test !isequal(LittleSet{Any}([1,2,3]), LittleSet{Int}([1,2,3,4])) + @test !isequal(LittleSet{Int}([1,2,3]), LittleSet{Any}([1,2,3,4])) + @test !isequal(LittleSet{Any}([1,2,3,4]), LittleSet{Int}([1,2,3])) + @test !isequal(LittleSet{Int}([1,2,3,4]), LittleSet{Any}([1,2,3])) + end + + @testset "eltype, empty" begin + s1 = empty(LittleSet([1,"hello"])) + @test isequal(s1, LittleSet()) + @test eltype(s1) === Any + s2 = empty(LittleSet{Float32}([2.0f0,3.0f0,4.0f0])) + @test isequal(s2, LittleSet()) + @test eltype(s2) === Float32 + end + + @testset "Core Functionality" begin + s = LittleSet(); push!(s,1); push!(s,2); push!(s,3) + @test !isempty(s) + @test in(1,s) + @test in(2,s) + @test length(s) == 3 + push!(s,1); push!(s,2); push!(s,3) + @test length(s) == 3 + @test pop!(s,1) == 1 + @test !in(1,s) + @test in(2,s) + @test length(s) == 2 + @test_throws KeyError pop!(s,1) + @test pop!(s,1,:foo) == :foo + @test length(delete!(s,2)) == 1 + @test !in(1,s) + @test !in(2,s) + @test pop!(s) == 3 + @test length(s) == 0 + @test isempty(s) + end + + @testset "copy" begin + data_in = [1,2,9,8,4] + s = LittleSet(data_in) + c = copy(s) + @test isequal(s,c) + v = pop!(s) + @test !in(v,s) + @test in(v,c) + push!(s,100) + push!(c,200) + @test !in(100,c) + @test !in(200,s) + end + + @testset "sizehint!, empty" begin + s = LittleSet([1]) + @test isequal(sizehint!(s, 10), LittleSet([1])) + @test isequal(empty!(s), LittleSet()) + # TODO: rehash + end + + @testset "iterate" begin + for data_in in ((7,8,4,5), + ("hello", 23, 2.7, (), [], (1,8))) + s = LittleSet(data_in) + + s_new = LittleSet() + for el in s + push!(s_new, el) + end + @test isequal(s, s_new) + + t = tuple(s...) + + @test t === data_in + @test length(t) == length(s) + for (e,f) in zip(t,s) + @test e === f + end + end + end + + @testset "union" begin + @test isequal(union(LittleSet([1])),LittleSet([1])) + s = ∪(LittleSet([1,2]), LittleSet([3,4])) + @test isequal(s, LittleSet([1,2,3,4])) + s = union(LittleSet([5,6,7,8]), LittleSet([7,8,9])) + @test isequal(s, LittleSet([5,6,7,8,9])) + s = LittleSet([1,3,5,7]) + union!(s,(2,3,4,5)) + # TODO: order is not the same, so isequal should return false... + @test isequal(s,LittleSet([1,2,3,4,5,7])) + end + + @testset "intersect" begin + @test isequal(intersect(LittleSet([1])),LittleSet([1])) + s = ∩(LittleSet([1,2]), LittleSet([3,4])) + @test isequal(s, LittleSet()) + s = intersect(LittleSet([5,6,7,8]), LittleSet([7,8,9])) + @test isequal(s, LittleSet([7,8])) + @test isequal(intersect(LittleSet([2,3,1]), LittleSet([4,2,3]), LittleSet([5,4,3,2])), LittleSet([2,3])) + end + + @testset "setdiff" begin + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet()), LittleSet([1,2,3])) + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet([1])), LittleSet([2,3])) + @test isequal(setdiff(LittleSet([1,2,3]), Set([1])), LittleSet([2,3])) + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet([1,2])), LittleSet([3])) + @test isequal(setdiff(LittleSet([1,2,3]), Set([1,2])), LittleSet([3])) + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet([1,2,3])), LittleSet()) + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet([4])), LittleSet([1,2,3])) + @test isequal(setdiff(LittleSet([1,2,3]), LittleSet([4,1])), LittleSet([2,3])) + s = LittleSet([1,3,5,7]) + setdiff!(s,(3,5)) + @test isequal(s,LittleSet([1,7])) + s = LittleSet([1,2,3,4]) + setdiff!(s, LittleSet([2,4,5,6])) + @test isequal(s,LittleSet([1,3])) + end + + @testset "ordering" begin + @test LittleSet() < LittleSet([1]) + @test LittleSet([1]) < LittleSet([1,2]) + @test !(LittleSet([3]) < LittleSet([1,2])) + @test !(LittleSet([3]) > LittleSet([1,2])) + @test LittleSet([1,2,3]) > LittleSet([1,2]) + @test !(LittleSet([3]) <= LittleSet([1,2])) + @test !(LittleSet([3]) >= LittleSet([1,2])) + @test LittleSet([1]) <= LittleSet([1,2]) + @test LittleSet([1,2]) <= LittleSet([1,2]) + @test LittleSet([1,2]) >= LittleSet([1,2]) + @test LittleSet([1,2,3]) >= LittleSet([1,2]) + @test !(LittleSet([1,2,3]) >= LittleSet([1,2,4])) + @test !(LittleSet([1,2,3]) <= LittleSet([1,2,4])) + end + + @testset "issubset, symdiff" begin + for (l,r) in ((LittleSet([1,2]), LittleSet([3,4])), + (LittleSet([5,6,7,8]), LittleSet([7,8,9])), + (LittleSet([1,2]), LittleSet([3,4])), + (LittleSet([5,6,7,8]), LittleSet([7,8,9])), + (LittleSet([1,2,3]), LittleSet()), + (LittleSet([1,2,3]), LittleSet([1])), + (LittleSet([1,2,3]), LittleSet([1,2])), + (LittleSet([1,2,3]), LittleSet([1,2,3])), + (LittleSet([1,2,3]), LittleSet([4])), + (LittleSet([1,2,3]), LittleSet([4,1]))) + @test issubset(intersect(l,r), l) + @test issubset(intersect(l,r), r) + @test issubset(l, union(l,r)) + @test issubset(r, union(l,r)) + @test isequal(union(intersect(l,r),symdiff(l,r)), union(l,r)) + end + @test ⊆(LittleSet([1]), LittleSet([1,2])) + + @test ⊊(LittleSet([1]), LittleSet([1,2])) + @test !⊊(LittleSet([1]), LittleSet([1])) + @test ⊈(LittleSet([1]), LittleSet([2])) + + @test symdiff(LittleSet([1,2,3,4]), LittleSet([2,4,5,6])) == LittleSet([1,3,5,6]) + + @test isequal(symdiff(LittleSet([1,2,3,4]), LittleSet([2,4,5,6])), LittleSet([1,3,5,6])) + + end + + @testset "filter" begin + s = LittleSet([1,2,3,4]) + @test isequal(filter(isodd,s), LittleSet([1,3])) + filter!(isodd, s) + @test isequal(s, LittleSet([1,3])) + end + + @testset "first" begin + @test_throws ArgumentError first(LittleSet()) + @test first(LittleSet([2])) == 2 + end + + @testset "empty set" begin + d = LittleSet{Char}() + @test length(d) == 0 + @test isempty(d) + @test !('c' in d) + push!(d, 'c') + @test !isempty(d) + empty!(d) + @test isempty(d) + end + + @testset "access, modification" begin + d = LittleSet{Char}() + + for c in 'a':'z' + push!(d, c) + end + + for c in 'a':'z' + @test c in d + end + + @test collect(d) == collect('a':'z') + end + + @testset "sort(!)" begin + x = [-4, 1, -5, 10, 7] + ox = LittleSet(x) + @test !issorted(ox) + sox = sort(ox) + @test issorted(sox) + sox = sort(ox; rev=true) + @test !issorted(sox) + @test issorted(sox; rev=true) + ox = LittleSet(x) + @test ox === sort!(ox) + @test issorted(ox) + ox = LittleSet(x) + @test ox === sort!(ox; rev=true) + @test !issorted(ox) + @test issorted(ox; rev=true) + end + +end # @testset LittleSet From b8c94e401ba3e9b6d1eb12249b756b6fc021b835 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sat, 12 Aug 2023 22:51:24 -0400 Subject: [PATCH 02/15] More tests and attempts to preserve provided type --- src/dict_support.jl | 2 - src/little_set.jl | 192 +++++++++++++++++++++++++++++++--------- test/test_little_set.jl | 35 +++++++- 3 files changed, 180 insertions(+), 49 deletions(-) diff --git a/src/dict_support.jl b/src/dict_support.jl index 2ab397f..0ee58a8 100644 --- a/src/dict_support.jl +++ b/src/dict_support.jl @@ -5,7 +5,5 @@ _tablesz(x::Integer) = x < 16 ? 16 : one(x)<<((sizeof(x)<<3)-leading_zeros(x-1)) hashindex(key, sz) = (reinterpret(Int,(hash(key))) & (sz-1)) + 1 -const ERROR_INDEX = typemin(Int) - const orderedset_seed = UInt === UInt64 ? 0x2114638a942a91a5 : 0xd86bdbf1 diff --git a/src/little_set.jl b/src/little_set.jl index da7c5f5..d805b74 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -3,6 +3,13 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} data::D LittleSet{T, D}(data) where {T,D} = new{T, D}(data) + function LittleSet{T, Tuple{Vararg{T}}}(@nospecialize(data)) where {T} + if isa(data, Tuple{Vararg{T}}) + new{T, Tuple{Vararg{T}}}(data) + else + new{T, Tuple{Vararg{T}}}(Tuple(data)) + end + end function LittleSet{T}(data::AbstractVector) where {T} if eltype(data) == T return new{T, typeof(data)}(data) @@ -31,8 +38,8 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end # `data` should not be directly accessed -Base.propertynames(::LittleSet) = () -function Base.propertynames(::LittleSet, ::Symbol) +Base.propertynames(@nospecialize(x::LittleSet)) = () +function Base.propertynames(@nospecialize(x::LittleSet), ::Symbol) throw(ArgumentError("LittleSet does not support public access to it's fields.")) end @@ -42,33 +49,39 @@ const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} Base.Tuple(s::FrozenLittleSet) = getfield(s, :data) Base.Tuple(s::UnfrozenLittleDict) = Tuple(getfield(s, :data)) -# find the index position of `key` -function find_key_index(key, s::LittleSet) - data = getfield(s, :data) - for i in eachindex(data) - isequal(@inbounds(data[i]), key) && return i - end - return ERROR_INDEX -end - freeze(s::AbstractSet{T}) where {T} = LittleSet{T}(Tuple(s)) -function hash(s::LittleSet, h::UInt) - hash(getfield(s, :data), hash(orderedset_seed, h)) -end +hash(s::LittleSet, h::UInt) = hash(getfield(s, :data), hash(orderedset_seed, h)) Base.length(s::LittleSet) = length(getfield(s, :data)) -Base.isempty(s::LittleSet) = isempty(getfield(s, :data)) +Base.isempty(s::UnfrozenLittleSet) = isempty(getfield(s, :data)) -Base.empty(s::LittleSet{T}) where {T} = LittleSet{T}(empty(getfield(s, :data))) +function Base.empty(@nospecialize(s::FrozenLittleSet)) + T = eltype(s) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + return LittleSet{T, Tuple{Vararg{T}}}(()) + else + return LittleSet{T, Tuple{}}(()) + end +end +Base.empty(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(empty(getfield(s, :data))) -Base.iterate(s::LittleSet, state=1) = iterate(getfield(s, :data), state) +Base.iterate(s::LittleSet, state...) = iterate(getfield(s, :data), state...) -Base.in(x, s::LittleSet) = in(x, getfield(s, :data)) +Base.in(x, s::UnfrozenLittleSet) = in(x, getfield(s, :data)) +function Base.in(x, s::FrozenLittleSet) + data = getfield(s, :data) + n = nfields(data) + while n > 0 + isequal(x, getfield(data, n)) && return true + n -= 1 + end + return false +end # since `Base.copy` is a shallow copy on collections, an immutable collection like `Tuple` the same -Base.copy(s::FrozenLittleSet) = s +Base.copy(@nospecialize(s::FrozenLittleSet)) = s Base.copy(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(copy(getfield(s, :data))) function Base.sizehint!(s::UnfrozenLittleSet, sz) @@ -76,8 +89,113 @@ function Base.sizehint!(s::UnfrozenLittleSet, sz) return s end -Base.filter!(f, s::UnfrozenLittleSet) = filter!(f, getfield(s, :data)) +function Base.emptymutable(s::UnfrozenLittleSet{T}, ::Type{U}=T) where {T, U} + LittleSet(Base.emptymutable(getfield(s, :data), U)) +end +function Base.emptymutable(@nospecialize(s::FrozenLittleSet), ::Type{U}=eltype(s)) where {U} + LittleSet(U[]) +end + +Base.copymutable(@nospecialize(s::FrozenLittleSet)) = LittleSet(eltype(s)[]) +function Base.copymutable(s::UnfrozenLittleSet{T}) where {T} + LittleSet{T}(Base.copymutable(getfield(s, :data))) +end + +function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} + LittleSet{T}(sort(getfield(s, :data); ks...)) +end +# HACK: this is a temporary hack to get around the lack of `sort` available for tuples +function Base.sort(s::FrozenLittleSet{T}; ks...) where {T} + LittleSet{T}(sort(T[getfield(s, :data)...]; ks...)) +end +function Base.sort!(s::UnfrozenLittleSet; ks...) + sort!(getfield(s, :data); ks...) + return s +end + +function Base.replace(f::Union{Function, Type}, s::LittleSet{T}; count::Integer=typemax(Int)) where {T} + newdata = replace(f, getfield(s, :data); count=count) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + T2 = eltype(newdata) + return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) + else + return LittleSet(newdata) + end +end +function Base.replace(s::LittleSet{T}, old_new::Pair...; count::Integer=typemax(Int)) where {T} + newdata = replace(getfield(s, :data), old_new...; count=count) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + T2 = eltype(newdata) + return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) + else + return LittleSet(newdata) + end +end + +Base.first(s::UnfrozenLittleSet, n::Integer) = LittleSet(first(getfield(s, :data), n)) +function Base.first(s::FrozenLittleSet{T}, n::Integer) where {T} + N = Int(n) + N < 0 && throw(ArgumentError("Number of elements must be nonnegative")) + data = getfield(s, :data) + stop = nfields(data) + if stop <= N + # max number of elements is everything so it's equivalent to `copy` + return s + elseif isa(s, LittleSet{T, Tuple{Vararg{T}}}) + return LittleSet{T, Tuple{Vararg{T}}}(Tuple(@inbounds(collect(data)[1:N]))) + else + return LittleSet{T}(ntuple(i->getfield(data, i), min(nfields(data), n))) + end +end + +Base.last(s::UnfrozenLittleSet, n::Integer) = LittleSet(last(getfield(s, :data), n)) +function Base.last(s::FrozenLittleSet{T}, n::Integer) where {T} + N = Int(n) + N < 0 && throw(ArgumentError("Number of elements must be nonnegative")) + data = getfield(s, :data) + stop = nfields(data) + offset = stop - N + if offset < 1 + # offset less than one means each element is returned so it's equivalent to `copy` + return s + elseif isa(s, LittleSet{T, Tuple{Vararg{T}}}) + return LittleSet{T, Tuple{Vararg{T}}}(Tuple(@inbounds(collect(data)[1:N]))) + else + return LittleSet{T}(ntuple(i->getfield(data, offset + i), N)) + end +end + +function Base.union(x::FrozenLittleSet{T1}, y::FrozenLittleSet{T2}) where {T1, T2} + xdata = getfield(x, :data) + nx = nfields(xdata) + ydata = getfield(y, :data) + ny = nfields(ydata) + if isa(x, LittleSet{T1, Tuple{Vararg{T1}}}) || isa(y, LittleSet{T2, Tuple{Vararg{T2}}}) + if nx < ny # nx is smaller so search for x items in y + newdata = (filter(!in(y), x)..., ydata...) + else # ny is smaller so search for y items in x + newdata = (xdata..., getfield(filter(!in(x), y), :data)...) + end + T = Union{T1, T2} + LittleSet{T, Tuple{Vararg{T}}}(newdata) + else + if nx < ny # nx is smaller so search for x items in y + return LittleSet((filter(!in(ydata), xdata)..., ydata...)) + else # ny is smaller so search for y items in x + return LittleSet((xdata..., filter(!in(xdata), ydata)...)) + end + end +end + +function Base.filter(f, s::LittleSet{T}) where {T} + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + return LittleSet{T, Tuple{Vararg{T}}}(Tuple(filter(f, collect(data)))) + else + return LittleSet(filter(f, getfield(s, :data))) + end +end +Base.filter!(f, s::UnfrozenLittleSet) = (filter!(f, getfield(s, :data)); return s) function Base.push!(s::UnfrozenLittleSet, val) data = getfield(s, :data) if !in(val, data) @@ -88,43 +206,31 @@ end Base.pop!(s::UnfrozenLittleSet) = pop!(getfield(s, :data)) function Base.pop!(s::UnfrozenLittleSet, key) - index = find_key_index(key, s) - index === ERROR_INDEX && throw(KeyError(key)) - deleteat!(getfield(s, :data), index) + data = getfield(s, :data) + index = findfirst(isequal(key), data) + index === nothing && throw(KeyError(key)) + deleteat!(data, index) key end function Base.pop!(s::UnfrozenLittleSet, key, default) - index = find_key_index(key, s) - if index === ERROR_INDEX + data = getfield(s, :data) + index = findfirst(isequal(key), data) + if index === nothing return default else - deleteat!(getfield(s, :data), index) + deleteat!(data, index) return key end end - Base.empty!(s::UnfrozenLittleSet) = (empty!(getfield(s, :data)); s) function Base.delete!(s::UnfrozenLittleSet, key) - index = find_key_index(key, s) - if index !== ERROR_INDEX + data = getfield(s, :data) + index = findfirst(isequal(key), data) + if index !== nothing deleteat!(getfield(s, :data), index) end return s end -function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} - LittleSet{T}(sort(getfield(s, :data); ks...)) -end -# this is a temporary hack to get around the lack of `sort` available for tuples -function Base.sort(s::FrozenLittleSet{T}; ks...) where {T} - LittleSet{T}(sort(T[getfield(s, :data)...]; ks...)) -end - -function Base.sort!(s::UnfrozenLittleSet; ks...) - sort!(getfield(s, :data); ks...) - return s -end - - diff --git a/test/test_little_set.jl b/test/test_little_set.jl index c3c8ac3..46d1048 100644 --- a/test/test_little_set.jl +++ b/test/test_little_set.jl @@ -125,7 +125,30 @@ using OrderedCollections, Test s = LittleSet([1,3,5,7]) union!(s,(2,3,4,5)) # TODO: order is not the same, so isequal should return false... - @test isequal(s,LittleSet([1,2,3,4,5,7])) + @test isequal(s, LittleSet([1,2,3,4,5,7])) + end + + @testset "first" begin + @test_throws ArgumentError first(LittleSet()) + @test first(LittleSet([2])) == 2 + + s = LittleSet([1,2,3]) + @test length(first(s, 2)) == 2 + @test length(first(s, 10)) == 3 + + s = LittleSet((1,2,3)) + @test length(first(s, 2)) == 2 + @test length(first(s, 10)) == 3 + end + + @testset "last" begin + s = LittleSet([1,2,3]) + @test length(last(s, 2)) == 2 + @test length(last(s, 10)) == 3 + + s = LittleSet((1,2,3)) + @test length(last(s, 2)) == 2 + @test length(last(s, 10)) == 3 end @testset "intersect" begin @@ -204,11 +227,15 @@ using OrderedCollections, Test @test isequal(filter(isodd,s), LittleSet([1,3])) filter!(isodd, s) @test isequal(s, LittleSet([1,3])) + s = LittleSet((1,2,3,4)) + @test isa(filter(isodd, s), OrderedCollections.FrozenLittleSet) end - @testset "first" begin - @test_throws ArgumentError first(LittleSet()) - @test first(LittleSet([2])) == 2 + @testset "replace" begin + s = LittleSet([1,2,3,4]) + @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet([0, 5, 3, 4])) + s = LittleSet{Int, Tuple{Vararg{Int}}}((1,2,3,4)) + @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet((0, 5, 3, 4))) end @testset "empty set" begin From 3502d309c1bb9c3e7138a4fe6868f31ddaebd82d Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sat, 12 Aug 2023 23:14:54 -0400 Subject: [PATCH 03/15] Fix some inference --- src/little_set.jl | 8 ++++---- test/test_little_set.jl | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index d805b74..891e883 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -122,10 +122,10 @@ function Base.replace(f::Union{Function, Type}, s::LittleSet{T}; count::Integer= return LittleSet(newdata) end end -function Base.replace(s::LittleSet{T}, old_new::Pair...; count::Integer=typemax(Int)) where {T} +function Base.replace(s::LittleSet{T}, old_new::Pair{F, S}...; count::Integer=typemax(Int)) where {T, F, S} newdata = replace(getfield(s, :data), old_new...; count=count) if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - T2 = eltype(newdata) + T2 = Union{T, S} return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) else return LittleSet(newdata) @@ -177,7 +177,7 @@ function Base.union(x::FrozenLittleSet{T1}, y::FrozenLittleSet{T2}) where {T1, T newdata = (xdata..., getfield(filter(!in(x), y), :data)...) end T = Union{T1, T2} - LittleSet{T, Tuple{Vararg{T}}}(newdata) + return LittleSet{T, Tuple{Vararg{T}}}(newdata) else if nx < ny # nx is smaller so search for x items in y return LittleSet((filter(!in(ydata), xdata)..., ydata...)) @@ -189,7 +189,7 @@ end function Base.filter(f, s::LittleSet{T}) where {T} if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - return LittleSet{T, Tuple{Vararg{T}}}(Tuple(filter(f, collect(data)))) + return LittleSet{T, Tuple{Vararg{T}}}(Tuple(filter(f, collect(getfield(s, :data))))) else return LittleSet(filter(f, getfield(s, :data))) end diff --git a/test/test_little_set.jl b/test/test_little_set.jl index 46d1048..5e6c997 100644 --- a/test/test_little_set.jl +++ b/test/test_little_set.jl @@ -118,8 +118,18 @@ using OrderedCollections, Test @testset "union" begin @test isequal(union(LittleSet([1])),LittleSet([1])) + s = ∪(LittleSet([1,2]), LittleSet([3,4])) @test isequal(s, LittleSet([1,2,3,4])) + + s = ∪(LittleSet((1, 2)), LittleSet((3,4))) + @test isequal(s, LittleSet((1,2,3,4))) + + s1 = LittleSet{Int, Tuple{Vararg{Int}}}((1, 2)) + s2 = LittleSet{Int, Tuple{Vararg{Int}}}((3,4)) + s = @inferred(∪(s1, s2)) + @test isequal(s, LittleSet((1,2,3,4))) + s = union(LittleSet([5,6,7,8]), LittleSet([7,8,9])) @test isequal(s, LittleSet([5,6,7,8,9])) s = LittleSet([1,3,5,7]) @@ -139,6 +149,9 @@ using OrderedCollections, Test s = LittleSet((1,2,3)) @test length(first(s, 2)) == 2 @test length(first(s, 10)) == 3 + + s = LittleSet{Int, Tuple{Vararg{Int}}}((1,2,3)) + @test length(@inferred(first(s, 2))) == 2 end @testset "last" begin @@ -149,6 +162,9 @@ using OrderedCollections, Test s = LittleSet((1,2,3)) @test length(last(s, 2)) == 2 @test length(last(s, 10)) == 3 + + s = LittleSet{Int, Tuple{Vararg{Int}}}((1,2,3)) + @test length(@inferred(last(s, 2))) == 2 end @testset "intersect" begin @@ -235,7 +251,7 @@ using OrderedCollections, Test s = LittleSet([1,2,3,4]) @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet([0, 5, 3, 4])) s = LittleSet{Int, Tuple{Vararg{Int}}}((1,2,3,4)) - @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet((0, 5, 3, 4))) + @test isequal(@inferred(replace(s, 1 => 0, 2 => 5)), LittleSet((0, 5, 3, 4))) end @testset "empty set" begin From 44f114a8302c583e54379ac80ef6ecabe26a3157 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sat, 19 Aug 2023 22:24:34 -0400 Subject: [PATCH 04/15] Fix `replace` implementation This overcomes the issue with Julia v1.6 not supporting replace for tuples and does a better job working around the opaque tuple variant --- src/little_set.jl | 191 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 41 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index 891e883..96ecf25 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -2,14 +2,22 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} data::D + global const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} + global const FrozenLittleSet{T} = LittleSet{T, <:Tuple} + global const OpaqueLittleSet{T} = LittleSet{T, Tuple{Vararg{T}}} + LittleSet{T, D}(data) where {T,D} = new{T, D}(data) - function LittleSet{T, Tuple{Vararg{T}}}(@nospecialize(data)) where {T} + function OpaqueLittleSet{T}(@nospecialize(data)) where {T} if isa(data, Tuple{Vararg{T}}) new{T, Tuple{Vararg{T}}}(data) else new{T, Tuple{Vararg{T}}}(Tuple(data)) end end + function OpaqueLittleSet(@nospecialize(data)) + T = eltype(data) + new{T, Tuple{Vararg{T}}}(data) + end function LittleSet{T}(data::AbstractVector) where {T} if eltype(data) == T return new{T, typeof(data)}(data) @@ -37,15 +45,25 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end end +# in cases where tuple parameters have been intentionally made opaque (such as +# `FrozenLittleSet{T, Tuple{Vararg{T}}}`), these methods allow accessing data without exposing the +# underlying `data` field and unintentionally specializing downstream code on the exact type +# representation of a tuple. +if isdefined(Base, Symbol("@assume_effects")) + Base.@assume_effects :nothrow function unsafe_getstate(x::FrozenLittleSet, state::Int) + getfield(getfield(x, :data), state) + end +else + unsafe_getstate(x::FrozenLittleSet, state::Int) = @inbounds(getfield(getfield(x, :data), state)) +end +unsafe_getstate(x::UnfrozenLittleSet, state::Int) = @inbounds(getfield(x, :data)[state]) + # `data` should not be directly accessed Base.propertynames(@nospecialize(x::LittleSet)) = () function Base.propertynames(@nospecialize(x::LittleSet), ::Symbol) throw(ArgumentError("LittleSet does not support public access to it's fields.")) end -const FrozenLittleSet{T} = LittleSet{T, <:Tuple} -const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} - Base.Tuple(s::FrozenLittleSet) = getfield(s, :data) Base.Tuple(s::UnfrozenLittleDict) = Tuple(getfield(s, :data)) @@ -53,52 +71,56 @@ freeze(s::AbstractSet{T}) where {T} = LittleSet{T}(Tuple(s)) hash(s::LittleSet, h::UInt) = hash(getfield(s, :data), hash(orderedset_seed, h)) -Base.length(s::LittleSet) = length(getfield(s, :data)) +Base.length(s::UnfrozenLittleSet) = length(getfield(s, :data)) +Base.length(@nospecialize(s::FrozenLittleSet)) = nfields(getfield(s, :data)) +#region empty Base.isempty(s::UnfrozenLittleSet) = isempty(getfield(s, :data)) +Base.isempty(@nospecialize(s::FrozenLittleSet)) = nfields(getfield(s, :data)) === 0 function Base.empty(@nospecialize(s::FrozenLittleSet)) - T = eltype(s) - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - return LittleSet{T, Tuple{Vararg{T}}}(()) + if isa(s, OpaqueLittleSet) + return OpaqueLittleSet{eltype(s)}(()) else - return LittleSet{T, Tuple{}}(()) + return LittleSet{eltype(s), Tuple{}}(()) end end Base.empty(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(empty(getfield(s, :data))) -Base.iterate(s::LittleSet, state...) = iterate(getfield(s, :data), state...) - -Base.in(x, s::UnfrozenLittleSet) = in(x, getfield(s, :data)) -function Base.in(x, s::FrozenLittleSet) - data = getfield(s, :data) - n = nfields(data) - while n > 0 - isequal(x, getfield(data, n)) && return true - n -= 1 - end - return false +function Base.emptymutable(s::UnfrozenLittleSet{T}, ::Type{U}=T) where {T, U} + LittleSet(Base.emptymutable(getfield(s, :data), U)) end +function Base.emptymutable(@nospecialize(s::FrozenLittleSet), ::Type{U}=eltype(s)) where {U} + LittleSet(U[]) +end +#endregion empty +#region copy # since `Base.copy` is a shallow copy on collections, an immutable collection like `Tuple` the same Base.copy(@nospecialize(s::FrozenLittleSet)) = s Base.copy(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(copy(getfield(s, :data))) +Base.copymutable(@nospecialize(s::FrozenLittleSet)) = LittleSet(eltype(s)[]) +function Base.copymutable(s::UnfrozenLittleSet{T}) where {T} + LittleSet{T}(Base.copymutable(getfield(s, :data))) +end +#endregion copy + function Base.sizehint!(s::UnfrozenLittleSet, sz) sizehint!(getfield(s, :data), sz) return s end +Base.iterate(s::LittleSet, state...) = iterate(getfield(s, :data), state...) -function Base.emptymutable(s::UnfrozenLittleSet{T}, ::Type{U}=T) where {T, U} - LittleSet(Base.emptymutable(getfield(s, :data), U)) -end -function Base.emptymutable(@nospecialize(s::FrozenLittleSet), ::Type{U}=eltype(s)) where {U} - LittleSet(U[]) -end - -Base.copymutable(@nospecialize(s::FrozenLittleSet)) = LittleSet(eltype(s)[]) -function Base.copymutable(s::UnfrozenLittleSet{T}) where {T} - LittleSet{T}(Base.copymutable(getfield(s, :data))) +Base.in(x, s::UnfrozenLittleSet) = in(x, getfield(s, :data)) +function Base.in(x, s::FrozenLittleSet) + data = getfield(s, :data) + n = nfields(data) + while n > 0 + isequal(x, getfield(data, n)) && return true + n -= 1 + end + return false end function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} @@ -113,25 +135,113 @@ function Base.sort!(s::UnfrozenLittleSet; ks...) return s end -function Base.replace(f::Union{Function, Type}, s::LittleSet{T}; count::Integer=typemax(Int)) where {T} - newdata = replace(f, getfield(s, :data); count=count) - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - T2 = eltype(newdata) - return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) +check_count(::Nothing) = nothing +function check_count(count::Integer) + count < 0 && throw(DomainError(count, "`count` must not be negative (got $count)")) + return min(count, typemax(Int)) % Int +end + +function replace_state(f, s::FrozenLittleSet, state::Int, cnt::Int) + if cnt > 0 + old_item = unsafe_getstate(s, state) + new_item = f(old_item) + if old_item == new_item + return new_item, cnt + else + return new_item, cnt - 1 + end else - return LittleSet(newdata) + unsafe_getstate(s, state), 0 + end +end +function replace_state(f, s::FrozenLittleSet, state::Int, ::Nothing) + (f(unsafe_getstate(s, state)), nothing) +end + +struct ReplacePairs{N, F, S} <: Function + old_new::NTuple{N, Pair{F, S}} +end +_secondtype(::ReplacePairs{<:Any, <:Any, S}) where {S} = S + +@inline function (rp::ReplacePairs)(item) + for (old_item, new_item) in getfield(rp, :old_new) + isequal(old_item, item) && return new_item end + return item +end + +function Base.replace(s::FrozenLittleSet, old_new::Pair...; count::Union{Integer, Nothing}=nothing) + replace(ReplacePairs(old_new), s; count=count) end -function Base.replace(s::LittleSet{T}, old_new::Pair{F, S}...; count::Integer=typemax(Int)) where {T, F, S} - newdata = replace(getfield(s, :data), old_new...; count=count) +function Base.replace(f::Union{Function, Type}, s::FrozenLittleSet{T}; count::Union{Integer, Nothing}=nothing) where {T} + data = getfield(s, :data) + N = nfields(data) + if N === 0 + newdata = () + elseif N === 1 + i1, cnt = replace_state(f, s, 1, check_count(count)) + newdata = (i1,) + elseif N === 2 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + newdata = (i1, i2) + elseif N === 3 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + newdata = (i1, i2, i3) + elseif N === 4 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + i4, cnt = replace_state(f, s, 4, cnt) + newdata = (i1, i2, i3, i4) + elseif N === 5 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + i4, cnt = replace_state(f, s, 4, cnt) + i5, cnt = replace_state(f, s, 5, cnt) + newdata = (i1, i2, i3, i4, i5) + elseif N === 6 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + i4, cnt = replace_state(f, s, 4, cnt) + i5, cnt = replace_state(f, s, 5, cnt) + i6, cnt = replace_state(f, s, 6, cnt) + newdata = (i1, i2, i3, i4, i5, i6) + elseif N === 7 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + i4, cnt = replace_state(f, s, 4, cnt) + i5, cnt = replace_state(f, s, 5, cnt) + i6, cnt = replace_state(f, s, 6, cnt) + i7, cnt = replace_state(f, s, 7, cnt) + newdata = (i1, i2, i3, i4, i5, i6, i7) + elseif N === 8 + i1, cnt = replace_state(f, s, 1, check_count(count)) + i2, cnt = replace_state(f, s, 2, cnt) + i3, cnt = replace_state(f, s, 3, cnt) + i4, cnt = replace_state(f, s, 4, cnt) + i5, cnt = replace_state(f, s, 5, cnt) + i6, cnt = replace_state(f, s, 6, cnt) + i7, cnt = replace_state(f, s, 7, cnt) + i8, cnt = replace_state(f, s, 8, cnt) + newdata = (i1, i2, i3, i4, i5, i6, i7, i8) + else + newdata = Tuple(replace(f, collect(data); count=count)) + end if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - T2 = Union{T, S} - return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) + Tnew = isa(f, ReplacePairs) ? Union{T, _secondtype(f)} : eltype(newdata) + return LittleSet{Tnew, Tuple{Vararg{Tnew}}}(newdata) else return LittleSet(newdata) end end + Base.first(s::UnfrozenLittleSet, n::Integer) = LittleSet(first(getfield(s, :data), n)) function Base.first(s::FrozenLittleSet{T}, n::Integer) where {T} N = Int(n) @@ -233,4 +343,3 @@ function Base.delete!(s::UnfrozenLittleSet, key) end return s end - From 08576d4040c034ece688fb28869a1f7b4837be3c Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sat, 19 Aug 2023 22:48:37 -0400 Subject: [PATCH 05/15] Fix versioned use of `@assume_effects` on v1.6 --- src/little_set.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index 96ecf25..c93f20b 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -49,7 +49,7 @@ end # `FrozenLittleSet{T, Tuple{Vararg{T}}}`), these methods allow accessing data without exposing the # underlying `data` field and unintentionally specializing downstream code on the exact type # representation of a tuple. -if isdefined(Base, Symbol("@assume_effects")) +@static if isdefined(Base, Symbol("@assume_effects")) Base.@assume_effects :nothrow function unsafe_getstate(x::FrozenLittleSet, state::Int) getfield(getfield(x, :data), state) end @@ -298,7 +298,12 @@ function Base.union(x::FrozenLittleSet{T1}, y::FrozenLittleSet{T2}) where {T1, T end function Base.filter(f, s::LittleSet{T}) where {T} - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + if isa(s, FrozenLittleSet) + N = length(s) + if N > 32 + newdata = Tuple(filter(f, collect(getfield(s, :data)))) + else + end return LittleSet{T, Tuple{Vararg{T}}}(Tuple(filter(f, collect(getfield(s, :data))))) else return LittleSet(filter(f, getfield(s, :data))) @@ -343,3 +348,4 @@ function Base.delete!(s::UnfrozenLittleSet, key) end return s end + From 267cee1fd41b682c828a75390be4c1b13483907e Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sat, 28 Dec 2024 21:52:30 -0500 Subject: [PATCH 06/15] Greatly simplify code and start adding documentation --- src/dict_support.jl | 1 + src/little_dict.jl | 6 +- src/little_set.jl | 323 ++++++++++++++++++-------------------------- 3 files changed, 132 insertions(+), 198 deletions(-) diff --git a/src/dict_support.jl b/src/dict_support.jl index 0ee58a8..21907a2 100644 --- a/src/dict_support.jl +++ b/src/dict_support.jl @@ -7,3 +7,4 @@ hashindex(key, sz) = (reinterpret(Int,(hash(key))) & (sz-1)) + 1 const orderedset_seed = UInt === UInt64 ? 0x2114638a942a91a5 : 0xd86bdbf1 +struct NotFoundSentinel end # Struct to mark not not found diff --git a/src/little_dict.jl b/src/little_dict.jl index 8b678a6..23de747 100644 --- a/src/little_dict.jl +++ b/src/little_dict.jl @@ -5,7 +5,7 @@ const StoreType{T} = Union{Tuple{Vararg{T}}, AbstractVector{T}} end """ - LittleDict(keys, vals)<:AbstractDict + LittleDict(keys, vals) <: AbstractDict An ordered dictionary type for small numbers of keys. Rather than using `hash` or some other sophisticated measure @@ -57,8 +57,7 @@ end # Other iterators should be copied to a Vector LittleDict(ks, vs) = LittleDict(collect(ks), collect(vs)) - -function LittleDict{K,V}(itr) where {K,V} +function LittleDict{K, V}(itr) where {K,V} ks = K[] vs = V[] for val in itr @@ -133,7 +132,6 @@ function Base.map!(f, iter::Base.ValueIterator{<:LittleDict}) return iter end -struct NotFoundSentinel end # Struct to mark not not found function Base.get(dd::LittleDict, key, default) @assert length(dd.keys) == length(dd.vals) for ii in 1:length(dd.keys) diff --git a/src/little_set.jl b/src/little_set.jl index c93f20b..eebc30b 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -1,4 +1,11 @@ +""" + LittleSet([itr]) <: AbstractSet + +Constructs an ordered set optimized for a small number of elements, given the +iterable `itr`. The underlying data is stored as either an `AbstractVector` or +a `Tuple` and is optimal for 30-50 elements, similar to [`LittleDict`](@ref). +""" struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} data::D @@ -8,11 +15,8 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} LittleSet{T, D}(data) where {T,D} = new{T, D}(data) function OpaqueLittleSet{T}(@nospecialize(data)) where {T} - if isa(data, Tuple{Vararg{T}}) - new{T, Tuple{Vararg{T}}}(data) - else - new{T, Tuple{Vararg{T}}}(Tuple(data)) - end + new_data = isa(data, Tuple) ? data : Tuple(data) + new{T, Tuple{Vararg{T}}}(new_data) end function OpaqueLittleSet(@nospecialize(data)) T = eltype(data) @@ -43,68 +47,69 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end end end -end -# in cases where tuple parameters have been intentionally made opaque (such as -# `FrozenLittleSet{T, Tuple{Vararg{T}}}`), these methods allow accessing data without exposing the -# underlying `data` field and unintentionally specializing downstream code on the exact type -# representation of a tuple. -@static if isdefined(Base, Symbol("@assume_effects")) - Base.@assume_effects :nothrow function unsafe_getstate(x::FrozenLittleSet, state::Int) - getfield(getfield(x, :data), state) + function Base.empty(s::LittleSet{T, D}) where {T, D} + if isa(s, OpaqueLittleSet) + return new{T, Tuple{Vararg{T}}}(()) + elseif D <: Tuple + return new{T, Tuple{}}(()) + else + return new{T, D}(empty(getfield(s, :data))) + end + end + function Base.emptymutable(s::LittleSet{T, D}, ::Type{U}=T) where {T, D, U} + if D <: Tuple + new_data = U[] + else + new_data = Base.emptymutable(getfield(s, :data), U) + end + return new{U, typeof(new_data)}(new_data) end -else - unsafe_getstate(x::FrozenLittleSet, state::Int) = @inbounds(getfield(getfield(x, :data), state)) end -unsafe_getstate(x::UnfrozenLittleSet, state::Int) = @inbounds(getfield(x, :data)[state]) -# `data` should not be directly accessed -Base.propertynames(@nospecialize(x::LittleSet)) = () -function Base.propertynames(@nospecialize(x::LittleSet), ::Symbol) - throw(ArgumentError("LittleSet does not support public access to it's fields.")) +function Base.Tuple(s::LittleSet) + data = getfield(s, :data) + isa(data, Tuple) ? data : Tuple(data) end -Base.Tuple(s::FrozenLittleSet) = getfield(s, :data) -Base.Tuple(s::UnfrozenLittleDict) = Tuple(getfield(s, :data)) - freeze(s::AbstractSet{T}) where {T} = LittleSet{T}(Tuple(s)) hash(s::LittleSet, h::UInt) = hash(getfield(s, :data), hash(orderedset_seed, h)) -Base.length(s::UnfrozenLittleSet) = length(getfield(s, :data)) -Base.length(@nospecialize(s::FrozenLittleSet)) = nfields(getfield(s, :data)) +function Base.length(s::LittleSet) + data = getfield(s, :data) + isa(data, Tuple) ? nfields(data) : length(data) +end -#region empty -Base.isempty(s::UnfrozenLittleSet) = isempty(getfield(s, :data)) -Base.isempty(@nospecialize(s::FrozenLittleSet)) = nfields(getfield(s, :data)) === 0 +function Base.isempty(s::LittleSet) + data = getfield(s, :data) + isa(data, Tuple) ? nfields(data) == 0 : isempty(data) +end -function Base.empty(@nospecialize(s::FrozenLittleSet)) - if isa(s, OpaqueLittleSet) - return OpaqueLittleSet{eltype(s)}(()) +function Base.copy(s::LittleSet{T, D}) where {T, D} + # since `Base.copy` is a shallow copy on collections, an immutable collection + # like `Tuple` is treated the same + if D <: Tuple + return s else - return LittleSet{eltype(s), Tuple{}}(()) + return LittleSet{T, D}(copy(getfield(s, :data))) end end -Base.empty(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(empty(getfield(s, :data))) -function Base.emptymutable(s::UnfrozenLittleSet{T}, ::Type{U}=T) where {T, U} - LittleSet(Base.emptymutable(getfield(s, :data), U)) -end -function Base.emptymutable(@nospecialize(s::FrozenLittleSet), ::Type{U}=eltype(s)) where {U} - LittleSet(U[]) -end -#endregion empty - -#region copy -# since `Base.copy` is a shallow copy on collections, an immutable collection like `Tuple` the same -Base.copy(@nospecialize(s::FrozenLittleSet)) = s -Base.copy(s::UnfrozenLittleSet{T}) where {T} = LittleSet{T}(copy(getfield(s, :data))) - -Base.copymutable(@nospecialize(s::FrozenLittleSet)) = LittleSet(eltype(s)[]) -function Base.copymutable(s::UnfrozenLittleSet{T}) where {T} - LittleSet{T}(Base.copymutable(getfield(s, :data))) +function Base.copymutable(s::LittleSet{T}) where {T} + data = getfield(s, :data) + if isa(data, Tuple) + i = nfields(data) + new_data = Vector{T}(undef, n) + while i != 0 + @inbounds new_data[i] = getfield(data, i) + i -= 1 + end + else + new_data = Base.copymutable(data) + end + LittleSet{T}(new_data) end -#endregion copy function Base.sizehint!(s::UnfrozenLittleSet, sz) sizehint!(getfield(s, :data), sz) @@ -112,136 +117,33 @@ function Base.sizehint!(s::UnfrozenLittleSet, sz) end Base.iterate(s::LittleSet, state...) = iterate(getfield(s, :data), state...) -Base.in(x, s::UnfrozenLittleSet) = in(x, getfield(s, :data)) -function Base.in(x, s::FrozenLittleSet) +function Base.in(x, s::LittleSet) data = getfield(s, :data) - n = nfields(data) - while n > 0 - isequal(x, getfield(data, n)) && return true - n -= 1 + if isa(data, Tuple) + n = nfields(data) + while n > 0 + isequal(x, getfield(data, n)) && return true + n -= 1 + end + return false + else + return in(x, data) end - return false end -function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} - LittleSet{T}(sort(getfield(s, :data); ks...)) -end # HACK: this is a temporary hack to get around the lack of `sort` available for tuples function Base.sort(s::FrozenLittleSet{T}; ks...) where {T} LittleSet{T}(sort(T[getfield(s, :data)...]; ks...)) end +function Base.sort(s::UnfrozenLittleSet{T}; ks...) where {T} + LittleSet{T}(sort(getfield(s, :data); ks...)) +end + function Base.sort!(s::UnfrozenLittleSet; ks...) sort!(getfield(s, :data); ks...) return s end -check_count(::Nothing) = nothing -function check_count(count::Integer) - count < 0 && throw(DomainError(count, "`count` must not be negative (got $count)")) - return min(count, typemax(Int)) % Int -end - -function replace_state(f, s::FrozenLittleSet, state::Int, cnt::Int) - if cnt > 0 - old_item = unsafe_getstate(s, state) - new_item = f(old_item) - if old_item == new_item - return new_item, cnt - else - return new_item, cnt - 1 - end - else - unsafe_getstate(s, state), 0 - end -end -function replace_state(f, s::FrozenLittleSet, state::Int, ::Nothing) - (f(unsafe_getstate(s, state)), nothing) -end - -struct ReplacePairs{N, F, S} <: Function - old_new::NTuple{N, Pair{F, S}} -end -_secondtype(::ReplacePairs{<:Any, <:Any, S}) where {S} = S - -@inline function (rp::ReplacePairs)(item) - for (old_item, new_item) in getfield(rp, :old_new) - isequal(old_item, item) && return new_item - end - return item -end - -function Base.replace(s::FrozenLittleSet, old_new::Pair...; count::Union{Integer, Nothing}=nothing) - replace(ReplacePairs(old_new), s; count=count) -end -function Base.replace(f::Union{Function, Type}, s::FrozenLittleSet{T}; count::Union{Integer, Nothing}=nothing) where {T} - data = getfield(s, :data) - N = nfields(data) - if N === 0 - newdata = () - elseif N === 1 - i1, cnt = replace_state(f, s, 1, check_count(count)) - newdata = (i1,) - elseif N === 2 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - newdata = (i1, i2) - elseif N === 3 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - newdata = (i1, i2, i3) - elseif N === 4 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - i4, cnt = replace_state(f, s, 4, cnt) - newdata = (i1, i2, i3, i4) - elseif N === 5 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - i4, cnt = replace_state(f, s, 4, cnt) - i5, cnt = replace_state(f, s, 5, cnt) - newdata = (i1, i2, i3, i4, i5) - elseif N === 6 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - i4, cnt = replace_state(f, s, 4, cnt) - i5, cnt = replace_state(f, s, 5, cnt) - i6, cnt = replace_state(f, s, 6, cnt) - newdata = (i1, i2, i3, i4, i5, i6) - elseif N === 7 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - i4, cnt = replace_state(f, s, 4, cnt) - i5, cnt = replace_state(f, s, 5, cnt) - i6, cnt = replace_state(f, s, 6, cnt) - i7, cnt = replace_state(f, s, 7, cnt) - newdata = (i1, i2, i3, i4, i5, i6, i7) - elseif N === 8 - i1, cnt = replace_state(f, s, 1, check_count(count)) - i2, cnt = replace_state(f, s, 2, cnt) - i3, cnt = replace_state(f, s, 3, cnt) - i4, cnt = replace_state(f, s, 4, cnt) - i5, cnt = replace_state(f, s, 5, cnt) - i6, cnt = replace_state(f, s, 6, cnt) - i7, cnt = replace_state(f, s, 7, cnt) - i8, cnt = replace_state(f, s, 8, cnt) - newdata = (i1, i2, i3, i4, i5, i6, i7, i8) - else - newdata = Tuple(replace(f, collect(data); count=count)) - end - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - Tnew = isa(f, ReplacePairs) ? Union{T, _secondtype(f)} : eltype(newdata) - return LittleSet{Tnew, Tuple{Vararg{Tnew}}}(newdata) - else - return LittleSet(newdata) - end -end - - Base.first(s::UnfrozenLittleSet, n::Integer) = LittleSet(first(getfield(s, :data), n)) function Base.first(s::FrozenLittleSet{T}, n::Integer) where {T} N = Int(n) @@ -297,20 +199,6 @@ function Base.union(x::FrozenLittleSet{T1}, y::FrozenLittleSet{T2}) where {T1, T end end -function Base.filter(f, s::LittleSet{T}) where {T} - if isa(s, FrozenLittleSet) - N = length(s) - if N > 32 - newdata = Tuple(filter(f, collect(getfield(s, :data)))) - else - end - return LittleSet{T, Tuple{Vararg{T}}}(Tuple(filter(f, collect(getfield(s, :data))))) - else - return LittleSet(filter(f, getfield(s, :data))) - end -end - -Base.filter!(f, s::UnfrozenLittleSet) = (filter!(f, getfield(s, :data)); return s) function Base.push!(s::UnfrozenLittleSet, val) data = getfield(s, :data) if !in(val, data) @@ -322,30 +210,77 @@ end Base.pop!(s::UnfrozenLittleSet) = pop!(getfield(s, :data)) function Base.pop!(s::UnfrozenLittleSet, key) data = getfield(s, :data) - index = findfirst(isequal(key), data) - index === nothing && throw(KeyError(key)) - deleteat!(data, index) - key + for i in eachindex(data) + k = @inbounds(data[i]) + if (key === k || isequal(key, k)) + deleteat!(data, i) + return k + end + end + throw(KeyError(key)) end function Base.pop!(s::UnfrozenLittleSet, key, default) data = getfield(s, :data) - index = findfirst(isequal(key), data) - if index === nothing - return default - else - deleteat!(data, index) - return key + for i in eachindex(data) + k = @inbounds(data[i]) + if (key === k || isequal(key, k)) + deleteat!(data, i) + return k + end end + return default end Base.empty!(s::UnfrozenLittleSet) = (empty!(getfield(s, :data)); s) function Base.delete!(s::UnfrozenLittleSet, key) data = getfield(s, :data) - index = findfirst(isequal(key), data) - if index !== nothing - deleteat!(getfield(s, :data), index) + for i in eachindex(data) + k = @inbounds(data[i]) + if (key === k || isequal(key, k)) + deleteat!(data, i) + break + end end return s end +function Base.replace( + f::Union{Function, Type}, + s::LittleSet{T}; + count::Integer=typemax(Int) +) where {T} + newdata = replace(f, getfield(s, :data); count=count) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + T2 = eltype(newdata) + return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) + else + return LittleSet(newdata) + end +end +function Base.replace( + s::LittleSet{T}, + old_new::Pair{F, S}...; + count::Integer=typemax(Int) +) where {T, F, S} + newdata = replace(getfield(s, :data), old_new...; count=count) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + T2 = Union{T, S} + return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) + else + return LittleSet(newdata) + end +end + +function Base.filter(f, s::LittleSet{T}) where {T} + newdata = filter(f, getfield(s, :data)) + if isa(s, OpaqueLittleSet) + return OpaqueLittleSet{T}(newdata) + else + return LittleSet(newdata) + end +end +function Base.filter!(f, s::UnfrozenLittleSet) + filter!(f, getfield(s, :data)) + return s +end From d92f2660eea691b537372a7f21f926eee969a1ab Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 11:57:45 -0500 Subject: [PATCH 07/15] Fix replace for LittleSet Julia v1.7+ supports `replace` for tuples so had to manually implement this here. Also removed inference testing for the current approach since it uses a temporary `Vector` to store results. This is probably a sold approach for any frozen set of tuples over 32, but it would be good to optimize for smaller cases in the future. --- src/little_set.jl | 129 ++++++++++++++++++++++++++++++++-------- test/test_little_set.jl | 2 +- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index eebc30b..d3bd285 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -18,7 +18,7 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} new_data = isa(data, Tuple) ? data : Tuple(data) new{T, Tuple{Vararg{T}}}(new_data) end - function OpaqueLittleSet(@nospecialize(data)) + function OpaquetleSet(@nospecialize(data)) T = eltype(data) new{T, Tuple{Vararg{T}}}(data) end @@ -245,42 +245,121 @@ function Base.delete!(s::UnfrozenLittleSet, key) return s end -function Base.replace( - f::Union{Function, Type}, - s::LittleSet{T}; - count::Integer=typemax(Int) -) where {T} - newdata = replace(f, getfield(s, :data); count=count) - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - T2 = eltype(newdata) - return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) +function Base.filter(f, s::LittleSet{T}) where {T} + newdata = filter(f, getfield(s, :data)) + if isa(s, OpaqueLittleSet) + return OpaqueLittleSet{T}(newdata) else return LittleSet(newdata) end end +function Base.filter!(f, s::UnfrozenLittleSet) + filter!(f, getfield(s, :data)) + return s +end + +mutable struct Replace{F} + const f::F + count::Int +end + +# these are copied from Julia's "base/set.jl" because tuple replace isn't +# supported before Julia v1.7 +function check_count(count::Integer) + count < 0 && throw(DomainError(count, "`count` must not be negative (got $count)")) + return min(count, typemax(Int)) % Int +end + + +function (f::Replace{F})(old_item) where {F} + c = getfield(f, :count) + if c > 0 + return old_item + else + if F <: Tuple + for p in getfield(f, :f) + p1, p2 = p + if old_item == p1 + setifield!(f, c - 1) + return p2 + end + end + return old_item + else + new_item = getfield(f, :f)(old_item) + if new_item != old_item + setifield!(f, c - 1) + end + end + return new_item + end +end + function Base.replace( s::LittleSet{T}, old_new::Pair{F, S}...; count::Integer=typemax(Int) ) where {T, F, S} - newdata = replace(getfield(s, :data), old_new...; count=count) - if isa(s, LittleSet{T, Tuple{Vararg{T}}}) - T2 = Union{T, S} - return LittleSet{T2, Tuple{Vararg{T2}}}(newdata) - else - return LittleSet(newdata) + replace(s; count=count) do x + @inline + for o_n in old_new + isequal(first(o_n), x) && return last(o_n) + end + return x end end -function Base.filter(f, s::LittleSet{T}) where {T} - newdata = filter(f, getfield(s, :data)) - if isa(s, OpaqueLittleSet) - return OpaqueLittleSet{T}(newdata) +# function Base.replace( +# s::LittleSet{T}, +# old_new::Pair{F, S}...; +# count::Integer=typemax(Int) +# ) where {T, F, S} +# old_data = getfield(s, :data) +# if isa(old_data, Tuple) +# new_data = map(Replace(old_new, count), old_data) +# T2 = eltype(new_data) +# if isa(s, LittleSet{T, Tuple{Vararg{T}}}) +# return LittleSet{T2, Tuple{Vararg{T2}}}(new_data) +# else +# return LittleSet{T2, typeof(new_data)}(new_data) +# end +# else +# new_data = replace(old_data, old_new...; count=count) +# return LittleSet(new_data) +# end +# end + +function Base.replace( + f::Union{Function, Type}, + s::LittleSet{T, D}; + count::Integer=typemax(Int) +) where {T, D} + old_data = getfield(s, :data) + if isa(old_data, Tuple) + c = check_count(count) + n = nfields(old_data) + v = Vector{Any}(undef, n) + for i in Base.OneTo(n) + old_item = @inbounds(getfield(old_data, i)) + if c > 0 + new_item = f(old_item) + if new_item == old_item + c -= 1 + end + else + new_item = old_item + end + @inbounds(setindex!(v, new_item, i)) + end + new_data = (v...,) + T2 = eltype(new_data) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) + return LittleSet{T2, Tuple{Vararg{T2}}}(new_data) + else + return LittleSet{T2, typeof(new_data)}(new_data) + end else - return LittleSet(newdata) + new_data = replace(f, old_data, count=count) + return LittleSet(new_data) end end -function Base.filter!(f, s::UnfrozenLittleSet) - filter!(f, getfield(s, :data)) - return s -end diff --git a/test/test_little_set.jl b/test/test_little_set.jl index 5e6c997..b3abe59 100644 --- a/test/test_little_set.jl +++ b/test/test_little_set.jl @@ -251,7 +251,7 @@ using OrderedCollections, Test s = LittleSet([1,2,3,4]) @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet([0, 5, 3, 4])) s = LittleSet{Int, Tuple{Vararg{Int}}}((1,2,3,4)) - @test isequal(@inferred(replace(s, 1 => 0, 2 => 5)), LittleSet((0, 5, 3, 4))) + @test isequal(replace(s, 1 => 0, 2 => 5), LittleSet((0, 5, 3, 4))) end @testset "empty set" begin From 66f0308b5a636ab723501ae8a2ad8514fcfabb2d Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 12:15:52 -0500 Subject: [PATCH 08/15] remove old scratch code --- src/little_set.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index d3bd285..2c7431b 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -258,11 +258,6 @@ function Base.filter!(f, s::UnfrozenLittleSet) return s end -mutable struct Replace{F} - const f::F - count::Int -end - # these are copied from Julia's "base/set.jl" because tuple replace isn't # supported before Julia v1.7 function check_count(count::Integer) From 7b3c495fcd1cd61ec7d78c446094efed9892ead1 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 12:29:57 -0500 Subject: [PATCH 09/15] zz --- src/little_set.jl | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index 2c7431b..4486ee8 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -266,30 +266,6 @@ function check_count(count::Integer) end -function (f::Replace{F})(old_item) where {F} - c = getfield(f, :count) - if c > 0 - return old_item - else - if F <: Tuple - for p in getfield(f, :f) - p1, p2 = p - if old_item == p1 - setifield!(f, c - 1) - return p2 - end - end - return old_item - else - new_item = getfield(f, :f)(old_item) - if new_item != old_item - setifield!(f, c - 1) - end - end - return new_item - end -end - function Base.replace( s::LittleSet{T}, old_new::Pair{F, S}...; From 592abf1b117cafcdc16f5f20d979340cd00df052 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 12:38:45 -0500 Subject: [PATCH 10/15] remove `@inline` that 1.6 doesn't support --- src/little_set.jl | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index 4486ee8..ce5c304 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -265,41 +265,18 @@ function check_count(count::Integer) return min(count, typemax(Int)) % Int end - function Base.replace( s::LittleSet{T}, old_new::Pair{F, S}...; count::Integer=typemax(Int) ) where {T, F, S} replace(s; count=count) do x - @inline for o_n in old_new isequal(first(o_n), x) && return last(o_n) end return x end end - -# function Base.replace( -# s::LittleSet{T}, -# old_new::Pair{F, S}...; -# count::Integer=typemax(Int) -# ) where {T, F, S} -# old_data = getfield(s, :data) -# if isa(old_data, Tuple) -# new_data = map(Replace(old_new, count), old_data) -# T2 = eltype(new_data) -# if isa(s, LittleSet{T, Tuple{Vararg{T}}}) -# return LittleSet{T2, Tuple{Vararg{T2}}}(new_data) -# else -# return LittleSet{T2, typeof(new_data)}(new_data) -# end -# else -# new_data = replace(old_data, old_new...; count=count) -# return LittleSet(new_data) -# end -# end - function Base.replace( f::Union{Function, Type}, s::LittleSet{T, D}; From f997d623c626ad7281257e5b17470dba923b46cb Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 13:02:53 -0500 Subject: [PATCH 11/15] Add LittleSet to docs --- docs/src/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/index.md b/docs/src/index.md index 43da3ef..78b8cd5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -77,6 +77,7 @@ Indeed the preceeding example (with the io redirected to `devnull`), runs 4x fas OrderedDict OrderedSet LittleDict +LittleSet freeze OrderedCollections.isordered ``` From ccf76376935d1a6f28cfa379354bd5be4e79e1d7 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 13:12:55 -0500 Subject: [PATCH 12/15] Move constant defs to appease nightly parsing error --- src/little_set.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index ce5c304..4fc8251 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -9,10 +9,6 @@ a `Tuple` and is optimal for 30-50 elements, similar to [`LittleDict`](@ref). struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} data::D - global const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} - global const FrozenLittleSet{T} = LittleSet{T, <:Tuple} - global const OpaqueLittleSet{T} = LittleSet{T, Tuple{Vararg{T}}} - LittleSet{T, D}(data) where {T,D} = new{T, D}(data) function OpaqueLittleSet{T}(@nospecialize(data)) where {T} new_data = isa(data, Tuple) ? data : Tuple(data) @@ -67,6 +63,10 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end end +const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} +const FrozenLittleSet{T} = LittleSet{T, <:Tuple} +const OpaqueLittleSet{T} = LittleSet{T, Tuple{Vararg{T}}} + function Base.Tuple(s::LittleSet) data = getfield(s, :data) isa(data, Tuple) ? data : Tuple(data) From ee2d90db231261b82b4470a28fe0943cccfc0df6 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 13:22:30 -0500 Subject: [PATCH 13/15] Fix LittleSet constructors --- src/little_set.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/little_set.jl b/src/little_set.jl index 4fc8251..9053596 100644 --- a/src/little_set.jl +++ b/src/little_set.jl @@ -10,13 +10,13 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} data::D LittleSet{T, D}(data) where {T,D} = new{T, D}(data) - function OpaqueLittleSet{T}(@nospecialize(data)) where {T} + function LittleSet{T, Tuple{Vararg{T}}}(@nospecialize(data)) where {T} new_data = isa(data, Tuple) ? data : Tuple(data) new{T, Tuple{Vararg{T}}}(new_data) end - function OpaquetleSet(@nospecialize(data)) - T = eltype(data) - new{T, Tuple{Vararg{T}}}(data) + function (LittleSet{T, Tuple{Vararg{T}}} where {T})(@nospecialize(data)) + T2 = eltype(data) + new{T2, Tuple{Vararg{T2}}}(data) end function LittleSet{T}(data::AbstractVector) where {T} if eltype(data) == T @@ -45,7 +45,7 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end function Base.empty(s::LittleSet{T, D}) where {T, D} - if isa(s, OpaqueLittleSet) + if isa(s, LittleSet{T, Tuple{Vararg{T}}}) return new{T, Tuple{Vararg{T}}}(()) elseif D <: Tuple return new{T, Tuple{}}(()) @@ -63,9 +63,9 @@ struct LittleSet{T, D<:StoreType{T}} <: AbstractSet{T} end end +const OpaqueLittleSet{T} = LittleSet{T, Tuple{Vararg{T}}} const UnfrozenLittleSet{T} = LittleSet{T, <:AbstractVector{T}} const FrozenLittleSet{T} = LittleSet{T, <:Tuple} -const OpaqueLittleSet{T} = LittleSet{T, Tuple{Vararg{T}}} function Base.Tuple(s::LittleSet) data = getfield(s, :data) From 289ecb27dc8577fb64b4d6552c798bd413ddb9bf Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Mon, 20 Jan 2025 13:23:17 -0500 Subject: [PATCH 14/15] Version bump --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f546f78..f2c8bdb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "OrderedCollections" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.7.0" +version = "1.8.0" [compat] julia = "1.6" From b86ae0881ff71cd7fe479e6e66678df92c92ba5b Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sun, 26 Jan 2025 21:55:26 -0500 Subject: [PATCH 15/15] Version bump --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f2c8bdb..0c4071d 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" version = "1.8.0" [compat] -julia = "1.6" +julia = "1.7.1" [extras] Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"