Skip to content

Commit 3b6525f

Browse files
committed
feat: allow registering units externally
- By interpolating `Units.UNIT_SYMBOLS` while registering units and updating vectors-and-maps-of-units, new units can be registered even from outside the `Units` module - Internally, units are registered lazily with `_lazy_register_unit` aka `UNIT_MAPPING` is updated after all units are defined.
1 parent 9681769 commit 3b6525f

File tree

6 files changed

+138
-74
lines changed

6 files changed

+138
-74
lines changed

src/DynamicQuantities.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ include("uparse.jl")
2323
include("symbolic_dimensions.jl")
2424
include("complex.jl")
2525
include("disambiguities.jl")
26-
26+
include("register_units.jl")
2727
include("deprecated.jl")
2828
export expand_units
29+
export @register_unit
2930

3031
import PackageExtensionCompat: @require_extensions
3132
import .Units
@@ -43,7 +44,6 @@ let _units_import_expr = :(using .Units: m, g)
4344
eval(_units_import_expr)
4445
end
4546

46-
4747
function __init__()
4848
@require_extensions
4949
end

src/register_units.jl

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
3+
import .SymbolicUnits:
4+
SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values!
5+
6+
# Update the unit collections
7+
function update_unit_mapping(name, value, unit_mapping::Dict{Symbol,Int} = UNIT_MAPPING)
8+
unit_mapping[name] = length(unit_mapping) + 1
9+
end
10+
11+
function update_all_values(name_symbol, unit)
12+
push!(ALL_SYMBOLS, name_symbol)
13+
push!(ALL_VALUES, unit)
14+
ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1)
15+
end
16+
17+
18+
# Register
19+
macro register_unit(name, value)
20+
return esc(_register_unit(name, value))
21+
end
22+
23+
function _register_unit(name::Symbol, value)
24+
name_symbol = Meta.quot(name)
25+
reg_expr = _lazy_register_unit(name, value)
26+
push!(reg_expr.args, quote
27+
$update_unit_mapping($name_symbol, $value)
28+
$update_all_values($name_symbol, $value)
29+
$update_symbolic_unit_values!($name_symbol)
30+
end)
31+
reg_expr
32+
end
33+

src/symbolic_dimensions.jl

+23-20
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
22
import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
33

4-
54
const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)
65

76
disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s
87

9-
const INDEX_TYPE = UInt8
108
# Prefer units over constants:
119
# For example, this means we can't have a symbolic Planck's constant,
1210
# as it is just "hours" (h), which is more common.
13-
const ALL_SYMBOLS = (
14-
UNIT_SYMBOLS...,
15-
disambiguate_symbol.(CONSTANT_SYMBOLS)...
16-
)
17-
const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...)
18-
const ALL_MAPPING = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS)))
11+
const INDEX_TYPE = UInt16
12+
# Prefer units over constants:
13+
# For example, this means we can't have a symbolic Planck's constant,
14+
# as it is just "hours" (h), which is more common.
15+
const ALL_SYMBOLS = [UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...]
16+
const ALL_VALUES = [UNIT_VALUES..., CONSTANT_VALUES...]
17+
const ALL_MAPPING = Dict(ALL_SYMBOLS .=> (INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS))))
1918

2019
"""
2120
AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
@@ -91,7 +90,7 @@ end
9190
function SymbolicDimensionsSingleton{R}(s::Symbol) where {R}
9291
i = get(ALL_MAPPING, s, INDEX_TYPE(0))
9392
iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).")
94-
return SymbolicDimensionsSingleton{R}(i)
93+
SymbolicDimensionsSingleton{R}(i)
9594
end
9695

9796
# Traits:
@@ -169,7 +168,7 @@ uexpand(q::QuantityArray) = uexpand.(q)
169168
uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
170169
171170
Convert a quantity `q` with base SI units to the symbolic units of `qout`, for `q` and `qout` with compatible units.
172-
Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`.
171+
Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`.
173172
"""
174173
function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
175174
@assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert."
@@ -224,7 +223,7 @@ end
224223
"""
225224
uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions})
226225
227-
Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e
226+
Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e
228227
a function equivalent to `q -> uconvert(qout, q)`.
229228
"""
230229
uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout)
@@ -368,6 +367,7 @@ to enable pretty-printing of units.
368367
"""
369368
module SymbolicUnits
370369

370+
371371
import ..UNIT_SYMBOLS
372372
import ..CONSTANT_SYMBOLS
373373
import ..SymbolicDimensionsSingleton
@@ -383,17 +383,19 @@ module SymbolicUnits
383383
import ...SymbolicDimensionsSingleton
384384
import ...constructorof
385385
import ...disambiguate_symbol
386-
import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE
387-
import ....DEFAULT_VALUE_TYPE
388-
import ....DEFAULT_DIM_BASE_TYPE
386+
import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
387+
import ...DEFAULT_VALUE_TYPE
388+
import ...DEFAULT_DIM_BASE_TYPE
389389

390390
const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
391391

392392
for unit in CONSTANT_SYMBOLS
393393
@eval begin
394394
const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
395395
DEFAULT_VALUE_TYPE(1.0),
396-
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
396+
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(
397+
$(QuoteNode(disambiguate_symbol(unit))),
398+
),
397399
)
398400
push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
399401
end
@@ -404,18 +406,19 @@ module SymbolicUnits
404406
import .Constants as SymbolicConstants
405407
import .Constants: SYMBOLIC_CONSTANT_VALUES
406408

407-
const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
408-
for unit in UNIT_SYMBOLS
409+
const SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
410+
411+
function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES)
409412
@eval begin
410413
const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
411414
DEFAULT_VALUE_TYPE(1.0),
412-
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit)))
415+
SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))),
413416
)
414-
push!(_SYMBOLIC_UNIT_VALUES, $unit)
417+
push!($symbolic_unit_values, $unit)
415418
end
416419
end
417-
const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES)
418420

421+
update_symbolic_unit_values!.(UNIT_SYMBOLS)
419422

420423
"""
421424
sym_uparse(raw_string::AbstractString)

src/units.jl

+46-45
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ import ..DEFAULT_QUANTITY_TYPE
66

77
@assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type."
88

9-
const _UNIT_SYMBOLS = Symbol[]
10-
const _UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
9+
const UNIT_SYMBOLS = Symbol[]
10+
const UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
11+
const UNIT_MAPPING = Dict{Symbol,Int}()
1112

12-
macro register_unit(name, value)
13-
return esc(_register_unit(name, value))
13+
macro _lazy_register_unit(name, value)
14+
return esc(_lazy_register_unit(name, value))
1415
end
1516

1617
macro add_prefixes(base_unit, prefixes)
1718
@assert prefixes.head == :tuple
18-
return esc(_add_prefixes(base_unit, prefixes.args, _register_unit))
19+
return esc(_add_prefixes(base_unit, prefixes.args, _lazy_register_unit))
1920
end
2021

21-
function _register_unit(name::Symbol, value)
22-
s = string(name)
23-
return quote
22+
function _lazy_register_unit(name::Symbol, value)
23+
name_symbol = Meta.quot(name)
24+
quote
25+
haskey($UNIT_MAPPING, $name_symbol) && throw("Unit $name_symbol already exists.")
2426
const $name = $value
25-
push!(_UNIT_SYMBOLS, Symbol($s))
26-
push!(_UNIT_VALUES, $name)
27+
push!($UNIT_SYMBOLS, $name_symbol)
28+
push!($UNIT_VALUES, $name)
2729
end
2830
end
2931

32+
3033
function _add_prefixes(base_unit::Symbol, prefixes, register_function)
3134
all_prefixes = (
3235
f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1,
@@ -42,13 +45,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function)
4245
end
4346

4447
# SI base units
45-
@register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1)
46-
@register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1)
47-
@register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1)
48-
@register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1)
49-
@register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1)
50-
@register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1)
51-
@register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1)
48+
@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length = 1)
49+
@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass = 1)
50+
@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time = 1)
51+
@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current = 1)
52+
@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature = 1)
53+
@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity = 1)
54+
@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount = 1)
5255

5356
@add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G)
5457
@add_prefixes g (p, n, μ, u, m, k)
@@ -88,17 +91,17 @@ end
8891
)
8992

9093
# SI derived units
91-
@register_unit Hz inv(s)
92-
@register_unit N kg * m / s^2
93-
@register_unit Pa N / m^2
94-
@register_unit J N * m
95-
@register_unit W J / s
96-
@register_unit C A * s
97-
@register_unit V W / A
98-
@register_unit F C / V
99-
@register_unit Ω V / A
100-
@register_unit ohm Ω
101-
@register_unit T N / (A * m)
94+
@_lazy_register_unit Hz inv(s)
95+
@_lazy_register_unit N kg * m / s^2
96+
@_lazy_register_unit Pa N / m^2
97+
@_lazy_register_unit J N * m
98+
@_lazy_register_unit W J / s
99+
@_lazy_register_unit C A * s
100+
@_lazy_register_unit V W / A
101+
@_lazy_register_unit F C / V
102+
@_lazy_register_unit Ω V / A
103+
@_lazy_register_unit ohm Ω
104+
@_lazy_register_unit T N / (A * m)
102105

103106
@add_prefixes Hz (n, μ, u, m, k, M, G)
104107
@add_prefixes N ()
@@ -156,17 +159,17 @@ end
156159

157160
# Common assorted units
158161
## Time
159-
@register_unit min 60 * s
160-
@register_unit minute min
161-
@register_unit h 60 * min
162-
@register_unit hr h
163-
@register_unit day 24 * h
164-
@register_unit d day
165-
@register_unit wk 7 * day
166-
@register_unit yr 365.25 * day
167-
@register_unit inch 2.54 * cm
168-
@register_unit ft 12 * inch
169-
@register_unit mi 5280 * ft
162+
@_lazy_register_unit min 60 * s
163+
@_lazy_register_unit minute min
164+
@_lazy_register_unit h 60 * min
165+
@_lazy_register_unit hr h
166+
@_lazy_register_unit day 24 * h
167+
@_lazy_register_unit d day
168+
@_lazy_register_unit wk 7 * day
169+
@_lazy_register_unit yr 365.25 * day
170+
@_lazy_register_unit inch 2.54 * cm
171+
@_lazy_register_unit ft 12 * inch
172+
@_lazy_register_unit mi 5280 * ft
170173

171174
@add_prefixes min ()
172175
@add_prefixes minute ()
@@ -178,7 +181,7 @@ end
178181
@add_prefixes yr (k, M, G)
179182

180183
## Volume
181-
@register_unit L dm^3
184+
@_lazy_register_unit L dm^3
182185

183186
@add_prefixes L (μ, u, m, c, d)
184187

@@ -188,7 +191,7 @@ end
188191
)
189192

190193
## Pressure
191-
@register_unit bar 100 * kPa
194+
@_lazy_register_unit bar 100 * kPa
192195

193196
@add_prefixes bar (m,)
194197

@@ -203,9 +206,7 @@ end
203206
# Do not wish to define physical constants, as the number of symbols might lead to ambiguity.
204207
# The user should define these instead.
205208

206-
"""A tuple of all possible unit symbols."""
207-
const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS)
208-
const UNIT_VALUES = Tuple(_UNIT_VALUES)
209-
const UNIT_MAPPING = NamedTuple([s => i for (i, s) in enumerate(UNIT_SYMBOLS)])
209+
# Update `UNIT_MAPPING` with all internally defined unit symbols.
210+
merge!(UNIT_MAPPING, Dict(UNIT_SYMBOLS .=> 1:lastindex(UNIT_SYMBOLS)))
210211

211212
end

test/runtests.jl

-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ else
1919
@safetestset "Measurements.jl integration tests" begin
2020
include("test_measurements.jl")
2121
end
22-
@safetestset "Meshes.jl integration tests" begin
23-
include("test_meshes.jl")
24-
end
2522
@safetestset "Unit tests" begin
2623
include("unittests.jl")
2724
end

test/unittests.jl

+34-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using DynamicQuantities
22
using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions
3-
using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
3+
using DynamicQuantities:
4+
DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
45
using DynamicQuantities: array_type, value_type, dim_type, quantity_type
56
using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof
67
using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value
8+
using DynamicQuantities: UNIT_VALUES, UNIT_MAPPING, UNIT_SYMBOLS, ALL_MAPPING, ALL_SYMBOLS, ALL_VALUES
79
using DynamicQuantities: map_dimensions
810
using Ratios: SimpleRatio
911
using SaferIntegers: SafeInt16
@@ -686,8 +688,8 @@ end
686688
@test !iszero(sym)
687689
end
688690

689-
q = 1.5us"km/s"
690-
@test q == 1.5 * us"km" / us"s"
691+
q = 1.5us"km/s"
692+
@test q == 1.5 * us"km" / us"s"
691693
@test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE})
692694
@test string(dimension(q)) == "s⁻¹ km"
693695
@test uexpand(q) == 1.5u"km/s"
@@ -1727,7 +1729,7 @@ end
17271729
) isa SymbolicDimensions{Int32}
17281730

17291731
@test copy(km) == km
1730-
1732+
17311733
# Any operation should immediately convert it:
17321734
@test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R}
17331735

@@ -1848,3 +1850,31 @@ end
18481850
y = Quantity(2.0im, mass=1)
18491851
@test_throws DimensionError x^y
18501852
end
1853+
1854+
# `@testset` rewrites the test block with a `let...end`, resulting in an invalid
1855+
# local `const` (ref: src/units.jl:26). To avoid it, register units outside the
1856+
# test block.
1857+
map_count_before_registering = length(UNIT_MAPPING)
1858+
all_map_count_before_registering = length(ALL_MAPPING)
1859+
@register_unit MyV u"V"
1860+
@register_unit MySV us"V"
1861+
@register_unit MySV2 us"km/h"
1862+
1863+
@testset "Register Unit" begin
1864+
@test MyV === u"V"
1865+
@test MyV == us"V"
1866+
@test MySV == us"V"
1867+
@test MySV2 == us"km/h"
1868+
1869+
@test length(UNIT_MAPPING) == map_count_before_registering + 3
1870+
@test length(ALL_MAPPING) == all_map_count_before_registering + 3
1871+
1872+
for my_unit in (MySV, MyV)
1873+
@test my_unit in UNIT_VALUES
1874+
@test my_unit in ALL_VALUES
1875+
end
1876+
for my_unit in (:MySV, :MyV)
1877+
@test my_unit in UNIT_SYMBOLS
1878+
@test my_unit in ALL_SYMBOLS
1879+
end
1880+
end

0 commit comments

Comments
 (0)