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
falseThis allows you to easily write functions with arguments restricted to variables having certain types.
julia> function goFaster(a::T) where T<:AbstractAcceleration endThis 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
│ ├─ Foot2
│ ├─ Inch2
│ ├─ Meter2
│ └─ Mile2
├...and so onSee docs/unitTypesTree.md for the full tree of predefined types.
Internally, a Measure is represented by
struct Meter <: AbstractLength
value::Float64 # the value as measured in this unit
endThe unit abbreviation and conversion function to/from the base unit are stored in UnitTypes.allUnitTypes, a package-level dictionary keyed by type.
A Dimension wraps a Measure to add geometric or physical context:
struct Diameter{T <: AbstractLength } <: AbstractDimension
measure::T
endThe 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 for Volume, this should be rarely used!@makeMeasure 1e3 Meter = 1 KiloMeter "km"- derives a new measure (KiloMeter) from 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 convert()s and other operators needed for common operations. If these are insufficient you will receive undefined method errors and can then work around the missing defitions, define them yourself, and/or open an issue.
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.
Naming conventions
Combining the names of units to get the resulting type name follows these simple rules:
- Multiplication is concatenation: Newton * Meter = NewtonMeter (==MeterNewton)
- Division and negative exponents are indicated by
Per: N*m/s^2 = NewtonMeterPerSecond2 - Numeric powers are preferred over words: Meter2, not SquareMeter
- No plurals: Meter, not Meters
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. Only units that have been defined by one of the macros may be represented with a named type. Arithmetic that produces a novel unit combination falls back to Catchall, which carries a dimension map but lacks a named type. Complex units may also need additional methods to correctly convert between units — see Temperature.jl for an example of manual unit conversion.
Package macros and functions
Measure.jl
UnitTypes.@makeBaseMeasure — Macro
macro makeBaseMeasure(quantityName, unitName, abbreviation::String, isAffine=false)
Make a new base measure which has no relationship to an existing unit. For example, in @makeBaseMeasure Length Meter "m":
quantityNameis the name of the measure, 'Length' above.unitNameis the name of the unit which will be used to make measures bearing that unit, 'Meter' above.abbreviationis the abbreviation of the unit name, used in all string presentations of the measure.
The macro will introduce AbstractLength <: AbstractMeasure and Meter() into the current scope.
To get the measure's value in the base unit as a float, see toBaseFloat().
UnitTypes.@makeMeasure — Macro
macro makeMeasure(xFactor, relation, newType, newAbbreviation)
Creates a new Measure from an existing base measure using leading conversion factors. The ExistingUnitType must already exist; NewUnitType will be created. The equation xFactor * ExistingUnitType = yFactor * NewUnitType defines the conversion. @makeMeasure 1e-3 Meter = 1 MilliMeter "mm"
Affine units (like Temperature) provide parenthesized anonymous functions as factors. xFactor converts NewUnitType values to ExistingUnitType; yFactor is the inverse. @makeMeasure (f->(f+459.67)*5/9) Kelvin = (k->k*9/5-459.67) Fahrenheit "°F"
UnitTypes.@relateMeasures — Macro
macro 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; the right side is the resultant type.
Single binary operations: @relateMeasures Meter*Newton = NewtonMeter @relateMeasures Newton/Meter2 = Pascal @relateMeasures 1/Second = Hertz # inverse: sets Hertz dims to {AbstractTime=>-1}
Compound expressions with multiple * and / are also supported. Intermediate types are created automatically, named by concatenating type names (*) or inserting "Per" (/): @makeBaseMeasure Force Newton "N" @relateMeasures KiloGram*Meter/Second/Second = Newton # creates KiloGramMeter and KiloGramMeterPerSecond as intermediate types
UnitTypes.@u_str — Macro
macro u_str(unit::String)
Macro to provide the 1.2u"cm" inline unit assignment. a = 1.2u"cm"
This works by looking up the unit string in allUnitTypes and returning the corresponding type. See https://docs.julialang.org/en/v1/manual/metaprogramming/#meta-non-standard-string-literals
UnitTypes.toBaseFloat — Function
toBaseFloat(m::AbstractMeasure) :: Float64
Returns measure m as a float in the base unit.
UnitTypes.abbreviation — Function
abbreviation(m::AbstractMeasure)::String
Returns the unit string for m.
Catchall.jl
UnitTypes.Catchall — Type
struct Catchall <: AbstractMeasure
Catch-all for unit expressions with no defined named type. The stored value is in SI base units; dimensions maps abstract dimension types to their integer exponents, e.g. {AbstractLength=>1, AbstractTime=>-1} for velocity.
Prefer registered named types (Meter, Second, Newton …) wherever possible. Catchall is produced only when arithmetic or u_str parsing yields a combination that has no matching entry in allUnitTypes.
UnitTypes.parseCatchall — Function
parseCatchall(str) -> Union{AbstractMeasure, Nothing}
Parses a compound unit string such as "mm*s/kg" or "m^2" into a measure. Each token is looked up by abbreviation in allUnitTypes; the combined scale factor and dimension map are computed, and resolveOrExpr is applied so a named type is returned whenever one matches.
Returns nothing if any token is unrecognised or affine (Temperature-style).
Syntax supported:
*separates numerator factors/introduces denominator factors (applies to all tokens after the first/per*-group)^Napplies an integer exponent (may be negative) to the preceding abbreviation
UnitTypes.UnitStepRange — Type
struct UnitStepRange{T<:AbstractMeasure}
A lazy, allocation-free stepped range over a consistent unit type. Constructed by the start:step:stop colon syntax when all three share the same abstract dimension. The element type is determined by start; step and stop are converted to match.
Dimension.jl
UnitTypes.@makeDimension — Macro
macro 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 — Macro
macro 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