JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 38 "Type Safe Unit Libraries, A Temperature Class"

Many rather dramatic software failures can be traced to improper assumptions about unit types. In one incident the error can be traced to one module using Metric units while another module used Imperial units. In this chapter, we extend a MSDN sample Temperature class into a type safe Temperature class that can be used to enforce unit type safety at compile time!

The Problem

bullet
1965 August 29: computer programming error: Gemini 5 landed 130 kilometers short of its planned
Pacific Ocean landing point due to a software error. The Earth's rotation rate had been 
programmed as one revolution per solar day instead of the correct value, 
one revolution per sidereal day.
bullet
Mars Climate Observer metric problem (1998) 
Two spacecraft, the Mars Climate Orbiter and the Mars Polar Lander, were part of a space programme that, 
in 1998, was supposed to study the Martian weather, climate, and water and carbon dioxide content of the 
atmosphere. But a problem occurred when a navigation error caused the lander to fly too low in the 
atmosphere and it was destroyed.

What caused the error? A sub-contractor on the Nasa programme had used imperial units 
(as used in the US), rather than the Nasa-specified metric units (as used in Europe).
bullet
It took the European Space Agency 10 years and $7 billion to produce Ariane 5, 
a giant rocket capable of hurling a pair of three-ton satellites into orbit with each 
launch and intended to give Europe overwhelming supremacy in the commercial space business.
All it took to explode that rocket less than a minute into its maiden voyage last June, 
scattering fiery rubble across the mangrove swamps of French Guiana, was a small computer
program trying to stuff a 64-bit number into a 16-bit space.

Unit Type Safety

C# is a strongly typed language so it is possible to design a class hierarchy that allows the coder to enforce unit type safety at compile time. Type safe unit libraries are available in C++ that provide compile time unit checking to "try to make it more difficult to make coding mistakes that incorrectly mix units of measure." In this chapter, we code a proof of concept type safe unit of temperature.

A Strongly Typed Temperature Hierarchy

Here is a prototype of a strongly type Temperature hierarchy with concrete classes Celsius, Fahrenheit, Kelvin and Rankine that inherit from an abstract base class, Temperature. Parameters and return values can be declared as a concrete type (Celsius,Fahrenheit,Kelvin,Rankine) or as the base class (Temperature) depending on the need for type safety.

Coders may create an instance of a concrete class as in:

Rankine r = new Rankine(491.688);
System.Console.WriteLine("Value is R: "+ r.Value);

But the following calls to Value will not compile:

((Temperature)r).Value; // will not compile, good behavior, ambiguous
Temperature t = new Rankine(491.688);
t.Value; // will not compile, good behavior, ambiguous

This class hierarchy is based on the Temperature class found in this sample code from MSDN. This twisted version differs in the following ways:

bulletThe Temperature class is declared abstract and various concrete classes inherit from the Temperature class.
bulletThere is no default Value to a unit type such as Fahrenheit. Concrete classes return a Value appropriate to the concrete type. The base class and derived types do provide getters and setters for all four unit types (F,C,K,R), but do not provide a default Value.
bulletSetters throw ArgumentOutOfRange exceptions for invalid values less than absolute zero.
bulletParse throws a FormatException, if no valid format is specified, rather than return a default object of type Fahrenheit.
bulletAdds support for Kelvin and Rankine.

Note: This is not a criticism of the sample MSDN Temperature class which was designed to demonstrate the use of the double class and the IComparable and IFormattable interfaces. In fact, the decision to extend this MSDN class is based on the desire to reuse the functionality in this sample class.

Here is a link to a conversion chart between the four supported temperature units.

This class hierarchy is extensible to some degree. For instance, new concrete classes can be added for the more obscure units such as Delisle, Newton, Reaumur and Romer. However, the base class Temperature will not provide getters and setters, parse or format in these units.

Rationale

The concrete classes may only return a Value in units appropriate for the class of the object. A reference of type Temperature should not return a default Value of class Fahrenheit since the value is not formatted and may not be appropriate for the concrete class. References of type Temperature should not return a an unformatted value appropriate for the concrete class as this would be extremely confusing, returning unformatted units of mixed types. Any concrete class can explicitly return a value in alternative units using explicit getters such as c.Fahrenheit. A concrete class may return alternative units in response to explicit formatting such as ToString("F",null). ToString() is overridden so that the actual value of the concrete class is returned as a formatted string. There is no ambiguity calling ToString() since the value is formatted. If the call to ToString() is on a reference of type Temperature, the units are still appropriate for the actual concrete class.

Compile Time Type Safety

Given a class TestConcrete with a method BoilingH2O.

    public class TestConcrete
    {
        public bool BoilingH2O(Fahrenheit f)
        {
            if (f.Value >= 211.9710) { return true; }
            else {return false;}
        }
    }

The method compiles as below when passed a parameter of type Fahrenheit.

TestConcrete test= new TestConcrete();
Fahrenheit fBoil = new Fahrenheit(212);
bool b = test.BoilingH2O(fBoil);
System.Console.WriteLine(b); // true

However, the compiler will generate an error at compile time if a parameter of type Celsius is passed to Boiling.

Celsius cBoil = new Celsius(100);
b = t.BoilingH2O(cBoil); // compile time error

Error 1 The best overloaded method match for 'TestTemperature.TestConcrete.BoilingH2O(TestTemperature.Fahrenheit)' has some invalid arguments
Error 2 Argument '1': cannot convert from 'TestTemperature.Celsius' to 'TestTemperature.Fahrenheit'

Without a type safe temperature unit, the method might be executed at runtime returning false, a runtime error. Awwk. This could result in some very overcooked spaghetti!

Using the Base Class Temperature

It is also possible to write a more generic version that takes a parameter of type Temperature and then extracts an explicit value in Fahrenheit from the concrete object. Here is our TestBase class and method which takes any Temperature t:

    public class TestBase
    {
        public bool BoilingH2O(Temperature t)
        {
            if (t.Fahrenheit >= 211.9710) { return true; }
            else { return false; }
        }
    }

The compiler will not complain if we pass a reference of type Celsius to this version of BoilingH2O.


TestBase tb = new TestBase();
System.Console.WriteLine(tb.BoilingH2O(cBoil)); // true

Alternatives

One alternative is a set of enums and a Length class that combines the data type as an enum and the value as a double. This could be use to enforce runtime unit type safety.

Usage

Since the base class Temperature implements the IComparable and IFormattable interfaces, it supports the IComparable.CompareTo and IFormattable.ToString(string format, IFormatProvider provider) methods. Temperature also provides a Parse method, but not a TryParse method (A TryParse method would require the proper concrete type as an out parameter, defeating the purpose of parsing the string for type).

public abstract class Temperature : IComparable, IFormattable

Constructor

First we can create an object of class Celsius by calling the constructor and passing a valid temperature value.

Celsius c = new Celsius(0);

Passing a value below absolute zero will throw an ArgumentOutOfRange exception.

Temperature t2 = new Celsius(-274);  // throws exception
Temperature t2 = new Kelvin(-1);  // throws exception

Temperature.ToString(string format, IFormatProvider provider)

Next we output a formatted string.

System.Console.WriteLine(c.ToString("F",null));  // 32'F

We can also embed the formatting within a string.

System.Console.WriteLine("Value is {0:C}", c); // 0'C
System.Console.WriteLine("Value is {0:K}", c); // 273.15'K
System.Console.WriteLine("Value is {0:R}", c); // 491.67'R
System.Console.WriteLine("Value is {0:F}", c); // 32'F

These calls to WriteLine in turn call the ToString(format,provider) method which, in turn, calls the appropriate getter. As a result the proper temperature value is returned for the given type of format C, K, R, or F. You can verify this by adding a breakpoint in the debugger at ToString(format,provider).

Note: c.Fahrenheit.ToString() calls double.ToString() not Temperature.ToString()

The behavior of the concrete class ToString() method is to return the value in the appropriate unit for the concrete class.


System.Console.WriteLine(c.ToString()); // "0'C"
Fahrenheit f = new Fahrenheit(32);
System.Console.WriteLine(f.ToString()); // "32'F"
Kelvin k= new Kelvin(273.15);
System.Console.WriteLine(k.ToString()); // "273.15'K"
Rankine r= new Rankine(491.67);
System.Console.WriteLine(r.ToString()); // "491.67'R"

Calling ToString() on a reference of type Temperature also returns the value in the appropriate unit type for the class of the actual object.

Temperature parent= (Temperature)c;
System.Console.WriteLine(parent.ToString()); // "0'C"

Pass null parameters as in c.ToString(null,null) again returns the value in Centigrade, but formatted as '?

System.Console.WriteLine(c.ToString(null,null)); // "0'?"

Temperature.CompareTo(Temperature t)

We can also compare two temperatures.

Temperature c1 = new Fahrenheit(212);
Temperature c2 = new Celsius(100);
        
int compare= c1.CompareTo(c2);
if (compare == 0) {System.Console.WriteLine("Equal :{0:F} {1:C}",c1,c2);}
else if (compare < 0) { System.Console.WriteLine("Less :{0:F} {1:C}", c1, c2); }
else if (compare > 0) { System.Console.WriteLine("Greater :{0:F} {1:C}", c1, c2); } // Equal

Comparisons are best done between two temperatures of the same concrete class. Any valid temperature is consider greater than a null. The static call Compare(c1,c2) is aesthetically more appealing since it works with (null,null) (see String.Compare(s1,s2) for example).

Static Temperature.Compare(Temperature t1, Temperature t2)

OK, so I added a static Compare method that takes null.

        // return -1 if t1 is less than t2 or t1 is null, but t2 is not null
        // return 0 if t1 == t2 or both t1 and t2 are null
        // return 1 if t1 is > t2 or if t1 is not null and t2 is null
        public static int Compare(Temperature t1, Temperature t2) {
            if (t1 == null && t2 == null)
            {
                return 0;
            }
            else if (t1 == null && t2 != null)
            {
                return -1;
            }
            else if (t1 != null && t2 == null)
            {
                return 1;
            }
            return t1.Celsius.CompareTo(t2.Celsius);
        }

 

Parse(string s)

We can create an object by parsing a string in the form of [ws][sign]digits['F|'C|'K|'R][ws].

Temperature.Parse("100'C"));
Temperature.Parse("100'C", NumberStyles.Any));
Temperature.Parse("100'C", NumberStyles.Any, null));

Parse will throw a FormatException if a properly formatted string is not passed. This prevents the creation of an ambiguous unit value. Parse returns a reference of type Temperature while creating an object of the appropriate concrete class.

Automatic Unit Conversion

The base class Temperature provides getters and setters for automatic conversion between units.

Celsius c = new Celsius(0);
// direct conversion
System.Console.WriteLine("Value is "+ c.Celsius+"C"); // 0C
System.Console.WriteLine("Value is "+ c.Fahrenheit+"F"); // 32F
System.Console.WriteLine("Value is "+ c.Kelvin+"K"); // 273.15K
System.Console.WriteLine("Value is "+ c.Rankine+"R"); // 491.67R

Code

Here is our prototype Temperature class and hierarchy. In Chapter 39 I coded a unit test module to verify this code. Enjoy!

 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;  // numbers styles
using System.Diagnostics;

namespace Temperature
{
    class Program
    {
        static void Main(string[] args)
        {
            // http://en.wikipedia.org/wiki/Rankine_scale
            // test in line formatting string
            System.Console.WriteLine("Testing IFormattable");
            Celsius c = new Celsius(Celsius.FREEZE_H2O);
            System.Console.WriteLine("Value expected is {1}'C--> {0:C}", c, Celsius.FREEZE_H2O);
            System.Console.WriteLine("Value expected is {1}'F--> {0:F}", c, Fahrenheit.FREEZE_H2O);
            System.Console.WriteLine("Value expected is {1}'K-->  {0:K}", c, Kelvin.FREEZE_H2O);
            System.Console.WriteLine("Value expected is {1}'R-->  {0:R}", c, Rankine.FREEZE_H2O);

            //  ******************************************************  //
            //((Temperature)r).Value; // will not compile, good behavior, ambiguous
            //Temperature t = new Rankine(491.688);
            //t.Value; // will not compile, good behavoir, ambiguous
            //  ******************************************************  //

            UnitTest unit = new UnitTest();
            unit.DoUnitTest();
            System.Console.WriteLine(unit.Log);

            System.Console.ReadLine();

        }
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // **************** PROTOTYPE CODE ** DO NOT USE ********************** //
    //  JAL 12.10.07    Extends MSDN Temperature Class                      //
    //   http://msdn2.microsoft.com/en-us/library/system.double.aspx        //        
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    // DEFINITION OF BOIL_H2O IS OLD SCHOOL 100'C NOT NEW SCHOOL 99.9839'C (see alt)
    // http://en.wikipedia.org/wiki/Rankine_scale
    // derived classes are used to enforce unit type safety at compile time

    /// <summary>
    /// *** The Celsius Class ***
    /// Extends the abstract Temperature class
    /// Conversion is handled by the base class Temperature
    /// The Value property is NOT available in the base class
    /// JAL 12.15.08
    /// </summary>
    public class Celsius : Temperature
    {
        static public readonly double BOIL_H2O = 100; // alt 99.9839
        static public readonly double FREEZE_H2O = 0;
        static public readonly double ABSOLUTE_ZERO = -273.15;
        //public Celsius() : this(ABSOLUTE_ZERO) { ;}
        // ASSERT t >= -273.15
        public Celsius(double t)
        {
            if (t < -273.15) { throw new ArgumentOutOfRangeException(); }
            this.m_valueCelsius = t;
        }
        public double Value
        {
            get
            {
                return this.m_valueCelsius;
            }
        }
        public override string ToString() // overriding base class returns X'C
        {
            return base.ToString("C", null);
        }
    } // end Celsius

    /// <summary>
    /// *** The Farhenheit Class ***
    /// Extends the abstract Temperature class
    /// Conversion is handled by the base class Temperature
    /// The Value property is NOT available in the base class
    /// JAL 12.15.08
    /// </summary>
    public class Fahrenheit : Temperature
    {
        static public readonly double BOIL_H2O = 212; // alt 211.9710
        static public readonly double FREEZE_H2O = 32;
        static public readonly double ABSOLUTE_ZERO = -459.67;
        //public Fahrenheit() : this(ABSOLUTE_ZERO) { ;}
        //ASSERT t>=-459.67
        public Fahrenheit(double t)
        {
            //if (t < -459.67) { throw new ArgumentOutOfRangeException(); }
            Celsius c = new Celsius(0);
            c.Fahrenheit = t;
            this.m_valueCelsius = c.Celsius;
        }
        public double Value
        {
            get
            {
                return this.Fahrenheit;
            }
        }
        public override string ToString() // overriding base class Temperature.ToString returns X'F
        {
            return base.ToString("F", null);
        }
    } // end Fahrenheit

    /// <summary>
    /// *** The Kelvin Class ***
    /// Extends the abstract Temperature class
    /// Conversion is handled by the base class Temperature
    /// The Value property is NOT available in the base class
    /// JAL 12.15.08
    /// </summary>
    public class Kelvin : Temperature
    {
        static public readonly double BOIL_H2O = 373.15; // alt 373.1339
        static public readonly double FREEZE_H2O = 273.15;
        static public readonly double ABSOLUTE_ZERO = 0;
        //public Kelvin() : this(ABSOLUTE_ZERO) { ;}
        //ASSERT t>=0
        public Kelvin(double t)
        {
            //if (t < 0) { throw new ArgumentOutOfRangeException(); }
            Celsius c = new Celsius(0);
            c.Kelvin = t;
            this.m_valueCelsius = c.Celsius;
        }
        public double Value
        {
            get
            {
                return this.Kelvin;
            }
        }
        public override string ToString() // overriding base class Temperature.ToString returns X'K
        {
            return base.ToString("K", null);
        }
    } // end Kelvin

    /// <summary>
    /// *** The Rankine Class ***
    /// Extends the abstract Temperature class
    /// Conversion is handled by the base class Temperature
    /// The Value property is NOT available in the base class
    /// JAL 12.15.08
    /// </summary>
    public class Rankine : Temperature
    {
        static public readonly double BOIL_H2O = 671.67; // alt 671.641
        static public readonly double FREEZE_H2O = 491.67;
        static public readonly double ABSOLUTE_ZERO = 0;
        //public Rankine() : this(ABSOLUTE_ZERO) { ;}
        //ASSERT t>=0
        public Rankine(double t)
        {
            //if (t < 0) { throw new ArgumentOutOfRangeException(); }
            Celsius c = new Celsius(0);
            c.Rankine = t;
            this.m_valueCelsius = c.Celsius;
        }
        public double Value
        {
            get
            {
                return this.Rankine;
            }
        }
        public override string ToString() // overriding base class Temperature.ToString returns X'R
        {
            return base.ToString("R", null);
        }
    } // end Rankine

    /// <summary>
    /// *** The Abstract Base Class Temperature ***
    /// Adapted from the MSDN Temperature Class
    /// A type safe Temperature hierarchy of
    /// Concrete classes Celsius, Fahrenheit, Kelvin, Rankine
    /// derive from Temperature
    /// Internally temperature is stored in Celsius as private data
    /// Temperature class stores the value as Double
    /// and delegates most of the functionality 
    /// to the Double implementation.
    /// DO NOT provide a Value property for this base class, ambiguous!
    /// JAL 12.10.08
    /// </summary>
    public abstract class Temperature : IComparable, IFormattable
    {
        // return -1 if t1 is less than t2 or t1 is null, but t2 is not null
        // return 0 if t1 == t2 or both t1 and t2 are null
        // return 1 if t1 is > t2 or if t1 is not null and t2 is null
        public static int Compare(Temperature t1, Temperature t2)
        {
            if (t1 == null && t2 == null)
            {
                return 0;
            }
            else if (t1 == null && t2 != null)
            {
                return -1;
            }
            else if (t1 != null && t2 == null)
            {
                return 1;
            }
            return t1.Celsius.CompareTo(t2.Celsius);
        }
        /// <summary>
        /// IComparable.CompareTo implementation.
        /// "Equal" is not "accurate" between different concrete types due to numeric inaccuracy
        /// ASSERT obj is of type Temperature
        /// This version takes obj as null
        /// </summary>
        public int CompareTo(object obj)
        {
            if (obj == null) { return 1; } // any non null string is greater than null
            if (obj is Temperature)
            {
                Temperature temp = (Temperature)obj;

                return m_valueCelsius.CompareTo(temp.m_valueCelsius);
            }

            throw new ArgumentException("Object is not a Temperature.");
        }
        /// <summary>
        /// ToString()
        /// </summary>
        public override string ToString()
        {
            return ToString(null, null);
        }
        /// <summary>
        /// IFormattable.ToString implementation.
        /// </summary>
        public string ToString(string format, IFormatProvider provider)
        {
            if (format != null)
            {
                if (format.Equals("F"))
                {
                    return String.Format("{0}'F", this.Fahrenheit.ToString());
                }
                else if (format.Equals("C"))
                {
                    return String.Format("{0}'C", this.Celsius.ToString());
                }
                else if (format.Equals("K"))
                {
                    return String.Format("{0}'K", this.Kelvin.ToString());
                }
                else if (format.Equals("R"))
                {
                    return String.Format("{0}'R", this.Rankine.ToString());
                }
            }
            // default
            return String.Format("{0}'?", this.Celsius.ToString()); ;
        }
        /// <summary>
        /// Parse
        /// </summary>
        /// <param name="s">String</param>
        /// <param name="ns">NumberStyles</param>
        /// <returns>Temperature</returns>
        public static Temperature Parse(string s, NumberStyles ns)
        {
            return Temperature.Parse(s, ns, null);
        }
        /// <summary>
        /// Parse
        /// </summary>
        /// <param name="s">String</param>
        /// <returns>Temperature</returns>
        public static Temperature Parse(string s)
        {
            return Temperature.Parse(s, NumberStyles.Any, null);
        }
        /// <summary>
        /// Parses the temperature from a string in form
        /// [ws][sign]digits['F|'C|'K|'R][ws]
        /// Class Factory Pattern
        /// ASSERT s != null
        /// ASSERT format is valid ** DO NOT ** allow construction of ambiguous temperature
        /// </summary>
        public static Temperature Parse(string s, NumberStyles styles, IFormatProvider provider)
        {
            Temperature temp;

            if (s.TrimEnd(null).EndsWith("'F"))
            {
                temp = new Fahrenheit(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider));
            }
            else if (s.TrimEnd(null).EndsWith("'C"))
            {
                temp = new Celsius(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider));
            }
            else if (s.TrimEnd(null).EndsWith("'K"))
            {
                temp = new Kelvin(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider));
            }
            else if (s.TrimEnd(null).EndsWith("'R"))
            {
                temp = new Rankine(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider));
            }
            else // invalid format prohibit ambiguous type
            {
                throw new FormatException();
            }

            return temp;
        }
        // TryParse makes no sense as you need to pass concrete type as out parameter
        // The value holder in Celsius
        protected double m_valueCelsius;

        //ASSERT value >=-273.15
        public double Celsius
        {
            get
            {
                return m_valueCelsius;
            }
            set
            {
                if (value < -273.15) { throw new ArgumentOutOfRangeException(); }
                m_valueCelsius = value;
            }
        }

        //ASSERT value >=-459.67
        public double Fahrenheit
        {
            get
            {
                return (m_valueCelsius * 1.8) + 32;
            }
            set
            {
                if (value < -459.67) { throw new ArgumentOutOfRangeException(); }
                m_valueCelsius = (value - 32) / 1.8;
            }
        }

        //ASSERT value >= 0
        public double Kelvin
        {
            get
            {
                return m_valueCelsius + 273.15;
            }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException();
                }
                m_valueCelsius = value - 273.15;
            }
        }

        //ASSERT value >=0
        public double Rankine
        {
            get
            {
                return (m_valueCelsius + 273.15) * 1.8;
            }
            set
            {
                if (value < 0) { throw new ArgumentOutOfRangeException(); }
                m_valueCelsius = (value / 1.8) - 273.15;
            }
        }
    } // end Temperature
}

This extended version of the MSDN Temperature class is type safe and can be used to enforce unit type safety at compile time.

Code Safe!

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1