A python library for tracking and verifying dimensional units of measure.
For background see the dimensional analysis wikipedia entry.
- A Tour of dimana
- Past, Present, and Future
- About This Document
dimana values can be parsed with the
>>> from dimana import Value >>> reward = Value('12.5 [BTC]') >>> reward <Value '12.5 [BTC]'>
The grammar for units can handle powers expressed with the
and a single division with the
>>> Value('9.807 [meter/sec^2]') <Value '9.807 [meter / sec^2]'>
Values track their units through arithmetic operations:
>>> time = Value('10 [min]') >>> rate = reward / time >>> rate <Value '1.25 [BTC / min]'>
Incoherent operations raise exceptions:
>>> reward + time # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... UnitsMismatch: 'BTC' does not match 'min'
A value associates a scalar amount with dimensional units. These
are available on the instance as
>>> rate.amount Decimal('1.25') >>> rate.units <Units 'BTC / min'>
The scalar amount of a value is represented with
instance on the
>>> reward.amount Decimal('12.5')
Arithmetic operations rely on the decimal library for arithmetic logic, including precision tracking:
>>> reward * Value('713.078000 [USD / BTC]') <Value '8913.4750000 [USD]'>
Units are available in the
units attribute of
instances. They are instances of
dimana.Units. You can parse
>>> from dimana import Units >>> meter = Units('meter') >>> meter <Units 'meter'> >>> sec = Units('sec') >>> sec <Units 'sec'>
There are four ways to create values:
- parsing a 'value text' with the constructor:
- as the result of arithmetic operations on other values,
- with the explicit constructor,
- by calling a
The first two are described above, the last two next:
Values can be constructed explicitly directly given
>>> from dimana import Value >>> from decimal import Decimal >>> Value(Decimal('23.50'), meter) <Value '23.50 [meter]'>
Note that this constructor is strict about types and the first argument must be a
>>> Value(7, meter) Traceback (most recent call last): ... TypeError: Expected 'Decimal', found 'int'
Many applications require a finite statically known set of
instances, and then need to create
Value instances from specific
Units instances. This is more specific (thus safer) when
the units are already known than calling the
Value constructor which
returns a value with arbitrary units.
>>> from decimal import Decimal >>> from dimana import Value, Units >>> METER = Units('METER') >>> userinput = '163' # In an application this might be from arbitrary input. >>> height = Value(Decimal(userinput), METER) >>> height <Value '163 [METER]'>
Because this pattern is so common,
Units instances support parsing
an amount directly by calling
>>> height2 = METER(userinput) >>> height == height2 True
Units instances matches the
'canonical parsing format':
>>> trolls = Value('3 [troll]') >>> print(trolls) 3 [troll] >>> trolls == Value(str(trolls)) True
repr() of these class instances contains the class name and the
>>> print(repr(trolls)) <Value '3 [troll]'> >>> print(repr(trolls.units)) <Units 'troll'>
This section explores the
Units class more closely.
Because the 0 and 1 amounts are very common, they are available as
attributes of a
>>> meter.zero <Value '0 [meter]'> >>> sec.one <Value '1 [sec]'>
The base case of units with 'no dimension' is available as
Scalar. This instance of
Units represents, for example,
>>> from dimana import Scalar >>> total = Value('125 [meter]') >>> current = Value('15 [meter]') >>> completion = current / total >>> completion <Value '0.12'> >>> completion.units is Scalar True
Parsing a value which does not specify units produces a scalar value:
>>> completion == Value('0.12') True
By design, dimana does not do implicit coercion of float instances into Value instances to help avoid numeric bugs:
>>> experience = Value('42 [XP]') >>> experience * 1.25 Traceback (most recent call last): ... TypeError: Expected 'Value', found 'float'
Scalar is necessary in these cases. Parsing
a value with no units specification gives a 'scalar value':
>>> experience * Value('1.25') <Value '52.50 [XP]'>
There is a single instance of
Units for each combination of unit:
>>> (meter + meter) is meter True >>> (meter / sec) is Units('meter / sec') True
Thus, to test if two
Units instances represent the same units,
just use the
>>> if meter is (Units('meter / sec') * sec): ... print('Yes, it is meters.') ... Yes, it is meters.
Units.match method does such a check and raises
if the units do not match:
>>> meter.match(Units('meter / sec') * sec) >>> meter.match(Units('meter / sec^2') * sec) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... UnitsMismatch: 'meter' does not match 'meter / sec'
This uniqueness depends globally on the unit string names, so if a large
application depended on two completely separate libraries, each of which
rely on dimana, and both libraries define
<Units 's'> they will
be using the same instance. This could be a problem if, for example,
one library uses the
S to represent seconds while the other uses
it to represent Siemens.
Each instance of
Units persists to the end of the process, so
Units dynamically could present a resource management
problem, especially if a malicious entity can instantiate arbitrary
(The plan is to wait for real life applications that encounter these problems before adding complexity to this package.)
There is no definite roadmap other than to adapt to existing users' needs. However, some potential new features would be:
- Python 3 support with an identical API.
- Support for more numeric operations.
- More streamlined interaction with
decimal, such as for rounding a
Valueto a given precision.
- Add an 'expression evaluator' for quick-and-easy interactive interpreter
- Add a commandline wrapper around
- Removed old class-scoped APIs, such as parse methods, in favor of using constructors directly.
- Added tox support for python 2.7 and 3.5.
- Extended the README.rst to have a more complete overview, a future roadmap, and this changelog.
- Made several breaking API changes:
- Now toplevel
dimanaonly publicly exposes
- Renamed the old
- Now toplevel
- Added code examples in README.rst and hooked doctests of that documentation into the unittest suite.
- Pivoted the API to the separation between
Unitswith the two
- Strict requirement of
Decimalinstances without implicit coercion.
The 0.1 line of dimana had a very different interface based on a single Dimana class, and a more rudimentary parser, and was generally a messier proof-of-concept.
- There was no representation of the modern
Unitsinstances, rather only the equivalent of
- It used dynamic type generation for what is now each instance of
- It had less obvious error messages and less complete unit testing.
- It had no documentation and no doctests.
There appears to be no way to accurately test exception details with doctest for both python 2 and 3. The best option seems to be to ignore exception details. :-<