From 4712b637042672c5fb993eb077c5e2a1839a2921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 00:26:41 +0000 Subject: [PATCH 01/10] Ordered dict, ODict, based on SwissDict --- base/Base.jl | 4 +- base/swiss_dict.jl | 651 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 base/swiss_dict.jl diff --git a/base/Base.jl b/base/Base.jl index f67015640b848..f05b62cbbd04e 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -168,7 +168,9 @@ using .Multimedia # Some type include("some.jl") -include("dict.jl") +include("dict.jl") # UDict +include("swiss_dict.jl") # ODict + include("abstractset.jl") include("set.jl") diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl new file mode 100644 index 0000000000000..3cf02f469483a --- /dev/null +++ b/base/swiss_dict.jl @@ -0,0 +1,651 @@ +const SWISS_DICT_LOAD_FACTOR = 0.97 +const _u8x16 = NTuple{16, VecElement{UInt8}} + +""" + ODict([itr]) + +`ODict{K,V}()` constructs an ordered dictionary with keys of type `K` and values of type `V`. +Keys are compared with [`isequal`](@ref) and hashed with [`hash`](@ref). +Given a single iterable argument, constructs a [`ODict`](@ref) whose key-value pairs +are taken from 2-tuples `(key,value)` generated by the argument. + +# Examples +```jldoctest +julia> ODict([("A", 1), ("B", 2)]) +ODict{String,Int64} with 2 entries: + "B" => 2 + "A" => 1 +``` + +Alternatively, a sequence of pair arguments may be passed. + +```jldoctest +julia> ODict("A"=>1, "B"=>2) +ODict{String,Int64} with 2 entries: + "B" => 2 + "A" => 1 +``` +""" +mutable struct ODict{K,V} <: AbstractDict{K,V} + slots::Vector{_u8x16} + keys::Vector{K} + vals::Vector{V} + nbfull::Int + count::Int + age::UInt + idxfloor::Int # an index <= the indices of all used slots + + function ODict{K,V}() where {K, V} + new(fill(_expand16(0x00),1), Vector{K}(undef, 16), Vector{V}(undef, 16), 0, 0, 0, 1) + end + function ODict{K,V}(d::ODict{K,V}) where {K, V} + new(copy(d.slots), copy(d.keys), copy(d.vals), d.nbfull, d.count, d.age, + d.idxfloor) + end + function ODict{K, V}(slots, keys, vals, nbfull, count, age, idxfloor) where {K, V} + new(slots, keys, vals, nbfull, count, age, idxfloor) + end +end +function ODict{K,V}(kv) where {K, V} + h = ODict{K,V}() + for (k,v) in kv + h[k] = v + end + return h +end +ODict{K,V}(p::Pair) where {K,V} = setindex!(ODict{K,V}(), p.second, p.first) +function ODict{K,V}(ps::Pair...) where {K, V} + h = ODict{K,V}() + sizehint!(h, length(ps)) + for p in ps + h[p.first] = p.second + end + return h +end +ODict() = ODict{Any,Any}() +ODict(kv::Tuple{}) = ODict() +Base.copy(d::ODict) = ODict(d) +Base.empty(d::ODict, ::Type{K}, ::Type{V}) where {K, V} = ODict{K, V}() + +ODict(ps::Pair{K,V}...) where {K,V} = ODict{K,V}(ps) +ODict(ps::Pair...) = ODict(ps) + +function ODict(kv) + try + dict_with_eltype((K, V) -> ODict{K, V}, kv, eltype(kv)) + catch e + if !isiterable(typeof(kv)) || !all(x->isa(x,Union{Tuple,Pair}),kv) + throw(ArgumentError("ODict(kv): kv needs to be an iterator of tuples or pairs")) + else + rethrow(e) + end + end +end + +# SIMD utilities +@inline _expand16(u::UInt8) = ntuple(i->VecElement(u), Val(16)) +_blsr(i::UInt32)= i & (i-Int32(1)) + +@inline _vcmp_eq(u::_u8x16, v::_u8x16) = Core.Intrinsics.llvmcall((""" +%cmp = icmp eq <16 x i8> %0, %1 +%cmp16 = bitcast <16 x i1> %cmp to i16 +%res = zext i16 %cmp16 to i32 +ret i32 %res +"""), UInt32, Tuple{_u8x16,_u8x16}, u, v) + +@inline _vcmp_le(u::_u8x16, v::_u8x16) = Core.Intrinsics.llvmcall((""" +%cmp = icmp ule <16 x i8> %0, %1 +%cmp16 = bitcast <16 x i1> %cmp to i16 +%res = zext i16 %cmp16 to i32 +ret i32 %res +"""), UInt32, Tuple{_u8x16,_u8x16}, u, v) + +@inline function _prefetchr(p::Ptr) + ccall("llvm.prefetch", llvmcall, Cvoid, (Ref{Int8}, Int32, Int32, Int32), Ptr{Int8}(p), 0, 3, 1) +end + +@inline function _prefetchw(p::Ptr) + ccall("llvm.prefetch", llvmcall, Cvoid, (Ref{Int8}, Int32, Int32, Int32), Ptr{Int8}(p), 1, 3, 1) +end + +@inline function _hashtag(u::Unsigned) + #extracts tag between 0x02 and 0xff from lower bits, rotates tag bits to front + u = u % UInt + tag = u % UInt8 + if UInt === UInt64 + hi = ((u>>8) | (u<<56)) % Int + else + hi = ((u>>8) | (u<<24)) % Int + end + tag = tag > 1 ? tag : tag+0x02 + return (hi, tag) +end + +Base.@propagate_inbounds function _slotget(slots::Vector{_u8x16}, i::Int) + @boundscheck 0 < i <= length(slots)*16 || throw(BoundsError(slots, 1 + (i-1)>>4)) + GC.@preserve slots begin + return unsafe_load(convert(Ptr{UInt8}, pointer(slots)), i) + end +end + +Base.@propagate_inbounds function _slotset!(slots::Vector{_u8x16}, v::UInt8, i::Int) + @boundscheck 0 < i <= length(slots)*16 || throw(BoundsError(slots, 1 + (i-1)>>4)) + GC.@preserve slots begin + return unsafe_store!(convert(Ptr{UInt8}, pointer(slots)), v, i) + end +end + +@inline function _find_candidates(v::_u8x16, tag::UInt8) + match = _vcmp_eq(v, _expand16(tag)) + return (match, v[16].value === 0x00) +end + +@inline _find_free(v::_u8x16) = _vcmp_le(v, _expand16(UInt8(1))) + +# Basic operations + +# get the index where a key is stored, or -1 if not present +ht_keyindex(h::ODict, key) = ht_keyindex(h::ODict, key, _hashtag(hash(key))...) +function ht_keyindex(h::ODict, key, i0, tag) + slots = h.slots + keys = h.keys + sz = length(slots) + i = i0 & (sz-1) + _prefetchr(pointer(h.keys, i*16+1)) + _prefetchr(pointer(h.vals, i*16+1)) + #Todo/discuss: _prefetchr(pointer(h.keys, i*16+9))? + @inbounds while true + msk = slots[i+1] + cands, done = _find_candidates(msk, tag) + while cands != 0 + off = trailing_zeros(cands) + idx = i*16 + off + 1 + isequal(keys[idx], key) && return idx + cands = _blsr(cands) + end + done && break + i = (i+1) & (sz-1) + end + return -1 +end + +# get the index where a key is stored, or -pos if not present +# and the key would be inserted at pos +# This version is for use by setindex! and get!. It never rehashes. +ht_keyindex2!(h::ODict, key) = ht_keyindex2!(h, key, _hashtag(hash(key))...) +@inline function ht_keyindex2!(h::ODict, key, i0, tag) + slots = h.slots + keys = h.keys + sz = length(slots) + i = i0 & (sz-1) + _prefetchw(pointer(h.keys, i*16+1)) + _prefetchw(pointer(h.vals, i*16+1)) + #Todo/discuss: _prefetchr(pointer(h.keys, i*16+9))? + @inbounds while true + msk = slots[i+1] + cands, done = _find_candidates(msk, tag) + while cands != 0 + off = trailing_zeros(cands) + idx = i*16 + off + 1 + isequal(keys[idx], key) && return idx, tag + cands = _blsr(cands) + end + done && break + i = (i+1) & (sz-1) + end + i = i0 & (sz-1) + @inbounds while true + msk = slots[i+1] + cands = _find_free(msk) + if cands != 0 + off = trailing_zeros(cands) + idx = i*16 + off + 1 + return -idx, tag + end + i = (i+1) & (sz-1) + end +end + +function _setindex!(h::ODict, v, key, index, tag) + @inbounds h.keys[index] = key + @inbounds h.vals[index] = v + h.count += 1 + h.age += 1 + so = _slotget(h.slots, index) + h.nbfull += (iszero(index & 0x0f) & (so==0x00)) + _slotset!(h.slots, tag, index) + if index < h.idxfloor + h.idxfloor = index + end + maybe_rehash_grow!(h) +end + +function _delete!(h::ODict{K,V}, index) where {K,V} + # Caller is responsible for maybe shrinking the ODict after the deletion. + isbitstype(K) || isbitsunion(K) || ccall(:jl_arrayunset, Cvoid, (Any, UInt), h.keys, index-1) + isbitstype(V) || isbitsunion(V) || ccall(:jl_arrayunset, Cvoid, (Any, UInt), h.vals, index-1) + isboundary = iszero(index & 0x0f) #boundaries: 16, 32, ... + @inbounds _slotset!(h.slots, ifelse(isboundary, 0x01, 0x00), index) + h.count -= 1 + h.age += 1 + maybe_rehash_shrink!(h) +end + + +# fast iteration over active slots. +function _iterslots(h::ODict, start::Int) + i0 = ((start-1) & (length(h.keys)-1))>>4 + 1 + off = (start-1) & 0x0f + @inbounds sl = _find_free(h.slots[i0>>4 + 1]) + sl = ((~sl & 0xffff)>>off) << off + return _iterslots(h, (i0, sl)) +end + +function _iterslots(h::ODict, state) + i, sl = state + while iszero(sl) + i += 1 + i <= length(h.slots) || return nothing + @inbounds msk = h.slots[i] + sl = _find_free(msk) + sl = (~sl & 0xffff) + end + return ((i-1)*16 + trailing_zeros(sl) + 1, (i, _blsr(sl))) +end + +# Dictionary resize logic: +# Guarantee 40% of buckets and 15% of entries free, and at least 25% of entries filled +# growth when > 85% entries full or > 60% buckets full, shrink when <25% entries full. +# >60% bucket full should be super rare outside of very bad hash collisions or +# super long-lived Dictionaries (expected 0.85^16 = 7% buckets full at 85% entries full). +# worst-case hysteresis: shrink at 25% vs grow at 30% if all hashes collide. +# expected hysteresis is 25% to 42.5%. +function maybe_rehash_grow!(h::ODict) + sz = length(h.keys) + if h.count > sz * SWISS_DICT_LOAD_FACTOR || (h.nbfull-1) * 10 > sz * 6 + rehash!(h, sz<<2) + end + end + +function maybe_rehash_shrink!(h::ODict) + sz = length(h.keys) + if h.count*4 < sz && sz > 16 + rehash!(h, sz>>1) + end +end + +function Base.sizehint!(d::ODict, newsz) + newsz = _tablesz(newsz*2) # *2 for keys and values in same array + oldsz = length(d.keys) + # grow at least 25% + if newsz < (oldsz*5)>>2 + return d + end + rehash!(d, newsz) +end + +function rehash!(h::ODict{K,V}, newsz = length(h.keys)) where {K, V} + olds = h.slots + oldk = h.keys + oldv = h.vals + sz = length(oldk) + newsz = _tablesz(newsz) + (newsz*SWISS_DICT_LOAD_FACTOR) > h.count || (newsz <<= 1) + h.age += 1 + h.idxfloor = 1 + if h.count == 0 + resize!(h.slots, newsz>>4) + fill!(h.slots, _expand16(0x00)) + resize!(h.keys, newsz) + resize!(h.vals, newsz) + h.nbfull = 0 + return h + end + nssz = newsz>>4 + slots = fill(_expand16(0x00), nssz) + keys = Vector{K}(undef, newsz) + vals = Vector{V}(undef, newsz) + age0 = h.age + nbfull = 0 + is = _iterslots(h, 1) + count = 0 + @inbounds while is !== nothing + i, s = is + k = oldk[i] + v = oldv[i] + i0, t = _hashtag(hash(k)) + i = i0 & (nssz-1) + idx = 0 + while true + msk = slots[i + 1] + cands = _find_free(msk) + if cands != 0 + off = trailing_zeros(cands) + idx = i*16 + off + 1 + break + end + i = (i+1) & (nssz-1) + end + _slotset!(slots, t, idx) + keys[idx] = k + vals[idx] = v + nbfull += iszero(idx & 0x0f) + count += 1 + if h.age != age0 + return rehash!(h, newsz) + end + is = _iterslots(h, s) + end + h.slots = slots + h.keys = keys + h.vals = vals + h.nbfull = nbfull + @assert h.age == age0 + @assert h.count == count + return h +end + +Base.isempty(t::ODict) = (t.count == 0) +Base.length(t::ODict) = t.count + +""" + empty!(collection) -> collection + +Remove all elements from a `collection`. + +# Examples +```jldoctest +julia> A = ODict("a" => 1, "b" => 2) +ODict{String,Int64} with 2 entries: + "b" => 2 + "a" => 1 + +julia> empty!(A); + +julia> A +ODict{String,Int64} with 0 entries +``` +""" +function Base.empty!(h::ODict{K,V}) where {K, V} + fill!(h.slots, _expand16(0x00)) + sz = length(h.keys) + empty!(h.keys) + empty!(h.vals) + resize!(h.keys, sz) + resize!(h.vals, sz) + h.nbfull = 0 + h.count = 0 + h.age += 1 + h.idxfloor = 1 + return h +end + +function Base.setindex!(h::ODict{K,V}, v0, key0) where {K, V} + key = convert(K, key0) + _setindex!(h, v0, key) +end + +function _setindex!(h::ODict{K,V}, v0, key::K) where {K, V} + v = convert(V, v0) + index, tag = ht_keyindex2!(h, key) + + if index > 0 + h.age += 1 + @inbounds h.keys[index] = key + @inbounds h.vals[index] = v + else + _setindex!(h, v, key, -index, tag) + end + + return h +end + +""" + get!(collection, key, default) + +Return the value stored for the given key, or if no mapping for the key is present, store +`key => default`, and return `default`. + +# Examples +```jldoctest +julia> d = ODict("a"=>1, "b"=>2, "c"=>3); + +julia> get!(d, "a", 5) +1 + +julia> get!(d, "d", 4) +4 + +julia> d +ODict{String,Int64} with 4 entries: + "c" => 3 + "b" => 2 + "a" => 1 + "d" => 4 +``` +""" +Base.get!(h::ODict{K,V}, key0, default) where {K,V} = get!(()->default, h, key0) + +""" + get!(f::Function, collection, key) + +Return the value stored for the given key, or if no mapping for the key is present, store +`key => f()`, and return `f()`. + +This is intended to be called using `do` block syntax: +```julia +get!(dict, key) do + # default value calculated here + time() +end +``` +""" +function Base.get!(default::Callable, h::ODict{K,V}, key0) where {K, V} + key = convert(K, key0) + return _get!(default, h, key) +end + +function _get!(default::Callable, h::ODict{K,V}, key::K) where {K, V} + index, tag = ht_keyindex2!(h, key) + + index > 0 && return @inbounds h.vals[index] + + age0 = h.age + v = convert(V, default()) + if h.age != age0 + index, tag = ht_keyindex2!(h, key) + end + if index > 0 + h.age += 1 + @inbounds h.keys[index] = key + @inbounds h.vals[index] = v + else + _setindex!(h, v, key, -index, tag) + end + return v +end + +function Base.getindex(h::ODict{K,V}, key) where {K, V} + index = ht_keyindex(h, key) + @inbounds return (index < 0) ? throw(KeyError(key)) : h.vals[index]::V +end + +""" + get(collection, key, default) + +Return the value stored for the given key, or the given default value if no mapping for the +key is present. + +# Examples +```jldoctest +julia> d = ODict("a"=>1, "b"=>2); + +julia> get(d, "a", 3) +1 + +julia> get(d, "c", 3) +3 +``` +""" +function Base.get(h::ODict{K,V}, key, default) where {K, V} + index = ht_keyindex(h, key) + @inbounds return (index < 0) ? default : h.vals[index]::V +end + +""" + get(f::Function, collection, key) + +Return the value stored for the given key, or if no mapping for the key is present, return +`f()`. Use [`get!`](@ref) to also store the default value in the dictionary. + +This is intended to be called using `do` block syntax + +```julia +get(dict, key) do + # default value calculated here + time() +end +``` +""" +function Base.get(default::Callable, h::ODict{K,V}, key) where {K, V} + index = ht_keyindex(h, key) + @inbounds return (index < 0) ? default() : h.vals[index]::V +end + +""" + haskey(collection, key) -> Bool + +Determine whether a collection has a mapping for a given `key`. + +# Examples +```jldoctest +julia> D = ODict('a'=>2, 'b'=>3) +ODict{Char,Int64} with 2 entries: + 'a' => 2 + 'b' => 3 + +julia> haskey(D, 'a') +true + +julia> haskey(D, 'c') +false +``` +""" +Base.haskey(h::ODict, key) = (ht_keyindex(h, key) > 0) +Base.in(key, v::KeySet{<:Any, <:ODict}) = (ht_keyindex(v.dict, key) > 0) + +""" + getkey(collection, key, default) + +Return the key matching argument `key` if one exists in `collection`, otherwise return `default`. + +# Examples +```jldoctest +julia> D = ODict('a'=>2, 'b'=>3) +ODict{Char,Int64} with 2 entries: + 'a' => 2 + 'b' => 3 + +julia> getkey(D, 'a', 1) +'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase) + +julia> getkey(D, 'd', 'a') +'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase) +``` +""" +function Base.getkey(h::ODict{K,V}, key, default) where {K, V} + index = ht_keyindex(h, key) + @inbounds return (index<0) ? default : h.keys[index]::K +end + +function _pop!(h::ODict, index) + @inbounds val = h.vals[index] + _delete!(h, index) + maybe_rehash_shrink!(h) + return val +end + +""" + pop!(collection, key[, default]) + +Delete and return the mapping for `key` if it exists in `collection`, otherwise return +`default`, or throw an error if `default` is not specified. + +# Examples +```jldoctest +julia> d = ODict("a"=>1, "b"=>2, "c"=>3); + +julia> pop!(d, "a") +1 + +julia> pop!(d, "d") +ERROR: KeyError: key "d" not found +Stacktrace: +[...] + +julia> pop!(d, "e", 4) +4 +``` +""" +function Base.pop!(h::ODict, key) + index = ht_keyindex(h, key) + return index > 0 ? _pop!(h, index) : throw(KeyError(key)) +end + +function Base.pop!(h::ODict, key, default) + index = ht_keyindex(h, key) + return index > 0 ? _pop!(h, index) : default +end + +function Base.pop!(h::ODict) + isempty(h) && throw(ArgumentError("ODict must be non-empty")) + is = _iterslots(h, h.idxfloor) + @assert is !== nothing + idx, s = is + @inbounds key = h.keys[idx] + @inbounds val = h.vals[idx] + _delete!(h, idx) + h.idxfloor = idx + return key => val +end + +""" + delete!(collection, key) + +Delete the mapping for the given key in a collection, and return the collection. + +# Examples +```jldoctest +julia> d = ODict("a"=>1, "b"=>2) +ODict{String,Int64} with 2 entries: + "b" => 2 + "a" => 1 + +julia> delete!(d, "b") +ODict{String,Int64} with 1 entry: + "a" => 1 +``` +""" +function Base.delete!(h::ODict, key) + index = ht_keyindex(h, key) + if index > 0 + _delete!(h, index) + end + maybe_rehash_shrink!(h) + return h +end + +Base.@propagate_inbounds function Base.iterate(h::ODict, state = h.idxfloor) + is = _iterslots(h, state) + is === nothing && return nothing + i, s = is + @inbounds p = h.keys[i] => h.vals[i] + return (p, s) +end + +Base.@propagate_inbounds function Base.iterate(v::Union{KeySet{<:Any, <:ODict}, Base.ValueIterator{<:ODict}}, state=v.dict.idxfloor) + is = _iterslots(v.dict, state) + is === nothing && return nothing + i, s = is + return (v isa KeySet ? v.dict.keys[i] : v.dict.vals[i], s) +end From 2962d47c1325f1fd3801de15f0f449396ef2267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 15:30:44 +0000 Subject: [PATCH 02/10] Update comments --- base/Base.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/Base.jl b/base/Base.jl index f05b62cbbd04e..b191efe14c715 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -168,8 +168,8 @@ using .Multimedia # Some type include("some.jl") -include("dict.jl") # UDict -include("swiss_dict.jl") # ODict +include("dict.jl") # TODO: Dict should be renamed UDict by Julia 2.0 +include("swiss_dict.jl") # Provides ODict implemented by SwissDict, just renamed in the file include("abstractset.jl") include("set.jl") From 7bf4845679e4d2cadefe005282ae1a8fcd6a128c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 15:34:11 +0000 Subject: [PATCH 03/10] Export ODict --- base/exports.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/base/exports.jl b/base/exports.jl index 2c0c628eec866..728c6e8efa1ac 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -60,6 +60,7 @@ export Missing, NTuple, IdDict, + ODict, OrdinalRange, Pair, PartialQuickSort, From b16b0bebc746e178ed2619c09a23adf1133dd869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 15:34:11 +0000 Subject: [PATCH 04/10] Export ODict From 3f5f9dbf65ed9ec27fe885a675261d5e7873832f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 16:30:58 +0000 Subject: [PATCH 05/10] Fix order in docs, was wrong in the original --- base/swiss_dict.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl index 3cf02f469483a..1c9275fe8b76f 100644 --- a/base/swiss_dict.jl +++ b/base/swiss_dict.jl @@ -13,8 +13,8 @@ are taken from 2-tuples `(key,value)` generated by the argument. ```jldoctest julia> ODict([("A", 1), ("B", 2)]) ODict{String,Int64} with 2 entries: - "B" => 2 "A" => 1 + "B" => 2 ``` Alternatively, a sequence of pair arguments may be passed. @@ -22,8 +22,8 @@ Alternatively, a sequence of pair arguments may be passed. ```jldoctest julia> ODict("A"=>1, "B"=>2) ODict{String,Int64} with 2 entries: - "B" => 2 "A" => 1 + "B" => 2 ``` """ mutable struct ODict{K,V} <: AbstractDict{K,V} @@ -357,8 +357,8 @@ Remove all elements from a `collection`. ```jldoctest julia> A = ODict("a" => 1, "b" => 2) ODict{String,Int64} with 2 entries: - "b" => 2 "a" => 1 + "b" => 2 julia> empty!(A); @@ -418,9 +418,9 @@ julia> get!(d, "d", 4) julia> d ODict{String,Int64} with 4 entries: - "c" => 3 - "b" => 2 "a" => 1 + "b" => 2 + "c" => 3 "d" => 4 ``` """ @@ -618,8 +618,8 @@ Delete the mapping for the given key in a collection, and return the collection. ```jldoctest julia> d = ODict("a"=>1, "b"=>2) ODict{String,Int64} with 2 entries: - "b" => 2 "a" => 1 + "b" => 2 julia> delete!(d, "b") ODict{String,Int64} with 1 entry: From a9d1d67bbd9a8bcb5d0652bc12192512d66ad271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 17:28:10 +0000 Subject: [PATCH 06/10] Whitespace --- base/swiss_dict.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl index 1c9275fe8b76f..234b4b78c3184 100644 --- a/base/swiss_dict.jl +++ b/base/swiss_dict.jl @@ -12,7 +12,7 @@ are taken from 2-tuples `(key,value)` generated by the argument. # Examples ```jldoctest julia> ODict([("A", 1), ("B", 2)]) -ODict{String,Int64} with 2 entries: +ODict{String, Int64} with 2 entries: "A" => 1 "B" => 2 ``` @@ -21,7 +21,7 @@ Alternatively, a sequence of pair arguments may be passed. ```jldoctest julia> ODict("A"=>1, "B"=>2) -ODict{String,Int64} with 2 entries: +ODict{String, Int64} with 2 entries: "A" => 1 "B" => 2 ``` @@ -356,14 +356,14 @@ Remove all elements from a `collection`. # Examples ```jldoctest julia> A = ODict("a" => 1, "b" => 2) -ODict{String,Int64} with 2 entries: +ODict{String, Int64} with 2 entries: "a" => 1 "b" => 2 julia> empty!(A); julia> A -ODict{String,Int64} with 0 entries +ODict{String, Int64} with 0 entries ``` """ function Base.empty!(h::ODict{K,V}) where {K, V} @@ -417,7 +417,7 @@ julia> get!(d, "d", 4) 4 julia> d -ODict{String,Int64} with 4 entries: +ODict{String, Int64} with 4 entries: "a" => 1 "b" => 2 "c" => 3 @@ -520,7 +520,7 @@ Determine whether a collection has a mapping for a given `key`. # Examples ```jldoctest julia> D = ODict('a'=>2, 'b'=>3) -ODict{Char,Int64} with 2 entries: +ODict{Char, Int64} with 2 entries: 'a' => 2 'b' => 3 @@ -542,7 +542,7 @@ Return the key matching argument `key` if one exists in `collection`, otherwise # Examples ```jldoctest julia> D = ODict('a'=>2, 'b'=>3) -ODict{Char,Int64} with 2 entries: +ODict{Char, Int64} with 2 entries: 'a' => 2 'b' => 3 @@ -617,12 +617,12 @@ Delete the mapping for the given key in a collection, and return the collection. # Examples ```jldoctest julia> d = ODict("a"=>1, "b"=>2) -ODict{String,Int64} with 2 entries: +ODict{String, Int64} with 2 entries: "a" => 1 "b" => 2 julia> delete!(d, "b") -ODict{String,Int64} with 1 entry: +ODict{String, Int64} with 1 entry: "a" => 1 ``` """ From ce3427a9cdf0e6488622ee4d9c00f32bc355b321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 18:18:14 +0000 Subject: [PATCH 07/10] One more doc fix for 0 entries --- base/swiss_dict.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl index 234b4b78c3184..a72b293e0a7cd 100644 --- a/base/swiss_dict.jl +++ b/base/swiss_dict.jl @@ -363,7 +363,7 @@ ODict{String, Int64} with 2 entries: julia> empty!(A); julia> A -ODict{String, Int64} with 0 entries +ODict{String, Int64}() ``` """ function Base.empty!(h::ODict{K,V}) where {K, V} From eaa1f90de3736e4262d55fd7e3866ce9d0ce54f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 20:19:15 +0000 Subject: [PATCH 08/10] Add tests and mark experimental --- base/abstractdict.jl | 18 + base/swiss_dict.jl | 3 + test/odict.jl | 1143 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1164 insertions(+) create mode 100644 test/odict.jl diff --git a/base/abstractdict.jl b/base/abstractdict.jl index 62bb3b8cf5a2e..650a45435dffa 100644 --- a/base/abstractdict.jl +++ b/base/abstractdict.jl @@ -361,6 +361,24 @@ function _typeddict(d::AbstractDict, others::AbstractDict...) Dict{K,V}(d) end +#TODO: better way to do, than copy and change from above: +mergewith(combine, d::ODict, others::AbstractDict...) = + mergewith!(combine, _typeddict(d, others...), others...) +#mergewith(combine) = (args...) -> mergewith(combine, args...) +merge(combine::Base.Callable, d::ODict, others::AbstractDict...) = + merge!(combine, _typeddict(d, others...), others...) + +function _typeddict(d::ODict, others::AbstractDict...) + K = promoteK(keytype(d), others...) + V = promoteV(valtype(d), others...) + ODict{K,V}(d) +end + +# Not needed? +merge(d::ODict, others::AbstractDict...) = + merge!(_typeddict(d, others...), others...) + + """ filter!(f, d::AbstractDict) diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl index a72b293e0a7cd..1f68ad03bf7e8 100644 --- a/base/swiss_dict.jl +++ b/base/swiss_dict.jl @@ -9,6 +9,9 @@ Keys are compared with [`isequal`](@ref) and hashed with [`hash`](@ref). Given a single iterable argument, constructs a [`ODict`](@ref) whose key-value pairs are taken from 2-tuples `(key,value)` generated by the argument. +!!! compat "Julia 1.6" +ODict is experimental and requires Julia 1.6 or later. + # Examples ```jldoctest julia> ODict([("A", 1), ("B", 2)]) diff --git a/test/odict.jl b/test/odict.jl new file mode 100644 index 0000000000000..21952fcfc1896 --- /dev/null +++ b/test/odict.jl @@ -0,0 +1,1143 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# copy of the other dict.jl test file, amended for ODict, partially commented out +# for stuff that didn't seem to matter. + +using Random + +@testset "Pair" begin + p = Pair(10,20) + @test p == (10=>20) + @test isequal(p,10=>20) + @test iterate(p)[1] == 10 + @test iterate(p, iterate(p)[2])[1] == 20 + @test iterate(p, iterate(p, iterate(p)[2])[2]) == nothing + @test firstindex(p) == 1 + @test lastindex(p) == length(p) == 2 + @test Base.indexed_iterate(p, 1, nothing) == (10,2) + @test Base.indexed_iterate(p, 2, nothing) == (20,3) + @test (1=>2) < (2=>3) + @test (2=>2) < (2=>3) + @test !((2=>3) < (2=>3)) + @test (2=>3) < (4=>3) + @test (1=>100) < (4=>1) + @test p[1] == 10 + @test p[2] == 20 + @test_throws BoundsError p[3] + @test_throws BoundsError p[false] + @test p[true] == 10 + @test p[2.0] == 20 + @test p[0x01] == 10 + @test_throws InexactError p[2.3] + @test first(p) == 10 + @test last(p) == 20 + @test eltype(p) == Int + @test eltype(4 => 5.6) == Union{Int,Float64} + @test vcat(1 => 2.0, 1.0 => 2) == [1.0 => 2.0, 1.0 => 2.0] +end + +@testset "ODict" begin + h = ODict() + for i=1:10000 + h[i] = i+1 + end + for i=1:10000 + @test (h[i] == i+1) + end + for i=1:2:10000 + delete!(h, i) + end + for i=1:2:10000 + h[i] = i+1 + end + for i=1:10000 + @test (h[i] == i+1) + end + for i=1:10000 + delete!(h, i) + end + @test isempty(h) + h[77] = 100 + @test h[77] == 100 + for i=1:10000 + h[i] = i+1 + end + for i=1:2:10000 + delete!(h, i) + end + for i=10001:20000 + h[i] = i+1 + end + for i=2:2:10000 + @test h[i] == i+1 + end + for i=10000:20000 + @test h[i] == i+1 + end + h = ODict{Any,Any}("a" => 3) + @test h["a"] == 3 + h["a","b"] = 4 + @test h["a","b"] == h[("a","b")] == 4 + h["a","b","c"] = 4 + @test h["a","b","c"] == h[("a","b","c")] == 4 + + @testset "eltype, keytype and valtype" begin + @test eltype(h) == Pair{Any,Any} + @test keytype(h) == Any + @test valtype(h) == Any + + td = ODict{AbstractString,Float64}() + @test eltype(td) == Pair{AbstractString,Float64} + @test keytype(td) == AbstractString + @test valtype(td) == Float64 + @test keytype(ODict{AbstractString,Float64}) === AbstractString + @test valtype(ODict{AbstractString,Float64}) === Float64 + end + # test rethrow of error in ctor + @test_throws DomainError ODict((sqrt(p[1]), sqrt(p[2])) for p in zip(-1:2, -1:2)) +end + +let x = ODict(3=>3, 5=>5, 8=>8, 6=>6) + pop!(x, 5) + for k in keys(x) + ODict{Int,Int}(x) + @test k in [3, 8, 6] + end +end + +let z = ODict() + get_KeyError = false + try + z["a"] + catch _e123_ + get_KeyError = isa(_e123_,KeyError) + end + @test get_KeyError +end + +_d = ODict("a"=>0) +@test isa([k for k in filter(x->length(x)==1, collect(keys(_d)))], Vector{String}) + +@testset "typeof" begin + d = ODict(((1, 2), (3, 4))) + @test d[1] === 2 + @test d[3] === 4 + d2 = ODict(1 => 2, 3 => 4) + d3 = ODict((1 => 2, 3 => 4)) + @test d == d2 == d3 + @test typeof(d) == typeof(d2) == typeof(d3) == ODict{Int,Int} + + d = ODict(((1, 2), (3, "b"))) + @test d[1] === 2 + @test d[3] == "b" + d2 = ODict(1 => 2, 3 => "b") + d3 = ODict((1 => 2, 3 => "b")) + @test d == d2 == d3 + @test typeof(d) == typeof(d2) == typeof(d3) == ODict{Int,Any} + + d = ODict(((1, 2), ("a", 4))) + @test d[1] === 2 + @test d["a"] === 4 + d2 = ODict(1 => 2, "a" => 4) + d3 = ODict((1 => 2, "a" => 4)) + @test d == d2 == d3 + @test typeof(d) == typeof(d2) == typeof(d3) == ODict{Any,Int} + + d = ODict(((1, 2), ("a", "b"))) + @test d[1] === 2 + @test d["a"] == "b" + d2 = ODict(1 => 2, "a" => "b") + d3 = ODict((1 => 2, "a" => "b")) + @test d == d2 == d3 + @test typeof(d) == typeof(d2) == typeof(d3) == ODict{Any,Any} +end + +@test_throws ArgumentError first(ODict()) +@test first(ODict(:f=>2)) == (:f=>2) + +@testset "constructing ODicts from iterators" begin + d = @inferred ODict(i=>i for i=1:3) + @test isa(d, ODict{Int,Int}) + @test d == ODict(1=>1, 2=>2, 3=>3) + d = ODict(i==1 ? (1=>2) : (2.0=>3.0) for i=1:2) + @test isa(d, ODict{Real,Real}) + @test d == ODict{Real,Real}(2.0=>3.0, 1=>2) +end + +@testset "type of ODict constructed from varargs of Pairs" begin + @test ODict(1=>1, 2=>2.0) isa ODict{Int,Real} + @test ODict(1=>1, 2.0=>2) isa ODict{Real,Int} + @test ODict(1=>1.0, 2.0=>2) isa ODict{Real,Real} + + for T in (Nothing, Missing) + @test ODict(1=>1, 2=>T()) isa ODict{Int,Union{Int,T}} + @test ODict(1=>T(), 2=>2) isa ODict{Int,Union{Int,T}} + @test ODict(1=>1, T()=>2) isa ODict{Union{Int,T},Int} + @test ODict(T()=>1, 2=>2) isa ODict{Union{Int,T},Int} + end +end + +@test_throws KeyError ODict("a"=>2)[Base.secret_table_token] + +@testset "issue #1821" begin + d = ODict{String, Vector{Int}}() + d["a"] = [1, 2] + @test_throws MethodError d["b"] = 1 + @test isa(repr(d), AbstractString) # check that printable without error +end + +@testset "issue #2344" begin + local bar + bestkey(d, key) = key + bestkey(d::AbstractDict{K,V}, key) where {K<:AbstractString,V} = string(key) + bar(x) = bestkey(x, :y) + @test bar(ODict(:x => [1,2,5])) == :y + @test bar(ODict("x" => [1,2,5])) == "y" +end + +mutable struct I1438T + id +end +import Base.hash +hash(x::I1438T, h::UInt) = hash(x.id, h) + +@testset "issue #1438" begin + seq = [26, 28, 29, 30, 31, 32, 33, 34, 35, 36, -32, -35, -34, -28, 37, 38, 39, 40, -30, + -31, 41, 42, 43, 44, -33, -36, 45, 46, 47, 48, -37, -38, 49, 50, 51, 52, -46, -50, 53] + xs = [ I1438T(id) for id = 1:53 ] + s = Set() + for id in seq + if id > 0 + x = xs[id] + push!(s, x) + @test in(x, s) # check that x can be found + else + delete!(s, xs[-id]) + end + end +end + +@testset "equality" for eq in (isequal, ==) + @test eq(ODict(), ODict()) + @test eq(ODict(1 => 1), ODict(1 => 1)) + @test !eq(ODict(1 => 1), ODict()) + @test !eq(ODict(1 => 1), ODict(1 => 2)) + @test !eq(ODict(1 => 1), ODict(2 => 1)) + + # Generate some data to populate dicts to be compared + data_in = [ (rand(1:1000), randstring(2)) for _ in 1:1001 ] + + # Populate the first dict + d1 = ODict{Int, AbstractString}() + for (k, v) in data_in + d1[k] = v + end + data_in = collect(d1) + # shuffle the data + for i in 1:length(data_in) + j = rand(1:length(data_in)) + data_in[i], data_in[j] = data_in[j], data_in[i] + end + # Inserting data in different (shuffled) order should result in + # equivalent dict. + d2 = ODict{Int, AbstractString}() + for (k, v) in data_in + d2[k] = v + end + + @test eq(d1, d2) + d3 = copy(d2) + d4 = copy(d2) + # Removing an item gives different dict + delete!(d1, data_in[rand(1:length(data_in))][1]) + @test !eq(d1, d2) + # Changing a value gives different dict + d3[data_in[rand(1:length(data_in))][1]] = randstring(3) + !eq(d1, d3) + # Adding a pair gives different dict + d4[1001] = randstring(3) + @test !eq(d1, d4) + + @test eq(ODict(), sizehint!(ODict(),96)) + + # ODictionaries of different types + @test !eq(ODict(1 => 2), ODict("dog" => "bone")) + @test eq(ODict{Int,Int}(), ODict{AbstractString,AbstractString}()) +end + +@testset "equality special cases" begin + @test ODict(1=>0.0) == ODict(1=>-0.0) + @test !isequal(ODict(1=>0.0), ODict(1=>-0.0)) + + @test ODict(0.0=>1) != ODict(-0.0=>1) + @test !isequal(ODict(0.0=>1), ODict(-0.0=>1)) + + @test ODict(1=>NaN) != ODict(1=>NaN) + @test isequal(ODict(1=>NaN), ODict(1=>NaN)) + + @test ODict(NaN=>1) == ODict(NaN=>1) + @test isequal(ODict(NaN=>1), ODict(NaN=>1)) + + @test ismissing(ODict(1=>missing) == ODict(1=>missing)) + @test isequal(ODict(1=>missing), ODict(1=>missing)) + d = ODict(1=>missing) + @test ismissing(d == d) + d = ODict(1=>[missing]) + @test ismissing(d == d) + d = ODict(1=>NaN) + @test d != d + @test isequal(d, d) + + @test ODict(missing=>1) == ODict(missing=>1) + @test isequal(ODict(missing=>1), ODict(missing=>1)) +end + +@testset "get!" begin # (get with default values assigned to the given location) + f(x) = x^2 + d = ODict(8=>19) + @test get!(d, 8, 5) == 19 + @test get!(d, 19, 2) == 2 + + @test get!(d, 42) do # d is updated with f(2) + f(2) + end == 4 + + @test get!(d, 42) do # d is not updated + f(200) + end == 4 + + @test get(d, 13) do # d is not updated + f(4) + end == 16 + + @test d == ODict(8=>19, 19=>2, 42=>4) +end + +@testset "getkey" begin + h = ODict(1=>2, 3 => 6, 5=>10) + @test getkey(h, 1, 7) == 1 + @test getkey(h, 4, 6) == 6 + @test getkey(h, "1", 8) == 8 +end + +@testset "show" begin + for d in (ODict("\n" => "\n", "1" => "\n", "\n" => "2"), + ODict(string(i) => i for i = 1:30), + ODict(reshape(1:i^2,i,i) => reshape(1:i^2,i,i) for i = 1:24), + ODict(String(Char['α':'α'+i;]) => String(Char['α':'α'+i;]) for i = (1:10)*10), + ODict("key" => zeros(0, 0))) + for cols in (12, 40, 80), rows in (2, 10, 24) + # Ensure output is limited as requested + s = IOBuffer() + io = Base.IOContext(s, :limit => true, :displaysize => (rows, cols)) + Base.show(io, MIME("text/plain"), d) + out = split(String(take!(s)),'\n') + for line in out[2:end] + @test textwidth(line) <= cols + end + @test length(out) <= rows + + for f in (keys, values) + s = IOBuffer() + io = Base.IOContext(s, :limit => true, :displaysize => (rows, cols)) + Base.show(io, MIME("text/plain"), f(d)) + out = split(String(take!(s)),'\n') + for line in out[2:end] + @test textwidth(line) <= cols + end + @test length(out) <= rows + end + end + # Simply ensure these do not throw errors + Base.show(IOBuffer(), d) + @test !isempty(summary(d)) + @test !isempty(summary(keys(d))) + @test !isempty(summary(values(d))) + end + # show on empty ODict + io = IOBuffer() + d = ODict{Int, String}() + show(io, d) + str = String(take!(io)) + @test str == "ODict{$(Int), String}()" + close(io) +end + +@testset "Issue #15739" begin # Compact REPL printouts of an `AbstractDict` use brackets when appropriate + d = ODict((1=>2) => (3=>45), (3=>10) => (10=>11)) + buf = IOBuffer() + show(IOContext(buf, :compact => true), d) + + # Check explicitly for the expected strings, since the CPU bitness effects + # dictionary ordering. + result = String(take!(buf)) + @test occursin("ODict", result) + @test occursin("(1=>2)=>(3=>45)", result) + @test occursin("(3=>10)=>(10=>11)", result) +end + +mutable struct Alpha end +Base.show(io::IO, ::Alpha) = print(io,"α") +@testset "issue #9463" begin + sbuff = IOBuffer() + io = Base.IOContext(sbuff, :limit => true, :displaysize => (10, 20)) + + Base.show(io, MIME("text/plain"), ODict(Alpha()=>1)) + local str = String(take!(sbuff)) + @test !occursin("…", str) + @test endswith(str, "α => 1") +end + +@testset "issue #2540" begin + d = ODict{Any,Any}(ODict(x => 1 for x in ['a', 'b', 'c'])) + @test d == ODict('a'=>1, 'b'=>1, 'c'=> 1) +end + +@testset "issue #2629" begin + d = ODict{AbstractString,AbstractString}(ODict(a=>"foo" for a in ["a","b","c"])) + @test d == ODict("a"=>"foo","b"=>"foo","c"=>"foo") +end + +@testset "issue #5886" begin + d5886 = ODict() + for k5886 in 1:11 + d5886[k5886] = 1 + end + for k5886 in keys(d5886) + # undefined ref if not fixed + d5886[k5886] += 1 + end +end + +@testset "issue #8877" begin + a = ODict("foo" => 0.0, "bar" => 42.0) + b = ODict("フー" => 17, "バー" => 4711) + @test typeof(merge(a, b)) === ODict{String,Float64} +end + +@testset "issue 9295" begin + d = ODict() + @test push!(d, 'a' => 1) === d + @test d['a'] == 1 + @test push!(d, 'b' => 2, 'c' => 3) === d + @test d['b'] == 2 + @test d['c'] == 3 + @test push!(d, 'd' => 4, 'e' => 5, 'f' => 6) === d + @test d['d'] == 4 + @test d['e'] == 5 + @test d['f'] == 6 + @test length(d) == 6 +end + +mutable struct T10647{T}; x::T; end +@testset "issue #10647" begin + a = IdDict() + a[1] = a + a[a] = 2 + a[3] = T10647(a) + @test isequal(a, a) + show(IOBuffer(), a) + Base.show(Base.IOContext(IOBuffer(), :limit => true), a) + Base.show(IOBuffer(), a) + Base.show(Base.IOContext(IOBuffer(), :limit => true), a) +end + +@testset "IdDict{Any,Any} and partial inference" begin + a = IdDict{Any,Any}() + a[1] = a + a[a] = 2 + + sa = empty(a) + @test isempty(sa) + @test isa(sa, IdDict{Any,Any}) + + @test length(a) == 2 + @test 1 in keys(a) + @test a in keys(a) + @test a[1] === a + @test a[a] === 2 + + ca = copy(a) + @test length(ca) == length(a) + @test isequal(ca, a) + @test ca !== a # make sure they are different objects + + ca = empty!(ca) + @test length(ca) == 0 + @test length(a) == 2 + + d = ODict('a'=>1, 'b'=>1, 'c'=> 3) + @test a != d + @test !isequal(a, d) + + @test length(IdDict{Any,Any}(1=>2, 1.0=>3)) == 2 + @test length(ODict(1=>2, 1.0=>3)) == 1 + + d = @inferred IdDict{Any,Any}(i=>i for i=1:3) + @test isa(d, IdDict{Any,Any}) + @test d == IdDict{Any,Any}(1=>1, 2=>2, 3=>3) + + d = @inferred IdDict{Any,Any}(Pair(1,1), Pair(2,2), Pair(3,3)) + @test isa(d, IdDict{Any,Any}) + @test d == IdDict{Any,Any}(1=>1, 2=>2, 3=>3) + @test eltype(d) == Pair{Any,Any} + + d = IdDict{Any,Int32}(:hi => 7) + let c = Ref{Any}(1.5) + f() = c[] + @test @inferred(get!(f, d, :hi)) === Int32(7) + @test_throws InexactError(:Int32, Int32, 1.5) get!(f, d, :hello) + end +end + +@testset "IdDict" begin + a = IdDict() + a[1] = a + a[a] = 2 + + sa = empty(a) + @test isempty(sa) + @test isa(sa, IdDict) + + @test length(a) == 2 + @test 1 in keys(a) + @test a in keys(a) + @test a[1] === a + @test a[a] === 2 + + ca = copy(a) + @test length(ca) == length(a) + @test isequal(ca, a) + @test ca !== a # make sure they are different objects + + ca = empty!(ca) + @test length(ca) == 0 + @test length(a) == 2 + + d = ODict('a'=>1, 'b'=>1, 'c'=> 3) + @test a != d + @test !isequal(a, d) + + @test length(IdDict(1=>2, 1.0=>3)) == 2 + @test length(ODict(1=>2, 1.0=>3)) == 1 + + d = @inferred IdDict(i=>i for i=1:3) + @test isa(d, IdDict) + @test d == IdDict(1=>1, 2=>2, 3=>3) + + d = @inferred IdDict(Pair(1,1), Pair(2,2), Pair(3,3)) + @test isa(d, IdDict) + @test d == IdDict(1=>1, 2=>2, 3=>3) + @test eltype(d) == Pair{Int,Int} + @test_throws KeyError d[:a] + @test_throws ArgumentError d[:a] = 1 + @test_throws MethodError d[1] = :a + + # copy constructor + d = IdDict(Pair(1,1), Pair(2,2), Pair(3,3)) + @test collect(values(IdDict{Int,Float64}(d))) == collect(values(d)) + @test_throws ArgumentError IdDict{Float64,Int}(d) + + # misc constructors + @test typeof(IdDict(1=>1, :a=>2)) == IdDict{Any,Int} + @test typeof(IdDict(1=>1, 1=>:a)) == IdDict{Int,Any} + @test typeof(IdDict(:a=>1, 1=>:a)) == IdDict{Any,Any} + @test typeof(IdDict(())) == IdDict{Any,Any} + + # check that returned values are inferred + d = @inferred IdDict(Pair(1,1), Pair(2,2), Pair(3,3)) + @test 1 == @inferred d[1] + @inferred setindex!(d, -1, 10) + @test d[10] == -1 + @test 1 == @inferred d[1] + @test get(d, -111, nothing) == nothing + @test 1 == @inferred get(d, 1, 1) + @test pop!(d, -111, nothing) == nothing + @test 1 == @inferred pop!(d, 1) + + # get! and delete! + d = @inferred IdDict(Pair(:a,1), Pair(:b,2), Pair(3,3)) + @test get!(d, "a", -1) == -1 + @test d["a"] == -1 + @test get!(d, "a", "b") == -1 + @test_throws MethodError get!(d, "b", "b") + @test delete!(d, "a") === d + @test !haskey(d, "a") + @test_throws ArgumentError get!(IdDict{Symbol,Any}(), 2, "b") + @test get!(IdDict{Int,Int}(), 1, 2.0) === 2 + @test get!(()->2.0, IdDict{Int,Int}(), 1) === 2 + + # sizehint! & rehash! + d = IdDict() + @test sizehint!(d, 10^4) === d + @test length(d.ht) >= 10^4 + d = IdDict() + for jj=1:30, i=1:10^4 + d[i] = i + end + for i=1:10^4 + @test d[i] == i + end + @test length(d.ht) >= 10^4 + @test d === Base.rehash!(d, 123452) # number needs to be even + + # not an iterator of tuples or pairs + @test_throws ArgumentError IdDict([1, 2, 3, 4]) + # test rethrow of error in ctor + @test_throws DomainError IdDict((sqrt(p[1]), sqrt(p[2])) for p in zip(-1:2, -1:2)) +end + +@testset "issue 30165, get! for IdDict" begin + f(x) = x^2 + d = IdDict(8=>19) + @test get!(d, 8, 5) == 19 + @test get!(d, 19, 2) == 2 + + @test get!(d, 42) do # d is updated with f(2) + f(2) + end == 4 + + @test get!(d, 42) do # d is not updated + f(200) + end == 4 + + @test get(d, 13) do # d is not updated + f(4) + end == 16 + + @test d == IdDict(8=>19, 19=>2, 42=>4) +end + +@testset "issue #26833, deletion from IdDict" begin + d = IdDict() + i = 1 + # generate many hash collisions + while length(d) < 32 # expected to occur at i <≈ 2^16 * 2^5 + if objectid(i) % UInt16 == 0x1111 + push!(d, i => true) + end + i += 1 + end + k = collect(keys(d)) + @test haskey(d, k[1]) + delete!(d, k[1]) + @test length(d) == 31 + @test !haskey(d, k[1]) + @test haskey(d, k[end]) + push!(d, k[end] => false) + @test length(d) == 31 + @test haskey(d, k[end]) + @test !pop!(d, k[end]) + @test !haskey(d, k[end]) + @test length(d) == 30 +end + + +@testset "Issue #7944" begin + d = ODict{Int,Int}() + get!(d, 0) do + d[0] = 1 + end + @test length(d) == 1 +end + +@testset "iteration" begin + d = ODict('a'=>1, 'b'=>1, 'c'=> 3) + @test [d[k] for k in keys(d)] == [d[k] for k in eachindex(d)] == + [v for (k, v) in d] == [d[x[1]] for (i, x) in enumerate(d)] +end + +@testset "generators, similar" begin + d = ODict(:a=>"a") + # TODO: restore when 0.7 deprecation is removed + #@test @inferred(map(identity, d)) == d +end + +@testset "Issue 12451" begin + @test_throws ArgumentError ODict(0) + @test_throws ArgumentError ODict([1]) + @test_throws ArgumentError ODict([(1,2),0]) +end + +# test ODict constructor's argument checking (for an iterable of pairs or tuples) +# make sure other errors can propagate when the nature of the iterator is not the problem +@test_throws InexactError ODict(convert(Int,1.5) for i=1:1) +@test_throws InexactError WeakKeyDict(convert(Int,1.5) for i=1:1) + +import Base.ImmutableDict +@testset "ImmutableDict" begin + d = ImmutableDict{String, String}() + k1 = "key1" + k2 = "key2" + v1 = "value1" + v2 = "value2" + d1 = ImmutableDict(d, k1 => v1) + d2 = ImmutableDict(d1, k2 => v2) + d3 = ImmutableDict(d2, k1 => v2) + d4 = ImmutableDict(d3, k2 => v1) + dnan = ImmutableDict{String, Float64}(k2, NaN) + dnum = ImmutableDict(dnan, k2 => 1) + + @test isempty(collect(d)) + @test !isempty(collect(d1)) + @test isempty(d) + @test !isempty(d1) + @test length(d) == 0 + @test length(d1) == 1 + @test length(d2) == 2 + @test length(d3) == 3 + @test length(d4) == 4 + @test !(k1 in keys(d)) + @test k1 in keys(d1) + @test k1 in keys(d2) + @test k1 in keys(d3) + @test k1 in keys(d4) + + @test !haskey(d, k1) + @test haskey(d1, k1) + @test haskey(d2, k1) + @test haskey(d3, k1) + @test haskey(d4, k1) + @test !(k2 in keys(d1)) + @test k2 in keys(d2) + @test !(k1 in values(d4)) + @test v1 in values(d4) + @test collect(d1) == [Pair(k1, v1)] + @test collect(d4) == reverse([Pair(k1, v1), Pair(k2, v2), Pair(k1, v2), Pair(k2, v1)]) + @test d1 == ImmutableDict(d, k1 => v1) + @test !((k1 => v2) in d2) + @test (k1 => v2) in d3 + @test (k1 => v1) in d4 + @test (k1 => v2) in d4 + @test in(k2 => "value2", d4, ===) + @test in(k2 => v2, d4, ===) + @test in(k2 => NaN, dnan, isequal) + @test in(k2 => NaN, dnan, ===) + @test !in(k2 => NaN, dnan, ==) + @test !in(k2 => 1, dnum, ===) + @test in(k2 => 1.0, dnum, ===) + @test !in(k2 => 1, dnum, <) + @test in(k2 => 0, dnum, <) + @test get(d1, "key1", :default) === v1 + @test get(d4, "key1", :default) === v2 + @test get(d4, "foo", :default) === :default + @test get(d, k1, :default) === :default + @test d1["key1"] === v1 + @test d4["key1"] === v2 + @test empty(d3) === d + @test empty(d) === d + + @test_throws KeyError d[k1] + @test_throws KeyError d1["key2"] + + v = [k1 => v1, k2 => v2] + d5 = ImmutableDict(v...) + @test d5 == d2 + @test reverse(collect(d5)) == v + d6 = ImmutableDict(:a => 1, :b => 3, :a => 2) + @test d6[:a] == 2 + @test d6[:b] == 3 + + @test !haskey(ImmutableDict(-0.0=>1), 0.0) +end + +@testset "filtering" begin + d = ODict(zip(1:1000,1:1000)) + f = p -> iseven(p.first) + @test filter(f, d) == filter!(f, copy(d)) == + invoke(filter!, Tuple{Function,AbstractDict}, f, copy(d)) == + ODict(zip(2:2:1000, 2:2:1000)) + d = ODict(zip(-1:3,-1:3)) + f = p -> sqrt(p.second) > 2 + # test rethrowing error from f + @test_throws DomainError filter(f, d) +end + +struct MyString <: AbstractString + str::String +end +struct MyInt <: Integer + val::UInt +end + +import Base.== +const global hashoffset = [UInt(190)] + +Base.hash(s::MyString) = hash(s.str) + hashoffset[] +Base.lastindex(s::MyString) = lastindex(s.str) +Base.iterate(s::MyString, v::Int=1) = iterate(s.str, v) +Base.isequal(a::MyString, b::MyString) = isequal(a.str, b.str) +==(a::MyString, b::MyString) = (a.str == b.str) + +Base.hash(v::MyInt) = v.val + hashoffset[] +Base.lastindex(v::MyInt) = lastindex(v.val) +Base.iterate(v::MyInt, i...) = iterate(v.val, i...) +Base.isequal(a::MyInt, b::MyInt) = isequal(a.val, b.val) +==(a::MyInt, b::MyInt) = (a.val == b.val) +@testset "issue #15077" begin + let badKeys = [ + "FINO_emv5.0","FINO_ema0.1","RATE_ema1.0","NIBPM_ema1.0", + "SAO2_emv5.0","O2FLOW_ema5.0","preop_Neuro/Psych_","gender_", + "FIO2_ema0.1","PEAK_ema5.0","preop_Reproductive_denies","O2FLOW_ema0.1", + "preop_Endocrine_denies","preop_Respiratory_", + "NIBPM_ema0.1","PROPOFOL_MCG/KG/MIN_decay5.0","NIBPD_ema1.0","NIBPS_ema5.0", + "anesthesiaStartTime","NIBPS_ema1.0","RESPRATE_ema1.0","PEAK_ema0.1", + "preop_GU_denies","preop_Cardiovascular_","PIP_ema5.0","preop_ENT_denies", + "preop_Skin_denies","preop_Renal_denies","asaCode_IIIE","N2OFLOW_emv5.0", + "NIBPD_emv5.0", # <--- here is the key that we later can't find + "NIBPM_ema5.0","preop_Respiratory_complete","ETCO2_ema5.0", + "RESPRATE_ema0.1","preop_Functional Status_<2","preop_Renal_symptoms", + "ECGRATE_ema5.0","FIO2_emv5.0","RESPRATE_emv5.0","7wu3ty0a4fs","BVO", + "4UrCWXUsaT" + ] + local d = ODict{AbstractString,Int}() + for i = 1:length(badKeys) + d[badKeys[i]] = i + end + # Check all keys for missing values + for i = 1:length(badKeys) + @test d[badKeys[i]] == i + end + + # Walk through all possible hash values (mod size of hash table) + for offset = 0:1023 + local d2 = ODict{MyString,Int}() + hashoffset[] = offset + for i = 1:length(badKeys) + d2[MyString(badKeys[i])] = i + end + # Check all keys for missing values + for i = 1:length(badKeys) + @test d2[MyString(badKeys[i])] == i + end + end + end + + + let badKeys = UInt16[0xb800,0xa501,0xcdff,0x6303,0xe40a,0xcf0e,0xf3df,0xae99,0x9913,0x741c, + 0xd01f,0xc822,0x9723,0xb7a0,0xea25,0x7423,0x6029,0x202a,0x822b,0x492c, + 0xd02c,0x862d,0x8f34,0xe529,0xf938,0x4f39,0xd03a,0x473b,0x1e3b,0x1d3a, + 0xcc39,0x7339,0xcf40,0x8740,0x813d,0xe640,0xc443,0x6344,0x3744,0x2c3d, + 0x8c48,0xdf49,0x5743] + # Walk through all possible hash values (mod size of hash table) + for offset = 0:1023 + local d2 = ODict{MyInt, Int}() + hashoffset[] = offset + for i = 1:length(badKeys) + d2[MyInt(badKeys[i])] = i + end + # Check all keys for missing values + for i = 1:length(badKeys) + @test d2[MyInt(badKeys[i])] == i + end + end + end +end + +# #18213 +ODict(1 => rand(2,3), 'c' => "asdf") # just make sure this does not trigger a deprecation + +@testset "WeakKeyDict" begin + A = [1] + B = [2] + C = [3] + + # construction + wkd = WeakKeyDict() + wkd[A] = 2 + wkd[B] = 3 + wkd[C] = 4 + dd = convert(ODict{Any,Any},wkd) + @test WeakKeyDict(dd) == wkd + @test convert(WeakKeyDict{Any, Any}, dd) == wkd + @test isa(WeakKeyDict(dd), WeakKeyDict{Any,Any}) + @test WeakKeyDict(A=>2, B=>3, C=>4) == wkd + @test isa(WeakKeyDict(A=>2, B=>3, C=>4), WeakKeyDict{Array{Int,1},Int}) + @test WeakKeyDict(a=>i+1 for (i,a) in enumerate([A,B,C]) ) == wkd + @test WeakKeyDict([(A,2), (B,3), (C,4)]) == wkd + @test WeakKeyDict{typeof(A), Int64}(Pair(A,2), Pair(B,3), Pair(C,4)) == wkd + @test WeakKeyDict(Pair(A,2), Pair(B,3), Pair(C,4)) == wkd + D = [[4.0]] + @test WeakKeyDict(Pair(A,2), Pair(B,3), Pair(D,4.0)) isa WeakKeyDict{Any, Any} + @test isa(WeakKeyDict(Pair(A,2), Pair(B,3.0), Pair(C,4)), WeakKeyDict{Array{Int,1},Any}) + @test isa(WeakKeyDict(Pair(convert(Vector{Number}, A),2), Pair(B,3), Pair(C,4)), WeakKeyDict{Any,Int}) + @test copy(wkd) == wkd + + @test length(wkd) == 3 + @test !isempty(wkd) + res = pop!(wkd, C) + @test res == 4 + @test length(wkd) == 2 + res = pop!(wkd, C, 3) + @test res == 3 + @test C ∉ keys(wkd) + @test 4 ∉ values(wkd) + @test length(wkd) == 2 + @test !isempty(wkd) + wkd = filter!( p -> p.first != B, wkd) + @test B ∉ keys(wkd) + @test 3 ∉ values(wkd) + @test length(wkd) == 1 + @test WeakKeyDict(Pair(A, 2)) == wkd + @test !isempty(wkd) + + wkd = empty!(wkd) + @test wkd == empty(wkd) + @test typeof(wkd) == typeof(empty(wkd)) + @test length(wkd) == 0 + @test isempty(wkd) + @test isa(wkd, WeakKeyDict) + + @test_throws ArgumentError WeakKeyDict([1, 2, 3]) + + wkd = WeakKeyDict(A=>1) + @test delete!(wkd, A) == empty(wkd) + @test delete!(wkd, A) === wkd + + # issue #26939 + d26939 = WeakKeyDict() + d26939[big"1.0" + 1.1] = 1 + GC.gc() # make sure this doesn't segfault + + # WeakKeyDict does not convert keys on setting + @test_throws ArgumentError WeakKeyDict{Vector{Int},Any}([5.0]=>1) + wkd = WeakKeyDict(A=>2) + @test_throws ArgumentError get!(wkd, [2.0], 2) + @test_throws ArgumentError get!(wkd, [1.0], 2) # get! fails even if the key is only + # used for getting and not setting + + # WeakKeyDict does convert on getting + wkd = WeakKeyDict(A=>2) + @test keytype(wkd)==Vector{Int} + @test wkd[[1.0]] == 2 + @test haskey(wkd, [1.0]) + @test pop!(wkd, [1.0]) == 2 + @test get(()->3, wkd, [2.0]) == 3 + + # map! on values of WKD + wkd = WeakKeyDict(A=>2, B=>3) + map!(v->v-1, values(wkd)) + @test wkd == WeakKeyDict(A=>1, B=>2) + + # get! + wkd = WeakKeyDict(A=>2) + get!(wkd, B, 3) + @test wkd == WeakKeyDict(A=>2, B=>3) + get!(()->4, wkd, C) + @test wkd == WeakKeyDict(A=>2, B=>3, C=>4) + @test_throws ArgumentError get!(()->5, wkd, [1.0]) +end + +@testset "issue #19995, hash of dicts" begin + @test hash(ODict(ODict(1=>2) => 3, ODict(4=>5) => 6)) != hash(ODict(ODict(4=>5) => 3, ODict(1=>2) => 6)) + a = ODict(ODict(3 => 4, 2 => 3) => 2, ODict(1 => 2, 5 => 6) => 1) + b = ODict(ODict(1 => 2, 2 => 3, 5 => 6) => 1, ODict(3 => 4) => 2) + @test hash(a) != hash(b) +end + +mutable struct Foo_15776 + x::Vector{Pair{Tuple{Function, Vararg{Int}}, Int}} +end +@testset "issue #15776, convert for pair" begin + z = [Pair((+,1,5,7), 3), Pair((-,6,5,3,5,8), 1)] + f = Foo_15776(z) + @test f.x[1].first == (+, 1, 5, 7) + @test f.x[1].second == 3 + @test f.x[2].first == (-, 6, 5, 3, 5, 8) + @test f.x[2].second == 1 +end + +@testset "issue #18708 error type for dict constructor" begin + @test_throws UndefVarError ODict(x => y for x in 1:10) +end + +mutable struct Error19179 <: Exception +end + +@testset "issue #19179 throwing error in dict constructor" begin + @test_throws Error19179 ODict(i => throw(Error19179()) for i in 1:10) +end + +# issue #18090 +let + d = ODict(i => i^2 for i in 1:10_000) + z = zip(keys(d), values(d)) + for (pair, tupl) in zip(d, z) + @test pair[1] == tupl[1] && pair[2] == tupl[2] + end +end + +struct NonFunctionCallable end +(::NonFunctionCallable)(args...) = +(args...) + +@testset "ODict merge" begin + d1 = ODict("A" => 1, "B" => 2) + d2 = ODict("B" => 3.0, "C" => 4.0) + @test @inferred merge(d1, d2) == ODict("A" => 1, "B" => 3, "C" => 4) + # merge with combiner function + @test @inferred mergewith(+, d1, d2) == ODict("A" => 1, "B" => 5, "C" => 4) + @test @inferred mergewith(*, d1, d2) == ODict("A" => 1, "B" => 6, "C" => 4) + @test @inferred mergewith(-, d1, d2) == ODict("A" => 1, "B" => -1, "C" => 4) + @test @inferred mergewith(NonFunctionCallable(), d1, d2) == ODict("A" => 1, "B" => 5, "C" => 4) + @test foldl(mergewith(+), [d1, d2]; init=ODict{Union{},Union{}}()) == + ODict("A" => 1, "B" => 5, "C" => 4) + # backward compatibility + @test @inferred merge(+, d1, d2) == ODict("A" => 1, "B" => 5, "C" => 4) +end + +@testset "ODict merge!" begin + d1 = ODict("A" => 1, "B" => 2) + d2 = ODict("B" => 3, "C" => 4) + @inferred merge!(d1, d2) + @test d1 == ODict("A" => 1, "B" => 3, "C" => 4) + # merge! with combiner function + @inferred mergewith!(+, d1, d2) + @test d1 == ODict("A" => 1, "B" => 6, "C" => 8) + @inferred mergewith!(*, d1, d2) + @test d1 == ODict("A" => 1, "B" => 18, "C" => 32) + @inferred mergewith!(-, d1, d2) + @test d1 == ODict("A" => 1, "B" => 15, "C" => 28) + @inferred mergewith!(NonFunctionCallable(), d1, d2) + @test d1 == ODict("A" => 1, "B" => 18, "C" => 32) + @test foldl(mergewith!(+), [d1, d2]; init=empty(d1)) == + ODict("A" => 1, "B" => 21, "C" => 36) + # backward compatibility + merge!(+, d1, d2) + @test d1 == ODict("A" => 1, "B" => 21, "C" => 36) +end + +@testset "ODict reduce merge" begin + function check_merge(i::Vector{<:ODict}, o) + r1 = reduce(merge, i) + r2 = merge(i...) + t = typeof(o) + @test r1 == o + @test r2 == o + @test typeof(r1) == t + @test typeof(r2) == t + end + check_merge([ODict(1=>2), ODict(1.0=>2.0)], ODict(1.0=>2.0)) + check_merge([ODict(1=>2), ODict(2=>Complex(1.0, 1.0))], + ODict(2=>Complex(1.0, 1.0), 1=>Complex(2.0, 0.0))) + check_merge([ODict(1=>2), ODict(3=>4)], ODict(3=>4, 1=>2)) + check_merge([ODict(3=>4), ODict(:a=>5)], ODict(:a => 5, 3 => 4)) +end + +@testset "misc error/io" begin + d = ODict('a'=>1, 'b'=>1, 'c'=> 3) + @test_throws ErrorException 'a' in d + key_str = sprint(show, keys(d)) + @test 'a' ∈ key_str + @test 'b' ∈ key_str + @test 'c' ∈ key_str +end + +@testset "ODict pop!" begin + d = ODict(1=>2, 3=>4) + @test pop!(d, 1) == 2 + @test_throws KeyError pop!(d, 1) + @test pop!(d, 1, 0) == 0 + @test pop!(d) == (3=>4) + @test_throws ArgumentError pop!(d) +end + +@testset "keys as a set" begin + d = ODict(1=>2, 3=>4) + @test keys(d) isa AbstractSet + @test empty(keys(d)) isa AbstractSet + let i = keys(d) ∩ Set([1,2]) + @test i isa AbstractSet + @test i == Set([1]) + end + @test Set(string(k) for k in keys(d)) == Set(["1","3"]) +end + +@testset "find" begin + @test findall(isequal(1), ODict(:a=>1, :b=>2)) == [:a] + @test sort(findall(isequal(1), ODict(:a=>1, :b=>1))) == [:a, :b] + @test isempty(findall(isequal(1), ODict())) + @test isempty(findall(isequal(1), ODict(:a=>2, :b=>3))) + + @test findfirst(isequal(1), ODict(:a=>1, :b=>2)) == :a + @test findfirst(isequal(1), ODict(:a=>1, :b=>1, :c=>3)) in (:a, :b) + @test findfirst(isequal(1), ODict()) === nothing + @test findfirst(isequal(1), ODict(:a=>2, :b=>3)) === nothing +end + +#== seemingly not related to ODict: + +@testset "ODict printing with limited rows" begin + local buf + buf = IOBuffer() + io = IOContext(buf, :displaysize => (4, 80), :limit => true) + d = Base.ImmutableDict(1=>2) + show(io, MIME"text/plain"(), d) + @test String(take!(buf)) == "Base.ImmutableDict{$Int, $Int} with 1 entry: …" + show(io, MIME"text/plain"(), keys(d)) + @test String(take!(buf)) == + "KeySet for a Base.ImmutableDict{$Int, $Int} with 1 entry. Keys: …" + + io = IOContext(io, :displaysize => (5, 80)) + show(io, MIME"text/plain"(), d) + @test String(take!(buf)) == "Base.ImmutableDict{$Int, $Int} with 1 entry:\n 1 => 2" + show(io, MIME"text/plain"(), keys(d)) + @test String(take!(buf)) == + "KeySet for a Base.ImmutableDict{$Int, $Int} with 1 entry. Keys:\n 1" + d = Base.ImmutableDict(d, 3=>4) + show(io, MIME"text/plain"(), d) + @test String(take!(buf)) == "Base.ImmutableDict{$Int, $Int} with 2 entries:\n ⋮ => ⋮" + show(io, MIME"text/plain"(), keys(d)) + @test String(take!(buf)) == + "KeySet for a Base.ImmutableDict{$Int, $Int} with 2 entries. Keys:\n ⋮" + + io = IOContext(io, :displaysize => (6, 80)) + show(io, MIME"text/plain"(), d) + @test String(take!(buf)) == + "Base.ImmutableDict{$Int, $Int} with 2 entries:\n 3 => 4\n 1 => 2" + show(io, MIME"text/plain"(), keys(d)) + @test String(take!(buf)) == + "KeySet for a Base.ImmutableDict{$Int, $Int} with 2 entries. Keys:\n 3\n 1" + d = Base.ImmutableDict(d, 5=>6) + show(io, MIME"text/plain"(), d) + @test String(take!(buf)) == + "Base.ImmutableDict{$Int, $Int} with 3 entries:\n 5 => 6\n ⋮ => ⋮" + show(io, MIME"text/plain"(), keys(d)) + @test String(take!(buf)) == + "KeySet for a Base.ImmutableDict{$Int, $Int} with 3 entries. Keys:\n 5\n ⋮" +end +==# + +@testset "copy!" begin + s = ODict(1=>2, 2=>3) + for a = ([3=>4], [0x3=>0x4], [3=>4, 5=>6, 7=>8], Pair{UInt,UInt}[3=>4, 5=>6, 7=>8]) + @test s === copy!(s, ODict(a)) == ODict(a) + if length(a) == 1 # current limitation of Base.ImmutableDict + @test s === copy!(s, Base.ImmutableDict(a[])) == ODict(a[]) + end + end +end + +@testset "map!(f, values(dict))" begin + @testset "AbstractDict & Fallback" begin + mutable struct TestDict{K, V} <: AbstractDict{K, V} + dict::ODict{K, V} + function TestDict(args...) + d = ODict(args...) + new{keytype(d), valtype(d)}(d) + end + end + Base.setindex!(td::TestDict, args...) = setindex!(td.dict, args...) + Base.getindex(td::TestDict, args...) = getindex(td.dict, args...) + Base.pairs(D::TestDict) = pairs(D.dict) + testdict = TestDict(:a=>1, :b=>2) + map!(v->v-1, values(testdict)) + @test testdict[:a] == 0 + @test testdict[:b] == 1 + end + @testset "ODict" begin + testdict = ODict(:a=>1, :b=>2) + map!(v->v-1, values(testdict)) + @test testdict[:a] == 0 + @test testdict[:b] == 1 + end +end From 9d29817963a6a4e1a60b1e6f765a7b5d641cbf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 20:33:24 +0000 Subject: [PATCH 09/10] Move from abstractdict.jl --- base/abstractdict.jl | 18 ------------------ base/swiss_dict.jl | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/base/abstractdict.jl b/base/abstractdict.jl index 650a45435dffa..62bb3b8cf5a2e 100644 --- a/base/abstractdict.jl +++ b/base/abstractdict.jl @@ -361,24 +361,6 @@ function _typeddict(d::AbstractDict, others::AbstractDict...) Dict{K,V}(d) end -#TODO: better way to do, than copy and change from above: -mergewith(combine, d::ODict, others::AbstractDict...) = - mergewith!(combine, _typeddict(d, others...), others...) -#mergewith(combine) = (args...) -> mergewith(combine, args...) -merge(combine::Base.Callable, d::ODict, others::AbstractDict...) = - merge!(combine, _typeddict(d, others...), others...) - -function _typeddict(d::ODict, others::AbstractDict...) - K = promoteK(keytype(d), others...) - V = promoteV(valtype(d), others...) - ODict{K,V}(d) -end - -# Not needed? -merge(d::ODict, others::AbstractDict...) = - merge!(_typeddict(d, others...), others...) - - """ filter!(f, d::AbstractDict) diff --git a/base/swiss_dict.jl b/base/swiss_dict.jl index 1f68ad03bf7e8..8761b999552c0 100644 --- a/base/swiss_dict.jl +++ b/base/swiss_dict.jl @@ -652,3 +652,25 @@ Base.@propagate_inbounds function Base.iterate(v::Union{KeySet{<:Any, <:ODict}, i, s = is return (v isa KeySet ? v.dict.keys[i] : v.dict.vals[i], s) end + + +#moved from abstractdict.jl (where I first added): + +#TODO: better way to do, than copy and change similar in +#abstractdict.jl that shouldn't either be there? +mergewith(combine, d::ODict, others::AbstractDict...) = + mergewith!(combine, _typeddict(d, others...), others...) +#mergewith(combine) = (args...) -> mergewith(combine, args...) +merge(combine::Base.Callable, d::ODict, others::AbstractDict...) = + merge!(combine, _typeddict(d, others...), others...) + +function _typeddict(d::ODict, others::AbstractDict...) + K = promoteK(keytype(d), others...) + V = promoteV(valtype(d), others...) + ODict{K,V}(d) +end + +# Not needed? +merge(d::ODict, others::AbstractDict...) = + merge!(_typeddict(d, others...), others...) + From dbde2c8d4d522992f2ff50f57d523b6953709141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1ll=20Haraldsson?= Date: Sun, 25 Oct 2020 21:12:29 +0000 Subject: [PATCH 10/10] Actually enable the odict.jl test file... --- test/choosetests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/choosetests.jl b/test/choosetests.jl index 887814c6bab0a..c75f04ac2d6e0 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -40,7 +40,7 @@ function choosetests(choices = []) "arrayops", "tuple", "reduce", "reducedim", "abstractarray", "intfuncs", "simdloop", "vecelement", "rational", "bitarray", "copy", "math", "fastmath", "functional", "iterators", - "operators", "ordering", "path", "ccall", "parse", "loading", "gmp", + "odict", "operators", "ordering", "path", "ccall", "parse", "loading", "gmp", "sorting", "spawn", "backtrace", "exceptions", "file", "read", "version", "namedtuple", "mpfr", "broadcast", "complex", @@ -146,7 +146,7 @@ function choosetests(choices = []) end end end - filter!(x -> (x != "stdlib" && !(x in STDLIBS)) , tests) + filter!(x -> (x != "stdlib" && !(x in STDLIBS)), tests) append!(tests, new_tests) explicit_pkg3 || filter!(x -> x != "Pkg/pkg", tests) explicit_libgit2 || filter!(x -> x != "LibGit2/online", tests)