
29 September 08
A Temperature struct for .NET
Did you ever wonder why .NET doesn't have a Temperature type? I suppose not, because I didn't find any implementation of such a thing on the internet.
I was working on documentation for CuttingEdge.Conditions, when I thought it would be nice to show some code samples using a Temperature class. However I noticed, there isn't such a thing in the .NET framework. That isn't that odd, because did you ever need a Temperature class? I didn't. After some tinkering I noticed my lovely Temperature class was becoming bigger and bigger. To big to be useful in documentation, so I decided to go for a simpler example :-). The Temperature type is a struct that can be used as follows:
Temperature dutchTemperature = Temperature.FromCelsius(16);
Temperature niceTemperature = Temperature.FromFahrenheit(88);
// Print the temperature in a culture sensitive way:
// Then next line Prints "60.8 °F" when the culture is en-US
// and "16 °C" when the culture is en-GB.
Console.WriteLine(dutchTemperature);
// The next line Prints "88 °F" when the culture is en-US
// and "31.11 °C" when the culture is en-GB.
Console.WriteLine(niceTemperature);
// Prints out "15.1111"
Console.WriteLine("Difference in celsius: {0}",
niceTemperature.Celsius - dutchTemperature.Celsius);
// Prints out "27.2"
Console.WriteLine("Difference in fahrenheit: {0}",
niceTemperature.Fahrenheit - dutchTemperature.Fahrenheit);
Temperature evenColder =
Temperature.FromFahrenheit(dutchTemperature.Fahrenheit - 20.0);
Console.WriteLine("Dutch winters:");
Console.WriteLine(" in fahrenheit: {0:F}", evenColder);
Console.WriteLine(" in celsius: {0:C}", evenColder);
Console.WriteLine(" in kelvin: {0:K}", evenColder);
And here is the actual code of Temperature struct:
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using CuttingEdge.Conditions;
/// <summary>
/// Represents a temperature.
/// </summary>
[Serializable, ComVisible(true)]
public struct Temperature : IComparable, IFormattable,
IComparable<Temperature>, IEquatable<Temperature>
{
/// <summary>
/// Represents the smallest possible value of <see cref="Temperature"/>.
/// This field is read-only.
/// </summary>
public static readonly Temperature MinValue =
new Temperature(0);
/// <summary>
/// Represents the biggest possible value of
/// <see cref="Temperature"/>. This field is read-only.
/// </summary>
public static readonly Temperature MaxValue =
new Temperature(Double.MaxValue);
// Sarray with all countries that use fahrenheit.
private static readonly string[] fahrenheitCultures = { "en-US" };
// The internal state is a double containing the Kelvin value.
private readonly double kelvin;
/// <summary>
/// Initializes a new instance of the Temperature struct.
/// </summary>
/// <param name="kelvin">The kelvin value.</param>
public Temperature(double kelvin)
{
kelvin.Requires("kelvin").IsGreaterOrEqual(0);
this.kelvin = kelvin;
}
private enum TemperatureScale
{
Celsius,
Fahrenheit,
Kelvin
}
/// <summary>Gets the temperature in Fahrenheit.</summary>
public double Fahrenheit
{
get { return Converter.FromKelvinToFahrenheit(this.kelvin); }
}
/// <summary>Gets the temperature in Celsius.</summary>
public double Celsius
{
get { return Converter.FromKelvinToCelsius(this.kelvin); }
}
/// <summary>Gets the temperature in Kelvin.</summary>
public double Kelvin
{
get { return this.kelvin; }
}
/// <summary>
/// Converts the specified double, which contains a celsius value,
/// to the equivalent <see cref="Temperature"/>.
/// </summary>
/// <param name="celsius">The celsius value.</param>
/// <returns>A <see cref="Temperature"/> that is the equivalent of
/// <paramref name="celsius"/>.</returns>
public static Temperature FromCelsius(double celsius)
{
celsius.Requires("celcius")
.IsGreaterOrEqual(-1 * Converter.ZeroDegreesCelsiusInKelvin);
double kelvin = Converter.FromCelsiusToKelvin(celsius);
return new Temperature(kelvin);
}
/// <summary>
/// Converts the specified double, which contains a fahrenheit value,
/// to the equivalent <see cref="Temperature"/>.
/// </summary>
/// <param name="fahrenheit">The fahrenheit value.</param>
/// <returns>A <see cref="Temperature"/> that is the equivalent of
/// <paramref name="fahrenheit"/>.</returns>
public static Temperature FromFahrenheit(double fahrenheit)
{
fahrenheit.Requires("fahrenheit")
.IsGreaterOrEqual(-1 * Converter.KelvinToFahrenheitOffset);
double kelvin = Converter.FromFahrenheitToKelvin(fahrenheit);
return new Temperature(kelvin);
}
/// <summary>
/// Converts the specified double, which contains a fahrenheit value,
/// to the equivalent <see cref="Temperature"/>.
/// </summary>
/// <param name="kelvin">The fahrenheit value.</param>
/// <returns>A <see cref="Temperature"/> that is the equivalent of
/// <paramref name="fahrenheit"/>.</returns>
public static Temperature FromKelvin(double kelvin)
{
kelvin.Requires("kelvin")
.IsGreaterOrEqual(Converter.KelvinMinimumValue);
return new Temperature(kelvin);
}
/// <summary>
/// Converts the string representation of a <see cref="Temperature"/>
/// equivalent. A return value indicates whether the conversion
/// succeeded or failed.
/// </summary>
/// <param name="s">A string containing a temperature to convert.</param>
/// <param name="result">When this method returns, contains the
/// temperature equivalent to the s parameter, if the conversion
/// succeeded, or <see cref="Temperature.MinValue"/> if the
/// conversion failed. The conversion fails if the s parameter is
/// null, is not a temperature in a valid format, or represents a
/// number less than <see cref="Temperature.MinValue" /> or greater
/// than <see cref="Temperature.MaxValue"/>. This parameter is
/// passed uninitialized.</param>
/// <returns>true if s was converted successfully; otherwise, false.</returns>
public static bool TryParse(string s, out Temperature result)
{
return TryParse(s, null, out result);
}
/// <summary>
/// Converts the string representation of a <see cref="Temperature"/>
/// equivalent. A return value indicates whether the conversion
/// succeeded or failed.
/// </summary>
/// <param name="s">A string containing a temperature to convert.</param>
/// <param name="provider">The format provider; or null.</param>
/// <param name="result">When this method returns, contains the
/// temperature equivalent to the s parameter, if the conversion
/// succeeded, or <see cref="Temperature.MinValue"/> if the
/// conversion failed. The conversion fails if the s parameter is
/// null, is not a temperature in a valid format, or represents a
/// number less than <see cref="Temperature.MinValue" /> or greater
/// than <see cref="Temperature.MaxValue"/>. This parameter is
/// passed uninitialized.</param>
/// <returns>true if s was converted successfully; otherwise, false.</returns>
public static bool TryParse(string s, IFormatProvider provider,
out Temperature result)
{
s.Requires("s").IsNotNullOrEmpty();
s = s.Trim();
string[] parts;
if (s.Contains("°"))
{
parts = s.Split('°');
}
else
{
parts = s.Split(' ');
// When the string contains multiple spaces in the middle,
// we remove them.
if (parts.Length > 2)
{
parts = new string[2] { parts[0], parts[parts.Length - 1] };
}
}
if (parts.Length == 2)
{
string number = parts[0].Trim();
string scale = parts[1].Trim();
if (number.Length > 0 && scale.Length == 1)
{
double value;
bool validDouble = Double.TryParse(number,
NumberStyles.Any, provider, out value);
if (validDouble)
{
if (scale == "K" || scale == "k")
{
result = Temperature.FromKelvin(value);
return true;
}
else if (scale == "F" || scale == "f")
{
result = Temperature.FromFahrenheit(value);
return true;
}
else if (scale == "C" || scale == "c")
{
result = Temperature.FromCelsius(value);
return true;
}
}
}
}
result = Temperature.MinValue;
return false;
}
/// <summary>
/// Converts the string representation of a number to its
/// <see cref="Temperature"/> equivalent.
/// </summary>
/// <param name="s">A string containing a temperature to convert.</param>
/// <returns>A <see cref="Temperature"/> equivalent to the
/// temperature contained in s.</returns>
public static Temperature Parse(string s)
{
return Temperature.Parse(s, null);
}
/// <summary>
/// Converts the string representation of a number to its
/// <see cref="Temperature"/> equivalent.
/// </summary>
/// <param name="s">A string containing a temperature to convert.</param>
/// <param name="provider">An System.IFormatProvider that supplies
/// culture-specific formatting information about s.</param>
/// <returns>A <see cref="Temperature"/> equivalent to the
/// temperature contained in s.</returns>
public static Temperature Parse(string s, IFormatProvider provider)
{
s.Requires("s").IsNotNullOrEmpty();
Temperature result;
bool valid = Temperature.TryParse(s, provider, out result);
if (!valid)
{
throw new FormatException(
"Input string was not in a correct format");
}
return result;
}
/// <summary>
/// Converts an <see cref="Temperature"/> to a
/// <see cref="System.Double"/>.
/// </summary>
/// <param name="value">The temperature</param>
/// <returns>A double containing the <see cref="Temperature.Kelvin"/>
/// value of the supplied <paramref name="value"/>.</returns>
public static explicit operator double(Temperature value)
{
// NOTE: We can not let this be an implicit operator,
// because this could lead to unexpected behavior.
return value.kelvin;
}
/// <summary>
/// Converts an <see cref="Temperature"/> to a
/// <see cref="System.Double"/>.
/// </summary>
/// <param name="value">The temperature</param>
/// <returns>A double containing the <see cref="Temperature.Kelvin"/>
/// value of the supplied <paramref name="value"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="value"/> is negative.</exception>
public static explicit operator Temperature(double value)
{
return new Temperature(value);
}
/// <summary>Returns a value indicating whether two instances of
/// <see cref="Temperature"/> are equal.</summary>
/// <returns>true if t1 and t2 are equal; otherwise, false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator ==(Temperature t1, Temperature t2)
{
return t1.Equals(t2);
}
/// <summary>Returns a value indicating whether two instances of
/// <see cref="Temperature"/> are not equal.</summary>
/// <returns>true if t1 and t2 are not equal; otherwise, false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator !=(Temperature t1, Temperature t2)
{
return !t1.Equals(t2);
}
/// <summary>Returns a value indicating whether a specified
/// <see cref="Temperature"/> is less than another specified
/// <see cref="Temperature"/>.</summary>
/// <returns>true if t1 is less than t2; otherwise, false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator <(Temperature t1, Temperature t2)
{
return t1.CompareTo(t2) < 0;
}
/// <summary>Returns a value indicating whether a specified
/// <see cref="Temperature"/> is less than or equal to another
/// specified <see cref="Temperature"/>.</summary>
/// <returns>true if t1 is less than or equal to t2; otherwise,
/// false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator <=(Temperature t1, Temperature t2)
{
return t1.CompareTo(t2) <= 0;
}
/// <summary>Returns a value indicating whether a specified
/// <see cref="Temperature"/> is greater than another specified
/// <see cref="Temperature"/>.</summary>
/// <returns>true if t1 is greater than t2; otherwise, false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator >(Temperature t1, Temperature t2)
{
return t1.CompareTo(t2) > 0;
}
/// <summary>Returns a value indicating whether a specified
/// <see cref="Temperature"/> is greater than or equal to another
/// specified <see cref="Temperature"/>.</summary>
/// <returns>true if t1 is greater than or equal to t2; otherwise,
/// false.</returns>
/// <param name="t2">A <see cref="Temperature"/>.</param>
/// <param name="t1">A <see cref="Temperature"/>.</param>
public static bool operator >=(Temperature t1, Temperature t2)
{
return t1.CompareTo(t2) >= 0;
}
/// <summary>Returns the hash code for this instance.</summary>
/// <returns>
/// A 32-bit signed integer that is the hash code for this instance.
/// </returns>
public override int GetHashCode()
{
return this.kelvin.GetHashCode();
}
/// <summary>
/// Returns a string representation of the current
/// <see cref="Temperature"/>.
/// </summary>
/// <returns><see cref="String"/> that represents the current
/// <see cref="Temperature"/>.</returns>
public override string ToString()
{
return this.ToString(null, CultureInfo.CurrentCulture);
}
/// <summary>
/// Returns a string representation of the current
/// <see cref="Temperature"/>.
/// </summary>
/// <param name="format">The <see cref="String"/> specifying the format
/// to use.-or- null to use the default format.</param>
/// <returns><see cref="String"/> that represents the current
/// <see cref="Temperature"/>.</returns>
public string ToString(string format)
{
return this.ToString(format, CultureInfo.CurrentCulture);
}
/// <summary>
/// Formats the value of the current instance using the specified
/// provider.
/// </summary>
/// <param name="formatProvider">The <see cref="IFormatProvider"/> to
/// use to format the value.-or- null to obtain the numeric format
/// information from the current locale setting of the operating
/// system.</param>
/// <returns>
/// A <see cref="String"/> containing the value of the current
/// instance in the specified format.
/// </returns>
public string ToString(IFormatProvider formatProvider)
{
return this.ToString(null, formatProvider);
}
/// <summary>
/// Formats the value of the current instance using the specified
/// format.
/// </summary>
/// <param name="format">The <see cref="String"/> specifying the format
/// to use.-or- null to use the default format.</param>
/// <param name="formatProvider">The <see cref="IFormatProvider"/> to
/// use to format the value.-or- null to obtain the numeric format
/// information from the current locale setting of the operating
/// system.</param>
/// <returns>
/// A <see cref="String"/> containing the value of the current
/// instance in the specified format.
/// </returns>
public string ToString(string format, IFormatProvider formatProvider)
{
TemperatureScale scale;
if (String.IsNullOrEmpty(format))
{
scale = GetTemperatureScaleForCulture(formatProvider);
}
else
{
scale = GetTemperatureScaleFromFormat(format);
}
return this.ToCultureSpecificString(scale, formatProvider);
}
/// <summary>
/// Compares the current <see cref="Temperature"/> with another
/// <see cref="Temperature"/>.
/// </summary>
/// <param name="other">The <see cref="Temperature"/> to compare with
/// this <see cref="Temperature"/>.</param>
/// <returns>
/// A 32-bit signed integer that indicates the relative order of the
/// <see cref="Temperature"/> values being compared. The return value
/// has the following meanings: Value Meaning Less than zero This
/// object is less than the <paramref name="other"/> parameter. Zero
/// This object is equal to <paramref name="other"/>. Greater than
/// zero This object is greater than <paramref name="other"/>.
/// </returns>
public int CompareTo(Temperature other)
{
return this.kelvin.CompareTo(other.kelvin);
}
/// <summary>
/// Indicates whether the current object is equal to another object
/// of the same type.
/// </summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>
/// true if the current object is equal to the
/// <paramref name="other"/> parameter; otherwise, false.
/// </returns>
public bool Equals(Temperature other)
{
return this.CompareTo(other) == 0;
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">Another object to compare to.</param>
/// <returns>
/// true if <paramref name="obj"/> and this instance are the same
/// type and represent the same value; otherwise, false.
/// </returns>
public override bool Equals(object obj)
{
if (!(obj is Temperature))
{
return false;
}
return this.Equals((Temperature)obj);
}
/// <summary>
/// Compares the current instance with another object of the same type.
/// </summary>
/// <param name="obj">An object to compare with this instance.</param>
/// <returns>
/// A 32-bit signed integer that indicates the relative order of the
/// objects being compared. The return value has these meanings:
/// Value Meaning Less than zero This instance is less than
/// <paramref name="obj"/>. Zero This instance is equal to
/// <paramref name="obj"/>. Greater than zero This instance is
/// greater than <paramref name="obj"/>.
/// </returns>
/// <exception cref="ArgumentException">Thrown when
/// <paramref name="obj"/> is not of type <see cref="Temperature"/>.
/// </exception>
public int CompareTo(object obj)
{
if (obj == null)
{
return 1;
}
obj.Requires("obj").IsOfType(typeof(Temperature));
return this.CompareTo((Temperature)obj);
}
private static TemperatureScale GetTemperatureScaleFromFormat(string format)
{
if (format == "C" || format == "c")
{
return TemperatureScale.Celsius;
}
if (format == "F" || format == "f")
{
return TemperatureScale.Fahrenheit;
}
if (format == "K" || format == "k")
{
return TemperatureScale.Kelvin;
}
// For any unrecognized format, throw an exception.
throw new FormatException(String.Format(CultureInfo.InvariantCulture,
"Invalid format string: '{0}'.", format));
}
private static TemperatureScale GetTemperatureScaleForCulture(
IFormatProvider formatProvider)
{
CultureInfo cultureInfo = formatProvider as CultureInfo;
if (cultureInfo == null)
{
cultureInfo = CultureInfo.CurrentCulture;
}
string currentCultureName = cultureInfo.Name;
string[] fahrenheitCultures = Temperature.fahrenheitCultures;
for (int i = 0; i < fahrenheitCultures.Length; i++)
{
if (currentCultureName == fahrenheitCultures[i])
{
return TemperatureScale.Fahrenheit;
}
}
return TemperatureScale.Celsius;
}
private string ToCultureSpecificString(TemperatureScale scale,
IFormatProvider formatProvider)
{
// When no formatProvider info is supplied, we use the current culture.
if (formatProvider == null)
{
formatProvider = CultureInfo.CurrentCulture;
}
switch (scale)
{
case TemperatureScale.Celsius:
return String.Format(formatProvider,
"{0} °C", this.Celsius);
case TemperatureScale.Fahrenheit:
return String.Format(formatProvider,
"{0} °F", this.Fahrenheit);
case TemperatureScale.Kelvin:
return String.Format(formatProvider,
"{0} K", this.Kelvin);
default:
throw new PostconditionException(
"Argument 'scale' is invalid.");
}
}
// Internal helper class for temperature conversions.
private static class Converter
{
public const double ZeroDegreesCelsiusInKelvin = 273.15D;
public const double KelvinToFahrenheitMultiplier = 9.0D / 5.0D;
public const double KelvinToFahrenheitOffset = 459.67D;
public const double KelvinMinimumValue = 0D;
public static double FromKelvinToFahrenheit(double kelvin)
{
return (kelvin * KelvinToFahrenheitMultiplier) -
KelvinToFahrenheitOffset;
}
public static double FromKelvinToCelsius(double kelvin)
{
return kelvin - ZeroDegreesCelsiusInKelvin;
}
public static double FromCelsiusToKelvin(double celsius)
{
return celsius + ZeroDegreesCelsiusInKelvin;
}
public static double FromFahrenheitToKelvin(double fahrenheit)
{
return (fahrenheit + KelvinToFahrenheitOffset) /
KelvinToFahrenheitMultiplier;
}
}
}
- C# - four comments / No trackbacks - § ¶
The code samples on my weblog are colorized using javascript, but
you disabled javascript (for my website) on your browser.
If you're interested in viewing the posted code snippets in
color, please enable javascript.
cool... yeah, probably won't be using that any time soon
2 things
- the first I feel like I already told you about it. Wouldn't be possible to remove the strings from the .Requires method?
kelvin.Requires("kelvin")
kelvin.Requires.Something...
- would it be possible to go back to full feed?
I knew there was a reason why I hadn't seen your blog in a while, is almost automatic to skip over the partial feeds
anyway, cool stuff
Eber Irigoyen (URL) - 30 09 08 - 00:02
you're right, I was just thinking out loud
but what about other options like
id.Requires(GreatherThan(0));
or just
id.Requires().GreatherThan(0);
Eber Irigoyen (URL) - 30 09 08 - 17:52
I can't remember we talked about this, but perhaps you read my blog. I wrote a bit about it
(http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=39).
I had a discussion on the Code Project with someone. You should read it. Your proposal is much like his. You can read about it here:
http://www.codeproject.com/KB/library/co..
and here:
http://www.codeproject.com/KB/library/co..
I like your proposal, but I don't really think it's possible to write "kelvin.Requires.Something" because, there is no such thing as 'extension properties'. We can only extend methods in C#. Next to that, we need the argumentName to be able to throw an expressive exception.
Steven (URL) - 30 09 08 - 20:58
Eber,
About your "id.Requires(GreatherThan(0))" proposal:
This is only possible if the GreatherThan method would be defined in the same class as the code you're writing. This isn't very convenient. It would be possible when you rewrite it as: "id.Requires(Conditions.GreatherThan(0))" but it wouldn't really increase usability. Think of the way IntelliSense is helping the user in the current implementation of CuttingEdge.Conditions. I believe this isn't the way to go.
About your second proposal: "id.Requires().GreatherThan(0)":
This syntax is actually supported. That code is equivalent to the following: "id.Requires("value").GreatherThan(0)". The parameterless Requires() method fills in "value" as argument name. There is with the "id.Requires()" syntax no way to determine the actual name of the argument, and you will have to be aware that the exception message contains "value" instead of the argument name.
I think It's time you start trying out the CuttingEdge.Conditions library ;-). Try it out and tell me what you like and what you don’t and please keep on thinking out loud! :-)
Steven (URL) - 30 09 08 - 21:15