As mentioned in the comments, degrees and rotations are a good example to avoid mixing up double values, especially between APIs.
I pulled out the Radians and Degrees classes we're currently using and here they are. Taking a look at them now (after so long) I want to clean them up (especially the comments/documentation) and make sure they're properly tested. Thankfully, I've managed to get time in the scheduling to do so. At any rate, use these at your own risk, I can't guarantee if all the math here is correct as I'm pretty sure we haven't actually used/tested all the functionality we wrote in.
Radians
///
/// Defines an angle in Radians
///
public struct Radians
{
public static readonly Radians ZERO_PI = 0;
public static readonly Radians ONE_PI = System.Math.PI;
public static readonly Radians TWO_PI = ONE_PI * 2;
public static readonly Radians HALF_PI = ONE_PI * 0.5;
public static readonly Radians QUARTER_PI = ONE_PI * 0.25;
#region Public Members
///
/// Angle value
///
public double Value;
///
/// Finds the Cosine of the angle
///
public double Cos
{
get
{
return System.Math.Cos(this);
}
}
///
/// Finds the Sine of the angle
///
public double Sin
{
get
{
return System.Math.Sin(this);
}
}
#endregion
///
/// Constructor
///
/// angle value in radians
public Radians(double value)
{
this.Value = value;
}
///
/// Gets the angle in degrees
///
/// Returns the angle in degrees
public Degrees GetDegrees()
{
return this;
}
public Radians Reduce()
{
double radian = this.Value;
bool IsNegative = radian < 0;
radian = System.Math.Abs(radian);
while (radian >= System.Math.PI * 2)
{
radian -= System.Math.PI * 2;
}
if (IsNegative && radian != 0)
{
radian = System.Math.PI * 2 - radian;
}
return radian;
}
#region operator overloading
///
/// Conversion of Degrees to Radians
///
///
///
public static implicit operator Radians(Degrees deg)
{
return new Radians(deg.Value * System.Math.PI / 180);
}
///
/// Conversion of integer to Radians
///
///
///
public static implicit operator Radians(int i)
{
return new Radians((double)i);
}
///
/// Conversion of float to Radians
///
///
///
public static implicit operator Radians(float f)
{
return new Radians((double)f);
}
///
/// Conversion of double to Radians
///
///
///
public static implicit operator Radians(double dbl)
{
return new Radians(dbl);
}
///
/// Conversion of Radians to double
///
///
///
public static implicit operator double(Radians rad)
{
return rad.Value;
}
///
/// Add Radians and a double
///
///
///
///
public static Radians operator +(Radians rad, double dbl)
{
return new Radians(rad.Value + dbl);
}
///
/// Add Radians to Radians
///
///
///
///
public static Radians operator +(Radians rad1, Radians rad2)
{
return new Radians(rad1.Value + rad2.Value);
}
///
/// Add Radians and Degrees
///
///
///
///
public static Radians operator +(Radians rad, Degrees deg)
{
return new Radians(rad.Value + deg.GetRadians().Value);
}
///
/// Sets Radians value negative
///
///
///
public static Radians operator -(Radians rad)
{
return new Radians(-rad.Value);
}
///
/// Subtracts a double from Radians
///
///
///
///
public static Radians operator -(Radians rad, double dbl)
{
return new Radians(rad.Value - dbl);
}
///
/// Subtracts Radians from Radians
///
///
///
///
public static Radians operator -(Radians rad1, Radians rad2)
{
return new Radians(rad1.Value - rad2.Value);
}
///
/// Subtracts Degrees from Radians
///
///
///
///
public static Radians operator -(Radians rad, Degrees deg)
{
return new Radians(rad.Value - deg.GetRadians().Value);
}
#endregion
public override string ToString()
{
return String.Format("{0}", this.Value);
}
public static Radians Convert(object value)
{
if (value is Radians)
return (Radians)value;
if (value is Degrees)
return (Degrees)value;
return System.Convert.ToDouble(value);
}
}
Degrees
public struct Degrees
{
public double Value;
public Degrees(double value) { this.Value = value; }
public Radians GetRadians()
{
return this;
}
public Degrees Reduce()
{
return this.GetRadians().Reduce();
}
public double Cos
{
get
{
return System.Math.Cos(this.GetRadians());
}
}
public double Sin
{
get
{
return System.Math.Sin(this.GetRadians());
}
}
#region operator overloading
public static implicit operator Degrees(Radians rad)
{
return new Degrees(rad.Value * 180 / System.Math.PI);
}
public static implicit operator Degrees(int i)
{
return new Degrees((double)i);
}
public static implicit operator Degrees(float f)
{
return new Degrees((double)f);
}
public static implicit operator Degrees(double d)
{
return new Degrees(d);
}
public static implicit operator double(Degrees deg)
{
return deg.Value;
}
public static Degrees operator +(Degrees deg, int i)
{
return new Degrees(deg.Value + i);
}
public static Degrees operator +(Degrees deg, double dbl)
{
return new Degrees(deg.Value + dbl);
}
public static Degrees operator +(Degrees deg1, Degrees deg2)
{
return new Degrees(deg1.Value + deg2.Value);
}
public static Degrees operator +(Degrees deg, Radians rad)
{
return new Degrees(deg.Value + rad.GetDegrees().Value);
}
public static Degrees operator -(Degrees deg)
{
return new Degrees(-deg.Value);
}
public static Degrees operator -(Degrees deg, int i)
{
return new Degrees(deg.Value - i);
}
public static Degrees operator -(Degrees deg, double dbl)
{
return new Degrees(deg.Value - dbl);
}
public static Degrees operator -(Degrees deg1, Degrees deg2)
{
return new Degrees(deg1.Value - deg2.Value);
}
public static Degrees operator -(Degrees deg, Radians rad)
{
return new Degrees(deg.Value - rad.GetDegrees().Value);
}
#endregion
public override string ToString()
{
return String.Format("{0}", this.Value);
}
public static Degrees Convert(object value)
{
if (value is Degrees)
return (Degrees)value;
if (value is Radians)
return (Radians)value;
return System.Convert.ToDouble(value);
}
}
Some sample usage
These really benefit when being used an an API. While, internally, your organization might decide to strictly stick with degrees or radians to avoid mixups, at least with these classes you can use the type that makes the most sense. For example, publicly consumed APIs or GUI APIs can use Degrees whereas your heavy math/trig or internal usage might use Radians. Considering the following classes/print function:
public class MyRadiansShape
{
public Radians Rotation { get; set; }
}
public class MyDegreesShape
{
public Degrees Rotation { get; set; }
}
public static void PrintRotation(Degrees degrees, Radians radians)
{
Console.WriteLine(String.Format("Degrees: {0}, Radians: {1}", degrees.Value, radians.Value));
}
Yeah, the code is pretty contrived (and terribly ambiguous) but that's OK! Just goes to show how it can help reduce accidental mixups.
var radiansShape = new MyRadiansShape() { Rotation = Math.PI / 2}; //prefer "Radians.HALF_PI" instead, but just as an example
var degreesShape = new MyDegreesShape() { Rotation = 90 };
PrintRotation(radiansShape.Rotation, radiansShape.Rotation);
PrintRotation(degreesShape.Rotation, degreesShape.Rotation);
PrintRotation(radiansShape.Rotation + degreesShape.Rotation, radiansShape.Rotation + degreesShape.Rotation);
//Degrees: 90, Radians: 1.5707963267949
//Degrees: 90, Radians: 1.5707963267949
//Degrees: 180, Radians: 3.14159265358979
Then they can be really useful for implementing other mathematical concepts based on angles, such as polar coordinates:
double distance = 5;
Polar polarCoordinate = new Polar(distance, (degreesShape.Rotation - radiansShape.Rotation) + Radians.QUARTER_PI);
Console.WriteLine("Polar Coordinate Angle: " + (Degrees)polarCoordinate.Angle); //because it's easier to read degrees!
//Polar Coordinate Angle: 45
Then finally, you could implement a Point2D class (or use the System.Windows.Point) with implicit conversions to/from Polar:
Point2D cartesianCoordinate = polarCoordinate;
Console.WriteLine(cartesianCoordinate.X + ", " + cartesianCoordinate.Y);
//3.53553390593274, 3.53553390593274
As I said, I want to take another pass at these classes, and probably eliminate the double implicit conversions to Radians to avoid a couple corner case mixups and compiler ambiguities that are possible. Those were actually there before we created the static ONE_PI, HALF_PI (and so on) fields and we were converting from some multiple of the Math.PI double.
EDIT: Here's the Polar class as a demonstration of additional implicit conversions. It takes advantage of the Radians class (and thus its implicit conversions) and the helper methods on it and the Point2D class. I haven't included it here, but the Polar class can easily implement operators interacting with the Point2D class but those aren't relevant for this discussion.
public struct Polar
{
public double Radius;
public Radians Angle;
public double X { get { return Radius * Angle.Cos; } }
public double Y { get { return Radius * Angle.Sin; } }
public Polar(double radius, Radians angle)
{
this.Radius = radius;
this.Angle = angle;
}
public Polar(Point2D point)
: this(point.Magnitude(), point.GetAngleFromOrigin())
{
}
public Polar(Point2D point, double radius)
: this(radius, point.GetAngleFromOrigin())
{
}
public Polar(Point2D point, Point2D origin)
: this(point - origin)
{
}
public Point2D ToCartesian()
{
return new Point2D(X, Y);
}
public static implicit operator Point2D(Polar polar)
{
return polar.ToCartesian();
}
public static implicit operator Polar(Point2D vector)
{
return new Polar(vector);
}
}