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)Degrees
Minutes
Seconds
,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
Degrees
types be equal (implemented internally) if we convert fromDegrees
toRadians
and 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");
}
}