diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a6b1cc3b..aeb0be86 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,10 +33,10 @@ jobs: - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@v1 - name: "Run tests" - shell: bash run: | julia --color=yes --project=. -e 'import Pkg; Pkg.add("Coverage")' julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user --project=. -e 'import Pkg; Pkg.test(coverage=true)' + DQ_TEST_UPREFERRED=true julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user --project=. -e 'import Pkg; Pkg.test(coverage=true)' julia --color=yes --project=. coverage.jl - name: "Coveralls" uses: coverallsapp/github-action@v2 diff --git a/Project.toml b/Project.toml index a68aec44..023ded69 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.4.0" [deps] Requires = "ae029012-a4dd-5104-9daa-d747884805df" +Tricks = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775" [weakdeps] Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" @@ -19,8 +20,8 @@ julia = "1.6" [extras] Ratios = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" -SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" +SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/ext/DynamicQuantitiesUnitfulExt.jl b/ext/DynamicQuantitiesUnitfulExt.jl index 80fb227e..b4eda721 100644 --- a/ext/DynamicQuantitiesUnitfulExt.jl +++ b/ext/DynamicQuantitiesUnitfulExt.jl @@ -10,19 +10,28 @@ else import ..Unitful: @u_str end -# This lets the user override the preferred units: -function unitful_equivalences() - si_units = (length=u"m", mass=u"kg", time=u"s", current=u"A", temperature=u"K", luminosity=u"cd", amount=u"mol") +function get_si_units() + return (length=u"m", mass=u"kg", time=u"s", current=u"A", temperature=u"K", luminosity=u"cd", amount=u"mol") +end + +function validate_upreferred() + si_units = get_si_units() for k in keys(si_units) if Unitful.upreferred(si_units[k]) !== si_units[k] error("Found custom `Unitful.preferunits`. This is not supported when interfacing Unitful and DynamicQuantities: you must leave the default `Unitful.upreferred`, which is the SI base units.") end end + return true +end + +function unitful_equivalences() + si_units = get_si_units() return NamedTuple((k => si_units[k] for k in keys(si_units))) end Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) = let + validate_upreferred() cumulator = DynamicQuantities.ustrip(x) dims = DynamicQuantities.dimension(x) equiv = unitful_equivalences() @@ -35,16 +44,17 @@ Base.convert(::Type{Unitful.Quantity}, x::DynamicQuantities.Quantity) = end Base.convert(::Type{DynamicQuantities.Quantity}, x::Unitful.Quantity{T}) where {T} = convert(DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE}, x) -Base.convert(::Type{DynamicQuantities.Quantity{T,R}}, x::Unitful.Quantity) where {T,R} = +Base.convert(::Type{DynamicQuantities.Quantity{T,D}}, x::Unitful.Quantity) where {T,R,D<:DynamicQuantities.AbstractDimensions{R}} = let value = Unitful.ustrip(Unitful.upreferred(x)) - dimension = convert(DynamicQuantities.Dimensions{R}, Unitful.dimension(x)) + dimension = convert(D, Unitful.dimension(x)) return DynamicQuantities.Quantity(convert(T, value), dimension) end -Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.Dimensions{DynamicQuantities.DEFAULT_DIM_TYPE}, d) +Base.convert(::Type{DynamicQuantities.Dimensions}, d::Unitful.Dimensions) = convert(DynamicQuantities.DEFAULT_DIM_TYPE, d) Base.convert(::Type{DynamicQuantities.Dimensions{R}}, d::Unitful.Dimensions{D}) where {R,D} = let + validate_upreferred() cumulator = DynamicQuantities.Dimensions{R}() for dim in D dim_symbol = _map_dim_name_to_dynamic_units(typeof(dim)) diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 0fe164fc..a636b171 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -1,5 +1,6 @@ module DynamicQuantities +export AbstractQuantity, AbstractDimensions export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount export uparse, @u_str diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 2d914ac9..836873d0 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -47,6 +47,7 @@ Base.convert(::Type{F}, x::Rational) where {F<:FixedRational} = F(x) Base.convert(::Type{Rational{R}}, x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F)) Base.convert(::Type{Rational}, x::F) where {F<:FixedRational} = Rational{eltype(F)}(x.num, denom(F)) Base.convert(::Type{AF}, x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F)) +Base.convert(::Type{I}, x::F) where {I<:Integer,F<:FixedRational} = convert(I, convert(Rational, x)) Base.round(::Type{T}, x::F) where {T,F<:FixedRational} = div(convert(T, x.num), convert(T, denom(F)), RoundNearest) Base.promote(x::Integer, y::F) where {F<:FixedRational} = (F(x), y) Base.promote(x::F, y::Integer) where {F<:FixedRational} = reverse(promote(y, x)) diff --git a/src/math.jl b/src/math.jl index 3da003b8..9f59e381 100644 --- a/src/math.jl +++ b/src/math.jl @@ -1,44 +1,46 @@ -Base.:*(l::Dimensions, r::Dimensions) = @map_dimensions(+, l, r) -Base.:*(l::Quantity, r::Quantity) = Quantity(l.value * r.value, l.dimensions * r.dimensions) -Base.:*(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions * r) -Base.:*(l::Dimensions, r::Quantity) = Quantity(r.value, l * r.dimensions) -Base.:*(l::Quantity, r) = Quantity(l.value * r, l.dimensions) -Base.:*(l, r::Quantity) = Quantity(l * r.value, r.dimensions) -Base.:*(l::Dimensions, r) = Quantity(r, l) -Base.:*(l, r::Dimensions) = Quantity(l, r) +Base.:*(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(+, l, r) +Base.:*(l::AbstractQuantity, r::AbstractQuantity) = new_quantity(typeof(l), ustrip(l) * ustrip(r), dimension(l) * dimension(r)) +Base.:*(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) * r) +Base.:*(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), ustrip(r), l * dimension(r)) +Base.:*(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) * r, dimension(l)) +Base.:*(l, r::AbstractQuantity) = new_quantity(typeof(r), l * ustrip(r), dimension(r)) +Base.:*(l::AbstractDimensions, r) = error("Please use an `AbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") +Base.:*(l, r::AbstractDimensions) = error("Please use an `AbstractQuantity` for multiplication. You used multiplication on types: $(typeof(l)) and $(typeof(r)).") -Base.:/(l::Dimensions, r::Dimensions) = @map_dimensions(-, l, r) -Base.:/(l::Quantity, r::Quantity) = Quantity(l.value / r.value, l.dimensions / r.dimensions) -Base.:/(l::Quantity, r::Dimensions) = Quantity(l.value, l.dimensions / r) -Base.:/(l::Dimensions, r::Quantity) = Quantity(inv(r.value), l / r.dimensions) -Base.:/(l::Quantity, r) = Quantity(l.value / r, l.dimensions) -Base.:/(l, r::Quantity) = l * inv(r) -Base.:/(l::Dimensions, r) = Quantity(inv(r), l) -Base.:/(l, r::Dimensions) = Quantity(l, inv(r)) +Base.:/(l::AbstractDimensions, r::AbstractDimensions) = map_dimensions(-, l, r) +Base.:/(l::AbstractQuantity, r::AbstractQuantity) = new_quantity(typeof(l), ustrip(l) / ustrip(r), dimension(l) / dimension(r)) +Base.:/(l::AbstractQuantity, r::AbstractDimensions) = new_quantity(typeof(l), ustrip(l), dimension(l) / r) +Base.:/(l::AbstractDimensions, r::AbstractQuantity) = new_quantity(typeof(r), inv(ustrip(r)), l / dimension(r)) +Base.:/(l::AbstractQuantity, r) = new_quantity(typeof(l), ustrip(l) / r, dimension(l)) +Base.:/(l, r::AbstractQuantity) = l * inv(r) +Base.:/(l::AbstractDimensions, r) = error("Please use an `AbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") +Base.:/(l, r::AbstractDimensions) = error("Please use an `AbstractQuantity` for division. You used division on types: $(typeof(l)) and $(typeof(r)).") -Base.:+(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value + r.value, l.dimensions) : throw(DimensionError(l, r)) -Base.:-(l::Quantity) = Quantity(-l.value, l.dimensions) -Base.:-(l::Quantity, r::Quantity) = l + (-r) +Base.:+(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? new_quantity(typeof(l), ustrip(l) + ustrip(r), dimension(l)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity) = new_quantity(typeof(l), -ustrip(l), dimension(l)) +Base.:-(l::AbstractQuantity, r::AbstractQuantity) = l + (-r) -Base.:+(l::Quantity, r) = dimension(l) == dimension(r) ? Quantity(l.value + r, l.dimensions) : throw(DimensionError(l, r)) -Base.:+(l, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l + r.value, r.dimensions) : throw(DimensionError(l, r)) -Base.:-(l::Quantity, r) = l + (-r) -Base.:-(l, r::Quantity) = l + (-r) +Base.:+(l::AbstractQuantity, r) = iszero(dimension(l)) ? new_quantity(typeof(l), ustrip(l) + r, dimension(l)) : throw(DimensionError(l, r)) +Base.:+(l, r::AbstractQuantity) = iszero(dimension(r)) ? new_quantity(typeof(r), l + ustrip(r), dimension(r)) : throw(DimensionError(l, r)) +Base.:-(l::AbstractQuantity, r) = l + (-r) +Base.:-(l, r::AbstractQuantity) = l + (-r) -_pow(l::Dimensions, r) = @map_dimensions(Base.Fix1(*, r), l) -_pow(l::Quantity{T}, r) where {T} = Quantity(l.value^r, _pow(l.dimensions, r)) -_pow_as_T(l::Quantity{T}, r) where {T} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r)) -Base.:^(l::Dimensions{R}, r::Integer) where {R} = _pow(l, r) -Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) -Base.:^(l::Quantity{T,R}, r::Integer) where {T,R} = _pow(l, r) -Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow_as_T(l, tryrationalize(R, r)) +# We don't promote on the dimension types: +_pow(l::AbstractDimensions{R}, r::R) where {R} = map_dimensions(Base.Fix1(*, r), l) +Base.:^(l::AbstractDimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) +Base.:^(l::AbstractQuantity{T,D}, r::Integer) where {T,R,D<:AbstractDimensions{R}} = new_quantity(typeof(l), ustrip(l)^r, dimension(l)^r) +Base.:^(l::AbstractQuantity{T,D}, r::Number) where {T,R,D<:AbstractDimensions{R}} = + let dim_pow = tryrationalize(R, r), val_pow = convert(T, dim_pow) + # Need to ensure we take the numerical power by the rationalized quantity: + return new_quantity(typeof(l), ustrip(l)^val_pow, dimension(l)^dim_pow) + end -Base.inv(d::Dimensions) = @map_dimensions(-, d) -Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions)) +Base.inv(d::AbstractDimensions) = map_dimensions(-, d) +Base.inv(q::AbstractQuantity) = new_quantity(typeof(q), inv(ustrip(q)), inv(dimension(q))) -Base.sqrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 2)) -Base.sqrt(q::Quantity) = Quantity(sqrt(q.value), sqrt(q.dimensions)) -Base.cbrt(d::Dimensions{R}) where {R} = d^inv(convert(R, 3)) -Base.cbrt(q::Quantity) = Quantity(cbrt(q.value), cbrt(q.dimensions)) +Base.sqrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 2)) +Base.sqrt(q::AbstractQuantity) = new_quantity(typeof(q), sqrt(ustrip(q)), sqrt(dimension(q))) +Base.cbrt(d::AbstractDimensions{R}) where {R} = d^inv(convert(R, 3)) +Base.cbrt(q::AbstractQuantity) = new_quantity(typeof(q), cbrt(ustrip(q)), cbrt(dimension(q))) -Base.abs(q::Quantity) = Quantity(abs(q.value), q.dimensions) +Base.abs(q::AbstractQuantity) = new_quantity(typeof(q), abs(ustrip(q)), dimension(q)) diff --git a/src/types.jl b/src/types.jl index 823d3cd3..8639e491 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,6 +1,11 @@ -const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} +import Tricks: static_fieldnames, static_fieldtypes + +const DEFAULT_DIM_BASE_TYPE = FixedRational{Int32,2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 +abstract type AbstractQuantity{T,D} end +abstract type AbstractDimensions{R} end + """ Dimensions{R} @@ -22,14 +27,13 @@ which is by default a rational number. # Constructors -- `Dimensions(args...)`: Pass all the dimensions as arguments. `R` is set to `DEFAULT_DIM_TYPE`. -- `Dimensions(; kws...)`: Pass a subset of dimensions as keyword arguments. `R` is set to `DEFAULT_DIM_TYPE`. +- `Dimensions(args...)`: Pass all the dimensions as arguments. `R` is set to `DEFAULT_DIM_BASE_TYPE`. +- `Dimensions(; kws...)`: Pass a subset of dimensions as keyword arguments. `R` is set to `DEFAULT_DIM_BASE_TYPE`. - `Dimensions(::Type{R}; kws...)` or `Dimensions{R}(; kws...)`: Pass a subset of dimensions as keyword arguments, with the output type set to `Dimensions{R}`. -- `Dimensions{R}(args...)`: Pass all the dimensions as arguments, with the output type set to `Dimensions{R}`. +- `Dimensions{R}()`: Create a dimensionless object typed as `Dimensions{R}`. - `Dimensions{R}(d::Dimensions)`: Copy the dimensions from another `Dimensions` object, with the output type set to `Dimensions{R}`. - """ -struct Dimensions{R <: Real} +struct Dimensions{R<:Real} <: AbstractDimensions{R} length::R mass::R time::R @@ -37,39 +41,19 @@ struct Dimensions{R <: Real} temperature::R luminosity::R amount::R - - function Dimensions(length::_R, - mass::_R, - time::_R, - current::_R, - temperature::_R, - luminosity::_R, - amount::_R) where {_R<:Real} - new{_R}(length, mass, time, current, temperature, luminosity, amount) - end - Dimensions(; kws...) = Dimensions(DEFAULT_DIM_TYPE; kws...) - Dimensions(::Type{_R}; kws...) where {_R} = Dimensions( - tryrationalize(_R, get(kws, :length, zero(_R))), - tryrationalize(_R, get(kws, :mass, zero(_R))), - tryrationalize(_R, get(kws, :time, zero(_R))), - tryrationalize(_R, get(kws, :current, zero(_R))), - tryrationalize(_R, get(kws, :temperature, zero(_R))), - tryrationalize(_R, get(kws, :luminosity, zero(_R))), - tryrationalize(_R, get(kws, :amount, zero(_R))), - ) - Dimensions{_R}(; kws...) where {_R} = Dimensions(_R; kws...) - Dimensions{_R}(args...) where {_R} = Dimensions(Base.Fix1(convert, _R).(args)...) - Dimensions{_R}(d::Dimensions) where {_R} = Dimensions{_R}(d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount) end -const DIMENSION_NAMES = Base.fieldnames(Dimensions) -const DIMENSION_SYNONYMS = (:m, :kg, :s, :A, :K, :cd, :mol) -const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS) +(::Type{D})(::Type{R}; kws...) where {R,D<:AbstractDimensions} = constructor_of(D){R}((tryrationalize(R, get(kws, k, zero(R))) for k in static_fieldnames(D))...) +(::Type{D})(; kws...) where {R,D<:AbstractDimensions{R}} = constructor_of(D)(R; kws...) +(::Type{D})(; kws...) where {D<:AbstractDimensions} = D(DEFAULT_DIM_BASE_TYPE; kws...) +(::Type{D})(d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D((getproperty(d, k) for k in static_fieldnames(D))...) + +const DEFAULT_DIM_TYPE = Dimensions{DEFAULT_DIM_BASE_TYPE} """ - Quantity{T,R} + Quantity{T,D} -Physical quantity with value `value` of type `T` and dimensions `dimensions` of type `Dimensions{R}`. +Physical quantity with value `value` of type `T` and dimensions `dimensions` of type `D`. For example, the velocity of an object with mass 1 kg and velocity 2 m/s is `Quantity(2, mass=1, length=1, time=-1)`. You should access these fields with `ustrip(q)`, and `dimensions(q)`. @@ -83,26 +67,37 @@ dimensions according to the operation. # Fields - `value::T`: value of the quantity of some type `T`. Access with `ustrip(::Quantity)` -- `dimensions::Dimensions{R}`: dimensions of the quantity with dimension type `R`. Access with `dimension(::Quantity)` +- `dimensions::D`: dimensions of the quantity. Access with `dimension(::Quantity)` # Constructors -- `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. -- `Quantity(x, ::Type{R}; kws...)`: Construct a quantity with value `x`. The dimensions parametric type is set to `R`. -- `Quantity(x, d::Dimensions{R})`: Construct a quantity with value `x` and dimensions `d`. -- `Quantity{T}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T`. -- `Quantity{T,R}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T` and dimensions parametric type set to `R`. +- `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value + type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. +- `Quantity(x, ::Type{D}; kws...)`: Construct a quantity with value `x` with dimensions given by the keyword arguments, + and the dimensions type set to `D`. +- `Quantity(x, d::D)`: Construct a quantity with value `x` and dimensions `d` of type `D`. +- `Quantity{T}(...)`: As above, but converting the value to type `T`. You may also pass a `Quantity` as input. +- `Quantity{T,D}(...)`: As above, but converting the value to type `T` and dimensions to `D`. You may also pass a + `Quantity` as input. """ -struct Quantity{T, R} +struct Quantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} value::T - dimensions::Dimensions{R} - - Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...)) - Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...)) - Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d) - Quantity{T}(q::Quantity) where {T} = Quantity(convert(T, q.value), dimension(q)) - Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) + dimensions::D end +(::Type{Q})(x::T, ::Type{D}; kws...) where {D<:AbstractDimensions,T,T2,Q<:AbstractQuantity{T2}} = constructor_of(Q)(convert(T2, x), D(; kws...)) +(::Type{Q})(x, ::Type{D}; kws...) where {D<:AbstractDimensions,Q<:AbstractQuantity} = constructor_of(Q)(x, D(; kws...)) +(::Type{Q})(x::T; kws...) where {T,T2,Q<:AbstractQuantity{T2}} = constructor_of(Q)(convert(T2, x), dim_type(Q)(; kws...)) +(::Type{Q})(x; kws...) where {Q<:AbstractQuantity} = constructor_of(Q)(x, dim_type(Q)(; kws...)) + +(::Type{Q})(q::AbstractQuantity) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = constructor_of(Q)(convert(T, ustrip(q)), convert(D, dimension(q))) +(::Type{Q})(q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = constructor_of(Q)(convert(T, ustrip(q)), dimension(q)) + +new_dimensions(::Type{D}, dims...) where {D<:AbstractDimensions} = constructor_of(D)(dims...) +new_quantity(::Type{Q}, l, r) where {Q<:AbstractQuantity} = constructor_of(Q)(l, r) + +dim_type(::Type{Q}) where {T,D<:AbstractDimensions,Q<:AbstractQuantity{T,D}} = D +dim_type(::Type{<:AbstractQuantity}) = DEFAULT_DIM_TYPE +constructor_of(::Type{T}) where {T} = Base.typename(T).wrapper struct DimensionError{Q1,Q2} <: Exception q1::Q1 diff --git a/src/utils.jl b/src/utils.jl index da8de16d..0e89e8f4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,81 +1,80 @@ -macro map_dimensions(f, l...) - # Create a new Dimensions object by applying f to each key - output = :(Dimensions()) - for dim in DIMENSION_NAMES - f_expr = :($f()) - for arg in l - push!(f_expr.args, :($arg.$dim)) - end - push!(output.args, f_expr) - end - return output |> esc +import Tricks: static_fieldnames + +function map_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} + dimension_type = promote_type(typeof(args).parameters...) + dimension_names = static_fieldnames(dimension_type) + return new_dimensions( + dimension_type, + ( + f((getproperty(arg, dim) for arg in args)...) + for dim in dimension_names + )... + ) end -macro all_dimensions(f, l...) +@generated function all_dimensions(f::F, args::AbstractDimensions...) where {F<:Function} # Test a function over all dimensions output = Expr(:&&) - for dim in DIMENSION_NAMES - f_expr = :($f()) - for arg in l - push!(f_expr.args, :($arg.$dim)) + dimension_type = promote_type(args...) + for dim in Base.fieldnames(dimension_type) + f_expr = :(f()) + for i=1:length(args) + push!(f_expr.args, :(args[$i].$dim)) end push!(output.args, f_expr) end - return output |> esc + return output end -Base.float(q::Quantity{T}) where {T<:AbstractFloat} = convert(T, q) -Base.convert(::Type{T}, q::Quantity) where {T<:Real} = +Base.float(q::AbstractQuantity{T}) where {T<:AbstractFloat} = convert(T, q) +Base.convert(::Type{T}, q::AbstractQuantity) where {T<:Real} = let - @assert iszero(q.dimensions) "Quantity $(q) has dimensions! Use `ustrip` instead." + @assert iszero(q.dimensions) "$(typeof(q)): $(q) has dimensions! Use `ustrip` instead." return convert(T, q.value) end -Base.isfinite(q::Quantity) = isfinite(q.value) -Base.keys(::Dimensions) = DIMENSION_NAMES -Base.iszero(d::Dimensions) = @all_dimensions(iszero, d) -Base.iszero(q::Quantity) = iszero(q.value) -Base.getindex(d::Dimensions, k::Symbol) = getfield(d, k) -Base.:(==)(l::Dimensions, r::Dimensions) = @all_dimensions(==, l, r) -Base.:(==)(l::Quantity, r::Quantity) = l.value == r.value && l.dimensions == r.dimensions -Base.:(==)(l, r::Quantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) -Base.:(==)(l::Quantity, r) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) -Base.isless(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? isless(ustrip(l), ustrip(r)) : throw(DimensionError(l, r)) -Base.isless(l::Quantity, r) = dimension(l) == dimension(r) ? isless(ustrip(l), r) : throw(DimensionError(l, r)) -Base.isless(l, r::Quantity) = dimension(l) == dimension(r) ? isless(l, ustrip(r)) : throw(DimensionError(l, r)) -Base.isapprox(l::Quantity, r::Quantity; kws...) = isapprox(l.value, r.value; kws...) && l.dimensions == r.dimensions -Base.length(::Dimensions) = 1 -Base.length(::Quantity) = 1 -Base.iterate(d::Dimensions) = (d, nothing) -Base.iterate(::Dimensions, ::Nothing) = nothing -Base.iterate(q::Quantity) = (q, nothing) -Base.iterate(::Quantity, ::Nothing) = nothing +Base.isfinite(q::AbstractQuantity) = isfinite(ustrip(q)) +Base.keys(d::AbstractDimensions) = static_fieldnames(typeof(d)) +# TODO: Make this more generic. +Base.iszero(d::AbstractDimensions) = all_dimensions(iszero, d) +Base.iszero(q::AbstractQuantity) = iszero(ustrip(q)) +Base.getindex(d::AbstractDimensions, k::Symbol) = getfield(d, k) +Base.:(==)(l::AbstractDimensions, r::AbstractDimensions) = all_dimensions(==, l, r) +Base.:(==)(l::AbstractQuantity, r::AbstractQuantity) = ustrip(l) == ustrip(r) && dimension(l) == dimension(r) +Base.:(==)(l, r::AbstractQuantity) = ustrip(l) == ustrip(r) && iszero(dimension(r)) +Base.:(==)(l::AbstractQuantity, r) = ustrip(l) == ustrip(r) && iszero(dimension(l)) +Base.isless(l::AbstractQuantity, r::AbstractQuantity) = dimension(l) == dimension(r) ? isless(ustrip(l), ustrip(r)) : throw(DimensionError(l, r)) +Base.isless(l::AbstractQuantity, r) = iszero(dimension(l)) ? isless(ustrip(l), r) : throw(DimensionError(l, r)) +Base.isless(l, r::AbstractQuantity) = iszero(dimension(r)) ? isless(l, ustrip(r)) : throw(DimensionError(l, r)) +Base.isapprox(l::AbstractQuantity, r::AbstractQuantity; kws...) = isapprox(ustrip(l), ustrip(r); kws...) && dimension(l) == dimension(r) +Base.length(::AbstractQuantity) = 1 +Base.iterate(q::AbstractQuantity) = (q, nothing) +Base.iterate(::AbstractQuantity, ::Nothing) = nothing # Multiplicative identities: -Base.one(::Type{Quantity{T,R}}) where {T,R} = Quantity(one(T), R) -Base.one(::Type{Quantity{T}}) where {T} = one(Quantity{T,DEFAULT_DIM_TYPE}) -Base.one(::Type{Quantity}) = one(Quantity{DEFAULT_VALUE_TYPE}) -Base.one(::Type{Dimensions{R}}) where {R} = Dimensions{R}() -Base.one(::Type{Dimensions}) = one(Dimensions{DEFAULT_DIM_TYPE}) -Base.one(q::Quantity) = Quantity(one(ustrip(q)), one(dimension(q))) -Base.one(d::Dimensions) = one(typeof(d)) +Base.one(::Type{Q}) where {T,D,Q<:AbstractQuantity{T,D}} = new_quantity(Q, one(T), D) +Base.one(::Type{Q}) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, one(T), DEFAULT_DIM_TYPE) +Base.one(::Type{Q}) where {Q<:AbstractQuantity} = new_quantity(Q, one(DEFAULT_VALUE_TYPE), DEFAULT_DIM_TYPE) +Base.one(::Type{D}) where {D<:AbstractDimensions} = D() +Base.one(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, one(ustrip(q)), one(dimension(q))) +Base.one(::D) where {D<:AbstractDimensions} = one(D) # Additive identities: -Base.zero(q::Quantity) = Quantity(zero(ustrip(q)), dimension(q)) -Base.zero(::Dimensions) = error("There is no such thing as an additive identity for a `Dimensions` object, as + is only defined for `Quantity`.") -Base.zero(::Type{<:Quantity}) = error("Cannot create an additive identity for a `Quantity` type, as the dimensions are unknown. Please use `zero(::Quantity)` instead.") -Base.zero(::Type{<:Dimensions}) = error("There is no such thing as an additive identity for a `Dimensions` type, as + is only defined for `Quantity`.") +Base.zero(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, zero(ustrip(q)), dimension(q)) +Base.zero(::AbstractDimensions) = error("There is no such thing as an additive identity for a `AbstractDimensions` object, as + is only defined for `AbstractQuantity`.") +Base.zero(::Type{<:AbstractQuantity}) = error("Cannot create an additive identity for a `AbstractQuantity` type, as the dimensions are unknown. Please use `zero(::AbstractQuantity)` instead.") +Base.zero(::Type{<:AbstractDimensions}) = error("There is no such thing as an additive identity for a `AbstractDimensions` type, as + is only defined for `AbstractQuantity`.") # Dimensionful 1: -Base.oneunit(q::Quantity) = Quantity(oneunit(ustrip(q)), dimension(q)) -Base.oneunit(::Dimensions) = error("There is no such thing as a dimensionful 1 for a `Dimensions` object, as + is only defined for `Quantity`.") -Base.oneunit(::Type{<:Quantity}) = error("Cannot create a dimensionful 1 for a `Quantity` type without knowing the dimensions. Please use `oneunit(::Quantity)` instead.") -Base.oneunit(::Type{<:Dimensions}) = error("There is no such thing as a dimensionful 1 for a `Dimensions` type, as + is only defined for `Quantity`.") +Base.oneunit(q::Q) where {Q<:AbstractQuantity} = new_quantity(Q, oneunit(ustrip(q)), dimension(q)) +Base.oneunit(::AbstractDimensions) = error("There is no such thing as a dimensionful 1 for a `AbstractDimensions` object, as + is only defined for `AbstractQuantity`.") +Base.oneunit(::Type{<:AbstractQuantity}) = error("Cannot create a dimensionful 1 for a `AbstractQuantity` type without knowing the dimensions. Please use `oneunit(::AbstractQuantity)` instead.") +Base.oneunit(::Type{<:AbstractDimensions}) = error("There is no such thing as a dimensionful 1 for a `AbstractDimensions` type, as + is only defined for `AbstractQuantity`.") -Base.show(io::IO, d::Dimensions) = +Base.show(io::IO, d::AbstractDimensions) = let tmp_io = IOBuffer() for k in keys(d) if !iszero(d[k]) - print(tmp_io, SYNONYM_MAPPING[k]) + print(tmp_io, dimension_name(d, k)) isone(d[k]) || pretty_print_exponent(tmp_io, d[k]) print(tmp_io, " ") end @@ -85,7 +84,13 @@ Base.show(io::IO, d::Dimensions) = s = replace(s, r"\s*$" => "") print(io, s) end -Base.show(io::IO, q::Quantity) = print(io, q.value, " ", q.dimensions) +Base.show(io::IO, q::AbstractQuantity{<:Real}) = print(io, ustrip(q), " ", dimension(q)) +Base.show(io::IO, q::AbstractQuantity) = print(io, "(", ustrip(q), ") ", dimension(q)) + +function dimension_name(::AbstractDimensions, k::Symbol) + default_dimensions = (length="m", mass="kg", time="s", current="A", temperature="K", luminosity="cd", amount="mol") + return get(default_dimensions, k, string(k)) +end string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x) pretty_print_exponent(io::IO, x) = print(io, to_superscript(string_rational(x))) @@ -103,90 +108,89 @@ tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x) Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: ", e.q1, " and ", e.q2, " have incompatible dimensions") -Base.convert(::Type{Quantity}, q::Quantity) = q -Base.convert(::Type{Quantity{T}}, q::Quantity) where {T} = Quantity{T}(q) -Base.convert(::Type{Quantity{T,R}}, q::Quantity) where {T,R} = Quantity{T,R}(q) +Base.convert(::Type{Q}, q::AbstractQuantity) where {Q<:AbstractQuantity} = q +Base.convert(::Type{Q}, q::AbstractQuantity) where {T,Q<:AbstractQuantity{T}} = new_quantity(Q, convert(T, ustrip(q)), dimension(q)) +Base.convert(::Type{Q}, q::AbstractQuantity) where {T,D,Q<:AbstractQuantity{T,D}} = new_quantity(Q, convert(T, ustrip(q)), convert(D, dimension(q))) -Base.convert(::Type{Dimensions}, d::Dimensions) = d -Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) +Base.convert(::Type{D}, d::AbstractDimensions) where {D<:AbstractDimensions} = d +Base.convert(::Type{D}, d::AbstractDimensions) where {R,D<:AbstractDimensions{R}} = D(d) """ - ustrip(q::Quantity) + ustrip(q::AbstractQuantity) Remove the units from a quantity. """ -ustrip(q::Quantity) = q.value -ustrip(::Dimensions) = error("Cannot remove units from a `Dimensions` object.") +ustrip(q::AbstractQuantity) = q.value +ustrip(::AbstractDimensions) = error("Cannot remove units from an `AbstractDimensions` object.") ustrip(q) = q """ - dimension(q::Quantity) + dimension(q::AbstractQuantity) -Get the dimensions of a quantity, returning a `Dimensions` object. +Get the dimensions of a quantity, returning an `AbstractDimensions` object. """ -dimension(q::Quantity) = q.dimensions -dimension(d::Dimensions) = d -dimension(_) = Dimensions() +dimension(q::AbstractQuantity) = q.dimensions +dimension(d::AbstractDimensions) = d """ - ulength(q::Quantity) - ulength(d::Dimensions) + ulength(q::AbstractQuantity) + ulength(d::AbstractDimensions) Get the length dimension of a quantity (e.g., meters^(ulength)). """ -ulength(q::Quantity) = ulength(dimension(q)) -ulength(d::Dimensions) = d.length +ulength(q::AbstractQuantity) = ulength(dimension(q)) +ulength(d::AbstractDimensions) = d.length """ - umass(q::Quantity) - umass(d::Dimensions) + umass(q::AbstractQuantity) + umass(d::AbstractDimensions) Get the mass dimension of a quantity (e.g., kg^(umass)). """ -umass(q::Quantity) = umass(dimension(q)) -umass(d::Dimensions) = d.mass +umass(q::AbstractQuantity) = umass(dimension(q)) +umass(d::AbstractDimensions) = d.mass """ - utime(q::Quantity) - utime(d::Dimensions) + utime(q::AbstractQuantity) + utime(d::AbstractDimensions) Get the time dimension of a quantity (e.g., s^(utime)) """ -utime(q::Quantity) = utime(dimension(q)) -utime(d::Dimensions) = d.time +utime(q::AbstractQuantity) = utime(dimension(q)) +utime(d::AbstractDimensions) = d.time """ - ucurrent(q::Quantity) - ucurrent(d::Dimensions) + ucurrent(q::AbstractQuantity) + ucurrent(d::AbstractDimensions) Get the current dimension of a quantity (e.g., A^(ucurrent)). """ -ucurrent(q::Quantity) = ucurrent(dimension(q)) -ucurrent(d::Dimensions) = d.current +ucurrent(q::AbstractQuantity) = ucurrent(dimension(q)) +ucurrent(d::AbstractDimensions) = d.current """ - utemperature(q::Quantity) - utemperature(d::Dimensions) + utemperature(q::AbstractQuantity) + utemperature(d::AbstractDimensions) Get the temperature dimension of a quantity (e.g., K^(utemperature)). """ -utemperature(q::Quantity) = utemperature(dimension(q)) -utemperature(d::Dimensions) = d.temperature +utemperature(q::AbstractQuantity) = utemperature(dimension(q)) +utemperature(d::AbstractDimensions) = d.temperature """ - uluminosity(q::Quantity) - uluminosity(d::Dimensions) + uluminosity(q::AbstractQuantity) + uluminosity(d::AbstractDimensions) Get the luminosity dimension of a quantity (e.g., cd^(uluminosity)). """ -uluminosity(q::Quantity) = uluminosity(dimension(q)) -uluminosity(d::Dimensions) = d.luminosity +uluminosity(q::AbstractQuantity) = uluminosity(dimension(q)) +uluminosity(d::AbstractDimensions) = d.luminosity """ - uamount(q::Quantity) - uamount(d::Dimensions) + uamount(q::AbstractQuantity) + uamount(d::AbstractDimensions) Get the amount dimension of a quantity (e.g., mol^(uamount)). """ -uamount(q::Quantity) = uamount(dimension(q)) -uamount(d::Dimensions) = d.amount +uamount(q::AbstractQuantity) = uamount(dimension(q)) +uamount(d::AbstractDimensions) = d.amount diff --git a/test/runtests.jl b/test/runtests.jl index 212af775..82e5973e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,10 +5,15 @@ import Ratios: SimpleRatio @eval Base.round(T, x::SimpleRatio) = round(T, x.num // x.den) end -@safetestset "Unitful.jl integration tests" begin - include("test_unitful.jl") -end - -@safetestset "Unit tests" begin - include("unittests.jl") +if parse(Bool, get(ENV, "DQ_TEST_UPREFERRED", "false")) + @safetestset "Test upreferred disallowed" begin + include("test_ban_upreferred.jl") + end +else + @safetestset "Unitful.jl integration tests" begin + include("test_unitful.jl") + end + @safetestset "Unit tests" begin + include("unittests.jl") + end end diff --git a/test/test_ban_upreferred.jl b/test/test_ban_upreferred.jl new file mode 100644 index 00000000..63db6118 --- /dev/null +++ b/test/test_ban_upreferred.jl @@ -0,0 +1,13 @@ +### These are tests we need to run with a fresh Julia runtime + +import Unitful +import Unitful: @u_str +Unitful.preferunits(u"km") +using Test +import DynamicQuantities + +x_unitful = 1.5u"km" +x_dq = DynamicQuantities.Quantity(1500.0, length=1) + +@test_throws ErrorException convert(DynamicQuantities.Quantity, x_unitful) +@test_throws ErrorException convert(Unitful.Quantity, x_dq) diff --git a/test/test_unitful.jl b/test/test_unitful.jl index eac5ab66..ac8cf72f 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -1,5 +1,5 @@ import DynamicQuantities -using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE +using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_VALUE_TYPE import Unitful import Unitful: @u_str import Ratios: SimpleRatio @@ -11,14 +11,30 @@ risapprox(x::Unitful.Quantity, y::Unitful.Quantity; kws...) = return isapprox(xfloat, yfloat; kws...) end -for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] - x = DynamicQuantities.Quantity(T(0.2), R, length=1, amount=2, current=-1 // 2, luminosity=2 // 5) +for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = DynamicQuantities.Dimensions{R} + x = DynamicQuantities.Quantity(T(0.2), D, length=1, amount=2, current=-1 // 2, luminosity=2 // 5) x_unitful = T(0.2)u"m*mol^2*A^(-1//2)*cd^(2//5)" @test risapprox(convert(Unitful.Quantity, x), x_unitful; atol=1e-6) @test typeof(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x))) <: DynamicQuantities.Quantity{T,DynamicQuantities.DEFAULT_DIM_TYPE} @test isapprox(convert(DynamicQuantities.Quantity, convert(Unitful.Quantity, x)), x; atol=1e-6) - @test isapprox(convert(DynamicQuantities.Quantity{T,R}, x_unitful), x; atol=1e-6) - @test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,R}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6) + @test isapprox(convert(DynamicQuantities.Quantity{T,D}, x_unitful), x; atol=1e-6) + @test risapprox(convert(Unitful.Quantity, convert(DynamicQuantities.Quantity{T,D}, x_unitful)), Unitful.upreferred(x_unitful); atol=1e-6) + + @test typeof(convert(DynamicQuantities.Dimensions, Unitful.dimension(x_unitful))) == DynamicQuantities.Dimensions{DEFAULT_DIM_BASE_TYPE} +end + +module MyScaleUnit + using Unitful + @dimension(๐’, "๐’", Scale) + @refunit(scale, "scale", Scale, ๐’, false) end + +Unitful.register(MyScaleUnit) + +x = 1.0u"scale" +@test typeof(x) <: Unitful.Quantity{Float64, MyScaleUnit.๐’} +@test_throws ErrorException convert(DynamicQuantities.Quantity, x) +# These are not supported because there is no SI equivalency diff --git a/test/unittests.jl b/test/unittests.jl index 40693909..cea8a5e5 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -1,16 +1,18 @@ using DynamicQuantities -using DynamicQuantities: DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE, DIMENSION_NAMES +using DynamicQuantities: FixedRational +using DynamicQuantities: DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE using Ratios: SimpleRatio using SaferIntegers: SafeInt16 using Test @testset "Basic utilities" begin - for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] - x = Quantity(T(0.2), R, length=1, mass=2.5) + for T in [DEFAULT_VALUE_TYPE, Float16, Float32, Float64], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + x = Quantity(T(0.2), D, length=1, mass=2.5) @test typeof(x).parameters[1] == T - @test typeof(x).parameters[2] == R + @test typeof(x).parameters[2] == D @test ulength(x) == R(1 // 1) @test umass(x) == R(5 // 2) @test ustrip(x) โ‰ˆ T(0.2) @@ -23,7 +25,7 @@ using Test y = x^2 @test typeof(x).parameters[1] == T - @test typeof(x).parameters[2] == R + @test typeof(x).parameters[2] == D @test ulength(y) == R(2 // 1) @test umass(y) == (5 // 1) @test ustrip(y) โ‰ˆ T(0.04) @@ -71,25 +73,25 @@ using Test @test iszero(x.dimensions) == false @test iszero(y.dimensions) == true - y = Quantity(T(2 // 10), R, length=1, mass=5 // 2) + y = Quantity(T(2 // 10), D, length=1, mass=5 // 2) @test y โ‰ˆ x - y = Quantity(T(2 // 10), R, length=1, mass=6 // 2) + y = Quantity(T(2 // 10), D, length=1, mass=6 // 2) @test !(y โ‰ˆ x) y = x * Inf32 @test typeof(y).parameters[1] == promote_type(T, Float32) - @test typeof(y).parameters[2] == R + @test typeof(y).parameters[2] == D @test isfinite(x) @test !isfinite(y) y = x^2.1 @test typeof(y).parameters[1] == T # Should not promote! Expect 2.1 to be converted to 21//10 - @test typeof(y).parameters[2] == R + @test typeof(y).parameters[2] == D @test ulength(y) == R(1 * (21 // 10)) @test umass(y) == R((5 // 2) * (21 // 10)) @test utime(y) == R(0) @@ -99,26 +101,26 @@ using Test @test uamount(y) == R(0) @test ustrip(y) โ‰ˆ T(0.2^2.1) - dimensionless = Quantity(one(T), R) + dimensionless = Quantity(one(T), D) y = T(2) + dimensionless @test ustrip(y) == T(3) @test dimension(y) == Dimensions(R) - @test typeof(y) == Quantity{T,R} + @test typeof(y) == Quantity{T,D} y = T(2) - dimensionless @test ustrip(y) == T(1) @test dimension(y) == Dimensions(R) - @test typeof(y) == Quantity{T,R} + @test typeof(y) == Quantity{T,D} y = dimensionless + T(2) @test ustrip(y) == T(3) y = dimensionless - T(2) @test ustrip(y) == T(-1) - @test_throws DimensionError Quantity(one(T), R, length=1) + 1.0 - @test_throws DimensionError Quantity(one(T), R, length=1) - 1.0 - @test_throws DimensionError 1.0 + Quantity(one(T), R, length=1) - @test_throws DimensionError 1.0 - Quantity(one(T), R, length=1) + @test_throws DimensionError Quantity(one(T), D, length=1) + 1.0 + @test_throws DimensionError Quantity(one(T), D, length=1) - 1.0 + @test_throws DimensionError 1.0 + Quantity(one(T), D, length=1) + @test_throws DimensionError 1.0 - Quantity(one(T), D, length=1) end x = Quantity(-1.2, length=2 // 5) @@ -127,28 +129,37 @@ using Test @test abs(x) == abs(Quantity(1.2, length=2 // 5)) end +@testset "Complex numbers" begin + x = (0.5 + 0.5im) * u"km/s" + @test string(x) == "(500.0 + 500.0im) m sโปยน" + @test typeof(x) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test typeof(x^2) == Quantity{Complex{Float64}, DEFAULT_DIM_TYPE} + @test x^2/u"km/s"^2 == Quantity(0.5im) + @test x^2.5 โ‰ˆ (-5.088059320440205e6 + 1.2283661817565577e7im) * u"m^(5/2) * s^(-5/2)" +end + @testset "Fallbacks" begin @test ustrip(0.5) == 0.5 @test ustrip(ones(32)) == ones(32) - @test dimension(0.5) == Dimensions() - @test dimension(ones(32)) == Dimensions() @test dimension(Dimensions()) === Dimensions() + @test_throws MethodError Dimensions(1.0) @test_throws ErrorException ustrip(Dimensions()) end @testset "Arrays" begin for T in [Float16, Float32, Float64], R in [Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + X = randn(T, 10) - uX = X .* Dimensions{R}(length=2.5, luminosity=0.5) + uX = X .* Quantity{T,D}(1, length=2.5, luminosity=0.5) - @test eltype(uX) <: Quantity{T,R} - @test typeof(sum(uX)) <: Quantity{T,R} + @test eltype(uX) <: Quantity{T,D} + @test typeof(sum(uX)) <: Quantity{T,D} @test sum(X) == ustrip(sum(uX)) - @test dimension(prod(uX)) == prod([Dimensions(length=2.5, luminosity=0.5) for i in 1:10]) - @test dimension(prod(uX)) == prod([Dimensions(R, length=2.5, luminosity=0.5) for i in 1:10]) - @test typeof(dimension(prod(uX))) <: Dimensions{R} - @test dimension(X[1]) == Dimensions() + @test dimension(prod(uX)) == dimension(prod([Quantity(T(1), D, length=2.5, luminosity=0.5) for i in 1:10])) + @test dimension(prod(uX)) == dimension(prod([Quantity(T(1), D, length=2.5, luminosity=0.5) for i in 1:10])) + @test typeof(dimension(prod(uX))) <: D uX = X .* Quantity(2, length=2.5, luminosity=0.5) @test sum(X) == 0.5 * ustrip(sum(uX)) @@ -157,9 +168,9 @@ end @test ustrip(x + ones(T, 32))[32] == 2 @test typeof(x + ones(T, 32)) <: Quantity{Vector{T}} @test typeof(x - ones(T, 32)) <: Quantity{Vector{T}} - @test typeof(ones(T, 32) * Dimensions(length=1)) <: Quantity{Vector{T}} - @test typeof(ones(T, 32) / Dimensions(length=1)) <: Quantity{Vector{T}} - @test ones(T, 32) / Dimensions(length=1) == Quantity(ones(T, 32), length=-1) + @test typeof(ones(T, 32) * Quantity(T(1), D, length=1)) <: Quantity{Vector{T}} + @test typeof(ones(T, 32) / Quantity(T(1), D, length=1)) <: Quantity{Vector{T}} + @test ones(T, 32) / Quantity(T(1), length=1) == Quantity(ones(T, 32), length=-1) end end @@ -173,16 +184,18 @@ end @test dimension(z) == Dimensions(length=1, mass=2) @test float(z / (z * -1 / 52)) โ‰ˆ ustrip(z) - @test Dimensions(length=1) / 0.5 == Quantity(2.0, length=1) - @test 0.5 / Dimensions(length=1) == Quantity(0.5, length=-1) - @test Dimensions(length=1) * 0.5 == Quantity(0.5, length=1) + # Invalid ways to make a quantity: + @test_throws ErrorException Dimensions(length=1) / 0.5 == Quantity(2.0, length=1) + @test_throws ErrorException 0.5 / Dimensions(length=1) + @test_throws ErrorException Dimensions(length=1) * 0.5 + @test 0.5 / Quantity(1, length=1) == Quantity(0.5, length=-1) @test 0.5 * Quantity(1, length=1) == Quantity(0.5, length=1) @test Quantity(0.5) / Dimensions(length=1) == Quantity(0.5, length=-1) @test Quantity(0.5, length=2) / Dimensions(length=1) == Quantity(0.5, length=1) @test Dimensions(length=1) / Quantity(0.5, length=2, mass=-5) == Quantity(2, length=-1, mass=5) - @test Dimensions{Int8}([0 for i=1:length(DIMENSION_NAMES)]...) == Dimensions{Int8}() + @test Dimensions{Int8}(zeros(Int, 7)...) == Dimensions{Int8}() @test zero(Quantity(0.0+0.0im)) + Quantity(1) == Quantity(1.0+0.0im, length=Int8(0)) @test oneunit(Quantity(0.0+0.0im)) - Quantity(1) == Quantity(0.0+0.0im, length=Int8(0)) @@ -219,7 +232,7 @@ end @test sqrt(z * -1) == Quantity(sqrt(52), length=1 // 2, mass=1) @test cbrt(z) == Quantity(cbrt(-52), length=1 // 3, mass=2 // 3) - @test 1.0 * (Dimensions(length=3)^2) == Quantity(1.0, length=6) + @test_throws ErrorException 1.0 * (Dimensions(length=3)^2) x = 0.9u"km/s" y = 0.3 * x @@ -254,9 +267,9 @@ end @test convert(Dimensions, d) === d q = Quantity(0.5, d) - q32_32 = convert(Quantity{Float32,Rational{Int32}}, q) - @test typeof(q) == Quantity{Float64,Rational{Int16}} - @test typeof(q32_32) == Quantity{Float32,Rational{Int32}} + q32_32 = convert(Quantity{Float32,Dimensions{Rational{Int32}}}, q) + @test typeof(q) == Quantity{Float64,Dimensions{Rational{Int16}}} + @test typeof(q32_32) == Quantity{Float32,Dimensions{Rational{Int32}}} @test ustrip(q) == 0.5 @test ustrip(q32_32) == 0.5 @test typeof(ustrip(q)) == Float64 @@ -265,8 +278,35 @@ end @test umass(q) == 2 @test umass(q32_32) == 2 @test typeof(umass(q32_32)) == Rational{Int32} - @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Rational{Int16}} + @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Dimensions{Rational{Int16}}} @test convert(Quantity, q) === q + + # Test that regular type promotion applies: + q = Quantity(2, d) + @test typeof(q) == Quantity{Int64,typeof(d)} + @test typeof(q ^ 2) == Quantity{Int64,typeof(d)} + @test typeof(0.5 * q) == Quantity{Float64,typeof(d)} + @test typeof(inv(q)) == Quantity{Float64,typeof(d)} + + # Automatic conversions via constructor: + for T in [Float16, Float32, Float64, BigFloat], R in [DEFAULT_DIM_BASE_TYPE, Rational{Int16}, Rational{Int32}, SimpleRatio{Int}, SimpleRatio{SafeInt16}] + D = Dimensions{R} + q = Quantity{T,D}(2, length=1.5) + @test typeof(q) == Quantity{T,D} + @test typeof(ustrip(q)) == T + @test typeof(ulength(q)) == R + + # Now, without R, the default will be DEFAULT_DIM_BASE_TYPE: + q = Quantity{T}(2, length=1.5) + @test typeof(q) == Quantity{T,DEFAULT_DIM_TYPE} + @test typeof(ustrip(q)) == T + @test typeof(ulength(q)) == DEFAULT_DIM_BASE_TYPE + + # Just dimensions: + d = D(length=1.5) + @test typeof(d) == D + @test typeof(ulength(d)) == R + end end @testset "Units" begin @@ -286,8 +326,8 @@ end @test ustrip(y) โ‰ˆ 0.0003 @test ulength(y) == 2 - y32 = convert(Quantity{Float32,Rational{Int16}}, y) - @test typeof(y32) == Quantity{Float32,Rational{Int16}} + y32 = convert(Quantity{Float32,Dimensions{Rational{Int16}}}, y) + @test typeof(y32) == Quantity{Float32,Dimensions{Rational{Int16}}} @test ustrip(y32) โ‰ˆ 0.0003 z = u"yr" @@ -305,3 +345,64 @@ end @test_throws LoadError eval(:(u":x")) end + +@testset "Additional tests of FixedRational" begin + @test convert(Int64, FixedRational{Int64,1000}(2 // 1)) == 2 + @test convert(Int32, FixedRational{Int64,1000}(3 // 1)) == 3 +end + +struct MyDimensions{R} <: AbstractDimensions{R} + length::R + mass::R + time::R +end +struct MyQuantity{T,D<:AbstractDimensions} <: AbstractQuantity{T,D} + value::T + dimensions::D +end + +@testset "Custom dimensions" begin + for T in [Float32, Float64], R in [Rational{Int64}, Rational{Int32}] + D = MyDimensions{R} + x = MyQuantity(T(0.1), D, length=0.5) + @test x * x โ‰ˆ MyQuantity(T(0.01), D, length=1) + @test typeof(x * x) == MyQuantity{T,D} + @test one(MyQuantity{T,D}) == MyQuantity(one(T), MyDimensions(R)) + @test zero(x) == MyQuantity(zero(T), D, length=0.5) + @test oneunit(x) + x == MyQuantity(T(1.1), D, length=0.5) + @test typeof(oneunit(x) + x) == MyQuantity{T,D} + + # Automatic conversions: + @test typeof(MyQuantity(1, MyDimensions, length=0.5)) == MyQuantity{typeof(1),MyDimensions{DEFAULT_DIM_BASE_TYPE}} + @test typeof(MyQuantity{T}(1, MyDimensions, length=0.5)) == MyQuantity{T,MyDimensions{DEFAULT_DIM_BASE_TYPE}} + @test typeof(MyQuantity{T}(1, D, length=0.5)) == MyQuantity{T,D} + @test typeof(MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{T,D} + @test typeof(0.5 * MyQuantity{T,D}(0.1, length=0.5)) == MyQuantity{promote_type(T,Float64),D} + + x = MyQuantity(big(0.1), length=1) + @test typeof(x) == MyQuantity{BigFloat,DEFAULT_DIM_TYPE} + @test typeof(MyQuantity{T}(x)) == MyQuantity{T,DEFAULT_DIM_TYPE} + @test typeof(MyQuantity{T,D}(x)) == MyQuantity{T,D} + + # Using MyDimensions inside regular Quantity: + x = Quantity(T(0.1), MyDimensions(R, length=0.5)) + @test typeof(x) == Quantity{T,MyDimensions{R}} + @test typeof(x * x) == Quantity{T,MyDimensions{R}} + @test ulength(x * x) == 1 + @test dimension(x * x) == MyDimensions(R, length=1) + + # Errors: + @test_throws ErrorException zero(MyQuantity{T,D}) + @test_throws ErrorException oneunit(MyQuantity{T,D}) + @test_throws ErrorException 1.0 * MyDimensions() + end + @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=0.5) + @test MyQuantity(0.1, DEFAULT_DIM_TYPE, length=0.5) == MyQuantity(0.1, length=1//2) + + # Can construct using args directly: + @test typeof(MyDimensions(1, 1, 1)) == MyDimensions{Int} + @test typeof(MyDimensions{Float64}(1, 1, 1)) == MyDimensions{Float64} + + # But, we always need to use a quantity when mixing with mathematical operations: + @test_throws ErrorException MyQuantity(0.1) + 0.1 * MyDimensions() +end