diff --git a/src/Microsoft.Extensions.Primitives/StringSegment.cs b/src/Microsoft.Extensions.Primitives/StringSegment.cs index 4550a37ff8d..a2d8d120a94 100644 --- a/src/Microsoft.Extensions.Primitives/StringSegment.cs +++ b/src/Microsoft.Extensions.Primitives/StringSegment.cs @@ -11,6 +11,11 @@ namespace Microsoft.Extensions.Primitives /// public struct StringSegment : IEquatable, IEquatable { + /// + /// A for . + /// + public static readonly StringSegment Empty = string.Empty; + /// /// Initializes an instance of the struct. /// @@ -113,6 +118,24 @@ public bool HasValue get { return Buffer != null; } } + /// + /// Gets the at a specified position in the current . + /// + /// The offset into the + /// The at a specified position. + public char this[int index] + { + get + { + if (index < 0 || (uint)index >= (uint)Length) + { + throw new IndexOutOfRangeException(); + } + + return Buffer[Offset + index]; + } + } + /// public override bool Equals(object obj) { @@ -152,6 +175,21 @@ public bool Equals(StringSegment other, StringComparison comparisonType) return string.Compare(Buffer, Offset, other.Buffer, other.Offset, textLength, comparisonType) == 0; } + // This handles StringSegment.Equals(string, StringSegment, StringComparison) and StringSegment.Equals(StringSegment, string, StringComparison) + // via the implicit type converter + /// + /// Determines whether two specified StringSegment objects have the same value. A parameter specifies the culture, case, and + /// sort rules used in the comparison. + /// + /// The first StringSegment to compare. + /// The second StringSegment to compare. + /// One of the enumeration values that specifies the rules for the comparison. + /// true if the objects are equal; otherwise, false. + public static bool Equals(StringSegment a, StringSegment b, StringComparison comparisonType) + { + return a.Equals(b, comparisonType); + } + /// /// Checks if the specified is equal to the current . /// @@ -224,6 +262,16 @@ public override int GetHashCode() return !left.Equals(right); } + // PERF: Do NOT add a implicit converter from StringSegment to String. That would negate most of the perf safety. + /// + /// Creates a new from the given . + /// + /// The to convert to a + public static implicit operator StringSegment(string value) + { + return new StringSegment(value); + } + /// /// Checks if the beginning of this matches the specified when compared using the specified . /// @@ -295,6 +343,18 @@ public string Substring(int offset, int length) return Buffer.Substring(Offset + offset, length); } + /// + /// Retrieves a that represents a substring from this . + /// The starts at the position specified by . + /// + /// The zero-based starting character position of a substring in this . + /// A that begins at in this + /// whose length is the remainder. + public StringSegment Subsegment(int offset) + { + return Subsegment(offset, Length - offset); + } + /// /// Retrieves a that represents a substring from this . /// The starts at the position specified by and has the specified . @@ -423,6 +483,16 @@ public StringSegment TrimEnd() return new StringSegment(Buffer, Offset, trimmedEnd - Offset + 1); } + /// + /// Indicates whether the specified StringSegment is null or an Empty string. + /// + /// The StringSegment to test. + /// + public static bool IsNullOrEmpty(StringSegment value) + { + return !value.HasValue || value.Length == 0; + } + /// /// Returns the represented by this or String.Empty if the does not contain a value. /// diff --git a/test/Microsoft.Extensions.Primitives.Tests/StringSegmentTest.cs b/test/Microsoft.Extensions.Primitives.Tests/StringSegmentTest.cs index a02e9a2fc8c..555e7621d6e 100644 --- a/test/Microsoft.Extensions.Primitives.Tests/StringSegmentTest.cs +++ b/test/Microsoft.Extensions.Primitives.Tests/StringSegmentTest.cs @@ -8,6 +8,30 @@ namespace Microsoft.Extensions.Primitives { public class StringSegmentTest { + [Fact] + public void StringSegment_Empty() + { + // Arrange & Act + var segment = StringSegment.Empty; + + // Assert + Assert.True(segment.HasValue); + Assert.Same(string.Empty, segment.Value); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Length); + } + + [Fact] + public void StringSegment_ImplicitConvertFromString() + { + StringSegment segment = "Hello"; + + Assert.True(segment.HasValue); + Assert.Equal(0, segment.Offset); + Assert.Equal(5, segment.Length); + Assert.Equal("Hello", segment.Value); + } + [Fact] public void StringSegment_StringCtor_AllowsNullBuffers() { @@ -136,6 +160,33 @@ public void StringSegment_HasValue_Invalid() Assert.False(hasValue); } + [Theory] + [InlineData("a", 0, 1, 0, 'a')] + [InlineData("abc", 1, 1, 0, 'b')] + [InlineData("abcdef", 1, 4, 0, 'b')] + [InlineData("abcdef", 1, 4, 1, 'c')] + [InlineData("abcdef", 1, 4, 2, 'd')] + [InlineData("abcdef", 1, 4, 3, 'e')] + public void StringSegment_Indexer_InRange(string value, int offset, int length, int index, char expected) + { + var segment = new StringSegment(value, offset, length); + + var result = segment[index]; + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("", 0, 0, 0)] + [InlineData("a", 0, 1, -1)] + [InlineData("a", 0, 1, 1)] + public void StringSegment_Indexer_OutOfRangeThrows(string value, int offset, int length, int index) + { + var segment = new StringSegment(value, offset, length); + + Assert.Throws(() => segment[index]); + } + public static TheoryData EndsWithData { get @@ -259,6 +310,46 @@ public void StringSegment_Equals_String_Valid(string candidate, StringComparison Assert.Equal(expectedResult, result); } + [Fact] + public void StringSegment_StaticEquals_Valid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + var segment2 = new StringSegment("Your Carport is blue", 5, 3); + + Assert.True(StringSegment.Equals(segment1, segment2)); + } + + [Fact] + public void StringSegment_StaticEquals_Invalid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 4); + var segment2 = new StringSegment("Your Carport is blue", 5, 4); + + Assert.False(StringSegment.Equals(segment1, segment2)); + } + + [Fact] + public void StringSegment_IsNullOrEmpty_Valid() + { + Assert.True(StringSegment.IsNullOrEmpty(null)); + Assert.True(StringSegment.IsNullOrEmpty(string.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(null))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty))); + Assert.True(StringSegment.IsNullOrEmpty(StringSegment.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty, 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 3, 0))); + } + + [Fact] + public void StringSegment_IsNullOrEmpty_Invalid() + { + Assert.False(StringSegment.IsNullOrEmpty("A")); + Assert.False(StringSegment.IsNullOrEmpty("ABCDefg")); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("A", 0 , 1))); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("ABCDefg", 3, 2))); + } + public static TheoryData GetHashCode_ReturnsSameValueForEqualSubstringsData { get