Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
Added IsValid, Bind, and BindToValid. Renamed Last to End (#1981)
Browse files Browse the repository at this point in the history
* Added IsValid, Bind, and BindToValid

* PR feedback
  • Loading branch information
KrzysztofCwalina authored Dec 15, 2017
1 parent b986281 commit d68a9a4
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 37 deletions.
114 changes: 96 additions & 18 deletions src/System.Buffers.Experimental/System/Range.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>
public readonly struct Range : IEnumerable<int>
{
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");
}
}
Expand All @@ -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>
{
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;
Expand All @@ -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");
}

Expand All @@ -88,5 +90,81 @@ IEnumerator<int> IEnumerable<int>.GetEnumerator()

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();

public bool Contains(int value)
{
if (!IsBound) throw new InvalidOperationException("Unbound ranges cannot contain");
return value >= First && value < End;
}

/// <summary>
/// Returns true if this Range is a valid range for a zero based index of specified length.
/// </summary>
/// <param name="length">zero based length.</param>
/// <returns></returns>
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;
}
}

/// <summary>
/// Converts an unbound Range (IsBound == false) to a bound one (IsBound == true).
/// </summary>
/// <param name="length">zero based length of an indexable 'list' the range is being bound to.</param>
/// <returns>Bound Range (IsBound == true).</returns>
/// <remarks>The method throws ArgumentOutOfRangeException Range.IsValid(lenght) returns false.</remarks>
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);
}

/// <summary>
/// Binds the range possibly adjusting it to fit in the length.
/// </summary>
/// <param name="length">zero based length.</param>
/// <returns></returns>
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();
}
}
}
73 changes: 54 additions & 19 deletions tests/System.Buffers.Experimental.Tests/RangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,35 +31,35 @@ 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());
Assert.Equal(14, range.Last());
}

[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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -106,7 +141,7 @@ public void Errors()
});
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
// 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<InvalidOperationException>(() =>
Expand All @@ -118,7 +153,7 @@ public void Errors()
Assert.Throws<InvalidOperationException>(() =>
{
// Cannot enumerate unbound
var unbound = Range.Construct(1, Range.UnboundedLast);
var unbound = Range.Construct(1, Range.UnboundedEnd);
unbound.GetEnumerator();
});
Assert.Throws<InvalidOperationException>(() =>
Expand All @@ -130,7 +165,7 @@ public void Errors()
Assert.Throws<InvalidOperationException>(() =>
{
// Cannot get length on unbound
var unbound = Range.Construct(1, Range.UnboundedLast);
var unbound = Range.Construct(1, Range.UnboundedEnd);
var length = unbound.Length;
});
}
Expand Down

0 comments on commit d68a9a4

Please sign in to comment.