Skip to content

Commit

Permalink
Expand string and span extension methods (#10629)
Browse files Browse the repository at this point in the history
This change covers several pieces of work on shared extension methods:

1. Augment and document `AsSpan()`, `AsSpanOrDefault()`, `AsMemory()`,
and `AsMemoryOrDefault()` extension methods targeting string.
2. Augment and document `Contains()`, `IndexOf()`, `StartsWith()`, and
`EndsWith()` extension methods targeting string.
3. Add `AsSpan()` and `AsMemory()` extensions methods targeting array
and Razor's polyfill `Index` and `Range` types.
  • Loading branch information
DustinCampbell authored Jul 17, 2024
2 parents 8e007c6 + 90cbe54 commit 8b0fe3d
Show file tree
Hide file tree
Showing 4 changed files with 745 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,188 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;

#if !NET
using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper;
#endif

namespace Microsoft.AspNetCore.Razor;

internal static class ArrayExtensions
{
/// <summary>
/// Creates a new span over the portion of the target array defined by an <see cref="Index"/> value.
/// </summary>
/// <param name="array">
/// The array to convert.
/// </param>
/// <param name="startIndex">
/// The starting index.
/// </param>
/// <remarks>
/// This uses Razor's <see cref="Index"/> type, which is type-forwarded on .NET.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="array"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="startIndex"/> is less than 0 or greater than <paramref name="array"/>.Length.
/// </exception>
/// <exception cref="ArrayTypeMismatchException">
/// <paramref name="array"/> is covariant, and the array's type is not exactly <typeparamref name="T"/>[].
/// </exception>
public static ReadOnlySpan<T> AsSpan<T>(this T[]? array, Index startIndex)
{
#if NET
return MemoryExtensions.AsSpan(array, startIndex);
#else
if (array is null)
{
if (!startIndex.Equals(Index.Start))
{
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
}

return default;
}

return MemoryExtensions.AsSpan(array, startIndex.GetOffset(array.Length));
#endif
}

/// <summary>
/// Creates a new span over the portion of the target array defined by a <see cref="Range"/> value.
/// </summary>
/// <param name="array">
/// The array to convert.
/// </param>
/// <param name="range">
/// The range of the array to convert.
/// </param>
/// <remarks>
/// This uses Razor's <see cref="Range"/> type, which is type-forwarded on .NET.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="array"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="range"/>'s start or end index is not within the bounds of the string.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="range"/>'s start index is greater than its end index.
/// </exception>
/// <exception cref="ArrayTypeMismatchException">
/// <paramref name="array"/> is covariant, and the array's type is not exactly <typeparamref name="T"/>[].
/// </exception>
public static ReadOnlySpan<T> AsSpan<T>(this T[]? array, Range range)
{
#if NET
return MemoryExtensions.AsSpan(array, range);
#else
if (array is null)
{
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
{
ThrowHelper.ThrowArgumentNull(nameof(array));
}

return default;
}

var (start, length) = range.GetOffsetAndLength(array.Length);
return MemoryExtensions.AsSpan(array, start, length);
#endif
}

/// <summary>
/// Creates a new memory region over the portion of the target starting at the specified index
/// to the end of the array.
/// </summary>
/// <param name="array">
/// The array to convert.
/// </param>
/// <param name="startIndex">
/// The first position of the array.
/// </param>
/// <remarks>
/// This uses Razor's <see cref="Index"/> type, which is type-forwarded on .NET.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="array"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="startIndex"/> is less than 0 or greater than <paramref name="array"/>.Length.
/// </exception>
/// <exception cref="ArrayTypeMismatchException">
/// <paramref name="array"/> is covariant, and the array's type is not exactly <typeparamref name="T"/>[].
/// </exception>
public static ReadOnlyMemory<T> AsMemory<T>(this T[]? array, Index startIndex)
{
#if NET
return MemoryExtensions.AsMemory(array, startIndex);
#else
if (array is null)
{
if (!startIndex.Equals(Index.Start))
{
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
}

return default;
}

return MemoryExtensions.AsMemory(array, startIndex.GetOffset(array.Length));
#endif
}

/// <summary>
/// Creates a new memory region over the portion of the target array beginning at
/// inclusive start index of the range and ending at the exclusive end index of the range.
/// </summary>
/// <param name="array">
/// The array to convert.
/// </param>
/// <param name="range">
/// The range of the array to convert.
/// </param>
/// <remarks>
/// This uses Razor's <see cref="Range"/> type, which is type-forwarded on .NET.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="array"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="range"/>'s start or end index is not within the bounds of the string.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="range"/>'s start index is greater than its end index.
/// </exception>
/// <exception cref="ArrayTypeMismatchException">
/// <paramref name="array"/> is covariant, and the array's type is not exactly <typeparamref name="T"/>[].
/// </exception>
public static ReadOnlyMemory<T> AsMemory<T>(this T[]? array, Range range)
{
#if NET
return MemoryExtensions.AsMemory(array, range);
#else
if (array is null)
{
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
{
ThrowHelper.ThrowArgumentNull(nameof(array));
}

return default;
}

var (start, length) = range.GetOffsetAndLength(array.Length);
return MemoryExtensions.AsMemory(array, start, length);
#endif
}

public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(
this (TKey key, TValue value)[] array, IEqualityComparer<TKey> keyComparer)
where TKey : notnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,6 @@ public static void SetCapacityIfLarger<T>(this ImmutableArray<T>.Builder builder
}
}

/// <summary>
/// Returns the current contents as an <see cref="ImmutableArray{T}"/> and sets
/// the collection to a zero length array.
/// </summary>
/// <remarks>
/// If <see cref="ImmutableArray{T}.Builder.Capacity"/> equals
/// <see cref="ImmutableArray{T}.Builder.Count"/>, the internal array will be extracted
/// as an <see cref="ImmutableArray{T}"/> without copying the contents. Otherwise, the
/// contents will be copied into a new array. The collection will then be set to a
/// zero-length array.
/// </remarks>
/// <returns>An immutable array.</returns>
public static ImmutableArray<T> DrainToImmutable<T>(this ImmutableArray<T>.Builder builder)
{
#if NET8_0_OR_GREATER
return builder.DrainToImmutable();
#else
if (builder.Count == 0)
{
return [];
}

if (builder.Count == builder.Capacity)
{
return builder.MoveToImmutable();
}

var result = builder.ToImmutable();
builder.Clear();
return result;
#endif
}

public static ImmutableArray<TResult> SelectAsArray<T, TResult>(this ImmutableArray<T> source, Func<T, TResult> selector)
{
return source switch
Expand Down
Loading

0 comments on commit 8b0fe3d

Please sign in to comment.