Skip to content

Commit

Permalink
Parsing of units from strings
Browse files Browse the repository at this point in the history
Generalize the handling of `@u_str` and the unit extension module
context to allow the same machinery to be used for a new `parseunit`
function which does the same thing at runtime.

Fixes PainterQubits#272 as a side effect of generalizing `@u_str`.
  • Loading branch information
c42f committed Jan 17, 2020
1 parent b3d7357 commit 1bb6058
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 47 deletions.
6 changes: 1 addition & 5 deletions src/Unitful.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export Quantity, DimensionlessQuantity, NoUnits, NoDims
export uconvertp, uconvertrp, convertr, convertrp, reflevel, linear
export @logscale, @logunit, @dB, @B, @cNp, @Np
export Level, Gain
export parseunit

const unitmodules = Vector{Module}()

Expand Down Expand Up @@ -64,9 +65,4 @@ include("logarithm.jl")
include("complex.jl")
include("pkgdefaults.jl")

function __init__()
# @u_str should be aware of units defined in module Unitful
Unitful.register(Unitful)
end

end
81 changes: 55 additions & 26 deletions src/user.jl
Original file line number Diff line number Diff line change
Expand Up @@ -502,67 +502,96 @@ julia> u"ħ"
"""
macro u_str(unit)
ex = Meta.parse(unit)
esc(replace_value(__module__, ex))
unitmods = [Unitful]
for m in Unitful.unitmodules
# Find registered unit extension modules which are also loaded by
# __module__ (required so that precompilation will work).
if isdefined(__module__, nameof(m)) && getfield(__module__, nameof(m)) === m
push!(unitmods, m)
end
end
esc(lookup_units(unitmods, ex))
end

"""
parseunit([unit_module(s),] string)
Parse a string as a unit type. The format for `string` must be a valid Julia
expression, and any identifiers will be looked up in the context `unit_module`
which may be a `Module` or a vector of `Module`s. By default
`unit_module=Unitful`.
Examples:
```jldoctest
julia> parseunit("m/s")
1.0 m s^-1
```
"""
parseunit(str::AbstractString) = parseunit(Unitful, str)

parseunit(unitmodule::Module, str) = parseunit([unitmodule], str)
function parseunit(unitmods, str)
ex = Meta.parse(str)
eval(lookup_units(unitmods, ex))
end

const allowed_funcs = [:*, :/, :^, :sqrt, :, :+, :-, ://]
function replace_value(targetmod, ex::Expr)
function lookup_units(unitmods, ex::Expr)
if ex.head == :call
ex.args[1] in allowed_funcs ||
error("""$(ex.args[1]) is not a valid function call when parsing a unit.
Only the following functions are allowed: $allowed_funcs""")
throw(ArgumentError(
"""$(ex.args[1]) is not a valid function call when parsing a unit.
Only the following functions are allowed: $allowed_funcs"""))
for i=2:length(ex.args)
if typeof(ex.args[i])==Symbol || typeof(ex.args[i])==Expr
ex.args[i]=replace_value(targetmod, ex.args[i])
ex.args[i]=lookup_units(unitmods, ex.args[i])
end
end
return ex
elseif ex.head == :tuple
for i=1:length(ex.args)
if typeof(ex.args[i])==Symbol
ex.args[i]=replace_value(targetmod, ex.args[i])
ex.args[i]=lookup_units(unitmods, ex.args[i])
else
error("only use symbols inside the tuple.")
throw(ArgumentError("Only use symbols inside the tuple."))
end
end
return ex
else
error("Expr head $(ex.head) must equal :call or :tuple")
throw(ArgumentError("Expr head $(ex.head) must equal :call or :tuple"))
end
end

function replace_value(targetmod, sym::Symbol)
f = m->()
inds = findall(unitmodules) do m
# Ensure that both the unit exists in the registered unit module, and
# that the target module (the one invoking `u_str`) has loaded it so
# that precompilation will work.
isdefined(m,sym) && ustrcheck_bool(getfield(m, sym)) &&
isdefined(targetmod, nameof(m)) && getfield(targetmod,nameof(m)) === m
end
function lookup_units(unitmods, sym::Symbol)
has_unit = m->(isdefined(m,sym) && ustrcheck_bool(getfield(m, sym)))
inds = findall(has_unit, unitmods)
if isempty(inds)
# Check whether unit exists in the global list to give an improved
# error message.
f = m->(isdefined(m,sym) && ustrcheck_bool(getfield(m, sym)))
hintidx = findfirst(f, unitmodules)
hintidx = findfirst(has_unit, unitmodules)
if hintidx !== nothing
hintmod = unitmodules[hintidx]
error("Symbol `$sym` was found in unit module $hintmod, but was not loaded into $targetmod. Consider `using $hintmod` within `$targetmod`?")
throw(ArgumentError(
"""Symbol `$sym` was found in the globally registered unit module $hintmod
but was not in the provided list of unit modules $(join(unitmods, ", ")).
(Consider `using $hintmod` in your module if you are using `@u_str`?)"""))
else
error("Symbol $sym could not be found in registered unit modules.")
throw(ArgumentError("Symbol $sym could not be found in unit modules $unitmods"))
end
end

m = unitmodules[inds[end]]
m = unitmods[inds[end]]
u = getfield(m, sym)

any(u != u1 for u1 in getfield.(unitmodules[inds[1:(end-1)]], sym)) &&
@warn(string("Symbol $sym was found in multiple registered unit modules. ",
"We will use the one from $m."))
any(u != u1 for u1 in getfield.(unitmods[inds[1:(end-1)]], sym)) &&
@warn """Symbol $sym was found in multiple registered unit modules.
We will use the one from $m."""
return u
end

replace_value(targetmod, literal::Number) = literal
lookup_units(unitmods, literal::Number) = literal

ustrcheck_bool(::Number) = true
ustrcheck_bool(::MixedUnits) = true
Expand Down
41 changes: 25 additions & 16 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -387,22 +387,27 @@ end
end
end

@testset "Unit string macro" begin
@test u"m" == m
@test u"m,s" == (m,s)
@test u"1.0" == 1.0
@test u"m/s" == m/s
@test u"1.0m/s" == 1.0m/s
@test u"m^-1" == m^-1
@test u"dB/Hz" == dB/Hz
@test u"3.0dB/Hz" == 3.0dB/Hz
@test_throws LoadError macroexpand(@__MODULE__, :(u"N m"))
@test_throws LoadError macroexpand(@__MODULE__, :(u"abs(2)"))
@test_throws LoadError @eval u"basefactor"

# test ustrcheck(::Quantity)
@test u"h" == Unitful.h
@test u"π" == π # issue 112
@testset "Unit string parsing" begin
@test parseunit("m") == m
@test parseunit("m,s") == (m,s)
@test parseunit("1.0") == 1.0
@test parseunit("m/s") == m/s
@test parseunit("1.0m/s") == 1.0m/s
@test parseunit("m^-1") == m^-1
@test parseunit("dB/Hz") == dB/Hz
@test parseunit("3.0dB/Hz") == 3.0dB/Hz

# Invalid unit strings
@test_throws Meta.ParseError parseunit("N m")
@test_throws ArgumentError parseunit("abs(2)")
@test_throws ArgumentError parseunit("(1,2)")
@test_throws ArgumentError parseunit("begin end")

# test ustrcheck_bool
@test_throws ArgumentError parseunit("basefactor") # non-Unit symbols
# ustrcheck_bool(::Quantity)
@test parseunit("h") == Unitful.h
@test parseunit("π") == π # issue 112
end

@testset "Unit and dimensional analysis" begin
Expand Down Expand Up @@ -1559,6 +1564,10 @@ module DoesUseFooUnits
end
@test DoesUseFooUnits.foo() === 1u"foo"

# Tests for unit extension modules in unit parsing
@test_throws ArgumentError parseunit(Unitful, "foo")
@test parseunit(FooUnits, "foo") === u"foo"

# Test to make sure user macros are working properly
module TUM
using Unitful
Expand Down

0 comments on commit 1bb6058

Please sign in to comment.