Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/SciNim/Unchained

A fully type safe, compile time only units library.
https://github.com/SciNim/Unchained

compile-time hacktoberfest meta-programming nim nim-lang type-safety units

Last synced: about 2 months ago
JSON representation

A fully type safe, compile time only units library.

Awesome Lists containing this project

README

        

* Unchained - Compile time only units checking
[[https://github.com/SciNim/unchained/workflows/unchained%20CI/badge.svg]]

=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:
#+begin_src nim
import unchained
let x = 10.m
let y = 10.Meter
doAssert x == y
#+end_src
- some imperial units
- all SI prefixes
#+begin_src nim
import unchained
let x = 10.Mm # mega meter
let y = 5.ng # nano gram
let z = 10.aT # atto tesla
#+end_src
- arbitrary math with units composing to new units, e.g. (which do not have
to be defined previously!),
#+begin_src nim
import unchained
let x = 10.m * 10.m * 10.m * 10.m * 10.m
doAssert typeof(x) is Meter⁵
#+end_src
without having to predefine a =Meter⁵= type
- automatic conversion between SI prefixes if a mix is used
#+begin_src nim
import unchained
let x = 5.kg + 5.lbs
doAssert typeof(x) is kg
doAssert x == 7.26796.kg
#+end_src
- manual conversion of units to compatible other units via ~to~
(e.g.
#+begin_src nim
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)
#+end_src
- comparisons between units compare real value taking into account SI
prefixes and even different units of the same quantity:
#+begin_src nim
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!
#+end_src
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=~ where
the given integer is the negative power used.
- all quantities (e.g. ~Length~, ~Mass~, ...) defined as a ~concept~
to allow matching different units of same quantity in function
argument
#+begin_src nim
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
#+end_src
- 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.
#+begin_src nim
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
#+end_src

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)
and ~N~ (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 a =distinct float= unit that has a converter to
=float= (such that =UnitLess= magically works with math functions
expecting floats).

** 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:
#+begin_src nim :tangle examples/speed_tool.nim
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])
#+end_src

#+begin_src sh :results drawer
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
#+end_src

#+RESULTS:
:results:
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⁻¹
:end:

which outputs:
#+begin_src sh
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⁻¹
#+end_src