7
\$\begingroup\$

I am working on a project where we work with some angles. Some of the angles are stored as radians and some are stored as degrees. Storing everything as either radians or degrees is an undesirable option.

To fix the unit I am using a concept called newtype in functional programming languages. This stackoverflow question explained how to implement something similar in C#.

Here is the working code I have which is working perfectly.

// Some constants, may get them from another place in production code.
internal abstract class Constants {
    public const float RadiansPerCircle = 2.0f*(float) System.Math.PI;
    public const float DegreesPerCircle = 360.0f;
    public const float RadiansPerDegree = RadiansPerCircle/DegreesPerCircle;
    public const float DegreesPerRadian = DegreesPerCircle/RadiansPerCircle;
}

public interface IAngle {
    float AsRadians { get; }
    float AsDegrees { get; }
}

// A struct is used instead of a class because we want a very thin wrapper
// around a float value. In functional programming languages this is usually
// called a `newtype`.
public struct Radians : IAngle {
    // The actual value is stored in the radians property.
    public float AsRadians { get; }

    // The degrees are computer from the radians property every time it is
    // requested. If the degrees are requested more commonly than the
    // radians, the `Radians` value should be used to store the value
    // instead.
    public float AsDegrees { get { return AsRadians*Constants.DegreesPerRadian; } }

    // The constructor trusts that the passed in float is representing an
    // angle in radians.
    public Radians(float radians) { this.AsRadians = radians; }

    // A static factory method is used instead of a constructor because
    // constructors do not support generic parameters. If we would not use
    // generics and simply take an IAngle as a parameter, the value has to
    // be boxed which is not what we want.
    public static Radians from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new Radians(angle.AsRadians);
    }
}

public struct Degrees : IAngle {
    public float AsRadians { get { return AsDegrees*Constants.RadiansPerDegree; } }
    public float AsDegrees { get; }
    public Degrees(float degrees) { this.AsDegrees = degrees; }
    public static Degrees from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new Degrees(angle.AsDegrees);
    }
}

In some places we need the degrees or radians within the range of a full circle. In other words, we want to limit the degrees to between 0 and 360 or the radians to between 0 and 2 PI.

In order not to repeat the calculations we want to create a variant of the Radians type called WrappedRadians of which we know that the value is already limited to the domain of a full circle.

The code exploded:

// Some constants, may get them from another place in production code.
internal abstract class Constants {
    public const float RadiansPerCircle = 2.0f*(float) System.Math.PI;
    public const float DegreesPerCircle = 360.0f;
    public const float RadiansPerDegree = RadiansPerCircle/DegreesPerCircle;
    public const float DegreesPerRadian = DegreesPerCircle/RadiansPerCircle;
}

public interface IAngle {
    float AsRadians { get; }
    float AsWrappedRadians { get; }
    float AsDegrees { get; }
    float AsWrappedDegrees { get; }
}

public struct Radians : IAngle {
    public float AsRadians { get; }
    public float AsWrappedRadians { get { return AsRadians % Constants.RadiansPerCircle; } }
    public float AsDegrees { get { return AsRadians*Constants.DegreesPerRadian; } }
    public float AsWrappedDegrees { get { return AsWrappedRadians*Constants.DegreesPerRadian; } }
    public Radians(float AsRadians) { this.AsRadians = AsRadians; }
    public static Radians from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new Radians(angle.AsRadians);
    }
}

public struct WrappedRadians : IAngle {
    public float AsRadians { get { return AsWrappedRadians; } }
    public float AsWrappedRadians { get; }
    public float AsDegrees { get { return AsWrappedDegrees; } }
    public float AsWrappedDegrees { get { return AsWrappedRadians*Constants.DegreesPerRadian; } }
    public WrappedRadians(float AsRadians) { this.AsWrappedRadians = AsRadians % Constants.RadiansPerCircle; }
    public static WrappedRadians from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new WrappedRadians(angle.AsWrappedRadians);
    }
}

public struct Degrees : IAngle {
    public float AsRadians { get { return AsDegrees*Constants.RadiansPerDegree; } }
    public float AsWrappedRadians { get { return AsWrappedDegrees*Constants.RadiansPerDegree; } }
    public float AsDegrees { get; }
    public float AsWrappedDegrees { get { return AsDegrees % Constants.DegreesPerCircle; } } 
    public Degrees(float AsDegrees) { this.AsDegrees = AsDegrees; }
    public static Degrees from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new Degrees(angle.AsDegrees);
    }
}

public struct WrappedDegrees : IAngle {
    public float AsRadians { get { return AsWrappedRadians; } }
    public float AsWrappedRadians { get { return AsWrappedDegrees*Constants.DegreesPerRadian; } }
    public float AsDegrees { get { return AsWrappedDegrees; } }
    public float AsWrappedDegrees { get; }
    public WrappedDegrees(float AsDegrees) { this.AsWrappedDegrees = AsDegrees % Constants.DegreesPerCircle; }
    public static WrappedDegrees from<TAngle>(TAngle angle) where TAngle : IAngle {
        return new WrappedDegrees(angle.AsWrappedDegrees);
    }
}

While the implementation is pretty neat on one hand, there is a fair amount of duplication. That could be worked around if default method implementations, value type (struct) inheritance, or macros were a thing. Due to the duplication, the implementation leaves lots of room for error. It is easy to use the wrong constant or property by accident. Of course this can be mitigated by doing rigorous unit testing but that is not tackling the source of the problem.

Then, we discovered that we also wanted a type called Circles that stored the angle as a fraction of a circle. Of course we'd also want WrappedCircles. You can imagine the increase in code size.

Problem statement

We have values that are used in different units. To work with them we are looking for an implementation that satisfies the following requirements.

  1. The abstraction must be zero-cost.
  2. It must be possible to store the values in any unit.
  3. It must be possible to get a value in any unit, regardless of in which unit the value is stored.
  4. It must not be possible to do erroneous calculations because values are in different units.
  5. The amount of code duplication must be limited.

I realize whether or not the requirements are good ones is debatable. This is however not the subject of this question.

If we drop one of the requirements, the solution becomes trivial. For example, we can drop requirement 2 by simply implementing only the Radians struct from the examples. Please refrain from questioning the requirements and focus on how to implement something that satisfies all of them.

The code I present in the examples satisfy all but requirement 5. What would help in keeping the code size down would be:

  1. inheritance for structs
  2. default implementations of methods in interfaces
  3. macros

Unfortunately, it seems C# does not offer any of these. My question comes down to: am I missing some C# feature that could be used or is there a different implementation design?

I expect that there is no other C# feature that I missed and can be utilized. I expect that there might be another design which satisfies all the requirements, but I have not thought of it yet.


In response to the suggestion to utilize conversion operators

Conversion operators make the types nicer to work with but they do not solve the duplication problem. The following implementation shows what the conversion operators can do for you and how they relate to the previously given code.

internal abstract class Constants {
    public const float RadiansPerRevolution = (float) (2*System.Math.PI);
    public const float RevolutionsPerRadian = 1/RadiansPerRevolution;
    public const float DegreesPerRevolution = 360;
    public const float RevolutionsPerDegree = 1/DegreesPerRevolution;
    public const float RadiansPerDegree = RadiansPerRevolution/DegreesPerRevolution;
    public const float DegreesPerRadian = DegreesPerRevolution/RadiansPerRevolution;
}

public interface IAngularUnit {
    float InDegrees { get; }
    float InRadians { get; }
    float InRevolutions { get; }
}

public struct Degrees : IAngularUnit {
    public float InDegrees { get; }
    public float InRadians { get { return Constants.RadiansPerDegree*InDegrees; } }
    public float InRevolutions { get { return Constants.RevolutionsPerDegree*InDegrees; } }
    private Degrees(float degrees) { InDegrees = degrees; }
    public static implicit operator Degrees(float value) { return new Degrees(value); }
    public static explicit operator float(Degrees degrees) { return degrees.InDegrees; }
    public static Degrees From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InDegrees; }
    public static implicit operator Degrees(Radians radians) { return Degrees.From(radians); }
    public static implicit operator Degrees(Revolutions revolutions) { return Degrees.From(revolutions); }
}

public struct Radians : IAngularUnit {
    public float InDegrees { get { return Constants.DegreesPerRadian*InRadians; } }
    public float InRadians { get; }
    public float InRevolutions { get { return Constants.RevolutionsPerRadian*InRadians; } }
    private Radians(float radians) { InRadians = radians; }
    public static implicit operator Radians(float value) { return new Radians(value); }
    public static explicit operator float(Radians radians) { return radians.InRadians; }
    public static Radians From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InRadians; }
    public static implicit operator Radians(Degrees degrees) { return Radians.From(degrees); }
    public static implicit operator Radians(Revolutions revolutions) { return Radians.From(revolutions); }
}

public struct Revolutions : IAngularUnit {
    public float InRevolutions { get; }
    public float InDegrees { get { return Constants.DegreesPerRevolution*InRevolutions; } }
    public float InRadians { get { return Constants.RadiansPerRevolution*InRevolutions; } }
    private Revolutions(float revolutions) { InRevolutions = revolutions; }
    public static implicit operator Revolutions(float revolutions) { return new Revolutions(revolutions); }
    public static explicit operator float(Revolutions revolutions) { return revolutions.InRevolutions; }
    public static Revolutions From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InRevolutions; }
    public static implicit operator Revolutions(Degrees degrees) { return Revolutions.From(degrees); }
    public static implicit operator Revolutions(Radians radians) { return Revolutions.From(radians); }
}

public class Entry {
    public static void Main() {
        Radians r = 3.14f;
        Degrees d = 359;

        // These conversions lose information on the unit, therefore
        // explicit casts are required.
        float rf = (float) r;
        float df = (float) d;

        // These should convert between units without losing information.
        Degrees r_as_d = r;
        Radians d_as_r = d;

        // These all work, but the overload variant requires a duplicated
        // implementation and the boxed variant boxes the angle.
        PrintInRadiansOverload(r);
        PrintInRadiansOverload(d);
        PrintInRadians(r);
        PrintInRadians(d);
        PrintInRadiansBoxed(r);
        PrintInRadiansBoxed(d);
    }

    public static void PrintInRadiansOverload(Radians radians) {
        System.Console.WriteLine("{0:F2} rad", (float) radians);
    }

    public static void PrintInRadiansOverload(Degrees degrees) {
        // Error prone because of duplication.
        System.Console.WriteLine("{0:F2} rad", (float) (Radians) degrees);
    }

    public static void PrintInRadians<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit {
        System.Console.WriteLine("{0:F2} rad", angle.InRadians);
    }

    public static void PrintInRadiansBoxed(IAngularUnit angle) {
        System.Console.WriteLine("{0:F2} rad", angle.InRadians);
    }
}

Note that I chose to define the conversions in the InRadians. It is also possible to define the conversions in the conversion operators like so.

public struct Degrees : IAngularUnit {
    public float InDegrees { get; }
    public float InRadians { get { return (float) (Radians) this; } }
    public float InRevolutions { get { return (float) (Revolutions) this; } }
    private Degrees(float degrees) { InDegrees = degrees; }
    public static implicit operator Degrees(float value) { return new Degrees(value); }
    public static explicit operator float(Degrees degrees) { return degrees.InDegrees; }
    public static Degrees From<TAngularUnit>(TAngularUnit angle) where TAngularUnit : IAngularUnit { return angle.InDegrees; }
    public static implicit operator Degrees(Radians radians) { return Constants.DegreesPerRadian*(float) radians; }
    public static implicit operator Degrees(Revolutions revolutions) { return Constants.DegreesPerRevolution*(float) revolutions; }
}

As long as the chosen implementation is consistent between the units, the implementations are equivalent.

\$\endgroup\$
2
  • \$\begingroup\$ In response to the suggestion to utilize conversion operators please put this in a self-answer. Do not add an improved version of the code after receiving an answer. Including revised versions of the code makes the question confusing, especially if someone later reviews the newer code. Otherwise I (or someone else) will have to revert it ;-] please read What should I do when someone answers my question? \$\endgroup\$
    – t3chb0t
    Commented Jun 16, 2017 at 7:37
  • \$\begingroup\$ @t3chb0t I do not have an answer. I could only extend my question to explain why conversion operators do not solve it. See meta.stackoverflow.com/questions/350766/…. \$\endgroup\$
    – Mick
    Commented Jun 16, 2017 at 10:59

3 Answers 3

6
\$\begingroup\$

I expect that there is no other C# feature that I missed and can be utilized.

I would never say that unless I really knew the entire specification and all the books about C# by heart.

There is indeed one feature that you miss, the implicit operator. With it you can make it virtually bullet-proof, very easy to use and test. Just add the operators to the types that you want to be able to convert from and to like:

class Radian : Angle
{
    public Radian(double value) : base(value) {}
    public static implicit operator Degree(Radian angle) => new Degree(3d);
    public static implicit operator double(Radian angle) => angle.Value;
}

class Degree : Angle
{
    public Degree(double value)  : base(value) {}
    public static implicit operator Radian(Degree angle) => new Radian(3d);
    public static implicit operator double(Degree angle) => angle.Value;
}

Finally tell the method that requires some angle the exact type it should take. For example radian. With each type having one more implicit operator double you don't even have to use the Value property.

public void CalculateSomething(Radian radian)
{
    var result = 2.0d * radian;
}

or use Angle everywhere and cast it to the target type inside the formula:

public void CalculateSomething2(Angle angle)
{
    var result = 2.0d * (Radian)angle;
}

but I find the first version better with the actual type.

This way you won't make any mistakes and the values will magically convert to the requried type.

You could also add one more operator to Radian and Degree to convert them to the other type and use in your formulas as exactly those types instead of just double.

As you have two more cases for WrappedRadian/Degree you'll need a few more operators to covert these ones too.


To reduce the repetitions each angle type could have helper methods like this one to implement each conversion only once. Mainly the operators would use them.

class Radian : Angle
{
    public static Radian FromDegree(double degree) { .. }
}
\$\endgroup\$
8
  • \$\begingroup\$ Thank you for the suggestion. I have a feeling this will not reduce the duplication unless the conversions are transitive. So if I make an implicit operator from Radians to Circles, and from Circles to Degrees, Will Radians r = new Degrees(60) work? Also, I am not sure that this is zero cost. Taking an IAngle as a parameter might box the value. To prevent this you would have to use generics. However using generics in the interface may prove problematic. \$\endgroup\$
    – Mick
    Commented Jun 15, 2017 at 11:52
  • \$\begingroup\$ I have to think about it but it might be better to use explicit operators to cast to float or double. Perhaps it is better not to provide them at all because you lose the unit information; you could do float a = new Radians(3.14); Degrees d = a; which is what we try to prevent by using newtypes in the first place. \$\endgroup\$
    – Mick
    Commented Jun 15, 2017 at 11:57
  • \$\begingroup\$ @Mick there is no zero-cost solution, only when you use double everywhere. If you convert the angles you have to calculate something and this costs a few ticks. As I've said. You don't use the interface at all. It might be helpful for some operations but not as an argument. You cannot use it for operators so you'll need to use actual types which this is all about. You don't want to make mistakes so you need to use something concrete and those types are exactly that. Whether they are or should be transitive is your choice. I don't know all the possible use cases and requirements. \$\endgroup\$
    – t3chb0t
    Commented Jun 15, 2017 at 11:57
  • \$\begingroup\$ @Mick explicit operators are for lossy conversions like double to float or long to int, but converting a float to double or int to long does not get cut so implicit operators are ok. I used double but if you have floats then yes, they should be explicit to indicate a possible precision loss. \$\endgroup\$
    – t3chb0t
    Commented Jun 15, 2017 at 12:00
  • \$\begingroup\$ The implementation provided in the examples is zero-cost. Meaning that there is no overhead for using the Degree or Radian types over float. Obviously converting between units requires computation. Check out PrintInRadians and PrintInRadiansBoxed gist.github.com/mickvangelderen/… for an example of how to use the interface. \$\endgroup\$
    – Mick
    Commented Jun 15, 2017 at 12:11
3
\$\begingroup\$

Creating multiple types for implementing different representations of the same type is probably not the best option. A simple solution is, to create one type that uses internally one of the available representations (it's rad in the code below). Further more, the type should provides methods for creating a value from any of the available representations and converting the value back to any of the representations:

public struct Angle : IComparable, IComparable<Angle>, IEquatable<Angle>
{
    private const float RadiansPerCircle = 2.0f * (float)System.Math.PI;
    private const float DegreesPerCircle = 360.0f;
    private const float RadiansPerDegree = RadiansPerCircle / DegreesPerCircle;

    private readonly float _radian;
    private Angle(float radian)
    {
        _radian = radian;
    }
    public float AsDegree => _radian / RadiansPerDegree;
    public float AsRadian => _radian;

    public static Angle FromRadian(float radian) => new Angle(radian);
    public static Angle FromDegrees(float degrees) => new Angle(degrees * RadiansPerDegree);

    public Angle Normalize()
    {
        var normalized = _radian % RadiansPerCircle;
        if (normalized < 0)
            normalized = RadiansPerCircle - normalized;
        return Angle.FromRadian(normalized);
    }

    // mathematical operators
    public static Angle operator +(Angle angle1, Angle angle2)
        => Angle.FromRadian(angle1._radian + angle2._radian);
    public static Angle operator -(Angle angle1, Angle angle2)
        => Angle.FromRadian(angle1._radian - angle2._radian);

    // ...

    // interface implementations
    public int CompareTo(Angle other) => _radian.CompareTo(other._radian);
    public bool Equals(Angle other) => _radian.Equals(other._radian);
    public int CompareTo(object obj) => obj is Angle
        ? _radian.CompareTo(((Angle)obj)._radian)
        : -1;

    // Equals / GetHashCode implementaion
    public override bool Equals(object obj) => obj is Angle && Equals((Angle)obj);
    public override int GetHashCode() => _radian.GetHashCode();

    public override string ToString() => $"{_radian} rad";
}

Update

AdrianoRepetti mentioned in comments, that the solution above may result in rounding errors when adding two angles in degree for example. That is a valid point, so the code below stores the values as well as its unit as internal representation. That allows to avoid that kind of inaccuracy. The API remains the same (except that the precision changed from float to double):

public struct Angle : IComparable, IComparable<Angle>, IEquatable<Angle>
{
    private const double RadiansPerCircle = 2.0f * (float)System.Math.PI;
    private const double DegreesPerCircle = 360.0f;
    private const double RadiansPerDegree = RadiansPerCircle / DegreesPerCircle;    

    private readonly double _value;
    private readonly Unit _unit;
    private readonly UnitHandler _handler;

    private enum Unit { Degree, Radian }

    private abstract class UnitHandler
    {
        public abstract double ConvertTo(double value, Unit unit);
        public abstract Angle Normalize(double value);
        public abstract Unit Unit { get;}

        public static UnitHandler[] All { get; } = new UnitHandler[]
        {
            new DegreeUnitHandler(),
            new RadianUnitHandler(),
        };
    }

    private class DegreeUnitHandler : UnitHandler
    {
        public override double ConvertTo(double value, Unit unit)
        {
            switch (unit)
            {
                case Unit.Degree:
                    return value;
                case Unit.Radian:
                    return value / RadiansPerDegree;
                default:
                    throw new NotImplementedException($"Case for type '{unit}' is not implemented.");
            }
        }
        public override Angle Normalize(double value)
        {
            var normalized = value % DegreesPerCircle;
            if (normalized < 0)
                normalized = DegreesPerCircle + normalized;
            return new Angle(normalized, Unit.Degree);
        }
        public override Unit Unit => Unit.Degree;
    }

    private class RadianUnitHandler : UnitHandler
    {
        public override double ConvertTo(double value, Unit unit)
        {
            switch (unit)
            {
                case Unit.Degree:
                return value * RadiansPerDegree;
            case Unit.Radian:
                return value;
            default:
                    throw new NotImplementedException($"Case for type '{unit}' is not implemented.");
            }
        }
        public override Angle Normalize(double value)
        {
            var normalized = value % DegreesPerCircle;
            if (normalized < 0)
                normalized = DegreesPerCircle + normalized;
            return new Angle(normalized, Unit.Degree);
        }
        public override Unit Unit => Unit.Radian;
    }

    private Angle(double value, Unit unit)
    {
        _value = value;
        _unit = unit;
        _handler = UnitHandler.All.Single(h => h.Unit == unit);
    }

    public double AsRadian => _handler.ConvertTo(_value, Unit.Radian);
    public double AsDegree => _handler.ConvertTo(_value, Unit.Degree);

    public static Angle FromRadian(float radian) => new Angle(radian, Unit.Radian);
    public static Angle FromDegrees(float degrees) => new Angle(degrees, Unit.Degree);

    public Angle Normalize() => _handler.Normalize(_value);

    private static TResult Combine<TResult>(Angle angle1, Angle angle2, Func<double, double, Unit, TResult> operation)
    {
        var targetUnit = angle1._unit;
        var value1 = angle1._value;
        var value2 = angle1._unit == angle2._unit
            ? angle2._value
            : angle2._handler.ConvertTo(angle2._value, targetUnit);                 
        return operation(value1, value2, targetUnit);
    }

    // mathematical operators
    public static Angle operator +(Angle angle1, Angle angle2)
        => Combine(angle1, angle2, (v1, v2, unit) => new Angle(v1 + v2, unit));

    public static Angle operator -(Angle angle1, Angle angle2)
        => Combine(angle1, angle2, (v1, v2, unit) => new Angle(v1 - v2, unit));

    // ...

    // interface implementations
    public int CompareTo(Angle other) => Combine(this, other, (v1, v2, unit) => v1.CompareTo(v2));
    public bool Equals(Angle other) => Combine(this, other, (v1, v2, unit) => v1.Equals(v2));
    public int CompareTo(object obj) => obj is Angle
        ? Combine(this, (Angle)obj, (v1, v2, unit) => v1.CompareTo(v2))
        : -1;

    // Equals / GetHashCode implementaion
    public override bool Equals(object obj) => obj is Angle && Equals((Angle)obj);
    public override int GetHashCode() => _value.GetHashCode() ^ _unit.GetHashCode();

    public override string ToString() => $"{_value} {_unit}";
}
\$\endgroup\$
17
  • \$\begingroup\$ Hmmm if this is only for presentation purposes (UI) then I'd agree but I'm not sure it's best solution for calculations. Floating point conversions will introduce unexpected numerical errors, even just summing two angles in degrees. Original unit of measure must be preserved to avoid unnecessary conversions (but hey, yes...you may use an enum...) \$\endgroup\$ Commented Jun 20, 2017 at 13:58
  • \$\begingroup\$ Good point. That's true for the single case when calculating mathematical operations of 2 angles in degrees. However, as soon as angles from different representations are combine, that is the case anyway. However, if that is really a problem, an improvement could be to store the value + it's unit internally and convert the values only if needed. (Further more, it makes sense to use doubles instead of float then ;). That is IMHO still better than creating one type for each unit and provide lots of conversion logic.... \$\endgroup\$
    – JanDotNet
    Commented Jun 20, 2017 at 14:17
  • 2
    \$\begingroup\$ This is the the best approach, IMHO. Writing business logic, that supports various angle formats is madness. Addition is easy, but good luck supporting 3 different implementations of Atan. OP should convert all input values to single format (radians) and do all the calculations using it. Imagine if there was differentDateTime implementation for every possible way to represent time... \$\endgroup\$
    – Nikita B
    Commented Jun 20, 2017 at 14:19
  • \$\begingroup\$ Yes, when you combine multiple values with different units then things become pretty tricky. Well, see my comments on Mick's answer: if you combine unit + value then you have a, let' say, quantity. In this case derived classes just define conversion to/from a reference unit (I'd say SI unit, probably). Not much code and pretty easy to extend. + (and others) operator will just convert to SI if units mismatch otherwise just add without precision loss. \$\endgroup\$ Commented Jun 20, 2017 at 14:24
  • 1
    \$\begingroup\$ @AdrianoRepetti you are not considering the error of the measurement itself. In my experience even the most accurate sensors have errors that are way, way larger than rounding error from any conversion (especially if you use double or decimal). A very strong case should be made to justify the accuracy, that requires multiple angle implementation. For now I'm just not seeing the real-life scenario where this is necessary. \$\endgroup\$
    – Nikita B
    Commented Jun 20, 2017 at 14:53
-5
\$\begingroup\$

The solution I ended up going with is essentially macros. This piece of code generates the C# code for three angular units between which can be converted by multiplication by a constant.

  1. It uses structs around a single float property. If the C# compiler is sophisticated enough it should be a zero-cost abstraction. The interface should probably only be used as a generic parameter constraint, unless the compiler can figure out a way to not box the values.
  2. It is possible to store values in any unit.
  3. Implicit conversions between the units are defined. This makes working with different units very nice.
  4. Arithmetic, comparison and conversion operators are only defined if they make sense. For example it is not possible to multiply two angles together.
  5. It is only possible to perform erroneous calculations if you explicitly cast a unit to a float first.
  6. The code duplication is limited in the code generation code. There seems to be no way around the duplication in the generated code.

This is the generated code.

namespace Units.Angles {

    public abstract class Constants {
        public const float RadiansPerRevolution = (float) (2*System.Math.PI);
        public const float RevolutionsPerRadian = 1/RadiansPerRevolution;
        public const float DegreesPerRevolution = 360;
        public const float RevolutionsPerDegree = 1/DegreesPerRevolution;
        public const float RadiansPerDegree = RadiansPerRevolution/DegreesPerRevolution;
        public const float DegreesPerRadian = DegreesPerRevolution/RadiansPerRevolution;
    }

    public interface IAngularUnit {
        Degrees ToDegrees { get; }
        Radians ToRadians { get; }
        Revolutions ToRevolutions { get; }
    }

    public struct Degrees : IAngularUnit {
        private float value { get; }
        public Degrees ToDegrees { get { return this; } }
        public Radians ToRadians { get { return this; } }
        public Revolutions ToRevolutions { get { return this; } }
        private Degrees(float degrees) { this.value = degrees; }
        public static implicit operator Degrees(float degrees) { return new Degrees(degrees); }
        public static explicit operator float(Degrees degrees) { return degrees.value; }
        public static implicit operator Degrees(Radians radians) { return ((float) radians)*Constants.DegreesPerRadian; }
        public static implicit operator Degrees(Revolutions revolutions) { return ((float) revolutions)*Constants.DegreesPerRevolution; }
        public static Degrees operator +(Degrees degrees) { return +degrees.value; }
        public static Degrees operator -(Degrees degrees) { return -degrees.value; }
        public static Degrees operator +(Degrees lhs, Degrees rhs) { return lhs.value + rhs.value; }
        public static Degrees operator -(Degrees lhs, Degrees rhs) { return lhs.value - rhs.value; }
        public static Degrees operator *(Degrees lhs, float rhs) { return lhs.value*rhs; }
        public static Degrees operator *(float lhs, Degrees rhs) { return lhs*rhs.value; }
        public static Degrees operator /(Degrees lhs, float rhs) { return lhs.value/rhs; }
        public static Degrees operator /(float lhs, Degrees rhs) { return lhs/rhs.value; }
        public static Degrees operator %(Degrees lhs, Degrees rhs) { return lhs.value % rhs.value; }
        public static bool operator ==(Degrees lhs, Degrees rhs) { return lhs.value == rhs.value; }
        public static bool operator !=(Degrees lhs, Degrees rhs) { return lhs.value != rhs.value; }
        public override bool Equals(object obj) { return obj is Degrees && this == (Degrees) obj; }
        public override int GetHashCode() { return value.GetHashCode(); }
        public static bool operator >(Degrees lhs, Degrees rhs) { return lhs.value > rhs.value; }
        public static bool operator <(Degrees lhs, Degrees rhs) { return lhs.value < rhs.value; }
        public static bool operator >=(Degrees lhs, Degrees rhs) { return lhs.value >= rhs.value; }
        public static bool operator <=(Degrees lhs, Degrees rhs) { return lhs.value <= rhs.value; }
    }

    public struct Radians : IAngularUnit {
        private float value { get; }
        public Degrees ToDegrees { get { return this; } }
        public Radians ToRadians { get { return this; } }
        public Revolutions ToRevolutions { get { return this; } }
        private Radians(float radians) { this.value = radians; }
        public static implicit operator Radians(float radians) { return new Radians(radians); }
        public static explicit operator float(Radians radians) { return radians.value; }
        public static implicit operator Radians(Degrees degrees) { return ((float) degrees)*Constants.RadiansPerDegree; }
        public static implicit operator Radians(Revolutions revolutions) { return ((float) revolutions)*Constants.RadiansPerRevolution; }
        public static Radians operator +(Radians radians) { return +radians.value; }
        public static Radians operator -(Radians radians) { return -radians.value; }
        public static Radians operator +(Radians lhs, Radians rhs) { return lhs.value + rhs.value; }
        public static Radians operator -(Radians lhs, Radians rhs) { return lhs.value - rhs.value; }
        public static Radians operator *(Radians lhs, float rhs) { return lhs.value*rhs; }
        public static Radians operator *(float lhs, Radians rhs) { return lhs*rhs.value; }
        public static Radians operator /(Radians lhs, float rhs) { return lhs.value/rhs; }
        public static Radians operator /(float lhs, Radians rhs) { return lhs/rhs.value; }
        public static Radians operator %(Radians lhs, Radians rhs) { return lhs.value % rhs.value; }
        public static bool operator ==(Radians lhs, Radians rhs) { return lhs.value == rhs.value; }
        public static bool operator !=(Radians lhs, Radians rhs) { return lhs.value != rhs.value; }
        public override bool Equals(object obj) { return obj is Radians && this == (Radians) obj; }
        public override int GetHashCode() { return value.GetHashCode(); }
        public static bool operator >(Radians lhs, Radians rhs) { return lhs.value > rhs.value; }
        public static bool operator <(Radians lhs, Radians rhs) { return lhs.value < rhs.value; }
        public static bool operator >=(Radians lhs, Radians rhs) { return lhs.value >= rhs.value; }
        public static bool operator <=(Radians lhs, Radians rhs) { return lhs.value <= rhs.value; }
    }

    public struct Revolutions : IAngularUnit {
        private float value { get; }
        public Degrees ToDegrees { get { return this; } }
        public Radians ToRadians { get { return this; } }
        public Revolutions ToRevolutions { get { return this; } }
        private Revolutions(float revolutions) { this.value = revolutions; }
        public static implicit operator Revolutions(float revolutions) { return new Revolutions(revolutions); }
        public static explicit operator float(Revolutions revolutions) { return revolutions.value; }
        public static implicit operator Revolutions(Degrees degrees) { return ((float) degrees)*Constants.RevolutionsPerDegree; }
        public static implicit operator Revolutions(Radians radians) { return ((float) radians)*Constants.RevolutionsPerRadian; }
        public static Revolutions operator +(Revolutions revolutions) { return +revolutions.value; }
        public static Revolutions operator -(Revolutions revolutions) { return -revolutions.value; }
        public static Revolutions operator +(Revolutions lhs, Revolutions rhs) { return lhs.value + rhs.value; }
        public static Revolutions operator -(Revolutions lhs, Revolutions rhs) { return lhs.value - rhs.value; }
        public static Revolutions operator *(Revolutions lhs, float rhs) { return lhs.value*rhs; }
        public static Revolutions operator *(float lhs, Revolutions rhs) { return lhs*rhs.value; }
        public static Revolutions operator /(Revolutions lhs, float rhs) { return lhs.value/rhs; }
        public static Revolutions operator /(float lhs, Revolutions rhs) { return lhs/rhs.value; }
        public static Revolutions operator %(Revolutions lhs, Revolutions rhs) { return lhs.value % rhs.value; }
        public static bool operator ==(Revolutions lhs, Revolutions rhs) { return lhs.value == rhs.value; }
        public static bool operator !=(Revolutions lhs, Revolutions rhs) { return lhs.value != rhs.value; }
        public override bool Equals(object obj) { return obj is Revolutions && this == (Revolutions) obj; }
        public override int GetHashCode() { return value.GetHashCode(); }
        public static bool operator >(Revolutions lhs, Revolutions rhs) { return lhs.value > rhs.value; }
        public static bool operator <(Revolutions lhs, Revolutions rhs) { return lhs.value < rhs.value; }
        public static bool operator >=(Revolutions lhs, Revolutions rhs) { return lhs.value >= rhs.value; }
        public static bool operator <=(Revolutions lhs, Revolutions rhs) { return lhs.value <= rhs.value; }
    }
}

This is the code to generate the above code. It would be better to generate it using a syntax tree but the time investment wasn't worth it for me.

using System.Collections.Generic;
using static System.Console;

namespace GenerateUnits {
    struct Unit {
        public string ClassName;
        public string ArgumentName => ClassName.ToLower();
        public Dictionary<string, string> ClassNameToConversionConstant;
    }

    class Program {
        static void Main(string[] args) {
            Unit[] units = new Unit[] {
                new Unit {
                    ClassName = "Degrees",
                    ClassNameToConversionConstant = new Dictionary<string, string>() {
                        { "Radians", "DegreesPerRadian" },
                        { "Revolutions", "DegreesPerRevolution" },
                    },
                },
                new Unit {
                    ClassName = "Radians",
                    ClassNameToConversionConstant = new Dictionary<string, string>() {
                        { "Degrees", "RadiansPerDegree" },
                        { "Revolutions", "RadiansPerRevolution" },
                    },
                },
                new Unit {
                    ClassName = "Revolutions",
                    ClassNameToConversionConstant = new Dictionary<string, string>() {
                        { "Degrees", "RevolutionsPerDegree" },
                        { "Radians", "RevolutionsPerRadian" },
                    },
                },
            };
            Write(
                "namespace Units.Angles {\n" +
                "\n" +
                "    public abstract class Constants {\n" +
                "        public const float RadiansPerRevolution = (float) (2*System.Math.PI);\n" + 
                "        public const float RevolutionsPerRadian = 1/RadiansPerRevolution;\n" + 
                "        public const float DegreesPerRevolution = 360;\n" + 
                "        public const float RevolutionsPerDegree = 1/DegreesPerRevolution;\n" + 
                "        public const float RadiansPerDegree = RadiansPerRevolution/DegreesPerRevolution;\n" + 
                "        public const float DegreesPerRadian = DegreesPerRevolution/RadiansPerRevolution;\n" + 
                "    }\n" + 
                "\n" +
                "    public interface IAngularUnit {\n" + 
                "        Degrees ToDegrees { get; }\n" + 
                "        Radians ToRadians { get; }\n" + 
                "        Revolutions ToRevolutions { get; }\n" + 
                "    }\n"
            );
            foreach (Unit unit in units) {
                WriteLine();
                WriteLine("    public struct {0} : IAngularUnit {{", unit.ClassName);
                WriteLine("        private float value { get; }");
                // Interface implementation.
                foreach (Unit innerUnit in units) {
                    WriteLine("        public {0} To{0} {{ get {{ return this; }} }}", innerUnit.ClassName);
                }
                // Constructor and conversion between floats.
                WriteLine("        private {0}(float {1}) {{ this.value = {1}; }}", unit.ClassName, unit.ArgumentName);
                WriteLine("        public static implicit operator {0}(float {1}) {{ return new {0}({1}); }}", unit.ClassName, unit.ArgumentName);
                WriteLine("        public static explicit operator float({0} {1}) {{ return {1}.value; }}", unit.ClassName, unit.ArgumentName);
                // Conversion operators from other units.
                foreach (Unit otherUnit in units) {
                    if (unit.ClassName == otherUnit.ClassName) continue;
                    string conversionConstant;
                    if (!unit.ClassNameToConversionConstant.TryGetValue(otherUnit.ClassName, out conversionConstant)) {
                        throw new System.Exception(string.Format("No conversion constant between {0} and {1}", unit.ClassName, otherUnit.ClassName));
                    }
                    WriteLine("        public static implicit operator {0}({1} {2}) {{ return ((float) {2})*Constants.{3}; }}", unit.ClassName, otherUnit.ClassName, otherUnit.ArgumentName, conversionConstant);
                }
                Write(
                    // Arithmethic operators.
                    "        public static {0} operator +({0} {1}) {{ return +{1}.value; }}\n" +
                    "        public static {0} operator -({0} {1}) {{ return -{1}.value; }}\n" +
                    "        public static {0} operator +({0} lhs, {0} rhs) {{ return lhs.value + rhs.value; }}\n" +
                    "        public static {0} operator -({0} lhs, {0} rhs) {{ return lhs.value - rhs.value; }}\n" +
                    "        public static {0} operator *({0} lhs, float rhs) {{ return lhs.value*rhs; }}\n" +
                    "        public static {0} operator *(float lhs, {0} rhs) {{ return lhs*rhs.value; }}\n" +
                    "        public static {0} operator /({0} lhs, float rhs) {{ return lhs.value/rhs; }}\n" +
                    "        public static {0} operator /(float lhs, {0} rhs) {{ return lhs/rhs.value; }}\n" +
                    "        public static {0} operator %({0} lhs, {0} rhs) {{ return lhs.value % rhs.value; }}\n" +
                    // Comparison operators.
                    "        public static bool operator ==({0} lhs, {0} rhs) {{ return lhs.value == rhs.value; }}\n" +
                    "        public static bool operator !=({0} lhs, {0} rhs) {{ return lhs.value != rhs.value; }}\n" +
                    "        public override bool Equals(object obj) {{ return obj is {0} && this == ({0}) obj; }}\n" +
                    "        public override int GetHashCode() {{ return value.GetHashCode(); }}\n" +
                    "        public static bool operator >({0} lhs, {0} rhs) {{ return lhs.value > rhs.value; }}\n" +
                    "        public static bool operator <({0} lhs, {0} rhs) {{ return lhs.value < rhs.value; }}\n" +
                    "        public static bool operator >=({0} lhs, {0} rhs) {{ return lhs.value >= rhs.value; }}\n" +
                    "        public static bool operator <=({0} lhs, {0} rhs) {{ return lhs.value <= rhs.value; }}\n"
                    , unit.ClassName, unit.ArgumentName
                );
                WriteLine("    }");
            }
            WriteLine("}");
        }
    }
}
\$\endgroup\$
9
  • \$\begingroup\$ Conversion operators make the types easier to work with. They do not address the duplication problem. In fact the duplication only got worse. My solution is to generate the source code. That is not what t3chb0t suggested. \$\endgroup\$
    – Mick
    Commented Jun 20, 2017 at 11:37
  • \$\begingroup\$ To avoid duplication I'd rework it little bit more introducing the concept of "unit of measure" and the concept of "quantity" (which is a value plus its unit of measure). Derived classes are pretty short and all the conversions are handled in the base class, without duplication (note that you can provide custom conversion or rely on a common denominator like SI units, each derived class has to expose ToSi() and FromSi() and nothing else). It's little bit more tricky to keep numerical precision but it is worth. However If you keep working with code generation I'd take a look to T4 templates \$\endgroup\$ Commented Jun 20, 2017 at 11:49
  • \$\begingroup\$ The quantity implementation is pretty easy to extend introducing new units of measures, multiplies and combinations (if/when you will require them). For example you have Meter which can be combined with Second to give velocity (which is a combine unit). Units may be grouped in families. A velocity might be converted from m/s to km/h to...you understand what I mean. \$\endgroup\$ Commented Jun 20, 2017 at 11:53
  • \$\begingroup\$ About T4: they're somehow similar to what you're doing (generating code) but they can be compiled together with your code, no need to write an external program to generate class to include for you. \$\endgroup\$ Commented Jun 20, 2017 at 11:56
  • \$\begingroup\$ @AdrianoRepetti Thank you for the suggestions. I have considered classes but wanted structs (value types) because I am using the units in a performance critical application. I have considered the common denominator pattern but was worried, like you mentioned, about the precision. Unfortunately T4 does not seem to be available for .NET Core. I found Scripty which might help but I consider the current code generation approach good enough. Again I really do appreciate your thoughtful comments. \$\endgroup\$
    – Mick
    Commented Jun 20, 2017 at 11:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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