-
Notifications
You must be signed in to change notification settings - Fork 344
Added IsValid, Bind, and BindToValid. Renamed Last to End #1981
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"); | ||
} | ||
} | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't First == UnboundedFirst? I thought only End is exclusive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The property returns true if both of the ends are bound. Otherwise false. The property can be used to see if some operations on the range would work, e.g. GetEnumerator works only for bound ranges. "Unbound" means "I don't know/care what the value is". It does not make sense to enumerate such ranges. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. So, you can't enumerate from [int.MinValue..int.MinValue + 5] similar to int.MaxValue? int.MaxValue being the sentinel for End makes sense given End is exclusive (for example [0..int.MaxValue] works fine. Are we treating the lower bound the same way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
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; | ||
|
@@ -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<int> IEnumerable<int>.GetEnumerator() | |
|
||
IEnumerator IEnumerable.GetEnumerator() | ||
=> GetEnumerator(); | ||
|
||
public bool Contains(int value) | ||
{ | ||
if (!IsBound) throw new InvalidOperationException("Unbound ranges cannot contain"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's unbound, then it contains any value. If it has unbound start, check the end only. Same for ranges with unbound start. if (IsBound) return Start <= value && value < End;
if (Start == UnboundStart) return value < End;
if (End == UnboundEnd) return Start <= value;
return false; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not forget to check direction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Theoretically yes, but what are the scenarios for calling Contains on unbound ranges? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exceptions are okay for class Example
{
private Range _range;
public Example(Range range) => _range = range;
public void DoSomething(int value)
{
if (_range.Contains(value))
{
}
else
{
}
}
} What if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also the switch (variable)
{
case in ..0: // should not throw
break;
// ...
case in 5..: // should not throw
break;
} |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should First be < length here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made it so that range can start after the last element. This is to allow empty ranges. |
||
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(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First
should be renamed toStart
to follow naming convention as @jcouv mentioned earlier in the previous PR:Start
andEnd
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I am not thrilled with Start. It sounds like an action is about to start. That's why I originally did not use End. But I will think about the naming a bit more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @YohDeadfall as
Start
seems to fit withEnd
. However, I also see @KrzysztofCwalina point. I likeFirst
andLast
better, though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Begin
would be fine, but unfortunately it shouldn't be used for historical reasons since everywhere in .NET onlyStart
/End
is used for such situations in parameter names, fields, properties. AlsoBegin
andEnd
are used for pre-Task
async operations in method names.