diff --git a/shared/src/main/scala/squants/thermal/Temperature.scala b/shared/src/main/scala/squants/thermal/Temperature.scala index f4825f5f..62b3b3db 100644 --- a/shared/src/main/scala/squants/thermal/Temperature.scala +++ b/shared/src/main/scala/squants/thermal/Temperature.scala @@ -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. @@ -84,6 +87,7 @@ 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)) @@ -91,6 +95,13 @@ final class Temperature private (val value: Double, val unit: TemperatureScale) 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)) @@ -98,6 +109,12 @@ final class Temperature private (val value: Double, val unit: TemperatureScale) 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) @@ -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)) @@ -135,6 +152,9 @@ 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)) } } @@ -142,7 +162,7 @@ object Temperature extends Dimension[Temperature] with BaseDimension { def name = "Temperature" def primaryUnit = Kelvin def siUnit = Kelvin - def units = Set(Kelvin, Fahrenheit, Celsius) + def units = Set(Kelvin, Fahrenheit, Celsius, Rankine) def dimensionSymbol = "Θ" } @@ -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. @@ -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. @@ -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) @@ -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) { diff --git a/shared/src/test/scala/squants/thermal/TemperatureSpec.scala b/shared/src/test/scala/squants/thermal/TemperatureSpec.scala index 528734b4..16c5ff84 100644 --- a/shared/src/test/scala/squants/thermal/TemperatureSpec.scala +++ b/shared/src/test/scala/squants/thermal/TemperatureSpec.scala @@ -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")) } @@ -47,25 +52,31 @@ 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 { @@ -73,6 +84,7 @@ class TemperatureSpec extends FlatSpec with Matchers { 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) @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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")) } diff --git a/temps.sc b/temps.sc new file mode 100644 index 00000000..e69de29b