Enum Unit Conversions of a Third Kind

This post is a response to a nicely articulated recent comment from grelf.net regarding my post Using Java Enums for Units Conversions. I mostly do not disagree with grelf.net's points and the small number of disagreements are minor (more a matter of taste/opinion than of anything else). Indeed, most of grelf.net's comments are difficult to argue with coming from his focus on accurately modeling a class that holds temperature characteristics. My focus in that post and in its other follow up post, Revisiting Enum Unit Conversions, is on using enums for conversions rather than on modeling temperatures correctly or even on providing a class to store a temperature value.

Like grelf.net, I too had read Cay Horstmann's blog post Whatever Floats Your Boat after seeing it featured in one of my almost-daily scanning of "Java.net today" headlines (that post is still featured on the main page as of this writing by the way). However, my interpretation of Horstmann's point was slightly different than grelf.net's. While grelf.net felt that Horstmann was making the point that "there is usually little point in using anything but double," I felt that Horstmann's point was a little less sweeping than that.

In my opinion, Horstmann was really stating that there's little reason to use float rather than double. I couldn't agree more. It is easy for me to agree with using double rather than float because I cannot even recall the last time I used float other than when an API I was calling required it. For an introductory class learning coding and Java, double is far less awkward and easier to use than BigDecimal, so I can see double being appropriate in Horstmann's case. Horstmann even states, "Clearly, BigDecimal would be the right choice ... reviewers were adamant not to use objects and method calls too early. So, BigDecimal wasn't going to fly." It is not surprising that it is more important to make the code easy to follow than to make it mathematically precise for beginning software developers, but practicing Java developers are certainly capable of learning and using BigDecimal when its precision is warranted.

Hortsmann does point out what most of us do know about using BigDecimal: "they are awful to use in Java." He briefly "digresses" to show Scala's convenient treatment treatment and I've certainly been spoiled by Groovy's "transparent" use of operator overloading with GDK-provided BigDecimal to allow normal mathematical operation symbols to be used to manipulate BigDecimal values.

Because of the "awkwardness" of Java's BigDecimal, I had thought about supplying an alternative API to my enum's conversion functions. As I stated in my blog post Caution: Double to BigDecimal in Java, I was aware of issues with naively using the BigDecimal constructor that accepts a double. I had thought about a String-based API because Strings work well with the BigDecimal constructor, but it seemed odd to make client code need to use Strings for working with temperature conversions. Out of laziness and a hurry to get the post out, I simply left the API with BigDecimal for the time being, but thought about changing it to use double in conjunction with BigDecimal.valueOf(double) and BigDecimal.doubleValue() (though the latter suffers potential BigDecimal-to-double loss of precision issues).

Providing a double-based (emphasis on the primitive) API has an added advantage of removing the need to check for null on passed-in objects. The primitive cannot be null, but it would still be possible for an NullPointerException to occur within the client's code if the client naively tried to pass a null to one of these methods and implicit unboxing was attempted. It is easiest to see the advantage of a double API when looking at client code. The following is a basic Java "test driver" of the TemperatureUnit3 enum.

Main3.java - Client 'Testing' TemperatureUnit3.java

package dustin.examples;

import java.math.BigDecimal;
import static java.lang.System.out;

/**
 * <p>This is a simple class that demonstrates use of the TemperatureUnit with
 * special focus on demonstrating that enum's conversions between temperature
 * scales (see <a href="#warning">warning</a>).</p>
 *
 * <p><span style="font-weight: bold; color: red;">WARNING:</span>
 * <a name="warning">This class has</a>
 * <span style="font-weight: bold; color: red;">NOT</span> been adequately
 * tested and some conversions are likely to not be properly coded. This
 * example is intended for demonstrative purposes only.</p>
 */
public class Main3
{
   public static void main(final String[] arguments)
   {
      final TemperatureUnit3 celsius = TemperatureUnit3.CELSIUS;
      out.println("Zero Celsius = " + celsius.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");
      out.println("100 Celsisus = " + celsius.convertToFahrenheit(new BigDecimal("100")) + " Fahrenheit");

      final TemperatureUnit3 kelvin = TemperatureUnit3.KELVIN;
      out.println("Zero Kelvin = " + kelvin.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");
      out.println("100 Kelvin = " + kelvin.convertToFahrenheit(new BigDecimal("100")) + " Fahrenheit");
      out.println("273.15 Kelvin = " + kelvin.convertToFahrenheit(new BigDecimal("273.15")) + " Fahrenheit");
      out.println("373.15 Kelvin = " + kelvin.convertToFahrenheit(new BigDecimal("373.15")) + " Fahrenheit");

      final TemperatureUnit3 rankine = TemperatureUnit3.RANKINE;
      out.println("671.641 Rankine = " + rankine.convertToFahrenheit(new BigDecimal("671.641")) + " Fahrenheit");

      out.println(TemperatureUnit3.FAHRENHEIT.getNamedFor());
      out.println("Units: " + TemperatureUnit3.FAHRENHEIT.getUnits());
      out.println("0 degrees Fahrenheit = " + TemperatureUnit3.FAHRENHEIT.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");
      out.println("100 degrees Celsius = " + TemperatureUnit3.CELSIUS.convertToFahrenheit(new BigDecimal("100")) + " Fahrenheit");
      out.println("0 degrees Celsius = " + TemperatureUnit3.CELSIUS.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");

      out.println("Celsius named for " + TemperatureUnit3.CELSIUS.getNamedFor());
      out.println("Units: " + TemperatureUnit3.CELSIUS.getUnits());
      out.println("0 degrees Celsius = " + TemperatureUnit3.CELSIUS.convertToCelsius(new BigDecimal("0")) + " Celsius");
      out.println("212 degrees Farenheit = " + TemperatureUnit3.FAHRENHEIT.convertToCelsius(new BigDecimal("212")).toPlainString() + " Celsius");
      out.println("273 Kelvin: " + TemperatureUnit3.KELVIN.convertToCelsius(new BigDecimal("273")) + " Celsisus");
      out.println("500 R: " + TemperatureUnit3.RANKINE.convertToCelsius(new BigDecimal("500")) + " Celsius");

      out.println("Kelvin named for " + TemperatureUnit3.KELVIN.getNamedFor());
      out.println("Units: " + TemperatureUnit3.KELVIN.getUnits());
      out.println("0 Kelvin = " + TemperatureUnit3.KELVIN.convertToCelsius(new BigDecimal("0")) + " Celsius");
      out.println("0 Kelvin = " + TemperatureUnit3.KELVIN.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");
      out.println("0 Kelvin = " + TemperatureUnit3.KELVIN.convertToRankine(new BigDecimal("0")) + " Rankine");
      out.println("100 Kelvin = " + TemperatureUnit3.KELVIN.convertToRankine(new BigDecimal("100")) + " Rankine");

      out.println("Rankine named for " + TemperatureUnit3.RANKINE.getNamedFor());
      out.println("Units: " + TemperatureUnit3.RANKINE.getUnits());
      out.println("0 Rankine = " + TemperatureUnit3.RANKINE.convertToCelsius(new BigDecimal("0")) + " Celsius");
      out.println("0 Rankine = " + TemperatureUnit3.RANKINE.convertToFahrenheit(new BigDecimal("0")) + " Fahrenheit");
      out.println("0 Rankine = " + TemperatureUnit3.RANKINE.convertToKelvin(new BigDecimal("0")) + " Kelvin");
      out.println("100 Rankine = " + TemperatureUnit3.RANKINE.convertToKelvin(new BigDecimal("100")) + " Kelvin");
   }
}

In the above client code, the requirement to instantiate a BigDecimal each time is onerous. It's not an issue with a Groovy client where the numerals can be supplied directly as literals and Groovy implicitly treats them as BigDecimal instances. In my revised enum that will be shown later in this post, I employ a double-based API.

The grelf.net design supports conversion and representation of a particular temperature whereas my implementation intentionally only supports conversion functionality. As grelf.net states, "A Temperature would have Units. Construct a Temperature using any chosen unit and read out its value in any chosen unit." The grelf.net implementation is for a temperature with possibility of conversion functionality. My implementation is actually not for a temperature, but rather for temperature conversion. One thing I do change in this post's latest implementation of the enum is its name. I am now calling it TemperatureScale because that is what it really represents with each instance of the enum being a temperature scale (but not an actually measured temperature).

My implementation is designed for situations where the need is not to store a temperature for passing around the system, but simply to convert one available temperature of a given scale to the equivalent temperature on a different scale. A contrived example of this use case is reading temperatures provided by some central service in Celsius scale, but then presenting them on a web page for customers in the United States in Fahrenheit. Similarly, reading a list of average temperatures at which certain scientific events occur and converting them to Celsius or Fahrenheit for public consumption can use the conversion. If one does want to store a particular value in addition to providing the ability to convert between scales, then grelf.net's design seems like a reasonable one.

I am reproducing grelf.net's code, provided in his original comment, because the Blogger comments don't support the nice appearance and color coded syntax that is supported in posts.

Code Slightly Adapted (to Compile) from grelf.net's Comment

package dustin.examples;

/**
 * <p>Class postulated in grelf.net's feedback comment to post "Enum Unit Conversions"
 * (http://marxsoftware.blogspot.com/2011/08/using-java-enums-for-units-conv...).
 * Only minor changes have been made to make the class compile.</p>
 *
 * <p><span style="font-size: 125%; color: red;">WARNING:</span> This class has
 * not been tested or even used by me (Dustin) in a runtime environment. It has
 * only been compiled without error or warning.</p>
 */
public class Temperature
{
   private double degreesK; // Keep values in Kelvin internally

   public enum Units
   {
      CELSIUS ("\u00b0C"), FAHRENHEIT ("\u00b0C"), KELVIN ("\u00b0K"); // Etc

      Units (String symbol) { this.symbol = symbol; } 
      public String getSymbol () { return this.symbol; }
      private String symbol;
   } // Units

   // Conversions to Kelvin:
   private double cToK (double degreesC) { return degreesC + 273.16; }
   private double fToK (double degreesF) { return (degreesF - 32.0) * 5.0 / 9.0 + 273.16; }

   // Conversions from Kelvin:
   private double kToC (double degreesK) { return degreesK - 273.16; }
   private double kToF (double degreesK) { return (degreesK - 273.16) * 9.0 / 5.0 + 32.0; }

   public Temperature (double value, Units units)
   {
      if (null == units) throw new ImpossibleUnitsException ();

      switch (units)
      {
         case CELSIUS: this.degreesK = cToK (value); break;
         case FAHRENHEIT: this.degreesK = fToK (value); break;
         case KELVIN: this.degreesK = value;
      }
   } // Temperature

   /** NB: Unchecked, to avoid awkwardness when instantiating Temperature. */
   public class ImpossibleUnitsException extends RuntimeException {}

   public double getValue (Units units)
   {
      double retValue;
      switch (units)
      {
         case CELSIUS: retValue = kToC (this.degreesK); break;
         case FAHRENHEIT: retValue = kToF (this.degreesK); break;
         default: retValue = this.degreesK;
      }
      return retValue;
   } // getValue

   /** NB: Not the overridden version */
   public String toString (Units units)
   {
      String retValue;
      switch (units)
      {
         case CELSIUS: retValue = kToC (this.degreesK) + Units.CELSIUS.getSymbol (); break;
         case FAHRENHEIT: retValue = kToF (this.degreesK) + Units.FAHRENHEIT.getSymbol (); break;
         default: retValue = this.degreesK + Units.KELVIN.getSymbol ();
      }
      return retValue;
   } // toString

} // Temperature

Returning to my enum and its focus on temperature scales and conversions between scales rather than on representation of a particular measured temperature, I have incorporated changes to it to deal with the awkwardness of BigDecimal and to be more appropriately named. Note that the enum itself still uses BigDecimal, but the awkwardness of it is hidden from clients. This is a general principle I like in software development: encapsulate specific business logic and associated awkwardness within a single construct and isolate clients from as much of these specifics as possible. This approach allows me to retain BigDecimal precision without requiring the client code to be aware that BigDecimal is even used.

TemperatureScale.java (AKA TemperatureUnit4)

Related:
1 2 Page 1
Page 1 of 2