Skip to content

Commit

Permalink
Added operation for moving elements within the span
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Oct 24, 2023
1 parent 6476dce commit 7c048d0
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 19 deletions.
41 changes: 35 additions & 6 deletions src/DotNext.Tests/SpanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,33 +534,62 @@ public static void SwapElements()
public static void TransformElements()
{
// left < right
Span<int> input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
Span<int> input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(0..2, 3..6);
Equal(new int[] { 4, 5, 6, 3, 1, 2 }, input.ToArray());

// left > right
input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(0..3, 4..6);
Equal(new int[] { 5, 6, 4, 1, 2, 3 }, input.ToArray());

// left is zero length
input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(1..1, 3..6);
Equal(new int[] { 1, 4, 5, 6, 2, 3 }, input.ToArray());

// right is zero length
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(0..2, 3..3);
Equal(new int[] { 3, 1, 2, 4, 5, 6 }, input.ToArray());

// no space between ranges
input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(0..2, 2..6);
Equal(new int[] { 3, 4, 5, 6, 1, 2 }, input.ToArray());

// left == right
input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(0..3, 3..6);
Equal(new int[] { 4, 5, 6, 1, 2, 3 }, input.ToArray());

// left and right are empty
input = stackalloc int[] { 1, 2, 3, 4, 5, 6 };
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Swap(1..1, 5..5);
Equal(new int[] { 1, 2, 3, 4, 5, 6 }, input.ToArray());

// overlapping
Throws<ArgumentException>(() => new int[] { 1, 2, 3, 4, 5, 6 }.AsSpan().Swap(0..2, 1..3));
}

[Fact]
public static void MoveRange()
{
// move from left to right
Span<int> input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Move(0..2, 3);
Equal(new int[] { 3, 1, 2, 4, 5, 6 }, input.ToArray());

// move from left to right
input = new int[] { 1, 2, 3, 4, 5, 6 };
input.Move(1..3, 6);
Equal(new int[] { 1, 4, 5, 6, 2, 3 }, input.ToArray());

// move from right to left
input.Move(4..6, 1);
Equal(new int[] { 1, 2, 3, 4, 5, 6 }, input.ToArray());

// out of range
Throws<ArgumentOutOfRangeException>(() => new int[] { 1, 2, 3, 4, 5, 6 }.AsSpan().Move(0..2, 1));
}
}
119 changes: 106 additions & 13 deletions src/DotNext/Span.cs
Original file line number Diff line number Diff line change
Expand Up @@ -762,11 +762,15 @@ public static ReadOnlySpan<TBase> Contravariance<T, TBase>(this ReadOnlySpan<T>
/// <param name="x">The first span.</param>
/// <param name="y">The second span.</param>
/// <exception cref="ArgumentOutOfRangeException">The length of <paramref name="y"/> is not of the same length as <paramref name="x"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="x"/> overlaps with <paramref name="y"/>.</exception>
public static void Swap<T>(this Span<T> x, Span<T> y)
{
if (x.Length != y.Length)
throw new ArgumentOutOfRangeException(nameof(y));

if (x.Overlaps(y))
throw new ArgumentException(ExceptionMessages.OverlappedRange, nameof(y));

SwapCore(x, y);
}

Expand Down Expand Up @@ -836,27 +840,28 @@ public static void Swap<T>(this Span<T> span, Range range1, Range range2)
var (start1, length1) = range1.GetOffsetAndLength(span.Length);
var (start2, length2) = range2.GetOffsetAndLength(span.Length);

if (start1 > start2)
{
Intrinsics.Swap(ref start1, ref start2);
Intrinsics.Swap(ref length1, ref length2);
}

var endOfLeftSegment = start1 + length1;
if (endOfLeftSegment > start2)
throw new ArgumentException(ExceptionMessages.OverlappedRange, nameof(range2));

if (length1 == length2)
{
// handle trivial case that allows to avoid allocation of a large buffer
Span.SwapCore(span.Slice(start1, length1), span.Slice(start2, length2));
}
else if (start1 < start2)
{
SwapCore(span, start1, length1, start2, length2);
}
else
{
SwapCore(span, start2, length2, start1, length1);
SwapCore(span, start1, length1, start2, length2, endOfLeftSegment);
}

static void SwapCore(Span<T> span, int start1, int length1, int start2, int length2)
static void SwapCore(Span<T> span, int start1, int length1, int start2, int length2, int endOfLeftSegment)
{
// check for overlapping
var endOfLeftSegment = start1 + length1;
if (endOfLeftSegment > start2)
throw new ArgumentException(ExceptionMessages.OverlappedRange, nameof(range2));

Span<T> sourceLarge,
sourceSmall,
destLarge,
Expand Down Expand Up @@ -903,12 +908,100 @@ static void SwapCore(Span<T> span, int start1, int length1, int start2, int leng

// rearrange elements
sourceLarge.CopyTo(buffer.Span);
var spaceBetweenRanges = start2 - endOfLeftSegment;
span.Slice(endOfLeftSegment, spaceBetweenRanges).CopyTo(span.Slice(endOfLeftSegment - shift, spaceBetweenRanges));
span[endOfLeftSegment..start2].CopyTo(span.Slice(start1 + length2));
sourceSmall.CopyTo(destSmall);
buffer.Span.CopyTo(destLarge);

buffer.Dispose();
}
}

/// <summary>
/// Moves the range within the span to the specified index.
/// </summary>
/// <typeparam name="T">The type of the elements in the span.</typeparam>
/// <param name="span">The span of elements to modify.</param>
/// <param name="range">The range of elements within <paramref name="span"/> to move.</param>
/// <param name="destinationIndex">The index of the element before which <paramref name="range"/> of elements will be placed.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="destinationIndex"/> is not a valid index within <paramref name="span"/>.</exception>
public static void Move<T>(this Span<T> span, Range range, Index destinationIndex)
{
var (sourceIndex, length) = range.GetOffsetAndLength(span.Length);

if (length is not 0)
MoveCore(span, sourceIndex, destinationIndex.GetOffset(span.Length), length);

static void MoveCore(Span<T> span, int sourceIndex, int destinationIndex, int length)
{
// prepare buffers
Span<T> source = span.Slice(sourceIndex, length),
destination,
sourceGap,
destinationGap;
if (sourceIndex > destinationIndex)
{
sourceGap = span[destinationIndex..sourceIndex];
destinationGap = span.Slice(destinationIndex + length);

destination = span.Slice(destinationIndex, length);
}
else
{
var endOfLeftSegment = sourceIndex + length;
switch (endOfLeftSegment.CompareTo(destinationIndex))
{
case 0:
return;
case > 0:
throw new ArgumentOutOfRangeException(nameof(destinationIndex));
case < 0:
sourceGap = span[endOfLeftSegment..destinationIndex];
destinationGap = span.Slice(sourceIndex);

destination = span.Slice(destinationIndex - length, length);
break;
}
}

// Perf: allocate buffer for smallest part of the span
if (source.Length > sourceGap.Length)
{
length = sourceGap.Length;

var temp = source;
source = sourceGap;
sourceGap = temp;

temp = destination;
destination = destinationGap;
destinationGap = temp;
}
else
{
Debug.Assert(length == source.Length);
}

// prepare buffer
MemoryRental<T> buffer;
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>() || length > MemoryRental<T>.StackallocThreshold)
{
buffer = new(length);
}
else
{
unsafe
{
void* bufferPtr = stackalloc byte[checked(Unsafe.SizeOf<T>() * length)];
buffer = new Span<T>(bufferPtr, length);
}
}

// rearrange buffers
source.CopyTo(buffer.Span);
sourceGap.CopyTo(destinationGap);
buffer.Span.CopyTo(destination);

buffer.Dispose();
}
}
}

0 comments on commit 7c048d0

Please sign in to comment.