Skip to content

Commit

Permalink
update QuantityRange's error message and docs to clarify behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
derekmorr committed May 25, 2017
1 parent 4eeea4b commit 853b609
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 56 deletions.
133 changes: 99 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ rate2: squants.market.CurrencyExchangeRate = USD/JPY 100.0

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

scala> // OR
| val rate4 = JPY(100) toThe USD(1)
Expand Down Expand Up @@ -659,44 +659,108 @@ northAmericanSales: squants.market.Money = 635.1047619047619 USD
```

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

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

scala> import squants.energy.{Kilowatts, Power}
import squants.energy.{Kilowatts, Power}
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

scala> val load1: Power = Kilowatts(1000)
load1: squants.energy.Power = 1000.0 kW
The `QuantityRange` constructor requires that `upper` is strictly greater than `lower`:

scala> val load2: Power = Kilowatts(5000)
load2: squants.energy.Power = 5000.0 kW
```scala
import squants.space.LengthConversions._
// import squants.space.LengthConversions._

scala> val range: QuantityRange[Power] = QuantityRange(load1, load2)
range: squants.QuantityRange[squants.energy.Power] = QuantityRange(1000.0 kW,5000.0 kW)
// 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)
```

Use multiplication and division to create a Seq of ranges from the original
This will fail because `lower` = `upper`:

```scala
// Create a Seq of 10 sequential ranges starting with the original and each the same size as the original
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)
... 1023 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
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:
```scala
val rs1 = range * 10
// Create a Seq of 10 sequential ranges each 1/10th of the original size
// 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:

```scala
val rs2 = range / 10
// Create a Seq of 10 sequential ranges each with a size of 400 kilowatts
// 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:
```scala
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))
```
Apply foreach, map and foldLeft/foldRight directly to QuantityRanges with a divisor

### 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:
```scala
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:
```scala
// foreach over each of the 400 kilometer ranges within the range
range.foreach(Kilometers(400)) {r => ???}
// map over each of 10 even parts of the range
range.map(10) {r => ???}
// fold over each 10 even parts of the range
range.foldLeft(10)(0) {(z, r) => ???}
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:
```scala
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
Expand Down Expand Up @@ -902,7 +966,7 @@ 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@b117142
// siLengths: squants.experimental.unitgroups.UnitGroup[squants.space.Length] = squants.experimental.unitgroups.si.strict.package$implicits$$anon$1@2f2cbffe
```

To print out units and their conversion factors to the primary SI unit, you could use this code:
Expand Down Expand Up @@ -959,16 +1023,16 @@ 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@1e7ccc51
// usCookingUnitGroup: squants.experimental.unitgroups.UnitGroup[squants.space.Volume]{val units: Set[squants.UnitOfMeasure[squants.space.Volume]]} = $anon$1@32316f15

// squants automatically sorts units
usCookingUnitGroup.sortedUnits.foreach(println)
// squants.space.Teaspoons$@71cbffb5
// squants.space.Tablespoons$@49c093cd
// squants.space.FluidOunces$@30e4abaf
// squants.space.UsPints$@a351ff9
// squants.space.UsQuarts$@6ba887db
// squants.space.UsGallons$@4f629ea7
// squants.space.Teaspoons$@4a6eacba
// squants.space.Tablespoons$@4e8b0223
// squants.space.FluidOunces$@d850963
// squants.space.UsPints$@2fa362d8
// squants.space.UsQuarts$@7a0a1c88
// squants.space.UsGallons$@66e22c8
```

The `UnitGroup` values provided with Squants are only samples and aren't intended to be exhaustive.
Expand Down Expand Up @@ -1004,7 +1068,7 @@ import squants.experimental.unitgroups.misc.AstronomicalLengthUnitGroup
Then create the formatter by passing in a unit group:
```scala
val astroFormatter = new DefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@e19717a
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@7938222b
```

Now, we create some values using human-unfriendly numbers:
Expand Down Expand Up @@ -1048,14 +1112,15 @@ we're adding support for `Length`.
```scala
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
```

```scala
implicit val astroFormatter = new DefaultFormatter(AstronomicalLengthUnitGroup)
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@4360fa36
// astroFormatter: squants.experimental.formatter.DefaultFormatter[squants.space.Length] = squants.experimental.formatter.DefaultFormatter@67c05762

val earthToJupiter = 588000000.km
// earthToJupiter: squants.space.Length = 588000000.0 km
Expand All @@ -1080,9 +1145,9 @@ This example won't compile because there is no `Formatter[Mass]` in implicit sco

```scala
scala> 5000.grams.inBestUnit
<console>:23: error: value grams is not a member of Int
<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
Expand Down
12 changes: 8 additions & 4 deletions shared/src/main/scala/squants/QuantityRange.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import scala.annotation.tailrec
* @tparam A the Quantity Type
*/
case class QuantityRange[A <: Quantity[A]](lower: A, upper: A) {
if (lower >= upper) throw new IllegalArgumentException("QuantityRange upper bound must be greater than or equal to the lower bound")
if (lower >= upper) {
throw new IllegalArgumentException("QuantityRange upper bound must be strictly greater than to the lower bound")
}

/**
* Create a Seq of `multiple` ranges equal in size to the original with sequential range values
Expand Down Expand Up @@ -254,7 +256,8 @@ case class QuantityRange[A <: Quantity[A]](lower: A, upper: A) {
def +-(that: A) = incFromDecTo(that)

/**
* Returns true if the quantity is contained within this range, otherwise false
* Returns true if the quantity is contained within this range, otherwise false.
* This check is *exclusive* of the range's upper limit.
* @param q Quantity
* @return
*/
Expand All @@ -279,11 +282,12 @@ case class QuantityRange[A <: Quantity[A]](lower: A, upper: A) {
def partiallyContains(range: QuantityRange[A]) = range.lower < upper && range.upper > lower

/**
* Returns true if `that` quantity is included within `this` range
* Returns true if `that` quantity is included within `this` range.
* This check is *inclusive* of the range's upper limit.
* @param q Quantity
* @return
*/
def includes(q: A) = q >= lower && q <= upper
def includes(q: A): Boolean = q >= lower && q <= upper

/**
* Returns true if `that` range is completely included in `this` range, otherwise false
Expand Down
83 changes: 66 additions & 17 deletions shared/src/main/tut/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,36 +474,84 @@ val northAmericanSales: Money = (CAD(275) + USD(350) + MXN(290)) in USD
```

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

```tut:reset
```tut:reset:silent
import squants.QuantityRange
import squants.energy.{Kilowatts, Power}
import squants.energy.{Kilowatts, Megawatts, Power}
```
```tut:book
val load1: Power = Kilowatts(1000)
val load2: Power = Kilowatts(5000)
val range: QuantityRange[Power] = QuantityRange(load1, load2)
```

Use multiplication and division to create a Seq of ranges from the original
### Inclusivity and Exclusivitiy

```tut:silent
// Create a Seq of 10 sequential ranges starting with the original and each the same size as the original
The `QuantityRange` constructor requires that `upper` is strictly greater than `lower`:

```tut:book
import squants.space.LengthConversions._
// this will work b/c upper > lower
QuantityRange(1.km, 5.km)
```

This will fail because `lower` = `upper`:

```tut:fail
QuantityRange(1.km, 1.km)
```

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

```tut
val distances = QuantityRange(1.km, 5.km)
distances.contains(5.km) // this is false b/c contains() doesn't include the upper range
distances.includes(5.km) // this is true b/c includes() does include the upper range
```

### 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:
```tut:book
val rs1 = range * 10
// Create a Seq of 10 sequential ranges each 1/10th of the original size
```
Create a Seq of 10 sequential ranges each 1/10th of the original size:

```tut:book
val rs2 = range / 10
// Create a Seq of 10 sequential ranges each with a size of 400 kilowatts
```

Create a Seq of 10 sequential ranges each with a size of 400 kilowatts:
```tut:book
val rs3 = range / Kilowatts(400)
```
Apply foreach, map and foldLeft/foldRight directly to QuantityRanges with a divisor

```tut:silent:fail
// foreach over each of the 400 kilometer ranges within the range
range.foreach(Kilometers(400)) {r => ???}
// map over each of 10 even parts of the range
range.map(10) {r => ???}
// fold over each 10 even parts of the range
range.foldLeft(10)(0) {(z, r) => ???}
### 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:
```tut:book
range.foreach(Megawatts(1)) { r => println(s"lower = ${r.lower}, upper = ${r.upper}") }
```

Subdivide range into 10 slices and map over each slice:
```tut:book
range.map(10) { r => r.upper }
```

Subdivide range into 10 slices and fold over them, using 0 Megawatts as a starting value:
```tut:book
range.foldLeft(10, Megawatts(0)) { (z, r) => z + r.upper }
```

NOTE - Because these implementations of foreach, map and fold* take a parameter (the divisor), these methods
Expand Down Expand Up @@ -771,6 +819,7 @@ we're adding support for `Length`.
```tut:reset:silent
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
Expand Down
6 changes: 5 additions & 1 deletion shared/src/test/scala/squants/QuantityRangeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ class QuantityRangeSpec extends FlatSpec with Matchers {

behavior of "QuantityRange"

it should "throw an IllegalArgumentException when the lower bound >= upper bound" in {
it should "throw an IllegalArgumentException when the lower bound > upper bound" in {
an[IllegalArgumentException] should be thrownBy QuantityRange(Meters(2), Meters(1))
}

it should "throw an IllegalArgumentException when the lower bound = upper bound" in {
an[IllegalArgumentException] should be thrownBy QuantityRange(Meters(2), Meters(2))
}

it should "create a range with lower and upper bound" in {
val r1 = QuantityRange(Meters(1), Meters(2))
r1.lower should be(Meters(1))
Expand Down

0 comments on commit 853b609

Please sign in to comment.