• Stars
    star
    922
  • Rank 49,555 (Top 1.0 %)
  • Language
    Scala
  • License
    Apache License 2.0
  • Created over 10 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The Scala API for Quantities, Units of Measure and Dimensional Analysis

Squants

The Scala API for Quantities, Units of Measure and Dimensional Analysis

Squants is a framework of data types and a domain specific language (DSL) for representing Quantities, their Units of Measure, and their Dimensional relationships. The API supports typesafe dimensional analysis, improved domain models and more. All types are immutable and thread-safe.

Website | GitHub | Wiki | Join the chat at https://gitter.im/typelevel/squants | Scaladocs | Build Status

Current Versions

Current Release: 1.6.0 (API Docs)

Development Build: 1.7.0-SNAPSHOT (API Docs)

Release History

Build services provided by Travis CI

NOTE - This README reflects the feature set in the branch it can be found. For more information on feature availability of a specific version see the Release History or the README for a that version

Installation

Repository hosting for Squants is provided by Sonatype. To use Squants in your SBT project add the following dependency to your build.

"org.typelevel"  %% "squants"  % "1.6.0"

or

"org.typelevel"  %% "squants"  % "1.7.0-SNAPSHOT"

To use Squants in your Maven project add the following dependency

<dependency>
    <groupId>org.typelevel</groupId>
    <artifactId>squants_2.11</artifactId>
    <version>1.6.0</version>
</dependency>

Beginning with Squants 0.4.x series, both Scala 2.10 and 2.11 builds are available. Beginning with Squants 1.x series, Scala 2.11, 2.12 and 2.13 builds are available. Scala.js is supported on version 0.6.31 and 1.0.0-RC1

To use Squants interactively in the Scala REPL, clone the git repo and run sbt squantsJVM/console

git clone https://github.com/typelevel/squants
cd squants
sbt squantsJVM/console

Third-party integrations

This is an incomplete list of third-party libraries that support squants:

If your library isn't listed here, please open a PR to add it!

Type Safe Dimensional Analysis

The Trouble with Doubles

When building programs that perform dimensional analysis, developers are quick to declare quantities using a basic numeric type, usually Double. While this may be satisfactory in some situations, it can often lead to semantic and other logic issues.

For example, when using a Double to describe quantities of Energy (kWh) and Power (kW), it is possible to compile a program that adds these two values together. This is not appropriate as kW and kWh measure quantities of two different dimensions. The unit kWh is used to measure an amount of Energy used or produced. The unit kW is used to measure Power/Load, the rate at which Energy is being used or produced, that is, Power is the first time derivative of Energy.

Power = Energy / Time

Consider the following code:

scala> val loadKw = 1.2
loadKw: Double = 1.2

scala> val energyMwh = 24.2
energyMwh: Double = 24.2

scala> val sumKw = loadKw + energyMwh
sumKw: Double = 25.4

This example not only adds quantities of different dimensions (Power vs Energy), it also fails to convert the scales implied in the val names (Mega vs Kilo). Because this code compiles, detection of these errors is pushed further into the development cycle.

Dimensional Type Safety

Only quantities with the same dimensions may be compared, equated, added, or subtracted.

Squants helps prevent errors like these by type checking operations at compile time and automatically applying scale and type conversions at run-time. For example:

scala> import squants.energy.{Kilowatts, Megawatts, Power}
import squants.energy.{Kilowatts, Megawatts, Power}

scala> val load1: Power = Kilowatts(12)
load1: squants.energy.Power = 12.0 kW

scala> val load2: Power = Megawatts(0.023)
load2: squants.energy.Power = 0.023 MW

scala> val sum = load1 + load2
sum: squants.energy.Power = 35.0 kW

scala> sum == Kilowatts(35)
res0: Boolean = true

scala> sum == Megawatts(0.035) // comparisons automatically convert scale
res1: Boolean = true

The above sample works because Kilowatts and Megawatts are both units of Power. Only the scale is different and the library applies an appropriate conversion. Also, notice that keeping track of the scale within the value name is no longer needed:

scala> import squants.energy.{Energy, Power, Kilowatts, KilowattHours}
import squants.energy.{Energy, Power, Kilowatts, KilowattHours}

scala> val load: Power = Kilowatts(1.2)
load: squants.energy.Power = 1.2 kW

scala> val energy: Energy = KilowattHours(23.0)
energy: squants.energy.Energy = 23.0 kWh

Invalid operations, like adding power and energy, no longer compile:

scala> val sum = load + energy
<console>:16: error: type mismatch;
 found   : squants.energy.Energy
 required: squants.energy.Power
       val sum = load + energy
                        ^

By using stronger types, we catch the error earlier in the development cycle, preventing the error made when using Double in the example above.

Dimensionally Correct Type Conversions

One may take quantities with different dimensions, and multiply or divide them.

Dimensionally correct type conversions are a key feature of Squants. Conversions are implemented by defining relationships between Quantity types using the * and / operators.

Code samples in this section assume these imports:

import squants.energy.{Kilowatts, Power}
import squants.time.{Hours, Days}

The following code demonstrates creating ratio between two quantities of the same dimension, resulting in a dimensionless value:

scala> val ratio = Days(1) / Hours(3)
ratio: Double = 8.0

This code demonstrates use of the Power.* method that takes a Time and returns an Energy:

scala> val load = Kilowatts(1.2)
load: squants.energy.Power = 1.2 kW

scala> val time = Hours(2)
time: squants.time.Time = 2.0 h

scala> val energyUsed = load * time
energyUsed: squants.energy.Energy = 2400.0 Wh

This code demonstrates use of the Energy./ method that takes a Time and returns a Power:

scala> val aveLoad: Power = energyUsed / time
aveLoad: squants.energy.Power = 1200.0 W

Unit Conversions

Code samples in this section assume these imports:

import scala.language.postfixOps
import squants.energy.{Gigawatts, Kilowatts, Power, Megawatts}
import squants.mass.MassConversions._
import squants.mass.{Kilograms, Pounds}
import squants.thermal.TemperatureConversions._
import squants.thermal.Fahrenheit

Quantity values are based in the units used to create them.

scala> val loadA: Power = Kilowatts(1200)
loadA: squants.energy.Power = 1200.0 kW

scala> val loadB: Power = Megawatts(1200)
loadB: squants.energy.Power = 1200.0 MW

Since Squants properly equates values of a like dimension, regardless of the unit, there is usually no reason to explicitly convert from one to the other. This is especially true if the user code is primarily performing dimensional analysis.

However, there are times when you may need to set a Quantity value to a specific unit (eg, for proper JSON encoding).

When necessary, a quantity can be converted to another unit using the in method.

scala> val loadA = Kilowatts(1200)
loadA: squants.energy.Power = 1200.0 kW

scala> val loadB = loadA in Megawatts
loadB: squants.energy.Power = 1.2 MW

scala> val loadC = loadA in Gigawatts
loadC: squants.energy.Power = 0.0012 GW

Sometimes you need to get the numeric value of the quantity in a specific unit (eg, for submission to an external service that requires a numeric in a specified unit or to perform analysis beyond Squant's domain)

When necessary, the value can be extracted in the desired unit with the to method.

scala> val load: Power = Kilowatts(1200)
load: squants.energy.Power = 1200.0 kW

scala> val kw: Double = load to Kilowatts
kw: Double = 1200.0

scala> val mw: Double = load to Megawatts
mw: Double = 1.2

scala> val gw: Double = load to Gigawatts
gw: Double = 0.0012

Most types include methods with convenient aliases for the to methods.

scala> val kw: Double = load toKilowatts
kw: Double = 1200.0

scala> val mw: Double = load toMegawatts
mw: Double = 1.2

scala> val gw: Double = load toGigawatts
gw: Double = 0.0012

NOTE - It is important to use the to method for extracting the numeric value, as this ensures you will be getting the numeric value for the desired unit. Quantity.value should not be accessed directly. To prevent improper usage, direct access to the Quantity.value field may be deprecated in a future version.

Creating strings formatted in the desired unit:

scala> val kw: String = load toString Kilowatts
kw: String = 1200.0 kW

scala> val mw: String = load toString Megawatts
mw: String = 1.2 MW

scala> val gw: String = load toString Gigawatts
gw: String = 0.0012 GW

Creating Tuple2[Double, String] that includes a numeric value and unit symbol:

scala> val load: Power = Kilowatts(1200)
load: squants.energy.Power = 1200.0 kW

scala> val kw = load toTuple
kw: (Double, String) = (1200.0,kW)

scala> val mw = load toTuple Megawatts
mw: (Double, String) = (1.2,MW)

scala> val gw = load toTuple Gigawatts
gw: (Double, String) = (0.0012,GW)

This can be useful for passing properly scaled quantities to other processes that do not use Squants, or require use of more basic types (Double, String)

Simple console based conversions (using DSL described below)

scala> 1.kilograms to Pounds
res0: Double = 2.2046226218487757

scala> kilogram / pound
res1: Double = 2.2046226218487757

scala> 2.1.pounds to Kilograms
res2: Double = 0.952543977

scala> 2.1.pounds / kilogram
res3: Double = 0.9525439770000002

scala> 100.C to Fahrenheit
res4: Double = 212.0

Mapping over Quantity values

Apply a Double => Double operation to the underlying value of a quantity, while preserving its type and unit.

scala> import squants.energy.Kilowatts
import squants.energy.Kilowatts

scala> val load = Kilowatts(2.0)
load: squants.energy.Power = 2.0 kW

scala> val newLoad = load.map(v => v * 2 + 10)
newLoad: squants.energy.Power = 14.0 kW

The q.map(f) method effectively expands to q.unit(f(q.to(q.unit))

NOTE - For Money objects, use the mapAmount method as this will retain the BigDecimal precision used there.

Approximations

Create an implicit Quantity value to be used as a tolerance in approximations. Then use the approx method (or =~, ~=, โ‰ˆ operators) like you would use the equals method (== operator).

scala> import squants.energy.{Kilowatts, Watts}
import squants.energy.{Kilowatts, Watts}

scala> val load = Kilowatts(2.0)
load: squants.energy.Power = 2.0 kW

scala> val reading = Kilowatts(1.9999)
reading: squants.energy.Power = 1.9999 kW

Calls to approx (and its symbolic aliases) use an implicit tolerance:

scala> implicit val tolerance = Watts(.1)
tolerance: squants.energy.Power = 0.1 W

scala> load =~ reading
res0: Boolean = true

scala> load โ‰ˆ reading
res1: Boolean = true

scala> load approx reading
res2: Boolean = true

The =~ and โ‰ˆ are the preferred operators as they have the correct precedence for equality operations. The ~= is provided for those who wish to use a more natural looking approx operator using standard characters. However, because of its lower precedence, user code may require parenthesis around these comparisons.

Vectors

All Quantity types in Squants represent the scalar value of a quantity. That is, there is no direction information encoded in any of the Quantity types. This is true even for Quantities which are normally vector quantities (ie. Velocity, Acceleration, etc).

Vector quantities in Squants are implemented as case classes that takes a variable parameter list of like quantities representing a set of point coordinates in Cartesian space. The SVector object is a factory for creating DoubleVectors and QuantityVectors. The dimensionality of the vector is determined by the number of arguments. Most basic vector operations are currently supported (addition, subtraction, scaling, cross and dot products)

scala> import squants.{QuantityVector, SVector}
import squants.{QuantityVector, SVector}

scala> import squants.space.{Kilometers, Length}
import squants.space.{Kilometers, Length}

scala> import squants.space.LengthConversions._
import squants.space.LengthConversions._

scala> val vector: QuantityVector[Length] = SVector(Kilometers(1.2), Kilometers(4.3), Kilometers(2.3))
vector: squants.QuantityVector[squants.space.Length] = QuantityVector(WrappedArray(1.2 km, 4.3 km, 2.3 km))

scala> val magnitude: Length = vector.magnitude        // returns the scalar value of the vector
magnitude: squants.space.Length = 5.021951811795888 km

scala> val normalized = vector.normalize(Kilometers)   // returns a corresponding vector scaled to 1 of the given unit
normalized: vector.SVectorType = QuantityVector(ArrayBuffer(0.2389509188800581 km, 0.8562407926535415 km, 0.45798926118677796 km))

scala> val vector2: QuantityVector[Length] = SVector(Kilometers(1.2), Kilometers(4.3), Kilometers(2.3))
vector2: squants.QuantityVector[squants.space.Length] = QuantityVector(WrappedArray(1.2 km, 4.3 km, 2.3 km))

scala> val vectorSum = vector + vector2        // returns the sum of two vectors
vectorSum: vector.SVectorType = QuantityVector(ArrayBuffer(2.4 km, 8.6 km, 4.6 km))

scala> val vectorDiff = vector - vector2       // return the difference of two vectors
vectorDiff: vector.SVectorType = QuantityVector(ArrayBuffer(0.0 km, 0.0 km, 0.0 km))

scala> val vectorScaled = vector * 5           // returns vector scaled 5 times
vectorScaled: vector.SVectorType = QuantityVector(ArrayBuffer(6.0 km, 21.5 km, 11.5 km))

scala> val vectorReduced = vector / 5          // returns vector reduced 5 time
vectorReduced: vector.SVectorType = QuantityVector(ArrayBuffer(0.24 km, 0.86 km, 0.45999999999999996 km))

scala> val vectorDouble = vector / 5.meters    // returns vector reduced and converted to DoubleVector
vectorDouble: squants.DoubleVector = DoubleVector(ArrayBuffer(240.0, 860.0, 459.99999999999994))

scala> val dotProduct = vector * vectorDouble  // returns the Dot Product of vector and vectorDouble
dotProduct: squants.space.Length = 5044.0 km

scala> val crossProduct = vector crossProduct vectorDouble  // currently only supported for 3-dimensional vectors
crossProduct: vector.SVectorType = QuantityVector(WrappedArray(0.0 km, 1.1368683772161603E-13 km, 0.0 km))

Simple non-quantity (Double based) vectors are also supported.

import squants.DoubleVector

val vector: DoubleVector = SVector(1.2, 4.3, 2.3, 5.4)   // a Four-dimensional vector

Dimensional conversions within Vector operations.

Currently dimensional conversions are supported by using the slightly verbose, but flexible map method.

scala> import squants.{DoubleVector, QuantityVector}
import squants.{DoubleVector, QuantityVector}

scala> import squants.motion.Velocity
import squants.motion.Velocity

scala> import squants.space.{Area, Kilometers, Length, Meters}
import squants.space.{Area, Kilometers, Length, Meters}

scala> import squants.time.Seconds
import squants.time.Seconds

scala> val vectorLength = QuantityVector(Kilometers(1.2), Kilometers(4.3), Kilometers(2.3))
vectorLength: squants.QuantityVector[squants.space.Length] = QuantityVector(WrappedArray(1.2 km, 4.3 km, 2.3 km))

scala> val vectorArea = vectorLength.map[Area](_ * Kilometers(2))      // QuantityVector(2.4 kmยฒ, 8.6 kmยฒ, 4.6 kmยฒ)
vectorArea: squants.QuantityVector[squants.space.Area] = QuantityVector(ArrayBuffer(2.4 kmยฒ, 8.6 kmยฒ, 4.6 kmยฒ))

scala> val vectorVelocity = vectorLength.map[Velocity](_ / Seconds(1)) // QuantityVector(1200.0 m/s, 4300.0 m/s, 2300.0 m/s)
vectorVelocity: squants.QuantityVector[squants.motion.Velocity] = QuantityVector(ArrayBuffer(1200.0 m/s, 4300.0 m/s, 2300.0 m/s))

scala> val vectorDouble = DoubleVector(1.2, 4.3, 2.3)
vectorDouble: squants.DoubleVector = DoubleVector(WrappedArray(1.2, 4.3, 2.3))

scala> val vectorLength = vectorDouble.map[Length](Kilometers(_))      // QuantityVector(1.2 km, 4.3 km, 2.3 km)
vectorLength: squants.QuantityVector[squants.space.Length] = QuantityVector(ArrayBuffer(1.2 km, 4.3 km, 2.3 km))

Convert QuantityVectors to specific units using the to or in method - much like Quantities.

scala> val vectorLength = QuantityVector(Kilometers(1.2), Kilometers(4.3), Kilometers(2.3))
vectorLength: squants.QuantityVector[squants.space.Length] = QuantityVector(WrappedArray(1.2 km, 4.3 km, 2.3 km))

scala> val vectorMetersNum = vectorLength.to(Meters)   // DoubleVector(1200.0, 4300.0, 2300.0)
vectorMetersNum: squants.DoubleVector = DoubleVector(ArrayBuffer(1200.0, 4300.0, 2300.0))

scala> val vectorMeters = vectorLength.in(Meters)      // QuantityVector(1200.0 m, 4300.0 m, 2300.0 m)
vectorMeters: squants.QuantityVector[squants.space.Length] = QuantityVector(ArrayBuffer(1200.0 m, 4300.0 m, 2300.0 m))

Market Package

Market Types are similar but not quite the same as other quantities in the library. The primary type, Money, is a Dimensional Quantity, and its Units of Measure are Currencies. However, because the conversion multipliers between currency units can not be predefined, many of the behaviors have been overridden and augmented to realize correct behavior.

Money

A Quantity of purchasing power measured in Currency units. Like other quantities, the Unit of Measures are used to create Money values.

scala> import squants.market.{BTC, JPY, USD, XAU}
import squants.market.{BTC, JPY, USD, XAU}

scala> val tenBucks = USD(10)      // Money: 10 USD
tenBucks: squants.market.Money = 1E+1 USD

scala> val someYen = JPY(1200)     // Money: 1200 JPY
someYen: squants.market.Money = 1.2E+3 JPY

scala> val goldStash = XAU(50)     // Money: 50 XAU
goldStash: squants.market.Money = 5E+1 XAU

scala> val digitalCache = BTC(50)  // Money: 50 BTC
digitalCache: squants.market.Money = 5E+1 BTC

Price

A Ratio between Money and another Quantity. A Price value is typed on a Quantity and can be denominated in any defined Currency.

Price = Money / Quantity

Assuming these imports:

import squants.{Dozen, Each}
import squants.energy.MegawattHours
import squants.market.USD
import squants.space.UsGallons

You can compute the following:

scala> val threeForADollar = USD(1) / Each(3)
threeForADollar: squants.market.Price[squants.Dimensionless] = 1 USD/3.0 ea

scala> val energyPrice = USD(102.20) / MegawattHours(1)
energyPrice: squants.market.Price[squants.energy.Energy] = 102.2 USD/1.0 MWh

scala> val milkPrice = USD(4) / UsGallons(1)
milkPrice: squants.market.Price[squants.space.Volume] = 4 USD/1.0 gal

scala> val costForABunch = threeForADollar * Dozen(10)
costForABunch: squants.market.Money = 4E+1 USD

scala> val energyCost = energyPrice * MegawattHours(4)
energyCost: squants.market.Money = 408.8 USD

scala> val milkQuota = USD(20) / milkPrice
milkQuota: squants.space.Volume = 5.0 gal

Conversions to Strings

scala> val money = USD(123.456)
money: squants.market.Money = 123.456 USD

scala> val s = money.toString  // returns full precision amount with currency code
s: String = 123.456 USD

scala> val s = money.toFormattedString // returns currency symbol and amount rounded based on currency rules
s: String = $123.46

FX Support

Currency Exchange Rates are used to define the conversion factors between currencies

scala> import squants.market.{CurrencyExchangeRate, JPY, Money, USD}
import squants.market.{CurrencyExchangeRate, JPY, Money, USD}

scala> // create an exchange rate
     | val rate1 = CurrencyExchangeRate(USD(1), JPY(100))
rate1: squants.market.CurrencyExchangeRate = USD/JPY 100.0

scala> // OR
     | val rate2 = USD / JPY(100)
rate2: squants.market.CurrencyExchangeRate = USD/JPY 100.0

scala> // OR
     | val rate3 = JPY(100) -> USD(1)
rate3: squants.market.CurrencyExchangeRate = USD/JPY 100.0

scala> // OR
     | val rate4 = JPY(100) toThe USD(1)
rate4: squants.market.CurrencyExchangeRate = USD/JPY 100.0

scala> val someYen: Money = JPY(350)
someYen: squants.market.Money = 3.5E+2 JPY

scala> val someBucks: Money = USD(23.50)
someBucks: squants.market.Money = 23.5 USD

Use the convert method which automatically converts the money to the 'other' currency:

scala> val dollarAmount: Money = rate1.convert(someYen)
dollarAmount: squants.market.Money = 3.5 USD

scala> val yenAmount: Money = rate1.convert(someBucks)
yenAmount: squants.market.Money = 2.35E+3 JPY

Or just use the * operator in either direction (money * rate, or rate * money):

scala> val dollarAmount2: Money = rate1 * someYen
dollarAmount2: squants.market.Money = 3.5 USD

scala> val yenAmount2: Money = someBucks * rate1
yenAmount2: squants.market.Money = 2.35E+3 JPY

Money Context

A MoneyContext can be implicitly declared to define default settings and applicable exchange rates within its scope. This allows your application to work with a default currency based on an application configuration or other dynamic source. It also provides support for updating exchange rates and using those rates for automatic conversions between currencies. The technique and frequency chosen for exchange rate updates is completely in control of the application.

Assuming these imports:

import squants.energy.MegawattHours
import squants.market.{CAD, JPY, MXN, USD}
import squants.market.defaultMoneyContext

You can compute:

scala> val exchangeRates = List(USD / CAD(1.05), USD / MXN(12.50), USD / JPY(100))
exchangeRates: List[squants.market.CurrencyExchangeRate] = List(USD/CAD 1.05, USD/MXN 12.5, USD/JPY 100.0)

scala> implicit val moneyContext = defaultMoneyContext withExchangeRates exchangeRates
moneyContext: squants.market.MoneyContext = MoneyContext(DefaultCurrency(USD),Currencies(ARS,AUD,BRL,BTC,CAD,CHF,CLP,CNY,CZK,DKK,ETH,EUR,GBP,HKD,INR,JPY,KRW,LTC,MXN,MYR,NAD,NOK,NZD,RUB,SEK,USD,XAG,XAU,ZAR),ExchangeRates(USD/CAD 1.05,USD/JPY 100.0,USD/MXN 12.5),AllowIndirectConversions(true))

scala> val energyPrice = USD(102.20) / MegawattHours(1)
energyPrice: squants.market.Price[squants.energy.Energy] = 102.2 USD/1.0 MWh

scala> val someMoney = Money(350) // 350 in the default Cur
someMoney: squants.market.Money = 3.5E+2 USD

scala> val usdMoney: Money = someMoney in USD
usdMoney: squants.market.Money = 3.5E+2 USD

scala> val usdBigDecimal: BigDecimal = someMoney to USD
usdBigDecimal: BigDecimal = 350.0

scala> val yenCost: Money = (energyPrice * MegawattHours(5)) in JPY
yenCost: squants.market.Money = 5.11E+4 JPY

scala> val northAmericanSales: Money = (CAD(275) + USD(350) + MXN(290)) in USD
northAmericanSales: squants.market.Money = 635.1047619047619047619047619047619 USD

Quantity Ranges

A QuantityRange is used to represent a range of Quantity values between an upper and lower bound:

import squants.QuantityRange
import squants.energy.{Kilowatts, Megawatts, Power}
val load1: Power = Kilowatts(1000)
// load1: squants.energy.Power = 1000.0 kW

val load2: Power = Kilowatts(5000)
// load2: squants.energy.Power = 5000.0 kW

val range: QuantityRange[Power] = QuantityRange(load1, load2)
// range: squants.QuantityRange[squants.energy.Power] = QuantityRange(1000.0 kW,5000.0 kW)

Inclusivity and Exclusivitiy

The QuantityRange constructor requires that upper is strictly greater than lower:

import squants.space.LengthConversions._
// import squants.space.LengthConversions._

// this will work b/c upper > lower
QuantityRange(1.km, 5.km)
// res1: squants.QuantityRange[squants.space.Length] = QuantityRange(1.0 km,5.0 km)

This will fail because lower = upper:

scala> QuantityRange(1.km, 1.km)
java.lang.IllegalArgumentException: QuantityRange upper bound must be strictly greater than to the lower bound
  at squants.QuantityRange.<init>(QuantityRange.scala:25)
  ... 43 elided

QuantityRange contains two functions that check if an element is part of the range, contains and includes. These differ in how they treat the range's upper bound: contains() excludes it but includes() includes it.

scala> val distances = QuantityRange(1.km, 5.km)
distances: squants.QuantityRange[squants.space.Length] = QuantityRange(1.0 km,5.0 km)

scala> distances.contains(5.km) // this is false b/c contains() doesn't include the upper range
res3: Boolean = false

scala> distances.includes(5.km) // this is true b/c includes() does include the upper range
res4: Boolean = true

QuantityRange transformation

The multiplication and division operators create a Seq of ranges from the original.

For example:

Create a Seq of 10 sequential ranges starting with the original and each the same size as the original:

val rs1 = range * 10
// rs1: squants.QuantitySeries[squants.energy.Power] = Vector(QuantityRange(1000.0 kW,5000.0 kW), QuantityRange(5000.0 kW,9000.0 kW), QuantityRange(9000.0 kW,13000.0 kW), QuantityRange(13000.0 kW,17000.0 kW), QuantityRange(17000.0 kW,21000.0 kW), QuantityRange(21000.0 kW,25000.0 kW), QuantityRange(25000.0 kW,29000.0 kW), QuantityRange(29000.0 kW,33000.0 kW), QuantityRange(33000.0 kW,37000.0 kW), QuantityRange(37000.0 kW,41000.0 kW))

Create a Seq of 10 sequential ranges each 1/10th of the original size:

val rs2 = range / 10
// rs2: squants.QuantitySeries[squants.energy.Power] = Vector(QuantityRange(1000.0 kW,1400.0 kW), QuantityRange(1400.0 kW,1800.0 kW), QuantityRange(1800.0 kW,2200.0 kW), QuantityRange(2200.0 kW,2600.0 kW), QuantityRange(2600.0 kW,3000.0 kW), QuantityRange(3000.0 kW,3400.0 kW), QuantityRange(3400.0 kW,3800.0 kW), QuantityRange(3800.0 kW,4200.0 kW), QuantityRange(4200.0 kW,4600.0 kW), QuantityRange(4600.0 kW,5000.0 kW))

Create a Seq of 10 sequential ranges each with a size of 400 kilowatts:

val rs3 = range / Kilowatts(400)
// rs3: squants.QuantitySeries[squants.energy.Power] = Vector(QuantityRange(1000.0 kW,1400.0 kW), QuantityRange(1400.0 kW,1800.0 kW), QuantityRange(1800.0 kW,2200.0 kW), QuantityRange(2200.0 kW,2600.0 kW), QuantityRange(2600.0 kW,3000.0 kW), QuantityRange(3000.0 kW,3400.0 kW), QuantityRange(3400.0 kW,3800.0 kW), QuantityRange(3800.0 kW,4200.0 kW), QuantityRange(4200.0 kW,4600.0 kW), QuantityRange(4600.0 kW,5000.0 kW))

QuantityRange operations

QuantityRange supports foreach, map, and foldLeft/foldRight. These vary slightly from the versions in the Scala standard library in that they take a divisior as the first parameter. The examples below illustrate their use.

Subdivide range into 1-Megawatt "slices", and foreach over each of slices:

range.foreach(Megawatts(1)) { r => println(s"lower = ${r.lower}, upper = ${r.upper}") }
// lower = 1000.0 kW, upper = 2000.0 kW
// lower = 2000.0 kW, upper = 3000.0 kW
// lower = 3000.0 kW, upper = 4000.0 kW
// lower = 4000.0 kW, upper = 5000.0 kW

Subdivide range into 10 slices and map over each slice:

range.map(10) { r => r.upper }
// res6: Seq[squants.energy.Power] = Vector(1400.0 kW, 1800.0 kW, 2200.0 kW, 2600.0 kW, 3000.0 kW, 3400.0 kW, 3800.0 kW, 4200.0 kW, 4600.0 kW, 5000.0 kW)

Subdivide range into 10 slices and fold over them, using 0 Megawatts as a starting value:

range.foldLeft(10, Megawatts(0)) { (z, r) => z + r.upper }
// res7: squants.energy.Power = 32.0 MW

NOTE - Because these implementations of foreach, map and fold* take a parameter (the divisor), these methods are not directly compatible with Scala's for comprehensions. To use in a for comprehension, apply the * or / operators as described above to create a Seq from the Range.

for {
    interval <- (0.seconds to 1.seconds) * 60  // 60 time ranges, 0s to 1s, 1s to 2s, ...., 59s to 60s
    ...
} yield ...

Natural Language DSL

Implicit conversions give the DSL some features that allows user code to express quantities in a more naturally expressive and readable way.

Code samples in this section assume these imports

import squants.energy.{Kilowatts, MegawattHours, Power}
import squants.market.{Price, USD}
import squants.time.Hours

Create Quantities using Unit Of Measure Factory objects (no implicits required):

scala> val load = Kilowatts(100)
load: squants.energy.Power = 100.0 kW

scala> val time = Hours(3.75)
time: squants.time.Time = 3.75 h

scala> val money = USD(112.50)
money: squants.market.Money = 112.5 USD

scala> val price = Price(money, MegawattHours(1))
price: squants.market.Price[squants.energy.Energy] = 112.5 USD/1.0 MWh

Create Quantities using Unit of Measure names and/or symbols (uses implicits):

import scala.language.postfixOps
import squants.energy.EnergyConversions._
import squants.energy.PowerConversions._
import squants.information.InformationConversions._
import squants.market.MoneyConversions._
import squants.space.LengthConversions._
import squants.time.TimeConversions._
scala> val load1 = 100 kW 			        // Simple expressions donโ€™t need dots
load1: squants.energy.Power = 100.0 kW

scala> val load2 = 100 megawatts
load2: squants.energy.Power = 100.0 MW

scala> val time = 3.hours + 45.minutes     // Compound expressions may need dots
time: squants.time.Time = 3.75 h

Create Quantities using operations between other Quantities:

scala> val energyUsed = 100.kilowatts * (3.hours + 45.minutes)
energyUsed: squants.energy.Energy = 375000.0 Wh

scala> val price = 112.50.USD / 1.megawattHours
price: squants.market.Price[squants.energy.Energy] = 112.5 USD/1.0 MWh

scala> val speed = 55.miles / 1.hours
speed: squants.motion.Velocity = 24.587249174399997 m/s

Create Quantities using formatted Strings:

scala> val load = Power("40 MW")
load: scala.util.Try[squants.energy.Power] = Success(40.0 MW)

Create Quantities using Tuples:

scala> val load = Power((40.5, "MW"))
load: scala.util.Try[squants.energy.Power] = Success(40.5 MW)

Use single unit values to simplify expressions:

scala> // Hours(1) == 1.hours == hour
     | val ramp = 100.kilowatts / hour
ramp: squants.energy.PowerRamp = 100000.0 W/h

scala> val speed = 100.kilometers / hour
speed: squants.motion.Velocity = 27.77777777777778 m/s

scala> // MegawattHours(1) == 1.megawattHours == megawattHour == MWh
     | val hi = 100.dollars / MWh
hi: squants.market.Price[squants.energy.Energy] = 1E+2 USD/1.0 MWh

scala> val low = 40.dollars / megawattHour
low: squants.market.Price[squants.energy.Energy] = 4E+1 USD/1.0 MWh

Implicit conversion support for using Doubles, Longs and BigDecimals on the left side of multiply and divide operations:

scala> val load = 10.22 * 4.MW
load: squants.energy.Power = 40.88 MW

scala> val driveArrayCapacity = 12 * 600.gb
driveArrayCapacity: squants.information.Information = 7200.0 GB

scala> val freq = 60 / second
freq: squants.time.Frequency = 60.0 Hz

scala> val freq2 = BigDecimal(36000000) / hour
freq2: squants.time.Frequency = 10000.0 Hz

Create Quantity Ranges using to or plusOrMinus (+-) operators:

val range1 = 1000.kW to 5000.kW	             // 1000.kW to 5000.kW
val range2 = 5000.kW plusOrMinus 1000.kW     // 4000.kW to 6000.kW
val range2 = 5000.kW +- 1000.kW              // 4000.kW to 6000.kW

Numeric Support

Most Quantities that support implicit conversions also include an implicit Numeric object that can be imported to your code where Numeric support is required. These follow the following pattern:

scala> import squants.mass.{Grams, Kilograms}
import squants.mass.{Grams, Kilograms}

scala> import squants.mass.MassConversions.MassNumeric
import squants.mass.MassConversions.MassNumeric

scala> val sum = List(Kilograms(100), Grams(34510)).sum
sum: squants.mass.Mass = 134510.0 g

NOTE - Because a quantity can not be multiplied by a like quantity and return a like quantity, the Numeric.times operation of numeric is implemented to throw an UnsupportedOperationException for all types except Dimensionless.

The MoneyNumeric implementation is a bit different than the implementations for other quantity types in a few important ways.

  1. MoneyNumeric is a class, not an object like the others.
  2. To create a MoneyNumeric value there must be an implicit MoneyContext in scope.
  3. The MoneyContext must contain applicable exchange rates if you will be applying cross-currency Numeric ops.

The following code provides a basic example for creating a MoneyNumeric:

import squants.market.defaultMoneyContext
import squants.market.MoneyConversions._
import squants.market.USD
implicit val moneyContext = defaultMoneyContext
scala> implicit val moneyNum = new MoneyNumeric()
moneyNum: squants.market.MoneyConversions.MoneyNumeric = MoneyNumeric(MoneyContext(DefaultCurrency(USD),Currencies(ARS,AUD,BRL,BTC,CAD,CHF,CLP,CNY,CZK,DKK,ETH,EUR,GBP,HKD,INR,JPY,KRW,LTC,MXN,MYR,NAD,NOK,NZD,RUB,SEK,USD,XAG,XAU,ZAR),ExchangeRates(),AllowIndirectConversions(true)))

scala> val sum = List(USD(100), USD(10)).sum
sum: squants.market.Money = 1.1E+2 USD

Unit groups

Squants provides an experimental API for grouping related UnitOfMeasure values together. This are called UnitGroups. Squants provides UnitGroup implementations for the SI, the US Customary system, and various other systems. End-users can create their own ad-hoc UnitGroups for UnitOfMeasures in a related dimension.

The UnitGroup trait defines two public fields: units, a Set[UnitOfMeasure], and sortedUnits, which contains units sorted in ascending order.

SI UnitGroups

Almost every Dimension in Squants has SI Units (with the exception of Information and Money). To avoid boilerplate, Squants generates UnitGroups for SI using implicits.

There are two UnitGroups provided for SI: "strict" and "expanded." Strict only includes SI UnitOfMeasure defined in the SI; "expanded" includes non-SI units that are commonly used in SI, such as litre, hectare, hour, minute, etc). See the linked document for a detailed list.

To summon the strict SI UnitGroup for Length, you would use this code:

import squants.space.Length
// import squants.space.Length

import squants.experimental.unitgroups.ImplicitDimensions.space._
// import squants.experimental.unitgroups.ImplicitDimensions.space._

import squants.experimental.unitgroups.UnitGroup
// import squants.experimental.unitgroups.UnitGroup

import squants.experimental.unitgroups.si.strict.implicits._
// import squants.experimental.unitgroups.si.strict.implicits._

val siLengths: UnitGroup[Length] = implicitly[UnitGroup[Length]]
// siLengths: squants.experimental.unitgroups.UnitGroup[squants.space.Length] = squants.experimental.unitgroups.si.strict.package$implicits$$anon$1@f52ca1b

To print out units and their conversion factors to the primary SI unit, you could use this code:

import squants.{Quantity, UnitOfMeasure}
// import squants.{Quantity, UnitOfMeasure}

def mkConversionFactor[A <: Quantity[A]](uom: UnitOfMeasure[A]): Double = {
  val one = uom(1)
  one.to(one.dimension.siUnit)
}
// mkConversionFactor: [A <: squants.Quantity[A]](uom: squants.UnitOfMeasure[A])Double

def mkTuple[A <: Quantity[A]](uom: UnitOfMeasure[A]): (String, Double) = {
  (uom.symbol, mkConversionFactor(uom))
}
// mkTuple: [A <: squants.Quantity[A]](uom: squants.UnitOfMeasure[A])(String, Double)

siLengths.sortedUnits.toList.map(mkTuple).foreach(println)
// (nm,1.0E-9)
// (ยตm,1.0E-6)
// (mm,0.001)
// (cm,0.01)
// (dm,0.1)
// (m,1.0)
// (dam,10.0)
// (hm,100.0)
// (km,1000.0)

Note that UnitGroup's sortedUnits field is a SortedSet, so before mapping over it, you will probably want to convert it to a List, otherwise the output may be resorted.

Non-SI UnitGroups

Other UnitGroup definitions don't use implicits. For example, squants.experimental.unitgroups.uscustomary.space.UsCustomaryLiquidVolumes or squants.experimental.unitgroups.misc.TroyMasses can be imported and used directly.

Creating an ad-hoc UnitGroup

To create an ad-hoc UnitGroup just implement the trait. For example, to make a US cooking measure UnitGroup:

import squants.{Quantity, Dimension}
// import squants.{Quantity, Dimension}

import squants.space._
// import squants.space._

import squants.experimental.unitgroups.UnitGroup
// import squants.experimental.unitgroups.UnitGroup

val usCookingUnitGroup = new UnitGroup[Volume] {
  // units don't have to be specified in-order.
  val units: Set[UnitOfMeasure[Volume]] = Set(UsPints, UsGallons, Teaspoons, Tablespoons, UsQuarts, FluidOunces)
}
// usCookingUnitGroup: squants.experimental.unitgroups.UnitGroup[squants.space.Volume]{val units: Set[squants.UnitOfMeasure[squants.space.Volume]]} = $anon$1@495c28e0

// squants automatically sorts units
usCookingUnitGroup.sortedUnits.foreach(println)
// squants.space.Teaspoons$@1f2aa9fd
// squants.space.Tablespoons$@20a4318e
// squants.space.FluidOunces$@3f31c22
// squants.space.UsPints$@6b5332c3
// squants.space.UsQuarts$@5293da73
// squants.space.UsGallons$@25de9dee

The UnitGroup values provided with Squants are only samples and aren't intended to be exhaustive. We encourage users to make their own UnitGroup defintitions and submit them as PRs if they're generally applicable.

Formatters

Squants provides an experimental API for formatting Quantities in the "best unit." For example, convert Inches(12) to Feet(1). This is useful for producing human-friendly output.

To use a formatter, you must implement the squants.formatters.Formatter trait:

trait Formatter[A <: Quantity[A]] {
  def inBestUnit(quantity: Quantity[A]): A
}

Default Formatter implementation

There is a default formatter implementation in squants.experimental.formatter.DefaultFormatter. This builds on the UnitGroup API discussed above to choose the best UnitOfMeasure for a Quantity. The DefaultFormatter algorithm will probably work for most use-cases, but users can create their own Formatters if they have custom needs.

To use DefaultFormatter import it, and a unit group:

import squants.experimental.formatter.DefaultFormatter
import squants.experimental.unitgroups.misc.AstronomicalLengthUnitGroup

Then create the formatter by passing in a unit group:

val astroFormatter = new DefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@790fe346

Now, we create some values using human-unfriendly numbers:

import squants.space.LengthConversions._
// import squants.space.LengthConversions._

val earthToJupiter = 588000000.km
// earthToJupiter: squants.space.Length = 588000000.0 km

val earthToVoyager1 = 2.06e10.km
// earthToVoyager1: squants.space.Length = 20600000000.0 km

val earthToAlphaCentauri = 4.1315e+13.km
// earthToAlphaCentauri: squants.space.Length = 41315000000000.0 km

And format them into appropriate units (AUs and Parsecs, in this case):

astroFormatter.inBestUnit(earthToJupiter)
// res3: squants.space.Length = 3.9305372278938457 au

astroFormatter.inBestUnit(earthToVoyager1)
// res4: squants.space.Length = 137.70249471872998 au

astroFormatter.inBestUnit(earthToAlphaCentauri)
// res5: squants.space.Length = 1.3389279634339382 pc

Implicit formatters

There is a nicer syntax for formatters available via implicits. This lets you write expressions such as 12.inches.inBestUnit. This syntax is added per-Dimension.

To use this syntax, first import squants.experimental.formatter.syntax._. Then, for each Dimension you wish to use, place a Formatter for the Dimension in implicit scope. In this example, we're adding support for Length.

import squants.experimental.formatter.DefaultFormatter
import squants.experimental.formatter.syntax._
import squants.mass.MassConversions._
import squants.space.Length
import squants.space.LengthConversions._
import squants.experimental.unitgroups.misc.AstronomicalLengthUnitGroup
implicit val astroFormatter = new DefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@135301a1

val earthToJupiter = 588000000.km
// earthToJupiter: squants.space.Length = 588000000.0 km

val earthToVoyager1 = 2.06e10.km
// earthToVoyager1: squants.space.Length = 20600000000.0 km

val earthToAlphaCentauri = 4.1315e+13.km
// earthToAlphaCentauri: squants.space.Length = 41315000000000.0 km

earthToJupiter.inBestUnit
// res0: squants.Quantity[squants.space.Length] = 3.9305372278938457 au

earthToVoyager1.inBestUnit
// res1: squants.Quantity[squants.space.Length] = 137.70249471872998 au

earthToAlphaCentauri.inBestUnit
// res2: squants.Quantity[squants.space.Length] = 1.3389279634339382 pc

This example won't compile because there is no Formatter[Mass] in implicit scope:

scala> 5000.grams.inBestUnit
<console>:26: error: could not find implicit value for parameter formatter: squants.experimental.formatter.Formatter[squants.mass.Mass]
       5000.grams.inBestUnit
                  ^

SI Formatters and implicit syntax

When using SI units, and the default formatter algorithm, you don't have to declare a Formatter and place it in implicit scope. The compiler can do that for you. This creates a very human-friendly API by using the appropriate imports.

First, import the SI unit groups and their implicits:

import squants.experimental.unitgroups.ImplicitDimensions.space._
import squants.experimental.unitgroups.si.strict.implicits._

Next, import the formatter syntax described above:

import squants.experimental.formatter.syntax._

Finally, add imports for implicitly deriving formatters:

scala> import squants.experimental.formatter.implicits._
import squants.experimental.formatter.implicits._

Now we can create quantities and format them by calling .inBestUnit directly:

import squants.space.LengthConversions._
5.cm.inBestUnit
// res0: squants.Quantity[squants.space.Length] = 5.0 cm

500.cm.inBestUnit
// res1: squants.Quantity[squants.space.Length] = 5.0 m

3000.meters.inBestUnit
// res2: squants.Quantity[squants.space.Length] = 3.0 km

Type Hierarchy

The type hierarchy includes the following core types: Quantity, Dimension, and UnitOfMeasure

Quantity and Dimension

A Dimension represents a type of Quantity. For example: Mass, Length, Time, etc.

A Quantity represents a dimensional value or measurement. A Quantity is a combination of a numeric value and a unit. For example: 2 lb, 10 km, 3.4 hr.

Squants has built in support for 54 quantity dimensions.

Unit of Measure

UnitOfMeasure is the scale or multiplier in which the Quantity is being measured. Squants has built in support for over 257 units of measure

For each Dimension a set of UOM objects implement a primary UOM trait typed to that Quantity. The UOM objects define the unit symbols, conversion factors, and factory methods for creating Quantities in that unit.

Quantity Implementations

The code for specific implementations include

  • A class representing the Quantity including cross-dimensional operations
  • A companion object representing the Dimension and set of available units
  • A base trait for its Units
  • A set of objects defining specific units, their symbols and conversion factors

This is an abbreviated example of how a Quantity type is constructed:

class Length(val value: Double, val unit: LengthUnit) extends Quantity[Length]  { ... }
object Length extends Dimension[Length]  { ... }
trait LengthUnit extends UnitOfMeasure[Length]  { ... }
object Meters extends LengthUnit { ... }
object Yards extends LengthUnit { ... }

The apply method of the UOM objects are implemented as factories for creating Quantity values.

val len1: Length = Meters(4.3)
val len2: Length = Yards(5)

Squants currently supports 257 units of measure

Time Derivatives

Special traits are used to establish a time derivative relationship between quantities.

For example Velocity is the 1st Time Derivative of Length (Distance), Acceleration is the 2nd Time Derivative.

class Length( ... ) extends Quantity[Length] with TimeIntegral[Velocity]
...
class Velocity( ... ) extends Quantity[Velocity] with TimeDerivative[Length] with TimeIntegral[Acceleration]
...
class Acceleration( ... ) extends Quantity[Acceleration] with TimeDerivative[Velocity]

These traits provide operations with time operands which result in correct dimensional transformations.

Using these imports:

import squants.energy.Kilowatts
import squants.motion.{Acceleration, Velocity}
import squants.space.{Kilometers, Length}
import squants.space.LengthConversions._
import squants.time.{Hours, Seconds, Time}
import squants.time.TimeConversions._

You can code the following:

scala> val distance: Length = Kilometers(100)
distance: squants.space.Length = 100.0 km

scala> val time: Time = Hours(2)
time: squants.time.Time = 2.0 h

scala> val velocity: Velocity = distance / time
velocity: squants.motion.Velocity = 13.88888888888889 m/s

scala> val acc: Acceleration = velocity / Seconds(1)
acc: squants.motion.Acceleration = 13.88888888888889 m/sยฒ

scala> val gravity = 32.feet / second.squared
gravity: squants.Acceleration = 9.7536195072 m/sยฒ

Power is the 1st Time Derivative of Energy, PowerRamp is the 2nd.

scala> val power = Kilowatts(100)
power: squants.energy.Power = 100.0 kW

scala> val time: Time = Hours(2)
time: squants.time.Time = 2.0 h

scala> val energy = power * time
energy: squants.energy.Energy = 200000.0 Wh

scala> val ramp = Kilowatts(50) / Hours(1)
ramp: squants.energy.PowerRamp = 50000.0 W/h

Use Cases

Dimensional Analysis

The primary use case for Squants, as described above, is to produce code that is typesafe within domains that perform dimensional analysis.

This code samples in this section use these imports:

import squants.energy.Energy
import squants.energy.EnergyConversions._
import squants.energy.PowerConversions._
import squants.market.{Money, Price}
import squants.market.MoneyConversions._
import squants.market.defaultMoneyContext
import squants.mass.{Density, Mass}
import squants.mass.MassConversions._
import squants.motion.{Acceleration, Velocity, VolumeFlow}
import squants.motion.AccelerationConversions._
import squants.space.LengthConversions._
import squants.space.VolumeConversions._
import squants.time.Time
import squants.time.TimeConversions._
scala> implicit val moneyContext = defaultMoneyContext
moneyContext: squants.market.MoneyContext = MoneyContext(DefaultCurrency(USD),Currencies(ARS,AUD,BRL,BTC,CAD,CHF,CLP,CNY,CZK,DKK,ETH,EUR,GBP,HKD,INR,JPY,KRW,LTC,MXN,MYR,NAD,NOK,NZD,RUB,SEK,USD,XAG,XAU,ZAR),ExchangeRates(),AllowIndirectConversions(true))

scala> val energyPrice: Price[Energy] = 45.25.money / megawattHour
energyPrice: squants.market.Price[squants.energy.Energy] = 45.25 USD/1.0 MWh

scala> val energyUsage: Energy = 345.kilowatts * 5.4.hours
energyUsage: squants.energy.Energy = 1863000.0000000002 Wh

scala> val energyCost: Money = energyPrice * energyUsage
energyCost: squants.market.Money = 84.30075000000000905 USD

scala> val dodgeViper: Acceleration = 60.miles / hour / 3.9.seconds
dodgeViper: squants.motion.Acceleration = 6.877552216615386 m/sยฒ

scala> val speedAfter5Seconds: Velocity = dodgeViper * 5.seconds
speedAfter5Seconds: squants.motion.Velocity = 34.38776108307693 m/s

scala> val timeTo100MPH: Time = 100.miles / hour / dodgeViper
timeTo100MPH: squants.time.Time = 6.499999999999999 s

scala> val density: Density = 1200.kilograms / cubicMeter
density: squants.mass.Density = 1200.0 kg/mยณ

scala> val volFlowRate: VolumeFlow = 10.gallons / minute
volFlowRate: squants.motion.VolumeFlow = 6.30901964E-4 mยณ/s

scala> val flowTime: Time = 30.minutes
flowTime: squants.time.Time = 30.0 m

scala> val totalMassFlow: Mass = volFlowRate * flowTime * density
totalMassFlow: squants.mass.Mass = 1362.7482422399999 kg

Domain Modeling

Another excellent use case for Squants is stronger typing for fields in your domain model.

Code samples in this section use these imports:

import scala.language.postfixOps

import squants.energy.{Energy, Power, PowerRamp}
import squants.energy.EnergyConversions._
import squants.energy.PowerConversions._
import squants.energy.PowerRampConversions._
import squants.market.Price
import squants.market.MoneyConversions._
import squants.time.Time
import squants.time.TimeConversions._

This is OK ...

case class Generator(
  id: String,
  maxLoadKW: Double,
  rampRateKWph: Double,
  operatingCostPerMWh: Double,
  currency: String,
  maintenanceTimeHours: Double)

val gen1 = Generator("Gen1", 5000, 7500, 75.4, "USD", 1.5)
val gen2 = Generator("Gen2", 100, 250, 2944.5, "JPY", 0.5)

... but this is much better

case class Generator(
  id: String,
  maxLoad: Power,
  rampRate: PowerRamp,
  operatingCost: Price[Energy],
  maintenanceTime: Time)

val gen1 = Generator("Gen1", 5 MW, 7.5.MW/hour, 75.4.USD/MWh, 1.5 hours)
val gen2 = Generator("Gen2", 100 kW, 250 kWph, 2944.5.JPY/MWh, 30 minutes)

Anticorruption Layers

Create wrappers around external services that use basic types to represent quantities. Your application code then uses the ACL to communicate with that system thus eliminating the need to deal with type and scale conversions in multiple places throughout your application logic.

class ScadaServiceAnticorruption(val service: ScadaService) {
  // ScadaService returns meter load as Double representing Megawatts
  def getLoad: Power = Megawatts(service.getLoad(meterId))
  }
  // ScadaService.sendTempBias requires a Double representing Fahrenheit
  def sendTempBias(temp: Temperature) =
    service.sendTempBias(temp.to(Fahrenheit))
}

Implement the ACL as a trait and mix in to the application's services where needed.

import squants.radio.{Irradiance, WattsPerSquareMeter}
import squants.thermal.{Celsius, Temperature}

trait WeatherServiceAntiCorruption {
  val service: WeatherService
  def getTemperature: Temperature = Celsius(service.getTemperature)
  def getIrradiance: Irradiance = WattsPerSquareMeter(service.getIrradiance)
}

Extend the pattern to provide multi-currency support

class MarketServiceAnticorruption(val service: MarketService)
     (implicit val moneyContext: = MoneyContext) {

  // MarketService.getPrice returns a Double representing $/MegawattHour
  def getPrice: Price[Energy] =
    (USD(service.getPrice) in moneyContext.defaultCurrency) / megawattHour

  // MarketService.sendBid requires a Double representing $/MegawattHour
  // and another Double representing the max amount of energy in MegawattHours
  def sendBid(bid: Price[Energy], limit: Energy) =
    service.sendBid((bid * megawattHour) to USD, limit to MegawattHours)
}

Build Anticorruption into Akka routers

// LoadReading message used within a Squants enabled application context
case class LoadReading(meterId: String, time: Long, load: Power)
class ScadaLoadListener(router: Router) extends Actor {
  def receive = {
   // ScadaLoadReading - from an external service - sends load as a string
   // eg, โ€œ10.3 MWโ€, โ€œ345 kWโ€
   case msg @ ScadaLoadReading(meterId, time, loadString) โ‡’
    // Parse the string and on success emit the Squants enabled event to routees
    Power(loadString) match {
      case Success(p) => router.route(LoadReading(meterId, time, p), sender())
      case Failure(e) => // react to QuantityStringParseException
    }
  }
}

... and REST API's with contracts that require basic types

trait LoadRoute extends HttpService {
  def repo: LoadRepository
  val loadRoute = {
    path("meter-reading") {
      // REST API contract requires load value and units in different fields
      // Units are string values that may be 'kW' or 'MW'
      post {
        parameters(meterId, time, loadDouble, unit) { (meterId, time, loadDouble, unit) =>
          complete {
            val load = unit match {
              case "kW" => Kilowatts(loadDouble)
              case "MW" => Megawatts(loadDouble)
            }
            repo.saveLoad(meterId, time, load)
          }
        }
      } ~
      // REST API contract requires load returned as a number representing megawatts
      get {
        parameters(meterId, time) { (meterId, time) =>
          complete {
            repo.getLoad(meterId, time) to Megawatts
          }
        }
      }
    }
  }
}

Contributors

Code of Conduct

Squants is a Typelevel Incubator Project and, as such, supports the Typelevel Code of Conduct.

Caveats

Code is offered as-is, with no implied warranty of any kind. Comments, criticisms, and/or praise are welcome, especially from scientists, engineers and the like.

Release procedure

Making a release requires permission to publish to sonatype, and a properly setup signing key:

To make a release do the following:

  • Ensure the version is not set to SNAPSHOT

  • Build the README using tut

  sbt tut
  • Publish a cross-version signed package (no cross-version available for Scala Native)
  sbt +squantsJVM/publishSigned
  sbt +squantsJS/publishSigned
  sbt squantsNative/publishSigned
  • Repeat for scala.js 1.0.0-RC1
  SCALAJS_VERSION=1.0.0-RC1 sbt +squantsJS/publishSigned
  • Then make a release (Note: after this step the release cannot be replaced)
  sbt sonatypeRelease

More Repositories

1

cats

Lightweight, modular, and extensible library for functional programming.
Scala
5,182
star
2

fs2

Compositional, streaming I/O library for Scala
Scala
2,359
star
3

doobie

Functional JDBC layer for Scala.
Scala
2,161
star
4

scalacheck

Property-based testing for Scala
Scala
1,908
star
5

cats-effect

The pure asynchronous runtime for Scala
Scala
1,817
star
6

spire

Powerful new number types and numeric abstractions for Scala.
Scala
1,761
star
7

skunk

A data access library for Scala + Postgres.
Scala
1,579
star
8

simulacrum

First class syntax support for type classes in Scala
Scala
937
star
9

kind-projector

Compiler plugin for making type lambdas (type projections) easier to write
Scala
915
star
10

frameless

Expressive types for Spark.
Scala
879
star
11

cats-collections

Data structures for pure functional programming in Scala
Scala
557
star
12

kittens

Automatic type class derivation for Cats
Scala
531
star
13

jawn

Jawn is for parsing jay-sawn (JSON)
Scala
431
star
14

log4cats

Logging Tools For Interaction with cats-effect
Scala
400
star
15

Laika

Site and E-book Generator and Customizable Text Markup Transformer for sbt, Scala and Scala.js
Scala
387
star
16

algebra

Experimental project to lay out basic algebra type classes
Scala
378
star
17

mouse

A small companion to cats
Scala
365
star
18

sbt-tpolecat

scalac options for the enlightened
Scala
328
star
19

discipline

Flexible law checking for Scala
Scala
328
star
20

natchez

functional tracing for cats
Scala
324
star
21

cats-tagless

Library of utilities for tagless final encoded algebras
Scala
314
star
22

cats-mtl

cats transformer type classes.
Scala
308
star
23

CT_from_Programmers.scala

Scala sample code for Bartosz Milewski's CT for Programmers
Scala
279
star
24

fs2-grpc

gRPC implementation for FS2/cats-effect
Scala
270
star
25

cats-parse

A parsing library for the cats ecosystem
Scala
233
star
26

machinist

Spire's macros for zero-cost operator enrichment
Scala
191
star
27

cats-effect-testing

Integration between cats-effect and test frameworks
Scala
191
star
28

shapeless-3

Generic programming for Scala
Scala
185
star
29

paiges

an implementation of Wadler's a prettier printer
Scala
183
star
30

grackle

Grackle: Functional GraphQL for the Typelevel stack
Scala
176
star
31

sbt-typelevel

Let sbt work for you.
Scala
170
star
32

munit-cats-effect

Integration library for MUnit & cats-effect
Scala
149
star
33

feral

Feral cats are homeless, feral functions are serverless
Scala
144
star
34

catbird

Birds and cats together
Scala
139
star
35

otel4s

An OpenTelemetry library for Scala based on Cats-Effect
Scala
138
star
36

fs2-chat

Sample project demonstrating use of fs2-io to build a chat client and server
Scala
123
star
37

spotted-leopards

Proof of concept for a cats-like library built using Dotty features
Scala
112
star
38

fabric

Object-Notation Abstraction for JSON, binary, HOCON, etc.
Scala
110
star
39

literally

Compile time validation of literal values built from strings
Scala
106
star
40

toolkit

Quickstart your next app with the Typelevel Toolkit!
Scala
94
star
41

cats-time

Cats Instances for Java Time
Scala
91
star
42

typelevel-nix

Development tools for Typelevel projects
Nix
87
star
43

cats-effect-cps

An incubator project for async/await syntax support for Cats Effect
Scala
81
star
44

vault

Type-safe, persistent storage for values of arbitrary types
Scala
81
star
45

shapeless-contrib

Interoperability libraries for Shapeless
Scala
79
star
46

scalacheck-effect

Effectful property testing built on ScalaCheck
Scala
76
star
47

coop

Cooperative multithreading as a pure monad transformer
Scala
68
star
48

claimant

Library to support automatic labeling of ScalaCheck properties.
Scala
68
star
49

typeclassic

Everything you need to make type classes first class.
Scala
61
star
50

scalaz-contrib

Interoperability libraries & additional data structures and instances for Scalaz
Scala
55
star
51

twiddles

Micro-library for building effectful protocols
Scala
55
star
52

monoids

Generic Monoids for Scala
Scala
51
star
53

fs2-netty

What it says on the tin!
Scala
47
star
54

sbt-catalysts

sbt utilities for open source projects
Scala
45
star
55

natchez-http4s

Glorious integration layer for Natchez and Http4s.
Scala
44
star
56

typelevel.github.com

Web site of typelevel.scala
HTML
40
star
57

jawn-fs2

Integration between jawn and fs2
Scala
38
star
58

keypool

A Keyed Pool Implementation for Scala
Scala
34
star
59

scalaz-specs2

Specs2 bindings for Scalaz
Scala
34
star
60

catalysts

Scala
34
star
61

simulacrum-scalafix

Simulacrum as Scalafix rules
Scala
34
star
62

case-insensitive

A case-insensitive string for Scala
Scala
34
star
63

scalaz-outlaws

outcasts no longer allowed in the ivory tower
Scala
28
star
64

bobcats

Typelevel's very own CryptoKitties!
Scala
28
star
65

scalac-options

A library for configuring scalac options
Scala
27
star
66

weaver-test

A test framework that runs everything in parallel.
Scala
27
star
67

ce3.g8

Scala
24
star
68

scalaz-scalatest

Scalatest bindings for scalaz.
Scala
23
star
69

general

Repository for general Typelevel information, activity and issues
19
star
70

discipline-munit

MUnit binding for Typelevel Discipline
Scala
18
star
71

cats-testkit-scalatest

Cats Testkit for Scalatest
Scala
18
star
72

unique

Unique Functional Values for Scala
Scala
17
star
73

discipline-scalatest

ScalaTest binding for Discipline
Scala
17
star
74

typelevel-scalafix

Scalafix rules for Typelevel projects
Scala
17
star
75

semigroups

Scala
16
star
76

cats-effect-shell

Command line debugging console for Cats Effect
Scala
15
star
77

jdk-index

A Jabba compatible index of JDK versions
Scala
14
star
78

cats-uri

URI implementation based on cats-parse with cats instances
Scala
14
star
79

typelevel.g8

A typelevel.g8 based on sbt-typelevel
Scala
14
star
80

catapult

Scala
13
star
81

discipline-specs2

Specs2 Integration for Discipline
Scala
9
star
82

governance

Typelevel governance
Scala
7
star
83

catz-cradle

Testbed for scala libraries and tools, based on examples from cats docs
Scala
7
star
84

spire-contrib

Interoperability libraries for spire
Shell
7
star
85

idna4s

Cross-platform Scala implementation of Internationalized Domain Names in Applications
Scala
7
star
86

scalac-compat

Lightweight tools for tackling Scalac version incompatibilities
Scala
6
star
87

steward

Runs Scala Steward for Typelevel projects
5
star
88

cats-effect-main

3
star
89

sacagawea

Common infrastructure for tracing functional effects
Scala
3
star
90

scalacheck-xml

Scalacheck instances for scala-xml
Scala
3
star
91

sorcery

WIP
2
star
92

scalacheck-web

ScalaCheck Web Site
Nix
2
star
93

sbt-catalysts.g8

Scala
2
star
94

feral.g8

Giter8 template for feral serverless
Scala
2
star
95

download-java

2
star
96

toolkit.g8

A Giter8 template for Typelevel Toolkit!
Scala
2
star
97

sbt-tls-crossproject

sbt-crossproject plugin for Typelevel Scala
Scala
1
star
98

await-cirrus

Depend on Cirrus CI from a GitHub Actions workflow
JavaScript
1
star
99

catalysts-docker

Shell
1
star
100

.github

a โœจspecial โœจ repository for project defaults and organization readme
1
star