// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Internal.Runtime.CompilerServices;
namespace System
{
///
/// Extension methods for Span{T}, Memory{T}, and friends.
///
public static partial class MemoryExtensions
{
///
/// Returns a value indicating whether the specified occurs within the .
/// The source span.
/// The value to seek within the source span.
/// One of the enumeration values that determines how the and are compared.
///
public static bool Contains(this ReadOnlySpan span, ReadOnlySpan value, StringComparison comparisonType)
{
return (IndexOf(span, value, comparisonType) >= 0);
}
///
/// Determines whether this and the specified span have the same characters
/// when compared using the specified option.
/// The source span.
/// The value to compare with the source span.
/// One of the enumeration values that determines how the and are compared.
///
public static bool Equals(this ReadOnlySpan span, ReadOnlySpan other, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
switch (comparisonType)
{
case StringComparison.CurrentCulture:
return (CultureInfo.CurrentCulture.CompareInfo.CompareOptionNone(span, other) == 0);
case StringComparison.CurrentCultureIgnoreCase:
return (CultureInfo.CurrentCulture.CompareInfo.CompareOptionIgnoreCase(span, other) == 0);
case StringComparison.InvariantCulture:
return (CompareInfo.Invariant.CompareOptionNone(span, other) == 0);
case StringComparison.InvariantCultureIgnoreCase:
return (CompareInfo.Invariant.CompareOptionIgnoreCase(span, other) == 0);
case StringComparison.Ordinal:
return EqualsOrdinal(span, other);
case StringComparison.OrdinalIgnoreCase:
return EqualsOrdinalIgnoreCase(span, other);
}
Debug.Fail("StringComparison outside range");
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool EqualsOrdinal(this ReadOnlySpan span, ReadOnlySpan value)
{
if (span.Length != value.Length)
return false;
if (value.Length == 0) // span.Length == value.Length == 0
return true;
return span.SequenceEqual(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool EqualsOrdinalIgnoreCase(this ReadOnlySpan span, ReadOnlySpan value)
{
if (span.Length != value.Length)
return false;
if (value.Length == 0) // span.Length == value.Length == 0
return true;
return CompareInfo.EqualsOrdinalIgnoreCase(ref MemoryMarshal.GetReference(span), ref MemoryMarshal.GetReference(value), span.Length);
}
// TODO https://github.com/dotnet/corefx/issues/27526
internal static bool Contains(this ReadOnlySpan source, char value)
{
for (int i = 0; i < source.Length; i++)
{
if (source[i] == value)
{
return true;
}
}
return false;
}
///
/// Compares the specified and using the specified ,
/// and returns an integer that indicates their relative position in the sort order.
/// The source span.
/// The value to compare with the source span.
/// One of the enumeration values that determines how the and are compared.
///
public static int CompareTo(this ReadOnlySpan span, ReadOnlySpan other, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
switch (comparisonType)
{
case StringComparison.CurrentCulture:
return CultureInfo.CurrentCulture.CompareInfo.CompareOptionNone(span, other);
case StringComparison.CurrentCultureIgnoreCase:
return CultureInfo.CurrentCulture.CompareInfo.CompareOptionIgnoreCase(span, other);
case StringComparison.InvariantCulture:
return CompareInfo.Invariant.CompareOptionNone(span, other);
case StringComparison.InvariantCultureIgnoreCase:
return CompareInfo.Invariant.CompareOptionIgnoreCase(span, other);
case StringComparison.Ordinal:
if (span.Length == 0 || other.Length == 0)
return span.Length - other.Length;
return string.CompareOrdinal(span, other);
case StringComparison.OrdinalIgnoreCase:
return CompareInfo.CompareOrdinalIgnoreCase(span, other);
}
Debug.Fail("StringComparison outside range");
return 0;
}
///
/// Reports the zero-based index of the first occurrence of the specified in the current .
/// The source span.
/// The value to seek within the source span.
/// One of the enumeration values that determines how the and are compared.
///
public static int IndexOf(this ReadOnlySpan span, ReadOnlySpan value, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
if (value.Length == 0)
{
return 0;
}
if (span.Length == 0)
{
return -1;
}
if (GlobalizationMode.Invariant)
{
return CompareInfo.InvariantIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
}
switch (comparisonType)
{
case StringComparison.CurrentCulture:
case StringComparison.CurrentCultureIgnoreCase:
return CultureInfo.CurrentCulture.CompareInfo.IndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.InvariantCulture:
case StringComparison.InvariantCultureIgnoreCase:
return CompareInfo.Invariant.IndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
default:
Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
return CompareInfo.Invariant.IndexOfOrdinal(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
}
}
///
/// Reports the zero-based index of the last occurrence of the specified in the current .
/// The source span.
/// The value to seek within the source span.
/// One of the enumeration values that determines how the and are compared.
///
public static int LastIndexOf(this ReadOnlySpan span, ReadOnlySpan value, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
if (value.Length == 0)
{
return span.Length > 0 ? span.Length - 1 : 0;
}
if (span.Length == 0)
{
return -1;
}
if (GlobalizationMode.Invariant)
{
return CompareInfo.InvariantIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None, fromBeginning: false);
}
switch (comparisonType)
{
case StringComparison.CurrentCulture:
case StringComparison.CurrentCultureIgnoreCase:
return CultureInfo.CurrentCulture.CompareInfo.LastIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.InvariantCulture:
case StringComparison.InvariantCultureIgnoreCase:
return CompareInfo.Invariant.LastIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
default:
Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
return CompareInfo.Invariant.LastIndexOfOrdinal(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
}
}
///
/// Copies the characters from the source span into the destination, converting each character to lowercase,
/// using the casing rules of the specified culture.
///
/// The source span.
/// The destination span which contains the transformed characters.
/// An object that supplies culture-specific casing rules.
/// If the source and destinations overlap, this method behaves as if the original values are in
/// a temporary location before the destination is overwritten.
/// The number of characters written into the destination span. If the destination is too small, returns -1.
///
/// Thrown when is null.
///
public static int ToLower(this ReadOnlySpan source, Span destination, CultureInfo culture)
{
if (culture == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.culture);
// Assuming that changing case does not affect length
if (destination.Length < source.Length)
return -1;
if (GlobalizationMode.Invariant)
TextInfo.ToLowerAsciiInvariant(source, destination);
else
culture.TextInfo.ChangeCase(source, destination, toUpper: false);
return source.Length;
}
///
/// Copies the characters from the source span into the destination, converting each character to lowercase,
/// using the casing rules of the invariant culture.
///
/// The source span.
/// The destination span which contains the transformed characters.
/// If the source and destinations overlap, this method behaves as if the original values are in
/// a temporary location before the destination is overwritten.
/// The number of characters written into the destination span. If the destination is too small, returns -1.
public static int ToLowerInvariant(this ReadOnlySpan source, Span destination)
{
// Assuming that changing case does not affect length
if (destination.Length < source.Length)
return -1;
if (GlobalizationMode.Invariant)
TextInfo.ToLowerAsciiInvariant(source, destination);
else
CultureInfo.InvariantCulture.TextInfo.ChangeCase(source, destination, toUpper: false);
return source.Length;
}
///
/// Copies the characters from the source span into the destination, converting each character to uppercase,
/// using the casing rules of the specified culture.
///
/// The source span.
/// The destination span which contains the transformed characters.
/// An object that supplies culture-specific casing rules.
/// If the source and destinations overlap, this method behaves as if the original values are in
/// a temporary location before the destination is overwritten.
/// The number of characters written into the destination span. If the destination is too small, returns -1.
///
/// Thrown when is null.
///
public static int ToUpper(this ReadOnlySpan source, Span destination, CultureInfo culture)
{
if (culture == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.culture);
// Assuming that changing case does not affect length
if (destination.Length < source.Length)
return -1;
if (GlobalizationMode.Invariant)
TextInfo.ToUpperAsciiInvariant(source, destination);
else
culture.TextInfo.ChangeCase(source, destination, toUpper: true);
return source.Length;
}
///
/// Copies the characters from the source span into the destination, converting each character to uppercase
/// using the casing rules of the invariant culture.
///
/// The source span.
/// The destination span which contains the transformed characters.
/// If the source and destinations overlap, this method behaves as if the original values are in
/// a temporary location before the destination is overwritten.
/// The number of characters written into the destination span. If the destination is too small, returns -1.
public static int ToUpperInvariant(this ReadOnlySpan source, Span destination)
{
// Assuming that changing case does not affect length
if (destination.Length < source.Length)
return -1;
if (GlobalizationMode.Invariant)
TextInfo.ToUpperAsciiInvariant(source, destination);
else
CultureInfo.InvariantCulture.TextInfo.ChangeCase(source, destination, toUpper: true);
return source.Length;
}
///
/// Determines whether the end of the matches the specified when compared using the specified option.
///
/// The source span.
/// The sequence to compare to the end of the source span.
/// One of the enumeration values that determines how the and are compared.
public static bool EndsWith(this ReadOnlySpan span, ReadOnlySpan value, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
if (value.Length == 0)
{
return true;
}
if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
{
if (string.GetCaseCompareOfComparisonCulture(comparisonType) == CompareOptions.None)
return span.EndsWith(value);
return (span.Length >= value.Length) ? (CompareInfo.CompareOrdinalIgnoreCase(span.Slice(span.Length - value.Length), value) == 0) : false;
}
if (span.Length == 0)
{
return false;
}
return (comparisonType >= StringComparison.InvariantCulture) ?
CompareInfo.Invariant.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType)) :
CultureInfo.CurrentCulture.CompareInfo.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
}
///
/// Determines whether the beginning of the matches the specified when compared using the specified option.
///
/// The source span.
/// The sequence to compare to the beginning of the source span.
/// One of the enumeration values that determines how the and are compared.
public static bool StartsWith(this ReadOnlySpan span, ReadOnlySpan value, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
if (value.Length == 0)
{
return true;
}
if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
{
if (string.GetCaseCompareOfComparisonCulture(comparisonType) == CompareOptions.None)
return span.StartsWith(value);
return (span.Length >= value.Length) ? (CompareInfo.CompareOrdinalIgnoreCase(span.Slice(0, value.Length), value) == 0) : false;
}
if (span.Length == 0)
{
return false;
}
return (comparisonType >= StringComparison.InvariantCulture) ?
CompareInfo.Invariant.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType)) :
CultureInfo.CurrentCulture.CompareInfo.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
}
///
/// Creates a new span over the portion of the target array.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span AsSpan(this T[] array, int start)
{
if (array == null)
{
if (start != 0)
ThrowHelper.ThrowArgumentOutOfRangeException();
return default;
}
if (default(T) == null && array.GetType() != typeof(T[]))
ThrowHelper.ThrowArrayTypeMismatchException();
if ((uint)start > (uint)array.Length)
ThrowHelper.ThrowArgumentOutOfRangeException();
return new Span(ref Unsafe.Add(ref Unsafe.As(ref array.GetRawSzArrayData()), start), array.Length - start);
}
///
/// Creates a new readonly span over the portion of the target string.
///
/// The target string.
/// Returns default when is null.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan AsSpan(this string text)
{
if (text == null)
return default;
return new ReadOnlySpan(ref text.GetRawStringData(), text.Length);
}
///
/// Creates a new readonly span over the portion of the target string.
///
/// The target string.
/// The index at which to begin this slice.
/// Thrown when is null.
///
/// Thrown when the specified index is not in range (<0 or >text.Length).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan AsSpan(this string text, int start)
{
if (text == null)
{
if (start != 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return default;
}
if ((uint)start > (uint)text.Length)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return new ReadOnlySpan(ref Unsafe.Add(ref text.GetRawStringData(), start), text.Length - start);
}
///
/// Creates a new readonly span over the portion of the target string.
///
/// The target string.
/// The index at which to begin this slice.
/// The desired length for the slice (exclusive).
/// Returns default when is null.
///
/// Thrown when the specified index or is not in range.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan AsSpan(this string text, int start, int length)
{
if (text == null)
{
if (start != 0 || length != 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return default;
}
if ((uint)start > (uint)text.Length || (uint)length > (uint)(text.Length - start))
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return new ReadOnlySpan(ref Unsafe.Add(ref text.GetRawStringData(), start), length);
}
/// Creates a new over the portion of the target string.
/// The target string.
/// Returns default when is null.
public static ReadOnlyMemory AsMemory(this string text)
{
if (text == null)
return default;
return new ReadOnlyMemory(text, 0, text.Length);
}
/// Creates a new over the portion of the target string.
/// The target string.
/// The index at which to begin this slice.
/// Returns default when is null.
///
/// Thrown when the specified index is not in range (<0 or >text.Length).
///
public static ReadOnlyMemory AsMemory(this string text, int start)
{
if (text == null)
{
if (start != 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return default;
}
if ((uint)start > (uint)text.Length)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return new ReadOnlyMemory(text, start, text.Length - start);
}
/// Creates a new over the portion of the target string.
/// The target string.
/// The index at which to begin this slice.
/// The desired length for the slice (exclusive).
/// Returns default when is null.
///
/// Thrown when the specified index or is not in range.
///
public static ReadOnlyMemory AsMemory(this string text, int start, int length)
{
if (text == null)
{
if (start != 0 || length != 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return default;
}
if ((uint)start > (uint)text.Length || (uint)length > (uint)(text.Length - start))
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start);
return new ReadOnlyMemory(text, start, length);
}
}
}