# dimana Release 0.3.0

Dimensional Analysis - arithmetic with measurement units.

SSPL-1.0
Install
``` pip install dimana==0.3.0 ```

# _DIM_ensional _ANA_lysis

A python library for tracking and verifying dimensional units of measure.

For background see the dimensional analysis wikipedia entry.

## A Tour of dimana

### Parsing Values

dimana values can be parsed with the `Value` constructor:

```>>> from dimana import Value
>>> reward = Value('12.5 [BTC]')
>>> reward
<Value '12.5 [BTC]'>```

The grammar for units can handle powers expressed with the `^` symbol, and a single division with the `/` symbol:

```>>> Value('9.807 [meter/sec^2]')
<Value '9.807 [meter / sec^2]'>```

### Arithmetic Operations

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'```

### Value Components

A value associates a scalar amount with dimensional units. These are available on the instance as `amount` and `units`:

```>>> rate.amount
Decimal('1.25')
>>> rate.units
<Units 'BTC / min'>```

#### Amounts

The scalar amount of a value is represented with `decimal.Decimal` instance on the `amount` attribute:

```>>> 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

Units are available in the `units` attribute of `Value` instances. They are instances of `dimana.Units`. You can parse `Units` instances directly:

```>>> from dimana import Units
>>> meter = Units('meter')
>>> meter
<Units 'meter'>
>>> sec = Units('sec')
>>> sec
<Units 'sec'>```

### Constructing Values

There are four ways to create values:

• parsing a 'value text' with the constructor: `Value`,
• as the result of arithmetic operations on other values,
• with the explicit constructor,
• by calling a `Units` instance.

The first two are described above, the last two next:

#### Explicit Value Constructor

Values can be constructed explicitly directly given `Decimal` and `Units` instances:

```>>> 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 `decimal.Decimal`:

```>>> Value(7, meter)
Traceback (most recent call last):
...
TypeError: Expected 'Decimal', found 'int'```

#### Calling Units

Many applications require a finite statically known set of `Units` instances, and then need to create `Value` instances from specific explicit `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.

For example:

```>>> 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 `Units` instances:

```>>> height2 = METER(userinput)
>>> height == height2
True```

### str() and repr()

The `str()`-ification of `Value` and `Units` instances matches the 'canonical parsing format':

```>>> trolls = Value('3 [troll]')
>>> print(trolls)
3 [troll]
>>> trolls == Value(str(trolls))
True```

The `repr()` of these class instances contains the class name and the `str()`-ification:

```>>> print(repr(trolls))
<Value '3 [troll]'>
>>> print(repr(trolls.units))
<Units 'troll'>```

This section explores the `Units` class more closely.

#### `zero` and `one`

Because the 0 and 1 amounts are very common, they are available as attributes of a `Units` instance:

```>>> meter.zero
<Value '0 [meter]'>
>>> sec.one
<Value '1 [sec]'>```

#### Scalar Units

The base case of units with 'no dimension' is available as `Scalar`. This instance of `Units` represents, for example, ratios:

```>>> 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'```

Using `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]'>```

#### Units Uniqueness and Matching

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 `is` operator:

```>>> if meter is (Units('meter / sec') * sec):
...     print('Yes, it is meters.')
...
Yes, it is meters.```

The `Units.match` method does such a check and raises `UnitsMismatch` 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'```
##### Uniqueness Implications

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 instantiating `Units` dynamically could present a resource management problem, especially if a malicious entity can instantiate arbitrary unit types.

(The plan is to wait for real life applications that encounter these problems before adding complexity to this package.)

## Past, Present, and Future

### Future

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 `Value` to a given precision.
• Add an 'expression evaluator' for quick-and-easy interactive interpreter calculations, eg: `dimana.eval`
• Add a commandline wrapper around `eval`.

### Changelog

#### 0.3

• 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.

#### 0.2.1

• Extended the README.rst to have a more complete overview, a future roadmap, and this changelog.
• Made several breaking API changes:
• Now toplevel `dimana` only publicly exposes `Units` and `Value`.
• Introduced `Units.from_string` parser.
• Introduced `zero` and `one` properties of `Units` instances.
• Renamed the old `Value.decimal` attribute to `Value.amount`.

#### 0.2.0

• Added code examples in README.rst and hooked doctests of that documentation into the unittest suite.
• Pivoted the API to the separation between `Value` and `Units` with the two `parse` methods.
• Strict requirement of `Decimal` instances without implicit coercion.

#### Before 0.2.0

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 `Units` instances, rather only the equivalent of `Value` instances.
• It used dynamic type generation for what is now each instance of `Units`.
• It had less obvious error messages and less complete unit testing.
• It had no documentation and no doctests.