Skip to content

Commit d68f753

Browse files
Checkpoint
1 parent 700c35e commit d68f753

3 files changed

Lines changed: 411 additions & 0 deletions

File tree

Source/StringSegmentSearch.cs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using Microsoft.Extensions.Primitives;
2+
using System;
3+
using System.Runtime.CompilerServices;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Open.Text;
7+
/// <summary>
8+
/// Represents a search operation within a string segment.
9+
/// </summary>
10+
public readonly ref struct StringSegmentSearch
11+
{
12+
/// <summary>
13+
/// The source segment where the search is to be performed.
14+
/// </summary>
15+
public StringSegment Source { get; }
16+
17+
/// <summary>
18+
/// The character sequences to be searched within the source.
19+
/// </summary>
20+
public ReadOnlySpan<char> Sequence { get; }
21+
22+
/// <summary>
23+
/// The string comparison option used for the search.
24+
/// </summary>
25+
public StringComparison Comparison { get; }
26+
27+
/// <summary>
28+
/// Indicates whether the search is to be performed from right to left.
29+
/// </summary>
30+
public bool RightToLeft { get; }
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="StringSegmentSearch"/> struct.
34+
/// </summary>
35+
/// <param name="source">
36+
/// <inheritdoc cref="Source" path="/summary"/>
37+
/// </param>
38+
/// <param name="search">
39+
/// <inheritdoc cref="Sequence" path="/summary"/>
40+
/// </param>
41+
/// <param name="comparisonType">
42+
/// <inheritdoc cref="Comparison" path="/summary"/>
43+
/// </param>
44+
/// <param name="rightToLeft">
45+
/// <inheritdoc cref="RightToLeft" path="/summary"/>
46+
/// </param>
47+
internal StringSegmentSearch(
48+
StringSegment source,
49+
ReadOnlySpan<char> search,
50+
StringComparison comparisonType,
51+
bool rightToLeft)
52+
{
53+
Source = source;
54+
Sequence = search;
55+
Comparison = comparisonType;
56+
RightToLeft = rightToLeft;
57+
}
58+
}
59+
60+
/// <summary>
61+
/// Represents a captured substring within a string segment based on a search operation.
62+
/// </summary>
63+
public readonly ref struct StringSegmentCapture
64+
{
65+
/// <summary>
66+
/// Gets the search operation that resulted in this capture.
67+
/// </summary>
68+
public StringSegmentSearch Search { get; }
69+
70+
/// <summary>
71+
/// Gets the captured substring as a string subsegment.
72+
/// </summary>
73+
public StringSubsegment Value { get; }
74+
75+
/// <summary>
76+
/// Returns <see langword="true"/> if the capture was successful; otherwise, <see langword="false"/>.
77+
/// </summary>
78+
public bool Success => Value.HasValue;
79+
80+
/// <summary>
81+
/// The index of the first character in the source <see cref="StringSegment"/> that is included in this subsegment.
82+
/// </summary>
83+
public int Index => Value.HasValue ? Value.Offset : -1;
84+
85+
/// <summary>
86+
/// Implicitly converts a <see cref="StringSegmentSearch"/> to a <see cref="StringSegmentCapture"/>.
87+
/// </summary>
88+
[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates")]
89+
public static implicit operator StringSegmentCapture(StringSegmentSearch capture)
90+
=> capture.First();
91+
92+
/// <summary>
93+
/// Implicitly converts a <see cref="StringSegmentCapture"/> to a <see cref="StringSubsegment"/>.
94+
/// </summary>
95+
[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates")]
96+
public static implicit operator StringSubsegment(StringSegmentCapture capture)
97+
=> capture.Value;
98+
99+
/// <summary>
100+
/// Implicitly converts a <see cref="StringSegmentCapture"/> to a <see cref="StringSegment"/>.
101+
/// </summary>
102+
[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates")]
103+
public static implicit operator StringSegment(StringSegmentCapture capture)
104+
=> capture.Value;
105+
106+
/// <inheritdoc />
107+
public override string ToString() => Value.ToString();
108+
109+
/// <summary>
110+
/// Initializes a new instance of the <see cref="StringSegmentCapture"/> struct.
111+
/// </summary>
112+
/// <param name="search">
113+
/// <inheritdoc cref="Search" path="/summary"/>
114+
/// </param>
115+
/// <param name="value">
116+
/// <inheritdoc cref="Value" path="/summary"/>
117+
/// </param>
118+
internal StringSegmentCapture(StringSegmentSearch search, StringSubsegment value)
119+
{
120+
Search = search;
121+
Value = value;
122+
}
123+
}
124+
125+
public static partial class TextExtensions
126+
{
127+
/// <summary>
128+
/// Starts a search for the specified character sequence within the source segment.
129+
/// </summary>
130+
/// <param name="source">
131+
/// <inheritdoc cref="StringSegmentSearch.Source" path="/summary"/>
132+
/// </param>
133+
/// <param name="search">
134+
/// <inheritdoc cref="StringSegmentSearch.Sequence" path="/summary"/>
135+
/// </param>
136+
/// <param name="comparisonType">
137+
/// <inheritdoc cref="StringSegmentSearch.Comparison" path="/summary"/>
138+
/// </param>
139+
/// <param name="rightToLeft">
140+
/// <inheritdoc cref="StringSegmentSearch.RightToLeft" path="/summary"/>
141+
/// </param>
142+
public static StringSegmentSearch Find(
143+
this StringSegment source,
144+
ReadOnlySpan<char> search,
145+
StringComparison comparisonType = StringComparison.Ordinal,
146+
bool rightToLeft = false)
147+
=> new(source, search, comparisonType, rightToLeft);
148+
149+
/// <inheritdoc cref="Find(StringSegment, ReadOnlySpan{char}, StringComparison, bool)"/>
150+
public static StringSegmentSearch Find(
151+
this string source,
152+
ReadOnlySpan<char> search,
153+
StringComparison comparisonType = StringComparison.Ordinal,
154+
bool rightToLeft = false)
155+
=> new(source, search, comparisonType, rightToLeft);
156+
157+
/// <summary>
158+
/// Finds the next occurrence of the specified character sequence within the source segment.
159+
/// </summary>
160+
public static StringSegmentCapture First(
161+
this StringSegmentSearch search)
162+
{
163+
if(search.Source.Length == 0 || search.Sequence.Length == 0)
164+
return default;
165+
166+
var i = search.RightToLeft
167+
? search.Source.LastIndexOf(search.Sequence, search.Comparison)
168+
: search.Source.IndexOf(search.Sequence, search.Comparison);
169+
170+
return new(search, i == -1
171+
? default
172+
: new(search.Source, i, search.Sequence.Length) );
173+
}
174+
175+
/// <summary>
176+
/// Finds the next occurrence of the specified character sequence within the source segment.
177+
/// </summary>
178+
public static StringSegmentCapture Next(
179+
this StringSegmentCapture capture)
180+
{
181+
var value = capture.Value;
182+
var len = value.Length;
183+
if (len == 0)
184+
return default;
185+
186+
var search = capture.Search;
187+
var source = search.Source;
188+
var i = capture.Search.RightToLeft
189+
? source.Subsegment(0, value.Offset).LastIndexOf(search.Sequence, search.Comparison)
190+
: source.IndexOf(search.Sequence, value.Offset + value.Length, search.Comparison);
191+
192+
return new(search, i == -1
193+
? default
194+
: new(source, i, len));
195+
}
196+
197+
/// <summary>
198+
/// Finds the next occurrence after the first occurrence of the specified character sequence within the source segment.
199+
/// </summary>
200+
public static StringSegmentCapture Next(
201+
this StringSegmentSearch search)
202+
=> search.First().Next();
203+
204+
/// <summary>
205+
/// Finds the last occurrence of the specified character sequence within the source segment.
206+
/// </summary>
207+
public static StringSegmentCapture Last(
208+
this StringSegmentSearch search)
209+
{
210+
if (search.Source.Length == 0 || search.Sequence.Length == 0)
211+
return default;
212+
213+
var i = search.RightToLeft
214+
? search.Source.IndexOf(search.Sequence, search.Comparison)
215+
: search.Source.LastIndexOf(search.Sequence, search.Comparison);
216+
217+
return new(search, i == -1
218+
? default
219+
: new(search.Source, i, search.Sequence.Length));
220+
}
221+
222+
/// <summary>
223+
/// Returns <see langword="true"/> if the capture has a value; otherwise <see langword="false"/>.
224+
/// </summary>
225+
public static bool Exists(this StringSegmentCapture capture)
226+
=> capture.Value.HasValue;
227+
228+
/// <summary>
229+
/// Returns <see langword="true"/> if the search has a value; otherwise <see langword="false"/>.
230+
/// </summary>
231+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
232+
public static bool Exists(this StringSegmentSearch search)
233+
=> search.First().Exists();
234+
235+
/// <summary>
236+
/// Resolves the value of the capture, or returns the specified default value.
237+
/// </summary>
238+
public static StringSubsegment Or(
239+
this StringSegmentCapture capture,
240+
StringSubsegment defaultValue)
241+
{
242+
var value = capture.Value;
243+
return value.HasValue ? value : defaultValue;
244+
}
245+
246+
/// <summary>
247+
/// Resolves the value of the search, or returns the specified default value.
248+
/// </summary>
249+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
250+
public static StringSubsegment Or(
251+
this StringSegmentSearch capture,
252+
StringSubsegment defaultValue)
253+
=> capture.First().Or(defaultValue);
254+
}

Source/StringSubsegment.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Microsoft.Extensions.Primitives;
2+
using System;
3+
using System.Diagnostics;
4+
using System.Runtime.CompilerServices;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace Open.Text;
8+
9+
/// <summary>
10+
/// A struct for representing a subsegment of a <see cref="StringSegment"/>.
11+
/// </summary>
12+
public readonly struct StringSubsegment : IEquatable<StringSubsegment>
13+
{
14+
/// <summary>
15+
/// Constructs a new <see cref="StringSubsegment"/> from the specified <see cref="StringSegment"/>.
16+
/// </summary>
17+
/// <param name="source"><inheritdoc cref="Source" path="/summary"/></param>
18+
/// <param name="offset"><inheritdoc cref="Offset" path="/summary"/></param>
19+
/// <param name="length"><inheritdoc cref="Length" path="/summary"/></param>
20+
internal StringSubsegment(
21+
StringSegment source,
22+
int offset,
23+
int length)
24+
{
25+
Debug.Assert(source.HasValue);
26+
Debug.Assert(offset >= 0 && offset <= source.Length);
27+
Debug.Assert(length >= 0 && offset + length <= source.Length);
28+
29+
Source = source;
30+
Offset = offset;
31+
Length = length;
32+
}
33+
34+
/// <summary>
35+
/// Indicates that the <see cref="Source"/> is has a value.
36+
/// </summary>
37+
public bool HasValue => Source.HasValue;
38+
39+
/// <summary>
40+
/// The source <see cref="StringSegment"/> from which this subsegment was created.
41+
/// </summary>
42+
public StringSegment Source { get; }
43+
44+
/// <summary>
45+
/// The index of the first character in the source <see cref="StringSegment"/> that is included in this subsegment.
46+
/// </summary>
47+
public int Offset { get; }
48+
49+
/// <summary>
50+
/// The number of characters in the source <see cref="StringSegment"/> that are included in this subsegment.
51+
/// </summary>
52+
public int Length { get; }
53+
54+
/// <summary>
55+
/// Gets a <see cref="ReadOnlySpan{T}"/> representing the defined subsegment.
56+
/// </summary>
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
public ReadOnlySpan<char> AsSpan()
59+
=> Source.AsSpan(Offset, Length);
60+
61+
/// <summary>
62+
/// Returns a new <see cref="StringSegment"/> representing the defined subsegment.
63+
/// </summary>
64+
public StringSegment AsSegment()
65+
=> Offset == 0 && Source.Length == Length ? Source : Source.Subsegment(Offset, Length);
66+
67+
/// <summary>
68+
/// Returns a new <see cref="StringSegment"/> representing the defined subsegment.
69+
/// </summary>
70+
[SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "AsSegment")]
71+
public static implicit operator StringSegment(StringSubsegment segment)
72+
=> segment.AsSegment();
73+
74+
/// <summary>
75+
/// Compares a read-only span of characters to this <see cref="StringSubsegment"/> for equality.
76+
/// </summary>
77+
public bool Equals(ReadOnlySpan<char> other, StringComparison comparisonType = StringComparison.Ordinal)
78+
=> Source.HasValue && other.Length == Length && AsSpan().Equals(other, comparisonType);
79+
80+
/// <summary>
81+
/// Compares two <see cref="StringSubsegment"/> instances for equality.
82+
/// </summary>
83+
public bool Equals(StringSubsegment other, StringComparison comparisonType = StringComparison.Ordinal)
84+
=> Source.HasValue
85+
? other.Source.HasValue && other.Length == Length && Equals(other.AsSpan(), comparisonType)
86+
: !other.Source.HasValue;
87+
88+
/// <inheritdoc />
89+
public override bool Equals(object obj)
90+
=> obj is StringSubsegment other && Equals(other);
91+
92+
/// <inheritdoc />
93+
public override int GetHashCode()
94+
=> AsSegment().GetHashCode();
95+
96+
/// <inheritdoc />
97+
public override string ToString()
98+
=> AsSpan().ToString();
99+
100+
bool IEquatable<StringSubsegment>.Equals(StringSubsegment other)
101+
=> Equals(other);
102+
103+
/// <summary>
104+
/// Compares two <see cref="StringSubsegment"/> instances for equality.
105+
/// </summary>
106+
public static bool operator ==(StringSubsegment left, StringSubsegment right)
107+
=> left.Equals(right);
108+
109+
/// <summary>
110+
/// Compares two <see cref="StringSubsegment"/> instances for inequality.
111+
/// </summary>
112+
public static bool operator !=(StringSubsegment left, StringSubsegment right)
113+
=> !left.Equals(right);
114+
}

0 commit comments

Comments
 (0)