diff --git a/NEWS.md b/NEWS.md index 39d19235b..82dba417d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # Unitful.jl changelog +## Unreleased + +* ![Feature:](https://img.shields.io/badge/-feature-green) `Unitful.numtype` is now part of the public API. It returns the underlying numeric type of a quantity (e.g. `Float64` for `1.0u"m"`), of any `Number` (where it just returns its type), or of a `Base.Enum` (the backing integer type). On Julia ≥ 1.11 the name is declared `public`. + ## v1.28.0 (2026-01-29) * ![Feature:](https://img.shields.io/badge/-feature-green) Dimensionless quantities now support `iseven` and `isodd` ([#829](https://github.com/JuliaPhysics/Unitful.jl/pull/829)). diff --git a/docs/src/manipulations.md b/docs/src/manipulations.md index 852c24832..340e0e5e2 100644 --- a/docs/src/manipulations.md +++ b/docs/src/manipulations.md @@ -34,6 +34,12 @@ Unitful.dimension Unitful.ustrip ``` +## Numeric type extraction + +```@docs +Unitful.numtype +``` + ## Unit multiplication ```@docs diff --git a/src/Unitful.jl b/src/Unitful.jl index 9769c4296..7030daff6 100644 --- a/src/Unitful.jl +++ b/src/Unitful.jl @@ -36,6 +36,11 @@ export @logscale, @logunit, @dB, @B, @cNp, @Np export Level, Gain export uparse +# Public, but not exported to avoid name clashes (`public` requires Julia ≥ 1.11). +if VERSION >= v"1.11.0-DEV.469" + eval(Meta.parse("public numtype")) +end + const unitmodules = Vector{Module}() function _basefactors(m::Module) diff --git a/src/utils.jl b/src/utils.jl index 23efcfdc2..310a96310 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,8 +1,41 @@ @inline isunitless(::Units) = false @inline isunitless(::Units{()}) = true +""" + numtype(x::Number) + numtype(::Type{T}) where {T<:Number} + numtype(x::Base.Enum) + numtype(::Type{T}) where {T<:Base.Enum} + +Return the underlying numeric type of a number or number type. + +For an [`AbstractQuantity`](@ref Unitful.AbstractQuantity), this is the type +parameter describing the value stored alongside the units. For an `Enum`, it is +the integer type that backs the enumeration. For a plain `Number`, there is +nothing to strip, so `numtype` returns its type. Other number-like types can +extend `numtype` by adding methods. + +```jldoctest +julia> using Unitful + +julia> Unitful.numtype(1.0u"m") +Float64 + +julia> Unitful.numtype(typeof(1.0u"m")) +Float64 + +julia> Unitful.numtype(1 + 2im) +Complex{Int64} +``` +""" +function numtype end + +@inline numtype(x::Number) = typeof(x) +@inline numtype(::Type{T}) where {T<:Number} = T @inline numtype(::AbstractQuantity{T}) where {T} = T @inline numtype(::Type{Q}) where {T, Q<:AbstractQuantity{T}} = T +@inline numtype(x::Base.Enum) = numtype(typeof(x)) +@inline numtype(::Type{E}) where {T, E<:Base.Enum{T}} = T @inline dimtype(u::Unit{U,D}) where {U,D} = D diff --git a/test/runtests.jl b/test/runtests.jl index 720afe1e9..3d6fdf535 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -562,12 +562,34 @@ Unitful.uconvert(U::Unitful.Units, q::QQQ) = uconvert(U, Quantity(q.val, cm)) # promoting the second time should not change the types @test_throws ErrorException promote(px, py) end - @testset "> Some internal behaviors" begin + @testset "> numtype" begin # quantities @test Unitful.numtype(Quantity{Float64}) <: Float64 @test Unitful.numtype(Quantity{Float64, 𝐋}) <: Float64 @test Unitful.numtype(typeof(1.0kg)) <: Float64 @test Unitful.numtype(1.0kg) <: Float64 + # plain numbers (public API fallback) + @test Unitful.numtype(1) === Int + @test Unitful.numtype(1.0) === Float64 + @test Unitful.numtype(1 + 2im) === Complex{Int} + @test Unitful.numtype(1 // 2) === Rational{Int} + @test Unitful.numtype(Float64) === Float64 + @test Unitful.numtype(Complex{Float32}) === Complex{Float32} + # Bool is a Number, so it hits the generic fallback + @test Unitful.numtype(true) === Bool + @test Unitful.numtype(Bool) === Bool + # Enums carry their backing integer type as a parameter + @eval @enum NumtypeEnum::Int32 numtype_a numtype_b + @test Unitful.numtype(numtype_a) === Int32 + @test Unitful.numtype(NumtypeEnum) === Int32 + # Do not accidentally support `Union{}` + @test_throws MethodError Unitful.numtype(Union{}) + # not a Number → MethodError + @test_throws MethodError Unitful.numtype("not a number") + @test_throws MethodError Unitful.numtype(String) + if VERSION >= v"1.11.0-DEV.469" + @test Base.ispublic(Unitful, :numtype) + end end end