- API documentation for
coulomb
coulomb
tutorial- github discussions
- Your Data Type is a Unit
- Why Your Data Schema Should Include Units
- Preventing Configuration Errors With Unit Types
- Unit Types for Avro Schema: Integrating Avro with Coulomb
- Algorithmic Unit Analysis
- A Unit Analysis of Linear Regression
The coulomb
libraries currently require scala 2.13.0, or higher.
The core coulomb
package can be included by adding the dependencies shown below.
Note that its two 3rd-party dependencies -- spire
and singleton-ops
-- are %Provided
,
and so you must also include them, if your project does not already do so. The shapeless
package is also a dependency, but is included transitively via spire
.
libraryDependencies ++= Seq(
"com.manyangled" %% "coulomb" % "0.5.8",
"org.typelevel" %% "spire" % "0.18.0",
"eu.timepit" %% "singleton-ops" % "0.5.2"
)
The coulomb
project also provides a selection of predefined units, which are available as
separate sub-packages.
libraryDependencies ++= Seq(
"com.manyangled" %% "coulomb-si-units" % "0.5.8", // The seven SI units: meter, second, kilogram, etc
"com.manyangled" %% "coulomb-accepted-units" % "0.5.8", // Common non-SI metric: liter, centimeter, gram, etc
"com.manyangled" %% "coulomb-time-units" % "0.5.8", // minute, hour, day, week
"com.manyangled" %% "coulomb-info-units" % "0.5.8", // bit, byte, nat
"com.manyangled" %% "coulomb-mks-units" % "0.5.8", // MKS units: Joule, Newton, Watt, Volt, etc
"com.manyangled" %% "coulomb-customary-units" % "0.5.8", // non-metric units: foot, mile, pound, gallon, pint, etc
"com.manyangled" %% "coulomb-temp-units" % "0.5.8" // Celsius and Fahrenheit temperature scales
)
In addition to core functionality and fundamental units, coulomb provides the following packages.
- coulomb-cats - define some cats typeclass integrations for Quantity
- coulomb-refined - integrates coulomb Quantity with Refined values
- coulomb-scalacheck - scalacheck Arbitrary and Cogen for Quantity
- coulomb-parser - parsing a DSL for unit expressions into typed unit Quantity
- coulomb-avro - an integration package with Apache Avro schema and i/o
- coulomb-pureconfig - extends the pureconfig with awareness of unit Quantity
- coulomb-pureconfig-refined - integrate coulomb + pureconfig + refined
- coulomb-typesafe-config - unit awareness for the typesafe config library
The core coulomb package and several other sub-packages are cross-published to scala.js
- coulomb
- coulomb-cats
- coulomb-scalacheck
- coulomb-refined
- coulomb-si-units
- coulomb-mks-units
- coulomb-accepted-units
- coulomb-time-units
- coulomb-temp-units
- coulomb-info-units
- coulomb-customary-units
The coulomb
project supports the Scala Code of Conduct;
all contributors are expected to respect this code.
Any violations of this code of conduct should be reported to the author.
- Running Tutorial Examples
- Features
Quantity
and Unit Expressions- Quantity Values
- String Representations
- Predefined Units
- Unit Types and Convertability
- Unit Conversions
- Unit Operations
- Declaring New Units
- Unitless Quantities
- Unit Prefixes
- Using
WithUnit
- Type Safe Configurations
- Absolute Temperature and Time Values
- Working with Type Parameters and Type-Classes
- Compute Model for Quantity Operations
- Unit Conversions for Custom Value Types
Except where otherwise noted, the following tutorial examples can be run in a scala REPL as follows:
% cd /path/to/coulomb/repo
% sbt coulomb_testsJVM/console
scala> import shapeless._, coulomb._, coulomb.si._, coulomb.siprefix._, coulomb.mks._, coulomb.time._, coulomb.info._, coulomb.binprefix._, coulomb.accepted._, coulomb.us._, coulomb.temp._, coulomb.define._, coulomb.parser._
Examples making use of numeric quantity operations depend on corresponding typeclasses for numeric algebras. Alebras for the common scala and spire numeric types can be obtained this way:
scala> import spire.std.any._ // import algebras for common numeric types
scala> import spire.std.double._ // import algebras for Double
The coulomb
libraries provide the following features:
Allow a programmer to associate unit analysis with values, in the form of static types
val length = 10.withUnit[Meter]
val duration = (30.0).withUnit[Second]
val mass = Quantity[Float, Kilogram](100)
Express those types with arbitrary and natural static type expressions
val speed = (100.0).withUnit[(Kilo %* Meter) %/ Hour]
val acceleration = (9.8).withUnit[Meter %/ (Second %^ 2)]
Let the compiler determine which unit expressions are equivalent (aka convertable) and transparently convert between them
val mps: Quantity[Double, Meter %/ Second] = (60.0).withUnit[Mile %/ Hour]
Cause a compile-time error when operations are attempted with non-convertable unit types
val mps: Quantity[Double, Meter %/ Second] = (60.0).withUnit[Mile] // compile-time type error!
Automatically determine correct output unit types for operations on unit quantities
val mps: Quantity[Double, Meter %/ Second] = 60D.withUnit[Mile] / 1D.withUnit[Hour]
Allow a programmer to easily declare new units that will work seamlessly with existing units
// a new unit of length:
trait Smoot
implicit val defineUnitSmoot = DerivedUnit[Smoot, Inch](67, name = "Smoot", abbv = "Smt")
// a unit of acceleration:
trait EarthGravity
implicit val defineUnitEG = DerivedUnit[EarthGravity, Meter %/ (Second %^ 2)](9.8, abbv = "g")
coulomb
defines the
class Quantity
for representing values with associated units.
Quantities are represented by their two type parameters: A value type N
(typically a numeric type such as Int or Double)
and a unit type U
which represents the unit associated with the value.
Here are some simple declarations of Quantity
objects:
import coulomb._
import coulomb.si._
val length = 10.withUnit[Meter] // An Int value of meters
val duration = (30.0).withUnit[Second] // a Double value in seconds
val mass = Quantity[Float, Kilogram](100) // a Float value in kg
Three operator types can be used for building more complex unit types:
%*
, %/
, and %^
.
import coulomb._
import coulomb.si._
val area = 100.withUnit[Meter %* Meter] // unit product
val speed = 10.withUnit[Meter %/ Second] // unit ratio
val volume = 50.withUnit[Meter %^ 3] // unit power
Using these operators, units can be composed into unit type expressions of arbitrary complexity.
val acceleration = (9.8).withUnit[Meter %/ (Second %^ 2)]
val ohms = (0.01).withUnit[(Kilogram %* (Meter %^ 2)) %/ ((Second %^ 3) %* (Ampere %^ 2))]
The internal representation type of a Quantity
is given by its first type parameter.
Each quantity's value is accessible via the value
field
import coulomb._, coulomb.info._, coulomb.siprefix._
val memory = 100.withUnit[Giga %* Byte] // type is: Quantity[Int, Giga %* Byte]
val raw: Int = memory.value // memory's raw integer value
Standard Scala types Float
, Double
, Int
and Long
are supported, as well as
any other numeric type N
for which spire
algebra typeclasses are defined,
for example BigDecimal
or spire
Rational
.
Algebra typeclasses for standard value types may be imported via import spire.std.any._
Operations on coulomb Quantity objects require only the typeclasses they need to operate. If you wish to work with operations that do not require algebras, then these typeclasses do not need to exist:
scala> import coulomb._, coulomb.si._
scala> case class Foo(foo: String) // no algebras are defined for this type
defined class Foo
scala> Foo("goo").withUnit[Meter].show // 'show' requires no algebra typeclass
res0: String = Foo(goo) m
The show
method can be used to obtain a human-readable string that represents a quantity's
type and value using standard unit abbreviations. The showFull
method uses full unit names.
The methods showUnit
and showUnitFull
output only the unit without the value.
scala> val bandwidth = 10.withUnit[(Giga %* Bit) %/ Second]
bandwidth: coulomb.Quantity[...] = coulomb.Quantity@40240000
scala> bandwidth.show
res1: String = 10 Gb/s
scala> bandwidth.showFull
res2: String = 10 gigabit/second
scala> bandwidth.showUnit
res3: String = Gb/s
scala> bandwidth.showUnitFull
res4: String = gigabit/second
A variety of units and prefixes are predefined by several coulomb
sub-packages, which are summarized here.
The relation between the packages below and maven packages is at the top of this page.
- coulomb.si: The Standard International base units Meter, Kilogram, Second, Ampere, Kelvin, Mole and Candela.
- coulomb.siprefix: Standard International prefixes Kilo, Mega, Milli, Micro, etc.
- coulomb.mks: The common "Meter-Kilogram-Second" derived units.
- coulomb.accepted: A selection of units accepted by the Standard International system.
- coulomb.time: time units minute, hour, day, week
- coulomb.info: Units of information: Byte, Bit and Nat.
- coulomb.us: Some customary non-SI units commonly used in the United States.
- coulomb.binprefix: The binary prefixes Kibi, Mebi, Gibi, etc.
- coulomb.temp: Temperature units and scales for Fahrenheit and Celsius
The concept of unit convertability is fundamental to the coulomb
library and its
implementation of unit analysis.
Two unit type expressions are convertable if they encode an equivalent
"abstract quantity."
For example, Meter
and Mile
are convertable because they both encode the abstract quantity of length
.
Foot %^ 3
and Liter
are convertable because they both encode a volume, or length^3
.
Kilo %* Meter %/ Hour
and Foot %* (Second %^ -1)
are convertable because they encode a velocity, or length / time
.
In coulomb
, abstract quantities like length
are represented by a unique BaseUnit
.
For example the base unit for length is the type coulomb.si.Meter
.
Compound abstract quantities such as length / time
or length ^ 3
are internally represented
by pairs of base units with exponents:
velocity
<=>length / time
<=>(Meter ^ 1)(Second ^ -1)
volume
<=>length ^ 3
<=>(Meter ^ 3)
acceleration
<=>length / time^2
<=>(Meter ^ 1)(Second ^ -2)
bandwidth
<=>information / time
<=>(Byte ^ 1)(Second ^ -1)
In coulomb
, a unit quantity will be implicitly converted into a quantity of a different unit type whenever those types are convertable.
Any attempt to convert between non-convertable unit types results in a compile-time type error.
scala> def foo(q: Quantity[Double, Meter %/ Second]) = q.showFull
scala> foo(60f.withUnit[Mile %/ Hour])
res5: String = 26.8224 meter/second
scala> foo(1f.withUnit[Mile %/ Minute])
res6: String = 26.8224 meter/second
scala> foo(1f.withUnit[Foot %/ Day])
res7: String = 3.5277778E-6 meter/second
scala> foo(1f.withUnit[Foot %* Day])
error: type mismatch;
As described in the previous section, unit quantities can be converted from one unit type to another when the two types are convertable. Unit conversions come in a couple different forms:
// Implicit conversion
scala> val vol: Quantity[Double, Meter %^ 3] = 4000D.withUnit[Liter]
vol: coulomb.Quantity[Double,coulomb.si.Meter %^ 3] = Quantity(4.0)
scala> vol.showFull
res2: String = 4.0 meter^3
// Explicit conversion using the `toUnit` method
scala> 4000D.withUnit[Liter].toUnit[Meter %^ 3].showFull
res3: String = 4.0 meter^3
Unit quantities support math operations +
, -
, *
, /
, and pow
.
Quantities must be of convertable unit types to be added or subtracted.
The type of the left-hand argument is taken as the type of the output:
scala> (1.withUnit[Foot] + 1.withUnit[Yard]).show
res4: String = 4 ft
scala> (4.withUnit[Foot] - 1.withUnit[Yard]).show
res5: String = 1 ft
Quantities of any unit types may be multiplied or divided. Result types are different than either argument:
scala> (60.withUnit[Mile] / 1.withUnit[Hour]).show
res6: String = 60 mi/h
scala> (1.withUnit[Yard] * 1.withUnit[Yard]).show
res7: String = 1 yd^2
scala> (1.withUnit[Yard] / 1.withUnit[Inch]).toUnit[Percent].show
res8: String = 3600 %
When raising a unit to a power, the exponent is given as a literal type:
scala> 3D.withUnit[Meter].pow[2].show
res13: String = 9.0 m^2
scala> Rational(3).withUnit[Meter].pow[-1].show
res14: String = 1/3 m^(-1)
scala> 3.withUnit[Meter].pow[0].show
res15: String = 1 unitless
The coulomb
library strives to make it easy to add new units which work seamlessly with the unit analysis type system.
There are two varieties of unit declaration: base units and derived units.
A base unit, as its name suggests, is not defined in terms of any other unit; it is axiomatic.
The Standard International Base Units are all declared as base units in the coulomb.si
subpackage.
In the coulomb.info
sub-package, Byte
is declared as the base unit of information.
Declaring a base unit is special in the sense that it also defines a new kind of fundamental abstract quantity.
For example, by declaring coulomb.si.Meter
as a base unit, coulomb
establishes Meter
as the canonical representation of the abstract quantity of Length.
Any other unit of length must be declared as a derived unit of Meter
, or it would be considered non-convertable with other lengths.
Here is an example of defining a new base unit Scoville
, representing an abstract quantity of Spicy Heat.
The BaseUnit
value must be defined as an implicit value:
import coulomb._
import coulomb.define._ // BaseUnit and DerivedUnit
object SpiceUnits {
trait Scoville
implicit val defineUnitScoville = BaseUnit[Scoville](name = "scoville", abbv = "sco")
}
The second variety of unit declarations is the derived unit, which is defined in terms of some unit expression involving previously-defined units. Derived units do not define new kinds of abstract quantity, and are generally more common than base units:
object NewUnits {
import coulomb._, coulomb.define._, coulomb.si._, coulomb.us._
// a furlong is 660 feet
trait Furlong
implicit val defineUnitFurlong = DerivedUnit[Furlong, Foot](coef = 660, abbv = "flg")
// speed of sound is 1130 feet/second (at sea level, 20C)
trait Mach
implicit val defineUnitMach = DerivedUnit[Mach, Foot %/ Second](coef = 1130, abbv = "mach")
// a standard earth gravity is 9.807 meters per second-squared
// Define an abbreviation "g"
trait EarthGravity
implicit val defineUnitEG = DerivedUnit[EarthGravity, Meter %/ (Second %^ 2)](coef = 9.807, abbv = "g")
// The maximum ping time to the moon
// https://twitter.com/cmuratori/status/1219847348433481729
trait MoonUnit
implicit val defineUnitMoonUnit = DerivedUnit[MoonUnit, Second](coef = 2.71321035034, abbv = "moo")
}
Notice that there are no constraints or requirements associated with the unit types Scoville
, Furlong
, etc.
These may simply be declared, as shown above, however they may also be pre-existing types.
In other words, you may define any type, pre-existing or otherwise, to be a coulomb
unit by declaring the
appropriate implicit value.
Newer versions of coulomb allow Base Units to be implicitly inferred for any type,
even if no BaseUnit object has been specifically declared, by importing the undeclaredBaseUnits
policy:
scala> import coulomb.policy.undeclaredBaseUnits._
import coulomb.policy.undeclaredBaseUnits._
scala> case class Foo(goo: String)
defined class Foo
scala> (1.withUnit[Foo] + 1.withUnit[Kilo %* Foo]).show
res1: String = 1001 Foo
scala> (10.withUnit[Seq[Int]] / 5.withUnit[Second]).show
res2: String = 2 Seq[Int]/s
When units in an expression all cancel out -- for example, a ratio of quantities with convertable units -- the value is said to be "unitless".
In coulomb
the unit expression type Unitless
represents this particular state.
Here are a few examples of situations when Unitless
values arise:
// ratios of convertable unit types are always unitless
scala> (1.withUnit[Yard] / 1.withUnit[Foot]).toUnit[Unitless].show
res1: String = 3 unitless
// raising to the zeroth power
scala> 100.withUnit[Second].pow[0].show
res2: String = 1 unitless
// Radians and other angular units are derived from Unitless
scala> math.Pi.withUnit[Radian].toUnit[Unitless].show
res3: String = 3.141592653589793 unitless
// Percentages
scala> 90D.withUnit[Percent].toUnit[Unitless].show
res4: String = 0.9 unitless
Unit prefixes are a first-class concept in coulomb
.
In fact, prefixes are derived units of Unitless
:
scala> 1.withUnit[Kilo].toUnit[Unitless].show
res1: String = 1000 unitless
scala> 1.withUnit[Kibi].toUnit[Unitless].show
res2: String = 1024 unitless
Because they are just another kind of unit, prefixes work seamlessly with all other units.
scala> 3.withUnit[Meter %^ 3].toUnit[Kilo %* Liter].showFull
res1: String = 3 kiloliter
scala> 3D.withUnit[Meter %^ 3].toUnit[Mega %* Liter].showFull
res2: String = 0.003 megaliter
scala> (1.withUnit[Kilo] * 1.withUnit[Meter]).toUnit[Meter].showFull
res3: String = 1000 meter
scala> (1D.withUnit[Meter] / 1D.withUnit[Mega]).toUnit[Meter].showFull
res4: String = 1.0E-6 meter
The coulomb
library comes with definitions for the standard SI prefixes, and also standard binary prefixes.
It is also easy to declare new prefix units using coulomb.define.PrefixUnit
scala> trait Dozen
defined trait Dozen
scala> implicit val defineUnitDozen = PrefixUnit[Dozen](coef = 12, abbv = "doz")
defineUnitDozen: coulomb.define.DerivedUnit[Dozen,coulomb.Unitless] = DerivedUnit(12, dozen, doz)
scala> 1D.withUnit[Dozen %* Inch].toUnit[Foot].show
res1: String = 1.0 ft
The WithUnit
type alias can be used to make unit definitions more readable. The following two
function definitions are equivalent:
def f1(duration: Quantity[Float, Second]) = duration + 1f.withUnit[Minute]
def f2(duration: Float WithUnit Second) = duration + 1f.withUnit[Minute]
There is a similar WithTemperature
alias for working with Temperature
values.
One of the significant use cases for coulomb is adding unit type awareness to software configurations "at the edge." The coulomb libraries include some integrations with popular configuration libraries:
- coulomb-avro - an integration package with Apache Avro schema and i/o
- coulomb-pureconfig - extends the pureconfig with awareness of unit Quantity
- coulomb-typesafe-config - unit awareness for the typesafe config library
In coulomb, both time and temperature units can serve as units in Quantity values, but they can also serve as measures against an absolute offset.
In the case of temperature units, the Temperature
type represents absolute temperature values, with respect to absolute zero.
The type EpochTime
represents absolute date/time moments, based on the unix epoch: midnight of Jan 1, 1970.
EpochTime is similar to java.time.Instant
, and can interoperate with it.
Temperature and EpochTime are both specializations of OffsetQuantity[N, U]
.
These obey somewhat different laws than Quantity:
OffsetQuantity - OffsetQuantity => Quantity // e.g. EpochTime - EpochTime => Quantity
OffsetQuantity + Quantity => OffsetQuantity // e.g. Temperature + Quantity => Temperature
OffsetQuantity - Quantity => OffsetQuantity
- Temperature units are documented with examples at coulomb-temp-units
- Time unit examples are documented under coulomb-time-units
Previous topics have focused on how to work with specific Quantity
and unit expressions.
However, suppose you wish to write your own "generic" functions or classes, where Quantity
values
have parameterized types? For these situations, coulomb
provides a set of implicit type-classes that
allow Quantity
operations to be supported with type parameters.
These typeclasses can be accessed via import coulomb.unitops._
The UnitString
type-class supports unit names and abbreviations:
scala> import coulomb.unitops._
scala> def uname[N, U](q: Quantity[N, U])(implicit us: UnitString[U]): String = us.full
uname: [N, U](q: coulomb.Quantity[N,U])(implicit us: coulomb.unitops.UnitString[U])String
scala> uname(3.withUnit[Meter %/ Second])
res0: String = meter/second
The various numeric operations are supported by a set of typeclasses, summarized in the following table.
operation | implicit class | algebra |
---|---|---|
< <= > >= === =:= | UnitOrd[N1,U1,N2,U2] | Order[N1] |
+ | UnitAdd[N1,U1,N2,U2] | AdditiveSemigroup[N1] |
- | UnitSub[N1,U1,N2,U2] | AdditiveGroup[N1] |
* | UnitMul[N1,U1,N2,U2] | MultiplicativeSemigroup[N1] |
/ | UnitDiv[N1,U1,N2,U2] | MultiplicativeGroup[N1] |
pow | UnitPow[N, U, P] | MultiplicativeSemigroup[N] |
unary - | UnitNeg[N] | AdditiveGroup[N] |
For common numeric types, the various algebras in the table above can be obtained via import spire.std.any._
, or individually as in import spire.std.double._
.
The following code block shows an example of using typeclasses to support some numeric Quantity operations:
scala> def operate[N1, U1, N2, U2](q1: Quantity[N1, U1], q2: Quantity[N2, U2])(implicit
add: UnitAdd[N1, U1, N2, U2],
mul: UnitMul[N1, U1, N2, U2],
pow: UnitPow[N1, U1, 3],
ord: UnitOrd[N1, U1, N2, U2]) = {
val r1 = q1 + q2
val r2 = q1 * q2
val r3 = q1.pow[3]
val r4 = q1 < q2
(r1, r2, r3, r4)
}
scala> val (r1, r2, r3, r4) = operate(2f.withUnit[Meter], 3f.withUnit[Meter])
r1: coulomb.Quantity[Float,coulomb.si.Meter] = Quantity(5.0)
r2: coulomb.Quantity[Float,coulomb.si.Meter %^ Int(2)] = Quantity(6.0)
r3: coulomb.Quantity[Float,coulomb.si.Meter %^ Int(3)] = Quantity(8.0)
r4: Boolean = true
scala> List(r1.show, r2.show, r3.show, r4.toString)
res1: List[String] = List(5.0 m, 6.0 m^2, 8.0 m^3, true)
The ability to convert between unit quantities is represented by the UnitConverter
typeclass.
This example illustrates the use of UnitConverter
:
scala> def pints[U](beer: Quantity[Double, U])(implicit
cnv: UnitConverter[Double, U, Double, Pint]): Unit = {
val pintsOfBeer = beer.toUnit[Pint]
print(s"I have so much beer: ${pintsOfBeer.showFull}")
}
scala> pints(500D.withUnit[Milli %* Liter])
I have so much beer: 1.0566882094325938 pint
The UnitConverter
typeclass is also used by default typeclasses for UnitAdd
, UnitMul
and the other numeric operations above.
This typeclass can be extended by adding unit converter policies.
Previous sections have disussed the various operations that can be performed on Quantity
objects.
As mentioned in the section on unit operations,
the quantity result type of binary operations is governed by the left-hand-side of an exression:
scala> (1.withUnit[Foot] + 1.withUnit[Yard]).show
res0: String = 4 ft
As described in the previous section on
operation typeclasses
the UnitAdd
typeclass implements unit quantity addition.
Here is the code for UnitAdd
:
trait UnitAdd[N1, U1, N2, U2] {
def vadd(v1: N1, v2: N2): N1
}
object UnitAdd {
implicit def evidenceASG0[N1, U1, N2, U2](implicit
as1: AdditiveSemigroup[N1],
uc: UnitConverter[N2, U2, N1, U1]): UnitAdd[N1, U1, N2, U2] =
new UnitAdd[N1, U1, N2, U2] {
def vadd(v1: N1, v2: N2): N1 = as1.plus(v1, uc.vcnv(v2))
}
}
As the above code suggests, all quantity operations by convention first convert the RHS value to the value and unit type of the left hand side,
and then use the appropriate algebra (in this example AdditiveSemigroup
) to perform that operation with respect to the LHS value and unit.
Note that this means only the LHS value type requires this algebra to exist.
The full set of Quantity operation typeclasses are defined in unitops.scala.
How do unit conversions operate?
Here is the default implementation of UnitConverter
:
trait UnitConverter[N1, U1, N2, U2] {
def vcnv(v: N1): N2
}
trait UnitConverterDefaultPriority {
implicit def witness[N1, U1, N2, U2](implicit
cu: ConvertableUnits[U1, U2],
cf1: ConvertableFrom[N1],
ct2: ConvertableTo[N2]): UnitConverter[N1, U1, N2, U2] =
new UnitConverter[N1, U1, N2, U2] {
def vcnv(v: N1): N2 = ct2.fromType[Rational](cf1.toType[Rational](v) * cu.coef)
}
}
As the above code illustrates, a standard unit conversion proceeds by:
- Converting the input type to
Rational
- Multiply by the unit conversion coefficient (also a Rational)
- Converting the result to the output type.
Coulomb uses the Rational
type as the intermediary because it can operate lossessly on integer values,
and it can accommodate most numeric repesentations with zero or minimal loss.
Note that any numeric precision loss in this process is most likely to occur during the final conversion to the LHS value type.
The potential for precision loss is greatest when working with integer value types, and particuarly when the LHS unit is larger than the RHS unit, as in the second conversion below:
scala> ((100.withUnit[Meter]) + (1.withUnit[Kilo %* Meter])).show
res0: String = 1100 m
scala> ((1.withUnit[Kilo %* Meter]) + (100.withUnit[Meter])).show
res1: String = 1 km
Some unit conversion specializations are applied. For example, in the case that both LHS and RHS units and value types are the same, the fast and lossless identity function is applied:
implicit def witnessIdentity[N, U]: UnitConverter[N, U, N, U] = {
new UnitConverter[N, U, N, U] {
@inline def vcnv(v: N): N = v
}
}
Another example is the case of Double
value types, where preconverting the conversion coefficient to Double
is efficient while
maintaining Double
accuracy:
implicit def witnessDouble[U1, U2](implicit
cu: ConvertableUnits[U1, U2]): UnitConverter[Double, U1, Double, U2] = {
val coef = cu.coef.toDouble
new UnitConverter[Double, U1, Double, U2] {
@inline def vcnv(v: Double): Double = v * coef
}
}
UnitConverter
and its full set of typeclass rules are defined in
unitops.scala.
Coulomb's typeclass rules can be extended to support new value types.
There are two components to a custom extension. The first is to define any desired algebras on the new value type. Defining algebras is optional, if no numeric operations need to be supported.
The second customization component is to define what it means to multiply a value by a conversion coefficient.
This can be accomplished using the UnitConverterPolicy
typeclass, defined in coulomb.unitops
.
In the following example, coulomb is extended to support the spire Complex
type.
In the case of Complex
, algebras such as additive and multiplicative (semi)groups are already defined.
scala> import coulomb.unitops._, spire.math.Complex, spire.algebra._
// define what it means to apply unit conversion coefficients to Complex
scala> implicit def complexPolicy[U1, U2]: UnitConverterPolicy[Complex[Double], U1, Complex[Double], U2] =
new UnitConverterPolicy[Complex[Double], U1, Complex[Double], U2] {
def convert(v: Complex[Double], cu: ConvertableUnits[U1, U2]): Complex[Double] = v * cu.coef.toDouble
}
scala> q.show
res0: String = (1.0 + 2.0i) m
scala> q.toUnit[Foot].show
res1: String = (3.2808398950131235 + 6.561679790026247i) ft
scala> (q * q).show
res2: String = (-3.0 + 4.0i) m^2
scala> (q + q).show
res3: String = (2.0 + 4.0i) m