SkySpark by SkyFoundry

17. Units

Overview

All numbers in Folio and Axon may be annotated with an optional unit. Units are checked and carry through on all numeric operations.

Unit System

As a general principle, all the data associated with a given site should exclusively use either the SI metric system or the US customary system. Mixing unit systems within one site will cause serious headaches for analytics which require temperature/pressure constants or correlation of variables of different units. All the standard libraries assume that data within a given site (and its associated weather station) are consistently either metric or US customary.

If you need to define a default constant in an function library designed to be used globally, then the convention is to create function which takes a site record. A good example is degreeDaysBase which defines a default degree-day balance point:

(site) => do
  if (isMetric(site)) 18°C else 65°F
end

Also see the isMetric function in the core library.

Database

The unit database used by SkySpark is defined by the Unit Fantom API. It is stored as a text file under "etc/sys/units.txt". Any unit identifier may be used immediately after a number literal to associate a unit:

23celsius         // full name allowed
102.5square_meter
23°C              // symbol preferred
102.5m²

By convention the symbol is the preferred notation. All encoding to Zinc uses the symbol, not the full name.

Comparison

Axon equality operators for numbers checks both the scalar and the unit. However if one number has a null unit, then the comparison is based only on the scalar:

123ft == 123ft  >>  true
123ft == 123    >>  true
123ft == 123m   >>  false

When performing greater or less than operations, the units much match or at least one must be null:

7ft > 3ft   >>  true
7ft > 3     >>  true
7ft > 3m    >>  raises UnitErr: ft <=> m

Arithmetic

Arithmetic with units is handled as follows:

// addition (associative)
a + a      >>  a
a + null   >>  a
°F + Δ°F   >>  °F
°C + Δ°C   >>  °C
a + b      >>  throws UnitErr

// subtraction
a - a      >>  a
a - null   >>  a
null - a   >>  a
°F - °F    >>  Δ°F
°C - °C    >>  Δ°C
°F - Δ°F   >>  °F
°C - Δ°C   >>  °C
a - b      >>  throws UnitErr

// multiplication (associative)
a * b      >>  a * b
a * null   >>  a

// division
a / b      >>  a / b
a / null   >>  a
null / b   >>  throws UnitErr

// modulo (remainder)
a % null   >>  a
a % b      >>  throws UnitErr
null % b   >>  throws UnitErr

As a general rule, if one of the numbers has a null unit, then the other operand's unit is carried through. Adding or subtracing two numbers with different units will raise an exception - automatic unit conversion is not performed. We also make special allowances for °F and °C since these temperature systems are not zero based.

Note that multiplication and division will attempt to derive the unit based on the unit of the operands. The derived unit must be matched against a unit predefined by the unit database or else an UnitErr is thrown.

Examples:

12kg + 5       >>  17 kg
12kg + 5kg     >>  17 kg
12kg + 5lb     >>  UnitErr: kg + lb
75°F - 50°F    >>  25 Δ°F
400kW * 2h     >>  800 kWh
800kW / 200m²  >>  4 kW/m²

Conversion

You can use the to function to convert between units:

65°F.to(1°C)     >>  18.333 °C
2000m².to(1ft²)  >>  21,527.83 ft²
100kWh.to(1BTU)  >>  341,280.104 BTU
100kWh.to(1L)    >>  Incovertable units: kWh and L

The scalar value of the to-unit is ignored, but by convention we use "1". Or you can use the unit string as follows:

65°F.to("°C")

Use the as function to change units without performing a conversion:

65°F.as(1°C)   >> 65°C
65°F.as("°C")  >> 65°C