I’m developing a generic Range
class in C# so that I can create comparison ranges. The idea is that I can replace code like this:
int MinValue = 22; int MaxValue = 42; ... if (val >= 22 && val < 42) { }
with this:
Range<int> MyRange = new BoundedRange( 22, RangeBoundType.Inclusive, 42, RangeBoundType.Exclusive); ... if (MyRange.Contains(val)) { }
In isolation it doesn’t look like a win but if you’re doing that range comparison in multiple places or working with multiple ranges, some of which can have an open-ended upper or lower bound, or bounds that can be inclusive or exclusive, working with a Range
type can save a lot of trouble.
Before we go on, let me introduce a little bit of notation.
First, the characters ‘[‘ and ‘]’ mean “inclusive”, and the characters ‘(‘ and ‘)’ mean “exclusive.” So, assuming we’re working with integral values, the range [1,30) means that the numbers 1 through 29 are in the range. Similarly, (1, 30] means that the numbers 2 through 30 are in the range.
To specify “unboundedness,” I use the character ‘*’. So the range [*,255] means all numbers less than or equal to 255. And, of course, the range [*,*] encompasses the entire range of values for a particular type. By the way, inclusive and exclusive are irrelevant when unboundedness is specified. That is, [*,255] and (*,255] are identical ranges.
A Range
type also allows me to do things like compare ranges to see if they overlap, if one contains the other, etc. And therein lies a problem. Consider the two byte ranges [0,*] and [*,255].
It should be clear that, since the range of a byte is 0 through 255 (inclusive), the two ranges are identical. The first range has a lower bound of 0, inclusive, and is unbounded on the upper end. The second range has defined bounds of 0 and 255. As programmers, we can tell by inspection that the two ranges are identical.
Unfortunately, there’s no way to write generic code that can determine that the ranges are equivalent. Although the primitive types available in C# all have defined maximum and minimum values (Int32.MaxValue
and Int32.MinValue
, for example), not all types do. And since I might want to have ranges of strings or other types that don’t have defined bounds, there’s no general way to tell the difference between a range that has no defined upper bound and a range whose upper bound is the maximum possible value for that type. If I wanted that ability, the only way would be to pass maximum and minimum possible values for each type to the Range
constructor–something that I’m not willing to do and might not even be possible. What’s the maximum value for a string?
The inability to determine that an unbounded range is equivalent to a bounded range makes for some interesting problems. For example, consider the two byte ranges [1,*] and [0,255]. It’s obvious to programmers that the second range contains the first range. But how would you write a function to determine that?
public bool Contains(Range<T> other) { // returns true if the current range contains the other range }
Since there’s no way to convert that unbounded value into a number, the code can only assume that the upper bound of the first range is larger than the upper bound of the second range. Therefore, Contains
will return false
.
Whether that’s an error is a matter of opinion. Logically, having Contains
return false
in this case is the correct thing to do because “unbounded” is larger than any number. But it seems like an error in the context of types like byte
that have limited ranges, because there’s really no such thing as an unbounded byte
. Rather, byte
has a maximum value of 255 and one would expect the code to know that.
As I said, it’d be possible to extend the Range
class so that it knows the minimum and maximum values for the primitive types, and give clients the ability to add that information for their own types if they want. That way, an unbounded upper value for an integer would get Int32.MaxValue
. The code would then “do the right thing” for the built-in types, and clients could define minimum and maximum values for their own types if they wanted to.
But I wonder if it’s worth going to all that trouble. Is it more sensible to explain the rules to programmers and expect them to define specific bounds (i.e. not use unbounded) if they think they’ll be working with ranges (as opposed to comparing values against ranges) of primitive types?