12
\$\begingroup\$

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 from Degrees to Radians and back to Degrees? 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");
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Are you asking us what your system should be able to do? \$\endgroup\$ – Nikita B Jan 16 '14 at 8:35
  • \$\begingroup\$ given nature properties, conversion rules of angle representations. What constraints, properties should type system have ? What is wrong right now in implementation? \$\endgroup\$ – diimdeep Jan 16 '14 at 8:51
13
\$\begingroup\$

I don't think you should treat the units as disjoint entities between which you convert. You have started your post by saying

Angle can be represented using following types

But actually what you meant is "units".

So the entity you are trying to measure is an angle. And code using it should, for most parts, not care what unit it represents - just that it represents a specific angle. Expressing it as a value in a specific unit is only really necessary for calculations in algorithms which require the angle to be of a specific unit, serialization, display, etc.

Therefore I suggest a different design: You have an Angle type which represents a specific angle. Choose whichever unit you like best as internal representation of it. The type then exposes methods to create an Angle from various value in specific units and convert them to such. Something along these lines:

struct Angle
{
    private double _Degrees; // I chose degrees as internal representation for an angle but as other have pointed out Radians might be better

    private Angle(double degrees)
    {
        _Degrees = degrees;
    }

    public static Angle FromDegrees(double degrees)
    {
        return new Angle(Normalize(degrees)); // Ensure angles are in [0-360[
    }

    public static Angle FromRadians(double radians)
    {
        return new Angle(RadiansToDegrees(radians));
    }

    public static Angle FromDegreesMinutesSeconds(DegreesMinutesSeconds dms)
    {
        return new Angle(DegreesMinutesSecondsToDegrees(dms));
    }

    public double AsDegrees()
    {
        return _Degrees;
    }

    public double AsRadians()
    {
        return DegreesToRadians(_Degrees);
    }

    public DegreesMinutesSeconds AsDegreesMinutesSeconds()
    {
        return DegreesToDegreesMinutesSeconds(_Degrees);
    }
} 

Consider overloading ==, !=. You could probably also overload <, <=, >= and >. Although you might have to consider questions like: Is 359 degrees greater or smaller than 5 degrees?

Override Equals and GetHashCode is you overload == (check Microsoft guidelines).

Overloading + and - (Angle + Angle, Angle + scalar, Angle - Angle, Angle - scalar) as well as * and / (Angle * scalar, Angle / scalar) makes for nicer calculations.

\$\endgroup\$
  • 2
    \$\begingroup\$ How would angle + scalar behave? I think that doesn't make much sense. \$\endgroup\$ – svick Jan 16 '14 at 11:53
  • \$\begingroup\$ As @svick noted above, Angle + scalar does not make sense unless it is perfectly obvious that the "default" unit of an Angle is for example degrees. Also, you chose degrees as the internal representation unit for the Angle - I would chose to use radians instead (the preferred SI unit for generic angles). Otherwise, this is pretty much the approach that I would have used. \$\endgroup\$ – Boise Jan 23 '14 at 8:18
  • \$\begingroup\$ @svick good point \$\endgroup\$ – ChrisWue Jan 23 '14 at 8:57
  • 1
    \$\begingroup\$ This is quite similar to TimeSpan in .NET. You can ask "how many minutes" or "how many seconds" it has, or conversely create a new one with TimeSpan.FromSeconds(), or TimeSpan.FromMinutes(). Internally, it is represented by Ticks, whatever that might be :o) \$\endgroup\$ – heltonbiker Dec 16 '14 at 12:40
  • \$\begingroup\$ Angle / Angle which returns a scalar is also another operator you may wish to implement. \$\endgroup\$ – Lukazoid Jan 18 '16 at 17:14
8
\$\begingroup\$

I'd consider a simpler, single class:

class Angle
{
    // The 'native' type, passed to System.Math methods
    public double Radians { get; set;}
    // Factory methods
    public static Angle FromRadians(double d) { ... }
    public static Angle FromDegrees(double d) { ... }
    public static Angle FromDegrees(int degrees, int minutes, double seconds) { ... }
    // Display methods
    public string ToRadians(int precision) { ... }
    public string ToDegrees(int precision) { ... }
}

I imagine that ToDegrees returns a string which you can display on a UI.

The difference between my answer and @ChrisWue's are:

  • I use radians as the internal/native unit, because they're what are expected by System.Math methods
  • I avoid returning degrees as a number (I return them as a string), to discourage users from performing calculations using degree -> radian -> degree conversions.

For your DMS type, perhaps "degrees" and "minutes" should be integer (not double) types.

\$\endgroup\$
  • 8
    \$\begingroup\$ That awkward moment when one realizes that you and @ChrisWue are two different persons (conspiracy theories aside) :) \$\endgroup\$ – Nikita B Jan 16 '14 at 10:34
7
\$\begingroup\$

Your implementation will be really hard to maintain as you keep adding different operations. Also the fact that you have multiple classes to represent single entity is kind of weird.

For example, look at DateTime implementation. It represents time in various formats via exposed properties and formattable ToString and Parse methods. But when it comes to manipulating DateTime values - it essentially comes down to manipulating Ticks (which is just one of the possible representations--the easiest one to deal with). I think you should do the same. Create a single Angle class, expose different representation via properties (Radians, Minutes, Seconds, Degrees, etc.), but when it comes to calculations - deal with radians representation only.

\$\endgroup\$
2
\$\begingroup\$

I see only one reason for having different representation. And that's precision errors. Degrees and radians probably will have different offsets when represented in double. But since almost any trigonometry operations will use radians there is not so much gain. You can just keep degrees and other reps temporary and/or in storage to show same value user entered, but eventually they will be converted to radians.

Consider radians to be angle value without any annotation and assumed by default for double that comes as angle. And just create value types that will be converted whenever you need work.

struct Degree {
    public double Value;
    public Degree(double value) : Value(value) {}
    public double AsRadians { get { return /* to radians */; } }
    public static Degree Radians(double d) { return /* from radians */; }
    public static implicit operator double(Degree d) { return d.AsRadians; }
    public static implicit operator Degree(double d) { return Radians(d); }
    public static explicit operator Degree(double d) { return Radians(d); }
    /* any operation require conversion to radians */
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.