NpgsqlRange improvements
See original GitHub issueThe problem
Working with NpgsqlRange
I’ve found that the following use cases are a big cumbersome to work with:
- Modifying an existing range
- Infinite values
- Nullable values
Modifying an existing range
Often times I’ve found that you need to reduce or extend an existing range and I’ve solved it with:
var oldRange = ...;
var newRange = new NpgsqlRange<T>(
oldRange.LowerBound, oldRange.LowerBoundIsInclusive, oldRange.LowerBoundIsInfinite,
newUpperBound, ..., ...);
This copies three values (the actual bound value, whether it’s inclusive, whether it’s infinite). In general any operation when you really need to have one of the bounds in a single variable is difficult to work with due to the fact that you have to carry over the extra 2 satellite values.
Infinite values
Postgres documentation specifies that:
The lower bound of a range can be omitted, meaning that all points less than the upper bound are included in the range. Likewise, if the upper bound of the range is omitted, then all points greater than the lower bound are included in the range. If both lower and upper bounds are omitted, all values of the element type are considered to be in the range.
This is equivalent to considering that the lower bound is “minus infinity”, or the upper bound is “plus infinity”, respectively. But note that these infinite values are never values of the range’s element type, and can never be part of the range. (So there is no such thing as an inclusive infinite bound — if you try to write one, it will automatically be converted to an exclusive bound.)
(emphasis mine)
This implies that, for example, new NpgsqlRange<T>(default(T), true, true, ...)
and new NpgsqlRange<T>(default(T), false, true)
is exactly the same thing and represents a range with an open left bound.
The main problem with the current interface is that in order for you to specify an infinite bound on either side of the range you need to also specify two extra values (the value and the inclusiveness) which don’t make much sense.
Nullable types
I think that in the context of a NpgsqlRange<T>
, T
should always be non nullable and null
should always be allowed as a value for either side of the range. This greatly helps when you have to assemble a range based on two different nullable values.
For example:
T? from = ...;
T? to = ...;
Currently in order to construct a range I would need to write:
new NpgsqlRange<T>(
from.GetValueOrDefault(), ..., !from.HasValue,
to.GetValueOrDefault(), ..., !to.HasValue);
which, as mentioned above, is rather redundant.
Proposed solution
Considering the above issues I propose that we store bounds in appropriate data structures so that their shape can be easily constructed and passed around.
Example implementation
public class Bound<T>
where T : struct // see below (*1)
{
public T Value { get; set; }
public bool IsInclusive { get; set; }
public bool IsInfinite { get; set; }
public static Bound<T> Include(T? bound)
{
return new Bound<T>
{
Value = bound.GetValueOrDefault(),
IsInclusive = bound.HasValue,
IsInfinite = !bound.HasValue,
};
}
public static Bound<T> Exclude(T? bound)
{
return new Bound<T>
{
Value = bound.GetValueOrDefault(),
IsInclusive = false,
IsInfinite = !bound.HasValue,
};
}
public static Bound<T> Open()
{
return new Bound<T>
{
Value = default(T),
IsInclusive = false,
IsInfinite = true,
};
}
}
In addition to this we would need to extend NpgsqlRange
with:
- A constructor that accepts two
Bound<T>
s - Two getters that return the corresponding
Lower
andUpper
bounds asBound<T>
s
*(1) I’m relatively new to C# and I’d like to support reference types, but I can’t seem to find a way to define that while also allowing T?
. I believe in C# 8 (with a specific enabled extension) this is a non-issue given that they “fixed” default nullability for reference types.
public class Bound<T>
where T : struct // see below (*1)
{
public static Bound<T> Open()
{
return new Bound<T>
{
Value = bound.GetValueOrDefault(),
IsInclusive = false,
IsInfinite = !bound.HasValue,
};
}
...
}
Usage examples:
In order to modify an existing range you could have:
var oldRange = ...;
var newRange = new NpgsqlRange<T>(oldRange.Lower, Bound<T>.Include(newUpperBound));
For open bounds:
new NpgsqlRange<T>(Bound<T>.Open(), Bound<T>.Exclude(upperBound));
For nullable types:
T? from = ...;
T? to = ...;
new NpgsqlRange<T>(Bound<T>.Include(from), Bound<T>.Exclude(to));
Issue Analytics
- State:
- Created 5 years ago
- Reactions:1
- Comments:11 (7 by maintainers)
I’m sorry if I came across as overly negative on this… Part of the discussion above was complicated with nullability questions and C# 8.0, struct vs. class, etc.
Again, I don’t think C# 8 introduces any changes that affect this (namely, the possibility to have a non-constrained generic type and to use T? within it). Regarding introducing something like Bound<T>, even if I personally don’t see a big need, I think if a full API proposal is submitted we’d definitely consider it - I really do mean that. This doesn’t mean we’d necessarily accept it - it could be argued that instantiating two Bound<T> instances to construct an NpgsqlRange<T> would in fact be more verbose/complicated than the current 6-param constructor, which is why we need a full proposal with a comparison to the current API, etc.
@austindrenski I’d appreciate it, thanks. 😄