Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/Microsoft.Extensions.Primitives/StringSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Microsoft.Extensions.Primitives
/// </summary>
public struct StringSegment : IEquatable<StringSegment>, IEquatable<string>
{
/// <summary>
/// A <see cref="StringSegment"/> for <see cref="string.Empty"/>.
/// </summary>
public static readonly StringSegment Empty = string.Empty;

/// <summary>
/// Initializes an instance of the <see cref="StringSegment"/> struct.
/// </summary>
Expand Down Expand Up @@ -113,6 +118,24 @@ public bool HasValue
get { return Buffer != null; }
}

/// <summary>
/// Gets the <see cref="char"/> at a specified position in the current <see cref="StringSegment"/>.
/// </summary>
/// <param name="index">The offset into the <see cref="StringSegment"/></param>
/// <returns>The <see cref="char"/> at a specified position.</returns>
public char this[int index]
{
get
{
if (index < 0 || (uint)index >= (uint)Length)
{
throw new IndexOutOfRangeException();
}

return Buffer[Offset + index];
}
}

/// <inheritdoc />
public override bool Equals(object obj)
{
Expand Down Expand Up @@ -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
/// <summary>
/// Determines whether two specified StringSegment objects have the same value. A parameter specifies the culture, case, and
/// sort rules used in the comparison.
/// </summary>
/// <param name="a">The first StringSegment to compare.</param>
/// <param name="b">The second StringSegment to compare.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the comparison.</param>
/// <returns><code>true</code> if the objects are equal; otherwise, <code>false</code>.</returns>
public static bool Equals(StringSegment a, StringSegment b, StringComparison comparisonType)
{
return a.Equals(b, comparisonType);
}

/// <summary>
/// Checks if the specified <see cref="string"/> is equal to the current <see cref="StringSegment"/>.
/// </summary>
Expand Down Expand Up @@ -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.
/// <summary>
/// Creates a new <see cref="StringSegment"/> from the given <see cref="string"/>.
/// </summary>
/// <param name="value">The <see cref="string"/> to convert to a <see cref="StringSegment"/></param>
public static implicit operator StringSegment(string value)
{
return new StringSegment(value);
}

/// <summary>
/// Checks if the beginning of this <see cref="StringSegment"/> matches the specified <see cref="string"/> when compared using the specified <paramref name="comparisonType"/>.
/// </summary>
Expand Down Expand Up @@ -295,6 +343,18 @@ public string Substring(int offset, int length)
return Buffer.Substring(Offset + offset, length);
}

/// <summary>
/// Retrieves a <see cref="StringSegment"/> that represents a substring from this <see cref="StringSegment"/>.
/// The <see cref="StringSegment"/> starts at the position specified by <paramref name="offset"/>.
/// </summary>
/// <param name="offset">The zero-based starting character position of a substring in this <see cref="StringSegment"/>.</param>
/// <returns>A <see cref="StringSegment"/> that begins at <paramref name="offset"/> in this <see cref="StringSegment"/>
/// whose length is the remainder.</returns>
public StringSegment Subsegment(int offset)
{
return Subsegment(offset, Length - offset);
}

/// <summary>
/// Retrieves a <see cref="StringSegment"/> that represents a substring from this <see cref="StringSegment"/>.
/// The <see cref="StringSegment"/> starts at the position specified by <paramref name="offset"/> and has the specified <paramref name="length"/>.
Expand Down Expand Up @@ -423,6 +483,16 @@ public StringSegment TrimEnd()
return new StringSegment(Buffer, Offset, trimmedEnd - Offset + 1);
}

/// <summary>
/// Indicates whether the specified StringSegment is null or an Empty string.
/// </summary>
/// <param name="value">The StringSegment to test.</param>
/// <returns></returns>
public static bool IsNullOrEmpty(StringSegment value)
{
return !value.HasValue || value.Length == 0;
}

/// <summary>
/// Returns the <see cref="string"/> represented by this <see cref="StringSegment"/> or <code>String.Empty</code> if the <see cref="StringSegment"/> does not contain a value.
/// </summary>
Expand Down
91 changes: 91 additions & 0 deletions test/Microsoft.Extensions.Primitives.Tests/StringSegmentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<IndexOutOfRangeException>(() => segment[index]);
}

public static TheoryData<string, StringComparison, bool> EndsWithData
{
get
Expand Down Expand Up @@ -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
Expand Down