diff --git a/src/Dimensions/dimension.jl b/src/Dimensions/dimension.jl index 38d07a631..74cdc620b 100644 --- a/src/Dimensions/dimension.jl +++ b/src/Dimensions/dimension.jl @@ -554,3 +554,20 @@ mean(A; dims=Ti) @dim Ti TimeDim "Time" const Time = Ti # For some backwards compat + +function Base.convert(::Type{D1}, dim::D2) where {D1<:Dimension{T},D2} where T + basetypeof(D2) <: basetypeof(D1) || + throw(ArgumentError("Cannot convert $D1 to $D2")) + rebuild(dim, convert(T, val(dim))) +end + +function Base.promote_rule( + ::Type{D1}, ::Type{D2} +) where {D1<:Dimension{T1},D2<:Dimension{T2}} where {T1,T2} + T = promote_type(T1, T2) + if basetypeof(D1) == basetypeof(D2) + basetypeof(D1){T} + else + Dimension{T} + end +end \ No newline at end of file diff --git a/src/Lookups/lookup_arrays.jl b/src/Lookups/lookup_arrays.jl index 9f69dc1cb..1d2a0b25e 100644 --- a/src/Lookups/lookup_arrays.jl +++ b/src/Lookups/lookup_arrays.jl @@ -1,4 +1,3 @@ - """ Lookup @@ -159,11 +158,15 @@ NoLookup() = NoLookup(AutoValues()) rebuild(l::NoLookup; data=parent(l), kw...) = NoLookup(data) +Base.convert(::Type{L1}, lookup::Lookup) where {L1<:NoLookup{A}} where A = + NoLookup(convert(A, axes(lookup, 1))) + # Used in @d broadcasts struct Length1NoLookup <: AbstractNoLookup end Length1NoLookup(::AbstractVector) = Length1NoLookup() rebuild(l::Length1NoLookup; kw...) = Length1NoLookup() + Base.parent(::Length1NoLookup) = Base.OneTo(1) """ @@ -176,7 +179,7 @@ is provided by this package. `AbstractSampled` must have `order`, `span` and `sampling` fields, or a `rebuild` method that accepts them as keyword arguments. """ -abstract type AbstractSampled{T,O<:Order,Sp<:Span,Sa<:Sampling} <: Aligned{T,O} end +abstract type AbstractSampled{T,A,O<:Order,Sp<:Span,Sa<:Sampling,M} <: Aligned{T,O} end span(lookup::AbstractSampled) = lookup.span sampling(lookup::AbstractSampled) = lookup.sampling @@ -192,6 +195,24 @@ function Base.:(==)(l1::AbstractSampled, l2::AbstractSampled) parent(l1) == parent(l2) end +function Base.promote_rule( + ::Type{S1}, ::Type{S2} +) where {S1<:AbstractSampled{T1,A1,O1,Sp1,Sa1,M1}, + S2<:AbstractSampled{T2,A2,O2,Sp2,Sa2,M2} +} where {T1,A1,O1,Sp1,Sa1,M1,T2,A2,O2,Sp2,Sa2,M2} + A = promote_type(A1, A2) + T = eltype(A) + O = promote_type(O1, O2) + Sp = promote_type(Sp1, Sp2) + Sa = promote_type(Sa1, Sa2) + M = promote_type(M1, M2) + if basetypeof(S1) == basetypeof(S2) + basetypeof(S1){T,A,O,Sp,Sa,M} + else + AbstractSampled{T,A,O,Sp,Sa,M} + end +end + for f in (:getindex, :view, :dotview) @eval begin # span may need its step size or bounds updated @@ -306,7 +327,7 @@ A = ones(x, y) 20 1.0 1.0 1.0 1.0 ``` """ -struct Sampled{T,A<:AbstractVector{T},O,Sp,Sa,M} <: AbstractSampled{T,O,Sp,Sa} +struct Sampled{T,A<:AbstractVector{T},O,Sp,Sa,M} <: AbstractSampled{T,A,O,Sp,Sa,M} data::A order::O span::Sp @@ -326,6 +347,17 @@ function rebuild(l::Sampled; Sampled(data, order, span, sampling, metadata) end +function Base.convert( + ::Type{S1}, lookup::AbstractSampled +) where {S1<:Sampled{T,A,O,Sp,Sa,M}} where {T,A,O,Sp,Sa,M} + Sampled(convert(A, parent(lookup)); + order=order(lookup), + span=span(lookup), + sampling=sampling(lookup), + metadata=convert(M, metadata(lookup)), + ) +end + # These are used to specialise dispatch: # When Cycling, we need to modify any `Selector`. After that # we switch to `NotCycling` and use `AbstractSampled` fallbacks. @@ -342,7 +374,7 @@ An abstract supertype for cyclic lookups. These are `AbstractSampled` lookups that are cyclic for `Selectors`. """ -abstract type AbstractCyclic{X,T,O,Sp,Sa} <: AbstractSampled{T,O,Sp,Sa} end +abstract type AbstractCyclic{X,T,A,O,Sp,Sa,M} <: AbstractSampled{T,A,O,Sp,Sa,M} end cycle(l::AbstractCyclic) = l.cycle cycle_status(l::AbstractCyclic) = l.cycle_status @@ -418,7 +450,7 @@ $SAMPLED_ARGUMENTS_DOC leap years breaking correct date cycling of a single year. If you actually need this behaviour, please make a GitHub issue. """ -struct Cyclic{X,T,A<:AbstractVector{T},O,Sp,Sa,M,C} <: AbstractCyclic{X,T,O,Sp,Sa} +struct Cyclic{X,T,A<:AbstractVector{T},O,Sp,Sa,M,C} <: AbstractCyclic{X,T,A,O,Sp,Sa,M} data::A order::O span::Sp @@ -464,18 +496,31 @@ But this can easily be extended, all methods are defined for `AbstractCategorica All `AbstractCategorical` must provide a `rebuild` method with `data`, `order` and `metadata` keyword arguments. """ -abstract type AbstractCategorical{T,O} <: Aligned{T,O} end +abstract type AbstractCategorical{T,A,O,M} <: Aligned{T,O} end order(lookup::AbstractCategorical) = lookup.order metadata(lookup::AbstractCategorical) = lookup.metadata const CategoricalEltypes = Union{AbstractChar,Symbol,AbstractString} +function Base.:(==)(l1::AbstractCategorical, l2::AbstractCategorical) + order(l1) == order(l2) && parent(l1) == parent(l2) +end + +function Base.promote_rule(::Type{S1}, ::Type{S2}) where { + S1<:AbstractCategorical{T1,A1,O1,M1}, S2<:AbstractCategorical{T2,A2,O2,M2} +} where {T1,A1,O1,M1,T2,A2,O2,M2} + T = promote_type(T1, T2) + A = promote_type(A1, A2) + O = promote_type(O1, O2) + M = promote_type(M1, M2) + AbstractCategorical{T,A,O,M} +end + function Adapt.adapt_structure(to, l::AbstractCategorical) rebuild(l; data=Adapt.adapt(to, parent(l)), metadata=NoMetadata()) end - """ Categorical <: AbstractCategorical @@ -533,10 +578,22 @@ function rebuild(l::Categorical; Categorical(data, order, metadata) end -function Base.:(==)(l1::AbstractCategorical, l2::AbstractCategorical) - order(l1) == order(l2) && parent(l1) == parent(l2) +function Base.promote_rule(::Type{S1}, ::Type{S2}) where { + S1<:Categorical{T1,A1,O1,M1}, S2<:Categorical{T2,A2,O2,M2} +} where {T1,A1,O1,M1,T2,A2,O2,M2} + T = promote_type(T1, T2) + A = promote_type(A1, A2) + O = promote_type(O1, O2) + M = promote_type(M1, M2) + Categorical{T,A,O,M} +end +function Base.convert(::Type{S1}, lookup::AbstractCategorical) where { + S1<:Categorical{T,A,O,M} +} where {T,A,O,M} + Categorical(convert(A, parent(lookup)); + order=order(lookup), metadata=convert(M, metadata(lookup)), + ) end - """ Unaligned <: Lookup @@ -971,4 +1028,4 @@ function promote_first(a1::AbstractArray, as::AbstractArray...) end return convert(C, a1) -end +end \ No newline at end of file diff --git a/src/Lookups/lookup_traits.jl b/src/Lookups/lookup_traits.jl index 1325072a5..b33e1fbed 100644 --- a/src/Lookups/lookup_traits.jl +++ b/src/Lookups/lookup_traits.jl @@ -234,6 +234,10 @@ val(span::Regular) = span.step Base.step(span::Regular) = span.step Base.:(==)(l1::Regular, l2::Regular) = val(l1) == val(l2) +Base.promote_rule(::Type{<:Regular{T1}}, ::Type{<:Regular{T2}}) where {T1,T2}= + Regular{promote_type(T1, T2)} +Base.convert(::Type{<:Regular{T1}}, span::Regular{T2}) where {T1,T2} = + Regular(convert(T1, val(span))) """ Irregular <: Span @@ -256,6 +260,13 @@ bounds(span::Irregular) = span.bounds val(span::Irregular) = span.bounds Base.:(==)(l1::Irregular, l2::Irregular) = val(l1) == val(l2) +function Base.promote_rule( + ::Type{<:Irregular{<:Tuple{T1,T2}}}, ::Type{<:Irregular{<:Tuple{T3,T4}}} +) where {T1,T2,T3,T4} + Irregular{Tuple{promote_type(T1, T3), promote_type(T2, T4)}} +end +Base.convert(::Type{Irregular{Tuple{T1,T2}}}, s::Irregular{Tuple{<:Any,<:Any}}) where {T1,T2} = + Irregular(convert(Tuple{T1,T2}, val(s))) """ Explicit(bounds::AbstractMatrix) @@ -272,6 +283,10 @@ Explicit() = Explicit(AutoBounds()) val(span::Explicit) = span.val Base.:(==)(l1::Explicit, l2::Explicit) = val(l1) == val(l2) +Base.promote_rule(::Type{<:Explicit{<:B1}}, ::Type{<:Explicit{<:B2}}) where {B1,B2} = + Explicit{promote_type(B1, B2)} +Base.convert(::Type{<:Explicit{<:B1}}, ::Explicit{<:B2}) where {B1,B2} = + Explicit{promote_type(B1, B2)} Adapt.adapt_structure(to, s::Explicit) = Explicit(Adapt.adapt_structure(to, val(s))) diff --git a/src/Lookups/metadata.jl b/src/Lookups/metadata.jl index 0e3250de3..5664d9a6b 100644 --- a/src/Lookups/metadata.jl +++ b/src/Lookups/metadata.jl @@ -1,6 +1,6 @@ """ - AbstractMetadata{X,T} + AbstractMetadata{X,K,V,T} Abstract supertype for all metadata wrappers. @@ -11,11 +11,15 @@ or simply saving data back to the same file type with identical metadata. Using a wrapper instead of `Dict` or `NamedTuple` also lets us pass metadata objects to [`set`](@ref) without ambiguity about where to put them. """ -abstract type AbstractMetadata{X,T} end +abstract type AbstractMetadata{X,K,V,T} <: AbstractDict{K,V} end -const _MetadataContents = Union{AbstractDict,NamedTuple} +const MetadataContents = Union{AbstractDict,NamedTuple} +const DefaultDict = Dict{Symbol,Any} const AllMetadata = Union{AbstractMetadata,AbstractDict} +valtype(::AbstractMetadata{<:Any,<:Any,<:Any,T}) where T = T +valtype(::Type{<:AbstractMetadata{<:Any,<:Any,<:Any,T}})where T = T + Base.get(m::AbstractMetadata, args...) = get(val(m), args...) Base.getindex(m::AbstractMetadata, key) = getindex(val(m), key) Base.setindex!(m::AbstractMetadata, x, key) = setindex!(val(m), x, key) @@ -38,20 +42,24 @@ Base.:(==)(m1::AbstractMetadata, m2::AbstractMetadata) = m1 isa typeof(m2) && va General [`Metadata`](@ref) object. The `X` type parameter categorises the metadata for method dispatch, if required. """ -struct Metadata{X,T<:_MetadataContents} <: AbstractMetadata{X,T} +struct Metadata{X,T<:MetadataContents,K,V} <: AbstractMetadata{X,T,K,V} val::T end -Metadata(val::T) where {T<:_MetadataContents} = Metadata{Nothing,T}(val) -Metadata{X}(val::T) where {X,T<:_MetadataContents} = Metadata{X,T}(val) +Metadata{X,T}(val::T) where {X,T<:NamedTuple} = + Metadata{X,T,Symbol,Any}(val) +Metadata{X,T}(val::T) where {X,T<:AbstractDict{K,V}} where {K,V} = + Metadata{X,T,K,V}(val) +Metadata(val::T) where {T<:MetadataContents} = Metadata{Nothing,T}(val) +Metadata{X}(val::T) where {X,T<:MetadataContents} = Metadata{X,T}(val) # NamedTuple/Dict constructor -# We have to combine these because the no-arg method is overwritten by empty kw. -function (::Type{M})(ps...; kw...) where M <: Metadata - if length(ps) > 0 && length(kw) > 0 - throw(ArgumentError("Metadata can be constructed with args of Pair to make a Dict, or kw for a NamedTuple. But not both.")) - end - length(kw) > 0 ? M((; kw...)) : M(Dict(ps...)) +(::Type{M})(p1::Pair, ps::Pair...) where M <: Metadata = M(Dict(p1, ps...)) +function (::Type{M})(; kw...) where M <: Metadata + M((; kw...)) end +Metadata() = Metadata(DefaultDict()) +Metadata{X}() where X = Metadata{X}(DefaultDict()) +Metadata{X,T}() where {X,T} = Metadata{X,T}(T()) ConstructionBase.constructorof(::Type{<:Metadata{X}}) where {X} = Metadata{X} @@ -74,14 +82,28 @@ Indicates an object has no metadata. But unlike using `nothing`, returning the fallback argument. `keys` returns `()` while `haskey` always returns `false`. """ -struct NoMetadata <: AbstractMetadata{Nothing,NamedTuple{(),Tuple{}}} end +struct NoMetadata <: AbstractMetadata{Nothing,Dict{Symbol,Any},Symbol,Any} end val(m::NoMetadata) = NamedTuple() Base.keys(::NoMetadata) = () -Base.haskey(::NoMetadata, args...) = false Base.get(::NoMetadata, key, fallback) = fallback Base.length(::NoMetadata) = 0 +Base.convert(::Type{NoMetadata}, s::Union{NamedTuple,AbstractMetadata,AbstractDict}) = + NoMetadata() + +Base.convert(::Type{Metadata}, ::NoMetadata) = Metadata() +Base.convert(::Type{Metadata}, m::MetadataContents) = Metadata(m) +Base.convert(::Type{Metadata{X}}, m::MetadataContents) where X = Metadata{X}(m) +Base.convert(::Type{Metadata{X,T}}, m::AbstractDict) where {X,T<:AbstractDict} = + Metadata{X,T}(T(m)) +Base.convert(::Type{Metadata{X,T}}, m::NamedTuple) where {X,T<:AbstractDict} = + Metadata{X,T}(T(metadatadict(m))) +Base.convert(::Type{Metadata{X,T}}, m::NamedTuple) where {X,T<:NamedTuple} = + Metadata{X,T}(T(m)) +Base.convert(::Type{Metadata{X,T}}, m::AbstractDict) where {X,T<:NamedTuple} = + Metadata{X,T}(T(pairs(metadatadict(m)))) + function Base.show(io::IO, mime::MIME"text/plain", metadata::Metadata{N}) where N print(io, "Metadata") @@ -96,12 +118,13 @@ end # Metadata utils -function metadatadict(dict) - symboldict = Dict{Symbol,Any}() - for (k, v) in dict +metadatadict(dict) = metadatadict(DefaultDict, dict) +function metadatadict(::Type{T}, dict) where T + symboldict = T() + for (k, v) in pairs(dict) symboldict[Symbol(k)] = v end - symboldict + return symboldict end metadata(x) = NoMetadata() @@ -110,4 +133,4 @@ units(x) = units(metadata(x)) units(m::NoMetadata) = nothing units(m::Nothing) = nothing units(m::Metadata) = get(m, :units, nothing) -units(m::AbstractDict) = get(m, :units, nothing) +units(m::AbstractDict) = get(m, :units, nothing) \ No newline at end of file diff --git a/src/array/array.jl b/src/array/array.jl index d3e054c28..cb11084a9 100644 --- a/src/array/array.jl +++ b/src/array/array.jl @@ -38,6 +38,7 @@ Base.checkbounds(::Type{Bool}, A::AbstractBasicDimArray, d1::IDim, dims::IDim... Base.checkbounds(A::AbstractBasicDimArray, d1::IDim, dims::IDim...) = Base.checkbounds(A, dims2indices(A, (d1, dims...))...) + """ AbstractDimArray <: AbstractBasicArray @@ -464,7 +465,7 @@ function DimArray(A::AbstractDimArray; ) DimArray(data, dims; refdims, name, metadata) end -DimArray{T}(A::AbstractDimArray; kw...) where T = DimArray(convert.(T, A)) +DimArray{T}(A::AbstractDimArray; kw...) where T = DimArray(convert.(T, A); kw...) DimArray{T}(A::AbstractDimArray{T}; kw...) where T = DimArray(A; kw...) # We collect other kinds of AbstractBasicDimArray # to avoid complicated nesting of dims @@ -485,7 +486,7 @@ function DimArray(f::Function, dim::Dimension; name=Symbol(nameof(f), "(", name( DimArray(f.(val(dim)), (dim,); name) end -DimArray(itr::Base.Generator; kwargs...) = rebuild(collect(itr); kwargs...) +DimArray(itr::Base.Generator; kw...) = rebuild(collect(itr); kw...) const DimVector = DimArray{T,1} where T const DimMatrix = DimArray{T,2} where T @@ -501,6 +502,31 @@ DimMatrix(A::AbstractMatrix, args...; kw...) = DimArray(A, args...; kw...) Base.convert(::Type{DimArray}, A::AbstractDimArray) = DimArray(A) Base.convert(::Type{DimArray{T}}, A::AbstractDimArray) where {T} = DimArray{T}(A) +function Base.convert( + ::Type{<:DimArray{T,N,DT,RT,AT,NaT,MeT}}, a::DimArray{S,N} +) where {T,N,S,DT,RT,AT,NaT,MeT} + rebuild(a; + data=convert(AT, parent(a)), + dims=convert(DT, dims(a)), + name=convert(NaT, name(a)), + refdims=RT <: Tuple{} ? () : convert(RT, refdims(a)), + metadata=convert(MeT, metadata(a)), + ) +end + +# Promote element types +function Base.promote_rule( + a::Type{<:DimArray{T,N,DT,RT,AT,NaT,MeT}}, b::Type{<:DimArray{S,N,DS,RS,AS,NaS,MeS}} +) where {T,S,N,DT,DS,RT,RS,AT,AS,NaT,NaS,MeT,MeS} + A = promote_type(AT, AS) + TS = eltype(A) + D = promote_type(DT, DS) + R = RT <: Tuple{} || RS <: Tuple{} ? Tuple{} : promote_type(RT, RS) + Na = promote_type(NaT, NaS) + M = promote_type(MeT, MeS) + return DimArray{TS,N,D,R,A,Na,M} +end + checkdims(A::AbstractArray{<:Any,N}, dims::Tuple) where N = checkdims(N, dims) checkdims(::Type{<:AbstractArray{<:Any,N}}, dims::Tuple) where N = checkdims(N, dims) checkdims(n::Integer, dims::Tuple) = length(dims) == n || _dimlengtherror(n, length(dims)) diff --git a/src/array/broadcast.jl b/src/array/broadcast.jl index 57f4966d0..b136656fa 100644 --- a/src/array/broadcast.jl +++ b/src/array/broadcast.jl @@ -77,7 +77,7 @@ function Broadcast.copy(bc::Broadcasted{DimensionalStyle{S}}) where S # unwrap AbstractDimArray data data = data isa AbstractDimArray ? parent(data) : data dims = format(Dimensions.promotedims(bdims...; skip_length_one=true), data) - return rebuild(A; data, dims, refdims=refdims(A), name=Symbol("")) + return rebuild(A; data, dims, refdims=refdims(A), name=_noname(name(A))) end function Base.copyto!(dest::AbstractArray, bc::Broadcasted{DimensionalStyle{S}}) where S @@ -96,7 +96,7 @@ end function Base.similar(bc::Broadcast.Broadcasted{DimensionalStyle{S}}, ::Type{T}) where {S,T} A = _firstdimarray(bc) - rebuildsliced(A, similar(_unwrap_broadcasted(bc), T, axes(bc)...), axes(bc), Symbol("")) + rebuildsliced(A, similar(_unwrap_broadcasted(bc), T, axes(bc)...), axes(bc), _noname(name(A))) end diff --git a/src/name.jl b/src/name.jl index b47fed9eb..2b025b5c2 100644 --- a/src/name.jl +++ b/src/name.jl @@ -39,3 +39,18 @@ Base.Symbol(::Name{X}) where X = X Base.string(::Name{X}) where X = string(X) name(x::Name) = x + + +Base.convert(::Type{NoName}, s::Symbol) = NoName() +Base.convert(::Type{Symbol}, ::NoName) = Symbol("") +# TODO should we check that X and s match? +Base.convert(::Type{Name{X}}, s::Symbol) where X = Name{X}() +Base.convert(::Type{Name}, s::Symbol) = Name{s}() +Base.convert(::Type{Symbol}, x::Name{X}) where X = X + +Base.promote_rule(::Type{NoName}, ::Type{Symbol}) = NoName +Base.promote_rule(::Type{NoName}, ::Type{<:Name}) = NoName +Base.promote_rule(::Type{Symbol}, ::Type{NoName}) = NoName +Base.promote_rule(::Type{<:Name}, ::Type{NoName}) = NoName +Base.promote_rule(::Type{<:Name}, ::Type{Symbol}) = Symbol +Base.promote_rule(::Type{Symbol}, ::Type{<:Name}) = Symbol \ No newline at end of file diff --git a/src/stack/stack.jl b/src/stack/stack.jl index 597297627..5bc55ae93 100644 --- a/src/stack/stack.jl +++ b/src/stack/stack.jl @@ -429,7 +429,7 @@ function DimStack(A::AbstractDimArray; layersfrom=nothing, metadata=metadata(A), refdims=refdims(A), kw... ) layers = if isnothing(layersfrom) - keys = name(A) in (NoName(), Symbol(""), Name(Symbol(""))) ? (:layer1,) : (name(A),) + keys = (_cleankey(name(A)),) NamedTuple{keys}((A,)) else keys = Tuple(_layerkeysfromdim(A, layersfrom)) diff --git a/test/array.jl b/test/array.jl index dc5aea74c..82c04c87f 100644 --- a/test/array.jl +++ b/test/array.jl @@ -1,5 +1,7 @@ -using DimensionalData, Test , Unitful, SparseArrays, Dates, Random -using DimensionalData: layerdims, checkdims +using DimensionalData, Test , Unitful, SparseArrays, Dates, Random, Statistics +using DimensionalData: layerdims, checkdims, Name, NoName +using DimensionalData.Lookups +using DimensionalData.Dimensions using LinearAlgebra using DimensionalData.Lookups, DimensionalData.Dimensions @@ -602,3 +604,59 @@ end @test Base.dataids(a) == Base.dataids(parent(a)) @test Base.mightalias(a, parent(a)) end + +#@testset "promotion" begin + a = rand(X(1:10)) + b = rand(Int, X(1:10)) + @test promote_type(typeof(a), typeof(b)) == typeof(a) + + M = fill(UInt16(32000), 2) + + x1 = X(1:2) + x2 = X(1.0:2.0) + T = promote_type(typeof(x1), typeof(x2)) + @test convert(T, x2) isa T + @test convert(T, x2) isa T + @test val(convert(T, x2)) === 1.0:1.0:2.0 + f1 = format(x1) + f2 = format(x2) + + P = promote_type(typeof(f1), typeof(f2)) + @test + typeof(convert(P, f1)) + typeof(f2) + @test convert(P, f2) isa typeof(f2) + + @test x2 isa promote_type(typeof(format(x1)), typeof(format(x2))) + + D = DimArray(M, x1) + D2 = DimArray(M, x1; name=:testname) + D3 = DimArray(M, x1; metadata=Metadata(:test => "test")) + D4 = DimArray(M, x2; metadata=Metadata(:test => "test")) + @test mean([D, D]) == + mean([D2, D2]) == + mean([D3, D3]) == + mean([D4, D4]) == + mean([M, M]) + @test mean([D, D]) == mean([D, D2]) == mean([D2, D]) + + @test typeof(convert(typeof(D), D2)) == typeof(D) + @test typeof(convert(typeof(D), D3)) == typeof(D) + D isa promote_type(typeof(D), typeof(D3)) + # @test + P = promote_type(typeof(D3), typeof(D)) + typeof(convert(P, D4)) <: P + typeof(D4) + <: P + @test typeof(convert(typeof(D2), D)) == typeof(D2) + @test typeof(convert(typeof(D3), D)) == typeof(D3) + @test + typeof(convert(typeof(D4), D)) + == + promote_type(typeof(D4), typeof(D)) + typeof(convert(typeof(D4), D)) + typeof(D4) + + @test promote_type(typeof(D), typeof(D2)) == + convert(typeof(D), D2) +end \ No newline at end of file