Thứ Tư, 10 tháng 8, 2011

Using Java Enums for Units Conversions

When I was writing code regularly in both C++ and Java in the early days of Java, I often missed Java's (at the time) lack of an enum. Fortunately, when Java finally got its enum, it was worth the wait. Not only does the Java enum provide a typesafe representation of finite values, but it supports methods on those values. Another feature of Java enums that I did not as immediately recognize, but which has really grown on me as I've used SDK-provided enums, is the ability to use Java enums' method support to provide conversions between the enum values. An example of an SDK classes that make use of this feature is the TimeUnit enum (see my blog post on this). Another example is Guava's CaseFormat enum (via its to(CaseFormat,String) method).

I have been using Java enums to represent finite values and to provide basic methods on the enum values for some time. An example of an enum representing temperature scales is shown next that uses these features that I have most commonly used.

TemperatureUnit.java Enum with Representation and Basic Characteristics Methods
package dustin.examples;



import java.math.BigDecimal;

import java.math.RoundingMode;



/**

* <p>Enum representing different temperature scales

* (<span style="font-weight: bold; color: red;">NOT</span> ready for production

* -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 enum TemperatureUnit

{

/** Celsius, used by most of the world's population. */

CELSIUS(new BigDecimal("0"), new BigDecimal("100"), '\u00B0' + "C", "Anders Celsius"),

/** Fahrenheit, commonly used in the United States. */

FAHRENHEIT(new BigDecimal("32"), new BigDecimal("212"), '\u00B0' + "F", "Daniel Gabriel Fahrenheit"),

/** Kelvin, commonly used in scientific endeavors. */

KELVIN(new BigDecimal("273.15"), new BigDecimal("373.15"), "K", "William Thomson, 1st Baron Kelvin"),

/** Rankine, */

RANKINE(new BigDecimal("491.67"), new BigDecimal("671.641"), '\u00B0' + "R", "William John Macquorn Rankine");



/** Freezing point of water for each temperature scale. */

private BigDecimal freezingPoint;



/** Boiling point of water for each temperature scale. */

private BigDecimal boilingPoint;



/** Units by which this temperature scale is expressed. */

private String units;



/** Name of person that this temperature scale is named for. */

private String namedFor;



private static final BigDecimal FIVE = new BigDecimal("5");

private static final BigDecimal NINE = new BigDecimal("9");

private static final BigDecimal THIRTY_TWO = new BigDecimal("32");

private static final BigDecimal KELVIN_CELSIUS_DELTA = new BigDecimal("273");

private static final BigDecimal RANKINE_FAHRENHEIT_DELTA = new BigDecimal("459.67");



/**

* Constructor for TemperatureUnit that accepts key characteristics of each

* temperature scale.

*

* @param newFreezingPoint Freezing point for this temperature scale.

* @param newBoilingPoint Boiling point for this temperature scale.

* @param newUnits Units of measurement for this temperature scale.

* @param newNamedFor Name of person after which temperature scale was named.

*/

TemperatureUnit(

final BigDecimal newFreezingPoint,

final BigDecimal newBoilingPoint,

final String newUnits,

final String newNamedFor)

{

this.freezingPoint = newFreezingPoint;

this.boilingPoint = newBoilingPoint;

this.units = newUnits;

this.namedFor = newNamedFor;

}



/**

* Provide the freezing point of water for this temperature scale.

*

* @return Freezing point of this temperature scale.

*/

public BigDecimal getFreezingPoint()

{

return this.freezingPoint;

}



/**

* Provide the boiling point of water for this temperature scale.

*

* @return Boiling point of this temperature scale.

*/

public BigDecimal getBoilingPoint()

{

return this.boilingPoint;

}



/**

* Unit of measurement for this temperature scale.

*

* @return Unit of measurement for this temperature scale.

*/

public String getUnits()

{

return this.units;

}



/**

* Provide the name of the person for which this temperature scale was named.

*

* @return Name of person for which this temperature scale was named.

*/

public String getNamedFor()

{

return this.namedFor;

}

}


The version of the enum above provides a typesafe representation of the finite set of temperature skills. This already makes this enum superior to those available in many languages. This enum also has characteristics about each temperature scale that are easily accessible via its methods. This makes the Java enum even more powerful. It gets better. The next code listing shows the same enum, but this time with conversion methods that support converting a temperature in one temperature scale to another temperature scale.

WARNING: I have not tested all conversions and some may be (likely are) incorrect. This is meant solely for demonstrative purposes, though most of the values I did try out in very simple testing converted similarly to when I put the same values in an online temperature converter.

TemperatureUnit.java with Temperature Scale Conversion Methods Added
package dustin.examples;



import java.math.BigDecimal;

import java.math.RoundingMode;



/**

* <p>Enum representing different temperature scales

* (<span style="font-weight: bold; color: red;">NOT</span> ready for production

* -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 enum TemperatureUnit

{

/** Celsius, used by most of the world's population. */

CELSIUS(new BigDecimal("0"), new BigDecimal("100"), '\u00B0' + "C", "Anders Celsius"),

/** Fahrenheit, commonly used in the United States. */

FAHRENHEIT(new BigDecimal("32"), new BigDecimal("212"), '\u00B0' + "F", "Daniel Gabriel Fahrenheit"),

/** Kelvin, commonly used in scientific endeavors. */

KELVIN(new BigDecimal("273.15"), new BigDecimal("373.15"), "K", "William Thomson, 1st Baron Kelvin"),

/** Rankine, */

RANKINE(new BigDecimal("491.67"), new BigDecimal("671.641"), '\u00B0' + "R", "William John Macquorn Rankine");



/** Freezing point of water for each temperature scale. */

private BigDecimal freezingPoint;



/** Boiling point of water for each temperature scale. */

private BigDecimal boilingPoint;



/** Units by which this temperature scale is expressed. */

private String units;



/** Name of person that this temperature scale is named for. */

private String namedFor;



private static final BigDecimal FIVE = new BigDecimal("5");

private static final BigDecimal NINE = new BigDecimal("9");

private static final BigDecimal THIRTY_TWO = new BigDecimal("32");

private static final BigDecimal KELVIN_CELSIUS_DELTA = new BigDecimal("273");

private static final BigDecimal RANKINE_FAHRENHEIT_DELTA = new BigDecimal("459.67");



/**

* Constructor for TemperatureUnit that accepts key characteristics of each

* temperature scale.

*

* @param newFreezingPoint Freezing point for this temperature scale.

* @param newBoilingPoint Boiling point for this temperature scale.

* @param newUnits Units of measurement for this temperature scale.

* @param newNamedFor Name of person after which temperature scale was named.

*/

TemperatureUnit(

final BigDecimal newFreezingPoint,

final BigDecimal newBoilingPoint,

final String newUnits,

final String newNamedFor)

{

this.freezingPoint = newFreezingPoint;

this.boilingPoint = newBoilingPoint;

this.units = newUnits;

this.namedFor = newNamedFor;

}



/**

* Provide the freezing point of water for this temperature scale.

*

* @return Freezing point of this temperature scale.

*/

public BigDecimal getFreezingPoint()

{

return this.freezingPoint;

}



/**

* Provide the boiling point of water for this temperature scale.

*

* @return Boiling point of this temperature scale.

*/

public BigDecimal getBoilingPoint()

{

return this.boilingPoint;

}



/**

* Unit of measurement for this temperature scale.

*

* @return Unit of measurement for this temperature scale.

*/

public String getUnits()

{

return this.units;

}



/**

* Provide the name of the person for which this temperature scale was named.

*

* @return Name of person for which this temperature scale was named.

*/

public String getNamedFor()

{

return this.namedFor;

}



/**

* Convert provided measure in this temperature scale to equivalent measure

* in Fahrenheit scale.

*

* @param sourceMeasure Numeric measurement in source scale.

* @return Numeric measurement equivalent in Fahrenheit scale; null if the

* provided source scale is not supported.

*/

public BigDecimal toFahrenheit(final BigDecimal sourceMeasure)

{

BigDecimal fahrenheit = null;

switch (this)

{

case CELSIUS:

fahrenheit = sourceMeasure.multiply(NINE).divide(FIVE).add(THIRTY_TWO);

break;

case FAHRENHEIT:

fahrenheit = sourceMeasure;

break;

case KELVIN:

fahrenheit = (sourceMeasure.subtract(this.freezingPoint)).multiply(NINE).divide(FIVE).add(THIRTY_TWO);

break;

case RANKINE:

fahrenheit = sourceMeasure.subtract(RANKINE_FAHRENHEIT_DELTA);

break;

}

return fahrenheit;

}



/**

* Convert provided measure in this temperature scale to equivalent measure

* in Celsius scale.

*

* @param sourceMeasure Numeric measurement in source scale.

* @return Numeric measurement equivalent in Celsius scale; null if the

* provided source scale is not supported.

*/

public BigDecimal toCelsius(final BigDecimal sourceMeasure)

{

BigDecimal celsius = null;

switch (this)

{

case CELSIUS:

celsius = sourceMeasure;

break;

case FAHRENHEIT:

celsius = sourceMeasure.subtract(THIRTY_TWO).divide(NINE.divide(FIVE));

break;

case KELVIN:

celsius = sourceMeasure.subtract(this.freezingPoint);

break;

case RANKINE:

celsius = (sourceMeasure.subtract(THIRTY_TWO).subtract(RANKINE_FAHRENHEIT_DELTA)).divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP);

break;

}

return celsius;

}



/**

* Convert provided measure in this temperature scale to equivalent measure

* in Kelvin scale.

*

* @param sourceMeasure Numeric measurement in source scale.

* @return Numeric measurement equivalent in Kelvin scale; null if the

* provided source scale is not supported.

*/

public BigDecimal toKelvin(final BigDecimal sourceMeasure)

{

BigDecimal kelvin = null;

switch (this)

{

case CELSIUS:

kelvin = sourceMeasure.add(this.freezingPoint);

break;

case FAHRENHEIT:

kelvin = sourceMeasure.add(RANKINE_FAHRENHEIT_DELTA).divide(NINE.divide(FIVE));

break;

case KELVIN:

kelvin = sourceMeasure;

break;

case RANKINE:

kelvin = sourceMeasure.divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP);

break;

}

return kelvin;

}



/**

* Convert provided measure in this temperature scale to equivalent measure

* in Rankine scale.

*

* @param sourceMeasure Numeric measurement in source scale.

* @return Numeric measurement equivalent in Rankine scale; null if the

* provided source scale is not supported.

*/

public BigDecimal toRankine(final BigDecimal sourceMeasure)

{

BigDecimal rankine = null;

switch (this)

{

case CELSIUS:

rankine = sourceMeasure.add(NINE.divide(FIVE)).add(THIRTY_TWO).add(RANKINE_FAHRENHEIT_DELTA);

break;

case FAHRENHEIT:

rankine = sourceMeasure.add(RANKINE_FAHRENHEIT_DELTA);

break;

case KELVIN:

rankine = sourceMeasure.multiply(NINE.divide(FIVE));

break;

case RANKINE:

rankine = sourceMeasure;

break;

}

return rankine;

}

}


The last version of TemperatureUnit.java adds conversion methods to the enum for converting between one supported temperature scale to another supported temperature scale.

NOTE: I used BigDecimal in the code to avoid rounding issues, but this means that one must be beware the non-terminating representation ArithmeticException. This also means that Java clients of this enum must provide numeric representations as BigDecimal. I could have allowed Strings to be provided and passed those internally to BigDecimal's constructor accepting String. For Groovy clients, which use BigDecimals for floating-point numbers implicitly, no special treatment is required.


Conclusion

The Java enum is a powerful feature of the language that allows one to elegantly represent finite values and to provide convenient access to characteristics of those finite values. In this post, I have demonstrated adding conversion methods to an enum to convert between its range of values. This places significant functionality in a single place and encapsulates conversion functionality within the enum itself.

Không có nhận xét nào:

Đăng nhận xét