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.
Current Release: 1.6.0 (API Docs)
Development Build: 1.7.0-SNAPSHOT (API Docs)
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
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
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!
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>valloadKw=1.2
loadKw:Double=1.2
scala>valenergyMwh=24.2
energyMwh:Double=24.2
scala>valsumKw= loadKw + energyMwh
sumKw:Double=25.4This 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.
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>importsquants.energy.{Kilowatts, Megawatts, Power}
importsquants.energy.{Kilowatts, Megawatts, Power}
scala>valload1:Power=Kilowatts(12)
load1: squants.energy.Power=12.0 kW
scala>valload2:Power=Megawatts(0.023)
load2: squants.energy.Power=0.023MW
scala>valsum= 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=trueThe 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>importsquants.energy.{Energy, Power, Kilowatts, KilowattHours}
importsquants.energy.{Energy, Power, Kilowatts, KilowattHours}
scala>valload:Power=Kilowatts(1.2)
load: squants.energy.Power=1.2 kW
scala>valenergy:Energy=KilowattHours(23.0)
energy: squants.energy.Energy=23.0 kWhInvalid operations, like adding power and energy, no longer compile:
scala>valsum= load + energy
<console>:16:error: typemismatch;
found : squants.energy.Energyrequired: squants.energy.Powervalsum= 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.
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:
importsquants.energy.{Kilowatts, Power}
importsquants.time.{Hours, Days}The following code demonstrates creating ratio between two quantities of the same dimension, resulting in a dimensionless value:
scala>valratio=Days(1) /Hours(3)
ratio:Double=8.0This code demonstrates use of the Power.* method that takes a Time and returns an Energy:
scala>valload=Kilowatts(1.2)
load: squants.energy.Power=1.2 kW
scala>valtime=Hours(2)
time: squants.time.Time=2.0 h
scala>valenergyUsed= load * time
energyUsed: squants.energy.Energy=2400.0WhThis code demonstrates use of the Energy./ method that takes a Time and returns a Power:
scala>valaveLoad:Power= energyUsed / time
aveLoad: squants.energy.Power=1200.0WCode samples in this section assume these imports:
importscala.language.postfixOpsimportsquants.energy.{Gigawatts, Kilowatts, Power, Megawatts}
importsquants.mass.MassConversions._importsquants.mass.{Kilograms, Pounds}
importsquants.thermal.TemperatureConversions._importsquants.thermal.FahrenheitQuantity values are based in the units used to create them.
scala>valloadA:Power=Kilowatts(1200)
loadA: squants.energy.Power=1200.0 kW
scala>valloadB:Power=Megawatts(1200)
loadB: squants.energy.Power=1200.0MWSince 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>valloadA=Kilowatts(1200)
loadA: squants.energy.Power=1200.0 kW
scala>valloadB= loadA in Megawatts
loadB: squants.energy.Power=1.2MW
scala>valloadC= loadA in Gigawatts
loadC: squants.energy.Power=0.0012GWSometimes 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>valload:Power=Kilowatts(1200)
load: squants.energy.Power=1200.0 kW
scala>valkw:Double= load to Kilowatts
kw:Double=1200.0
scala>valmw:Double= load to Megawatts
mw:Double=1.2
scala>valgw:Double= load to Gigawatts
gw:Double=0.0012Most types include methods with convenient aliases for the to methods.
scala>valkw:Double= load toKilowatts
kw:Double=1200.0
scala>valmw:Double= load toMegawatts
mw:Double=1.2
scala>valgw:Double= load toGigawatts
gw:Double=0.0012NOTE - 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>valkw:String= load toString Kilowatts
kw:String=1200.0 kW
scala>valmw:String= load toString Megawatts
mw:String=1.2MW
scala>valgw:String= load toString Gigawatts
gw:String=0.0012GWCreating Tuple2[Double, String] that includes a numeric value and unit symbol:
scala>valload:Power=Kilowatts(1200)
load: squants.energy.Power=1200.0 kW
scala>valkw= load toTuple
kw: (Double, String) = (1200.0,kW)
scala>valmw= load toTuple Megawatts
mw: (Double, String) = (1.2,MW)
scala>valgw= 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.0Apply a Double => Double operation to the underlying value of a quantity, while preserving its type and unit.
scala>importsquants.energy.Kilowattsimportsquants.energy.Kilowatts
scala>valload=Kilowatts(2.0)
load: squants.energy.Power=2.0 kW
scala>valnewLoad= load.map(v => v *2+10)
newLoad: squants.energy.Power=14.0 kWThe 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.
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>importsquants.energy.{Kilowatts, Watts}
importsquants.energy.{Kilowatts, Watts}
scala>valload=Kilowatts(2.0)
load: squants.energy.Power=2.0 kW
scala>valreading=Kilowatts(1.9999)
reading: squants.energy.Power=1.9999 kWCalls to approx (and its symbolic aliases) use an implicit tolerance:
scala>implicitvaltolerance=Watts(.1)
tolerance: squants.energy.Power=0.1W
scala> load =~ reading
res0:Boolean=true
scala> load ≈ reading
res1:Boolean=true
scala> load approx reading
res2:Boolean=trueThe =~ 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.
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>importsquants.{QuantityVector, SVector}
importsquants.{QuantityVector, SVector}
scala>importsquants.space.{Kilometers, Length}
importsquants.space.{Kilometers, Length}
scala>importsquants.space.LengthConversions._importsquants.space.LengthConversions._
scala>valvector: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>valmagnitude:Length= vector.magnitude // returns the scalar value of the vector
magnitude: squants.space.Length=5.021951811795888 km
scala>valnormalized= 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>valvector2: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>valvectorSum= vector + vector2 // returns the sum of two vectors
vectorSum: vector.SVectorType=QuantityVector(ArrayBuffer(2.4 km, 8.6 km, 4.6 km))
scala>valvectorDiff= vector - vector2 // return the difference of two vectors
vectorDiff: vector.SVectorType=QuantityVector(ArrayBuffer(0.0 km, 0.0 km, 0.0 km))
scala>valvectorScaled= vector *5// returns vector scaled 5 times
vectorScaled: vector.SVectorType=QuantityVector(ArrayBuffer(6.0 km, 21.5 km, 11.5 km))
scala>valvectorReduced= vector /5// returns vector reduced 5 time
vectorReduced: vector.SVectorType=QuantityVector(ArrayBuffer(0.24 km, 0.86 km, 0.45999999999999996 km))
scala>valvectorDouble= vector /5.meters // returns vector reduced and converted to DoubleVector
vectorDouble: squants.DoubleVector=DoubleVector(ArrayBuffer(240.0, 860.0, 459.99999999999994))
scala>valdotProduct= vector * vectorDouble // returns the Dot Product of vector and vectorDouble
dotProduct: squants.space.Length=5044.0 km
scala>valcrossProduct= 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.
importsquants.DoubleVectorvalvector:DoubleVector=SVector(1.2, 4.3, 2.3, 5.4) // a Four-dimensional vectorCurrently dimensional conversions are supported by using the slightly verbose, but flexible map method.
scala>importsquants.{DoubleVector, QuantityVector}
importsquants.{DoubleVector, QuantityVector}
scala>importsquants.motion.Velocityimportsquants.motion.Velocity
scala>importsquants.space.{Area, Kilometers, Length, Meters}
importsquants.space.{Area, Kilometers, Length, Meters}
scala>importsquants.time.Secondsimportsquants.time.Seconds
scala>valvectorLength=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>valvectorArea= 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>valvectorVelocity= 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>valvectorDouble=DoubleVector(1.2, 4.3, 2.3)
vectorDouble: squants.DoubleVector=DoubleVector(WrappedArray(1.2, 4.3, 2.3))
scala>valvectorLength= 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>valvectorLength=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>valvectorMetersNum= vectorLength.to(Meters) // DoubleVector(1200.0, 4300.0, 2300.0)
vectorMetersNum: squants.DoubleVector=DoubleVector(ArrayBuffer(1200.0, 4300.0, 2300.0))
scala>valvectorMeters= 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 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.
A Quantity of purchasing power measured in Currency units. Like other quantities, the Unit of Measures are used to create Money values.
scala>importsquants.market.{BTC, JPY, USD, XAU}
importsquants.market.{BTC, JPY, USD, XAU}
scala>valtenBucks=USD(10) // Money: 10 USD
tenBucks: squants.market.Money=1E+1USD
scala>valsomeYen=JPY(1200) // Money: 1200 JPY
someYen: squants.market.Money=1.2E+3JPY
scala>valgoldStash=XAU(50) // Money: 50 XAU
goldStash: squants.market.Money=5E+1XAU
scala>valdigitalCache=BTC(50) // Money: 50 BTC
digitalCache: squants.market.Money=5E+1BTCA 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:
importsquants.{Dozen, Each}
importsquants.energy.MegawattHoursimportsquants.market.USDimportsquants.space.UsGallonsYou can compute the following:
scala>valthreeForADollar=USD(1) /Each(3)
threeForADollar: squants.market.Price[squants.Dimensionless] =1USD/3.0 ea
scala>valenergyPrice=USD(102.20) /MegawattHours(1)
energyPrice: squants.market.Price[squants.energy.Energy] =102.2USD/1.0MWh
scala>valmilkPrice=USD(4) /UsGallons(1)
milkPrice: squants.market.Price[squants.space.Volume] =4USD/1.0 gal
scala>valcostForABunch= threeForADollar *Dozen(10)
costForABunch: squants.market.Money=4E+1USD
scala>valenergyCost= energyPrice *MegawattHours(4)
energyCost: squants.market.Money=408.8USD
scala>valmilkQuota=USD(20) / milkPrice
milkQuota: squants.space.Volume=5.0 galConversions to Strings
scala>valmoney=USD(123.456)
money: squants.market.Money=123.456USD
scala>vals= money.toString // returns full precision amount with currency code
s:String=123.456USD
scala>vals= money.toFormattedString // returns currency symbol and amount rounded based on currency rules
s:String= $123.46Currency Exchange Rates are used to define the conversion factors between currencies
scala>importsquants.market.{CurrencyExchangeRate, JPY, Money, USD}
importsquants.market.{CurrencyExchangeRate, JPY, Money, USD}
scala>// create an exchange rate|valrate1=CurrencyExchangeRate(USD(1), JPY(100))
rate1: squants.market.CurrencyExchangeRate=USD/JPY100.0
scala>// OR|valrate2=USD/JPY(100)
rate2: squants.market.CurrencyExchangeRate=USD/JPY100.0
scala>// OR|valrate3=JPY(100) ->USD(1)
rate3: squants.market.CurrencyExchangeRate=USD/JPY100.0
scala>// OR|valrate4=JPY(100) toThe USD(1)
rate4: squants.market.CurrencyExchangeRate=USD/JPY100.0
scala>valsomeYen:Money=JPY(350)
someYen: squants.market.Money=3.5E+2JPY
scala>valsomeBucks:Money=USD(23.50)
someBucks: squants.market.Money=23.5USDUse the convert method which automatically converts the money to the 'other' currency:
scala>valdollarAmount:Money= rate1.convert(someYen)
dollarAmount: squants.market.Money=3.5USD
scala>valyenAmount:Money= rate1.convert(someBucks)
yenAmount: squants.market.Money=2.35E+3JPYOr just use the * operator in either direction (money * rate, or rate * money):
scala>valdollarAmount2:Money= rate1 * someYen
dollarAmount2: squants.market.Money=3.5USD
scala>valyenAmount2:Money= someBucks * rate1
yenAmount2: squants.market.Money=2.35E+3JPYA 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:
importsquants.energy.MegawattHoursimportsquants.market.{CAD, JPY, MXN, USD}
importsquants.market.defaultMoneyContextYou can compute:
scala>valexchangeRates=List(USD/CAD(1.05), USD/MXN(12.50), USD/JPY(100))
exchangeRates:List[squants.market.CurrencyExchangeRate] =List(USD/CAD1.05, USD/MXN12.5, USD/JPY100.0)
scala>implicitvalmoneyContext= 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/CAD1.05,USD/JPY100.0,USD/MXN12.5),AllowIndirectConversions(true))
scala>valenergyPrice=USD(102.20) /MegawattHours(1)
energyPrice: squants.market.Price[squants.energy.Energy] =102.2USD/1.0MWh
scala>valsomeMoney=Money(350) // 350 in the default Cur
someMoney: squants.market.Money=3.5E+2USD
scala>valusdMoney:Money= someMoney in USD
usdMoney: squants.market.Money=3.5E+2USD
scala>valusdBigDecimal:BigDecimal= someMoney to USD
usdBigDecimal:BigDecimal=350.0
scala>valyenCost:Money= (energyPrice *MegawattHours(5)) in JPY
yenCost: squants.market.Money=5.11E+4JPY
scala>valnorthAmericanSales:Money= (CAD(275) +USD(350) +MXN(290)) in USD
northAmericanSales: squants.market.Money=635.1047619047619047619047619047619USDA QuantityRange is used to represent a range of Quantity values between an upper and lower bound:
importsquants.QuantityRangeimportsquants.energy.{Kilowatts, Megawatts, Power}valload1:Power=Kilowatts(1000)
// load1: squants.energy.Power = 1000.0 kWvalload2:Power=Kilowatts(5000)
// load2: squants.energy.Power = 5000.0 kWvalrange:QuantityRange[Power] =QuantityRange(load1, load2)
// range: squants.QuantityRange[squants.energy.Power] = QuantityRange(1000.0 kW,5000.0 kW)The QuantityRange constructor requires that upper is strictly greater than lower:
importsquants.space.LengthConversions._// import squants.space.LengthConversions._// this will work b/c upper > lowerQuantityRange(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 elidedQuantityRange 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>valdistances=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=trueThe 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:
valrs1= 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:
valrs2= 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:
valrs3= 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 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 kWSubdivide 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 MWNOTE - 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 ...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
importsquants.energy.{Kilowatts, MegawattHours, Power}
importsquants.market.{Price, USD}
importsquants.time.HoursCreate Quantities using Unit Of Measure Factory objects (no implicits required):
scala>valload=Kilowatts(100)
load: squants.energy.Power=100.0 kW
scala>valtime=Hours(3.75)
time: squants.time.Time=3.75 h
scala>valmoney=USD(112.50)
money: squants.market.Money=112.5USD
scala>valprice=Price(money, MegawattHours(1))
price: squants.market.Price[squants.energy.Energy] =112.5USD/1.0MWhCreate Quantities using Unit of Measure names and/or symbols (uses implicits):
importscala.language.postfixOpsimportsquants.energy.EnergyConversions._importsquants.energy.PowerConversions._importsquants.information.InformationConversions._importsquants.market.MoneyConversions._importsquants.space.LengthConversions._importsquants.time.TimeConversions._scala>valload1=100 kW // Simple expressions don’t need dots
load1: squants.energy.Power=100.0 kW
scala>valload2=100 megawatts
load2: squants.energy.Power=100.0MW
scala>valtime=3.hours +45.minutes // Compound expressions may need dots
time: squants.time.Time=3.75 hCreate Quantities using operations between other Quantities:
scala>valenergyUsed=100.kilowatts * (3.hours +45.minutes)
energyUsed: squants.energy.Energy=375000.0Wh
scala>valprice=112.50.USD/1.megawattHours
price: squants.market.Price[squants.energy.Energy] =112.5USD/1.0MWh
scala>valspeed=55.miles /1.hours
speed: squants.motion.Velocity=24.587249174399997 m/sCreate Quantities using formatted Strings:
scala>valload=Power("40 MW")
load: scala.util.Try[squants.energy.Power] =Success(40.0MW)Create Quantities using Tuples:
scala>valload=Power((40.5, "MW"))
load: scala.util.Try[squants.energy.Power] =Success(40.5MW)Use single unit values to simplify expressions:
scala>// Hours(1) == 1.hours == hour|valramp=100.kilowatts / hour
ramp: squants.energy.PowerRamp=100000.0W/h
scala>valspeed=100.kilometers / hour
speed: squants.motion.Velocity=27.77777777777778 m/s
scala>// MegawattHours(1) == 1.megawattHours == megawattHour == MWh|valhi=100.dollars /MWh
hi: squants.market.Price[squants.energy.Energy] =1E+2USD/1.0MWh
scala>vallow=40.dollars / megawattHour
low: squants.market.Price[squants.energy.Energy] =4E+1USD/1.0MWhImplicit conversion support for using Doubles, Longs and BigDecimals on the left side of multiply and divide operations:
scala>valload=10.22*4.MW
load: squants.energy.Power=40.88MW
scala>valdriveArrayCapacity=12*600.gb
driveArrayCapacity: squants.information.Information=7200.0GB
scala>valfreq=60/ second
freq: squants.time.Frequency=60.0Hz
scala>valfreq2=BigDecimal(36000000) / hour
freq2: squants.time.Frequency=10000.0HzCreate Quantity Ranges using to or plusOrMinus (+-) operators:
valrange1=1000.kW to 5000.kW // 1000.kW to 5000.kWvalrange2=5000.kW plusOrMinus 1000.kW // 4000.kW to 6000.kWvalrange2=5000.kW +-1000.kW // 4000.kW to 6000.kWMost 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>importsquants.mass.{Grams, Kilograms}
importsquants.mass.{Grams, Kilograms}
scala>importsquants.mass.MassConversions.MassNumericimportsquants.mass.MassConversions.MassNumeric
scala>valsum=List(Kilograms(100), Grams(34510)).sum
sum: squants.mass.Mass=134510.0 gNOTE - 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.
- MoneyNumeric is a class, not an object like the others.
- To create a MoneyNumeric value there must be an implicit MoneyContext in scope.
- 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:
importsquants.market.defaultMoneyContextimportsquants.market.MoneyConversions._importsquants.market.USDimplicitvalmoneyContext= defaultMoneyContextscala>implicitvalmoneyNum=newMoneyNumeric()
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>valsum=List(USD(100), USD(10)).sum
sum: squants.market.Money=1.1E+2USDSquants 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.
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:
importsquants.space.Length// import squants.space.Lengthimportsquants.experimental.unitgroups.ImplicitDimensions.space._// import squants.experimental.unitgroups.ImplicitDimensions.space._importsquants.experimental.unitgroups.UnitGroup// import squants.experimental.unitgroups.UnitGroupimportsquants.experimental.unitgroups.si.strict.implicits._// import squants.experimental.unitgroups.si.strict.implicits._valsiLengths:UnitGroup[Length] = implicitly[UnitGroup[Length]]
// siLengths: squants.experimental.unitgroups.UnitGroup[squants.space.Length] = squants.experimental.unitgroups.si.strict.package$implicits$$anon$1@f52ca1bTo print out units and their conversion factors to the primary SI unit, you could use this code:
importsquants.{Quantity, UnitOfMeasure}
// import squants.{Quantity, UnitOfMeasure}defmkConversionFactor[A<:Quantity[A]](uom: UnitOfMeasure[A]):Double= {
valone= uom(1)
one.to(one.dimension.siUnit)
}
// mkConversionFactor: [A <: squants.Quantity[A]](uom: squants.UnitOfMeasure[A])DoubledefmkTuple[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.
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.
To create an ad-hoc UnitGroup just implement the trait. For example, to make a US cooking measure UnitGroup:
importsquants.{Quantity, Dimension}
// import squants.{Quantity, Dimension}importsquants.space._// import squants.space._importsquants.experimental.unitgroups.UnitGroup// import squants.experimental.unitgroups.UnitGroupvalusCookingUnitGroup=newUnitGroup[Volume] {
// units don't have to be specified in-order.valunits: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$@25de9deeThe 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.
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:
traitFormatter[A<:Quantity[A]] {
definBestUnit(quantity: Quantity[A]):A
}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:
importsquants.experimental.formatter.DefaultFormatterimportsquants.experimental.unitgroups.misc.AstronomicalLengthUnitGroupThen create the formatter by passing in a unit group:
valastroFormatter=newDefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@790fe346Now, we create some values using human-unfriendly numbers:
importsquants.space.LengthConversions._// import squants.space.LengthConversions._valearthToJupiter=588000000.km
// earthToJupiter: squants.space.Length = 588000000.0 kmvalearthToVoyager1=2.06e10.km
// earthToVoyager1: squants.space.Length = 20600000000.0 kmvalearthToAlphaCentauri=4.1315e+13.km
// earthToAlphaCentauri: squants.space.Length = 41315000000000.0 kmAnd 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 pcThere 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.
importsquants.experimental.formatter.DefaultFormatterimportsquants.experimental.formatter.syntax._importsquants.mass.MassConversions._importsquants.space.Lengthimportsquants.space.LengthConversions._importsquants.experimental.unitgroups.misc.AstronomicalLengthUnitGroupimplicitvalastroFormatter=newDefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@135301a1valearthToJupiter=588000000.km
// earthToJupiter: squants.space.Length = 588000000.0 kmvalearthToVoyager1=2.06e10.km
// earthToVoyager1: squants.space.Length = 20600000000.0 kmvalearthToAlphaCentauri=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 pcThis 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
^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:
importsquants.experimental.unitgroups.ImplicitDimensions.space._importsquants.experimental.unitgroups.si.strict.implicits._Next, import the formatter syntax described above:
importsquants.experimental.formatter.syntax._Finally, add imports for implicitly deriving formatters:
scala>importsquants.experimental.formatter.implicits._importsquants.experimental.formatter.implicits._Now we can create quantities and format them by calling .inBestUnit directly:
importsquants.space.LengthConversions._5.cm.inBestUnit
// res0: squants.Quantity[squants.space.Length] = 5.0 cm500.cm.inBestUnit
// res1: squants.Quantity[squants.space.Length] = 5.0 m3000.meters.inBestUnit
// res2: squants.Quantity[squants.space.Length] = 3.0 kmThe type hierarchy includes the following core types: Quantity, Dimension, and UnitOfMeasure
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.
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.
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:
classLength(valvalue:Double, valunit:LengthUnit) extendsQuantity[Length] { ... }
objectLengthextendsDimension[Length] { ... }
traitLengthUnitextendsUnitOfMeasure[Length] { ... }
objectMetersextendsLengthUnit { ... }
objectYardsextendsLengthUnit { ... }The apply method of the UOM objects are implemented as factories for creating Quantity values.
vallen1:Length=Meters(4.3)
vallen2:Length=Yards(5)Squants currently supports 257 units of measure
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.
classLength( ... ) extendsQuantity[Length] withTimeIntegral[Velocity]
...
classVelocity( ... ) extendsQuantity[Velocity] withTimeDerivative[Length] withTimeIntegral[Acceleration]
...
classAcceleration( ... ) extendsQuantity[Acceleration] withTimeDerivative[Velocity]These traits provide operations with time operands which result in correct dimensional transformations.
Using these imports:
importsquants.energy.Kilowattsimportsquants.motion.{Acceleration, Velocity}
importsquants.space.{Kilometers, Length}
importsquants.space.LengthConversions._importsquants.time.{Hours, Seconds, Time}
importsquants.time.TimeConversions._You can code the following:
scala>valdistance:Length=Kilometers(100)
distance: squants.space.Length=100.0 km
scala>valtime:Time=Hours(2)
time: squants.time.Time=2.0 h
scala>valvelocity:Velocity= distance / time
velocity: squants.motion.Velocity=13.88888888888889 m/s
scala>valacc:Acceleration= velocity /Seconds(1)
acc: squants.motion.Acceleration=13.88888888888889 m/s²
scala>valgravity=32.feet / second.squared
gravity: squants.Acceleration=9.7536195072 m/s²Power is the 1st Time Derivative of Energy, PowerRamp is the 2nd.
scala>valpower=Kilowatts(100)
power: squants.energy.Power=100.0 kW
scala>valtime:Time=Hours(2)
time: squants.time.Time=2.0 h
scala>valenergy= power * time
energy: squants.energy.Energy=200000.0Wh
scala>valramp=Kilowatts(50) /Hours(1)
ramp: squants.energy.PowerRamp=50000.0W/hThe 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:
importsquants.energy.Energyimportsquants.energy.EnergyConversions._importsquants.energy.PowerConversions._importsquants.market.{Money, Price}
importsquants.market.MoneyConversions._importsquants.market.defaultMoneyContextimportsquants.mass.{Density, Mass}
importsquants.mass.MassConversions._importsquants.motion.{Acceleration, Velocity, VolumeFlow}
importsquants.motion.AccelerationConversions._importsquants.space.LengthConversions._importsquants.space.VolumeConversions._importsquants.time.Timeimportsquants.time.TimeConversions._scala>implicitvalmoneyContext= 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>valenergyPrice:Price[Energy] =45.25.money / megawattHour
energyPrice: squants.market.Price[squants.energy.Energy] =45.25USD/1.0MWh
scala>valenergyUsage:Energy=345.kilowatts *5.4.hours
energyUsage: squants.energy.Energy=1863000.0000000002Wh
scala>valenergyCost:Money= energyPrice * energyUsage
energyCost: squants.market.Money=84.30075000000000905USD
scala>valdodgeViper:Acceleration=60.miles / hour /3.9.seconds
dodgeViper: squants.motion.Acceleration=6.877552216615386 m/s²
scala>valspeedAfter5Seconds:Velocity= dodgeViper *5.seconds
speedAfter5Seconds: squants.motion.Velocity=34.38776108307693 m/s
scala>valtimeTo100MPH:Time=100.miles / hour / dodgeViper
timeTo100MPH: squants.time.Time=6.499999999999999 s
scala>valdensity:Density=1200.kilograms / cubicMeter
density: squants.mass.Density=1200.0 kg/m³
scala>valvolFlowRate:VolumeFlow=10.gallons / minute
volFlowRate: squants.motion.VolumeFlow=6.30901964E-4 m³/s
scala>valflowTime:Time=30.minutes
flowTime: squants.time.Time=30.0 m
scala>valtotalMassFlow:Mass= volFlowRate * flowTime * density
totalMassFlow: squants.mass.Mass=1362.7482422399999 kgAnother excellent use case for Squants is stronger typing for fields in your domain model.
Code samples in this section use these imports:
importscala.language.postfixOpsimportsquants.energy.{Energy, Power, PowerRamp}
importsquants.energy.EnergyConversions._importsquants.energy.PowerConversions._importsquants.energy.PowerRampConversions._importsquants.market.Priceimportsquants.market.MoneyConversions._importsquants.time.Timeimportsquants.time.TimeConversions._This is OK ...
caseclassGenerator(
id: String,
maxLoadKW: Double,
rampRateKWph: Double,
operatingCostPerMWh: Double,
currency: String,
maintenanceTimeHours: Double)
valgen1=Generator("Gen1", 5000, 7500, 75.4, "USD", 1.5)
valgen2=Generator("Gen2", 100, 250, 2944.5, "JPY", 0.5)... but this is much better
caseclassGenerator(
id: String,
maxLoad: Power,
rampRate: PowerRamp,
operatingCost: Price[Energy],
maintenanceTime: Time)
valgen1=Generator("Gen1", 5MW, 7.5.MW/hour, 75.4.USD/MWh, 1.5 hours)
valgen2=Generator("Gen2", 100 kW, 250 kWph, 2944.5.JPY/MWh, 30 minutes)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.
classScadaServiceAnticorruption(valservice:ScadaService) {
// ScadaService returns meter load as Double representing MegawattsdefgetLoad:Power=Megawatts(service.getLoad(meterId))
}
// ScadaService.sendTempBias requires a Double representing FahrenheitdefsendTempBias(temp: Temperature) =
service.sendTempBias(temp.to(Fahrenheit))
}Implement the ACL as a trait and mix in to the application's services where needed.
importsquants.radio.{Irradiance, WattsPerSquareMeter}
importsquants.thermal.{Celsius, Temperature}
traitWeatherServiceAntiCorruption {
valservice:WeatherServicedefgetTemperature:Temperature=Celsius(service.getTemperature)
defgetIrradiance:Irradiance=WattsPerSquareMeter(service.getIrradiance)
}Extend the pattern to provide multi-currency support
classMarketServiceAnticorruption(valservice:MarketService)
(implicitvalmoneyContext:=MoneyContext) {
// MarketService.getPrice returns a Double representing $/MegawattHourdefgetPrice: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 MegawattHoursdefsendBid(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 contextcaseclassLoadReading(meterId: String, time: Long, load: Power)
classScadaLoadListener(router: Router) extendsActor {
defreceive= {
// 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 routeesPower(loadString) match {
caseSuccess(p) => router.route(LoadReading(meterId, time, p), sender())
caseFailure(e) =>// react to QuantityStringParseException
}
}
}... and REST API's with contracts that require basic types
traitLoadRouteextendsHttpService {
defrepo:LoadRepositoryvalloadRoute= {
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 {
valload= 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
}
}
}
}
}
}- Gary Keorkunian (garyKeorkunian)
- Jeremy Apthorp (nornagon)
- Steve Barham (stevebarham)
- Derek Morr (derekmorr)
- Michael Korbakov (rmihael)
- Florian Nussberger (fnussber)
- Ajay Chandran (ajaychandran)
- Gia Bảo (giabao)
- Josh Lemer (joshlemer)
- Dave DeCarpio (DaveDeCaprio)
- Carlos Quiroz (cquiroz)
- Szabolcs Berecz (khernyo)
- Matt Hicks (darkfrog26)
- golem131 (golem131)
- Ian O'Hara (ianohara)
- Shadaj Laddad (shadaj)
- Ian McIntosh (cranst0n)
- Doug Hurst (robotsnowfall)
- Philip Axelrod (Paxelord)
Squants is a Typelevel Incubator Project and, as such, supports the Typelevel Code of Conduct.
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.
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
SNAPSHOTBuild 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
