UnitTypes.jl
This package provides physical units as Julia types.
julia> using UnitTypes
julia> x = Meter(3)
3m
julia> typeof(x)
Meter
julia> typeof(x) <: AbstractLength
true
julia> typeof(x) <: AbstractCapacitance
false
This allows you to easily write functions with arguments restricted to variables having certain types.
julia> function goFaster(a::T) where T<:AbstractAcceleration end
This leads to correctness and very clear error messages.
julia> goFaster(3u"m/s")
ERROR: MethodError: no method matching goFaster(::MeterPerSecond)
Closest candidates are:
goFaster(::AbstractAcceleration)
Type hierarchy
UnitTypes introduces an abstract type hierarchy of:
AbstractMeasure
├─ AbstractAcceleration
│ └─ MeterPerSecond2
├─ AbstractAngle
│ ├─ Degree
│ └─ Radian
├─ AbstractArea
│ ├─ Acre
│ ├─ Meter2
│ ├─ SquareFoot
│ └─ SquareMile
├...and so on
See docs/unitTypesTree.md for the full tree of predefined types.
Internally, a Measure is represented by
struct Meter <: AbstractLength
value::Number
toBase::Number
unit::String
end
and a Dimension by
struct Diameter{T <: AbstractLength } <: AbstractDimension
value::T
end
The idea is that a Measure is some quantity bearing units, while a Dimension is some context-specific application of a Measure.
Within a Dimension multiple Measures may logically be used as long as they are dimensionally consistent. For instance, a circle may be described by its radius, diameter, or circumference, concepts that can be interchangeably converted, using any Measure of extent (<:AbstractLength). A function creating a circle can then internally store radii while accepting Radius, Diameter, or Circumference arguments as the user prefers, since the type system provides conversion between the argument and the function's internal convention.
Please open an issue with a minimal working example if you run into conversion errors or think additional units should be defined by the package.
Introducing new types
Macros are used to introduce and create relationships around new types:
@makeBaseMeasure Length Meter "m"
- introduces a new basic Measure like Meter for Length or Meter3 Volume, this should be rarely used!@makeMeasure Meter(1000) = KiloMeter(1) "km"
- derives a new measure (KiloMeter) from some an existing measure (Meter) with a conversion ratio (1000m = 1km)@relateMeasures KiloGram*MeterPerSecond2=Newton
- relates the product of types to another type, all types preexisting.
For working with Dimensions:
@makeDimension Diameter Meter
- creates the Dimension Diameter measured in Meters@relateDimensions Diameter = 2.0*Radius
- relates the Dimensions Diameter and Radius by the scalar 2.0.
The macros in Measure.jl and Dimension.jl define the necessary convert()s and other operators. While these macros suffice for most units, defining nonlinear units (like temperature) requires additional plumbing. See the temperature converts in Temperature.jl for an example.
Logical operations
Using units correctly requires distinguishing between valid and invalid operations, which in some cases means not allowing apparently convenient operations. Inches can be added, as can inch and millimeter, but only when computing area does inch*inch make sense. Inch * 3 is convenient while 3 / Inch is unlikely to be desirable. These conceptual gotchas are especially obvious in affine units like Temperature, where 0°C + 10°F is not 42°F but rather -12.2°C.
With use, patience, and issues, these coherence rules will become more clear and explained by example.
Comparison with other packages
Unitful.jl
Unitful leverages parametric types to store units, giving flexibility at the cost of compile-time type uncertainty. It's two major limitations are the avoidance of angular measures, as they are not first-class entities but rather ratios, and rather lengthy type unions that clutter outputs, especially on error:
julia> function goSlower(x<:Unitful.Acceleration) end
goSlower (generic function with 1 method)
julia> goSlower(1u"mm")
ERROR: MethodError: no method matching goSlower(::Quantity{Int64, 𝐋 , Unitful.FreeUnits{(mm,), 𝐋 , nothing}})
Closest candidates are:
goSlower(::T) where T<:(Union{Quantity{T, 𝐋 𝐓^-2, U}, Level{L, S, Quantity{T, 𝐋 𝐓^-2, U}} where {L, S}} where {T, U})
As Unitful is the dominant unit package and has wide use and support, we provide a separate package ExchangeUnitful to enable interoperation with Unitful.
DynamicQuantities.jl
DynamicQuantities is newer and faster than Unitful because it "defines a simple statically-typed Quantity type for storing physical units." It does this by storing the exponents on the basic units, allowing any unit traceable to SI to be used. But this performant representation hurts readability, and while the unit representation may be able to be hidden behind overrides of show(), Julia is designed for types to be read and manipulated directly by users.
UnitTypes.jl
In the presence of Julia's type-heavy UI, these two, good attempts feel misdirected and motivate this package's literal typing of units. The limitation is that UnitTypes does not have a catch-all unit representation. Only units that have been defined by one of the macros may be represented, and complex units may need to have additional methods written to correctly convert between units. See Temperature.jl for an example of manual unit conversion.
Package macros and functions
UnitTypes.@makeBaseMeasure
— Macromacro makeBaseMeasure(quantityName, unitName, unitSymbol::String, isAffine=false)
Make a new base measure which has no relationship to an existing unit. For example, in @makeBaseMeasure Length Meter "m"
:
quantityName
is the name of the measure, 'Length' above.unitName
is the name of the unit which will be used to make measures bearing that unit, 'Meter' above.unitSymbol
is the abbreviation of the unit name, used in all string presentations of the measure.isAffine
is normally false, if true the +-/* operations are not added for this and derived units and need to be added by hand.
The macro will introduce AbstractLength <: AbstractMeasure
and Meter()
into the current scope.
Measures created by the macro have fields:
value::Number
raw value of the measuretoBase::Number
== 1 for base measuresunit::String
the unit to be displayed
To get the measure's value in the base unit as a float, see toBaseFloat().
UnitTypes.@makeMeasure
— Macromacro makeMeasure(relation, unit="NoUnit", defineConverts=true)
Creates a new Measure from an existing base measure. The left hand side of the equation must already exist, while the right hand side should be undefined, with the trailing string providing the unit symbol.
@makeMeasure Meter(1) = MilliMeter(1000) "mm"
The resulting types are defined in the containing module, not in UnitTypes, as seen by println(names(MyModule, all=true))
.
UnitTypes.@relateMeasures
— Macromacro relateMeasures(relation)
Adds a multiplicative relationship between the left and right sides of the equation, allowing units to be multiplied and divided with consistent units. All types must already be defined and only one * is supported on the left side, while the right should the resultant type.
@relateMeasures Meter*Newton = NewtonMeter
UnitTypes.@makeDimension
— Macromacro makeDimension(dimName, measure)
Make a new dimension dimName
of measure
; also creates 'AbstractdimName
'
``` @makeDimension Diameter Meter
d = Diameter(MilliMeter(3.4))
r = Radius(d)
```
UnitTypes.@relateDimensions
— Macromacro relateDimensions(relation)
Defines various Base.: functions that facilitate the given (linear) relationship. All types must already be defined and written in the form type1 = factor * type2
, as in: @relateDimensions Diameter = 2.0*Radius