Range Kata

I have recently come across the Range kata. In this post I dive into a variation of this kata using integer numbers. One implementation is A Range kata implementation in C# by Mark Seemann focuses on property testing and comparing Haskell, F# and C# implementations.

The referenced post has chosen using Church encoding as the implementation which results in a more complicated code in C#. In this post I will focus on building on the recently added features of .NET 8: IBinaryInteger<T>.

My test cases cover the samples described by the kata. Although I don't find these test cases extensive, they give a good enough starting point. That said, do not expect the code below to handle edge-cases that has no test case detailed in the kata.

This implementation utilizes the following relatively new C# features:

  • record struct types

  • numerics with IBinaryInteger<T>

public record struct Range<T> where T : IBinaryInteger<T>
{
    public T Start { get; }
    public T End { get; }

    public Range(Endpoint<T> start, Endpoint<T> end)
        => (Start, End) = start.AsIncStart <= end.AsIncEnd ? (start.AsIncStart, end.AsIncEnd) : throw new ArgumentException();

    public bool IsInRange(T value) => Start <= value && End >= value;

    public bool Contains(Range<T> range) => IsInRange(range.Start) && IsInRange(range.End);

    public bool Overlaps(Range<T> range) => IsInRange(range.Start) || IsInRange(range.End) || range.IsInRange(Start) || range.IsInRange(End);

    public IEnumerable<T> GetAllPoints()
    {
        T current = Start;
        while (current <= End)
            yield return current++;
    }
}

public record struct Endpoint<T>(T Value, bool Inclusive) where T : IBinaryInteger<T>
{
    public T AsIncStart => Inclusive ? Value : Value + T.One;
    public T AsIncEnd => Inclusive ? Value : Value - T.One;
}

Both Range<T> and Endpoint<T> types are generic, so any IBinaryInteger<T> implementation could be used as a type parameter. This includes byte, int, short, long.

While the IBinaryInteger<T> restriction could be loosened to support floating values via INumber<T>, it would require a different implementation of Endpoint<T> type and further clarification on the response of the GetAllPoints method.

The IBinaryInteger<T> provides the necessary abstractions with static abstract methods. In this example comparison (<= and >= operators), addition operation and One value are used.

Another choice is using record struct instead of class. The primary benefit of this is having a default implementation for ToString() and equality comparison. This also gives value semantics to the type. Note, that Range<T> is immutable but Endpoint<T> is not. This is not a problem as Endpoint<T> is only required for the construction of a range, alternatively one could implement it as an extension method or a static helper method as well.

Finally, Endpoint<T> uses the record struct types with primary constructor like feature, allowing to define its properties with the type definition.

Using these features the resulted code ends up being 26 lines long, making it fairly compact compared to other implementations. Certainly not the most compact, but my goal was to maintain a good readability along the way.