=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:
import unchained let x = 10.m let y = 10.Meter doAssert x == y
import unchained let x = 10.Mm # mega meter let y = 5.ng # nano gram let z = 10.aT # atto tesla
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
import unchained let x = 5.kg + 5.lbs doAssert typeof(x) is kg doAssert x == 7.26796.kg
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 usdoAssert x.to(km•h⁻¹) == 18.km•h⁻¹
toDef
macro can be used to both define and convert a unit,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
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=
import 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
A longer snippet showing different features below. See also [[examples/bethe_bloch.nim]] for a more complicated use case.
import unchained block:
let mass = 5.kg let a = 9.81.m•s⁻² block:
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:
quantity
but different scalelet a = 5.kg let b = 500.g doAssert typeof(a + b) is KiloGram doAssert a + b == 5.5.kg
block:
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⁻²
let F: Newton = mass a let F2: Newton = a mass
doAssert typeof(F / mass) is N•kg⁻¹ doAssert typeof((F / mass).to(Meter•Second⁻²)) is Meter•Second⁻² doAssert F / mass == a block:
let force = 1.kg 1.m 1.s⁻² echo force # 1 Newton doAssert typeof(force) is Newton block:
let f = 10.N doAssert typeof(f.to(kN)) is KiloNewton doAssert f.to(kN) == 0.01.kN block:
let E_e⁻_rest: Joule = m_e cc # math operations *cannot*
use superscripts!
from std/math import sin
block:
let x = 5.kg let y = 10.kg discard sin(x / y) ## compiles gives correct result (~0.48) let x2 = 10.m
block:
let mass = 100.lbs let distance = 100.inch block:
let m1 = 100.lbs let m2 = 10.kg doAssert typeof(m1 + m2) is KiloGram doAssert m1 + m2 == 55.359237.KiloGram block:
let speed = (0.1 * c).toNaturalUnit() # fraction of c, defined in constants
let m_e = 9.1093837015e-31.kg.toNaturalUnit()
let p = speed * m_e # result will be in eV
doAssert p.to(keV) == 51.099874.keV
when false:
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:
.
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•
and superscript is to circumvent Nim's identifier
rules!** Why "Unchained"? Un = Unit Chain = [[https://en.wikipedia.org/wiki/Chain_(unit)][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...
** Units and ~cligen~
~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
km/h : 12 km•h⁻¹ mph : 7.2 mi•h⁻¹ m/s : 3.2 m•s⁻¹ :end:
which outputs:
km/h : 12 km•h⁻¹ mph : 7.2 mi•h⁻¹ m/s : 3.2 m•s⁻¹