Skip to content

Commit

Permalink
Add ReadOnlySpan<T>.{Starts/Ends}With(T).
Browse files Browse the repository at this point in the history
Force zero-extension on all postive integer casting.
Add github action Test
  • Loading branch information
aianlinb committed Jun 6, 2024
1 parent b20bc34 commit 9c512ed
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 87 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: Test

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build-and-test:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
[![NuGet](https://img.shields.io/nuget/v/aianlinb.SystemExtensions)](https://www.nuget.org/packages/aianlinb.SystemExtensions)
[![NuGet](https://img.shields.io/nuget/v/aianlinb.SystemExtensions)](https://www.nuget.org/packages/aianlinb.SystemExtensions)
[![Test](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml/badge.svg)](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml)
21 changes: 13 additions & 8 deletions SystemExtensions.Test/SpanExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,28 @@ public void StartsWith_EndsWith_Test() {
var span = new ReadOnlySpan<int>(array);
var excepted1 = array[..2];
var excepted2 = array[2..];
var excepted3 = new ReadOnlySpan<int>([99, 999]);
var excepted3 = span[0];
var excepted4 = span[^1];

// Act
var actual1 = SpanExtensions.StartsWith(span, excepted1);
var actual2 = SpanExtensions.StartsWith(span, excepted2);
var actual3 = SpanExtensions.StartsWith(span, excepted3);
var actual4 = SpanExtensions.EndsWith(span, excepted1);
var actual5 = SpanExtensions.EndsWith(span, excepted2);
var actual6 = SpanExtensions.EndsWith(span, excepted3);
var actual4 = SpanExtensions.StartsWith(span, excepted4);
var actual5 = SpanExtensions.EndsWith(span, excepted1);
var actual6 = SpanExtensions.EndsWith(span, excepted2);
var actual7 = SpanExtensions.EndsWith(span, excepted3);
var actual8 = SpanExtensions.EndsWith(span, excepted4);

// Assert
Assert.AreEqual(MemoryExtensions.StartsWith(span, excepted1), actual1);
Assert.AreEqual(MemoryExtensions.StartsWith(span, excepted2), actual2);
Assert.AreEqual(MemoryExtensions.StartsWith(span, excepted3), actual3);
Assert.AreEqual(MemoryExtensions.EndsWith(span, excepted1), actual4);
Assert.AreEqual(MemoryExtensions.EndsWith(span, excepted2), actual5);
Assert.AreEqual(MemoryExtensions.EndsWith(span, excepted3), actual6);
Assert.IsTrue(actual3);
Assert.IsFalse(actual4);
Assert.AreEqual(MemoryExtensions.EndsWith(span, excepted1), actual5);
Assert.AreEqual(MemoryExtensions.EndsWith(span, excepted2), actual6);
Assert.IsFalse(actual7);
Assert.IsTrue(actual8);
}
[TestMethod]
public void Replace_Test() {
Expand Down
6 changes: 3 additions & 3 deletions SystemExtensions/Collections/ArrayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public static T[] Insert<T>(this T[] array, int index, T item) {
var span = new Span<T>(result); // cache to avoid duplicate type checks
if (index != 0)
new ReadOnlySpan<T>(array, 0, index).CopyToUnchecked(ref MemoryMarshal.GetReference(span)); // index check here
Unsafe.Add(ref MemoryMarshal.GetReference(span), index) = item;
Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)index) = item;
if (index != array.Length)
new ReadOnlySpan<T>(array, index, array.Length - index).CopyToUnchecked(
ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index + 1));
ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)(index + 1)));
return result;
}

Expand All @@ -27,7 +27,7 @@ public static T[] RemoveAt<T>(this T[] array, int index) {
new ReadOnlySpan<T>(array, 0, index).CopyToUnchecked(ref MemoryMarshal.GetReference(span)); // index check here
if (index != result.Length)
new ReadOnlySpan<T>(array, index + 1, result.Length - index).CopyToUnchecked(
ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index));
ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)index));
return result;
}

Expand Down
43 changes: 24 additions & 19 deletions SystemExtensions/Collections/ValueList.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Buffers;
using System.Buffers;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -57,18 +56,18 @@ public ValueList() : this(0) { }

public void Add(T item) {
EnsureRemainingCapacity(1);
Unsafe.Add(ref GetPinnableReference(), Count) = item;
Unsafe.Add(ref GetPinnableReference(), (nint)(uint)Count) = item;
++Count;
}
public void AddRange(scoped ReadOnlySpan<T> items) {
EnsureRemainingCapacity(items.Length);
items.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), Count));
items.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)Count));
Count += items.Length;
}
public void AddRange(scoped in ReadOnlySequence<T> items) {
EnsureRemainingCapacity(checked((int)items.Length));
foreach (var segment in items) {
segment.Span.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), Count));
segment.Span.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)Count));
Count += segment.Length;
}
}
Expand Down Expand Up @@ -143,8 +142,8 @@ private void Grow(int capacity) {

public void Insert(int index, T item) {
EnsureRemainingCapacity(1);
AsReadOnlySpan(index).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index + 1));
Unsafe.Add(ref GetPinnableReference(), index) = item;
AsReadOnlySpan(index).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)(index + 1)));
Unsafe.Add(ref GetPinnableReference(), (nint)(uint)index) = item;
++Count;
}
/// <remarks>
Expand All @@ -155,8 +154,9 @@ public void InsertRange(int index, scoped ReadOnlySpan<T> items) {
// There's a rare case that the `items` overlaps with the destination below line copying to,
// but the memory there is uninitialized and shouldn't be used by the user,
// so we treat it as an unsafe behavior caused by the user themselves and don't check it.
AsReadOnlySpan(index).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index + items.Length));
items.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index));
var span = AsSpan(index);
span.CopyToUnchecked(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)items.Length));
items.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)index));
Count += items.Length;
}
/// <remarks>
Expand All @@ -165,9 +165,10 @@ public void InsertRange(int index, scoped ReadOnlySpan<T> items) {
public void InsertRange(int index, scoped in ReadOnlySequence<T> items) {
var len = checked((int)items.Length);
EnsureRemainingCapacity(len);
AsReadOnlySpan(index).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index + len));
var span = AsSpan(index);
span.CopyToUnchecked(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)len));
foreach (var segment in items) {
segment.Span.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index));
segment.Span.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)index));
index += segment.Length;
}
Count += len;
Expand All @@ -183,11 +184,11 @@ public bool Remove(T item) {
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveAt(int index) {
AsReadOnlySpan(index + 1).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), index));
AsReadOnlySpan(index + 1).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)index));
--Count;

if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
Unsafe.Add(ref GetPinnableReference(), Count) = default!;
Unsafe.Add(ref GetPinnableReference(), (nint)(uint)Count) = default!;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveRange(int startIndex) {
Expand All @@ -198,10 +199,15 @@ public void RemoveRange(int startIndex) {
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveRange(int startIndex, int length) {
if (length == 0)
return;
if (Environment.Is64BitProcess) {
if ((ulong)(uint)startIndex + (ulong)(uint)length > (ulong)(uint)Count)
ThrowHelper.Throw<ArgumentOutOfRangeException>();
} else {
if ((uint)startIndex > (uint)Count || (uint)length > (uint)(Count - startIndex))
ThrowHelper.Throw<ArgumentOutOfRangeException>();
}

AsReadOnlySpan(startIndex + length).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), startIndex));
AsReadOnlySpan().SliceUnchecked(startIndex + length).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)startIndex));
Count -= length;

if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
Expand All @@ -227,12 +233,11 @@ public void RemoveRanges(IEnumerable<Range> ranges) {
var length = offset - lastEnd;
if (length < 0)
ThrowHelper.Throw<ArgumentException>("The provided ranges must be sorted in ascending order, and must not overlap");
AsReadOnlySpan(lastEnd, length)
.CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), pos));
AsReadOnlySpan(lastEnd, length).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)pos));
pos += length;
lastEnd = end;
}
AsReadOnlySpan(lastEnd).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), pos));
AsReadOnlySpan(lastEnd).CopyToUnchecked(ref Unsafe.Add(ref GetPinnableReference(), (nint)(uint)pos));
pos += Count - lastEnd;
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
AsSpan(pos).Clear();
Expand Down
20 changes: 17 additions & 3 deletions SystemExtensions/Spans/SpanExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,13 @@ ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(value)),
return SpanHelpersWithoutIEquatable.LastIndexOf(ref MemoryMarshal.GetReference(span), span.Length, ref MemoryMarshal.GetReference(value), value.Length);
}

/// <summary>
/// Determines whether the first element of the <paramref name="span"/> equals to the <paramref name="value"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool StartsWith<T>(this scoped ReadOnlySpan<T> span, T value) {
return !span.IsEmpty && EqualityComparer<T>.Default.Equals(value, MemoryMarshal.GetReference(span));
}
/// <summary>
/// Determines whether the specified sequence appears at the start of the <paramref name="span"/>.
/// </summary>
Expand All @@ -395,6 +402,13 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(value)),
return valueLength <= span.Length && SpanHelpersWithoutIEquatable.SequenceEqual(ref MemoryMarshal.GetReference(span), ref MemoryMarshal.GetReference(value), valueLength);
}
/// <summary>
/// Determines whether the last element of the <paramref name="span"/> equals to the <paramref name="value"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool EndsWith<T>(this scoped ReadOnlySpan<T> span, T value) {
return !span.IsEmpty && EqualityComparer<T>.Default.Equals(value, Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)(span.Length - 1) /* force zero-extension */));
}
/// <summary>
/// Determines whether the specified sequence appears at the end of the <paramref name="span"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -497,8 +511,8 @@ public static unsafe void Replace<T>(this scoped ReadOnlySpan<T> source, scoped
unchecked {
if (byteOffset != 0 &&
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
((nuint)byteOffset < (nuint)((nint)source.Length * sizeof(T)) ||
(nuint)byteOffset > (nuint)(-((nint)destination.Length * sizeof(T))))) {
((nuint)byteOffset < (nuint)((nint)(uint)source.Length * sizeof(T)) ||
(nuint)byteOffset > (nuint)(-((nint)(uint)destination.Length * sizeof(T))))) {
#pragma warning restore CS8500
ThrowHelper.Throw<ArgumentException>("Source and destination span overlapped", nameof(destination));
}
Expand Down Expand Up @@ -714,7 +728,7 @@ private struct Ranges {
public Span<Range> AsSpan() => MemoryMarshal.CreateSpan(ref firstRange, stackAllocationLength);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref Range UnsafeAt(int index) => ref Unsafe.Add(ref Unsafe.AsRef(ref firstRange), index);
public ref Range UnsafeAt(int index) => ref Unsafe.Add(ref Unsafe.AsRef(ref firstRange), (nint)(uint)index);
}
}

Expand Down
27 changes: 14 additions & 13 deletions SystemExtensions/Spans/SpanHelpersForObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,31 @@ public static int LastIndexOf(scoped ref object? searchSpace, object? value, int
}

public static int IndexOfAny<T>(scoped ref IEquatable<T>? searchSpace, int searchSpaceLength, ref T value, int valueLength) {
for (int i = 0; i < searchSpaceLength; ++i) {
if (Unsafe.Add(ref searchSpace, i) is IEquatable<T> candidate /*null check*/) {
for (int j = 0; j < valueLength; j++)
for (nint i = 0, slen = searchSpaceLength, vlen = valueLength; i < slen; ++i) {
var candidate = Unsafe.Add(ref searchSpace, i);
if (candidate is not null) {
for (nint j = 0; j < vlen; j++)
if (candidate.Equals(Unsafe.Add(ref value, j)))
return i;
return (int)i;
} else {
for (int j = 0; j < valueLength; j++)
for (nint j = 0; j < vlen; j++)
if (Unsafe.Add(ref value, j) is null)
return i;
return (int)i;
}
}
return -1; // not found
}
public static int IndexOfAny(scoped ref object? searchSpace, int searchSpaceLength, ref object? value, int valueLength) {
for (int i = 0; i < searchSpaceLength; ++i) {
for (nint i = 0, slen = searchSpaceLength, vlen = valueLength; i < slen; ++i) {
var candidate = Unsafe.Add(ref searchSpace, i);
if (candidate is not null) {
for (int j = 0; j < valueLength; ++j)
for (nint j = 0; j < vlen; ++j)
if (candidate.Equals(Unsafe.Add(ref value, j)))
return i;
return (int)i;
} else {
for (int j = 0; j < valueLength; ++j)
for (nint j = 0; j < vlen; ++j)
if (Unsafe.Add(ref value, j) is null)
return i;
return (int)i;
}
}
return -1; // not found
Expand Down Expand Up @@ -114,7 +115,7 @@ public static void Replace(scoped ref object? src, ref object? dst, object? oldV

public static int Count<T>(scoped ref T current, IEquatable<T> value, int length) {
int count = 0;
ref T end = ref Unsafe.Add(ref current, length);
ref T end = ref Unsafe.Add(ref current, (nint)(uint)length);
while (Unsafe.IsAddressLessThan(ref current, ref end)) {
if (value.Equals(current))
++count;
Expand All @@ -124,7 +125,7 @@ public static int Count<T>(scoped ref T current, IEquatable<T> value, int length
}
public static int Count(scoped ref object? current, object? value, int length) {
int count = 0;
ref var end = ref Unsafe.Add(ref current, length);
ref var end = ref Unsafe.Add(ref current, (nint)(uint)length);
if (value is not null)
while (Unsafe.IsAddressLessThan(ref current, ref end)) {
if (value.Equals(current))
Expand Down
Loading

0 comments on commit 9c512ed

Please sign in to comment.