diff --git a/README.md b/README.md index 63f7ae13..d3b711b2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,15 @@ julia> room_temp = 100kPa 100000.0 m⁻¹ kg s⁻² ``` +Note that `Units` is an exported submodule, so you can +also access this as `Units.kPa`. You may like to define + +```julia +julia> const U = Units +``` + +so that you can simply write, say, `U.kPa` or `C.m_e`. + This supports a wide range of SI base and derived units, with common prefixes. @@ -169,6 +178,12 @@ julia> Constants.c 2.99792458e8 m s⁻¹ ``` +which you may like to define as + +```julia +julia> const C = Constants +``` + These can also be used inside the `u"..."` macro: ```julia @@ -176,6 +191,12 @@ julia> u"Constants.c * Hz" 2.99792458e8 m s⁻² ``` +Similarly, you can just import each individual constant: + +```julia +julia> using DynamicQuantities.Constants: h +``` + For the full list, see the [docs](https://symbolicml.org/DynamicQuantities.jl/dev/constants/). @@ -220,6 +241,22 @@ julia> uconvert(us"nm", 5e-9u"m") # can also write 5e-9u"m" |> uconvert(us"nm") 5.0 nm ``` +Finally, you can also import these directly: + +```julia +julia> using DynamicQuantities.SymbolicUnits: cm +``` + +or constants: + +```julia +julia> using DynamicQuantities.SymbolicConstants: h +``` + +Note that `SymbolicUnits` and `SymbolicConstants` are exported, +so you can simply access these as `SymbolicUnits.cm` and `SymbolicConstants.h`, +respectively. + ### Arrays For working with an array of quantities that have the same dimensions, diff --git a/docs/src/types.md b/docs/src/types.md index 2a0bc17d..65a82599 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -25,6 +25,17 @@ Another type which subtypes `AbstractDimensions` is `SymbolicDimensions`: SymbolicDimensions ``` +Just note that all of the symbolic units and constants are stored using the +immutable `SymbolicDimensionsSingleton`, which shares the same +supertype `AbstractSymbolicDimensions <: AbstractDimensions`. These get immediately +converted to the mutable `SymbolicDimensions` when used in any +calculation. + +```@docs +SymbolicDimensionsSingleton +AbstractSymbolicDimensions +``` + ## Arrays ```@docs @@ -40,6 +51,19 @@ which is subtyped to `Any`. ```@docs GenericQuantity AbstractGenericQuantity +``` + +In the other direction, there is also `RealQuantity`, +which is subtyped to `Real`. + +```@docs +RealQuantity +AbstractRealQuantity +``` + +More general, these are each contained in the following: + +```@docs UnionAbstractQuantity DynamicQuantities.ABSTRACT_QUANTITY_TYPES ``` diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 16f09420..a8252a1a 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -30,7 +30,7 @@ for (_, _, Q) in ABSTRACT_QUANTITY_TYPES validate_upreferred() cumulator = DynamicQuantities.ustrip(x) dims = DynamicQuantities.dimension(x) - if dims isa DynamicQuantities.SymbolicDimensions + if dims isa DynamicQuantities.AbstractSymbolicDimensions throw(ArgumentError("Conversion of a `DynamicQuantities." * string($Q) * "` to a `Unitful.Quantity` is not defined with dimensions of type `SymbolicDimensions`. Instead, you can first use the `uexpand` function to convert the dimensions to their base SI form of type `Dimensions`, then convert this quantity to a `Unitful.Quantity`.")) end equiv = unitful_equivalences() diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 9f49b04c..46e83662 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,8 +1,12 @@ module DynamicQuantities export Units, Constants, SymbolicUnits, SymbolicConstants -export AbstractDimensions, AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity -export Quantity, GenericQuantity, RealQuantity, Dimensions, SymbolicDimensions, QuantityArray, DimensionError +export AbstractQuantity, AbstractGenericQuantity, AbstractRealQuantity, UnionAbstractQuantity +export Quantity, GenericQuantity, RealQuantity +export AbstractDimensions, Dimensions, NoDims +export AbstractSymbolicDimensions, SymbolicDimensions, SymbolicDimensionsSingleton +export QuantityArray +export DimensionError export ustrip, dimension export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index a71df4cd..42f56286 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -99,15 +99,16 @@ dimension_names(::Type{<:AbstractSymbolicDimensions}) = ALL_SYMBOLS Base.propertynames(::AbstractSymbolicDimensions) = ALL_SYMBOLS Base.getindex(d::AbstractSymbolicDimensions, k::Symbol) = getproperty(d, k) constructorof(::Type{<:SymbolicDimensions}) = SymbolicDimensions -constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R} +constructorof(::Type{<:SymbolicDimensionsSingleton}) = SymbolicDimensionsSingleton with_type_parameters(::Type{<:SymbolicDimensions}, ::Type{R}) where {R} = SymbolicDimensions{R} with_type_parameters(::Type{<:SymbolicDimensionsSingleton}, ::Type{R}) where {R} = SymbolicDimensionsSingleton{R} nzdims(d::SymbolicDimensions) = getfield(d, :nzdims) nzdims(d::SymbolicDimensionsSingleton) = (getfield(d, :dim),) nzvals(d::SymbolicDimensions) = getfield(d, :nzvals) nzvals(::SymbolicDimensionsSingleton{R}) where {R} = (one(R),) -Base.eltype(::AbstractSymbolicDimensions{R}) where {R} = R -Base.eltype(::Type{<:AbstractSymbolicDimensions{R}}) where {R} = R + +# Need to construct with `R` if available, as can't figure it out otherwise: +constructorof(::Type{<:SymbolicDimensionsSingleton{R}}) where {R} = SymbolicDimensionsSingleton{R} # Conversion: function SymbolicDimensions(d::SymbolicDimensionsSingleton{R}) where {R} @@ -170,7 +171,7 @@ uexpand(q::QuantityArray) = uexpand.(q) Convert a quantity `q` with base SI units to the symbolic units of `qout`, for `q` and `qout` with compatible units. Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`. """ -function uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions}) @assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert." qout_expanded = uexpand(qout) dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded)) @@ -178,7 +179,7 @@ function uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimension new_dim = dimension(qout) return new_quantity(typeof(q), new_val, new_dim) end -function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions}) +function uconvert(qout::UnionAbstractQuantity{<:Any,<:SymbolicDimensions}, q::QuantityArray{<:Any,<:Any,<:Dimensions}) @assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert." qout_expanded = uexpand(qout) dimension(q) == dimension(qout_expanded) || throw(DimensionError(q, qout_expanded)) @@ -186,7 +187,39 @@ function uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions new_dim = dimension(qout) return QuantityArray(new_array, new_dim, quantity_type(q)) end -# TODO: Method for converting SymbolicDimensions -> SymbolicDimensions + +# Ensure we always do operations with SymbolicDimensions: +function uconvert( + qout::UnionAbstractQuantity{T,<:SymbolicDimensionsSingleton{R}}, + q::Union{ + <:UnionAbstractQuantity{<:Any,<:Dimensions}, + <:QuantityArray{<:Any,<:Any,<:Dimensions}, + }, +) where {T,R} + return uconvert( + convert( + with_type_parameters( + typeof(qout), + T, + with_type_parameters(SymbolicDimensions, R), + ), + qout, + ), + q, + ) +end + +# Allow user to convert SymbolicDimensions -> SymbolicDimensions +function uconvert( + qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions{R}}, + q::Union{ + <:UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}, + <:QuantityArray{<:Any,<:Any,<:AbstractSymbolicDimensions}, + }, +) where {R} + return uconvert(qout, uexpand(q)) +end + """ uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}) @@ -197,7 +230,7 @@ a function equivalent to `q -> uconvert(qout, q)`. uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout) Base.copy(d::SymbolicDimensions) = SymbolicDimensions(copy(nzdims(d)), copy(nzvals(d))) -Base.copy(d::SymbolicDimensionsSingleton) = constructorof(d)(getfield(d, :dim)) +Base.copy(d::SymbolicDimensionsSingleton) = constructorof(typeof(d))(getfield(d, :dim)) function Base.:(==)(l::AbstractSymbolicDimensions, r::AbstractSymbolicDimensions) nzdims_l = nzdims(l) @@ -336,6 +369,7 @@ to enable pretty-printing of units. module SymbolicUnits import ..UNIT_SYMBOLS + import ..CONSTANT_SYMBOLS import ..SymbolicDimensionsSingleton import ...constructorof import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE @@ -353,22 +387,34 @@ module SymbolicUnits import ....DEFAULT_VALUE_TYPE import ....DEFAULT_DIM_BASE_TYPE + const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] + for unit in CONSTANT_SYMBOLS - @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( - DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) - ) + @eval begin + const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit)))) + ) + push!(_SYMBOLIC_CONSTANT_VALUES, $unit) + end end + const SYMBOLIC_CONSTANT_VALUES = Tuple(_SYMBOLIC_CONSTANT_VALUES) end import .Constants import .Constants as SymbolicConstants + import .Constants: SYMBOLIC_CONSTANT_VALUES + const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[] for unit in UNIT_SYMBOLS - @eval const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( - DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) - ) + @eval begin + const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))) + ) + push!(_SYMBOLIC_UNIT_VALUES, $unit) + end end + const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES) """ @@ -387,18 +433,50 @@ module SymbolicUnits `Quantity(1.0, SymbolicDimensions, c=2, Hz=2)`. However, note that due to namespace collisions, a few physical constants are automatically converted. """ - function sym_uparse(raw_string::AbstractString) - raw_result = eval(Meta.parse(raw_string)) - return copy(as_quantity(raw_result))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE + function sym_uparse(s::AbstractString) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return copy(eval(ex))::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE end as_quantity(q::DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE) = q as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") + + function map_to_scope(ex::Expr) + if ex.head == :call + ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + return ex + elseif ex.head == :. && ex.args[1] == :Constants + @assert ex.args[2] isa QuoteNode + return lookup_constant(ex.args[2].value) + else + throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `SymbolicConstants`) are expected.")) + end + end + function map_to_scope(sym::Symbol) + if sym in UNIT_SYMBOLS + return lookup_unit(sym) + elseif sym in CONSTANT_SYMBOLS + throw(ArgumentError("Symbol $sym found in `SymbolicConstants` but not `SymbolicUnits`. Please access the `SymbolicConstants` module. For example, `u\"SymbolicConstants.$sym\"`.")) + else + throw(ArgumentError("Symbol $sym not found in `SymbolicUnits` or `SymbolicConstants`.")) + end + end + function map_to_scope(ex) + return ex + end + function lookup_unit(ex::Symbol) + i = findfirst(==(ex), UNIT_SYMBOLS)::Int + return as_quantity(SYMBOLIC_UNIT_VALUES[i]) + end + function lookup_constant(ex::Symbol) + i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int + return as_quantity(SYMBOLIC_CONSTANT_VALUES[i]) + end end -import .SymbolicUnits: sym_uparse -import .SymbolicUnits: SymbolicConstants +import .SymbolicUnits: as_quantity, sym_uparse, SymbolicConstants, map_to_scope """ us"[unit expression]" @@ -416,7 +494,9 @@ module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to namespace collisions, a few physical constants are automatically converted. """ macro us_str(s) - return esc(SymbolicUnits.sym_uparse(s)) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return esc(ex) end function Base.promote_rule(::Type{SymbolicDimensionsSingleton{R1}}, ::Type{SymbolicDimensionsSingleton{R2}}) where {R1,R2} diff --git a/src/types.jl b/src/types.jl index 68f519b0..02423100 100644 --- a/src/types.jl +++ b/src/types.jl @@ -122,6 +122,20 @@ end const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE} +""" + NoDims{R} + +A type representing the dimensions of a non-quantity. + +For any `getproperty` call on this type, the result is `zero(R)`. +""" +struct NoDims{R<:Real} <: AbstractDimensions{R} +end + +Base.getproperty(::NoDims{R}, ::Symbol) where {R} = zero(R) + +const DEFAULT_DIMENSIONLESS_TYPE = NoDims{DEFAULT_DIM_BASE_TYPE} + """ Quantity{T<:Number,D<:AbstractDimensions} <: AbstractQuantity{T,D} <: Number diff --git a/src/uparse.jl b/src/uparse.jl index e913d044..fa90c751 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -4,7 +4,8 @@ import ..constructorof import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE -import ..Units: UNIT_SYMBOLS +import ..Units: UNIT_SYMBOLS, UNIT_VALUES +import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants function _generate_units_import() @@ -34,11 +35,13 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ function uparse(s::AbstractString) - return as_quantity(eval(Meta.parse(s)))::DEFAULT_QUANTITY_TYPE + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return eval(ex)::DEFAULT_QUANTITY_TYPE end as_quantity(q::DEFAULT_QUANTITY_TYPE) = q -as_quantity(x::Number) = convert(DEFAULT_QUANTITY_TYPE, x) +as_quantity(x::Number) = convert(DEFAULT_QUANTITY_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") """ @@ -54,7 +57,41 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ macro u_str(s) - return esc(uparse(s)) + ex = map_to_scope(Meta.parse(s)) + ex = :($as_quantity($ex)) + return esc(ex) +end + +function map_to_scope(ex::Expr) + if ex.head == :call + ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + return ex + elseif ex.head == :. && ex.args[1] == :Constants + @assert ex.args[2] isa QuoteNode + return lookup_constant(ex.args[2].value) + else + throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `Constants`) are expected.")) + end +end +function map_to_scope(sym::Symbol) + if sym in UNIT_SYMBOLS + return lookup_unit(sym) + elseif sym in CONSTANT_SYMBOLS + throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please access the `Constants` module. For example, `u\"Constants.$sym\"`.")) + else + throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + end +end +function map_to_scope(ex) + return ex +end +function lookup_unit(ex::Symbol) + i = findfirst(==(ex), UNIT_SYMBOLS)::Int + return UNIT_VALUES[i] +end +function lookup_constant(ex::Symbol) + i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int + return CONSTANT_VALUES[i] end end diff --git a/src/utils.jl b/src/utils.jl index c7fde640..407b1552 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -28,6 +28,13 @@ end function Base.promote_rule(::Type{Dimensions{R1}}, ::Type{Dimensions{R2}}) where {R1,R2} return Dimensions{promote_type(R1,R2)} end +function Base.promote_rule(::Type{NoDims{R1}}, ::Type{NoDims{R2}}) where {R1,R2} + return NoDims{promote_type(R1,R2)} +end +function Base.promote_rule(::Type{NoDims{R1}}, ::Type{D}) where {R1,R2,D<:AbstractDimensions{R2}} + # The `R1` type is "unused" so we ignore it + return D +end # Define all the quantity x quantity promotion rules """ @@ -247,6 +254,7 @@ for f in ( @eval Base.$f(q::UnionAbstractQuantity) = $f(ustrip(q)) end Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) +Base.iszero(::NoDims) = true Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) @@ -340,12 +348,14 @@ ustrip(::AbstractDimensions) = error("Cannot remove units from an `AbstractDimen """ dimension(q::AbstractQuantity) dimension(q::AbstractGenericQuantity) + dimension(x) Get the dimensions of a quantity, returning an `AbstractDimensions` object. """ dimension(q::UnionAbstractQuantity) = q.dimensions dimension(d::AbstractDimensions) = d dimension(aq::AbstractArray{<:UnionAbstractQuantity}) = allequal(dimension.(aq)) ? dimension(first(aq)) : throw(DimensionError(aq[begin], aq[begin+1:end])) +dimension(_) = DEFAULT_DIMENSIONLESS_TYPE() """ ulength(q::AbstractQuantity) diff --git a/test/unittests.jl b/test/unittests.jl index 59b31bb0..57074e1a 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,9 +1,10 @@ using DynamicQuantities -using DynamicQuantities: FixedRational +using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using DynamicQuantities: array_type, value_type, dim_type, quantity_type using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value +using DynamicQuantities: map_dimensions using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using StaticArrays: SArray, MArray @@ -156,6 +157,8 @@ end @test abs(x) == abs(Quantity(1.2, length=2 // 5)) @test abs2(x) == Quantity(abs2(-1.2), length=4 // 5) + @test copy(x) == x + @test iszero(x) == false @test iszero(x * 0) == true @test isfinite(x) == true @@ -461,6 +464,7 @@ end z = u"yr" @test utime(z) == 1 @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 + @test z == uparse("yr") # Test type stability of extreme range of units @test typeof(u"1") == DEFAULT_QUANTITY_TYPE @@ -471,7 +475,17 @@ end @test typeof(u"fm") == DEFAULT_QUANTITY_TYPE @test typeof(u"fm"^2) == DEFAULT_QUANTITY_TYPE - @test_throws LoadError eval(:(u":x")) + @test_throws ErrorException eval(:(u":x")) + + VERSION >= v"1.9" && @test_throws "Symbol x not found" uparse("x") + VERSION >= v"1.9" && @test_throws "Symbol c found in `Constants` but not `Units`" uparse("c") + VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("import ..Units") + VERSION >= v"1.9" && @test_throws "Unexpected expression" uparse("(m, m)") + @test_throws LoadError eval(:(us"x")) + VERSION >= v"1.9" && @test_throws "Symbol x not found" sym_uparse("x") + VERSION >= v"1.9" && @test_throws "Symbol c found in `SymbolicConstants` but not `SymbolicUnits`" sym_uparse("c") + VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("import ..Units") + VERSION >= v"1.9" && @test_throws "Unexpected expression" sym_uparse("(m, m)") end @testset "Constants" begin @@ -684,6 +698,7 @@ end @inferred f2(5) @test uexpand(f2(5)) == u"s"^5 + @test_throws ErrorException uparse("'c'") @test_throws ErrorException sym_uparse("'c'") # For constants which have a namespace collision, the numerical expansion is used: @@ -724,6 +739,25 @@ end @test qa isa Vector{Quantity{Float64,SymbolicDimensions{Rational{Int}}}} DynamicQuantities.with_type_parameters(SymbolicDimensions{Float64}, Rational{Int}) == SymbolicDimensions{Rational{Int}} + # Many symbols in one: + x = us"pm * fm * nm * μm * mm * cm * dm * m * km" + y = us"s" + @test inv(x) != x + @test dimension(inv(x)).pm == -1 + @test x != y + @test y != x + @test dimension(uexpand(x * y)) == dimension(u"m^9 * s") + z = uexpand(x) + @test x == z + + # Trigger part of map_dimensions missed elsewhere + x = us"km" + y = us"km" + @test x * y |> uexpand == u"km^2" + @test x / y |> uexpand == u"1" + @test map_dimensions(+, dimension(us"km"), dimension(us"km")) == dimension(us"km^2") + @test map_dimensions(-, dimension(us"km"), dimension(us"km")) == dimension(us"1") + @testset "Promotion with Dimensions" begin x = 0.5u"cm" y = -0.03u"m" @@ -1623,6 +1657,117 @@ end @eval @test all($f.($qx_real_dimensions, $qy_dimensions) .== $ground_truth) @eval @test all($f.($qx_dimensions, $qy_real_dimensions) .== $ground_truth) end + + # Should be able to compare against `NoDims`: + @test Quantity(1.0) >= 1.0 + @test !(Quantity(1.0) > 1.0) +end + +@testset "Tests of `NoDims`" begin + @test promote_type(NoDims{Int16}, NoDims{Int32}) === NoDims{Int32} + + # Prefer other types, always: + @test promote_type(Dimensions{Int16}, NoDims{Int32}) === Dimensions{Int16} + @test promote_type(MyDimensions{Int16}, NoDims{Int32}) === MyDimensions{Int16} + + # Always zero dimensions + @test iszero(dimension(1.0)) + @test iszero(dimension([1.0, 2.0])) + @test dimension(1.0) * u"1" == u"1" + @test typeof(dimension(1.0) * u"1") === typeof(u"1") + + # Even when accessed: + @test NoDims().km == 0 + @test NoDims().m != 1 + + # Even weird user-defined dimensions: + @test NoDims().cookie == 0 + + # Always returns the same type: + @test NoDims{Int32}().cookie isa Int32 + @test NoDims{Int32}().cookie == 0 +end + +@testset "Tests of SymbolicDimensionsSingleton" begin + km = SymbolicUnits.km + m = SymbolicUnits.m + @test km isa Quantity{T,SymbolicDimensionsSingleton{R}} where {T,R} + @test dimension(km) isa SymbolicDimensionsSingleton + @test dimension(km) isa AbstractSymbolicDimensions + + @test dimension(km).km == 1 + @test dimension(km).m == 0 + VERSION >= v"1.9" && + @test_throws "is not available as a symbol" dimension(km).γ + @test !iszero(dimension(km)) + @test inv(km) == us"km^-1" + @test inv(km) == u"km^-1" + + @test !iszero(dimension(SymbolicConstants.c)) + @test SymbolicConstants.c isa Quantity{T,SymbolicDimensionsSingleton{R}} where {T,R} + + # Constructors + @test SymbolicDimensionsSingleton(:cm) isa SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE} + @test constructorof(SymbolicDimensionsSingleton) === SymbolicDimensionsSingleton + + @test with_type_parameters( + SymbolicDimensionsSingleton{Int64}, + Int32 + ) === SymbolicDimensionsSingleton{Int32} + + @test convert( + SymbolicDimensions, + SymbolicDimensionsSingleton{Int32}(:cm) + ) isa SymbolicDimensions{Int32} + + @test copy(km) == km + + # Any operation should immediately convert it: + @test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R} + + # Test promotion explicitly for coverage: + @test promote_type( + SymbolicDimensionsSingleton{Int16}, + SymbolicDimensionsSingleton{Int32} + ) === SymbolicDimensions{Int32} + # ^ Note how we ALWAYS convert to SymbolicDimensions, even + # if the types are the same. + @test promote_type( + SymbolicDimensionsSingleton{Int16}, + SymbolicDimensions{Int32} + ) === SymbolicDimensions{Int32} + @test promote_type( + SymbolicDimensionsSingleton{Int64}, + Dimensions{Int16} + ) === Dimensions{Int64} + + # Test map_dimensions explicitly for coverage: + @test map_dimensions(-, dimension(km)).km == -1 + @test map_dimensions(-, dimension(km)) isa SymbolicDimensions + @test map_dimensions(+, dimension(km), dimension(m)).km == 1 + @test map_dimensions(+, dimension(km), dimension(m)).m == 1 + @test map_dimensions(+, dimension(km), dimension(m)).cm == 0 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).km == 1 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).m == 1 + @test map_dimensions(+, dimension(km), SymbolicDimensions(dimension(m))).cm == 0 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).km == 1 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).m == 1 + @test map_dimensions(+, SymbolicDimensions(dimension(km)), dimension(m)).cm == 0 + + # Note that we avoid converting to SymbolicDimensionsSingleton for uconvert: + @test km |> uconvert(us"m") == 1000m + @test km |> uconvert(us"m") isa Quantity{T,SymbolicDimensions{R}} where {T,R} + @test [km, km] isa Vector{Quantity{T,SymbolicDimensionsSingleton{R}}} where {T,R} + @test [km^2, km] isa Vector{Quantity{T,SymbolicDimensions{R}}} where {T,R} + + # No issue when converting to SymbolicDimensionsSingleton (gets + # converted) + @test uconvert(km, u"m") == 0.001km + @test uconvert(km, u"m") isa Quantity{T,SymbolicDimensions{R}} where {T,R} + + # Symbolic dimensions retain symbols: + @test QuantityArray([km, km]) |> uconvert(us"m") == [1000m, 1000m] + @test QuantityArray([km, km]) |> uconvert(us"m") != [km, km] end @testset "Test div" begin