Hello, I am Laszlo

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.