Skip to content

Commit

Permalink
Merge pull request #161 from typelevel/rankine
Browse files Browse the repository at this point in the history
add rankine temperature scale
  • Loading branch information
cquiroz authored Dec 27, 2016
2 parents 7a948c5 + 5904f54 commit 6cf46f2
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 3 deletions.
51 changes: 48 additions & 3 deletions shared/src/main/scala/squants/thermal/Temperature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ import scala.util.{ Failure, Success, Try }
* Of course, these scales set their respective zero values well above absolute zero.
* This is done to provide a granular and reasonably sized ranges of values for dealing with everyday temperatures.
*
* This library supports another absolute scale, the Rankine scale. Rankine sets its zero at absolute zero,
* but degrees are measure in Fahrenheit (as opposed to Celsius, as the Kelvin scale uses).
*
* In consideration of these more unique scale conversions, two conversion types are supported: Degrees and Scale.
*
* Scale based conversions DO adjust for the zero offset.
* Thus 5 degrees C is the same as 41 degrees F on the thermometer.
*
* Degrees based conversions DO NOT adjust for the zero point.
* Thus 5 degrees C|K is the same amount of temperature as 9 degrees F.
* Thus 5 degrees C|K is the same amount of temperature as 9 degrees F|R.
*
* When creating a temperature it is not important to consider these differences.
* It is also irrelevant when performing operation on temperatures in the same scale.
Expand Down Expand Up @@ -84,20 +87,34 @@ final class Temperature private (val value: Double, val unit: TemperatureScale)
case (Fahrenheit, Fahrenheit, _) this
case (Celsius, Celsius, _) this
case (Kelvin, Kelvin, _) this
case (Rankine, Rankine, _) this

case (Fahrenheit, Celsius, true) Celsius(TemperatureConversions.fahrenheitToCelsiusScale(value))
case (Celsius, Fahrenheit, true) Fahrenheit(TemperatureConversions.celsiusToFahrenheitScale(value))
case (Celsius, Kelvin, true) Kelvin(TemperatureConversions.celsiusToKelvinScale(value))
case (Kelvin, Celsius, true) Celsius(TemperatureConversions.kelvinToCelsiusScale(value))
case (Fahrenheit, Kelvin, true) Kelvin(TemperatureConversions.fahrenheitToKelvinScale(value))
case (Kelvin, Fahrenheit, true) Fahrenheit(TemperatureConversions.kelvinToFahrenheitScale(value))
case (Fahrenheit, Rankine, true) Rankine(TemperatureConversions.fahrenheitToRankineScale(value))
case (Rankine, Fahrenheit, true) Fahrenheit(TemperatureConversions.rankineToFahrenheitScale(value))
case (Celsius, Rankine, true) Rankine(TemperatureConversions.celsiusToRankineScale(value))
case (Rankine, Celsius, true) Celsius(TemperatureConversions.rankineToCelsiusScale(value))
case (Kelvin, Rankine, true) Rankine(TemperatureConversions.kelvinToRankineScale(value))
case (Rankine, Kelvin, true) Kelvin(TemperatureConversions.rankineToKelvinScale(value))


case (Fahrenheit, Celsius, false) Celsius(TemperatureConversions.fahrenheitToCelsiusDegrees(value))
case (Celsius, Fahrenheit, false) Fahrenheit(TemperatureConversions.celsiusToFahrenheitDegrees(value))
case (Celsius, Kelvin, false) Kelvin(TemperatureConversions.celsiusToKelvinDegrees(value))
case (Kelvin, Celsius, false) Celsius(TemperatureConversions.kelvinToCelsiusDegrees(value))
case (Fahrenheit, Kelvin, false) Kelvin(TemperatureConversions.fahrenheitToKelvinDegrees(value))
case (Kelvin, Fahrenheit, false) Fahrenheit(TemperatureConversions.kelvinToFahrenheitDegrees(value))
case (Fahrenheit, Rankine, false) Rankine(TemperatureConversions.fahrenheitToRankineDegrees(value))
case (Rankine, Fahrenheit, false) Fahrenheit(TemperatureConversions.rankineToFahrenheitDegrees(value))
case (Celsius, Rankine, false) Rankine(TemperatureConversions.celsiusToRankineDegrees(value))
case (Rankine, Celsius, false) Celsius(TemperatureConversions.rankineToCelsiusDegrees(value))
case (Kelvin, Rankine, false) Rankine(TemperatureConversions.kelvinToRankineDegrees(value))
case (Rankine, Kelvin, false) Kelvin(TemperatureConversions.rankineToKelvinDegrees(value))
}

def in(unit: TemperatureScale) = convert(unit, withOffset = true)
Expand All @@ -124,7 +141,7 @@ object Temperature extends Dimension[Temperature] with BaseDimension {
def apply[A](n: A, scale: TemperatureScale)(implicit num: Numeric[A]) = new Temperature(num.toDouble(n), scale)

def apply(s: String): Try[Temperature] = {
val regex = "([-+]?[0-9]*\\.?[0-9]+)[ °]*(f|F|c|C|k|K)".r
val regex = "([-+]?[0-9]*\\.?[0-9]+)[ °]*(f|F|c|C|k|K|r|R)".r
s match {
case regex(value, Fahrenheit.symbol) Success(Fahrenheit(value.toDouble))
case regex(value, "f") Success(Fahrenheit(value.toDouble))
Expand All @@ -135,14 +152,17 @@ object Temperature extends Dimension[Temperature] with BaseDimension {
case regex(value, Kelvin.symbol) Success(Kelvin(value.toDouble))
case regex(value, "k") Success(Kelvin(value.toDouble))
case regex(value, "K") Success(Kelvin(value.toDouble))
case regex(value, Rankine.symbol) Success(Rankine(value.toDouble))
case regex(value, "r") Success(Rankine(value.toDouble))
case regex(value, "R") Success(Rankine(value.toDouble))
case _ Failure(QuantityParseException("Unable to parse Temperature", s))
}
}

def name = "Temperature"
def primaryUnit = Kelvin
def siUnit = Kelvin
def units = Set(Kelvin, Fahrenheit, Celsius)
def units = Set(Kelvin, Fahrenheit, Celsius, Rankine)
def dimensionSymbol = "Θ"
}

Expand Down Expand Up @@ -176,10 +196,19 @@ object Kelvin extends TemperatureScale with PrimaryUnit with SiBaseUnit {
def apply(temperature: Temperature): Temperature = temperature.inKelvin
}

object Rankine extends TemperatureScale {
val symbol = "°R"
val self = this
protected def converterFrom = TemperatureConversions.rankineToKelvinScale
protected def converterTo = TemperatureConversions.kelvinToRankineScale
def apply(temperature: Temperature): Temperature = temperature.in(Rankine)
}

object TemperatureConversions {
lazy val kelvin = Kelvin(1)
lazy val fahrenheit = Fahrenheit(1)
lazy val celsius = Celsius(1)
lazy val rankine = Rankine(1)

/*
* Degree conversions are used to convert a quantity of degrees from one scale to another.
Expand All @@ -192,6 +221,13 @@ object TemperatureConversions {
def kelvinToCelsiusDegrees(kelvin: Double) = kelvin
def fahrenheitToKelvinDegrees(fahrenheit: Double) = fahrenheit * 5d / 9d
def kelvinToFahrenheitDegrees(kelvin: Double) = kelvin * 9d / 5d
def celsiusToRankineDegrees(celsius: Double) = celsius * 9d / 5d
def rankineToCelsiusDegrees(rankine: Double) = rankine * 5d / 9d
def fahrenheitToRankineDegrees(fahrenheit: Double) = fahrenheit
def rankineToFahrenheitDegrees(rankine: Double) = rankine
def kelvinToRankineDegrees(kelvin: Double) = kelvin * 9d / 5d
def rankineToKelvinDegrees(rankine: Double) = rankine * 5d / 9d


/*
* Scale conversions are used to convert a "thermometer" temperature from one scale to another.
Expand All @@ -204,6 +240,12 @@ object TemperatureConversions {
def kelvinToCelsiusScale(kelvin: Double) = kelvin - 273.15
def fahrenheitToKelvinScale(fahrenheit: Double) = (fahrenheit + 459.67) * 5d / 9d
def kelvinToFahrenheitScale(kelvin: Double) = kelvin * 9d / 5d - 459.67
def celsiusToRankineScale(celsius: Double) = (celsius + 273.15) * 9d / 5d
def rankineToCelsiusScale(rankine: Double) = (rankine - 491.67) * 5d / 9d
def fahrenheitToRankineScale(fahrenheit: Double) = fahrenheit + 459.67
def rankineToFahrenheitScale(rankine: Double) = rankine - 459.67
def kelvinToRankineScale(kelvin: Double) = kelvin * 9d / 5d
def rankineToKelvinScale(rankine: Double) = rankine * 5d / 9d

implicit class TemperatureConversions[A](n: A)(implicit num: Numeric[A]) {
def C = Celsius(n)
Expand All @@ -216,6 +258,9 @@ object TemperatureConversions {
def K = Kelvin(n)
def kelvin = Kelvin(n)
def degreesKelvin = Kelvin(n)
def R = Rankine(n)
def rankine = Rankine(n)
def degreesRankine = Rankine(n)
}

implicit class TemperatureStringConversion(s: String) {
Expand Down
65 changes: 65 additions & 0 deletions shared/src/test/scala/squants/thermal/TemperatureSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ class TemperatureSpec extends FlatSpec with Matchers {
Kelvin(1).toKelvinScale should be(1)
Fahrenheit(1).toFahrenheitScale should be(1)
Celsius(1).toCelsiusScale should be(1)
Rankine(1).toScale(Rankine) should be(1)

Kelvin(1).toKelvinDegrees should be(1)
Fahrenheit(1).toFahrenheitDegrees should be(1)
Celsius(1).toCelsiusDegrees should be(1)
Rankine(1).toDegrees(Rankine) should be(1)
}

they should "create values from properly formatted Strings" in {
Temperature("10.22°F").get should be(Fahrenheit(10.22))
Temperature("10.22°K").get should be(Kelvin(10.22))
Temperature("10.22°C").get should be(Celsius(10.22))
Temperature("10.22°R").get should be(Rankine(10.22))
Temperature("10.22 F").get should be(Fahrenheit(10.22))
Temperature("10.22 K").get should be(Kelvin(10.22))
Temperature("10.22 C").get should be(Celsius(10.22))
Temperature("10.22 R").get should be(Rankine(10.22))
Temperature("10.22 Z").failed.get should be(QuantityParseException("Unable to parse Temperature", "10.22 Z"))
Temperature("ZZ F").failed.get should be(QuantityParseException("Unable to parse Temperature", "ZZ F"))
}
Expand All @@ -47,32 +52,39 @@ class TemperatureSpec extends FlatSpec with Matchers {
x.toKelvinScale should be(0)
x.toFahrenheitScale should be(-459.67)
x.toCelsiusScale should be(-273.15)
x.toScale(Rankine) should be(0)
x.to(Kelvin) should be(0)
x.to(Fahrenheit) should be(-459.67)
x.to(Celsius) should be(-273.15)
x.to(Rankine) should be(0)

val y = Fahrenheit(32)
y.toKelvinScale should be(273.15)
y.toFahrenheitScale should be(32)
y.toCelsiusScale should be(0)
y.toScale(Rankine) should be(491.67)
y.to(Kelvin) should be(273.15)
y.to(Fahrenheit) should be(32)
y.to(Celsius) should be(0)
y.to(Rankine) should be(491.67)

val z = Celsius(100)
z.toKelvinScale should be(373.15)
z.toFahrenheitScale should be(212)
z.toCelsiusScale should be(100)
z.toScale(Rankine) should be(671.67)
z.to(Kelvin) should be(373.15)
z.to(Fahrenheit) should be(212)
z.to(Celsius) should be(100)
z.to(Rankine) should be(671.67)
}

they should "properly convert to all supported Units of Measure (Degrees)" in {
val x = Kelvin(5)
x.toKelvinDegrees should be(5)
x.toFahrenheitDegrees should be(9)
x.toCelsiusDegrees should be(5)
x.toDegrees(Rankine) should be(9)

val y = Fahrenheit(9)
y.toKelvinDegrees should be(5)
Expand All @@ -82,6 +94,7 @@ class TemperatureSpec extends FlatSpec with Matchers {
y.toDegrees(Kelvin) should be(5)
y.toDegrees(Fahrenheit) should be(9)
y.toDegrees(Celsius) should be(5)
y.toDegrees(Rankine) should be(9)
}

they should "properly rebox Kelvin to all supported Units of Measure (Scale)" in {
Expand All @@ -90,6 +103,7 @@ class TemperatureSpec extends FlatSpec with Matchers {
x.inCelsius should be(Celsius(-273.15))
x.in(Fahrenheit) should be(Fahrenheit(-459.67))
x.in(Celsius) should be(Celsius(-273.15))
x.in(Rankine) should be(Rankine(0))
}

they should "properly rebox Fahrenheit to all supported Units of Measure (Scale)" in {
Expand All @@ -98,6 +112,7 @@ class TemperatureSpec extends FlatSpec with Matchers {
x.inCelsius should be(Celsius((5d / 9) * -32))
(x.in(Kelvin) - Kelvin((5d / 9) * 459.67)).value < 0.000000000001 should be(right = true)
x.in(Celsius) should be(Celsius((5d / 9) * -32))
x.in(Rankine) should be(Rankine(459.67))
}

they should "properly rebox Celsius to all supported Units of Measure (Scale)" in {
Expand All @@ -106,52 +121,63 @@ class TemperatureSpec extends FlatSpec with Matchers {
x.inFahrenheit should be(Fahrenheit(32))
x.in(Kelvin) should be(Kelvin(273.15))
x.in(Fahrenheit) should be(Fahrenheit(32))
x.in(Rankine).value should be(491.67 +- 0.000000000001)
}

they should "properly plus Temperatures in like scale (Scale + Degrees)" in {
Kelvin(10) + Kelvin(20) should be(Kelvin(30))
Fahrenheit(10) + Fahrenheit(20) should be(Fahrenheit(30))
Celsius(10) + Celsius(20) should be(Celsius(30))
Rankine(10) + Rankine(20) should be(Rankine(30))
}

they should "properly minus Temperatures in like scale (Scale + Degrees)" in {
Kelvin(30) - Kelvin(20) should be(Kelvin(10))
Fahrenheit(30) - Fahrenheit(20) should be(Fahrenheit(10))
Celsius(30) - Celsius(20) should be(Celsius(10))
Rankine(30) - Rankine(20) should be(Rankine(10))
}

they should "properly plus Temperatures in different scales (Scale + Degrees)" in {
Kelvin(10) + Celsius(20) should be(Kelvin(30))
Kelvin(10) + Fahrenheit(18) should be(Kelvin(20))
Kelvin(10) + Rankine(18) should be(Kelvin(20))

Fahrenheit(10) + Kelvin(20) should be(Fahrenheit(46))
Fahrenheit(10) + Celsius(20) should be(Fahrenheit(46))
Fahrenheit(10) + Rankine(20) should be(Fahrenheit(30))

Celsius(10) + Kelvin(20) should be(Celsius(30))
Celsius(10) + Fahrenheit(36) should be(Celsius(30))
Celsius(10) + Rankine(36) should be(Celsius(30))
}

they should "properly minus Temperatures in different scales (Scale + Degrees)" in {
Kelvin(50) - Celsius(20) should be(Kelvin(30))
Kelvin(30) - Fahrenheit(18) should be(Kelvin(20))
Kelvin(30) - Rankine(18) should be(Kelvin(20))

Fahrenheit(100) - Kelvin(20) should be(Fahrenheit(64))
Fahrenheit(100) - Celsius(20) should be(Fahrenheit(64))
Fahrenheit(100) - Rankine(20) should be(Fahrenheit(80))

Celsius(50) - Kelvin(20) should be(Celsius(30))
Celsius(50) - Fahrenheit(36) should be(Celsius(30))
Celsius(50) - Rankine(36) should be(Celsius(30))
}

they should "properly times Double (Degrees)" in {
Kelvin(10) * 5 should be(Kelvin(50))
Fahrenheit(10) * 5 should be(Fahrenheit(50))
Celsius(10) * 5 should be(Celsius(50))
Rankine(10) * 5 should be(Rankine(50))
}

they should "properly divide Double (Degrees)" in {
Kelvin(10) / 5 should be(Kelvin(2))
Fahrenheit(10) / 5 should be(Fahrenheit(2))
Celsius(10) / 5 should be(Celsius(2))
Rankine(10) / 5 should be(Rankine(2))
}

they should "compare Temperatures in the like scales (Scales)" in {
Expand All @@ -175,6 +201,13 @@ class TemperatureSpec extends FlatSpec with Matchers {
Celsius(10) should be(Celsius(10))
Celsius(10) < Celsius(10.1) should be(right = true)
Celsius(10) > Celsius(9.9) should be(right = true)

Rankine(10).compare(Rankine(10)) should be(0)
Rankine(10).compare(Rankine(10.1)) should be(-1)
Rankine(10).compare(Rankine(9.9)) should be(1)
Rankine(10) should be(Rankine(10))
Rankine(10) < Rankine(10.1) should be(right = true)
Rankine(10) > Rankine(9.9) should be(right = true)
}

it should "compare a non-null Quantity to a null and return a proper result" in {
Expand Down Expand Up @@ -212,21 +245,30 @@ class TemperatureSpec extends FlatSpec with Matchers {
Kelvin(10).toString should be("10.0°K")
Fahrenheit(10).toString should be("10.0°F")
Celsius(10).toString should be("10.0°C")
Rankine(10).toString should be("10.0°R")

val k = Kelvin(0)
k.toString(Kelvin) should be("0.0°K")
k.toString(Fahrenheit) should be("-459.67°F")
k.toString(Celsius) should be("-273.15°C")
k.toString(Rankine) should be("0.0°R")

val c = Celsius(0)
c.toString(Kelvin) should be("273.15°K")
c.toString(Fahrenheit) should be("32.0°F")
c.toString(Celsius) should be("0.0°C")
c.toString(Rankine) should be("491.66999999999996°R")

val f = Fahrenheit(32)
f.toString(Kelvin) should be("273.15°K")
f.toString(Fahrenheit) should be("32.0°F")
f.toString(Celsius) should be("0.0°C")
f.toString(Rankine) should be("491.67°R")

val r = Rankine(0)
r.toString(Kelvin) should be("0.0°K")
r.toString(Fahrenheit) should be("-459.67°F")
r.toString(Celsius) should be("-273.15°C")
}

they should "return Energy when multiplied by ThermalCapacity" in {
Expand All @@ -241,6 +283,7 @@ class TemperatureSpec extends FlatSpec with Matchers {
kelvin should be(Kelvin(1))
celsius should be(Celsius(1))
fahrenheit should be(Fahrenheit(1))
rankine should be(Rankine(1))
}

they should "provide proper formulas for conversion between scales" in {
Expand All @@ -252,6 +295,13 @@ class TemperatureSpec extends FlatSpec with Matchers {
fahrenheitToCelsiusScale(32) should be(0)
kelvinToCelsiusScale(0) should be(-273.15)
celsiusToKelvinScale(0) should be(273.15)

kelvinToRankineScale(100) should be(180)
rankineToKelvinScale(180) should be(100)
celsiusToRankineScale(0) should be(491.67 +- 0.001)
rankineToCelsiusScale(491.67) should be(0)
fahrenheitToRankineScale(0) should be(459.67)
rankineToFahrenheitScale(0) should be(-459.67)
}

they should "provide proper formulas for conversion between degrees" in {
Expand All @@ -263,6 +313,13 @@ class TemperatureSpec extends FlatSpec with Matchers {
fahrenheitToCelsiusDegrees(9) should be(5)
kelvinToCelsiusDegrees(1) should be(1)
celsiusToKelvinDegrees(1) should be(1)

kelvinToRankineDegrees(5) should be(9)
rankineToKelvinDegrees(9) should be(5)
celsiusToRankineDegrees(5) should be(9)
rankineToCelsiusDegrees(9) should be(5)
fahrenheitToRankineDegrees(1) should be(1)
rankineToFahrenheitDegrees(1) should be(1)
}

they should "provide implicit conversion from Double" in {
Expand All @@ -272,13 +329,19 @@ class TemperatureSpec extends FlatSpec with Matchers {
d.C should be(Celsius(d))
d.celsius should be(Celsius(d))
d.degreesCelsius should be(Celsius(d))

d.F should be(Fahrenheit(d))
d.Fah should be(Fahrenheit(d))
d.fahrenheit should be(Fahrenheit(d))
d.degreesFahrenheit should be(Fahrenheit(d))

d.K should be(Kelvin(d))
d.kelvin should be(Kelvin(d))
d.degreesKelvin should be(Kelvin(d))

d.R should be(Rankine(d))
d.rankine should be(Rankine(d))
d.degreesRankine should be(Rankine(d))
}

they should "provide implicit conversions from String" in {
Expand All @@ -290,6 +353,8 @@ class TemperatureSpec extends FlatSpec with Matchers {
"10.22 F".toTemperature.get should be(Fahrenheit(10.22))
"10.22 K".toTemperature.get should be(Kelvin(10.22))
"10.22 C".toTemperature.get should be(Celsius(10.22))
"10.22°R".toTemperature.get should be(Rankine(10.22))
"10.22 R".toTemperature.get should be(Rankine(10.22))
"10.22 Z".toTemperature.failed.get should be(QuantityParseException("Unable to parse Temperature", "10.22 Z"))
"ZZ F".toTemperature.failed.get should be(QuantityParseException("Unable to parse Temperature", "ZZ F"))
}
Expand Down
Empty file added temps.sc
Empty file.

0 comments on commit 6cf46f2

Please sign in to comment.