Unchained
is a fully type safe, compile time only units
library. There is absolutely no performance loss over pure float
based code (aside from insertion of possible conversion factors, but
those would have to be written by hand otherwise of course).
It supports:
- all base SI units and (most) compound SI units
- units as short and long name:
import unchained let x = 10.m let y = 10.Meter doAssert x == y
- some imperial units
- all SI prefixes
import unchained let x = 10.Mm # mega meter let y = 5.ng # nano gram let z = 10.aT # atto tesla
- arbitrary math with units composing to new units, e.g. (which do not have
to be defined previously!),
import unchained let x = 10.m * 10.m * 10.m * 10.m * 10.m doAssert typeof(x) is Meter⁵
without having to predefine a
Meter⁵
type - automatic conversion between SI prefixes if a mix is used
import unchained let x = 5.kg + 5.lbs doAssert typeof(x) is kg doAssert x == 7.26796.kg
- manual conversion of units to compatible other units via
to
(e.g.import unchained let x = 5.m•s⁻¹ defUnit(km•h⁻¹) # needs to be defined to be able to convert to # `to` could be a macro that defines it for us doAssert x.to(km•h⁻¹) == 18.km•h⁻¹ # the `toDef` macro can be used to both define and convert a unit, # but under certain use cases it can break (see its documentation)
- comparisons between units compare real value taking into account SI prefixes and even different units of the same quantity:
import unchained
let x = 10.Mm # mega meter
doAssert x == 10_000_000.m
let y = 5.ng # nano gram
doAssert y == 5e-9.g
let z = 10.aT # atto tesla
doAssert z == 10e-18.T
# and even different units of same quantity
let a = 5000.inch•s⁻¹
let b = a.toDef(km•h⁻¹) # defines the unit and convers `a` to it
doAssert b == 457.2.km•h⁻¹
doAssert typeof(a) is inch•s⁻¹ # SI units have higher precedence than non SI
doAssert typeof(b) is km•h⁻¹
doAssert a == b # comparison is true, as the effective value is the same!
Note: comparison between units is performed using an almostEqual
implementation. By default it uses ε = 1e-8
. The power can be
changed at CT by using the -d:UnitCompareEpsilon=<integer>
where
the given integer is the negative power used.
- all quantities (e.g.
Length
,Mass
, …) defined as aconcept
to allow matching different units of same quantity in function argumentimport unchained proc force[M: Mass, A: Acceleration](m: M, a: A): Force = m * a let m = 80.kg let g = 9.81.m•s⁻² let f = force(m, g) doAssert typeof(f) is Newton doAssert f == 784.8.N
- define your own custom unit systems, see examples/custom_unit_system.nim
- …
A longer snippet showing different features below. See also examples/bethe_bloch.nim for a more complicated use case.
import unchained
block:
# defining simple units
let mass = 5.kg
let a = 9.81.m•s⁻²
block:
# addition and subtraction of same units
let a = 5.kg
let b = 10.kg
doAssert typeof(a + b) is KiloGram
doAssert a + b == 15.kg
doAssert typeof(a - b) is KiloGram
doAssert a - b == -5.kg
block:
# addition and subtraction of units of the same ``quantity`` but different scale
let a = 5.kg
let b = 500.g
doAssert typeof(a + b) is KiloGram
doAssert a + b == 5.5.kg
# if units do not match, the SI unit is used!
block:
# product of prefixed SI unit keeps same prefix unless multiple units of same quantity involved
let a = 1.m•s⁻²
let b = 500.g
doAssert typeof(a * b) is Gram•Meter•Second⁻²
doAssert typeof((a * b).to(MilliNewton)) is MilliNewton
doAssert a * b == 500.g•m•s⁻²
block:
let mass = 5.kg
let a = 9.81.m•s⁻²
# unit multiplication has to be commutative
let F: Newton = mass * a
let F2: Newton = a * mass
# unit division works as expected
doAssert typeof(F / mass) is N•kg⁻¹
doAssert typeof((F / mass).to(Meter•Second⁻²)) is Meter•Second⁻²
doAssert F / mass == a
block:
# automatic deduction of compound units for simple cases
let force = 1.kg * 1.m * 1.s⁻²
echo force # 1 Newton
doAssert typeof(force) is Newton
block:
# conversion between units of the same quantity
let f = 10.N
doAssert typeof(f.to(kN)) is KiloNewton
doAssert f.to(kN) == 0.01.kN
block:
# pre-defined physical constants
let E_e⁻_rest: Joule = m_e * c*c # math operations `*cannot*` use superscripts!
# m_e = electron mass in kg
# c = speed of light in vacuum in m/s
from std/math import sin
block:
# automatic CT error if argument of e.g. sin, ln are not unit less
let x = 5.kg
let y = 10.kg
discard sin(x / y) ## compiles gives correct result (~0.48)
let x2 = 10.m
# sin(x2 / y) ## errors at CT due to non unit less argument
block:
# imperial units
let mass = 100.lbs
let distance = 100.inch
block:
# mixing of non SI and SI units (via conversion to SI units)
let m1 = 100.lbs
let m2 = 10.kg
doAssert typeof(m1 + m2) is KiloGram
doAssert m1 + m2 == 55.359237.KiloGram
block:
# natural unit conversions
let speed = (0.1 * c).toNaturalUnit() # fraction of c, defined in `constants`
let m_e = 9.1093837015e-31.kg.toNaturalUnit()
# math between natural units remains natural
let p = speed * m_e # result will be in `eV`
doAssert p.to(keV) == 51.099874.keV
## If there is demand the following kind of syntax may be implemented in the future
when false:
# units using english language (using accented quotes)
let a = 10.`meter per second squared`
let b = 5.`kilogram meter per second squared`
check typeof(a) is Meter•Second⁻²
check typeof(b) is Newton
check a == 10.m•s⁻²
check b == 5.N
Things to note:
- real units use capital letters and are verbose
- shorthands defined for all typical units using their common
abbreviation (upper or lower case depending on the unit, e.g.
s
(second) andN
(Newton) - conversion of numbers to units done using `.` call and using shorthand names
- `•` symbol is product of units to allow unambiguous parsing of units -> specific unicode symbol may become user customizable in the future
- no division of units, but negative exponents
- exponents are in superscript
- usage of `•` and superscript is to circumvent Nim’s identifier rules!
- SI units are the base. If ambiguous operation that can be solved by
unit conversion, SI units are used (in the default SI unit system
predefined when simply importing
unchained
) - math operations cannot use superscripts!
- some physical constants are defined, more likely in the future
- conversion from prefixed SI unit to non prefixed SI unit only happens if multiple prefixed units of same quantity involved
UnitLess
is adistinct float
unit that has a converter tofloat
(such thatUnitLess
magically works with math functions expecting floats).
Un = Unit Chain = A unit
You shall be unchained from the shackles of dealing with painful errors due to unit mismatches by using this lib! Tada!
Hint: The unit Chain
does not exist in this library…
cligen
is arguably the most powerful and at the same time convenient
to use command line argument parser in Nim land (and likely across
languages…; plus a lot of other things!).
For that reason it is a common desire to combine Unchained
units as
an command line argument to a program that uses cligen
to parse the
arguments. Thanks to cligen's
extensive options to expand its
features, we now provide a simple submodule you can import in order to
support Unchained
units in your program. Here’s a short example
useful for the runners among you, a simple script to convert a given
speed (in mph, km/h or m/s) to a time per minute / per mile / 5K / 10K
/ … distance or vice versa:
import unchained, math, strutils
defUnit(mi•h⁻¹)
defUnit(km•h⁻¹)
defUnit(m•s⁻¹)
proc timeStr[T: Time](t: T): string =
let (h, mr) = splitDecimal(t.to(Hour).float)
let (m, s) = splitDecimal(mr.Hour.to(Minute).float)
result =
align(pretty(h.Hour, 0, true, ffDecimal), 6, ' ') &
" " & align(pretty(m.Minute, 0, true, ffDecimal), 8, ' ') &
" " & align(pretty(s.Minute.to(Second), 0, true, ffDecimal), 6, ' ')
template print(d, x) = echo "$#: $#" % [alignLeft(d, 9), align(x, 10)]
proc echoTimes[V: Velocity](v: V) =
print("1K", timeStr 1.0 / (v / 1.km))
print("1 mile", timeStr 1.0 / (v / 1.Mile))
print("5K", timeStr 1.0 / (v / 5.km))
print("10K", timeStr 1.0 / (v / 10.km))
print("Half", timeStr 1.0 / (v / (42.195.km / 2.0)))
print("Marathon", timeStr 1.0 / (v / 42.195.km))
print("50K", timeStr 1.0 / (v / 50.km))
print("100K", timeStr 1.0 / (v / 100.km)) # maybe a bit aspirational at the same pace, huh?
print("100 mile", timeStr 1.0 / (v / 100.Mile)) # let's hope it's not Leadville
proc mph(v: mi•h⁻¹) = echoTimes(v)
proc kmh(v: km•h⁻¹) = echoTimes(v)
proc mps(v: m•s⁻¹) = echoTimes(v)
proc speed(d: km, hour = 0.0.h, min = 0.0.min, sec = 0.0.s) =
let t = hour + min + sec
print("km/h", pretty((d / t).to(km•h⁻¹), 2, true))
print("mph", pretty((d / t).to(mi•h⁻¹), 2, true))
print("m/s", pretty((d / t).to( m•s⁻¹), 2, true))
when isMainModule:
import unchained / cligenParseUnits # just import this and then you can use `unchained` units as parameters!
import cligen
dispatchMulti([mph], [kmh], [mps], [speed])
nim c examples/speed_tool
examples/speed_tool mph -v 7.0 # without unit, assumed is m•h⁻¹
echo "----------------------------------------"
examples/speed_tool kmh -v 12.5.km•h⁻¹ # with explicit unit
echo "----------------------------------------"
examples/speed_tool speed -d 11.24.km --min 58 --sec 4
which outputs:
1K : 0 h 5 min 20 s
1 mile : 0 h 8 min 34 s
5K : 0 h 26 min 38 s
10K : 0 h 53 min 16 s
Half : 1 h 52 min 22 s
Marathon : 3 h 44 min 44 s
50K : 4 h 26 min 18 s
100K : 8 h 52 min 36 s
100 mile : 14 h 17 min 9 s
----------------------------------------
1K : 0 h 4 min 48 s
1 mile : 0 h 7 min 43 s
5K : 0 h 24 min 0 s
10K : 0 h 48 min 0 s
Half : 1 h 41 min 16 s
Marathon : 3 h 22 min 32 s
50K : 4 h 0 min 0 s
100K : 8 h 0 min 0 s
100 mile : 12 h 52 min 29 s
----------------------------------------
km/h : 12 km•h⁻¹
mph : 7.2 mi•h⁻¹
m/s : 3.2 m•s⁻¹