// 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); } } }