From d68a9a4cfae4c62a0cb05c204cf8d98d6876833a Mon Sep 17 00:00:00 2001 From: Krzysztof Cwalina Date: Fri, 15 Dec 2017 15:58:01 -0800 Subject: [PATCH] Added IsValid, Bind, and BindToValid. Renamed Last to End (#1981) * Added IsValid, Bind, and BindToValid * PR feedback --- .../System/Range.cs | 114 +++++++++++++++--- .../RangeTests.cs | 73 ++++++++--- 2 files changed, 150 insertions(+), 37 deletions(-) diff --git a/src/System.Buffers.Experimental/System/Range.cs b/src/System.Buffers.Experimental/System/Range.cs index 6199ab06715..f0a2fbaf129 100644 --- a/src/System.Buffers.Experimental/System/Range.cs +++ b/src/System.Buffers.Experimental/System/Range.cs @@ -3,22 +3,23 @@ using System.Collections; using System.Collections.Generic; +using System.Text; namespace System { // TODO: consider allowing Last > First. Ennumeration will count down. - public struct Range : IEnumerable + public readonly struct Range : IEnumerable { public const int UnboundedFirst = Int32.MinValue; - public const int UnboundedLast = Int32.MaxValue; + public const int UnboundedEnd = Int32.MaxValue; public readonly int First; - public readonly int Last; // Last is exclusive + public readonly int End; // End is exclusive public uint Length { get { - if (IsBound) return (uint)((long)Last - (long)First); + if (IsBound) return (uint)((long)End - (long)First); throw new InvalidOperationException("cannot get length of unbound range"); } } @@ -29,42 +30,43 @@ public Range(int first, uint length) throw new ArgumentOutOfRangeException(nameof(first)); First = first; - Last = (int)(first + length); + End = (int)(first + length); - if (Last < First) throw new ArgumentOutOfRangeException(nameof(length)); + if (End < First) throw new ArgumentOutOfRangeException(nameof(length)); } - private Range(int first, int last) + private Range(int first, int end) { First = first; - Last = last; + End = end; } - public bool IsBound => First != UnboundedFirst && Last != UnboundedLast; + public bool IsBound => First != UnboundedFirst && End != UnboundedEnd; - public void Deconstruct(out int first, out int last) + public void Deconstruct(out int first, out int end) { first = First; - last = Last; + end = End; } - public static Range Construct(int first, int last) - => new Range(first, last); + + public static Range Construct(int first, int end) + => new Range(first, end); public struct Enumerator : IEnumerator { int _current; - int _last; + int _end; - internal Enumerator(int first, int last) + internal Enumerator(int first, int end) { _current = first - 1; - _last = last; + _end = end; } public bool MoveNext() { _current++; - return _current < _last; + return _current < _end; } public int Current => _current; @@ -79,7 +81,7 @@ void IDisposable.Dispose() { } // TODO: write benchmark for this public Enumerator GetEnumerator() { - if(IsBound) return new Enumerator(First, Last); + if(IsBound) return new Enumerator(First, End); throw new InvalidOperationException("cannot enumerate unbound range"); } @@ -88,5 +90,81 @@ IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool Contains(int value) + { + if (!IsBound) throw new InvalidOperationException("Unbound ranges cannot contain"); + return value >= First && value < End; + } + + /// + /// Returns true if this Range is a valid range for a zero based index of specified length. + /// + /// zero based length. + /// + public bool IsValid(int length) + { + if (First == UnboundedFirst) + { + if (End == UnboundedEnd) return true; + return End <= length; + } + else // First is bounded + { + if (First < 0) return false; + if (End == UnboundedEnd) return First <= length; + if (First > length) return false; + return End <= length; + } + } + + /// + /// Converts an unbound Range (IsBound == false) to a bound one (IsBound == true). + /// + /// zero based length of an indexable 'list' the range is being bound to. + /// Bound Range (IsBound == true). + /// The method throws ArgumentOutOfRangeException Range.IsValid(lenght) returns false. + public Range Bind(int length) + { + if (!IsValid(length)) throw new ArgumentOutOfRangeException(nameof(length)); + if (IsBound) return this; + + int first = 0; + if (First != UnboundedFirst) first = First; + + int end; + if (End != UnboundedEnd) end = End; + else end = length; + + return new Range(first, end); + } + + /// + /// Binds the range possibly adjusting it to fit in the length. + /// + /// zero based length. + /// + public Range BindToValid(int length) + { + int first = First; + if (first < 0) first = 0; + if (first > length - 1) first = length; + + int end = End; + if (end == UnboundedEnd || end > length) end = length; + + return new Range(first, end); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append('['); + if (First != UnboundedFirst) sb.Append(First); + sb.Append(".."); + if (End != UnboundedEnd) sb.Append(End); + sb.Append(']'); + return sb.ToString(); + } } } diff --git a/tests/System.Buffers.Experimental.Tests/RangeTests.cs b/tests/System.Buffers.Experimental.Tests/RangeTests.cs index 4e2ab1c070d..b2e0d75bb1d 100644 --- a/tests/System.Buffers.Experimental.Tests/RangeTests.cs +++ b/tests/System.Buffers.Experimental.Tests/RangeTests.cs @@ -20,7 +20,7 @@ public void Basics() Assert.Equal(first, range.First); Assert.Equal(length, range.Length); - Assert.Equal(length + first, range.Last); + Assert.Equal(length + first, range.End); long sum = 0; uint numberOfItems = 0; @@ -31,22 +31,22 @@ public void Basics() } Assert.Equal(numberOfItems, range.Length); - Assert.Equal((range.First + (long)range.Last - 1) * range.Length / 2, sum); + Assert.Equal((range.First + (long)range.End - 1) * range.Length / 2, sum); (int f, int l) = range; Assert.Equal(range.First, f); - Assert.Equal(range.Last, l); + Assert.Equal(range.End, l); - var constructed = Range.Construct(first, range.Last); + var constructed = Range.Construct(first, range.End); Assert.Equal(range.First, constructed.First); - Assert.Equal(range.Last, constructed.Last); + Assert.Equal(range.End, constructed.End); Assert.Equal(range.Length, constructed.Length); } } } [Fact] - public void FirstIsInclusiveLastIsExclusive() + public void FirstIsInclusiveEndIsExclusive() { var range = Range.Construct(10, 15); Assert.Equal(10, range.First()); @@ -54,12 +54,12 @@ public void FirstIsInclusiveLastIsExclusive() } [Fact] - public void UnboundedLast() + public void UnboundedEnd() { - var unboundedLast = Range.Construct(10, Range.UnboundedLast); - Assert.False(unboundedLast.IsBound); - Assert.Equal(10, unboundedLast.First); - Assert.Equal(Range.UnboundedLast, unboundedLast.Last); + var unboundedEnd = Range.Construct(10, Range.UnboundedEnd); + Assert.False(unboundedEnd.IsBound); + Assert.Equal(10, unboundedEnd.First); + Assert.Equal(Range.UnboundedEnd, unboundedEnd.End); } [Fact] @@ -68,16 +68,51 @@ public void UnboundedFirst() var unboundedFirst = Range.Construct(Range.UnboundedFirst, 10); Assert.False(unboundedFirst.IsBound); Assert.Equal(Range.UnboundedFirst, unboundedFirst.First); - Assert.Equal(10, unboundedFirst.Last); + Assert.Equal(10, unboundedFirst.End); } [Fact] public void Unbounded() { - var unbounded = Range.Construct(Range.UnboundedFirst, Range.UnboundedLast); + var unbounded = Range.Construct(Range.UnboundedFirst, Range.UnboundedEnd); Assert.False(unbounded.IsBound); Assert.Equal(Range.UnboundedFirst, unbounded.First); - Assert.Equal(Range.UnboundedLast, unbounded.Last); + Assert.Equal(Range.UnboundedEnd, unbounded.End); + } + + [Theory] + [InlineData(0, 0, 0, true)] + [InlineData(0, 1, 1, true)] + [InlineData(0, 1, 0, false)] // non empty range not valid for empty array + [InlineData(-1, 0, 0, false)] // lower bound negative + public void IsValidArrayRange(int first, int end, int length, bool result) + { + var range = Range.Construct(first, end); + Assert.Equal(result, range.IsValid(length)); + } + + [Theory] + [InlineData(Range.UnboundedFirst, 0, 0, 0, 0)] + [InlineData(Range.UnboundedFirst, 1, 10, 0, 1)] + [InlineData(0, Range.UnboundedEnd, 1, 0, 1)] + public void Bind(int first, int end, int length, int resultFirst, int resultEnd) + { + var range = Range.Construct(first, end); + var result = range.Bind(length); + Assert.Equal(resultFirst, result.First); + Assert.Equal(resultEnd, result.End); + } + + [Theory] + [InlineData(1, 0, Range.UnboundedEnd, 1)] + [InlineData(1, 1, Range.UnboundedEnd, 0)] + [InlineData(1, 3, Range.UnboundedEnd, 0)] + public void BindToValid(int arrayLength, int first, int end, uint boundRangeLength) + { + var range = Range.Construct(first, end); + var result = range.BindToValid(arrayLength); + Assert.Equal(boundRangeLength, result.Length); + Assert.Equal(boundRangeLength == 0 ? arrayLength : first, result.First); } [Theory] @@ -87,12 +122,12 @@ public void Unbounded() [InlineData(int.MinValue + 1, 1, int.MinValue + 2)] // full range [InlineData(int.MinValue + 1, uint.MaxValue - 2, int.MaxValue -1)] - public void BoundaryConditions(int first, uint length, int last) + public void BoundaryConditions(int first, uint length, int end) { var empty = new Range(first, length); Assert.Equal(length, empty.Length); Assert.Equal(first, empty.First); - Assert.Equal(last, empty.Last); + Assert.Equal(end, empty.End); } [Fact] @@ -106,7 +141,7 @@ public void Errors() }); Assert.Throws(() => { - // MaxValue is used as a sentinel for Last, so Last cannot endup being it. + // MaxValue is used as a sentinel for End, so End cannot end up being it. var tooLong = new Range(int.MaxValue, 1); }); Assert.Throws(() => @@ -118,7 +153,7 @@ public void Errors() Assert.Throws(() => { // Cannot enumerate unbound - var unbound = Range.Construct(1, Range.UnboundedLast); + var unbound = Range.Construct(1, Range.UnboundedEnd); unbound.GetEnumerator(); }); Assert.Throws(() => @@ -130,7 +165,7 @@ public void Errors() Assert.Throws(() => { // Cannot get length on unbound - var unbound = Range.Construct(1, Range.UnboundedLast); + var unbound = Range.Construct(1, Range.UnboundedEnd); var length = unbound.Length; }); }