From 0a7219fb010826c434e71ac26db2b3bb4be5d717 Mon Sep 17 00:00:00 2001
From: Michael Krisper <michael.krisper@tugraz.at>
Date: Tue, 14 Apr 2015 12:38:16 +0200
Subject: [PATCH] added tests for si units, changed internal si units to enums

---
 VectoCore/Utils/SI.cs         | 229 ++++++++++++++++++++++++----------
 VectoCoreTest/Utils/SITest.cs |  92 +++++++++++++-
 2 files changed, 249 insertions(+), 72 deletions(-)

diff --git a/VectoCore/Utils/SI.cs b/VectoCore/Utils/SI.cs
index 1e29afeb4c..82a168b371 100644
--- a/VectoCore/Utils/SI.cs
+++ b/VectoCore/Utils/SI.cs
@@ -1,8 +1,8 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.Contracts;
-using System.Dynamic;
 using System.Linq;
 using System.Runtime.Serialization;
 using TUGraz.VectoCore.Exceptions;
@@ -52,16 +52,14 @@ namespace TUGraz.VectoCore.Utils
 
 	public class Newton : SIBase<Newton>
 	{
-		protected Newton(double val) : base(val, new SI().Newton) {}
-
 		public Newton() : this(0) {}
+		protected Newton(double val) : base(val, new SI().Newton) {}
 	}
 
 	public class Radian : SIBase<Radian>
 	{
 		public Radian() : this(0) {}
-
-		public Radian(double val) : base(val, new SI().Radian) {}
+		protected Radian(double val) : base(val, new SI().Radian) {}
 	}
 
 	public class NewtonMeter : SIBase<NewtonMeter>
@@ -172,27 +170,88 @@ namespace TUGraz.VectoCore.Utils
 		#endregion
 	}
 
+	/// <summary>
+	/// Class for Representing SI Units.
+	/// </summary>
 	[DataContract]
 	public class SI : IComparable
 	{
-		[DataMember] protected readonly string[] Denominator;
+		[DataMember] protected readonly Unit[] Denominator;
 		[DataMember] protected readonly int Exponent;
-		[DataMember] protected readonly string[] Numerator;
+		[DataMember] protected readonly Unit[] Numerator;
 		[DataMember] protected readonly bool Reciproc;
 		[DataMember] protected readonly bool Reverse;
 		[DataMember] protected double Val;
 
+		[SuppressMessage("ReSharper", "InconsistentNaming")]
+		protected enum Unit
+		{
+			/// <summary>
+			/// kilo
+			/// </summary>
+			k,
+
+			/// <summary>
+			/// seconds
+			/// </summary>
+			s,
+
+			/// <summary>
+			/// meter
+			/// </summary>
+			m,
+
+			/// <summary>
+			/// gramm
+			/// </summary>
+			g,
+
+			/// <summary>
+			/// Watt
+			/// </summary>
+			W,
+
+			/// <summary>
+			/// Newton
+			/// </summary>
+			N,
+
+			/// <summary>
+			/// %
+			/// </summary>
+			Percent,
+
+			/// <summary>
+			/// minutes
+			/// </summary>
+			min,
+
+			/// <summary>
+			/// centi
+			/// </summary>
+			c,
+
+			/// <summary>
+			/// Hour
+			/// </summary>
+			h
+		}
+
+		/// <summary>
+		/// Creates a new dimensionless SI Unit.
+		/// </summary>
+		/// <param name="val"></param>
 		public SI(double val = 0.0)
 		{
 			Val = val;
 			Reciproc = false;
 			Reverse = false;
-			Numerator = new string[0];
-			Denominator = new string[0];
+			Numerator = new Unit[0];
+			Denominator = new Unit[0];
 			Exponent = 1;
 		}
 
-		protected SI(double val, IEnumerable<string> numerator, IEnumerable<string> denominator,
+		protected SI(double val, IEnumerable<Unit> numerator, IEnumerable<Unit> denominator,
 			bool reciproc = false,
 			bool reverse = false, int exponent = 1)
 		{
@@ -219,7 +278,7 @@ namespace TUGraz.VectoCore.Utils
 		protected SI(double val, SI unit)
 			: this(val, unit.Numerator, unit.Denominator) {}
 
-		protected SI(SI si, double? factor = null, string fromUnit = null, string toUnit = null,
+		protected SI(SI si, double? factor = null, Unit? fromUnit = null, Unit? toUnit = null,
 			bool? reciproc = null, bool? reverse = null, int? exponent = null)
 		{
 			Contract.Requires(si != null);
@@ -264,18 +323,19 @@ namespace TUGraz.VectoCore.Utils
 			Denominator = numerator.ToArray();
 		}
 
-		private void UpdateUnit(string fromUnit, string toUnit, ICollection<string> units)
+		private void UpdateUnit(Unit? fromUnit, Unit? toUnit, ICollection<Unit> units)
 		{
-			if (Reverse && !string.IsNullOrEmpty(fromUnit)) {
-				if (units.Contains(fromUnit)) {
-					units.Remove(fromUnit);
+			if (Reverse && fromUnit.HasValue) {
+				if (units.Contains(fromUnit.Value)) {
+					units.Remove(fromUnit.Value);
 				} else {
-					throw new VectoException("Unit missing. Conversion not possible.");
+					throw new VectoException(string.Format("Unit missing. Conversion not possible. [{0}] does not contain a [{1}].",
+						string.Join(", ", units), fromUnit));
 				}
 			}
 
-			if (!string.IsNullOrEmpty(toUnit)) {
-				units.Add(toUnit);
+			if (toUnit.HasValue) {
+				units.Add(toUnit.Value);
 			}
 		}
 
@@ -292,7 +352,7 @@ namespace TUGraz.VectoCore.Utils
 		}
 
 		/// <summary>
-		/// Casts the SI Unit to the concrete unit type if the units are correct.
+		/// Casts the SI Unit to the concrete unit type (if the units allow such an cast).
 		/// </summary>
 		/// <typeparam name="T"></typeparam>
 		/// <returns></returns>
@@ -302,38 +362,42 @@ namespace TUGraz.VectoCore.Utils
 			if (!HasEqualUnit(t)) {
 				throw new VectoException(string.Format("SI Unit Conversion failed: From {0} to {1}", this, t));
 			}
-			Contract.Assert(HasEqualUnit(t), string.Format("SI Unit Conversion failed: From {0} to {1}", this, t));
 			return t;
 		}
 
+		/// <summary>
+		/// Converts the derived SI units to the basic units and returns this as a new SI object.
+		/// </summary>
+		/// <returns></returns>
 		public SI ToBasicUnits()
 		{
-			var numerator = new List<string>();
-			var denominator = new List<string>();
+			var numerator = new List<Unit>();
+			var denominator = new List<Unit>();
 			Numerator.ToList().ForEach(unit => ConvertToBasicUnits(unit, numerator, denominator));
 			Denominator.ToList().ForEach(unit => ConvertToBasicUnits(unit, denominator, numerator));
 			return new SI(Val, numerator, denominator);
 		}
 
-		private static void ConvertToBasicUnits(string unit, ICollection<string> numerator,
-			ICollection<string> denominator)
+
+		private static void ConvertToBasicUnits(Unit unit, ICollection<Unit> numerator,
+			ICollection<Unit> denominator)
 		{
 			switch (unit) {
-				case "W":
-					numerator.Add("k");
-					numerator.Add("g");
-					numerator.Add("m");
-					numerator.Add("m");
-					denominator.Add("s");
-					denominator.Add("s");
-					denominator.Add("s");
+				case Unit.W:
+					numerator.Add(Unit.k);
+					numerator.Add(Unit.g);
+					numerator.Add(Unit.m);
+					numerator.Add(Unit.m);
+					denominator.Add(Unit.s);
+					denominator.Add(Unit.s);
+					denominator.Add(Unit.s);
 					break;
-				case "N":
-					numerator.Add("k");
-					numerator.Add("g");
-					numerator.Add("m");
-					denominator.Add("s");
-					denominator.Add("s");
+				case Unit.N:
+					numerator.Add(Unit.k);
+					numerator.Add(Unit.g);
+					numerator.Add(Unit.m);
+					denominator.Add(Unit.s);
+					denominator.Add(Unit.s);
 					break;
 				default:
 					numerator.Add(unit);
@@ -403,7 +467,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Gramm
 		{
-			get { return new SI(new SI(this, toUnit: "k"), 0.001, "g", "g"); }
+			get { return new SI(new SI(this, toUnit: Unit.k), 0.001, Unit.g, Unit.g); }
 		}
 
 		/// <summary>
@@ -412,7 +476,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Newton
 		{
-			get { return new SI(this, fromUnit: "N", toUnit: "N"); }
+			get { return new SI(this, fromUnit: Unit.N, toUnit: Unit.N); }
 		}
 
 		/// <summary>
@@ -421,7 +485,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Watt
 		{
-			get { return new SI(this, fromUnit: "W", toUnit: "W"); }
+			get { return new SI(this, fromUnit: Unit.W, toUnit: Unit.W); }
 		}
 
 		/// <summary>
@@ -430,7 +494,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Meter
 		{
-			get { return new SI(this, fromUnit: "m", toUnit: "m"); }
+			get { return new SI(this, fromUnit: Unit.m, toUnit: Unit.m); }
 		}
 
 		/// <summary>
@@ -439,7 +503,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Second
 		{
-			get { return new SI(this, fromUnit: "s", toUnit: "s"); }
+			get { return new SI(this, fromUnit: Unit.s, toUnit: Unit.s); }
 		}
 
 		/// <summary>
@@ -453,7 +517,7 @@ namespace TUGraz.VectoCore.Utils
 
 		public SI GradientPercent
 		{
-			get { return new SI(this, factor: Math.Atan(Val) / Val, fromUnit: "%", toUnit: ""); }
+			get { return new SI(this, factor: Math.Atan(Val) / Val, fromUnit: Unit.Percent); }
 		}
 
 		/// <summary>
@@ -471,7 +535,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Hour
 		{
-			get { return new SI(this, 3600.0, "h", "s"); }
+			get { return new SI(this, 3600.0, Unit.h, Unit.s); }
 		}
 
 		/// <summary>
@@ -480,7 +544,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Minute
 		{
-			get { return new SI(this, 60.0, "min", "s"); }
+			get { return new SI(this, 60.0, Unit.min, Unit.s); }
 		}
 
 		/// <summary>
@@ -489,7 +553,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Kilo
 		{
-			get { return new SI(this, 1000.0, "k"); }
+			get { return new SI(this, 1000.0, Unit.k); }
 		}
 
 		/// <summary>
@@ -498,7 +562,7 @@ namespace TUGraz.VectoCore.Utils
 		[DebuggerHidden]
 		public SI Centi
 		{
-			get { return new SI(this, 1.0 / 100.0, "c"); }
+			get { return new SI(this, 1.0 / 100.0, Unit.c); }
 		}
 
 		#endregion
@@ -507,29 +571,34 @@ namespace TUGraz.VectoCore.Utils
 
 		public static SI operator +(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '+' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 
 			return new SI(si1.Val + si2.Val, si1.Numerator, si1.Denominator);
 		}
 
 		public static SI operator -(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
-
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '-' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 			return new SI(si1.Val - si2.Val, si1.Numerator, si1.Denominator);
 		}
 
 		public static SI operator *(SI si1, SI si2)
 		{
-			var numerator = si1.Numerator.Concat(si2.Numerator).Where(d => d != "rad");
-			var denominator = si1.Denominator.Concat(si2.Denominator).Where(d => d != "rad");
+			var numerator = si1.Numerator.Concat(si2.Numerator);
+			var denominator = si1.Denominator.Concat(si2.Denominator);
 			return new SI(si1.Val * si2.Val, numerator, denominator);
 		}
 
 		public static SI operator /(SI si1, SI si2)
 		{
-			var numerator = si1.Numerator.Concat(si2.Denominator).Where(d => d != "rad");
-			var denominator = si1.Denominator.Concat(si2.Numerator).Where(d => d != "rad");
+			var numerator = si1.Numerator.Concat(si2.Denominator);
+			var denominator = si1.Denominator.Concat(si2.Numerator);
 			return new SI(si1.Val / si2.Val, numerator, denominator);
 		}
 
@@ -580,25 +649,37 @@ namespace TUGraz.VectoCore.Utils
 
 		public static bool operator <(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '<' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 			return si1.Val < si2.Val;
 		}
 
 		public static bool operator >(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '>' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 			return si1.Val > si2.Val;
 		}
 
 		public static bool operator <=(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '<=' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 			return si1.Val <= si2.Val;
 		}
 
 		public static bool operator >=(SI si1, SI si2)
 		{
-			Contract.Requires(si1.HasEqualUnit(si2));
+			if (!si1.HasEqualUnit(si2)) {
+				throw new VectoException(
+					string.Format("Operator '>=' can only operate on SI Objects with the same unit. Got: {0} + {1}", si1, si2));
+			}
 			return si1.Val >= si2.Val;
 		}
 
@@ -678,6 +759,14 @@ namespace TUGraz.VectoCore.Utils
 			return string.Format("{0} [{1}]", Val, GetUnitString());
 		}
 
+		public virtual string ToString(string format)
+		{
+			if (string.IsNullOrEmpty(format)) {
+				format = "";
+			}
+			return string.Format("{0:" + format + "} [{2}]", Val, format, GetUnitString());
+		}
+
 		#endregion
 
 		#region Equality members
@@ -695,11 +784,6 @@ namespace TUGraz.VectoCore.Utils
 					ToBasicUnits().Numerator.OrderBy(x => x).SequenceEqual(si.ToBasicUnits().Numerator.OrderBy(x => x));
 		}
 
-		protected bool Equals(SI other)
-		{
-			return Val.Equals(other.Val) && HasEqualUnit(other);
-		}
-
 		public override bool Equals(object obj)
 		{
 			if (ReferenceEquals(null, obj)) {
@@ -709,12 +793,13 @@ namespace TUGraz.VectoCore.Utils
 				return true;
 			}
 			var other = obj as SI;
-			return other != null && Equals(other);
+			return other != null && Val.Equals(other.Val) && HasEqualUnit(other);
 		}
 
 		public override int GetHashCode()
 		{
 			unchecked {
+				// ReSharper disable once NonReadonlyMemberInGetHashCode
 				var hashCode = Val.GetHashCode();
 				hashCode = (hashCode * 397) ^ (Numerator != null ? Numerator.GetHashCode() : 0);
 				hashCode = (hashCode * 397) ^ (Denominator != null ? Denominator.GetHashCode() : 0);
@@ -722,13 +807,23 @@ namespace TUGraz.VectoCore.Utils
 			}
 		}
 
-		int IComparable.CompareTo(object obj)
+		public int CompareTo(object obj)
 		{
 			var si = (obj as SI);
-			if (si == null || this > si) {
+			if (si == null) {
+				return 1;
+			}
+
+			if (!HasEqualUnit(si)) {
+				if (si.Numerator.Length + si.Denominator.Length <= Numerator.Length + Denominator.Length) {
+					return -1;
+				}
 				return 1;
 			}
 
+			if (this > si) {
+				return 1;
+			}
 			return this < si ? -1 : 0;
 		}
 
diff --git a/VectoCoreTest/Utils/SITest.cs b/VectoCoreTest/Utils/SITest.cs
index 8f989c4539..94325d67b9 100644
--- a/VectoCoreTest/Utils/SITest.cs
+++ b/VectoCoreTest/Utils/SITest.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TUGraz.VectoCore.Exceptions;
 using TUGraz.VectoCore.Utils;
 
 namespace TUGraz.VectoCore.Tests.Utils
@@ -8,18 +9,95 @@ namespace TUGraz.VectoCore.Tests.Utils
 	[TestClass]
 	public class SITest
 	{
-		public static void AssertException<T>(Action func, string message) where T : Exception
+		/// <summary>
+		/// Assert an expected Exception.
+		/// </summary>
+		/// <typeparam name="T"></typeparam>
+		/// <param name="func"></param>
+		/// <param name="message"></param>
+		public static void AssertException<T>(Action func, string message = null) where T : Exception
 		{
 			try {
 				func();
-				Assert.Fail();
+				Assert.Fail("Expected Exception {0}, but no exception occured.", typeof (T));
 			} catch (T ex) {
-				Assert.AreEqual(message, ex.Message);
+				if (message != null) {
+					Assert.AreEqual(message, ex.Message);
+				}
 			}
 		}
 
+		public void SI_TypicalUsageTest()
+		{
+			//mult
+			var angularVelocity = 600.RPMtoRad();
+			var torque = 1500.SI<NewtonMeter>();
+			var power = angularVelocity * torque;
+			Assert.IsInstanceOfType(power, typeof (Watt));
+			Assert.AreEqual(600 * 1500, power.Double());
+
+			var siStandardMult = power * torque;
+			Assert.IsInstanceOfType(siStandardMult, typeof (SI));
+			Assert.AreEqual(600 * 1500 * 1500, siStandardMult.Double());
+			Assert.IsTrue(siStandardMult.HasEqualUnit(new SI().Watt.Newton.Meter));
+
+			//div
+			var torque2 = power / angularVelocity;
+			Assert.IsInstanceOfType(torque2, typeof (NewtonMeter));
+			Assert.AreEqual(1500, torque2.Double());
+
+			var siStandardDiv = power / power;
+			Assert.IsInstanceOfType(siStandardMult, typeof (SI));
+			Assert.IsTrue(siStandardDiv.HasEqualUnit(new SI()));
+			Assert.AreEqual(600 * 1500 * 1500, siStandardMult.Double());
+
+
+			//add
+			var angularVelocity2 = 400.SI<RoundsPerMinute>().Cast<PerSecond>();
+			var angVeloSum = angularVelocity + angularVelocity2;
+			Assert.IsInstanceOfType(angVeloSum, typeof (PerSecond));
+			Assert.AreEqual(400 + 600, angVeloSum.Double());
+			AssertException<VectoException>(() => { var x = 500.SI().Watt + 300.SI().Newton; });
+
+			//subtract
+			var angVeloDiff = angularVelocity - angularVelocity2;
+			Assert.IsInstanceOfType(angVeloDiff, typeof (PerSecond));
+			Assert.AreEqual(600 - 400, angVeloDiff.Double());
+
+			//general si unit
+			var generalSIUnit = 60000.SI().Gramm.Per.Kilo.Watt.Hour.ConvertTo().Kilo.Gramm.Per.Watt.Second;
+			Assert.IsInstanceOfType(generalSIUnit, typeof (SI));
+			Assert.AreEqual(1, generalSIUnit.Double());
+
+
+			//type conversion
+			var engineSpeed = 600;
+			var angularVelocity3 = engineSpeed.RPMtoRad();
+
+			// convert between units measures
+			var angularVelocity4 = engineSpeed.SI().Rounds.Per.Minute.ConvertTo().Radian.Per.Second;
+
+			// cast SI to specialized unit classes.
+			var angularVelocity5 = angularVelocity2.Cast<PerSecond>();
+			Assert.AreEqual(angularVelocity3, angularVelocity5);
+			Assert.AreEqual(angularVelocity3.Double(), angularVelocity4.Double());
+			Assert.IsInstanceOfType(angularVelocity3, typeof (PerSecond));
+			Assert.IsInstanceOfType(angularVelocity5, typeof (PerSecond));
+			Assert.IsInstanceOfType(angularVelocity4, typeof (SI));
+
+
+			// ConvertTo only allows conversion if the units are correct.
+			AssertException<VectoException>(() => { var x = 40.SI<Newton>().ConvertTo().Watt; });
+			var res1 = 40.SI<Newton>().ConvertTo().Newton;
+
+			// Cast only allows the cast if the units are correct.
+			AssertException<VectoException>(() => { var x = 40.SI().Newton.Cast<Watt>(); });
+			var res2 = 40.SI().Newton.Cast<Newton>();
+		}
+
+
 		[TestMethod]
-		public void TestSI()
+		public void SI_Test()
 		{
 			var si = new SI();
 			Assert.AreEqual(0.0, si.Double());
@@ -64,11 +142,15 @@ namespace TUGraz.VectoCore.Tests.Utils
 
 			var y = 2.SI();
 			Assert.AreEqual((2 * 5).SI(), y * x);
+
+			var percent = 10.SI<Radian>().ConvertTo().GradientPercent;
+			Assert.AreEqual(67.975.ToString("F3") + " [Percent]", percent.ToString("F3"));
+			Assert.AreEqual(67.975, percent.Double(), 0.001);
 		}
 
 		[TestMethod]
 		[SuppressMessage("ReSharper", "SuggestVarOrType_SimpleTypes")]
-		public void SITests_Addition_Subtraction()
+		public void SI_Test_Addition_Subtraction()
 		{
 			var v1 = 600.SI<NewtonMeter>();
 			var v2 = 455.SI<NewtonMeter>();
-- 
GitLab