using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
// ReSharper disable once CheckNamespace
namespace NpgsqlTypes;
///
/// Represents a PostgreSQL range type.
///
/// The element type of the values in the range.
///
/// See: https://www.postgresql.org/docs/current/static/rangetypes.html
///
public readonly struct NpgsqlRange : IEquatable>
{
// -----------------------------------------------------------------------------------------------
// Regarding bitwise flag checks via @roji:
//
// > Note that Flags.HasFlag() used to be very inefficient compared to simply doing the
// > bit operation - this is why I've always avoided it. .NET Core 2.1 adds JIT intrinstics
// > for this, making Enum.HasFlag() fast, but I honestly don't see the value over just doing
// > a bitwise and operation, which would also be fast under .NET Core 2.0 and .NET Framework.
//
// See:
// - https://github.com/npgsql/npgsql/pull/1939#pullrequestreview-121308396
// - https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performance-improvements-in-net-core-2-1
// -----------------------------------------------------------------------------------------------
///
/// Defined by PostgreSQL to represent an empty range.
///
const string EmptyLiteral = "empty";
///
/// Defined by PostgreSQL to represent an infinite lower bound.
/// Some element types may have specific handling for this value distinct from a missing or null value.
///
const string LowerInfinityLiteral = "-infinity";
///
/// Defined by PostgreSQL to represent an infinite upper bound.
/// Some element types may have specific handling for this value distinct from a missing or null value.
///
const string UpperInfinityLiteral = "infinity";
///
/// Defined by PostgreSQL to represent an null bound.
/// Some element types may have specific handling for this value distinct from an infinite or missing value.
///
const string NullLiteral = "null";
///
/// Defined by PostgreSQL to represent a lower inclusive bound.
///
const char LowerInclusiveBound = '[';
///
/// Defined by PostgreSQL to represent a lower exclusive bound.
///
const char LowerExclusiveBound = '(';
///
/// Defined by PostgreSQL to represent an upper inclusive bound.
///
const char UpperInclusiveBound = ']';
///
/// Defined by PostgreSQL to represent an upper exclusive bound.
///
const char UpperExclusiveBound = ')';
///
/// Defined by PostgreSQL to separate the values for the upper and lower bounds.
///
const char BoundSeparator = ',';
///
/// The used by to convert bounds into .
///
static TypeConverter? BoundConverter;
///
/// True if implements ; otherwise, false.
///
static readonly bool HasEquatableBounds = typeof(IEquatable).IsAssignableFrom(typeof(T));
///
/// Represents the empty range. This field is read-only.
///
public static readonly NpgsqlRange Empty = new(default, default, RangeFlags.Empty);
///
/// The lower bound of the range. Only valid when is false.
///
[MaybeNull, AllowNull]
public T LowerBound { get; }
///
/// The upper bound of the range. Only valid when is false.
///
[MaybeNull, AllowNull]
public T UpperBound { get; }
///
/// The characteristics of the boundaries.
///
internal readonly RangeFlags Flags;
///
/// True if the lower bound is part of the range (i.e. inclusive); otherwise, false.
///
public bool LowerBoundIsInclusive => (Flags & RangeFlags.LowerBoundInclusive) != 0;
///
/// True if the upper bound is part of the range (i.e. inclusive); otherwise, false.
///
public bool UpperBoundIsInclusive => (Flags & RangeFlags.UpperBoundInclusive) != 0;
///
/// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false.
///
public bool LowerBoundInfinite => (Flags & RangeFlags.LowerBoundInfinite) != 0;
///
/// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false.
///
public bool UpperBoundInfinite => (Flags & RangeFlags.UpperBoundInfinite) != 0;
///
/// True if the range is empty; otherwise, false.
///
public bool IsEmpty => (Flags & RangeFlags.Empty) != 0;
///
/// Constructs an with inclusive and definite bounds.
///
/// The lower bound of the range.
/// The upper bound of the range.
public NpgsqlRange([AllowNull] T lowerBound, [AllowNull] T upperBound)
: this(lowerBound, true, false, upperBound, true, false) { }
///
/// Constructs an with definite bounds.
///
/// The lower bound of the range.
/// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false.
/// The upper bound of the range.
/// True if the upper bound is part of the range (i.e. inclusive); otherwise, false.
public NpgsqlRange(
[AllowNull] T lowerBound, bool lowerBoundIsInclusive,
[AllowNull] T upperBound, bool upperBoundIsInclusive)
: this(lowerBound, lowerBoundIsInclusive, false, upperBound, upperBoundIsInclusive, false) { }
///
/// Constructs an .
///
/// The lower bound of the range.
/// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false.
/// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false.
/// The upper bound of the range.
/// True if the upper bound is part of the range (i.e. inclusive); otherwise, false.
/// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false.
public NpgsqlRange(
[AllowNull] T lowerBound, bool lowerBoundIsInclusive, bool lowerBoundInfinite,
[AllowNull] T upperBound, bool upperBoundIsInclusive, bool upperBoundInfinite)
: this(
lowerBound,
upperBound,
EvaluateBoundaryFlags(
lowerBoundIsInclusive,
upperBoundIsInclusive,
lowerBoundInfinite,
upperBoundInfinite)) { }
///
/// Constructs an .
///
/// The lower bound of the range.
/// The upper bound of the range.
/// The characteristics of the range boundaries.
internal NpgsqlRange([AllowNull] T lowerBound, [AllowNull] T upperBound, RangeFlags flags) : this()
{
// TODO: We need to check if the bounds are implicitly empty. E.g. '(1,1)' or '(0,0]'.
// See: https://github.com/npgsql/npgsql/issues/1943.
LowerBound = (flags & RangeFlags.LowerBoundInfinite) != 0 ? default : lowerBound;
UpperBound = (flags & RangeFlags.UpperBoundInfinite) != 0 ? default : upperBound;
Flags = flags;
if (IsEmptyRange(LowerBound, UpperBound, Flags))
{
LowerBound = default!;
UpperBound = default!;
Flags = RangeFlags.Empty;
}
}
///
/// Attempts to determine if the range is malformed or implicitly empty.
///
/// The lower bound of the range.
/// The upper bound of the range.
/// The characteristics of the range boundaries.
///
/// True if the range is implicitly empty; otherwise, false.
///
static bool IsEmptyRange([AllowNull] T lowerBound, [AllowNull] T upperBound, RangeFlags flags)
{
// ---------------------------------------------------------------------------------
// We only want to check for those conditions that are unambiguously erroneous:
// 1. The bounds must not be default values (including null).
// 2. The bounds must be definite (non-infinite).
// 3. The bounds must be inclusive.
// 4. The bounds must be considered equal.
//
// See:
// - https://github.com/npgsql/npgsql/pull/1939
// - https://github.com/npgsql/npgsql/issues/1943
// ---------------------------------------------------------------------------------
if ((flags & RangeFlags.Empty) == RangeFlags.Empty)
return true;
if ((flags & RangeFlags.Infinite) == RangeFlags.Infinite)
return false;
if ((flags & RangeFlags.Inclusive) == RangeFlags.Inclusive)
return false;
if (lowerBound is null || upperBound is null)
return false;
if (!HasEquatableBounds)
return lowerBound?.Equals(upperBound) ?? false;
var lower = (IEquatable)lowerBound;
var upper = (IEquatable)upperBound;
return !lower.Equals(default!) && !upper.Equals(default!) && lower.Equals(upperBound);
}
///
/// Evaluates the boundary flags.
///
/// True if the lower bound is is part of the range (i.e. inclusive); otherwise, false.
/// True if the lower bound is indefinite (i.e. infinite or unbounded); otherwise, false.
/// True if the upper bound is part of the range (i.e. inclusive); otherwise, false.
/// True if the upper bound is indefinite (i.e. infinite or unbounded); otherwise, false.
///
/// The boundary characteristics.
///
static RangeFlags EvaluateBoundaryFlags(bool lowerBoundIsInclusive, bool upperBoundIsInclusive, bool lowerBoundInfinite, bool upperBoundInfinite)
{
var result = RangeFlags.None;
// This is the only place flags are calculated.
if (lowerBoundIsInclusive)
result |= RangeFlags.LowerBoundInclusive;
if (upperBoundIsInclusive)
result |= RangeFlags.UpperBoundInclusive;
if (lowerBoundInfinite)
result |= RangeFlags.LowerBoundInfinite;
if (upperBoundInfinite)
result |= RangeFlags.UpperBoundInfinite;
// PostgreSQL automatically converts inclusive-infinities.
// See: https://www.postgresql.org/docs/current/static/rangetypes.html#RANGETYPES-INFINITE
if ((result & RangeFlags.LowerInclusiveInfinite) == RangeFlags.LowerInclusiveInfinite)
result &= ~RangeFlags.LowerBoundInclusive;
if ((result & RangeFlags.UpperInclusiveInfinite) == RangeFlags.UpperInclusiveInfinite)
result &= ~RangeFlags.UpperBoundInclusive;
return result;
}
///
/// Indicates whether the on the left is equal to the on the right.
///
/// The on the left.
/// The on the right.
///
/// True if the on the left is equal to the on the right; otherwise, false.
///
public static bool operator ==(NpgsqlRange x, NpgsqlRange y) => x.Equals(y);
///
/// Indicates whether the on the left is not equal to the on the right.
///
/// The on the left.
/// The on the right.
///
/// True if the on the left is not equal to the on the right; otherwise, false.
///
public static bool operator !=(NpgsqlRange x, NpgsqlRange y) => !x.Equals(y);
///
public override bool Equals(object? o) => o is NpgsqlRange range && Equals(range);
///
public bool Equals(NpgsqlRange other)
{
if (Flags != other.Flags)
return false;
if (HasEquatableBounds)
{
var lowerEqual = LowerBound is null
? other.LowerBound is null
: !(other.LowerBound is null) && ((IEquatable)LowerBound).Equals(other.LowerBound);
if (!lowerEqual)
return false;
return UpperBound is null
? other.UpperBound is null
: !(other.UpperBound is null) && ((IEquatable)UpperBound).Equals(other.UpperBound);
}
return
(LowerBound?.Equals(other.LowerBound) ?? other.LowerBound is null) &&
(UpperBound?.Equals(other.UpperBound) ?? other.UpperBound is null);
}
///
public override int GetHashCode()
=> unchecked((397 * (int)Flags) ^ (397 * (LowerBound?.GetHashCode() ?? 0)) ^ (397 * (UpperBound?.GetHashCode() ?? 0)));
///
public override string ToString()
{
if (IsEmpty)
return EmptyLiteral;
var sb = new StringBuilder();
sb.Append(LowerBoundIsInclusive ? LowerInclusiveBound : LowerExclusiveBound);
if (!LowerBoundInfinite)
sb.Append(LowerBound);
sb.Append(BoundSeparator);
if (!UpperBoundInfinite)
sb.Append(UpperBound);
sb.Append(UpperBoundIsInclusive ? UpperInclusiveBound : UpperExclusiveBound);
return sb.ToString();
}
// TODO: rewrite this to use ReadOnlySpan for the 4.1 release
///
/// Parses the well-known text representation of a PostgreSQL range type into a .
///
/// A PosgreSQL range type in a well-known text format.
///
/// The represented by the .
///
///
/// Malformed range literal.
///
///
/// Malformed range literal. Missing left parenthesis or bracket.
///
///
/// Malformed range literal. Missing right parenthesis or bracket.
///
///
/// Malformed range literal. Missing comma after lower bound.
///
///
/// See: https://www.postgresql.org/docs/current/static/rangetypes.html
///
[RequiresUnreferencedCode("Parse implementations for certain types of T may require members that have been trimmed.")]
public static NpgsqlRange Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
value = value.Trim();
if (value.Length < 3)
throw new FormatException("Malformed range literal.");
if (string.Equals(value, EmptyLiteral, StringComparison.OrdinalIgnoreCase))
return Empty;
var lowerInclusive = value[0] == LowerInclusiveBound;
var lowerExclusive = value[0] == LowerExclusiveBound;
if (!lowerInclusive && !lowerExclusive)
throw new FormatException("Malformed range literal. Missing left parenthesis or bracket.");
var upperInclusive = value[^1] == UpperInclusiveBound;
var upperExclusive = value[^1] == UpperExclusiveBound;
if (!upperInclusive && !upperExclusive)
throw new FormatException("Malformed range literal. Missing right parenthesis or bracket.");
var separator = value.IndexOf(BoundSeparator);
if (separator == -1)
throw new FormatException("Malformed range literal. Missing comma after lower bound.");
if (separator != value.LastIndexOf(BoundSeparator))
// TODO: this should be replaced to handle quoted commas.
throw new NotSupportedException("Ranges with embedded commas are not currently supported.");
// Skip the opening bracket and stop short of the separator.
var lowerSegment = value.Substring(1, separator - 1).Trim();
// Skip past the separator and stop short of the closing bracket.
var upperSegment = value.Substring(separator + 1, value.Length - separator - 2).Trim();
// TODO: infinity literals have special meaning to some types (e.g. daterange), we should consider a flag to track them.
var lowerInfinite =
lowerSegment.Length == 0 ||
string.Equals(lowerSegment, string.Empty, StringComparison.OrdinalIgnoreCase) ||
string.Equals(lowerSegment, NullLiteral, StringComparison.OrdinalIgnoreCase) ||
string.Equals(lowerSegment, LowerInfinityLiteral, StringComparison.OrdinalIgnoreCase);
var upperInfinite =
upperSegment.Length == 0 ||
string.Equals(upperSegment, string.Empty, StringComparison.OrdinalIgnoreCase) ||
string.Equals(upperSegment, NullLiteral, StringComparison.OrdinalIgnoreCase) ||
string.Equals(upperSegment, UpperInfinityLiteral, StringComparison.OrdinalIgnoreCase);
BoundConverter ??= TypeDescriptor.GetConverter(typeof(T));
var lower = lowerInfinite ? default : (T?)BoundConverter.ConvertFromString(lowerSegment);
var upper = upperInfinite ? default : (T?)BoundConverter.ConvertFromString(upperSegment);
return new NpgsqlRange(lower, lowerInclusive, lowerInfinite, upper, upperInclusive, upperInfinite);
}
///
/// Represents a type converter for .
///
[RequiresUnreferencedCode("ConvertFrom implementations for certain types of T may require members that have been trimmed.")]
public class RangeTypeConverter : TypeConverter
{
///
/// Adds a to the closed form .
///
public static void Register() =>
TypeDescriptor.AddAttributes(
typeof(NpgsqlRange),
new TypeConverterAttribute(typeof(RangeTypeConverter)));
///
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(string);
///
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
=> destinationType == typeof(string);
///
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
=> value is string s ? Parse(s) : base.ConvertFrom(context, culture, value);
///
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
=> value is null ? string.Empty : value.ToString();
}
}
///
/// Represents characteristics of range type boundaries.
///
///
/// See: https://www.postgresql.org/docs/current/static/rangetypes.html
///
[Flags]
enum RangeFlags : byte
{
///
/// The default flag. The range is not empty and has boundaries that are definite and exclusive.
///
None = 0,
///
/// The range is empty. E.g. '(0,0)', 'empty'.
///
Empty = 1,
///
/// The lower bound is inclusive. E.g. '[0,5]', '[0,5)', '[0,)'.
///
LowerBoundInclusive = 2,
///
/// The upper bound is inclusive. E.g. '[0,5]', '(0,5]', '(,5]'.
///
UpperBoundInclusive = 4,
///
/// The lower bound is infinite or indefinite. E.g. '(null,5]', '(-infinity,5]', '(,5]'.
///
LowerBoundInfinite = 8,
///
/// The upper bound is infinite or indefinite. E.g. '[0,null)', '[0,infinity)', '[0,)'.
///
UpperBoundInfinite = 16,
///
/// Both the lower and upper bounds are inclusive.
///
Inclusive = LowerBoundInclusive | UpperBoundInclusive,
///
/// Both the lower and upper bounds are indefinite.
///
Infinite = LowerBoundInfinite | UpperBoundInfinite,
///
/// The lower bound is both inclusive and indefinite. This represents an error condition.
///
LowerInclusiveInfinite = LowerBoundInclusive | LowerBoundInfinite,
///
/// The upper bound is both inclusive and indefinite. This represents an error condition.
///
UpperInclusiveInfinite = UpperBoundInclusive | UpperBoundInfinite
}