Skip to content

Commit

Permalink
Add IBufferWriter<byte>.AsStream().
Browse files Browse the repository at this point in the history
Add MemoryStream.AsIBufferWriter()
Add byte[].AsStream().
Add List<T>.AsMemory() and .AsSpan().
Add PickFirstEnumerator<T>.
Move SystemExtensions.System.FIle to SystemExtensions.System.IO.File.
Update README.md
  • Loading branch information
aianlinb committed Jul 25, 2024
1 parent 686292e commit 3253ea3
Show file tree
Hide file tree
Showing 21 changed files with 446 additions and 60 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
[![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)
[![Test](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml/badge.svg)](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml)

Useful and high-performance helper classes and extension methods.

## Highlights
- SpanExtensions
- StreamExtensions
- ValueList
- SubStream
22 changes: 22 additions & 0 deletions System.Private.CoreLib/MemoryStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Threading.Tasks;

namespace System.IO;
/// <summary>
/// Forwarded to System.Private.CoreLib.dll at runtime
/// </summary>
public class MemoryStream {
public byte[] _buffer;
public readonly int _origin;
public int _position;
public int _length;
public int _capacity;
public bool _expandable;
public bool _writable;
public bool _exposable;
public bool _isOpen;
public Task<int>? _lastReadTask;
public const int MemStreamMaxLength = int.MaxValue;

public extern void EnsureNotClosed();
public extern bool EnsureCapacity(int value);
}
17 changes: 17 additions & 0 deletions SystemExtensions.Test/ArrayExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,21 @@ public void AsList_Test(int count) {
Assert.AreEqual(0, result.Count);
Assert.AreEqual(array.Length, result.Capacity);
}

[TestMethod]
public void ByteArrayAsStream_Test() {
// Arrange
byte[] buffer = [1, 2, 3];

// Act
using var ms = buffer.AsStream(0, -1, false, true, true);

// Assert
Assert.AreEqual(buffer.Length, ms.Length);
Assert.AreEqual(buffer.Length, ms.Capacity);
Assert.IsFalse(ms.CanWrite);
Assert.AreSame(buffer, ms.GetBuffer());
ms.Capacity = 5;
Assert.AreNotSame(buffer, ms.GetBuffer());
}
}
45 changes: 41 additions & 4 deletions SystemExtensions.Test/CollectionExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
namespace SystemExtensions.Tests;
using System.Runtime.InteropServices;

namespace SystemExtensions.Tests;

[TestClass]
public class CollectionExtensionsTests {
private static readonly int[] enumerable = [1, 2, 3, 4, 5];
[TestMethod]
public void IndexOf_Test() {
// Arrange
var enumerable = new[] { 1, 2, 3, 4, 5 };

// Act + Assert
for (var i = 0; i < enumerable.Length; ++i)
Assert.AreEqual(i, Collections.CollectionExtensions.IndexOf(enumerable, enumerable[i]));
}

[TestMethod]
public void PickFirstEnumerator_Test() {
// Arrange
using var pfe = new PickFirstEnumerator<int>(enumerable);

// Act + Assert
Assert.AreEqual(enumerable[0], pfe.First);
using var et = enumerable.AsEnumerable().GetEnumerator();
while (pfe.MoveNext()) {
Assert.IsTrue(et.MoveNext());
Assert.AreEqual(et.Current, pfe.Current);
}
pfe.Reset();
Assert.AreEqual(enumerable[0], pfe.First);
Assert.IsTrue(enumerable.SequenceEqual(pfe));
}

[TestMethod]
public unsafe void ListAsSpanAsMemory_Test() {
// Arrange
var list = new List<int>(enumerable);

// Act
var span = list.AsSpan();
var memory = list.AsMemory();

// Assert
Assert.AreEqual(list.Count, span.Length);
Assert.AreEqual(list.Count, memory.Length);
fixed (int* expected = CollectionsMarshal.AsSpan(list), actual1 = span) {
var actual2 = (int*)memory.Pin().Pointer;
Assert.IsTrue(expected == actual1);
Assert.IsTrue(expected == actual2);
}
}
}
2 changes: 1 addition & 1 deletion SystemExtensions.Test/FileTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace SystemExtensions.Tests;
using File = SystemExtensions.System.File;
using File = SystemExtensions.System.IO.File;
using System = global::System;

[TestClass]
Expand Down
75 changes: 75 additions & 0 deletions SystemExtensions.Test/StreamExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
using System.Buffers;
using System.Data;

using SystemExtensions.Streams;

namespace SystemExtensions.Tests;
[TestClass]
public class StreamExtensionsTests {
Expand Down Expand Up @@ -131,4 +136,74 @@ public void ReadToEndTest() {
new ReadOnlySpan<byte>(expected).Slice(pos).SequenceEqual(result1);
new ReadOnlySpan<byte>(expected).Slice(pos).SequenceEqual(result2);
}

[TestMethod]
[DataRow(false)]
[DataRow(true)]
public void BufferWriterWrapper_Test(bool getMemory) {
// Arrange
using var ms = new MemoryStream(4);
var bww = ms.AsIBufferWriter();
ReadOnlySpan<byte> data = [1, 2, 3];

// Act + Assert
data.CopyTo(getMemory ? bww.GetMemory().Span : bww.GetSpan());
bww.Advance(data.Length);
Assert.AreEqual(4, ms.Capacity); // not changed
Assert.AreEqual(data.Length, ms.Length);

data.CopyTo(getMemory ? bww.GetMemory(5).Span : bww.GetSpan(5));
Assert.IsTrue(data.Length + 5 <= ms.Capacity); // expanded
bww.Advance(data.Length);
Assert.AreEqual(data.Length + data.Length, ms.Length);

var result = ms.ToArray().AsReadOnlySpan();
Assert.IsTrue(data.SequenceEqual(result[..data.Length]));
Assert.IsTrue(data.SequenceEqual(result.Slice(data.Length)));

ms.SetLength(2);
ms.Capacity = 2;
Assert.AreNotEqual(0, (getMemory ? bww.GetMemory().Span : bww.GetSpan()).Length);
Assert.IsTrue(3 <= ms.Capacity); // expanded
}

[TestMethod]
public async Task BufferWriterStream_Test() {
// Arrange
var abw = new ArrayBufferWriter<byte>();
var bws = abw.AsStream();
byte[] data = [1, 2, 3];

// Act + Assert
Assert.IsTrue(bws.CanWrite);
Assert.IsFalse(bws.CanRead);
Assert.IsFalse(bws.CanSeek);
Assert.ThrowsException<NotSupportedException>(() => bws.Position = 1);
Assert.ThrowsException<NotSupportedException>(() => bws.Read([], default, default));
Assert.ThrowsException<NotSupportedException>(() => bws.Read(default));
await Assert.ThrowsExceptionAsync<NotSupportedException>(() => bws.ReadAsync(default).AsTask());
await Assert.ThrowsExceptionAsync<NotSupportedException>(() => bws.ReadAsync([], default, default, default));
Assert.ThrowsException<NotSupportedException>(() => bws.BeginRead([], default, default, default, default));
Assert.ThrowsException<NotSupportedException>(() => bws.Seek(default, default));
Assert.ThrowsException<NotSupportedException>(() => bws.SetLength(default));

bws.Write(data, 0, 2);
Assert.IsTrue(abw.WrittenSpan.SequenceEqual(new(data, 0, 2)));
abw.ResetWrittenCount();
#pragma warning disable CA1835
await bws.WriteAsync(data, 0, 2);
#pragma warning restore CA1835
Assert.IsTrue(abw.WrittenSpan.SequenceEqual(new(data, 0, 2)));
abw.ResetWrittenCount();

bws.Write(data);
Assert.IsTrue(abw.WrittenSpan.SequenceEqual(data));
abw.ResetWrittenCount();
await bws.WriteAsync(data);
Assert.IsTrue(abw.WrittenSpan.SequenceEqual(data));

bws.WriteByte(7);
Assert.AreEqual(data.Length + 1, abw.WrittenCount);
Assert.AreEqual(7, abw.WrittenSpan[data.Length]);
}
}
24 changes: 24 additions & 0 deletions SystemExtensions/Collections/ArrayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,28 @@ public static List<T> AsList<T>(this T[] array, int count) {
result._size = count;
return Unsafe.As<List<T>>(result);
}

/// <summary>
/// Creates a <see cref="MemoryStream"/> that use <paramref name="buffer"/> as its underlying array.
/// And with an additional parameter <paramref name="expandable"/> than the original constructor: <see cref="MemoryStream(byte[], int, int, bool, bool)"/>.
/// </summary>
/// <param name="buffer">The underlying buffer for creating the <see cref="MemoryStream"/>.</param>
/// <param name="index">
/// The index of the <paramref name="buffer"/> at which the stream begins.
/// <para>Must be 0 if <paramref name="expandable"/> is <see langword="true"/> due to the internal implementation.</para>
/// </param>
/// <param name="count">The initial length of the stream in bytes</param>
/// <param name="writable">The setting of the <see cref="MemoryStream.CanWrite"/> property, which determines whether the stream supports writing.</param>
/// <param name="publiclyVisible">Whether allows the underlying array of the stream to be returned by <see cref="MemoryStream.GetBuffer"/> or <see cref="MemoryStream.TryGetBuffer"/>.</param>
/// <param name="expandable">Whether the stream can be expanded. That is, whether the <see cref="MemoryStream.Capacity"/> of this stream can be changed.</param>
/// <returns>The created <see cref="MemoryStream"/></returns>
public static MemoryStream AsStream(this byte[] buffer, int index = 0, int count = -1, bool writable = true, bool publiclyVisible = false, bool expandable = false) {
if (count == -1)
count = unchecked(buffer.Length - index);
if (expandable && index != 0)
ThrowHelper.Throw<ArgumentException>("The expandable is true while index != 0", nameof(expandable));
var ms = new MemoryStream(buffer, index, count, writable, publiclyVisible);
Unsafe.As<corelib::System.IO.MemoryStream>(ms)._expandable = expandable;
return ms;
}
}
31 changes: 28 additions & 3 deletions SystemExtensions/Collections/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.Collections;
extern alias corelib;

using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace SystemExtensions.Collections;

public static class CollectionExtensions {
public static int IndexOf<T>(this IEnumerable<T> source, T value) {
switch (source) {
Expand Down Expand Up @@ -41,12 +45,33 @@ public static int IndexOf<T>(this IEnumerable<T> source, Predicate<T> match, out
return -1;
}

/// <summary>
/// Get a <see cref="Span{T}"/> view over a <see cref="List{T}"/>'s data.
/// </summary>
/// <remarks>
/// While the returned <see cref="Span{T}"/> is in use, items should not be added/removed to/from the <paramref name="list"/>,
/// and the <see cref="List{T}.Capacity"/> should not be changed.
/// <para>Same as <see cref="CollectionsMarshal.AsSpan"/></para>
/// </remarks>
public static Span<T> AsSpan<T>(this List<T> list) => CollectionsMarshal.AsSpan(list);
/// <summary>
/// Get a <see cref="Memory{T}"/> view over a <see cref="List{T}"/>'s data.
/// </summary>
/// <remarks>
/// While the returned <see cref="Memory{T}"/> is in use, items should not be added/removed to/from the <paramref name="list"/>,
/// and the <see cref="List{T}.Capacity"/> should not be changed.
/// </remarks>
public static Memory<T> AsMemory<T>(this List<T> list) {
var l = Unsafe.As<corelib::System.Collections.Generic.List<T>>(list);
return new(l._items, 0, l._size);
}

/// <summary>
/// Returns a wrapper with <see cref="IEnumerable{T}.GetEnumerator"/> method that returns <paramref name="source"/> as is.
/// </summary>
/// <remarks>
/// Note that the returned <see cref="IEnumerable{T}"/> cannot be enumerate repeatedly.
/// And it will start from current state of the <see cref="IEnumerator{T}"/> passed.
/// Note that the returned <see cref="IEnumerable{T}"/> cannot be enumerated repeatedly (unless calling <see cref="IEnumerator.Reset"/> of <paramref name="source"/>).
/// And it will start from current state of the <paramref name="source"/>.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static EnumeratorWrapper<T> AsEnumerable<T>(this IEnumerator<T> source) => new(source);
Expand Down
76 changes: 76 additions & 0 deletions SystemExtensions/Collections/PickFirstEnumerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections;

namespace SystemExtensions.Collections;

/// <summary>
/// Wrap a <see cref="IEnumerator{T}"/> to pick its first value and save to <see cref="First"/>,
/// but still can iterate all values including the first one without calling <see cref="IEnumerator.Reset"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is useful when you want to get the first value before a foreach loop,
/// but don't want to re-enumerate the collection.
/// </para>
/// <para>
/// Note that the <see cref="GetEnumerator"/> returns the <see cref="PickFirstEnumerator{T}"/> itself,
/// so this <see cref="IEnumerable{T}"/> can't be used again before calling <see cref="Reset"/>.
/// </para>
/// </remarks>
public struct PickFirstEnumerator<T> : IEnumerator<T>, IEnumerable<T> {
public PickFirstEnumerator(IEnumerable<T> enumerable) : this(enumerable.GetEnumerator()) { }
public PickFirstEnumerator(IEnumerator<T> enumerator) {
if (enumerator.MoveNext())
First = enumerator.Current;
else
State = 1;
BaseEnumerator = enumerator;
}

/// <summary>
/// The base <see cref="IEnumerator{T}"/> that was passed to the constructor.
/// </summary>
public readonly IEnumerator<T> BaseEnumerator { get; }
/// <summary>
/// The first value of <see cref="BaseEnumerator"/>, or <see langword="default"/> if it's empty.
/// </summary>
public readonly T? First { get; }
private byte State; // 0: Initial, 1: Initial (Empty), 2: First moved, 3: Other

public readonly T Current => State switch {
0 or 1 => default!,
2 => First!,
_ => BaseEnumerator.Current
};
readonly object IEnumerator.Current => Current!;

public bool MoveNext() {
switch (State) {
case 0:
State = 2;
return true;
case 1:
State = 3;
return false;
case 2:
State = 3;
break;
}
return BaseEnumerator.MoveNext();
}

public void Reset() {
BaseEnumerator.Reset();
this = new(BaseEnumerator);
}

public readonly void Dispose() => BaseEnumerator.Dispose();

/// <summary>
/// Returns <see langword="this"/> as is.
/// </summary>
/// <remarks>
/// See the remarks of <see cref="PickFirstEnumerator{T}"/>.
/// </remarks>
public readonly IEnumerator<T> GetEnumerator() => this;
readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Loading

0 comments on commit 3253ea3

Please sign in to comment.