I want to implement a Type system for different representations of an angle value. Motivation to implement this as a type system comes from this question.
Angle can be represented using the following types:
Degrees(45.5)DegreesMinutesSeconds,DMS(45*30'00")Radians(0.7941)
It should support conversions between types (like Convert.ToInt32(someDouble)), and from system types (to/from double).
- What properties should this type system have?
- Should
Degreestypes be equal (implemented internally) if we convert fromDegreestoRadiansand back toDegrees? Or it should be compared as doubles with Epsilon value outside? - In what cases should explicit and implicit conversion operators be used, keeping in mind value precision loss on conversion?
Here is the work in progress:
// in current implementation this test will fail [Test] public void DegreesToRadiansToDegreesEquivalence() { Degree initialDegree = 0; for (int i = 0; initialDegree < 360; initialDegree += 1, i++) { Radian convertedToRadian = (Radian) initialDegree; Degree resultDegree = (Degree) convertedToRadian; Assert.AreEqual(initialDegree, resultDegree, Double.Epsilon, String.Format("{0} {1}", i, (double)convertedToRadian)); } } public struct Degree { private readonly double _value; // Construct Degree from Double public Degree(Double value) { _value = value; } // Converts Degree to Double private Double ToDouble() { return _value; } // Implicitly Degree -> Double public static implicit operator Degree(Double value) { return new Degree(value); } // Implicitly Degree -> Double public static implicit operator Double(Degree d) { return d.ToDouble(); } // Explicitly Degree -> Radian public static explicit operator Degree(Radian r) { return ConvertAngle.ToDegree(r); } // Explicitly Degree -> Radian public static explicit operator Radian(Degree d) { return ConvertAngle.ToRadian(d); } } // todo better to preserve sign in all fields because otherwise it would be impossible to represent 0°00'-20" // store sign separate ? easy to implement operators - + public struct DMS : IEquatable<DMS> { public readonly double Degrees; public readonly double Minutes; public readonly double Seconds; public DMS(double degree, double minute, double second) { Degrees = Math.Floor(degree); Minutes = Math.Abs(Math.Floor(minute)); Seconds = Math.Abs(second); } public bool Equals(DMS other) { return (Math.Abs(other.Degrees - this.Degrees) < double.Epsilon) && (Math.Abs(other.Minutes - this.Minutes) < double.Epsilon) && (Math.Abs(other.Seconds - this.Seconds) < double.Epsilon); } } public struct Radian { private readonly double _value; // Construct Degree from Double public Radian(Double value) { _value = value; } // Converts Degree to Double private Double ToDouble() { return _value; } // Implicitly Double -> Radian public static implicit operator Radian(Double value) { return new Radian(value); } // Implicitly Radian -> Double public static implicit operator Double(Radian d) { return d.ToDouble(); } } public enum AngleFormat { Degrees, DegreesMinutes, DegreesMinutesSeconds, Radians } public static class ConvertAngle { public static Degree ToDegree(Radian radians) { return radians * 180.0 / Math.PI; } public static Degree ToDegree(DMS dms) { // todo seems sign problem here return dms.Degrees + dms.Minutes / 60 + dms.Seconds / 3600; } public static Radian ToRadian(Degree degrees) { return degrees * Math.PI / 180.0; } public static Radian ToRadian(DMS dms) { return ToRadian((Degree)(dms.Degrees + dms.Minutes / 60 + dms.Seconds / 3600)); } public static DMS ToDMS(Radian radian) { return ToDMS(ToDegree(radian)); } public static DMS ToDMS(Degree degree) { double degrees = Math.Floor(degree); double rem = (degree - degrees) * 60.0; double minutes = Math.Floor(rem); double seconds = (rem - minutes) * 60.0; return new DMS(degrees, minutes, seconds); } public static string ToString(Radian radian, AngleFormat format, int precision) { if (format == AngleFormat.Radians) { string formStr = "{0:F" + precision + "}"; return String.Format(formStr, radian); } else if (format == AngleFormat.Degrees || format == AngleFormat.DegreesMinutes || format == AngleFormat.DegreesMinutesSeconds) { return ToString((Degree)radian, format, precision); } throw new NotImplementedException(); return ""; } public static string ToString(Degree degree, AngleFormat format, int precision) { DMS dms = ToDMS(degree); switch (format) { // todo use precision case AngleFormat.Degrees: case AngleFormat.DegreesMinutes: case AngleFormat.DegreesMinutesSeconds: return ToString(ToDMS(degree), format, precision); case AngleFormat.Radians: return ToString((Radian)degree, format, precision); } throw new NotImplementedException(); return ""; } public static string ToString(DMS dms, AngleFormat format, int precision) { switch (format) { // todo here do we need to combine min and sec ??? or we need diffrent format option case AngleFormat.Degrees: return String.Format("{0:D}°", dms.Degrees); case AngleFormat.DegreesMinutes: return String.Format("{0:D}°{1:D}'", dms.Degrees, dms.Minutes); case AngleFormat.DegreesMinutesSeconds: string secondsPrecisionFormat = "F" + Math.Abs(precision).ToString("D"); //string d = dms.Degrees.ToString(""); //CultureInfo currentCulture = CultureInfo.CurrentCulture; string stringFormat = "{0:F0}° {1:F0}' {2:" + secondsPrecisionFormat + "}\""; return String.Format(stringFormat, dms.Degrees, dms.Minutes, dms.Seconds); case AngleFormat.Radians: // todo convert to radians ?? break; } throw new ArgumentOutOfRangeException("format"); } }