Skip to content

Commit 551d6f6

Browse files
V3 update.
1 parent 7d01e97 commit 551d6f6

11 files changed

Lines changed: 840 additions & 344 deletions

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
# Open.Text
22

3-
A set of useful classes for working with strings and formatting values.
3+
A set of useful classes for working with strings, spans, and value formatting.
44

55
[![NuGet](https://img.shields.io/nuget/v/Open.Text.svg)](https://www.nuget.org/packages/Open.Text/)
6+
7+
## Features
8+
9+
* v3.x is a major overhaul with much improved methods and expanded tests and coverage.
10+
* Avoids allocation wherever possible.
11+
12+
### String vs Span Equality
13+
14+
Optimized `.Equals(...)` extension methods for comparing spans and strings.
15+
16+
### String & Span Splitting
17+
18+
#### `SplitToEnumerable`
19+
20+
Returns each string segment of the split through an enumerable instead of all at once in an array.
21+
22+
#### `SplitToMemory`
23+
24+
Produces an enumerable where each segment is yielded as a `ReadOnlyMemory<char>`.
25+
26+
### Trimming
27+
28+
#### `TrimStartPattern` & `TrimEndPattern`
29+
30+
Similar to their character trimming counterparts, these methods can trim sequences of characters or regular expression patterns.
31+
32+
### String Segments
33+
34+
Similar to `ArraySegment`, `StringSegment` offers methods for operating on strings without requiring allocation.
35+
36+
Instead of extensions like `string.BeforeFirst(search)`, now you can call `string.First(search).Preceding()`.
37+
38+
### StringBuilder Extensions
39+
40+
* Extensions for adding segments with separators.
41+
* Extensions for adding spans without creating a string first.
42+
* Extensions for converting enumerables to a `StringBuilder`.
43+
44+
### ... And more
45+
46+
Various formatting and `Regex` extensions including `Capture.AsSpan()` for getting a `ReadOnlySpan<char>` instead of allocating a string.
47+

Source/Extensions.Split.cs

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Diagnostics;
45
using System.Diagnostics.Contracts;
56
using System.Linq;
@@ -144,7 +145,7 @@ public static ReadOnlySpan<char> FirstSplit(this ReadOnlySpan<char> source,
144145
}
145146

146147
var i = source.IndexOf(splitSequence, comparisonType);
147-
return FirstSplitSpan(source, i, 1, out nextIndex);
148+
return FirstSplitSpan(source, i, splitSequence.Length, out nextIndex);
148149
}
149150

150151
#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
@@ -198,19 +199,18 @@ IEnumerable<string> SplitAsEnumerableCoreOmitEmpty()
198199
}
199200
}
200201

201-
202202
/// <summary>
203203
/// Enumerates a string by segments that are separated by the split character.
204204
/// </summary>
205205
/// <param name="source">The source characters to look through.</param>
206206
/// <param name="splitSequence">The sequence to find.</param>
207-
/// <param name="comparisonType">The string comparison type to use.</param>
208207
/// <param name="options">Can specify to omit empty entries.</param>
208+
/// <param name="comparisonType">The string comparison type to use.</param>
209209
/// <returns>The portion of the source up to and excluding the sequence searched for.</returns>
210210
public static IEnumerable<string> SplitToEnumerable(this string source,
211211
string splitSequence,
212-
StringComparison comparisonType = StringComparison.Ordinal,
213-
StringSplitOptions options = StringSplitOptions.None)
212+
StringSplitOptions options = StringSplitOptions.None,
213+
StringComparison comparisonType = StringComparison.Ordinal)
214214
{
215215
if (source is null) throw new ArgumentNullException(nameof(source));
216216
if (splitSequence is null) throw new ArgumentNullException(nameof(splitSequence));
@@ -301,8 +301,8 @@ IEnumerable<ReadOnlyMemory<char>> SplitAsMemoryOmitEmpty()
301301
/// <inheritdoc cref="SplitToEnumerable(string, string, StringComparison, StringSplitOptions)"/>
302302
public static IEnumerable<ReadOnlyMemory<char>> SplitAsMemory(this string source,
303303
string splitSequence,
304-
StringComparison comparisonType = StringComparison.Ordinal,
305-
StringSplitOptions options = StringSplitOptions.None)
304+
StringSplitOptions options = StringSplitOptions.None,
305+
StringComparison comparisonType = StringComparison.Ordinal)
306306
{
307307
if (source is null) throw new ArgumentNullException(nameof(source));
308308
if (splitSequence is null) throw new ArgumentNullException(nameof(splitSequence));
@@ -324,9 +324,10 @@ public static IEnumerable<ReadOnlyMemory<char>> SplitAsMemory(this string source
324324
IEnumerable<ReadOnlyMemory<char>> SplitAsMemoryCore()
325325
{
326326
var startIndex = 0;
327+
var splitLen = splitSequence.Length;
327328
do
328329
{
329-
yield return FirstSplitMemory(source, startIndex, source.IndexOf(splitSequence, startIndex, comparisonType), 1, out var nextIndex);
330+
yield return FirstSplitMemory(source, startIndex, source.IndexOf(splitSequence, startIndex, comparisonType), splitLen, out var nextIndex);
330331
startIndex = nextIndex;
331332
}
332333
while (startIndex != -1);
@@ -335,16 +336,113 @@ IEnumerable<ReadOnlyMemory<char>> SplitAsMemoryCore()
335336
IEnumerable<ReadOnlyMemory<char>> SplitAsMemoryOmitEmpty()
336337
{
337338
var startIndex = 0;
339+
var splitLen = splitSequence.Length;
338340
do
339341
{
340-
var result = FirstSplitMemory(source, startIndex, source.IndexOf(splitSequence, startIndex, comparisonType), 1, out var nextIndex);
342+
var result = FirstSplitMemory(source, startIndex, source.IndexOf(splitSequence, startIndex, comparisonType), splitLen, out var nextIndex);
341343
if (result.Length != 0) yield return result;
342344
startIndex = nextIndex;
343345
}
344346
while (startIndex != -1);
345347
}
346348
}
349+
347350
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
348351

352+
static readonly ImmutableArray<string> SingleEmpty = ImmutableArray.Create(string.Empty);
353+
354+
/// <summary>
355+
/// Splits a sequence of characters into strings using the character provided.
356+
/// </summary>
357+
/// <param name="source">The source string to split up.</param>
358+
/// <param name="splitCharacter">The character to split by.</param>
359+
/// <param name="options">Can specify to omit empty entries.</param>
360+
/// <returns>The resultant list of string segments.</returns>
361+
public static IReadOnlyList<string> Split(this ReadOnlySpan<char> source,
362+
char splitCharacter,
363+
StringSplitOptions options = StringSplitOptions.None)
364+
{
365+
switch (options)
366+
{
367+
case StringSplitOptions.None when source.Length == 0:
368+
return SingleEmpty;
369+
370+
case StringSplitOptions.RemoveEmptyEntries when source.Length == 0:
371+
return ImmutableArray<string>.Empty;
372+
373+
case StringSplitOptions.RemoveEmptyEntries:
374+
{
375+
Debug.Assert(!source.IsEmpty);
376+
var list = new List<string>();
377+
378+
loop:
379+
var result = source.FirstSplit(splitCharacter, out var nextIndex);
380+
if (!result.IsEmpty) list.Add(result.ToString());
381+
if (nextIndex == -1) return list;
382+
source = source.Slice(nextIndex);
383+
goto loop;
384+
}
385+
386+
default:
387+
{
388+
Debug.Assert(!source.IsEmpty);
389+
var list = new List<string>();
390+
loop:
391+
var result = source.FirstSplit(splitCharacter, out var nextIndex);
392+
list.Add(result.IsEmpty ? string.Empty : result.ToString());
393+
if (nextIndex == -1) return list;
394+
source = source.Slice(nextIndex);
395+
goto loop;
396+
}
397+
}
398+
}
399+
400+
/// <summary>
401+
/// Splits a sequence of characters into strings using the character sequence provided.
402+
/// </summary>
403+
/// <param name="source">The source string to split up.</param>
404+
/// <param name="splitSequence">The sequence to split by.</param>
405+
/// <param name="options">Can specify to omit empty entries.</param>
406+
/// <param name="comparisonType">The optional comparsion type.</param>
407+
/// <returns>The resultant list of string segments.</returns>
408+
public static IReadOnlyList<string> Split(this ReadOnlySpan<char> source,
409+
in ReadOnlySpan<char> splitSequence,
410+
StringSplitOptions options = StringSplitOptions.None,
411+
StringComparison comparisonType = StringComparison.Ordinal)
412+
{
413+
switch (options)
414+
{
415+
case StringSplitOptions.None when source.IsEmpty:
416+
return SingleEmpty;
417+
418+
case StringSplitOptions.RemoveEmptyEntries when source.IsEmpty:
419+
return ImmutableArray<string>.Empty;
420+
421+
case StringSplitOptions.RemoveEmptyEntries:
422+
{
423+
Debug.Assert(!source.IsEmpty);
424+
var list = new List<string>();
425+
426+
loop:
427+
var result = source.FirstSplit(splitSequence, out var nextIndex, comparisonType);
428+
if (!result.IsEmpty) list.Add(result.ToString());
429+
if (nextIndex == -1) return list;
430+
source = source.Slice(nextIndex);
431+
goto loop;
432+
}
433+
434+
default:
435+
{
436+
Debug.Assert(!source.IsEmpty);
437+
var list = new List<string>();
438+
loop:
439+
var result = source.FirstSplit(splitSequence, out var nextIndex, comparisonType);
440+
list.Add(result.IsEmpty ? string.Empty : result.ToString());
441+
if (nextIndex == -1) return list;
442+
source = source.Slice(nextIndex);
443+
goto loop;
444+
}
445+
}
446+
}
349447
}
350448
}

Source/Extensions.StringSegment.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Diagnostics.Contracts;
3+
4+
namespace Open.Text
5+
{
6+
public static partial class Extensions
7+
{
8+
/// <summary>
9+
/// Returns a StringSegment representing the source string.
10+
/// </summary>
11+
public static StringSegment AsSegment(this string source)
12+
=> StringSegment.Create(source);
13+
14+
/// <inheritdoc cref="AsSegment(string)"/>
15+
/// <param name="start">The index to start the segment.</param>
16+
public static StringSegment AsSegment(this string source, int start)
17+
=> StringSegment.Create(source, start);
18+
19+
/// <inheritdoc cref="AsSegment(string, int)"/>
20+
/// <param name="length">The length of the segment.</param>
21+
public static StringSegment AsSegment(this string source, int start, int length)
22+
=> StringSegment.Create(source, start, length);
23+
24+
/// <summary>
25+
/// Finds the first instance of a string and returns a StringSegment for subsequent use.
26+
/// </summary>
27+
/// <param name="source">The source string to search.</param>
28+
/// <param name="search">The string pattern to look for.</param>
29+
/// <param name="comparisonType">The string comparision type to use. Default is Ordinal.</param>
30+
/// <returns>
31+
/// The segment representing the found string.
32+
/// If not found, the StringSegment.IsValid property will be false.
33+
/// </returns>
34+
public static StringSegment First(this string source, string search, StringComparison comparisonType = StringComparison.Ordinal)
35+
{
36+
if (source is null) throw new ArgumentNullException(nameof(source));
37+
if (search is null) throw new ArgumentNullException(nameof(search));
38+
Contract.EndContractBlock();
39+
40+
if (search.Length == 0)
41+
return default;
42+
43+
var i = source.IndexOf(search, comparisonType);
44+
return i == -1 ? default : StringSegment.Create(source, i, search.Length);
45+
}
46+
47+
/// <inheritdoc cref="First(string, string, StringComparison)" />
48+
public static StringSegment First(this StringSegment source, in ReadOnlySpan<char> search, StringComparison comparisonType = StringComparison.Ordinal)
49+
{
50+
if (!source.IsValid) throw new ArgumentException("Must be a valid segment.", nameof(source));
51+
Contract.EndContractBlock();
52+
53+
if (search.IsEmpty)
54+
return default;
55+
56+
var i = source.AsSpan().IndexOf(search, comparisonType);
57+
return i == -1 ? default : StringSegment.Create(source.Source, source.Index + i, search.Length);
58+
}
59+
60+
/// <inheritdoc cref="First(string, string, StringComparison)" />
61+
public static StringSegment First(this StringSegment source, string search, StringComparison comparisonType = StringComparison.Ordinal)
62+
{
63+
if (search is null) throw new ArgumentNullException(nameof(search));
64+
Contract.EndContractBlock();
65+
66+
return First(source, search.AsSpan(), comparisonType);
67+
}
68+
69+
/// <inheritdoc cref="First(string, string, StringComparison)"/>
70+
/// <summary>
71+
/// Finds the last instance of a string and returns a StringSegment for subsequent use.
72+
/// </summary>
73+
public static StringSegment Last(this string source, string search, StringComparison comparisonType = StringComparison.Ordinal)
74+
{
75+
if (source is null) throw new ArgumentNullException(nameof(source));
76+
if (search is null) throw new ArgumentNullException(nameof(search));
77+
Contract.EndContractBlock();
78+
79+
if (search.Length == 0)
80+
return default;
81+
82+
var i = source.LastIndexOf(search, comparisonType);
83+
return i == -1 ? default : StringSegment.Create(source, i, search.Length);
84+
}
85+
86+
/// <inheritdoc cref="Last(string, string, StringComparison)" />
87+
public static StringSegment Last(this StringSegment source, in ReadOnlySpan<char> search, StringComparison comparisonType = StringComparison.Ordinal)
88+
{
89+
if (!source.IsValid) throw new ArgumentException("Must be a valid segment.", nameof(source));
90+
Contract.EndContractBlock();
91+
if (search.IsEmpty)
92+
return default;
93+
94+
var i = source.AsSpan().LastIndexOf(search, comparisonType);
95+
return i == -1 ? default : StringSegment.Create(source.Source, source.Index + i, search.Length);
96+
}
97+
98+
/// <inheritdoc cref="Last(string, string, StringComparison)" />
99+
public static StringSegment Last(this StringSegment source, string search, StringComparison comparisonType = StringComparison.Ordinal)
100+
{
101+
if (search is null) throw new ArgumentNullException(nameof(search));
102+
Contract.EndContractBlock();
103+
104+
return Last(source, search.AsSpan(), comparisonType);
105+
}
106+
107+
108+
}
109+
}

0 commit comments

Comments
 (0)