From 961482099e7e807d27050409f714583fc4a78d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20S=C3=A1nchez=20Ram=C3=ADrez?= Date: Mon, 26 May 2025 17:37:35 +0200 Subject: [PATCH] Merge BijectiveDicts into Bijections --- src/Bijections.jl | 183 ++++++++++++++++++++++++++-------------- src/inversion.jl | 27 ------ test/runtests.jl | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 90 deletions(-) delete mode 100644 src/inversion.jl diff --git a/src/Bijections.jl b/src/Bijections.jl index 1374e2e..b82c9ea 100644 --- a/src/Bijections.jl +++ b/src/Bijections.jl @@ -5,42 +5,31 @@ import Base: ==, collect, delete!, - display, getindex, + get, inv, isempty, iterate, length, - setindex!, - show + setindex! export Bijection, active_inv, - collect, - display, domain, - getindex, image, inverse, - isempty, - length, - setindex!, - show - -struct Bijection{S,T} <: AbstractDict{S,T} - f::Dict{S,T} # map from domain to range - finv::Dict{T,S} # inverse map from range to domain - - # standard constructor - function Bijection{S,T}() where {S,T} - F = Dict{S,T}() - G = Dict{T,S}() - new(F, G) + hasvalue + +struct Bijection{K,V,F,Finv} <: AbstractDict{K,V} + f::F # map from domain to range + finv::Finv # inverse map from range to domain + + function Bijection(f::F, finv::Finv) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}} + new{K,V,F,Finv}(f, finv) end - # private, unsafe constructor - function Bijection{S,T}(F::Dict{S,T}, G::Dict{T,S}) where {S,T} - new(F, G) + function Bijection{K,V,F,Finv}(f::F, finv::Finv) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}} + new{K,V,F,Finv}(f, finv) end end @@ -67,50 +56,59 @@ function Bijection(x::S, y::T) where {S,T} return b end +# F, Finv default to `Dict{K,V}` and `Dict{V,K}` respectively +function Bijection{K,V}(args...; kwargs...) where {K,V} + Bijection{K,V,Dict{K,V},Dict{V,K}}(args...; kwargs...) +end + # Convert an `AbstractDict` to a `Bijection` -function Bijection(dict::AbstractDict{S,T}) where {S,T} - vals = values(dict) - if length(dict) != length(unique(vals)) - error("Repeated value found in dict") - end +Bijection(f::F) where {F<:AbstractDict} = Bijection(f, dict_inverse(f)) - b = Bijection{S,T}() - for (k, v) in pairs(dict) - b[k] = v - end - return b +function Bijection{K,V,F,Finv}(f::F) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}} + Bijection{K,V,F,Finv}(f, Finv(Iterators.map(reverse, f))) end # Copy constructor -Bijection(b::Bijection) = Bijection(collect(b)) +Bijection(b::Bijection) = Bijection(copy(b.f), copy(b.finv)) -## Convert a list of pairs to a Bijection -function Bijection(pair_list::Vector{Pair{S,T}}) where {S,T} - p = unique(pair_list) # remove duplicate pairs - n = length(p) +# if not defined, it will just return a copy of `b.f` +Base.copy(b::Bijection) = Bijection(copy(b.f), copy(b.finv)) - xs = first.(p) - ys = last.(p) - if length(xs) != length(unique(xs)) || length(ys) != length(unique(ys)) - error("Repeated key or value found in pair list") - end +# Convert a list of pairs to a Bijection (already contains empty constructor) +function Bijection{K,V,F,Finv}(pairs::Pair...) where {K,V,F,Finv} + Bijection{K,V,F,Finv}(F(pairs...), Finv(Iterators.map(reverse, pairs))) +end - b = Bijection{S,T}() - for xy in p - x, y = xy - b[x] = y - end - return b +function Bijection{K,V,F,Finv}(pairs::Vector) where {K,V,F,Finv} + Bijection{K,V,F,Finv}(F(pairs...), Finv(Iterators.map(reverse, pairs))) end -# Decent way to print out a bijection -function show(io::IO, b::Bijection{S,T}) where {S,T} - print(io, "Bijection{$S,$T} (with $(length(b)) pairs)") +# shortcuts for creating a Bijection from a list of pairs +Bijection(pairs::Pair...) = Bijection(Dict(pairs...), Dict(Iterators.map(reverse, pairs))) +Bijection(pairs::Vector) = Bijection(Dict(pairs...), Dict(Iterators.map(reverse, pairs))) + +# create a Bijection of same type but no entries +function Base.empty(b::Bijection, ::Type{K}, ::Type{V}) where {K,V} + Bijection(empty(b.f, K, V), empty(b.finv, V, K)) end # equality checking ==(a::Bijection, b::Bijection) = a.f == b.f +""" + Base.haskey(b::Bijection, x) + +Checks if `x` is in the domain of the Bijection `b`.` +""" +Base.haskey(b::Bijection, x) = haskey(b.f, x) + +""" + hasvalue(b::Bijection, y) + +Checks if `y` is in the image of the Bijection `b`. It is equivalent to checking if the inverse mapping `b.finv` has `y` as a key, so it should as fast as `haskey`. +""" +hasvalue(b::Bijection, y) = haskey(b.finv, y) + # Add a relation to a bijection: b[x] = y """ setindex!(b::Bijection, y, x) @@ -118,13 +116,19 @@ end For a `Bijection` `b` use the syntax `b[x]=y` to add `(x,y)` to `b`. """ function setindex!(b::Bijection, y, x) - if in(x, domain(b)) || in(y, image(b)) - error("One of x or y already in this Bijection") - else - b.f[x] = y - b.finv[y] = x + haskey(b.finv, y) && throw(ArgumentError("inserting $x => $y would break bijectiveness")) + + # if update of existing key, then remove old value from finv + # TODO test this!! + if haskey(b.f, x) + old_y = b.f[x] + delete!(b.finv, old_y) end - b + + b.f[x] = y + b.finv[y] = x + + return b end # retreive b[x] where x is in domain @@ -152,6 +156,50 @@ end # the notation b(y) is a shortcut for inverse(b,y) (b::Bijection)(y) = inverse(b, y) +# WARN this uses internals so it's dangerous! do not make it public +# this is just used for the default case, but in general the method should be extended for new `AbstractDict` types +""" + inverse_dict_type(D::Type{<:AbstractDict}) + +Returns the type of the inverse dictionary for a given `AbstractDict` type `D`. +This is used internally to create the inverse mapping in a `Bijection`. +""" +function inverse_dict_type(D::Type{<:AbstractDict{K,V}}) where {K,V} + @warn "Using the default `inverse_dict_type` for $D. This may not be optimal for your specific dictionary type." + D.name.wrapper{V,K} +end + +inverse_dict_type(::Type{Dict{K,V}}) where {K,V} = Dict{V,K} +inverse_dict_type(::Type{IdDict{K,V}}) where {K,V} = IdDict{V,K} +inverse_dict_type(::Type{Base.ImmutableDict{K,V}}) where {K,V} = Base.ImmutableDict{V,K} +inverse_dict_type(::Type{Base.PersistentDict{K,V}}) where {K,V} = Base.PersistentDict{V,K} + +function dict_inverse(d::D) where {D<:AbstractDict} + allunique(values(d)) || throw(ArgumentError("dict is not bijective")) + inverse_dict_type(D)(reverse.(collect(d))) +end + +""" + inv(b::Bijection) + +Creates a new `Bijection` that is the inverse of `b`. +Subsequence changes to `b` will not affect `inv(b)`. + +See also [`active_inv`](@ref). +""" +inv(b::Bijection) = copy(active_inv(b)) + +""" + active_inv(b::Bijection) + +Creates a `Bijection` that is the inverse of `b`. +The original `b` and the new `Bijection` returned are tied together so that changes to one immediately affect the other. +In this way, the two `Bijection`s remain inverses in perpetuity. + +See also [`inv`](@ref). +""" +active_inv(b::Bijection) = Bijection(b.finv, b.f) + # Remove a pair (x,y) from a bijection """ delete!(b::Bijection, x) @@ -159,6 +207,9 @@ end Deletes the ordered pair `(x,b[x])` from `b`. """ function delete!(b::Bijection, x) + # replicate `Dict` behavior: if x is not in the domain, do nothing + haskey(b, x) || return b + y = b[x] delete!(b.f, x) delete!(b.finv, y) @@ -197,6 +248,8 @@ Returns an iterator for the keys for `b`. """ domain(b::Bijection) = keys(b) +Base.keys(b::Bijection) = keys(b.f) + # return the image as an array of values """ image(b::Bijection) @@ -205,11 +258,10 @@ Returns an iterator for the values of `b`. """ image(b::Bijection) = values(b) -iterate(b::Bijection{S,T}, s::Int) where {S,T} = iterate(b.f, s) -iterate(b::Bijection{S,T}) where {S,T} = iterate(b.f) +Base.values(b::Bijection) = values(b.f) -# convert a Bijection into a Dict; probably not useful -Base.Dict(b::Bijection) = copy(b.f) +iterate(b::Bijection, s) = iterate(b.f, s) +iterate(b::Bijection) = iterate(b.f) """ get(b::Bijection, key, default) @@ -220,7 +272,12 @@ function get(b::Bijection, key, default) get(b.f, key, default) end -include("inversion.jl") +function Base.sizehint!(b::Bijection, sz) + sizehint!(b.f, sz) + sizehint!(b.finv, sz) + b +end + include("composition.jl") end # end of module Bijections diff --git a/src/inversion.jl b/src/inversion.jl deleted file mode 100644 index 62baccf..0000000 --- a/src/inversion.jl +++ /dev/null @@ -1,27 +0,0 @@ -""" -`inv(b::Bijection)` creates a new `Bijection` that is the -inverse of `b`. -Subsequence changes to `b` will not affect `inv(b)`. - -See also `active_inv`. -""" -function inv(b::Bijection{S,T}) where {S,T} - bb = Bijection{T,S}() - for (x, y) in b - bb[y] = x - end - return bb -end - -""" -`active_inv(b::Bijection)` creates a `Bijection` that is the -inverse of `b`. The original `b` and the new `Bijection` returned -are tied together so that changes to one immediately affect the -other. In this way, the two `Bijection`s remain inverses in -perpetuity. - -See also `inv`. -""" -function active_inv(b::Bijection{S,T}) where {S,T} - return Bijection{T,S}(b.finv, b.f) -end diff --git a/test/runtests.jl b/test/runtests.jl index 0d5fb73..5677321 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,3 +57,213 @@ end c = a * b @test c["hi"] == 10 end + +# Test empty constructor +@testset "empty_constructor" begin + b = Bijection{Int,String}() + @test length(b) == 0 + + b = Bijection{Int,String,Dict{Int,String},Dict{String,Int}}() + @test length(b) == 0 + + b = Bijection{Int,String,IdDict{Int,String},IdDict{String,Int}}() + @test length(b) == 0 +end + +# Test constructor with arguments +@testset "constructor_with_args" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test length(b) == 2 + @test b[1] == "one" + @test b[2] == "two" + + b = Bijection{Int,String,Dict{Int,String},Dict{String,Int}}(1 => "one", 2 => "two") + @test length(b) == 2 + @test b[1] == "one" + @test b[2] == "two" + + b = Bijection{Int,String,IdDict{Int,String},IdDict{String,Int}}(1 => "one", 2 => "two") + @test length(b) == 2 + @test b[1] == "one" + @test b[2] == "two" +end + +# Test constructor from dictionary +@testset "constructor_from_dict" begin + d = Dict(1 => "one", 2 => "two") + b = Bijection(d) + @test length(b) == 2 + @test b[1] == "one" + @test b[2] == "two" + + d = IdDict(1 => "one", 2 => "two") + b = Bijection(d) + @test length(b) == 2 + @test b[1] == "one" + @test b[2] == "two" + + # test construction with different Finv dictionary type + d = Dict("one" => 1, "two" => 2) + b = Bijection{String,Int,Dict{String,Int},IdDict{Int,String}}(d) + @test length(b) == 2 + @test b["one"] == 1 + @test b["two"] == 2 +end + +# Test inv function +@testset "inv" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + inv_b = inv(b) + @test length(inv_b) == 2 + @test inv_b["one"] == 1 + @test inv_b["two"] == 2 + @test inv_b.f !== b.finv && inv_b.f == b.finv + @test inv_b.finv !== b.f && inv_b.finv == b.f +end + +# Test active_inv function +@testset "active_inv" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + active_b = active_inv(b) + @test length(active_b) == 2 + @test active_b["one"] == 1 + @test active_b["two"] == 2 + @test active_b.f === b.finv + @test active_b.finv === b.f +end + +# Test copy function +@testset "copy" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + b_copy = copy(b) + @test length(b_copy) == 2 + @test b_copy[1] == "one" + @test b_copy[2] == "two" +end + +# Test empty function +@testset "empty" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + empty_b = empty(b) + @test length(empty_b) == 0 + @test typeof(empty_b) == typeof(b) +end + +# Test getindex function +@testset "getindex" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test b[1] == "one" + @test b[2] == "two" +end + +# Test setindex! function +@testset "setindex!" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + b[3] = "three" + + @test length(b) == 3 + @test b[3] == "three" + @test b("three") == 3 + + # test update of existing key + b[1] = "uno" + @test b[1] == "uno" + @test !hasvalue(b, "one") +end + +# Test get function +@testset "get" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test get(b, 1, "default") == "one" + @test get(b, 2, "default") == "two" + @test get(b, 3, "default") == "default" +end + +# Test sizehint! function +@testset "sizehint!" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + nslots = length(b.f.slots) + sizehint!(b, nslots + 1) + @test length(b) == 2 + @test length(b.f.slots) > nslots +end + +# Test iterate function +@testset "iterate" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test issetequal(collect(b), [1 => "one", 2 => "two"]) +end + +# Test keys function +@testset "keys" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test issetequal(keys(b), [1, 2]) + @test issetequal(keys(inv(b)), ["one", "two"]) +end + +# Test values function +@testset "values" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test issetequal(values(b), ["one", "two"]) + @test issetequal(values(inv(b)), [1, 2]) +end + +# Test haskey function +@testset "haskey" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test haskey(b, 1) + @test haskey(b, 2) + @test !haskey(b, 3) + + # test haskey with mutable key type on `Dict` + b = Bijection{Vector{Int},String}([1] => "one", [2] => "two") + @test haskey(b, [1]) + @test haskey(b, [2]) + @test !haskey(b, [3]) + + # test haskey with mutable key type on `IdDict` + k1 = [1] + k2 = [2] + b = Bijection{Vector{Int},String,IdDict{Vector{Int},String},IdDict{String,Vector{Int}}}( + k1 => "one", k2 => "two" + ) + @test haskey(b, k1) + @test haskey(b, k2) + @test !haskey(b, [1]) + @test !haskey(b, [2]) + @test !haskey(b, [3]) +end + +# Test hasvalue function +@testset "hasvalue" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + @test hasvalue(b, "one") + @test hasvalue(b, "two") + @test !hasvalue(b, "three") + + # test hasvalue with mutable key type on `Dict` + b = Bijection{String,Vector{Int}}("one" => [1], "two" => [2]) + @test hasvalue(b, [1]) + @test hasvalue(b, [2]) + @test !hasvalue(b, [3]) + + # test hasvalue with mutable key type on `IdDict` + v1 = [1] + v2 = [2] + b = Bijection{String,Vector{Int},IdDict{String,Vector{Int}},IdDict{Vector{Int},String}}( + "one" => v1, "two" => v2 + ) + @test hasvalue(b, v1) + @test hasvalue(b, v2) + @test !hasvalue(b, [1]) + @test !hasvalue(b, [2]) + @test !hasvalue(b, [3]) +end + +# Test delete! function +@testset "delete!" begin + b = Bijection{Int,String}(1 => "one", 2 => "two") + delete!(b, 1) + @test !haskey(b, 1) + @test !haskey(inv(b), "one") +end