Resource / URL Qualifier
Last edited on 15 January 2009

From blog post "A Geographic Distance Class in C#".

/*
	Distance.cs - By Forrest Croce, January 2009.
	This code is released as open source;  please attribute the author and source.
*/
using System;

namespace ForrestCroce.Geography {
	/// 
	/// Distinguishes between different systems of units of measure.
	/// 
	public enum UnitSystem {
		/// 
		/// The Imperial units ( foot, mile, etc ) are more or less only used in the United States anymore.
		/// 
		Imperial,
		/// 
		/// The metric system units ( meter, kilometer ) have taken over most in of the world.
		/// 
		Metric
	}

	/// 
	/// Distinguishes between the units making up the imperial system of measuring distance.
	/// 
	public enum ImperialUnits {
		/// 
		/// An inch.
		/// 
		Inch,
		/// 
		/// A foot;  12 inches.
		/// 
		Foot,
		/// 
		/// A yard;  3 feet.
		/// 
		Yard,
		/// 
		/// A mile;  5,280 feet.
		/// 
		Mile,
		/// 
		/// A nautical ( or Admirality ) mile;  1,852 meters by convention.
		/// 
		NauticalMile
	}

	/// 
	/// Distinguishes between the units making up the metric system of measuring distance.
	/// 
	public enum MetricUnits {
		/// 
		/// A millimeter ( mm ).
		/// 
		Millimeter,
		/// 
		/// A centimeter ( cm );  10 mms.
		/// 
		Centimeter,
		/// 
		/// A decimeter;  10 cms.
		/// 
		Decimeter,
		/// 
		/// A meter;  10 decimeters or 100 cms.
		/// 
		Meter,
		/// 
		/// A kilometer;  1,000 meters.
		/// 
		Kilometer
	}

	/// 
	/// Represents the distance between two points ( on a 2D or 3D surface ) in much the same way a TimeSpan represents the
	/// distance
	/// 
	public class Distance {
		#region Conversion constants
		private const decimal InchesPerFoot = 12;
		private const decimal FeetPerYard = 3;
		private const decimal YardsPerMile = 1760;
		private const decimal FeetPerMile = 5280;
		private const decimal MetersPerNauticalMile = 1852;

		private const decimal CentimetersPerMillimeter = 10;
		private const decimal CentimetersPerDecimeter = 0.1M;
		private const decimal CentimetersPerMeter = 0.01M;
		private const decimal CentimetersPerKilometer = 0.00001M;

		//private const decimal MilesPerKilometer = 0.621371192M; // From Google
		//private const decimal MilesPerKilometer = 0.621371192237M; // From http://www.conversion-metric.org/
		private const decimal MilesPerKilometer = 0.621371192237334M; // From http://www.unitsconversion.com.ar/
		#endregion

		private UnitSystem nativeType;
		private ImperialUnits nativeImperialType;
		private MetricUnits nativeMetricType;
		private decimal nativeValue;

		#region New / class constructor
		/// 
		/// Creates a new, empty instance of the Distance class.
		/// 
		public Distance() {
		}

		/// 
		/// Creates a new instance of the Distance class, and populates it with a value and unit to describe that value.
		/// 
		/// The raw number representing the amount of distance.
		/// What scale the 'value' number exists on, eg feet, yards, miles, etc.
		public Distance(decimal value, ImperialUnits unit) {
			SetValue(value, unit);
		}

		/// 
		/// Creates a new instance of the Distance class, and populates it with a value and unit to describe that value.
		/// 
		/// The raw number representing the amount of distance.
		/// What scale the 'value' number exists on, eg meters, kilometers, etc.
		public Distance(decimal value, MetricUnits unit) {
			SetValue(value, unit);
		}

		public Distance(decimal value, Enum unit) {
			SetValue(value, unit);
		}
		#endregion

		/// 
		/// Provides a string representation of this amount of distance, as a number plus the unit label.
		/// 
		/// A string with the number of units and unit type making up this instance.
		public override string ToString() {
			return InternalValue.ToString("#,##0.00") + " " + (nativeType == UnitSystem.Metric ? nativeMetricType.ToString() : nativeImperialType.ToString());
		}

		#region Conversion functions
		/// 
		/// Converts between imperial units of distance.
		/// 
		/// A value to be converted.
		/// The unit the value should be converted from ( eg feet ).
		/// The unit hte value should be converted to ( eg miles ).
		/// A decimal containing the equivelant value in the new format.
		public static decimal Convert(decimal value, ImperialUnits fromUnit, ImperialUnits toUnit) {
			if (fromUnit == toUnit)
				return value;

			#region Inches
			if (fromUnit == ImperialUnits.Inch) {
				if (toUnit == ImperialUnits.Foot)
					return value / InchesPerFoot;

				if (toUnit == ImperialUnits.Yard)
					return value / InchesPerFoot / FeetPerYard;

				if (toUnit == ImperialUnits.Mile)
					return value / InchesPerFoot / FeetPerMile;

				if (toUnit == ImperialUnits.NauticalMile)
					return Convert(value / InchesPerFoot / FeetPerMile, ImperialUnits.Mile, MetricUnits.Meter) / MetersPerNauticalMile;
			}
			#endregion
			#region Feet
			if (fromUnit == ImperialUnits.Foot) {
				if (toUnit == ImperialUnits.Inch)
					return value * InchesPerFoot;

				if (toUnit == ImperialUnits.Yard)
					return value / FeetPerYard;

				if (toUnit == ImperialUnits.Mile)
					return value / FeetPerMile;

				if (toUnit == ImperialUnits.NauticalMile)
					return Convert(value / FeetPerMile, ImperialUnits.Mile, MetricUnits.Meter) / MetersPerNauticalMile;
			}
			#endregion
			#region Yards
			if (fromUnit == ImperialUnits.Yard) {
				if (toUnit == ImperialUnits.Inch)
					return value * FeetPerYard * InchesPerFoot;

				if (toUnit == ImperialUnits.Foot)
					return value * FeetPerYard;

				if (toUnit == ImperialUnits.Mile)
					return value / YardsPerMile;

				if (toUnit == ImperialUnits.NauticalMile)
					return Convert(value / YardsPerMile, ImperialUnits.Mile, MetricUnits.Meter) / MetersPerNauticalMile;
			}
			#endregion
			#region Miles
			if (fromUnit == ImperialUnits.Mile) {
				if (toUnit == ImperialUnits.Inch)
					return value * FeetPerMile * InchesPerFoot;

				if (toUnit == ImperialUnits.Foot)
					return value * FeetPerMile;

				if (toUnit == ImperialUnits.Yard)
					return value * FeetPerMile / FeetPerYard;

				if (toUnit == ImperialUnits.NauticalMile)
					return Convert(value, ImperialUnits.Mile, MetricUnits.Meter) / MetersPerNauticalMile;
			}
			#endregion
			#region Nautical miles
			if (fromUnit == ImperialUnits.NauticalMile) {
				if (toUnit == ImperialUnits.Inch)
					return Convert(value * MetersPerNauticalMile, MetricUnits.Meter, ImperialUnits.Inch);

				if (toUnit == ImperialUnits.Foot)
					return Convert(value * MetersPerNauticalMile, MetricUnits.Meter, ImperialUnits.Inch) / InchesPerFoot;

				if (toUnit == ImperialUnits.Yard)
					return Convert(value * MetersPerNauticalMile, MetricUnits.Meter, ImperialUnits.Inch) / InchesPerFoot / FeetPerYard;

				if (toUnit == ImperialUnits.Mile)
					return Convert(value * MetersPerNauticalMile, MetricUnits.Meter, ImperialUnits.Inch) / InchesPerFoot / FeetPerYard / YardsPerMile;
			}
			#endregion

			return decimal.MinValue;
		}

		/// 
		/// Converts between metric units of distance.
		/// 
		/// A value to be converted.
		/// The unit the value should be converted from ( eg meters ).
		/// The unit hte value should be converted to ( eg kilometers ).
		/// A decimal containing the equivelant value in the new format.
		public static decimal Convert(decimal value, MetricUnits fromUnit, MetricUnits toUnit) {
			if (fromUnit == toUnit)
				return value;

			if (fromUnit == MetricUnits.Millimeter) {
				if (toUnit == MetricUnits.Centimeter)
					return value / CentimetersPerMillimeter;

				if (toUnit == MetricUnits.Decimeter)
					return (value / CentimetersPerMillimeter) * CentimetersPerDecimeter;

				if (toUnit == MetricUnits.Meter)
					return (value / CentimetersPerMillimeter) * CentimetersPerMeter;

				if (toUnit == MetricUnits.Kilometer)
					return (value / CentimetersPerMillimeter) * CentimetersPerKilometer;
			}

			if (fromUnit == MetricUnits.Centimeter) {
				if (toUnit == MetricUnits.Millimeter)
					return value * CentimetersPerMillimeter;

				if (toUnit == MetricUnits.Decimeter)
					return value * CentimetersPerDecimeter;

				if (toUnit == MetricUnits.Meter)
					return value * CentimetersPerMeter;

				if (toUnit == MetricUnits.Kilometer)
					return value * CentimetersPerKilometer;
			}

			if (fromUnit == MetricUnits.Decimeter) {
				if (toUnit == MetricUnits.Millimeter)
					return value / CentimetersPerDecimeter * CentimetersPerMillimeter;

				if (toUnit == MetricUnits.Centimeter)
					return value / CentimetersPerDecimeter;

				if (toUnit == MetricUnits.Meter)
					return value / CentimetersPerDecimeter * CentimetersPerMeter;

				if (toUnit == MetricUnits.Kilometer)
					return value / CentimetersPerDecimeter * CentimetersPerKilometer;
			}

			if (fromUnit == MetricUnits.Meter) {
				if (toUnit == MetricUnits.Millimeter)
					return (value / CentimetersPerMeter) * CentimetersPerMillimeter;

				if (toUnit == MetricUnits.Centimeter)
					return value / CentimetersPerMeter;

				if (toUnit == MetricUnits.Decimeter)
					return (value / CentimetersPerMeter) * CentimetersPerDecimeter;

				if (toUnit == MetricUnits.Kilometer)
					return (value / CentimetersPerMeter) * CentimetersPerKilometer;
			}

			if (fromUnit == MetricUnits.Kilometer) {
				if (toUnit == MetricUnits.Millimeter)
					return (value * CentimetersPerMillimeter) / CentimetersPerKilometer;

				if (toUnit == MetricUnits.Centimeter)
					return value / CentimetersPerKilometer;

				if (toUnit == MetricUnits.Decimeter)
					return (value * CentimetersPerDecimeter) / CentimetersPerKilometer;

				if (toUnit == MetricUnits.Meter)
					return (value * CentimetersPerMeter) / CentimetersPerKilometer;
			}

			return decimal.MinValue;
		}

		/// 
		/// Converts from a imperial to a metric unit of distance.
		/// 
		/// A value to be converted.
		/// The unit the value should be converted from ( eg miles ).
		/// The unit hte value should be converted to ( eg kilometers ).
		/// A decimal containing the equivelant value in the new format.
		public static decimal Convert(decimal value, ImperialUnits fromUnit, MetricUnits toUnit) {
			decimal miles = fromUnit == ImperialUnits.Mile ? value : Convert(value, fromUnit, ImperialUnits.Mile);
			decimal kms = miles / MilesPerKilometer;

			return Convert(kms, MetricUnits.Kilometer, toUnit);
		}

		/// 
		/// Converts from a metric to a imperial unit of distance.
		/// 
		/// A value to be converted.
		/// The unit the value should be converted from ( eg kilometers ).
		/// The unit hte value should be converted to ( eg miles ).
		/// A decimal containing the equivelant value in the new format.
		public static decimal Convert(decimal value, MetricUnits fromUnit, ImperialUnits toUnit) {
			decimal kms = fromUnit == MetricUnits.Kilometer ? value : Convert(value, fromUnit, MetricUnits.Kilometer);
			decimal miles = kms * MilesPerKilometer;

			return Convert(miles, ImperialUnits.Mile, toUnit);
		}

		/// 
		/// Converts between units of distance, without needing to be told explicitly which measurement type each
		/// unit falls into.  For example, this method can convert feet to yards, or feet to meters.
		/// 
		/// A value to be converted.
		/// The unit the value should be converted from ( eg meters ).
		/// The unit hte value should be converted to ( eg kilometers ).
		/// A decimal containing the equivelant value in the new format.
		public static decimal Convert(decimal value, Enum fromUnit, Enum toUnit) {
			if (fromUnit == toUnit)
				return value;

			if (fromUnit is ImperialUnits && toUnit is ImperialUnits)
				return Convert(value, (ImperialUnits)fromUnit, (ImperialUnits)toUnit);

			if (fromUnit is MetricUnits && toUnit is MetricUnits)
				return Convert(value, (MetricUnits)fromUnit, (MetricUnits)toUnit);

			if (fromUnit is ImperialUnits && toUnit is MetricUnits)
				return Convert(value, (ImperialUnits)fromUnit, (MetricUnits)toUnit);

			if (fromUnit is MetricUnits && toUnit is ImperialUnits)
				return Convert(value, (MetricUnits)fromUnit, (ImperialUnits)toUnit);

			if (!(fromUnit is ImperialUnits) && !(fromUnit is MetricUnits))
				throw new ArgumentException("All unit type enumerations must be either ImperialUnits or MetricUnits.", "fromUnit");

			if (!(toUnit is ImperialUnits) && !(toUnit is MetricUnits))
				throw new ArgumentException("All unit type enumerations must be either ImperialUnits or MetricUnits.", "toUnit");

			throw new ArgumentException("All unit type enumerations must be either ImperialUnits or MetricUnits.");
		}

		/// 
		/// Converts the InternalValue of one Distance object (d2) to match another Distance object's (d1's) unit-type, and returns the corresponding value.
		/// 
		/// The Distance object to match.
		/// The Distance object to convert from.
		/// The distance represented by d2, in terms of d1's internal unit of measure.
		public static decimal Normalize(Distance d1, Distance d2) {
			if (d1.InternalUnitType == UnitSystem.Metric && d2.InternalUnitType == UnitSystem.Metric)
				return Distance.Convert(d2.InternalValue, d2.InternalMetricUnit, d1.InternalMetricUnit);
			else if (d1.InternalUnitType == UnitSystem.Imperial && d2.InternalUnitType == UnitSystem.Imperial)
				return Distance.Convert(d2.InternalValue, d2.InternalImperialUnit, d1.InternalImperialUnit);
			else if (d1.InternalUnitType == UnitSystem.Metric && d2.InternalUnitType == UnitSystem.Imperial)
				return Distance.Convert(d2.InternalValue, d2.InternalImperialUnit, d1.InternalMetricUnit);
			else if (d1.InternalUnitType == UnitSystem.Imperial && d2.InternalUnitType == UnitSystem.Metric)
				return Distance.Convert(d2.InternalValue, d2.InternalMetricUnit, d1.InternalImperialUnit);

			return 0; // This code will never get to execute, but is needed without turning the last else if into just an else.
		}
		#endregion

		#region Set value without having to call the property or create a new instance
		/// 
		/// Allows the user to set a distance value for a imperial unit of measure, without having to call the specific property representing that unit.
		/// 
		/// The distance as a raw number.
		/// The unit the distance should be represented in.
		public void SetValue(decimal value, ImperialUnits unit) {
			nativeType = UnitSystem.Imperial;
			nativeImperialType = unit;
			nativeValue = value;
		}

		/// 
		/// Allows the user to set a distance value for a metric unit of measure, without having to call the specific property representing that unit.
		/// 
		/// The distance as a raw number.
		/// The unit the distance should be represented in.
		public void SetValue(decimal value, MetricUnits unit) {
			nativeType = UnitSystem.Metric;
			nativeMetricType = unit;
			nativeValue = value;
		}

		/// 
		/// Allows the user to set a distance value for a imperial or metric unit of measure, without having to call the specific property representing that unit.
		/// 
		/// The distance as a raw number.
		/// The unit the distance should be represented in.
		public void SetValue(decimal value, Enum unit) {
			if (unit is ImperialUnits)
				SetValue(value, (ImperialUnits)unit);
			else if (unit is MetricUnits)
				SetValue(value, (MetricUnits)unit);
			else
				throw new ArgumentException("The unit must be a member of the ImperialUnits or MetricUnits enumeration.");
		}
		#endregion

		#region Properties
		#region Imperial
		/// 
		/// Gets or sets the distance being described in terms of inches ( in ).
		/// 
		public decimal Inches {
			get {
				if (nativeType == UnitSystem.Imperial && nativeImperialType == ImperialUnits.Inch)
					return nativeValue;

				if (nativeType == UnitSystem.Imperial)
					return Distance.Convert(nativeValue, nativeImperialType, ImperialUnits.Inch);

				return Distance.Convert(nativeValue, nativeMetricType, ImperialUnits.Inch);
			}
			set {
				SetValue(value, ImperialUnits.Inch);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of feet ( ft ).
		/// 
		public decimal Feet {
			get {
				if (nativeType == UnitSystem.Imperial && nativeImperialType == ImperialUnits.Foot)
					return nativeValue;

				if (nativeType == UnitSystem.Imperial)
					return Distance.Convert(nativeValue, nativeImperialType, ImperialUnits.Foot);

				return Distance.Convert(nativeValue, nativeMetricType, ImperialUnits.Foot);
			}
			set {
				SetValue(value, ImperialUnits.Inch);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of yards.
		/// 
		public decimal Yards {
			get {
				if (nativeType == UnitSystem.Imperial && nativeImperialType == ImperialUnits.Yard)
					return nativeValue;

				if (nativeType == UnitSystem.Imperial)
					return Distance.Convert(nativeValue, nativeImperialType, ImperialUnits.Yard);

				return Distance.Convert(nativeValue, nativeMetricType, ImperialUnits.Yard);
			}
			set {
				SetValue(value, ImperialUnits.Yard);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of miles ( mi ).
		/// 
		public decimal Miles {
			get {
				if (nativeType == UnitSystem.Imperial && nativeImperialType == ImperialUnits.Mile)
					return nativeValue;

				if (nativeType == UnitSystem.Imperial)
					return Distance.Convert(nativeValue, nativeImperialType, ImperialUnits.Mile);

				return Distance.Convert(nativeValue, nativeMetricType, ImperialUnits.Mile);
			}
			set {
				SetValue(value, ImperialUnits.Mile);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of nautical miles ( nm ).
		/// 
		public decimal NauticalMiles {
			get {
				if (nativeType == UnitSystem.Imperial && nativeImperialType == ImperialUnits.NauticalMile)
					return nativeValue;

				if (nativeType == UnitSystem.Imperial)
					return Distance.Convert(nativeValue, nativeImperialType, ImperialUnits.NauticalMile);

				return Distance.Convert(nativeValue, nativeMetricType, ImperialUnits.NauticalMile);
			}
			set {
				SetValue(value, ImperialUnits.NauticalMile);
			}
		}
		#endregion
		#region Metric
		/// 
		/// Gets or sets the distance being described in terms of millimeters ( mm ).
		/// 
		public decimal Millimeters {
			get {
				if (nativeType == UnitSystem.Metric && nativeMetricType == MetricUnits.Millimeter)
					return nativeValue;

				if (nativeType == UnitSystem.Metric)
					return Distance.Convert(nativeValue, nativeMetricType, MetricUnits.Millimeter);

				return Distance.Convert(nativeValue, nativeImperialType, MetricUnits.Millimeter);
			}
			set {
				SetValue(value, MetricUnits.Millimeter);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of cenimeters ( cm ).
		/// 
		public decimal Cenimeters {
			get {
				if (nativeType == UnitSystem.Metric && nativeMetricType == MetricUnits.Centimeter)
					return nativeValue;

				if (nativeType == UnitSystem.Metric)
					return Distance.Convert(nativeValue, nativeMetricType, MetricUnits.Centimeter);

				return Distance.Convert(nativeValue, nativeImperialType, MetricUnits.Centimeter);
			}
			set {
				SetValue(value, MetricUnits.Centimeter);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of decimeters.
		/// 
		public decimal Decimeters {
			get {
				if (nativeType == UnitSystem.Metric && nativeMetricType == MetricUnits.Decimeter)
					return nativeValue;

				if (nativeType == UnitSystem.Metric)
					return Distance.Convert(nativeValue, nativeMetricType, MetricUnits.Decimeter);

				return Distance.Convert(nativeValue, nativeImperialType, MetricUnits.Decimeter);
			}
			set {
				SetValue(value, MetricUnits.Decimeter);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of meters ( m ).
		/// 
		public decimal Meters {
			get {
				if (nativeType == UnitSystem.Metric && nativeMetricType == MetricUnits.Meter)
					return nativeValue;

				if (nativeType == UnitSystem.Metric)
					return Distance.Convert(nativeValue, nativeMetricType, MetricUnits.Meter);

				return Distance.Convert(nativeValue, nativeImperialType, MetricUnits.Meter);
			}
			set {
				SetValue(value, MetricUnits.Meter);
			}
		}

		/// 
		/// Gets or sets the distance being described in terms of kilometers ( km ).
		/// 
		public decimal Kilometers {
			get {
				if (nativeType == UnitSystem.Metric && nativeMetricType == MetricUnits.Kilometer)
					return nativeValue;

				if (nativeType == UnitSystem.Metric)
					return Distance.Convert(nativeValue, nativeMetricType, MetricUnits.Kilometer);

				return Distance.Convert(nativeValue, nativeImperialType, MetricUnits.Kilometer);
			}
			set {
				SetValue(value, MetricUnits.Kilometer);
			}
		}
		#endregion

		#region Internal values
		/// 
		/// Gets the internal value being used by this instance of the Distance object.  This is an advanced property, and is being exposed
		/// to allow large sums to be computed without introducing rounding errors which would be forced by the need to convert each item to
		/// a common unit.
		/// 
		public decimal InternalValue {
			get {
				return nativeValue;
			}
		}

		/// 
		/// Gets the type of units ( imperial or metric ) being used internally by this Distance object.
		/// 
		public UnitSystem InternalUnitType {
			get {
				return nativeType;
			}
		}

		/// 
		/// Returns the unit of measure being used natively as an Enum, so that client code will not have to use if statements while creating or comparing instances.
		/// 
		public Enum InternalUnit {
			get {
				if (nativeType == UnitSystem.Metric)
					return nativeMetricType;

				return nativeImperialType;
			}
		}

		/// 
		/// Gets the metric unit being used internally by this Distance object, if metric is being used.
		/// 
		public MetricUnits InternalMetricUnit {
			get {
				return nativeMetricType;
			}
		}

		/// 
		/// Gets the imperial unit being used internally by this Distance object, if imperial is being used.
		/// 
		public ImperialUnits InternalImperialUnit {
			get {
				return nativeImperialType;
			}
		}
		#endregion
		#endregion

		#region Operator overloading
		/// 
		/// Adds two Distance objects together, and returns the result, after correcting for different units of measure.
		/// 
		/// The first Distance object to add.
		/// The second Distance object to add.
		/// A new Distance object, with the combined total of value of the two parts.
		public static Distance operator +(Distance d1, Distance d2) {
			return new Distance(d1.InternalValue + Distance.Normalize(d1, d2), d1.InternalUnit);
		}

		/// 
		/// Subtracts two Distance objects together, and returns the result, after correcting for different units of measure.
		/// 
		/// The first Distance object to subtract.
		/// The second Distance object to subtract.
		/// A new Distance object, with the combined total difference in value of the two points.
		public static Distance operator -(Distance d1, Distance d2) {
			return new Distance(d1.InternalValue - Distance.Normalize(d1, d2), d1.InternalUnit);
		}

		/// 
		/// Determines whether a Distance object is less than another Distance object.
		/// 
		/// The first object to use in the comparison.
		/// The second object to use in the comparison.
		/// True if the first object (d1) is less than the second object (d2);  false otherwise.
		public static bool operator <(Distance d1, Distance d2) {
			return d1.InternalValue < Distance.Normalize(d1, d2);
		}

		/// 
		/// Determines whether a Distance object is greater than another Distance object.
		/// 
		/// The first object to use in the comparison.
		/// The second object to use in the comparison.
		/// True if the first object (d1) is greater than the second object (d2);  false otherwise.
		public static bool operator >(Distance d1, Distance d2) {
			return d1.InternalValue > Distance.Normalize(d1, d2);
		}
		#endregion
	}
}