From c97ca19299bc020891273b20ee9cdafb918f4b23 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sat, 27 Jul 2024 10:12:59 -0700 Subject: [PATCH 1/8] Add new string extension methods and optimize usings Introduced new extension methods for ReadOnlySpan, StringSegment, and string to enhance string operations with various comparison options. Added global using directives for commonly used namespaces to simplify code. Removed redundant using directives and updated project version to 7.1.0. --- Benchmarks/EnumParseTests.cs | 1 - Source/EnumValue.cs | 11 +- Source/Extensions.Equals.cs | 5 +- Source/Extensions.FirstLast.cs | 137 ++++++++++++ Source/Extensions.IndexOf.cs | 343 +++++++++++++++++++++++++++++ Source/Extensions.Split.cs | 9 +- Source/Extensions.StringSegment.cs | 255 +-------------------- Source/Extensions.Trim.cs | 8 +- Source/Extensions._.cs | 78 +------ Source/Open.Text.csproj | 3 +- Source/SpanComparable.cs | 5 +- Source/StringBuilderEnumerator.cs | 5 +- Source/StringBuilderExtensions.cs | 7 +- Source/StringBuilderHelper.cs | 6 +- Source/StringComparable.cs | 6 +- Source/StringSegmentComparer.cs | 7 +- Source/StringSegmentEnumerator.cs | 5 +- Source/StringSegmentSearch.cs | 7 +- Source/StringSubsegment.cs | 6 +- Source/_Global.cs | 8 + 20 files changed, 508 insertions(+), 404 deletions(-) create mode 100644 Source/Extensions.FirstLast.cs create mode 100644 Source/Extensions.IndexOf.cs create mode 100644 Source/_Global.cs diff --git a/Benchmarks/EnumParseTests.cs b/Benchmarks/EnumParseTests.cs index 646c491..7759074 100644 --- a/Benchmarks/EnumParseTests.cs +++ b/Benchmarks/EnumParseTests.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using FastEnumUtility; -using System.Security; namespace Open.Text.Benchmarks; diff --git a/Source/EnumValue.cs b/Source/EnumValue.cs index b2ac3bc..d579153 100644 --- a/Source/EnumValue.cs +++ b/Source/EnumValue.cs @@ -1,15 +1,6 @@ -// Ignore Spelling: Deconstruct - -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using System.Threading; using static System.Linq.Expressions.Expression; //using static FastExpressionCompiler.LightExpression.Expression; namespace Open.Text; diff --git a/Source/Extensions.Equals.cs b/Source/Extensions.Equals.cs index 5f15444..6f2f202 100644 --- a/Source/Extensions.Equals.cs +++ b/Source/Extensions.Equals.cs @@ -1,7 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; - -namespace Open.Text; +namespace Open.Text; public static partial class TextExtensions { diff --git a/Source/Extensions.FirstLast.cs b/Source/Extensions.FirstLast.cs new file mode 100644 index 0000000..999160c --- /dev/null +++ b/Source/Extensions.FirstLast.cs @@ -0,0 +1,137 @@ +namespace Open.Text; +public static partial class TextExtensions +{ + /// + /// Finds the first instance of a string and returns a StringSegment for subsequent use. + /// + /// The source string to search. + /// The string pattern to look for. + /// The string comparison type to use. Default is Ordinal. + /// + /// The segment representing the found string. + /// If not found, the result will have a length of 0 and its property will be . + /// + public static StringSegment First(this StringSegment source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); + Contract.EndContractBlock(); + + if (search.Length == 0) + return default; + + var i = source.IndexOf(search, comparisonType); + return i == -1 ? default : source.Subsegment(i, search.Length); + } + + /// + public static StringSegment First(this StringSegment source, char search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); + Contract.EndContractBlock(); + + var i = source.IndexOf(search, comparisonType); + return i == -1 ? default : source.Subsegment(i, 1); + } + + /// + public static StringSegment First(this string source, char search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + Contract.EndContractBlock(); + + var i = source.IndexOf(search, comparisonType); + return i == -1 ? default : source.AsSegment(i, 1); + } + + /// + public static StringSegment First(this string source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + Contract.EndContractBlock(); + + return First(source.AsSegment(), search, comparisonType); + } + + /// + /// Finds the first instance of a pattern and returns a StringSegment for subsequent use. + /// + /// The source string to search. + /// The pattern to look for. + /// + /// The segment representing the found string. + /// If not found, the StringSegment.HasValue property will be false. + /// + /// If the pattern is right-to-left, then it will return the first segment from the right. + [ExcludeFromCodeCoverage] // Reason: would just test already tested code. + public static StringSegment First(this string source, Regex pattern) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (pattern is null) throw new ArgumentNullException(nameof(pattern)); + Contract.EndContractBlock(); + + var match = pattern.Match(source); + return match.Success ? new(source, match.Index, match.Length) : default; + } + + /// + public static StringSegment First(this StringSegment source, ReadOnlySpan search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); + Contract.EndContractBlock(); + + if (search.IsEmpty) + return default; + + var i = source.AsSpan().IndexOf(search, comparisonType); + return i == -1 ? default : new(source.Buffer, source.Offset + i, search.Length); + } + + /// + /// Finds the last instance of a string and returns a StringSegment for subsequent use. + /// + /// + public static StringSegment Last(this StringSegment source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); + Contract.EndContractBlock(); + + if (search.Length == 0) + return default; + + var i = source.AsSpan().LastIndexOf(search, comparisonType); + return i == -1 ? default : source.Subsegment(i, search.Length); + } + + /// + public static StringSegment Last(this string source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) + => Last(source.AsSegment(), search, comparisonType); + + /// + /// Finds the last instance of a pattern and returns a StringSegment for subsequent use. + /// + /// If the pattern is right-to-left, then it will return the last segment from the right (first segment from the left). + /// + public static StringSegment Last(this string source, Regex pattern) + { + if (source is null) throw new ArgumentNullException(nameof(source)); + if (pattern is null) throw new ArgumentNullException(nameof(pattern)); + Contract.EndContractBlock(); + + var matches = pattern.Matches(source); + if (matches.Count == 0) return default; + var match = matches[matches.Count - 1]; + return new(source, match.Index, match.Length); + } + + /// + public static StringSegment Last(this StringSegment source, ReadOnlySpan search, StringComparison comparisonType = StringComparison.Ordinal) + { + if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); + Contract.EndContractBlock(); + if (search.IsEmpty) + return default; + + var i = source.AsSpan().LastIndexOf(search, comparisonType); + return i == -1 ? default : new(source.Buffer, source.Offset + i, search.Length); + } +} diff --git a/Source/Extensions.IndexOf.cs b/Source/Extensions.IndexOf.cs new file mode 100644 index 0000000..ac0064c --- /dev/null +++ b/Source/Extensions.IndexOf.cs @@ -0,0 +1,343 @@ +namespace Open.Text; +public static partial class TextExtensions +{ + /// + public static int IndexOf(this ReadOnlySpan source, char search, StringComparison comparisonType) + { + Func toUpper; + switch (comparisonType) + { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + case StringComparison.InvariantCulture: + return source.IndexOf(search); + case StringComparison.CurrentCultureIgnoreCase: + toUpper = static c => char.ToUpper(c, CultureInfo.CurrentCulture); + break; + case StringComparison.InvariantCultureIgnoreCase: + toUpper = char.ToUpperInvariant; + break; + case StringComparison.OrdinalIgnoreCase: + toUpper = char.ToUpper; + break; + default: + throw new ArgumentException("Invalid comparison type.", nameof(comparisonType)); + } + + var searchUpper = toUpper(search); + if(searchUpper == search) + return source.IndexOf(search); + + for (var i = 0; i < source.Length; i++) + { + var c = source[i]; + if (c == search) return i; + if (toUpper(c) == searchUpper) + return i; + } + + return -1; + } + + /// + public static int LastIndexOf(this ReadOnlySpan source, char search, StringComparison comparisonType) + { + Func toUpper; + switch (comparisonType) + { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + case StringComparison.InvariantCulture: + return source.LastIndexOf(search); + case StringComparison.CurrentCultureIgnoreCase: + toUpper = static c => char.ToUpper(c, CultureInfo.CurrentCulture); + break; + case StringComparison.InvariantCultureIgnoreCase: + toUpper = char.ToUpperInvariant; + break; + case StringComparison.OrdinalIgnoreCase: + toUpper = char.ToUpper; + break; + default: + throw new ArgumentException("Invalid comparison type.", nameof(comparisonType)); + } + + var searchUpper = toUpper(search); + if (searchUpper == search) + return source.LastIndexOf(search); + + for (var i = source.Length - 1; i >= 0; i--) + { + var c = source[i]; + if (c == search) return i; + if (toUpper(c) == searchUpper) + return i; + } + + return -1; + } + + /// + public static int IndexOf(this StringSegment source, char search, StringComparison comparisonType) + => source.HasValue ? source.AsSpan().IndexOf(search, comparisonType) : -1; + + /// + public static int LastIndexOf(this StringSegment source, char search, StringComparison comparisonType) + => source.HasValue ? source.AsSpan().LastIndexOf(search, comparisonType) : -1; + +#if NETSTANDARD2_0 + /// + public static int IndexOf(this string source, char search, StringComparison comparisonType) + => source is null ? throw new ArgumentNullException(nameof(source)) : source.AsSpan().IndexOf(search, comparisonType); + + /// + public static int LastIndexOf(this string source, char search, StringComparison comparisonType) + => source is null ? throw new ArgumentNullException(nameof(source)) : source.AsSpan().LastIndexOf(search, comparisonType); +#endif + + /// + /// Note: The NET Standard 2.0 version of this extension allocates strings to call the string.IndexOf method. + /// This non-extension version will (if possible) avoid allocating strings to find a sequence. + /// The one improved case is for . + /// +#if NETSTANDARD2_0 + public static int IndexOf( + ReadOnlySpan source, + ReadOnlySpan sequence, + StringComparison comparisonType = StringComparison.Ordinal) + { + switch (comparisonType) + { + case StringComparison.OrdinalIgnoreCase: + break; + + case StringComparison.Ordinal: + return source.IndexOf(sequence); + + // Use the existing extension as either it will work fine, or no way to avoid allocation. + case StringComparison.CurrentCulture: + case StringComparison.InvariantCulture: + case StringComparison.CurrentCultureIgnoreCase: + case StringComparison.InvariantCultureIgnoreCase: + return source.IndexOf(sequence, comparisonType); + + default: + throw new ArgumentException("Invalid comparison type.", nameof(comparisonType)); + } + + int sequenceLength = sequence.Length; + if (sequenceLength == 0) + throw new ArgumentException("Sequence must have at least one character.", nameof(sequence)); + + int sourceLength = source.Length; + if (sourceLength == 0) return -1; + if (sequenceLength > sourceLength) return -1; + + // Note: This is the only case where we can potentially avoid allocating a string. + int max = sourceLength - sequenceLength; + int i = -1; + while (max > i++) + { + var span = source.Slice(i, sequenceLength); + // Note: in rare cases, this will still convert to strings for comparison and could allocate. + if (span.Equals(sequence, StringComparison.OrdinalIgnoreCase)) + return i; + } + + return -1; + } +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf( + ReadOnlySpan source, + ReadOnlySpan sequence, + StringComparison comparisonType = StringComparison.Ordinal) + => source.IndexOf(sequence, comparisonType); +#endif + + +#if NET6_0_OR_GREATER +#else + /// + /// Reports the zero-based index of the first occurrence + /// of the specified . + /// + /// The source sequence to seek through. + /// The sequence to look for. + /// One of the enumeration values that specifies the rules for the search. + /// If start index is less than zero. + /// + /// Note: The NET Standard 2.x version of this extension allocates strings to call the string.LastIndexOf method. + /// This non-extension version will (if possible) avoid allocating strings to find a sequence. + /// The one improved case is for . + /// + public static int LastIndexOf( + this ReadOnlySpan source, + ReadOnlySpan sequence, + StringComparison comparisonType) + { + switch (comparisonType) + { + case StringComparison.OrdinalIgnoreCase: + break; + + case StringComparison.Ordinal: + return source.LastIndexOf(sequence); + + // These cases are not supported by the NET Standard 2.x version and will have to allocate. + case StringComparison.CurrentCulture: + case StringComparison.InvariantCulture: + case StringComparison.CurrentCultureIgnoreCase: + case StringComparison.InvariantCultureIgnoreCase: + return source.ToString().LastIndexOf(sequence.ToString(), StringComparison.CurrentCulture); + + default: + throw new ArgumentException("Invalid comparison type.", nameof(comparisonType)); + } + + int sequenceLength = sequence.Length; + if (sequenceLength == 0) + throw new ArgumentException("Sequence must have at least one character.", nameof(sequence)); + + int sourceLength = source.Length; + if (sourceLength == 0) return -1; + if (sequenceLength > sourceLength) return -1; + + // Note: This is the only case where we can potentially avoid allocating a string. + int max = sourceLength - sequenceLength; + do + { + var span = source.Slice(max, sequenceLength); + // Note: in rare cases, this will still convert to strings for comparison and could allocate. + if (span.Equals(sequence, StringComparison.OrdinalIgnoreCase)) + return max; + } + while (max-- > 0); + + return -1; + } +#endif + + +#pragma warning disable CS1587 // XML comment is not placed on a valid language element + /// + /// Reports the zero-based index of the first occurrence + /// of the specified . + /// + /// The source sequence to seek through. + /// The sequence to look for. + /// One of the enumeration values that specifies the rules for the search. + /// If start index is less than zero. +#if NET6_0_OR_GREATER +#else + /// +#endif + public static int LastIndexOf(this StringSegment source, ReadOnlySpan sequence, StringComparison comparisonType) +#pragma warning restore CS1587 // XML comment is not placed on a valid language element + => source.AsSpan().LastIndexOf(sequence, comparisonType); + + /// + /// Reports the zero-based index of the first occurrence + /// of the specified + /// starting from the . + /// + /// The source sequence to seek through. + /// The sequence to look for. + /// The search starting position. + /// One of the enumeration values that specifies the rules for the search. + /// If start index is less than zero. + public static int IndexOf( + this ReadOnlySpan source, + ReadOnlySpan sequence, + int startIndex, + StringComparison comparisonType = StringComparison.Ordinal) + { + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, "Must be at least zero."); + + if(startIndex == 0) + return IndexOf(source, sequence, comparisonType); + + var span = source.Slice(startIndex); + var index = span.IndexOf(sequence, comparisonType); + return index == -1 ? -1 : index + startIndex; + } + + /// + public static int IndexOf( + this StringSegment source, + StringSegment sequence, + int startIndex = 0, + StringComparison comparisonType = StringComparison.Ordinal) + => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + + /// + public static int IndexOf( + this StringSegment source, + ReadOnlySpan sequence, + int startIndex = 0, + StringComparison comparisonType = StringComparison.Ordinal) + => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + + /// + /// Reports the zero-based index of the first occurrence + /// of the specified . + /// + /// + public static int IndexOf( + this StringSegment source, + StringSegment sequence, + StringComparison comparisonType) + => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType); + + /// + public static int IndexOf( + this StringSegment source, + ReadOnlySpan sequence, + StringComparison comparisonType) + => IndexOf(source.AsSpan(), sequence, comparisonType); + + /// + public static int IndexOf( + this string source, + StringSegment sequence, + StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType); + + /// + /// Checks if the is contained + /// within the using the . + /// + /// + /// if the is contained; + /// otherwise . + /// + public static bool Contains( + this StringSegment source, + StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) != -1; + + /// + public static bool Contains( + this StringSegment source, + ReadOnlySpan sequence, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source.AsSpan(), sequence, comparisonType) != -1; + + /// + public static bool Contains( + this ReadOnlySpan source, + StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source, sequence.AsSpan(), comparisonType) != -1; + + /// + public static bool Contains( + this string source, + StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) != -1; + + /// + public static bool Contains( + this string source, + ReadOnlySpan sequence, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source.AsSpan(), sequence, comparisonType) != -1; +} diff --git a/Source/Extensions.Split.cs b/Source/Extensions.Split.cs index 3feef01..d16a3b6 100644 --- a/Source/Extensions.Split.cs +++ b/Source/Extensions.Split.cs @@ -1,11 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Open.Text; +namespace Open.Text; public static partial class TextExtensions { diff --git a/Source/Extensions.StringSegment.cs b/Source/Extensions.StringSegment.cs index f9ad090..676a206 100644 --- a/Source/Extensions.StringSegment.cs +++ b/Source/Extensions.StringSegment.cs @@ -1,12 +1,5 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Linq; +using System.Diagnostics; using System.Text; -using System.Text.RegularExpressions; namespace Open.Text; @@ -40,43 +33,6 @@ public static StringSegment AsSegment(this string buffer, int offset, int length return new(buffer, offset, length); } - /// - /// Creates a for enumerating over the characters in the . - /// - public static StringSegmentEnumerable AsEnumerable(this StringSegment segment) - => new(segment); - - /// - /// Finds the first instance of a string and returns a StringSegment for subsequent use. - /// - /// The source string to search. - /// The string pattern to look for. - /// The string comparison type to use. Default is Ordinal. - /// - /// The segment representing the found string. - /// If not found, the StringSegment.HasValue property will be false. - /// - public static StringSegment First(this StringSegment source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); - Contract.EndContractBlock(); - - if (search.Length == 0) - return default; - - var i = source.IndexOf(search, comparisonType); - return i == -1 ? default : source.Subsegment(i, search.Length); - } - - /// - public static StringSegment First(this string source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - Contract.EndContractBlock(); - - return First(source.AsSegment(), search, comparisonType); - } - /// Shortcut for .AsSegment().Trim(). /// [ExcludeFromCodeCoverage] // Reason: would just test already tested code. @@ -84,213 +40,10 @@ public static StringSegment TrimAsSegment(this string buffer) => buffer is null ? default : new StringSegment(buffer).Trim(); /// - /// Finds the first instance of a pattern and returns a StringSegment for subsequent use. - /// - /// The source string to search. - /// The pattern to look for. - /// - /// The segment representing the found string. - /// If not found, the StringSegment.HasValue property will be false. - /// - /// If the pattern is right-to-left, then it will return the first segment from the right. - [ExcludeFromCodeCoverage] // Reason: would just test already tested code. - public static StringSegment First(this string source, Regex pattern) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - if (pattern is null) throw new ArgumentNullException(nameof(pattern)); - Contract.EndContractBlock(); - - var match = pattern.Match(source); - return match.Success ? new(source, match.Index, match.Length) : default; - } - - /// - public static StringSegment First(this StringSegment source, ReadOnlySpan search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); - Contract.EndContractBlock(); - - if (search.IsEmpty) - return default; - - var i = source.AsSpan().IndexOf(search, comparisonType); - return i == -1 ? default : new(source.Buffer, source.Offset + i, search.Length); - } - - /// - /// Finds the last instance of a string and returns a StringSegment for subsequent use. - /// - /// - public static StringSegment Last(this StringSegment source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); - Contract.EndContractBlock(); - - if (search.Length == 0) - return default; - - var i = source.AsSpan().LastIndexOf(search, comparisonType); - return i == -1 ? default : source.Subsegment(i, search.Length); - } - - /// - public static StringSegment Last(this string source, StringSegment search, StringComparison comparisonType = StringComparison.Ordinal) - => Last(source.AsSegment(), search, comparisonType); - - /// - /// Finds the last instance of a pattern and returns a StringSegment for subsequent use. - /// - /// If the pattern is right-to-left, then it will return the last segment from the right (first segment from the left). - /// - public static StringSegment Last(this string source, Regex pattern) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - if (pattern is null) throw new ArgumentNullException(nameof(pattern)); - Contract.EndContractBlock(); - - var matches = pattern.Matches(source); - if (matches.Count == 0) return default; - var match = matches[matches.Count - 1]; - return new(source, match.Index, match.Length); - } - - /// - public static StringSegment Last(this StringSegment source, ReadOnlySpan search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (!source.HasValue) throw new ArgumentException(MustBeSegmentWithValue, nameof(source)); - Contract.EndContractBlock(); - if (search.IsEmpty) - return default; - - var i = source.AsSpan().LastIndexOf(search, comparisonType); - return i == -1 ? default : new(source.Buffer, source.Offset + i, search.Length); - } - - /// - /// Reports the zero-based index of the first occurrence of the specified string - /// in the current System.String object. Parameters specify the starting search position - /// in the current string and the type of search to use for the specified string. - /// - /// The source segment to seek through. - /// The segment to look for. - /// The search starting position. - /// One of the enumeration values that specifies the rules for the search. - /// If start index is less than zero. - public static int IndexOf( - this StringSegment segment, - StringSegment value, - int startIndex = 0, - StringComparison comparisonType = StringComparison.Ordinal) - { - if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, "Must be at least zero."); - - var len = value.Length; - if (len == 0 || startIndex + len > segment.Length) return -1; - if (startIndex == 0 && len == segment.Length) - return segment.Equals(value, comparisonType) ? 0 : -1; - - if (comparisonType == StringComparison.Ordinal) - { - startIndex = segment.IndexOf(value[0], startIndex); - if (startIndex == -1) return -1; - } - - var max = segment.Length - len; - for (var i = startIndex; i <= max; i++) - { - if (segment.Subsegment(i, len).Equals(value, comparisonType)) - return i; - } - - return -1; - } - - /// - public static int IndexOf( - this StringSegment segment, - ReadOnlySpan value, - int startIndex = 0, - StringComparison comparisonType = StringComparison.Ordinal) - { - if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, "Must be at least zero."); - - var len = value.Length; - if (len == 0 || startIndex + len > segment.Length) return -1; - if (startIndex == 0 && len == segment.Length) - return segment.Equals(value, comparisonType) ? 0 : -1; - - if (comparisonType == StringComparison.Ordinal) - { - startIndex = segment.IndexOf(value[0], startIndex); - if (startIndex == -1) return -1; - } - - var max = segment.Length - len; - for (var i = startIndex; i <= max; i++) - { - if (segment.Subsegment(i, len).Equals(value, comparisonType)) - return i; - } - - return -1; - } - - /// - public static int IndexOf( - this StringSegment segment, - StringSegment value, - StringComparison comparisonType) - => IndexOf(segment, value, 0, comparisonType); - - /// - public static int IndexOf( - this StringSegment segment, - ReadOnlySpan value, - StringComparison comparisonType) - => IndexOf(segment, value, 0, comparisonType); - - /// - public static int IndexOf( - this string segment, - StringSegment value, - StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(segment.AsSegment(), value, 0, comparisonType); - - /// - /// Checks if value is contained in the sequence using the comparison type. - /// - /// true if the value of is contained (using the comparison type); otherwise false. - public static bool Contains( - this StringSegment segment, - StringSegment value, StringComparison comparisonType = StringComparison.Ordinal) - => segment.IndexOf(value, comparisonType) != -1; - - /// - public static bool Contains( - this StringSegment segment, - ReadOnlySpan value, StringComparison comparisonType = StringComparison.Ordinal) - => segment.IndexOf(value, comparisonType) != -1; - - /// - /// Checks if value is contained in the sequence using the comparison type. + /// Creates a for enumerating over the characters in the . /// - /// true if the value of is contained (using the comparison type); otherwise false. - public static bool Contains( - this ReadOnlySpan span, - StringSegment value, StringComparison comparisonType = StringComparison.Ordinal) - => span.IndexOf(value, comparisonType) != -1; - - /// - public static bool Contains( - this string segment, - StringSegment value, StringComparison comparisonType = StringComparison.Ordinal) - => segment.AsSegment().IndexOf(value, comparisonType) != -1; - - /// - public static bool Contains( - this string segment, - ReadOnlySpan value, StringComparison comparisonType = StringComparison.Ordinal) - => segment.AsSegment().IndexOf(value, comparisonType) != -1; + public static StringSegmentEnumerable AsEnumerable(this StringSegment segment) + => new(segment); /// public static IEnumerable SplitAsSegments( diff --git a/Source/Extensions.Trim.cs b/Source/Extensions.Trim.cs index 13fca85..0536b95 100644 --- a/Source/Extensions.Trim.cs +++ b/Source/Extensions.Trim.cs @@ -1,10 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Text.RegularExpressions; - -namespace Open.Text; +namespace Open.Text; public static partial class TextExtensions { diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index b6d3fea..16e5bad 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -1,15 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; - -[assembly: CLSCompliant(true)] +[assembly: CLSCompliant(true)] namespace Open.Text; /// @@ -31,71 +20,6 @@ public static partial class TextExtensions /// public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern = new(@"^\s*\w+\s*$", RegexOptions.Compiled); - /// - /// Reports the zero-based index position of the last occurrence of a specified string - /// within this instance. The search starts at a specified character position and - /// proceeds backward toward the beginning of the string. - /// - /// The source string to search in. - /// The search string to look for. If the search is null or empty this method returns null. - /// The comparison type to use when searching. - /// The source of the string before the search string. Returns null if search string is not found. - public static int LastIndexOf(this ReadOnlySpan source, - ReadOnlySpan search, StringComparison comparisonType) - { - if (search.Length > source.Length) - return -1; - - if (comparisonType == StringComparison.Ordinal) - return source.LastIndexOf(search); - - // Nothing found? - var i = source.IndexOf(search, comparisonType); - if (i == -1) - return i; - - // Next possible can't fit? - var n = i + search.Length; - if (n + search.Length > source.Length) - return i; - - // Recurse to get the last one. - var next = source - .Slice(n) - .LastIndexOf(search, comparisonType); - - return next == -1 ? i : (n + next); - } - - /// - public static int LastIndexOf(this StringSegment source, - ReadOnlySpan search, StringComparison comparisonType = StringComparison.Ordinal) - { - if (search.Length > source.Length) - return -1; - - if (comparisonType == StringComparison.Ordinal) - return source.AsSpan().LastIndexOf(search); - - // Nothing found? - var i = source.IndexOf(search, comparisonType); - if (i == -1) - return i; - - // Next possible can't fit? - var n = i + search.Length; - if (n + search.Length > source.Length) - return i; - - // Recurse to get the last one. - var next = source - .AsSpan() - .Slice(n) - .LastIndexOf(search, comparisonType); - - return next == -1 ? i : (n + next); - } - /// /// Converts a string to title-case. /// diff --git a/Source/Open.Text.csproj b/Source/Open.Text.csproj index cfc8ded..fb053a1 100644 --- a/Source/Open.Text.csproj +++ b/Source/Open.Text.csproj @@ -4,6 +4,7 @@ netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 latest enable + enable true AllEnabledByDefault latest @@ -19,7 +20,7 @@ https://github.com/Open-NET-Libraries/Open.Text git string, span, enum, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions, stringcomparable, spancomparable, stringsegment, splitassegment - 7.0.3 + 7.1.0 MIT true diff --git a/Source/SpanComparable.cs b/Source/SpanComparable.cs index 98daf13..76bc990 100644 --- a/Source/SpanComparable.cs +++ b/Source/SpanComparable.cs @@ -1,7 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; namespace Open.Text; diff --git a/Source/StringBuilderEnumerator.cs b/Source/StringBuilderEnumerator.cs index 5efa7a3..669f3a5 100644 --- a/Source/StringBuilderEnumerator.cs +++ b/Source/StringBuilderEnumerator.cs @@ -1,7 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.Text; namespace Open.Text; diff --git a/Source/StringBuilderExtensions.cs b/Source/StringBuilderExtensions.cs index 078956f..e211a0c 100644 --- a/Source/StringBuilderExtensions.cs +++ b/Source/StringBuilderExtensions.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Text; +using System.Text; namespace Open.Text; diff --git a/Source/StringBuilderHelper.cs b/Source/StringBuilderHelper.cs index e6457cf..a017159 100644 --- a/Source/StringBuilderHelper.cs +++ b/Source/StringBuilderHelper.cs @@ -1,8 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; +using System.Text; namespace Open.Text; diff --git a/Source/StringComparable.cs b/Source/StringComparable.cs index 0eb01ad..dec8b5d 100644 --- a/Source/StringComparable.cs +++ b/Source/StringComparable.cs @@ -1,8 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Text; +namespace Open.Text; /// /// A struct for comparing a against other values. diff --git a/Source/StringSegmentComparer.cs b/Source/StringSegmentComparer.cs index d559f9b..bb85c43 100644 --- a/Source/StringSegmentComparer.cs +++ b/Source/StringSegmentComparer.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Open.Text; +namespace Open.Text; /// /// A comparer for that uses existing diff --git a/Source/StringSegmentEnumerator.cs b/Source/StringSegmentEnumerator.cs index b803e1e..087de5a 100644 --- a/Source/StringSegmentEnumerator.cs +++ b/Source/StringSegmentEnumerator.cs @@ -1,7 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; namespace Open.Text; diff --git a/Source/StringSegmentSearch.cs b/Source/StringSegmentSearch.cs index df4b4dc..1732d1e 100644 --- a/Source/StringSegmentSearch.cs +++ b/Source/StringSegmentSearch.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Runtime.CompilerServices; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Text; +namespace Open.Text; /// /// Represents a search operation within a string segment. /// diff --git a/Source/StringSubsegment.cs b/Source/StringSubsegment.cs index 044ab5c..1eb7c97 100644 --- a/Source/StringSubsegment.cs +++ b/Source/StringSubsegment.cs @@ -1,8 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; namespace Open.Text; diff --git a/Source/_Global.cs b/Source/_Global.cs new file mode 100644 index 0000000..d72b8a2 --- /dev/null +++ b/Source/_Global.cs @@ -0,0 +1,8 @@ +global using Microsoft.Extensions.Primitives; +global using System.Runtime.CompilerServices; +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Diagnostics.Contracts; +global using System.Globalization; +global using System.Reflection; +global using System.Text.RegularExpressions; \ No newline at end of file From 63f69a6c7dad88042dde00a564e3dcc83e8ecf44 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 09:45:17 -0700 Subject: [PATCH 2/8] Expansion and cleanup. --- Source/Extensions.Equals.cs | 36 +---------------- Source/Extensions.IndexOf.cs | 42 ++++++++++++++++++-- Source/Extensions.Split.cs | 32 ++++++++++----- Source/Extensions.StringSegment.cs | 6 +-- Source/Extensions._.cs | 13 +++--- Source/Open.Text.csproj | 5 +++ Source/StringSegmentSearch.cs | 18 ++++++++- Tests/ComparableTests.cs | 6 +-- Tests/EnumValueTests.cs | 20 +++------- Tests/EqualityTests.cs | 6 +-- Tests/FindApiTests.cs | 9 ++--- Tests/FormattingTests.cs | 5 +-- Tests/IndexOfTests.cs | 28 +++++++++++++ Tests/Open.Text.Tests.csproj | 5 ++- Tests/ParseTests.cs | 7 +--- Tests/RegexTests.cs | 6 +-- Tests/SplitTests.cs | 63 +++++++++++++++++++++--------- Tests/StringBuilderTests.cs | 13 +++--- Tests/StringSegmentTests.cs | 13 +++--- Tests/TrimTests.cs | 9 ++--- Tests/_Global.cs | 3 ++ 21 files changed, 212 insertions(+), 133 deletions(-) create mode 100644 Tests/IndexOfTests.cs create mode 100644 Tests/_Global.cs diff --git a/Source/Extensions.Equals.cs b/Source/Extensions.Equals.cs index 6f2f202..a9e6940 100644 --- a/Source/Extensions.Equals.cs +++ b/Source/Extensions.Equals.cs @@ -111,43 +111,11 @@ public static bool SequenceEqual(this StringSegment source, ReadOnlySpan o /// public static bool TrimmedEquals(this string? source, ReadOnlySpan other, StringComparison comparisonType = StringComparison.Ordinal) - => source is not null - && source.Length >= other.Length - && source.AsSpan().Trim().Equals(other, comparisonType); + => source?.AsSpan().Trim().Equals(other, comparisonType) == true; /// public static bool TrimmedEquals(this string? source, StringSegment other, StringComparison comparisonType = StringComparison.Ordinal) - { - if (source is null) return !other.HasValue; - if (!other.HasValue) return false; - int slen = source.Length, olen = other.Length; - if (slen < olen) return false; - var span = source.Trim(); - slen = span.Length; - return slen == olen && slen switch - { - 0 => true, - 1 when comparisonType == StringComparison.Ordinal => span[0] == other[0], - _ => span.Equals(other, comparisonType), - }; - } - - /// - public static bool TrimmedEquals(this string? source, string? other, StringComparison comparisonType = StringComparison.Ordinal) - { - if (source is null) return other is null; - if (other is null) return false; - int slen = source.Length, olen = other.Length; - if (slen < olen) return false; - var span = source.AsSpan().Trim(); - slen = span.Length; - return slen == olen && slen switch - { - 0 => true, - 1 when comparisonType == StringComparison.Ordinal => span[0] == other[0], - _ => span.Equals(other.AsSpan(), comparisonType), - }; - } + => source is null ? !other.HasValue : other.HasValue && source.AsSpan().Trim().Equals(other, comparisonType); /// public static bool TrimmedEquals(this StringSegment source, StringSegment other, StringComparison comparisonType = StringComparison.Ordinal) diff --git a/Source/Extensions.IndexOf.cs b/Source/Extensions.IndexOf.cs index ac0064c..76621db 100644 --- a/Source/Extensions.IndexOf.cs +++ b/Source/Extensions.IndexOf.cs @@ -155,7 +155,6 @@ public static int IndexOf( => source.IndexOf(sequence, comparisonType); #endif - #if NET6_0_OR_GREATER #else /// @@ -218,7 +217,6 @@ public static int LastIndexOf( } #endif - #pragma warning disable CS1587 // XML comment is not placed on a valid language element /// /// Reports the zero-based index of the first occurrence @@ -233,8 +231,16 @@ public static int LastIndexOf( /// #endif public static int LastIndexOf(this StringSegment source, ReadOnlySpan sequence, StringComparison comparisonType) -#pragma warning restore CS1587 // XML comment is not placed on a valid language element => source.AsSpan().LastIndexOf(sequence, comparisonType); +#pragma warning restore CS1587 // XML comment is not placed on a valid language element + + /// + public static int LastIndexOf(this ReadOnlySpan source, StringSegment sequence, StringComparison comparisonType) + => source.LastIndexOf(sequence.AsSpan(), comparisonType); + + /// + public static int LastIndexOf(this StringSegment source, StringSegment sequence, StringComparison comparisonType) + => source.AsSpan().LastIndexOf(sequence.AsSpan(), comparisonType); /// /// Reports the zero-based index of the first occurrence @@ -263,6 +269,14 @@ public static int IndexOf( return index == -1 ? -1 : index + startIndex; } + /// + public static int IndexOf( + this ReadOnlySpan source, + StringSegment sequence, + int startIndex = 0, + StringComparison comparisonType = StringComparison.Ordinal) + => source.IndexOf(sequence.AsSpan(), startIndex, comparisonType); + /// public static int IndexOf( this StringSegment source, @@ -271,6 +285,14 @@ public static int IndexOf( StringComparison comparisonType = StringComparison.Ordinal) => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + /// + public static int IndexOf( + this string source, + ReadOnlySpan sequence, + int startIndex = 0, + StringComparison comparisonType = StringComparison.Ordinal) + => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + /// public static int IndexOf( this StringSegment source, @@ -290,6 +312,13 @@ public static int IndexOf( StringComparison comparisonType) => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType); + /// + public static int IndexOf( + this string source, + ReadOnlySpan sequence, + StringComparison comparisonType) + => IndexOf(source.AsSpan(), sequence, comparisonType); + /// public static int IndexOf( this StringSegment source, @@ -297,6 +326,13 @@ public static int IndexOf( StringComparison comparisonType) => IndexOf(source.AsSpan(), sequence, comparisonType); + /// + public static int IndexOf( + this ReadOnlySpan source, + StringSegment sequence, + StringComparison comparisonType) + => IndexOf(source, sequence.AsSpan(), comparisonType); + /// public static int IndexOf( this string source, diff --git a/Source/Extensions.Split.cs b/Source/Extensions.Split.cs index d16a3b6..3cd8095 100644 --- a/Source/Extensions.Split.cs +++ b/Source/Extensions.Split.cs @@ -157,6 +157,13 @@ public static ReadOnlySpan FirstSplit(this ReadOnlySpan source, return FirstSplitSpan(source, i, splitSequence.Length, out nextIndex); } + /// + public static ReadOnlySpan FirstSplit(this ReadOnlySpan source, + StringSegment splitSequence, + out int nextIndex, + StringComparison comparisonType = StringComparison.Ordinal) + => source.FirstSplit(splitSequence.AsSpan(), out nextIndex, comparisonType); + /// /// Enumerates a string by segments that are separated by the split character. /// @@ -165,8 +172,8 @@ public static ReadOnlySpan FirstSplit(this ReadOnlySpan source, /// Can specify to omit empty entries. /// An enumerable of the segments. public static IEnumerable SplitToEnumerable(this string source, - char splitCharacter, - StringSplitOptions options = StringSplitOptions.None) + char splitCharacter, + StringSplitOptions options = StringSplitOptions.None) { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); @@ -177,7 +184,7 @@ public static IEnumerable SplitToEnumerable(this string source, ? Enumerable.Repeat(string.Empty, 1) : SplitAsEnumerableCore(), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty() : SplitAsEnumerableCoreOmitEmpty(), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; @@ -232,7 +239,7 @@ public static IEnumerable SplitToEnumerable( ? Enumerable.Repeat(string.Empty, 1) : SplitAsEnumerableCore(source, splitSequence, comparisonType), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty() : SplitAsEnumerableCoreOmitEmpty(source, splitSequence, comparisonType), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; @@ -277,7 +284,7 @@ public static IEnumerable> SplitAsMemory( ? Enumerable.Repeat(ReadOnlyMemory.Empty, 1) : SplitAsMemoryCore(source, splitCharacter), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty>() : SplitAsMemoryOmitEmpty(source, splitCharacter), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; @@ -325,7 +332,7 @@ public static IEnumerable> SplitAsMemory(this string source ? Enumerable.Repeat(ReadOnlyMemory.Empty, 1) : SplitAsMemoryCore(source, splitSequence, comparisonType), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty>() : SplitAsMemoryOmitEmpty(source, splitSequence, comparisonType), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; @@ -356,7 +363,7 @@ static IEnumerable> SplitAsMemoryCore(string source, string } } - static readonly IReadOnlyList SingleEmpty = Array.AsReadOnly(new[] { string.Empty }); + static readonly IReadOnlyList SingleEmpty = new List { string.Empty }.AsReadOnly(); /// /// Splits a sequence of characters into strings using the character provided. @@ -375,7 +382,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, return SingleEmpty; case StringSplitOptions.RemoveEmptyEntries when source.Length == 0: - return []; + return Array.Empty(); case StringSplitOptions.RemoveEmptyEntries: { @@ -423,7 +430,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, return SingleEmpty; case StringSplitOptions.RemoveEmptyEntries when source.IsEmpty: - return []; + return Array.Empty(); case StringSplitOptions.RemoveEmptyEntries: { @@ -451,4 +458,11 @@ public static IReadOnlyList Split(this ReadOnlySpan source, } } } + + /// + public static IReadOnlyList Split(this ReadOnlySpan source, + StringSegment splitSequence, + StringSplitOptions options = StringSplitOptions.None, + StringComparison comparisonType = StringComparison.Ordinal) + => source.Split(splitSequence.AsSpan(), options, comparisonType); } diff --git a/Source/Extensions.StringSegment.cs b/Source/Extensions.StringSegment.cs index 676a206..ef88063 100644 --- a/Source/Extensions.StringSegment.cs +++ b/Source/Extensions.StringSegment.cs @@ -69,7 +69,7 @@ public static IEnumerable SplitAsSegments( ? Enumerable.Repeat(StringSegment.Empty, 1) : SplitAsSegmentsCore(source, splitCharacter), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty() : SplitAsSegmentsCoreOmitEmpty(source, splitCharacter), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; @@ -152,7 +152,7 @@ public static IEnumerable SplitAsSegments( ? throw new ArgumentNullException(nameof(pattern)) : source.Length == 0 ? options == StringSplitOptions.RemoveEmptyEntries - ? [] + ? Enumerable.Empty() : Enumerable.Repeat(StringSegment.Empty, 1) : SplitCore(source, pattern, options); @@ -207,7 +207,7 @@ public static IEnumerable SplitAsSegments( ? Enumerable.Repeat(StringSegment.Empty, 1) : SplitAsSegmentsCore(source, splitSequence, comparisonType), StringSplitOptions.RemoveEmptyEntries => source.Length == 0 - ? [] + ? Enumerable.Empty() : SplitAsSegmentsCoreOmitEmpty(source, splitSequence, comparisonType), _ => throw new System.ComponentModel.InvalidEnumArgumentException(), }; diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index 16e5bad..71fd37f 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -13,12 +13,14 @@ public static partial class TextExtensions /// /// Compiled pattern for finding alpha-numeric sequences. /// - public static readonly Regex ValidAlphaNumericOnlyPattern = new(@"^\w+$", RegexOptions.Compiled); + public static readonly Regex ValidAlphaNumericOnlyPattern + = new(@"^\w+$", RegexOptions.Compiled); /// /// Compiled pattern for finding alpha-numeric sequences and possible surrounding white-space. /// - public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern = new(@"^\s*\w+\s*$", RegexOptions.Compiled); + public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern + = new(@"^\s*\w+\s*$", RegexOptions.Compiled); /// /// Converts a string to title-case. @@ -182,7 +184,7 @@ public static IEnumerable AsSegments(this Regex pattern, string i : input is null ? throw new ArgumentNullException(nameof(input)) : input.Length == 0 - ? [] + ? Enumerable.Empty() : AsSegmentsCore(pattern, input); static IEnumerable AsSegmentsCore(Regex pattern, string input) @@ -326,6 +328,7 @@ public static string ReplaceWhiteSpace(this string source, string replace = " ") /// /// String constant for carriage return and then newline. /// + [Obsolete("Use Environment.NewLine instead.")] public const string Newline = "\r\n"; /// @@ -338,7 +341,7 @@ public static void WriteLineNoTabs(this TextWriter writer, string? s = null) Contract.EndContractBlock(); if (s is not null) writer.Write(s); - writer.Write(Newline); + writer.Write(Environment.NewLine); } /// @@ -371,7 +374,7 @@ public static int GetHashCodeFromChars(this ReadOnlySpan chars, StringComp int length = chars.Length > maxChars ? maxChars : chars.Length; int hash = 17; - switch(comparisonType) + switch (comparisonType) { case StringComparison.Ordinal: case StringComparison.CurrentCulture: diff --git a/Source/Open.Text.csproj b/Source/Open.Text.csproj index fb053a1..07f9d25 100644 --- a/Source/Open.Text.csproj +++ b/Source/Open.Text.csproj @@ -29,6 +29,11 @@ logo.png True README.md + IDE0301;IDE0079 + + + + $(NoWarn);nullable diff --git a/Source/StringSegmentSearch.cs b/Source/StringSegmentSearch.cs index 1732d1e..b9bf79a 100644 --- a/Source/StringSegmentSearch.cs +++ b/Source/StringSegmentSearch.cs @@ -40,7 +40,7 @@ public readonly ref struct StringSegmentSearch /// /// internal StringSegmentSearch( - StringSegment source, + StringSegment source, ReadOnlySpan search, StringComparison comparisonType, bool rightToLeft) @@ -149,6 +149,22 @@ public static StringSegmentSearch Find( bool rightToLeft = false) => new(source, search, comparisonType, rightToLeft); + /// + public static StringSegmentSearch Find( + this string source, + StringSegment search, + StringComparison comparisonType = StringComparison.Ordinal, + bool rightToLeft = false) + => new(source, search, comparisonType, rightToLeft); + + /// + public static StringSegmentSearch Find( + this StringSegment source, + StringSegment search, + StringComparison comparisonType = StringComparison.Ordinal, + bool rightToLeft = false) + => new(source, search, comparisonType, rightToLeft); + /// /// Finds the next occurrence of the specified character sequence within the source segment. /// diff --git a/Tests/ComparableTests.cs b/Tests/ComparableTests.cs index b97ef7e..ec67be7 100644 --- a/Tests/ComparableTests.cs +++ b/Tests/ComparableTests.cs @@ -1,8 +1,6 @@ -using System; -using Xunit; - -namespace Open.Text.Tests; +namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class ComparableTests { [Theory] diff --git a/Tests/EnumValueTests.cs b/Tests/EnumValueTests.cs index c2c78a8..7ea4c7f 100644 --- a/Tests/EnumValueTests.cs +++ b/Tests/EnumValueTests.cs @@ -1,21 +1,12 @@ -using FluentAssertions; -using System; -using System.Linq; -using Xunit; +namespace Open.Text.Tests; -namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Field)] -public class LetterAttribute : Attribute +public class LetterAttribute(char upper, char lower) : Attribute { - public LetterAttribute(char upper, char lower) - { - Upper = upper; - Lower = lower; - } - - public char Upper { get; } - public char Lower { get; } + public char Upper { get; } = upper; + public char Lower { get; } = lower; public bool EqualsLetter(char letter) => letter == Upper || letter == Lower; @@ -130,6 +121,7 @@ public enum LargeEnum Item065 } +[ExcludeFromCodeCoverage] public static class EnumValueTests { [Fact] diff --git a/Tests/EqualityTests.cs b/Tests/EqualityTests.cs index 3056f6c..356e17b 100644 --- a/Tests/EqualityTests.cs +++ b/Tests/EqualityTests.cs @@ -1,8 +1,6 @@ -using System; -using Xunit; - -namespace Open.Text.Tests; +namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public class EqualityTests { static readonly ReadOnlyMemory Chars = new[] { ' ', '\t' }; diff --git a/Tests/FindApiTests.cs b/Tests/FindApiTests.cs index cb4b245..50bf1b8 100644 --- a/Tests/FindApiTests.cs +++ b/Tests/FindApiTests.cs @@ -1,15 +1,14 @@ -using FluentAssertions; -using Microsoft.Extensions.Primitives; -using Xunit; +using Microsoft.Extensions.Primitives; namespace Open.Text.Tests; + +[ExcludeFromCodeCoverage] public static class FindAPITests { [Fact] public static void Exists() { - const string source = "Hello World!"; - StringSegment segment = source; + StringSegment segment = "Hello World!"; { var e = segment.Find("Hello"); e.Exists().Should().BeTrue(); diff --git a/Tests/FormattingTests.cs b/Tests/FormattingTests.cs index 6e931b5..ac7b908 100644 --- a/Tests/FormattingTests.cs +++ b/Tests/FormattingTests.cs @@ -1,7 +1,6 @@ -using Xunit; - -namespace Open.Text.Tests; +namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class FormattingTests { [Theory] diff --git a/Tests/IndexOfTests.cs b/Tests/IndexOfTests.cs new file mode 100644 index 0000000..2443f39 --- /dev/null +++ b/Tests/IndexOfTests.cs @@ -0,0 +1,28 @@ +namespace Open.Text.Tests; + +[ExcludeFromCodeCoverage] +public static class IndexOfTests +{ + const string HELLO_WORLD_IM_HERE = "Hello World, I'm here"; + + [Theory] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.OrdinalIgnoreCase)] + public static void IndexOf(string source, string value, StringComparison comparison) + { + var expected = source.IndexOf(value, comparison); + source.IndexOf(value.AsSpan(), comparison).Should().Be(expected); + source.IndexOf(value.AsSegment(), comparison).Should().Be(expected); + source.AsSpan().IndexOf(value, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, comparison).Should().Be(expected); + source.IndexOf(value.AsSpan(), 0, comparison).Should().Be(expected); + source.IndexOf(value.AsSegment(), 0, comparison).Should().Be(expected); + source.AsSpan().IndexOf(value, 0, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, 0, comparison).Should().Be(expected); + source.IndexOf(value.AsSpan(), 2, comparison).Should().Be(expected); + source.IndexOf(value.AsSegment(), 2, comparison).Should().Be(expected); + source.AsSpan().IndexOf(value, 2, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, 2, comparison).Should().Be(expected); + } +} diff --git a/Tests/Open.Text.Tests.csproj b/Tests/Open.Text.Tests.csproj index 4c34135..6f205f8 100644 --- a/Tests/Open.Text.Tests.csproj +++ b/Tests/Open.Text.Tests.csproj @@ -1,10 +1,13 @@  - net6.0 + net472;net6.0;net8.0 enable false + latest false + enable + IDE0301 diff --git a/Tests/ParseTests.cs b/Tests/ParseTests.cs index b293b75..6d0e549 100644 --- a/Tests/ParseTests.cs +++ b/Tests/ParseTests.cs @@ -1,9 +1,6 @@ -using FluentAssertions; -using System; -using Xunit; - -namespace Open.Text.Tests; +namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class ParseTests { [Theory] diff --git a/Tests/RegexTests.cs b/Tests/RegexTests.cs index 5bd5237..debb84a 100644 --- a/Tests/RegexTests.cs +++ b/Tests/RegexTests.cs @@ -1,10 +1,8 @@ -using FluentAssertions; -using System; -using System.Text.RegularExpressions; -using Xunit; +using System.Text.RegularExpressions; namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class RegexTests { [Fact] diff --git a/Tests/SplitTests.cs b/Tests/SplitTests.cs index 37c82c1..64493b3 100644 --- a/Tests/SplitTests.cs +++ b/Tests/SplitTests.cs @@ -1,10 +1,17 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using Xunit; +using System.Text.RegularExpressions; +using System.ComponentModel; namespace Open.Text.Tests; +#if NET472 +internal static class Shim +{ + internal static string[] Split(this string sequence, char separator, StringSplitOptions options = StringSplitOptions.None) + => sequence.Split([separator], options); +} +#endif + +[ExcludeFromCodeCoverage] public static class SplitTests { private const string DefaultTestString = "Hello,there"; @@ -27,21 +34,20 @@ public static void InvalidStart() } [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1196:Call extension method as instance method.", Justification = "Preferred.")] public static void ThrowIfNull() { - Assert.Throws(() => TextExtensions.FirstSplit(default!, ',', out _)); - Assert.Throws(() => TextExtensions.FirstSplit(default!, ",", out _)); - Assert.Throws(() => TextExtensions.FirstSplit(DefaultTestString, default(string)!, out _)); - Assert.Throws(() => TextExtensions.SplitToEnumerable(default!, ',')); - Assert.Throws(() => TextExtensions.SplitToEnumerable(default!, ",")); - Assert.Throws(() => TextExtensions.SplitToEnumerable(DefaultTestString, default(string)!)); - Assert.Throws(() => TextExtensions.SplitAsMemory(default!, ',')); - Assert.Throws(() => TextExtensions.SplitAsMemory(default!, ",")); - Assert.Throws(() => TextExtensions.SplitAsMemory(DefaultTestString, default(string)!)); - Assert.Throws(() => TextExtensions.SplitAsSegments(default!, ',')); - Assert.Throws(() => TextExtensions.SplitAsSegments(default!, ",")); - Assert.Throws(() => TextExtensions.SplitAsSegments(DefaultTestString, default(string)!)); + Assert.Throws(() => default(string)!.FirstSplit(',', out _)); + Assert.Throws(() => default(string)!.FirstSplit(",", out _)); + Assert.Throws(() => DefaultTestString.FirstSplit(default(string)!, out _)); + Assert.Throws(() => default(string)!.SplitToEnumerable(',')); + Assert.Throws(() => default(string)!.SplitToEnumerable(",")); + Assert.Throws(() => DefaultTestString.SplitToEnumerable(default(string)!)); + Assert.Throws(() => default(string)!.SplitAsMemory(',')); + Assert.Throws(() => default(string)!.SplitAsMemory(",")); + Assert.Throws(() => DefaultTestString.SplitAsMemory(default(string)!)); + Assert.Throws(() => default(string)!.SplitAsSegments(',')); + Assert.Throws(() => default(string)!.SplitAsSegments(",")); + Assert.Throws(() => DefaultTestString.SplitAsSegments(default(string)!)); } [Theory] @@ -52,8 +58,9 @@ public static void ThrowIfNull() [InlineData("Hello,there,I")] public static void FirstSplit(string sequence) { - Assert.Equal(sequence.FirstSplit(',', out _).ToString(), sequence.AsSpan().FirstSplit(',', out _).ToString()); - Assert.Equal(sequence.FirstSplit(",", out _).ToString(), sequence.AsSpan().FirstSplit(",", out _).ToString()); + var span = sequence.AsSpan(); + Assert.Equal(sequence.FirstSplit(',', out _).ToString(), span.FirstSplit(',', out _).ToString()); + Assert.Equal(sequence.FirstSplit(",", out _).ToString(), span.FirstSplit(",", out _).ToString()); } [Theory] @@ -91,6 +98,7 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl TextExtensions .ValidAlphaNumericOnlyPattern .Matches(sequence) + .Cast() .Select(m => m.Value), TextExtensions .ValidAlphaNumericOnlyPattern @@ -109,6 +117,23 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl stringSegment.ReplaceToString(",", sep)); } + [Fact] + public static void SplitOptionsInvalid() + { + Assert.Throws( + () => "hello there".SplitAsMemory(',', (StringSplitOptions)10000)); + Assert.Throws( + () => "hello there".SplitAsMemory(",", (StringSplitOptions)10000)); + Assert.Throws( + () => "hello there".SplitAsSegments(',', (StringSplitOptions)10000)); + Assert.Throws( + () => "hello there".SplitAsSegments(",", (StringSplitOptions)10000)); + Assert.Throws( + () => "hello there".SplitAsMemory(',', (StringSplitOptions)10000)); + Assert.Throws( + () => "hello there".SplitAsMemory(",", (StringSplitOptions)10000)); + } + [Theory] [InlineData("Hello", "ll", "He,o")] [InlineData("HelLo", "Ll", "He,o")] diff --git a/Tests/StringBuilderTests.cs b/Tests/StringBuilderTests.cs index b52396b..969bf86 100644 --- a/Tests/StringBuilderTests.cs +++ b/Tests/StringBuilderTests.cs @@ -1,11 +1,8 @@ -using FluentAssertions; -using System; -using System.Linq; -using System.Text; -using Xunit; +using System.Text; namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class StringBuilderTests { [Theory] @@ -84,10 +81,10 @@ public static void ToStringBuilderSeparatedChar(string? source, char separator) var span = source.AsSpan(); var a = source.AsSpan().ToArray(); - var joined = string.Join(separator, a); + var joined = string.Join(""+separator, a); var list = a.ToList(); list.Insert(0, 'X'); - var xValue = string.Join(separator, list); + var xValue = string.Join("" + separator, list); { var sb = a.ToStringBuilder(separator); Assert.Equal(joined, sb.ToString()); @@ -242,7 +239,7 @@ public static void Trim(string expected, string a, string b, string c, string d) public static void TrimChars(string expected, string a, string b, string c, string d) { var sb = new StringBuilder(); - sb.Append(a).Append(b).Append(c).Append(d).Trim(" !"); + sb.Append(a).Append(b).Append(c).Append(d).Trim(" !".AsSpan()); Assert.Equal(expected, sb.ToString()); } diff --git a/Tests/StringSegmentTests.cs b/Tests/StringSegmentTests.cs index 7bd77a1..e7fc694 100644 --- a/Tests/StringSegmentTests.cs +++ b/Tests/StringSegmentTests.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Primitives; -using System; -using Xunit; namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class StringSegmentTests { [Fact] @@ -35,10 +34,12 @@ public static void BeforeFirst(string source, string pattern, string? expected, else { var p = first.Preceding(); - Assert.Equal(expected, p.ToString()); - Assert.True(p.ToString().Equals(expected, comparisonType)); - Assert.True(p.AsSpan().Equals(expected, comparisonType)); - Assert.True(p.AsMemory().Span.Equals(expected, comparisonType)); + var pString = p.ToString(); + pString.Should().Be(expected); + + pString.Equals(expected, comparisonType).Should().BeTrue(); + p.AsSpan().Equals(expected, comparisonType).Should().BeTrue(); + p.AsMemory().Span.Equals(expected, comparisonType).Should().BeTrue(); } } diff --git a/Tests/TrimTests.cs b/Tests/TrimTests.cs index 1707fe1..f5e9bec 100644 --- a/Tests/TrimTests.cs +++ b/Tests/TrimTests.cs @@ -1,9 +1,8 @@ -using System; -using System.Text.RegularExpressions; -using Xunit; +using System.Text.RegularExpressions; namespace Open.Text.Tests; +[ExcludeFromCodeCoverage] public static class TrimTests { [Fact] @@ -114,9 +113,9 @@ public static void TrimmedEquals() { Assert.True(string.Empty.TrimmedEquals(string.Empty)); Assert.True(" ".AsSegment().TrimmedEquals(string.Empty)); - Assert.True(" ".AsSegment().TrimmedEquals(string.Empty, new[] { ' ' }.AsSpan())); + Assert.True(" ".AsSegment().TrimmedEquals(string.Empty, " ".AsSpan())); Assert.True(" ".AsSegment().TrimmedEquals(ReadOnlySpan.Empty)); - Assert.True(" ".AsSegment().TrimmedEquals(ReadOnlySpan.Empty, new[] { ' ' }.AsSpan())); + Assert.True(" ".AsSegment().TrimmedEquals(ReadOnlySpan.Empty, " ".AsSpan())); Assert.True(" ".TrimmedEquals(string.Empty)); Assert.False("AB ".TrimmedEquals("ABC")); Assert.False("A ".TrimmedEquals("ABC")); diff --git a/Tests/_Global.cs b/Tests/_Global.cs new file mode 100644 index 0000000..7605611 --- /dev/null +++ b/Tests/_Global.cs @@ -0,0 +1,3 @@ +global using FluentAssertions; +global using Xunit; +global using System.Diagnostics.CodeAnalysis; \ No newline at end of file From 283a11da90c05ae138bae93b361f6242fcbaba8b Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 10:15:28 -0700 Subject: [PATCH 3/8] Fix collection intializer null-ref. --- Source/Extensions._.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index 71fd37f..5851ae9 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -7,8 +7,14 @@ namespace Open.Text; public static partial class TextExtensions { private const uint BYTE_RED = 1024; - private static readonly string[] _byte_labels = ["KB", "MB", "GB", "TB", "PB"]; - private static readonly string[] _number_labels = ["K", "M", "B"]; + [SuppressMessage("Style", + "IDE0300:Simplify collection initialization", + Justification = "Can cause NullReferenceException when initializing a static class.")] + private static readonly string[] _byte_labels = new[] { "KB", "MB", "GB", "TB", "PB" }; + [SuppressMessage("Style", + "IDE0300:Simplify collection initialization", + Justification = "Can cause NullReferenceException when initializing a static class.")] + private static readonly string[] _number_labels = new[] { "K", "M", "B" }; /// /// Compiled pattern for finding alpha-numeric sequences. From 9e813c9429a7c121f3f3cc1d1018c231740c5f46 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:45:32 -0700 Subject: [PATCH 4/8] Fixed issue with legacy .NET capture. --- Source/Extensions.Split.cs | 10 ++-- Source/Extensions._.cs | 118 ++++++++++++------------------------- Source/Open.Text.csproj | 2 +- Source/RegexExtensions.cs | 75 +++++++++++++++++++++++ Source/RegexPatterns.cs | 24 ++++++++ Tests/FormattingTests.cs | 2 +- Tests/SplitTests.cs | 3 + 7 files changed, 147 insertions(+), 87 deletions(-) create mode 100644 Source/RegexExtensions.cs create mode 100644 Source/RegexPatterns.cs diff --git a/Source/Extensions.Split.cs b/Source/Extensions.Split.cs index 3cd8095..4c9cf0b 100644 --- a/Source/Extensions.Split.cs +++ b/Source/Extensions.Split.cs @@ -1,5 +1,9 @@ namespace Open.Text; +internal static class SingleEmpty +{ + public static readonly IReadOnlyList Instance = Array.AsReadOnly(new[] { string.Empty }); +} public static partial class TextExtensions { static ReadOnlySpan FirstSplitSpan(StringSegment source, int start, int i, int n, out int nextIndex) @@ -363,8 +367,6 @@ static IEnumerable> SplitAsMemoryCore(string source, string } } - static readonly IReadOnlyList SingleEmpty = new List { string.Empty }.AsReadOnly(); - /// /// Splits a sequence of characters into strings using the character provided. /// @@ -379,7 +381,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, switch (options) { case StringSplitOptions.None when source.Length == 0: - return SingleEmpty; + return SingleEmpty.Instance; case StringSplitOptions.RemoveEmptyEntries when source.Length == 0: return Array.Empty(); @@ -427,7 +429,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, switch (options) { case StringSplitOptions.None when source.IsEmpty: - return SingleEmpty; + return SingleEmpty.Instance; case StringSplitOptions.RemoveEmptyEntries when source.IsEmpty: return Array.Empty(); diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index 5851ae9..05abea2 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -7,26 +7,41 @@ namespace Open.Text; public static partial class TextExtensions { private const uint BYTE_RED = 1024; - [SuppressMessage("Style", - "IDE0300:Simplify collection initialization", - Justification = "Can cause NullReferenceException when initializing a static class.")] - private static readonly string[] _byte_labels = new[] { "KB", "MB", "GB", "TB", "PB" }; - [SuppressMessage("Style", - "IDE0300:Simplify collection initialization", - Justification = "Can cause NullReferenceException when initializing a static class.")] - private static readonly string[] _number_labels = new[] { "K", "M", "B" }; - /// - /// Compiled pattern for finding alpha-numeric sequences. - /// - public static readonly Regex ValidAlphaNumericOnlyPattern - = new(@"^\w+$", RegexOptions.Compiled); + private static IEnumerable ByteLabels { + get { + yield return "KB"; + yield return "MB"; + yield return "GB"; + yield return "TB"; + yield return "PB"; + } + } - /// - /// Compiled pattern for finding alpha-numeric sequences and possible surrounding white-space. - /// - public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern - = new(@"^\s*\w+\s*$", RegexOptions.Compiled); + private static IEnumerable NumberLabels + { + get + { + yield return "K"; + yield return "M"; + yield return "G"; + } + } + + /// + [Obsolete("Use RegexPatterns.ValidAlphaNumericOnlyPattern.")] + public static Regex ValidAlphaNumericOnlyPattern + => RegexPatterns.ValidAlphaNumericOnlyPattern; + + /// + [Obsolete("Use RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern.")] + public static Regex ValidAlphaNumericOnlyUntrimmedPattern + => RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern; + + /// + [Obsolete("Use RegexPatterns.WhiteSpacePattern.")] + public static Regex WhiteSpacePattern + => RegexPatterns.WhiteSpacePattern; /// /// Converts a string to title-case. @@ -121,60 +136,7 @@ public static string ToFormat(this T? value, string? format = null, CultureIn [ExcludeFromCodeCoverage] public static bool IsAlphaNumeric(this string source, bool trim = false) => !string.IsNullOrWhiteSpace(source) - && (trim ? ValidAlphaNumericOnlyUntrimmedPattern : ValidAlphaNumericOnlyPattern).IsMatch(source); - - #region Regex helper methods. - private static readonly Func _textDelegate = (Func) - typeof(Capture).GetProperty("Text", BindingFlags.Instance | BindingFlags.NonPublic)! - .GetGetMethod(nonPublic: true)! - .CreateDelegate(typeof(Func)); - - /// - /// Returns a ReadOnlySpan of the capture without creating a new string. - /// - /// This is a stop-gap until .NET 6 releases the .ValueSpan property. - /// The capture to get the span from. - public static ReadOnlySpan AsSpan(this Capture capture) - => capture is null - ? throw new ArgumentNullException(nameof(capture)) - : _textDelegate.Invoke(capture).AsSpan(capture.Index, capture.Length); - - /// - /// Gets a group by name. - /// - /// The group collection to get the group from. - /// The declared name of the group. - /// The value of the requested group or null if not found. - /// Groups or groupName is null. - public static string? GetValue(this GroupCollection groups, string groupName) - { - if (groups is null) - throw new ArgumentNullException(nameof(groups)); - if (groupName is null) - throw new ArgumentNullException(nameof(groupName)); - Contract.EndContractBlock(); - - var group = groups[groupName]; - return group.Success - ? group.Value - : null; - } - - /// The value of the requested group or an empty span if not found. - /// - public static ReadOnlySpan GetValueSpan(this GroupCollection groups, string groupName) - { - if (groups is null) - throw new ArgumentNullException(nameof(groups)); - if (groupName is null) - throw new ArgumentNullException(nameof(groupName)); - Contract.EndContractBlock(); - - var group = groups[groupName]; - return group.Success - ? group.AsSpan() - : []; - } + && (trim ? RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern : RegexPatterns.ValidAlphaNumericOnlyPattern).IsMatch(source); /// /// Returns the available matches as StringSegments. @@ -203,7 +165,6 @@ static IEnumerable AsSegmentsCore(Regex pattern, string input) } } } - #endregion #region Numeric string formatting. /// @@ -251,7 +212,7 @@ public static string ToByteString(this double bytes, string decimalFormat = "N1" return string.Format(cultureInfo, bytes == 1 ? BYTE : BYTES, bytes); var label = string.Empty; - foreach (var s in _byte_labels) + foreach (var s in ByteLabels) { label = s; bytes /= BYTE_RED; @@ -284,7 +245,7 @@ public static string ToMetricString(this double number, string decimalFormat = " return number.ToString(decimalFormat, cultureInfo ?? CultureInfo.InvariantCulture); var label = string.Empty; - foreach (var s in _number_labels) + foreach (var s in NumberLabels) { label = s; number /= 1000; @@ -309,11 +270,6 @@ public static string ToMetricString(this int number, string decimalFormat = "N1" => ToMetricString((double)number, decimalFormat, cultureInfo); #endregion - /// - /// Compiled Regex for finding white-space. - /// - public static readonly Regex WhiteSpacePattern = new(@"\s+", RegexOptions.Compiled); - /// /// Replaces any white-space with the specified string. /// Collapses multiple white-space characters to a single space if no replacement specified. @@ -328,7 +284,7 @@ public static string ReplaceWhiteSpace(this string source, string replace = " ") if (replace is null) throw new ArgumentNullException(nameof(replace)); Contract.EndContractBlock(); - return WhiteSpacePattern.Replace(source, replace); + return RegexPatterns.WhiteSpacePattern.Replace(source, replace); } /// diff --git a/Source/Open.Text.csproj b/Source/Open.Text.csproj index 07f9d25..c870717 100644 --- a/Source/Open.Text.csproj +++ b/Source/Open.Text.csproj @@ -20,7 +20,7 @@ https://github.com/Open-NET-Libraries/Open.Text git string, span, enum, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions, stringcomparable, spancomparable, stringsegment, splitassegment - 7.1.0 + 8.0.0 MIT true diff --git a/Source/RegexExtensions.cs b/Source/RegexExtensions.cs new file mode 100644 index 0000000..fc705cb --- /dev/null +++ b/Source/RegexExtensions.cs @@ -0,0 +1,75 @@ +namespace Open.Text; + +/// +/// A set of regular expression extensions. +/// +public static class RegexExtensions +{ + private static Func GetOriginalTextDelegate() + { + var textProp = typeof(Capture).GetProperty("Text", BindingFlags.Instance | BindingFlags.NonPublic); + if(textProp is not null) + { + var method = textProp.GetGetMethod(nonPublic: true) + ?? throw new InvalidOperationException("Could not find the Text property getter."); + + return (Func)method.CreateDelegate(typeof(Func)); + } + + // Some older versions of .NET use this instead. + var textField = typeof(Capture).GetField("_text", BindingFlags.Instance | BindingFlags.NonPublic); + return textField is not null + ? (capture => (string)textField.GetValue(capture)!) + : throw new NotSupportedException("Capture: could not find the Text property or _text field."); + } + + private static readonly Func _textDelegate = GetOriginalTextDelegate(); + + + /// + /// Returns a ReadOnlySpan of the capture without creating a new string. + /// + /// This is a stop-gap until .NET 6 releases the .ValueSpan property. + /// The capture to get the span from. + public static ReadOnlySpan AsSpan(this Capture capture) + => capture is null + ? throw new ArgumentNullException(nameof(capture)) + : _textDelegate(capture).AsSpan(capture.Index, capture.Length); + + /// + /// Gets a group by name. + /// + /// The group collection to get the group from. + /// The declared name of the group. + /// The value of the requested group or null if not found. + /// Groups or groupName is null. + public static string? GetValue(this GroupCollection groups, string groupName) + { + if (groups is null) + throw new ArgumentNullException(nameof(groups)); + if (groupName is null) + throw new ArgumentNullException(nameof(groupName)); + Contract.EndContractBlock(); + + var group = groups[groupName]; + return group.Success + ? group.Value + : null; + } + + /// The value of the requested group or an empty span if not found. + /// + public static ReadOnlySpan GetValueSpan(this GroupCollection groups, string groupName) + { + if (groups is null) + throw new ArgumentNullException(nameof(groups)); + if (groupName is null) + throw new ArgumentNullException(nameof(groupName)); + Contract.EndContractBlock(); + + var group = groups[groupName]; + return group.Success + ? group.AsSpan() + : []; + } +} diff --git a/Source/RegexPatterns.cs b/Source/RegexPatterns.cs new file mode 100644 index 0000000..14b0c89 --- /dev/null +++ b/Source/RegexPatterns.cs @@ -0,0 +1,24 @@ +namespace Open.Text; + +/// +/// A set of commonly used regular expression patterns. +/// +public static class RegexPatterns +{ + /// + /// Compiled pattern for finding alpha-numeric sequences. + /// + public static readonly Regex ValidAlphaNumericOnlyPattern + = new(@"^\w+$", RegexOptions.Compiled); + + /// + /// Compiled pattern for finding alpha-numeric sequences and possible surrounding white-space. + /// + public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern + = new(@"^\s*\w+\s*$", RegexOptions.Compiled); + + /// + /// Compiled Regex for finding white-space. + /// + public static readonly Regex WhiteSpacePattern = new(@"\s+", RegexOptions.Compiled); +} diff --git a/Tests/FormattingTests.cs b/Tests/FormattingTests.cs index ac7b908..67c8e39 100644 --- a/Tests/FormattingTests.cs +++ b/Tests/FormattingTests.cs @@ -31,7 +31,7 @@ public static void ToByteString(string expected, double bytes) [InlineData("999.1", 999.1)] [InlineData("1.1K", 1110)] [InlineData("1.1M", 1110 * 1000)] - [InlineData("1.1B", 1110 * 1000 * 1000)] + [InlineData("1.1G", 1110 * 1000 * 1000)] public static void ToMetricString(string expected, double bytes) => Assert.Equal(expected, bytes.ToMetricString()); } diff --git a/Tests/SplitTests.cs b/Tests/SplitTests.cs index 64493b3..b6f7624 100644 --- a/Tests/SplitTests.cs +++ b/Tests/SplitTests.cs @@ -94,6 +94,8 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl Assert.Equal(segments, span.Split(',', options)); Assert.Equal(segments, span.Split(",", options)); +#pragma warning disable CS0618 // Type or member is obsolete + // Use obsolete values to ensure they still work. Assert.Equal( TextExtensions .ValidAlphaNumericOnlyPattern @@ -104,6 +106,7 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl .ValidAlphaNumericOnlyPattern .AsSegments(sequence) .Select(m => m.Value)); +#pragma warning restore CS0618 // Type or member is obsolete var stringSegment = sequence.AsSegment(); var ss = stringSegment.SplitAsSegments(",", options).Select(s => s.Value).ToArray(); From 1974e3e8b0256d2cc0451f3ad07e71a205503e41 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 11:45:32 -0700 Subject: [PATCH 5/8] Fixed issue with legacy .NET capture. --- Source/Extensions.Split.cs | 10 ++-- Source/Extensions._.cs | 118 ++++++++++++------------------------- Source/Open.Text.csproj | 2 +- Source/RegexExtensions.cs | 74 +++++++++++++++++++++++ Source/RegexPatterns.cs | 24 ++++++++ Tests/FormattingTests.cs | 2 +- Tests/SplitTests.cs | 3 + 7 files changed, 146 insertions(+), 87 deletions(-) create mode 100644 Source/RegexExtensions.cs create mode 100644 Source/RegexPatterns.cs diff --git a/Source/Extensions.Split.cs b/Source/Extensions.Split.cs index 3cd8095..4c9cf0b 100644 --- a/Source/Extensions.Split.cs +++ b/Source/Extensions.Split.cs @@ -1,5 +1,9 @@ namespace Open.Text; +internal static class SingleEmpty +{ + public static readonly IReadOnlyList Instance = Array.AsReadOnly(new[] { string.Empty }); +} public static partial class TextExtensions { static ReadOnlySpan FirstSplitSpan(StringSegment source, int start, int i, int n, out int nextIndex) @@ -363,8 +367,6 @@ static IEnumerable> SplitAsMemoryCore(string source, string } } - static readonly IReadOnlyList SingleEmpty = new List { string.Empty }.AsReadOnly(); - /// /// Splits a sequence of characters into strings using the character provided. /// @@ -379,7 +381,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, switch (options) { case StringSplitOptions.None when source.Length == 0: - return SingleEmpty; + return SingleEmpty.Instance; case StringSplitOptions.RemoveEmptyEntries when source.Length == 0: return Array.Empty(); @@ -427,7 +429,7 @@ public static IReadOnlyList Split(this ReadOnlySpan source, switch (options) { case StringSplitOptions.None when source.IsEmpty: - return SingleEmpty; + return SingleEmpty.Instance; case StringSplitOptions.RemoveEmptyEntries when source.IsEmpty: return Array.Empty(); diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index 5851ae9..05abea2 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -7,26 +7,41 @@ namespace Open.Text; public static partial class TextExtensions { private const uint BYTE_RED = 1024; - [SuppressMessage("Style", - "IDE0300:Simplify collection initialization", - Justification = "Can cause NullReferenceException when initializing a static class.")] - private static readonly string[] _byte_labels = new[] { "KB", "MB", "GB", "TB", "PB" }; - [SuppressMessage("Style", - "IDE0300:Simplify collection initialization", - Justification = "Can cause NullReferenceException when initializing a static class.")] - private static readonly string[] _number_labels = new[] { "K", "M", "B" }; - /// - /// Compiled pattern for finding alpha-numeric sequences. - /// - public static readonly Regex ValidAlphaNumericOnlyPattern - = new(@"^\w+$", RegexOptions.Compiled); + private static IEnumerable ByteLabels { + get { + yield return "KB"; + yield return "MB"; + yield return "GB"; + yield return "TB"; + yield return "PB"; + } + } - /// - /// Compiled pattern for finding alpha-numeric sequences and possible surrounding white-space. - /// - public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern - = new(@"^\s*\w+\s*$", RegexOptions.Compiled); + private static IEnumerable NumberLabels + { + get + { + yield return "K"; + yield return "M"; + yield return "G"; + } + } + + /// + [Obsolete("Use RegexPatterns.ValidAlphaNumericOnlyPattern.")] + public static Regex ValidAlphaNumericOnlyPattern + => RegexPatterns.ValidAlphaNumericOnlyPattern; + + /// + [Obsolete("Use RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern.")] + public static Regex ValidAlphaNumericOnlyUntrimmedPattern + => RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern; + + /// + [Obsolete("Use RegexPatterns.WhiteSpacePattern.")] + public static Regex WhiteSpacePattern + => RegexPatterns.WhiteSpacePattern; /// /// Converts a string to title-case. @@ -121,60 +136,7 @@ public static string ToFormat(this T? value, string? format = null, CultureIn [ExcludeFromCodeCoverage] public static bool IsAlphaNumeric(this string source, bool trim = false) => !string.IsNullOrWhiteSpace(source) - && (trim ? ValidAlphaNumericOnlyUntrimmedPattern : ValidAlphaNumericOnlyPattern).IsMatch(source); - - #region Regex helper methods. - private static readonly Func _textDelegate = (Func) - typeof(Capture).GetProperty("Text", BindingFlags.Instance | BindingFlags.NonPublic)! - .GetGetMethod(nonPublic: true)! - .CreateDelegate(typeof(Func)); - - /// - /// Returns a ReadOnlySpan of the capture without creating a new string. - /// - /// This is a stop-gap until .NET 6 releases the .ValueSpan property. - /// The capture to get the span from. - public static ReadOnlySpan AsSpan(this Capture capture) - => capture is null - ? throw new ArgumentNullException(nameof(capture)) - : _textDelegate.Invoke(capture).AsSpan(capture.Index, capture.Length); - - /// - /// Gets a group by name. - /// - /// The group collection to get the group from. - /// The declared name of the group. - /// The value of the requested group or null if not found. - /// Groups or groupName is null. - public static string? GetValue(this GroupCollection groups, string groupName) - { - if (groups is null) - throw new ArgumentNullException(nameof(groups)); - if (groupName is null) - throw new ArgumentNullException(nameof(groupName)); - Contract.EndContractBlock(); - - var group = groups[groupName]; - return group.Success - ? group.Value - : null; - } - - /// The value of the requested group or an empty span if not found. - /// - public static ReadOnlySpan GetValueSpan(this GroupCollection groups, string groupName) - { - if (groups is null) - throw new ArgumentNullException(nameof(groups)); - if (groupName is null) - throw new ArgumentNullException(nameof(groupName)); - Contract.EndContractBlock(); - - var group = groups[groupName]; - return group.Success - ? group.AsSpan() - : []; - } + && (trim ? RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern : RegexPatterns.ValidAlphaNumericOnlyPattern).IsMatch(source); /// /// Returns the available matches as StringSegments. @@ -203,7 +165,6 @@ static IEnumerable AsSegmentsCore(Regex pattern, string input) } } } - #endregion #region Numeric string formatting. /// @@ -251,7 +212,7 @@ public static string ToByteString(this double bytes, string decimalFormat = "N1" return string.Format(cultureInfo, bytes == 1 ? BYTE : BYTES, bytes); var label = string.Empty; - foreach (var s in _byte_labels) + foreach (var s in ByteLabels) { label = s; bytes /= BYTE_RED; @@ -284,7 +245,7 @@ public static string ToMetricString(this double number, string decimalFormat = " return number.ToString(decimalFormat, cultureInfo ?? CultureInfo.InvariantCulture); var label = string.Empty; - foreach (var s in _number_labels) + foreach (var s in NumberLabels) { label = s; number /= 1000; @@ -309,11 +270,6 @@ public static string ToMetricString(this int number, string decimalFormat = "N1" => ToMetricString((double)number, decimalFormat, cultureInfo); #endregion - /// - /// Compiled Regex for finding white-space. - /// - public static readonly Regex WhiteSpacePattern = new(@"\s+", RegexOptions.Compiled); - /// /// Replaces any white-space with the specified string. /// Collapses multiple white-space characters to a single space if no replacement specified. @@ -328,7 +284,7 @@ public static string ReplaceWhiteSpace(this string source, string replace = " ") if (replace is null) throw new ArgumentNullException(nameof(replace)); Contract.EndContractBlock(); - return WhiteSpacePattern.Replace(source, replace); + return RegexPatterns.WhiteSpacePattern.Replace(source, replace); } /// diff --git a/Source/Open.Text.csproj b/Source/Open.Text.csproj index 07f9d25..c870717 100644 --- a/Source/Open.Text.csproj +++ b/Source/Open.Text.csproj @@ -20,7 +20,7 @@ https://github.com/Open-NET-Libraries/Open.Text git string, span, enum, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions, stringcomparable, spancomparable, stringsegment, splitassegment - 7.1.0 + 8.0.0 MIT true diff --git a/Source/RegexExtensions.cs b/Source/RegexExtensions.cs new file mode 100644 index 0000000..00daaf5 --- /dev/null +++ b/Source/RegexExtensions.cs @@ -0,0 +1,74 @@ +namespace Open.Text; + +/// +/// A set of regular expression extensions. +/// +public static class RegexExtensions +{ + private static Func GetOriginalTextDelegate() + { + var textProp = typeof(Capture).GetProperty("Text", BindingFlags.Instance | BindingFlags.NonPublic); + if(textProp is not null) + { + var method = textProp.GetGetMethod(nonPublic: true) + ?? throw new InvalidOperationException("Could not find the Text property getter."); + + return (Func)method.CreateDelegate(typeof(Func)); + } + + // Some older versions of .NET use this instead. + var textField = typeof(Capture).GetField("_text", BindingFlags.Instance | BindingFlags.NonPublic); + return textField is not null + ? (capture => (string)textField.GetValue(capture)!) + : throw new NotSupportedException("Capture: could not find the Text property or _text field."); + } + + private static readonly Func _textDelegate = GetOriginalTextDelegate(); + + /// + /// Returns a ReadOnlySpan of the capture without creating a new string. + /// + /// This is a stop-gap until .NET 6 releases the .ValueSpan property. + /// The capture to get the span from. + public static ReadOnlySpan AsSpan(this Capture capture) + => capture is null + ? throw new ArgumentNullException(nameof(capture)) + : _textDelegate(capture).AsSpan(capture.Index, capture.Length); + + /// + /// Gets a group by name. + /// + /// The group collection to get the group from. + /// The declared name of the group. + /// The value of the requested group or null if not found. + /// Groups or groupName is null. + public static string? GetValue(this GroupCollection groups, string groupName) + { + if (groups is null) + throw new ArgumentNullException(nameof(groups)); + if (groupName is null) + throw new ArgumentNullException(nameof(groupName)); + Contract.EndContractBlock(); + + var group = groups[groupName]; + return group.Success + ? group.Value + : null; + } + + /// The value of the requested group or an empty span if not found. + /// + public static ReadOnlySpan GetValueSpan(this GroupCollection groups, string groupName) + { + if (groups is null) + throw new ArgumentNullException(nameof(groups)); + if (groupName is null) + throw new ArgumentNullException(nameof(groupName)); + Contract.EndContractBlock(); + + var group = groups[groupName]; + return group.Success + ? group.AsSpan() + : []; + } +} diff --git a/Source/RegexPatterns.cs b/Source/RegexPatterns.cs new file mode 100644 index 0000000..14b0c89 --- /dev/null +++ b/Source/RegexPatterns.cs @@ -0,0 +1,24 @@ +namespace Open.Text; + +/// +/// A set of commonly used regular expression patterns. +/// +public static class RegexPatterns +{ + /// + /// Compiled pattern for finding alpha-numeric sequences. + /// + public static readonly Regex ValidAlphaNumericOnlyPattern + = new(@"^\w+$", RegexOptions.Compiled); + + /// + /// Compiled pattern for finding alpha-numeric sequences and possible surrounding white-space. + /// + public static readonly Regex ValidAlphaNumericOnlyUntrimmedPattern + = new(@"^\s*\w+\s*$", RegexOptions.Compiled); + + /// + /// Compiled Regex for finding white-space. + /// + public static readonly Regex WhiteSpacePattern = new(@"\s+", RegexOptions.Compiled); +} diff --git a/Tests/FormattingTests.cs b/Tests/FormattingTests.cs index ac7b908..67c8e39 100644 --- a/Tests/FormattingTests.cs +++ b/Tests/FormattingTests.cs @@ -31,7 +31,7 @@ public static void ToByteString(string expected, double bytes) [InlineData("999.1", 999.1)] [InlineData("1.1K", 1110)] [InlineData("1.1M", 1110 * 1000)] - [InlineData("1.1B", 1110 * 1000 * 1000)] + [InlineData("1.1G", 1110 * 1000 * 1000)] public static void ToMetricString(string expected, double bytes) => Assert.Equal(expected, bytes.ToMetricString()); } diff --git a/Tests/SplitTests.cs b/Tests/SplitTests.cs index 64493b3..b6f7624 100644 --- a/Tests/SplitTests.cs +++ b/Tests/SplitTests.cs @@ -94,6 +94,8 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl Assert.Equal(segments, span.Split(',', options)); Assert.Equal(segments, span.Split(",", options)); +#pragma warning disable CS0618 // Type or member is obsolete + // Use obsolete values to ensure they still work. Assert.Equal( TextExtensions .ValidAlphaNumericOnlyPattern @@ -104,6 +106,7 @@ public static void Split(string sequence, StringSplitOptions options = StringSpl .ValidAlphaNumericOnlyPattern .AsSegments(sequence) .Select(m => m.Value)); +#pragma warning restore CS0618 // Type or member is obsolete var stringSegment = sequence.AsSegment(); var ss = stringSegment.SplitAsSegments(",", options).Select(s => s.Value).ToArray(); From c5e88b13b25f79a21c07594ac99c30e692f2d120 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 14:11:59 -0700 Subject: [PATCH 6/8] IndexOf, LastIndexOf, and Contains extensions coverage. --- Source/Extensions.IndexOf.cs | 112 ++++++++++++++++++++------ Tests/IndexOfTests.cs | 147 ++++++++++++++++++++++++++++++++++- 2 files changed, 233 insertions(+), 26 deletions(-) diff --git a/Source/Extensions.IndexOf.cs b/Source/Extensions.IndexOf.cs index 76621db..099ce69 100644 --- a/Source/Extensions.IndexOf.cs +++ b/Source/Extensions.IndexOf.cs @@ -25,7 +25,7 @@ public static int IndexOf(this ReadOnlySpan source, char search, StringCom } var searchUpper = toUpper(search); - if(searchUpper == search) + if (searchUpper == search) return source.IndexOf(search); for (var i = 0; i < source.Length; i++) @@ -39,6 +39,15 @@ public static int IndexOf(this ReadOnlySpan source, char search, StringCom return -1; } + /// + public static int IndexOf(this ReadOnlySpan source, char search, int startIndex, StringComparison comparisonType) + { + if (startIndex >= source.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, "Must be less than the length of the source."); + var i = source.Slice(startIndex).IndexOf(search, comparisonType); + return i == -1 ? -1 : i + startIndex; + } + /// public static int LastIndexOf(this ReadOnlySpan source, char search, StringComparison comparisonType) { @@ -77,22 +86,30 @@ public static int LastIndexOf(this ReadOnlySpan source, char search, Strin return -1; } + /// + public static int LastIndexOf(this string source, char search, StringComparison comparisonType) + => source.AsSpan().LastIndexOf(search, comparisonType); + + /// + public static int LastIndexOf(this StringSegment source, char search, StringComparison comparisonType) + => source.AsSpan().LastIndexOf(search, comparisonType); + /// public static int IndexOf(this StringSegment source, char search, StringComparison comparisonType) => source.HasValue ? source.AsSpan().IndexOf(search, comparisonType) : -1; - /// - public static int LastIndexOf(this StringSegment source, char search, StringComparison comparisonType) - => source.HasValue ? source.AsSpan().LastIndexOf(search, comparisonType) : -1; + /// + public static int IndexOf(this StringSegment source, char search, int startIndex, StringComparison comparisonType) + => source.HasValue ? source.AsSpan().IndexOf(search, startIndex, comparisonType) : -1; #if NETSTANDARD2_0 /// public static int IndexOf(this string source, char search, StringComparison comparisonType) - => source is null ? throw new ArgumentNullException(nameof(source)) : source.AsSpan().IndexOf(search, comparisonType); + => source.AsSpan().IndexOf(search, comparisonType); /// - public static int LastIndexOf(this string source, char search, StringComparison comparisonType) - => source is null ? throw new ArgumentNullException(nameof(source)) : source.AsSpan().LastIndexOf(search, comparisonType); + public static int LastIndexOf(this string source, char search) + => source.AsSpan().LastIndexOf(search); #endif /// @@ -106,6 +123,9 @@ public static int IndexOf( ReadOnlySpan sequence, StringComparison comparisonType = StringComparison.Ordinal) { + // This is a weird decision, but must be this way to maintain parity with other methods. + if (sequence.IsEmpty) return 0; + switch (comparisonType) { case StringComparison.OrdinalIgnoreCase: @@ -126,9 +146,6 @@ public static int IndexOf( } int sequenceLength = sequence.Length; - if (sequenceLength == 0) - throw new ArgumentException("Sequence must have at least one character.", nameof(sequence)); - int sourceLength = source.Length; if (sourceLength == 0) return -1; if (sequenceLength > sourceLength) return -1; @@ -175,6 +192,10 @@ public static int LastIndexOf( ReadOnlySpan sequence, StringComparison comparisonType) { + // This is a weird decision, but must be this way to maintain parity with other methods. + if (sequence.IsEmpty) + return source.Length; + switch (comparisonType) { case StringComparison.OrdinalIgnoreCase: @@ -195,9 +216,6 @@ public static int LastIndexOf( } int sequenceLength = sequence.Length; - if (sequenceLength == 0) - throw new ArgumentException("Sequence must have at least one character.", nameof(sequence)); - int sourceLength = source.Length; if (sourceLength == 0) return -1; if (sequenceLength > sourceLength) return -1; @@ -234,13 +252,21 @@ public static int LastIndexOf(this StringSegment source, ReadOnlySpan sequ => source.AsSpan().LastIndexOf(sequence, comparisonType); #pragma warning restore CS1587 // XML comment is not placed on a valid language element + /// + public static int LastIndexOf(this string source, StringSegment sequence, StringComparison comparisonType) + => sequence.HasValue ? source.AsSpan().LastIndexOf(sequence.AsSpan(), comparisonType) : -1; + + /// + public static int LastIndexOf(this string source, ReadOnlySpan sequence, StringComparison comparisonType) + => source.AsSpan().LastIndexOf(sequence, comparisonType); + /// public static int LastIndexOf(this ReadOnlySpan source, StringSegment sequence, StringComparison comparisonType) - => source.LastIndexOf(sequence.AsSpan(), comparisonType); + => sequence.HasValue ? source.LastIndexOf(sequence.AsSpan(), comparisonType) : -1; /// public static int LastIndexOf(this StringSegment source, StringSegment sequence, StringComparison comparisonType) - => source.AsSpan().LastIndexOf(sequence.AsSpan(), comparisonType); + => sequence.HasValue ? source.AsSpan().LastIndexOf(sequence.AsSpan(), comparisonType) : -1; /// /// Reports the zero-based index of the first occurrence @@ -261,7 +287,10 @@ public static int IndexOf( if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, "Must be at least zero."); - if(startIndex == 0) + if (sequence.IsEmpty) + return 0; + + if (startIndex == 0) return IndexOf(source, sequence, comparisonType); var span = source.Slice(startIndex); @@ -275,7 +304,7 @@ public static int IndexOf( StringSegment sequence, int startIndex = 0, StringComparison comparisonType = StringComparison.Ordinal) - => source.IndexOf(sequence.AsSpan(), startIndex, comparisonType); + => sequence.HasValue ? source.IndexOf(sequence.AsSpan(), startIndex, comparisonType) : -1; /// public static int IndexOf( @@ -293,6 +322,14 @@ public static int IndexOf( StringComparison comparisonType = StringComparison.Ordinal) => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + /// + public static int IndexOf( + this string source, + StringSegment sequence, + int startIndex = 0, + StringComparison comparisonType = StringComparison.Ordinal) + => source.AsSpan().IndexOf(sequence, startIndex, comparisonType); + /// public static int IndexOf( this StringSegment source, @@ -310,7 +347,7 @@ public static int IndexOf( this StringSegment source, StringSegment sequence, StringComparison comparisonType) - => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType); + => sequence.HasValue ? IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) : -1; /// public static int IndexOf( @@ -331,14 +368,39 @@ public static int IndexOf( this ReadOnlySpan source, StringSegment sequence, StringComparison comparisonType) - => IndexOf(source, sequence.AsSpan(), comparisonType); + => sequence.HasValue ? IndexOf(source, sequence.AsSpan(), comparisonType) : -1; /// public static int IndexOf( this string source, StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType); + => sequence.HasValue ? IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) : -1; + + /// + /// Checks if the is contained + /// within the using the . + /// + /// + /// if the is contained; + /// otherwise . + /// + public static bool Contains( + this ReadOnlySpan source, + char value, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source, value, comparisonType) != -1; + + /// + public static bool Contains( + this StringSegment source, + char value, StringComparison comparisonType = StringComparison.Ordinal) + => IndexOf(source, value, comparisonType) != -1; + +#if NETSTANDARD2_0 + /// + public static bool Contains(this string source, char value, StringComparison comparisonType) + => source.IndexOf(value, comparisonType) != -1; +#endif /// /// Checks if the is contained @@ -351,29 +413,29 @@ public static int IndexOf( public static bool Contains( this StringSegment source, StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) != -1; + => IndexOf(source, sequence, comparisonType) != -1; /// public static bool Contains( this StringSegment source, ReadOnlySpan sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source.AsSpan(), sequence, comparisonType) != -1; + => IndexOf(source, sequence, comparisonType) != -1; /// public static bool Contains( this ReadOnlySpan source, StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source, sequence.AsSpan(), comparisonType) != -1; + => IndexOf(source, sequence, comparisonType) != -1; /// public static bool Contains( this string source, StringSegment sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source.AsSpan(), sequence.AsSpan(), comparisonType) != -1; + => IndexOf(source, sequence, comparisonType) != -1; /// public static bool Contains( this string source, ReadOnlySpan sequence, StringComparison comparisonType = StringComparison.Ordinal) - => IndexOf(source.AsSpan(), sequence, comparisonType) != -1; + => IndexOf(source, sequence, comparisonType) != -1; } diff --git a/Tests/IndexOfTests.cs b/Tests/IndexOfTests.cs index 2443f39..1c2cc70 100644 --- a/Tests/IndexOfTests.cs +++ b/Tests/IndexOfTests.cs @@ -3,12 +3,23 @@ [ExcludeFromCodeCoverage] public static class IndexOfTests { - const string HELLO_WORLD_IM_HERE = "Hello World, I'm here"; + const string HELLO_WORLD_IM_HERE = "Hello World, world I'm here"; [Theory] + [InlineData(HELLO_WORLD_IM_HERE, "", StringComparison.Ordinal)] [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.InvariantCulture)] [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.OrdinalIgnoreCase)] [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.InvariantCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.InvariantCultureIgnoreCase)] public static void IndexOf(string source, string value, StringComparison comparison) { var expected = source.IndexOf(value, comparison); @@ -24,5 +35,139 @@ public static void IndexOf(string source, string value, StringComparison compari source.IndexOf(value.AsSegment(), 2, comparison).Should().Be(expected); source.AsSpan().IndexOf(value, 2, comparison).Should().Be(expected); source.AsSegment().IndexOf(value, 2, comparison).Should().Be(expected); + + var found = expected != -1; + source.Contains(value, comparison).Should().Be(found); + source.AsSpan().Contains(value, comparison).Should().Be(found); + source.Contains(value.AsSpan(), comparison).Should().Be(found); + source.AsSpan().Contains(value.AsSpan(), comparison).Should().Be(found); + } + + [Theory] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, StringComparison.InvariantCultureIgnoreCase)] + public static void IndexOfNull(string source, StringComparison comparison) + { + // An empty ReadOnlySpan appears to be always be a valid match of index 0 because it can't be determined if null. + // Where as with a StringSegment, null can never be found but it's not necessary to throw. + + var value = default(string).AsSegment(); + source.IndexOf(value.AsSpan(), comparison).Should().Be(0); + source.IndexOf(value, comparison).Should().Be(-1); + source.AsSpan().IndexOf(value, comparison).Should().Be(-1); + source.AsSegment().IndexOf(value, comparison).Should().Be(-1); + source.IndexOf(value.AsSpan(), 0, comparison).Should().Be(0); + source.IndexOf(value, 0, comparison).Should().Be(-1); + source.AsSpan().IndexOf(value, 0, comparison).Should().Be(-1); + source.AsSegment().IndexOf(value, 0, comparison).Should().Be(-1); + source.IndexOf(value.AsSpan(), 2, comparison).Should().Be(0); + source.IndexOf(value, 2, comparison).Should().Be(-1); + source.AsSpan().IndexOf(value, 2, comparison).Should().Be(-1); + source.AsSegment().IndexOf(value, 2, comparison).Should().Be(-1); + + source.LastIndexOf(value.AsSpan(), comparison).Should().Be(HELLO_WORLD_IM_HERE.Length); + source.LastIndexOf(value, comparison).Should().Be(-1); + source.AsSpan().LastIndexOf(value, comparison).Should().Be(-1); + source.AsSegment().LastIndexOf(value, comparison).Should().Be(-1); + + source.Contains(value, comparison).Should().BeFalse(); + source.AsSpan().Contains(value, comparison).Should().BeFalse(); + source.Contains(value.AsSpan(), comparison).Should().BeTrue(); + source.AsSpan().Contains(value.AsSpan(), comparison).Should().BeTrue(); + } + + [Theory] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.InvariantCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.InvariantCultureIgnoreCase)] + public static void IndexOfChar(string source, char value, StringComparison comparison) + { + var expected = source.IndexOf(value, comparison); + source.AsSpan().IndexOf(value, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, comparison).Should().Be(expected); + Assert.Throws(() => source.AsSpan().IndexOf(value, -1, comparison)); + source.AsSpan().IndexOf(value, 0, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, 0, comparison).Should().Be(expected); + source.AsSpan().IndexOf(value, 2, comparison).Should().Be(expected); + source.AsSegment().IndexOf(value, 2, comparison).Should().Be(expected); + + var found = expected != -1; + source.Contains(value, comparison).Should().Be(found); + source.AsSpan().Contains(value, comparison).Should().Be(found); + source.AsSegment().Contains(value, comparison).Should().Be(found); + } + + // Now the LastIndexOf tests. + [Theory] + [InlineData(HELLO_WORLD_IM_HERE, "", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "World", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "world", StringComparison.InvariantCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, "foo", StringComparison.InvariantCultureIgnoreCase)] + public static void LastIndexOf(string source, string value, StringComparison comparison) + { + // This reports differently for 4.72 than later versions. + // We will retain modern behavior as it makes slighty more sense. + var expected = source.LastIndexOf(value, comparison); +#if NET472 + if (value == "") + expected++; +#endif + source.LastIndexOf(value.AsSpan(), comparison).Should().Be(expected); + source.LastIndexOf(value.AsSegment(), comparison).Should().Be(expected); + source.AsSpan().LastIndexOf(value, comparison).Should().Be(expected); + source.AsSegment().LastIndexOf(value, comparison).Should().Be(expected); + source.AsSpan().LastIndexOf(value.AsSpan(), comparison).Should().Be(expected); + source.AsSegment().LastIndexOf(value.AsSpan(), comparison).Should().Be(expected); + source.AsSpan().LastIndexOf(value.AsSegment(), comparison).Should().Be(expected); + source.AsSegment().LastIndexOf(value.AsSegment(), comparison).Should().Be(expected); + if (value.Length == 0) + string.Empty.LastIndexOf(value.AsSpan(), comparison).Should().Be(0); + } + + [Theory] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'W', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'w', StringComparison.InvariantCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.Ordinal)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.CurrentCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.InvariantCulture)] + [InlineData(HELLO_WORLD_IM_HERE, 'X', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.OrdinalIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.CurrentCultureIgnoreCase)] + [InlineData(HELLO_WORLD_IM_HERE, 'x', StringComparison.InvariantCultureIgnoreCase)] + public static void LastIndexOfChar(string source, char value, StringComparison comparison) + { + var expected = source.LastIndexOf(value, comparison); + source.AsSpan().LastIndexOf(value, comparison).Should().Be(expected); + source.AsSegment().LastIndexOf(value, comparison).Should().Be(expected); } } From 067f99546d6e3180c6b7addf3768b0c6860d5626 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:01:41 -0700 Subject: [PATCH 7/8] Added benchmarks for IndexOfTests. --- Benchmarks/EnumAttributeTests.cs | 1 - Benchmarks/EnumParseTests.cs | 16 ++++--- Benchmarks/IndexOfTests.cs | 63 ++++++++++++++++++++++++++ Benchmarks/Open.Text.Benchmarks.csproj | 10 ++-- Benchmarks/Program.cs | 9 +++- 5 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 Benchmarks/IndexOfTests.cs diff --git a/Benchmarks/EnumAttributeTests.cs b/Benchmarks/EnumAttributeTests.cs index 634306d..7716f29 100644 --- a/Benchmarks/EnumAttributeTests.cs +++ b/Benchmarks/EnumAttributeTests.cs @@ -2,7 +2,6 @@ namespace Open.Text.Benchmarks; -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] public class EnumAttributeTests { public static IReadOnlyList GetAttribute(Greek value) diff --git a/Benchmarks/EnumParseTests.cs b/Benchmarks/EnumParseTests.cs index 7759074..2f869cd 100644 --- a/Benchmarks/EnumParseTests.cs +++ b/Benchmarks/EnumParseTests.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using CommandLine; using FastEnumUtility; namespace Open.Text.Benchmarks; @@ -68,8 +69,9 @@ abstract class Tests static readonly Dictionary LookupD = Enum - .GetValues() - .ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.Ordinal); + .GetValues(typeof(Greek)) + .Cast() + .ToDictionary(e => e.GetName(), e => e, StringComparer.Ordinal); protected virtual bool Lookup(string value, out Greek e) => LookupD.TryGetValue(value, out e); @@ -261,8 +263,9 @@ public override Greek FastEnumParse() static readonly Dictionary LookupD = Enum - .GetValues() - .ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.OrdinalIgnoreCase); + .GetValues(typeof(Greek)) + .Cast() + .ToDictionary(e => e.GetName(), e => e, StringComparer.OrdinalIgnoreCase); protected override bool Lookup(string value, out Greek e) => LookupD.TryGetValue(value, out e); @@ -327,8 +330,9 @@ public override Greek FastEnumParse() static readonly Dictionary LookupD = Enum - .GetValues() - .ToDictionary(e => Enum.GetName(e)!, e => e, StringComparer.OrdinalIgnoreCase); + .GetValues(typeof(Greek)) + .Cast() + .ToDictionary(e => e.GetName(), e => e, StringComparer.OrdinalIgnoreCase); protected override bool Lookup(string value, out Greek e) => LookupD.TryGetValue(value, out e); diff --git a/Benchmarks/IndexOfTests.cs b/Benchmarks/IndexOfTests.cs new file mode 100644 index 0000000..fde4552 --- /dev/null +++ b/Benchmarks/IndexOfTests.cs @@ -0,0 +1,63 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Open.Text.Benchmarks; + +[MemoryDiagnoser] +[SimpleJob(RuntimeMoniker.Net472, baseline: true)] +[SimpleJob(RuntimeMoniker.Net60)] +[SimpleJob(RuntimeMoniker.Net80)] +public class IndexOfTests +{ + public const string Text = "The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog"; + public static readonly string TextUpper = Text.ToUpper(); + public const string Search = "fox"; + public const string SearchCased = "Fox"; + + private StringComparison _comparison = StringComparison.Ordinal; + private StringComparison _comparisonCaseIgnored; + + [Params( + StringComparison.Ordinal, + StringComparison.CurrentCulture, + StringComparison.InvariantCulture)] + public StringComparison Comparison + { + get => _comparison; + set + { + _comparison = value; + _comparisonCaseIgnored = _comparison switch + { + StringComparison.Ordinal => StringComparison.OrdinalIgnoreCase, + StringComparison.CurrentCulture => StringComparison.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture => StringComparison.InvariantCultureIgnoreCase, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; + } + } + + [Benchmark(Baseline = true)] + public int IndexOf() + => Text.IndexOf(Search, _comparison); + + [Benchmark] + public int IndexOfCaseIgnored() + => Text.IndexOf(SearchCased, _comparisonCaseIgnored); + + [Benchmark] + public int IndexOfSpan() + => Text.IndexOf(Search.AsSpan(), _comparison); + + [Benchmark] + public int IndexOfSpanCaseIgnored() + => Text.IndexOf(SearchCased.AsSpan(), _comparisonCaseIgnored); + + [Benchmark] + public int IndexOfSpanSlice() + => Text.IndexOf(Text.AsSpan(16, 3), _comparison); + + [Benchmark] + public int IndexOfSpanSliceCaseIgnored() + => Text.IndexOf(TextUpper.AsSpan(16, 3), _comparisonCaseIgnored); +} diff --git a/Benchmarks/Open.Text.Benchmarks.csproj b/Benchmarks/Open.Text.Benchmarks.csproj index c9fda7a..0b20459 100644 --- a/Benchmarks/Open.Text.Benchmarks.csproj +++ b/Benchmarks/Open.Text.Benchmarks.csproj @@ -1,15 +1,17 @@ - + Exe - net8.0 + net472;net6.0;net8.0 enable enable + latest + CA1822 - - + + diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs index d98429a..d57d654 100644 --- a/Benchmarks/Program.cs +++ b/Benchmarks/Program.cs @@ -1,9 +1,14 @@ using BenchmarkDotNet.Running; using Open.Text.Benchmarks; +//BenchmarkSwitcher +// .FromAssembly(typeof(Program).Assembly) +// .Run(args); // crucial to make it work + //BenchmarkRunner.Run(); -//enchmarkRunner.Run(); +//BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); -BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); +BenchmarkRunner.Run(); From a974ddef76d40078d604168cd0ad154e733e5838 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:39:25 -0700 Subject: [PATCH 8/8] Cleanup. --- Benchmarks/CharAssumptionTests.cs | 1 - Benchmarks/EnumToStringTests.cs | 1 - Benchmarks/IsDefinedTests.cs | 2 - Benchmarks/StringConcatTests.cs | 2 - Source/EnumValue.cs | 25 ++-- Source/Extensions.Equals.cs | 45 +++--- Source/Extensions.IndexOf.cs | 2 +- Source/Extensions.Split.cs | 1 + Source/Extensions.StringSegment.cs | 3 +- Source/Extensions._.cs | 34 +---- Source/RegexExtensions.cs | 30 +++- Source/StringBuilderExtensions.cs | 22 +-- Source/StringBuilderHelper.cs | 212 ++++++++++++++--------------- Source/StringSegmentSearch.cs | 4 +- Source/StringSubsegment.cs | 4 +- Source/_Global.cs | 2 +- Tests/IndexOfTests.cs | 2 +- Tests/SplitTests.cs | 4 +- Tests/StringBuilderTests.cs | 2 +- Tests/_Global.cs | 2 +- 20 files changed, 191 insertions(+), 209 deletions(-) diff --git a/Benchmarks/CharAssumptionTests.cs b/Benchmarks/CharAssumptionTests.cs index 923808c..25023c3 100644 --- a/Benchmarks/CharAssumptionTests.cs +++ b/Benchmarks/CharAssumptionTests.cs @@ -2,7 +2,6 @@ namespace Open.Text.Benchmarks; -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "For benchmarking.")] public class CharAssumptionTests { const string TestString = "abcdefghijklmnopqrstuvwxyz0123456789"; diff --git a/Benchmarks/EnumToStringTests.cs b/Benchmarks/EnumToStringTests.cs index 006e110..f4bd94c 100644 --- a/Benchmarks/EnumToStringTests.cs +++ b/Benchmarks/EnumToStringTests.cs @@ -3,7 +3,6 @@ namespace Open.Text.Benchmarks; -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Benchmarking.")] public class EnumToStringTests { static readonly IReadOnlyList Values = Enum.GetValues(typeof(Greek)).Cast().ToArray(); diff --git a/Benchmarks/IsDefinedTests.cs b/Benchmarks/IsDefinedTests.cs index cac172b..8f0755c 100644 --- a/Benchmarks/IsDefinedTests.cs +++ b/Benchmarks/IsDefinedTests.cs @@ -1,9 +1,7 @@ using BenchmarkDotNet.Attributes; -using System.Diagnostics.CodeAnalysis; namespace Open.Text.Benchmarks; -[SuppressMessage("Performance", "CA1822:Mark members as static")] public class IsDefinedTests { static readonly IReadOnlyList Values = Enumerable.Range(-2, 30).ToArray(); diff --git a/Benchmarks/StringConcatTests.cs b/Benchmarks/StringConcatTests.cs index a54a64d..0559a9c 100644 --- a/Benchmarks/StringConcatTests.cs +++ b/Benchmarks/StringConcatTests.cs @@ -1,12 +1,10 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Primitives; using System.Text; -using System.Diagnostics.CodeAnalysis; namespace Open.Text.Benchmarks; [MemoryDiagnoser] -[SuppressMessage("Performance", "CA1822:Mark members as static")] public class StringConcatTests { public static readonly string Phrase diff --git a/Source/EnumValue.cs b/Source/EnumValue.cs index d579153..e4943df 100644 --- a/Source/EnumValue.cs +++ b/Source/EnumValue.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Diagnostics; using System.Linq.Expressions; using static System.Linq.Expressions.Expression; //using static FastExpressionCompiler.LightExpression.Expression; @@ -357,8 +356,8 @@ public static bool IsDefined(T value) /// Returns the from the provided if it maps directly to the underlying value. /// public static bool TryGetValue(T value, out TEnum e) - where T : notnull - => Underlying.Map.TryGetValue(value, out e!); + where T : notnull + => Underlying.Map.TryGetValue(value, out e!); private string GetDebuggerDisplay() { @@ -524,7 +523,7 @@ public static bool TryParse(string value, out TEnum e) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TEnum Parse(StringSegment value) where TEnum : notnull, Enum - => TryParse(value, false, out var e) ? e + => TryParse(value, false, out var e) ? e : throw new ArgumentException(string.Format(NotFoundMessage, value), nameof(value)); /// @@ -540,7 +539,7 @@ public static TEnum Parse(string value, bool ignoreCase) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TEnum Parse(StringSegment value, bool ignoreCase) where TEnum : notnull, Enum - { + { var buffer = value.Buffer ?? throw new ArgumentNullException(nameof(value)); return value.Length == buffer.Length ? Parse(value.Buffer, ignoreCase) @@ -552,13 +551,13 @@ public static TEnum Parse(StringSegment value, bool ignoreCase) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParse(StringSegment value, out TEnum e) where TEnum : notnull, Enum - => TryParse(value, false, out e); + => TryParse(value, false, out e); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParse(string name, bool ignoreCase, out TEnum e) where TEnum : notnull, Enum - => ignoreCase + => ignoreCase ? EnumValue.IgnoreCaseLookup.TryGetValue(name, out e!) : TryParse(name, out e); @@ -567,7 +566,7 @@ public static bool TryParse(string name, bool ignoreCase, out TEnum e) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseIgnoreCase(string name, out TEnum e) where TEnum : notnull, Enum - => EnumValue.IgnoreCaseLookup.TryGetValue(name, out e!); + => EnumValue.IgnoreCaseLookup.TryGetValue(name, out e!); /// /// Converts the string representation of the name of one or more enumerated constants to an equivalent enumerated object. @@ -578,7 +577,7 @@ public static bool TryParseIgnoreCase(string name, out TEnum e) /// true if the value was found; otherwise false. public static bool TryParse(StringSegment name, bool ignoreCase, out TEnum e) where TEnum : notnull, Enum - { + { var len = name.Length; if (len == 0) goto notFound; @@ -617,8 +616,8 @@ public static bool TryParse(StringSegment name, bool ignoreCase, out TEnu [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGetValue(T value, out TEnum e) where TEnum : notnull, Enum - where T : notnull - => EnumValue.TryGetValue(value, out e); + where T : notnull + => EnumValue.TryGetValue(value, out e); /// /// Uses an expression tree to do an fast lookup the name of the enum value. @@ -630,14 +629,14 @@ public static bool TryGetValue(T value, out TEnum e) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetName(this TEnum value) where TEnum : notnull, Enum - => EnumValue.NameLookup(value); + => EnumValue.NameLookup(value); /// /// Retrieves the attributes for a given enum value. /// public static IReadOnlyList GetAttributes(this TEnum value) where TEnum : notnull, Enum - { + { return EnumValue.Attributes.GetOrAdd(value, GetAttributesCore); static IReadOnlyList GetAttributesCore(TEnum value) diff --git a/Source/Extensions.Equals.cs b/Source/Extensions.Equals.cs index a9e6940..4b1ba2b 100644 --- a/Source/Extensions.Equals.cs +++ b/Source/Extensions.Equals.cs @@ -3,12 +3,12 @@ public static partial class TextExtensions { /// - /// Optimized equals for comparing as span vs a string. + /// Compares a sequence of characters with another. /// - /// The source span. - /// The string to compare to. + /// The source sequence. + /// The other to compare to. /// The string comparison type. - /// True if the are contents equal. + /// if the are contents equal; otherwise public static bool Equals(this ReadOnlySpan source, StringSegment other, StringComparison comparisonType) { if (!other.HasValue) return false; @@ -34,13 +34,7 @@ public static bool Equals(this Span source, StringSegment other, StringCom }; } - /// - /// Optimized equals for comparing spans. - /// - /// The source span. - /// The span to compare to. - /// The string comparison type. - /// True if the are contents equal. + /// public static bool Equals(this Span source, ReadOnlySpan other, StringComparison comparisonType) { var len = source.Length; @@ -52,13 +46,7 @@ public static bool Equals(this Span source, ReadOnlySpan other, Stri }; } - /// - /// Optimized equals for comparing as string to a span. - /// - /// The source string. - /// The span to compare to. - /// The string comparison type. - /// True if the are contents equal. + /// public static bool Equals(this string? source, ReadOnlySpan other, StringComparison comparisonType) { if (source is null) return false; @@ -71,7 +59,7 @@ public static bool Equals(this string? source, ReadOnlySpan other, StringC }; } - /// + /// public static bool Equals(this StringSegment source, ReadOnlySpan other, StringComparison comparisonType) { if (!source.HasValue) return false; @@ -85,27 +73,27 @@ public static bool Equals(this StringSegment source, ReadOnlySpan other, S } /// Use for varying case sensitivity. - /// + /// public static bool SequenceEqual(this ReadOnlySpan source, StringSegment other) => Equals(source, other, StringComparison.Ordinal); /// Use for varying case sensitivity. - /// + /// public static bool SequenceEqual(this Span source, StringSegment other) => Equals(source, other, StringComparison.Ordinal); /// Use for varying case sensitivity. - /// + /// public static bool SequenceEqual(this Span source, Span other) => Equals(source, other, StringComparison.Ordinal); /// Use for varying case sensitivity. - /// + /// public static bool SequenceEqual(this string? source, ReadOnlySpan other) => Equals(source, other, StringComparison.Ordinal); /// Use for varying case sensitivity. - /// + /// public static bool SequenceEqual(this StringSegment source, ReadOnlySpan other) => Equals(source, other, StringComparison.Ordinal); @@ -183,13 +171,14 @@ public static bool TrimmedEquals(this string? source, ReadOnlySpan other, && source.AsSpan().Trim(trimChars).Equals(other, comparisonType); /// - /// Optimized equals for comparing a trimmed string with another string. + /// Compares a sequence of characters with leading and trailing whitespace removed with another. /// - /// The source string to virtually trim. - /// The string to compare to. + /// Only "trims" the source and not the other used to compare agains. + /// The source sequence. + /// The other to compare to. /// The characters to trim. /// The string comparison type. - /// True if the are contents equal. + /// public static bool TrimmedEquals(this StringSegment source, StringSegment other, ReadOnlySpan trimChars, StringComparison comparisonType = StringComparison.Ordinal) => source.HasValue ? other.HasValue diff --git a/Source/Extensions.IndexOf.cs b/Source/Extensions.IndexOf.cs index 099ce69..9a21854 100644 --- a/Source/Extensions.IndexOf.cs +++ b/Source/Extensions.IndexOf.cs @@ -39,7 +39,7 @@ public static int IndexOf(this ReadOnlySpan source, char search, StringCom return -1; } - /// + /// public static int IndexOf(this ReadOnlySpan source, char search, int startIndex, StringComparison comparisonType) { if (startIndex >= source.Length) diff --git a/Source/Extensions.Split.cs b/Source/Extensions.Split.cs index 4c9cf0b..d3bcab2 100644 --- a/Source/Extensions.Split.cs +++ b/Source/Extensions.Split.cs @@ -4,6 +4,7 @@ internal static class SingleEmpty { public static readonly IReadOnlyList Instance = Array.AsReadOnly(new[] { string.Empty }); } + public static partial class TextExtensions { static ReadOnlySpan FirstSplitSpan(StringSegment source, int start, int i, int n, out int nextIndex) diff --git a/Source/Extensions.StringSegment.cs b/Source/Extensions.StringSegment.cs index ef88063..5a5a605 100644 --- a/Source/Extensions.StringSegment.cs +++ b/Source/Extensions.StringSegment.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Text; +using System.Text; namespace Open.Text; diff --git a/Source/Extensions._.cs b/Source/Extensions._.cs index 05abea2..fd63aec 100644 --- a/Source/Extensions._.cs +++ b/Source/Extensions._.cs @@ -8,8 +8,10 @@ public static partial class TextExtensions { private const uint BYTE_RED = 1024; - private static IEnumerable ByteLabels { - get { + private static IEnumerable ByteLabels + { + get + { yield return "KB"; yield return "MB"; yield return "GB"; @@ -138,34 +140,6 @@ public static bool IsAlphaNumeric(this string source, bool trim = false) => !string.IsNullOrWhiteSpace(source) && (trim ? RegexPatterns.ValidAlphaNumericOnlyUntrimmedPattern : RegexPatterns.ValidAlphaNumericOnlyPattern).IsMatch(source); - /// - /// Returns the available matches as StringSegments. - /// - /// The pattern to search with. - /// The string to search. - /// An enumerable containing the found segments. - /// If the pattern or input is null. - public static IEnumerable AsSegments(this Regex pattern, string input) - { - return pattern is null - ? throw new ArgumentNullException(nameof(pattern)) - : input is null - ? throw new ArgumentNullException(nameof(input)) - : input.Length == 0 - ? Enumerable.Empty() - : AsSegmentsCore(pattern, input); - - static IEnumerable AsSegmentsCore(Regex pattern, string input) - { - var match = pattern.Match(input); - while (match.Success) - { - yield return new(input, match.Index, match.Length); - match = match.NextMatch(); - } - } - } - #region Numeric string formatting. /// /// Shortcut for formating Nullable<T>. diff --git a/Source/RegexExtensions.cs b/Source/RegexExtensions.cs index 00daaf5..6579a38 100644 --- a/Source/RegexExtensions.cs +++ b/Source/RegexExtensions.cs @@ -8,7 +8,7 @@ public static class RegexExtensions private static Func GetOriginalTextDelegate() { var textProp = typeof(Capture).GetProperty("Text", BindingFlags.Instance | BindingFlags.NonPublic); - if(textProp is not null) + if (textProp is not null) { var method = textProp.GetGetMethod(nonPublic: true) ?? throw new InvalidOperationException("Could not find the Text property getter."); @@ -71,4 +71,32 @@ public static ReadOnlySpan GetValueSpan(this GroupCollection groups, strin ? group.AsSpan() : []; } + + /// + /// Returns the available matches as StringSegments. + /// + /// The pattern to search with. + /// The string to search. + /// An enumerable containing the found segments. + /// If the pattern or input is null. + public static IEnumerable AsSegments(this Regex pattern, string input) + { + return pattern is null + ? throw new ArgumentNullException(nameof(pattern)) + : input is null + ? throw new ArgumentNullException(nameof(input)) + : input.Length == 0 + ? Enumerable.Empty() + : AsSegmentsCore(pattern, input); + + static IEnumerable AsSegmentsCore(Regex pattern, string input) + { + var match = pattern.Match(input); + while (match.Success) + { + yield return new(input, match.Index, match.Length); + match = match.NextMatch(); + } + } + } } diff --git a/Source/StringBuilderExtensions.cs b/Source/StringBuilderExtensions.cs index e211a0c..eab1010 100644 --- a/Source/StringBuilderExtensions.cs +++ b/Source/StringBuilderExtensions.cs @@ -434,22 +434,22 @@ public static StringBuilder Append(this StringBuilder target, ReadOnlySpan /// /// Appends the characters from another this instance. /// - public static StringBuilder Append(this StringBuilder target, StringBuilder value) - { - if (target is null) throw new ArgumentNullException(nameof(target)); + public static StringBuilder Append(this StringBuilder target, StringBuilder value) + { + if (target is null) throw new ArgumentNullException(nameof(target)); if (value is null) return target; var len = value.Length; - for(var i = 0;i - /// Trims whitespace from the end of the . - /// - /// If is null. - public static StringBuilder TrimEnd(this StringBuilder sb) + /// + /// Trims whitespace from the end of the . + /// + /// If is null. + public static StringBuilder TrimEnd(this StringBuilder sb) { if (sb is null) throw new ArgumentNullException(nameof(sb)); Contract.EndContractBlock(); diff --git a/Source/StringBuilderHelper.cs b/Source/StringBuilderHelper.cs index a017159..a47e363 100644 --- a/Source/StringBuilderHelper.cs +++ b/Source/StringBuilderHelper.cs @@ -8,10 +8,10 @@ namespace Open.Text; [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "")] public readonly record struct StringBuilderHelper { - /// - /// The underlying . - /// - public StringBuilder Builder { get; } + /// + /// The underlying . + /// + public StringBuilder Builder { get; } /// public override string ToString() @@ -33,16 +33,16 @@ public StringBuilderHelper() /// Constructs a new with a of the . /// public StringBuilderHelper(int initialCapacity) - : this(new StringBuilder(initialCapacity)) { } + : this(new StringBuilder(initialCapacity)) { } - /// - /// Appends the characters to the underlying . - /// - public static StringBuilderHelper Add(StringBuilderHelper helper, string characters) - { + /// + /// Appends the characters to the underlying . + /// + public static StringBuilderHelper Add(StringBuilderHelper helper, string characters) + { helper.Builder.Append(characters); - return helper; - } + return helper; + } /// /// Appends the to the underlying . @@ -52,114 +52,114 @@ public static StringBuilderHelper Add(StringBuilderHelper helper, string charact /// public static StringBuilderHelper operator +(StringBuilderHelper helper, string characters) - => Add(helper, characters); + => Add(helper, characters); - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, StringSegment characters) - { - helper.Builder.AppendSegment(characters); - return helper; + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, StringSegment characters) + { + helper.Builder.AppendSegment(characters); + return helper; } - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, ReadOnlySpan characters) - { - helper.Builder.Append(characters); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, Span characters) - { - helper.Builder.Append(characters); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, char[] characters) - { - if (characters is null) return helper; - helper.Builder.Append(characters.AsSpan()); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, ReadOnlyMemory characters) - { - helper.Builder.Append(characters.Span); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, Memory characters) - { - helper.Builder.Append(characters.Span); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, ArraySegment characters) - { - helper.Builder.Append(characters.AsSpan()); - return helper; - } - - /// - public static StringBuilderHelper operator +(StringBuilderHelper helper, IEnumerable characters) - { - if (characters is null) return helper; - var sb = helper.Builder; - foreach(var c in characters) - sb.Append(c); - return helper; - } - - /// - /// Creates a new instance beginning with the specified . - /// - internal static StringBuilderHelper NewFrom(T sequence) - => throw new NotImplementedException(); + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, ReadOnlySpan characters) + { + helper.Builder.Append(characters); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, Span characters) + { + helper.Builder.Append(characters); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, char[] characters) + { + if (characters is null) return helper; + helper.Builder.Append(characters.AsSpan()); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, ReadOnlyMemory characters) + { + helper.Builder.Append(characters.Span); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, Memory characters) + { + helper.Builder.Append(characters.Span); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, ArraySegment characters) + { + helper.Builder.Append(characters.AsSpan()); + return helper; + } + + /// + public static StringBuilderHelper operator +(StringBuilderHelper helper, IEnumerable characters) + { + if (characters is null) return helper; + var sb = helper.Builder; + foreach (var c in characters) + sb.Append(c); + return helper; + } + + /// + /// Creates a new instance beginning with the specified . + /// + internal static StringBuilderHelper NewFrom(T sequence) + => throw new NotImplementedException(); /// [ExcludeFromCodeCoverage] public static implicit operator StringBuilderHelper(string sequence) - => string.IsNullOrEmpty(sequence) ? new() : new(new StringBuilder(sequence)); + => string.IsNullOrEmpty(sequence) ? new() : new(new StringBuilder(sequence)); - /// - public static implicit operator StringBuilderHelper(char value) - => new(new StringBuilder(1).Append(value)); + /// + public static implicit operator StringBuilderHelper(char value) + => new(new StringBuilder(1).Append(value)); - /// - public static implicit operator StringBuilderHelper(StringSegment value) - => new(new StringBuilder(value.Length).AppendSegment(value)); + /// + public static implicit operator StringBuilderHelper(StringSegment value) + => new(new StringBuilder(value.Length).AppendSegment(value)); - /// - public static implicit operator StringBuilderHelper(ReadOnlySpan value) - => new(new StringBuilder(value.Length).Append(value)); + /// + public static implicit operator StringBuilderHelper(ReadOnlySpan value) + => new(new StringBuilder(value.Length).Append(value)); - /// - public static implicit operator StringBuilderHelper(ReadOnlyMemory value) - => new(new StringBuilder(value.Length).Append(value.Span)); + /// + public static implicit operator StringBuilderHelper(ReadOnlyMemory value) + => new(new StringBuilder(value.Length).Append(value.Span)); - /// - public static implicit operator StringBuilderHelper(Memory value) - => new(new StringBuilder(value.Length).Append(value.Span)); + /// + public static implicit operator StringBuilderHelper(Memory value) + => new(new StringBuilder(value.Length).Append(value.Span)); - /// - public static implicit operator StringBuilderHelper(ArraySegment value) - => new(new StringBuilder(value.Count).Append(value.AsSpan())); + /// + public static implicit operator StringBuilderHelper(ArraySegment value) + => new(new StringBuilder(value.Count).Append(value.AsSpan())); - /// - public static implicit operator StringBuilderHelper(char[] value) - => value is null ? new() : new(new StringBuilder(value.Length).Append(value.AsSpan())); + /// + public static implicit operator StringBuilderHelper(char[] value) + => value is null ? new() : new(new StringBuilder(value.Length).Append(value.AsSpan())); - /// - public static implicit operator StringBuilderHelper(StringBuilder value) - => value is null ? new() : new(new StringBuilder(value.Length).Append(value)); + /// + public static implicit operator StringBuilderHelper(StringBuilder value) + => value is null ? new() : new(new StringBuilder(value.Length).Append(value)); - /// - /// Converts the instance to a string. - /// - public static implicit operator string(StringBuilderHelper value) - => value.Builder.ToString(); + /// + /// Converts the instance to a string. + /// + public static implicit operator string(StringBuilderHelper value) + => value.Builder.ToString(); } diff --git a/Source/StringSegmentSearch.cs b/Source/StringSegmentSearch.cs index b9bf79a..895e272 100644 --- a/Source/StringSegmentSearch.cs +++ b/Source/StringSegmentSearch.cs @@ -171,7 +171,7 @@ public static StringSegmentSearch Find( public static StringSegmentCapture First( this StringSegmentSearch search) { - if(search.Source.Length == 0 || search.Sequence.Length == 0) + if (search.Source.Length == 0 || search.Sequence.Length == 0) return default; var i = search.RightToLeft @@ -180,7 +180,7 @@ public static StringSegmentCapture First( return new(search, i == -1 ? default - : new(search.Source, i, search.Sequence.Length) ); + : new(search.Source, i, search.Sequence.Length)); } /// diff --git a/Source/StringSubsegment.cs b/Source/StringSubsegment.cs index 1eb7c97..bc2c68c 100644 --- a/Source/StringSubsegment.cs +++ b/Source/StringSubsegment.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace Open.Text; +namespace Open.Text; /// /// A struct for representing a subsegment of a . diff --git a/Source/_Global.cs b/Source/_Global.cs index d72b8a2..1b6c93f 100644 --- a/Source/_Global.cs +++ b/Source/_Global.cs @@ -1,8 +1,8 @@ global using Microsoft.Extensions.Primitives; -global using System.Runtime.CompilerServices; global using System.Diagnostics; global using System.Diagnostics.CodeAnalysis; global using System.Diagnostics.Contracts; global using System.Globalization; global using System.Reflection; +global using System.Runtime.CompilerServices; global using System.Text.RegularExpressions; \ No newline at end of file diff --git a/Tests/IndexOfTests.cs b/Tests/IndexOfTests.cs index 1c2cc70..5756c77 100644 --- a/Tests/IndexOfTests.cs +++ b/Tests/IndexOfTests.cs @@ -134,7 +134,7 @@ public static void LastIndexOf(string source, string value, StringComparison com // We will retain modern behavior as it makes slighty more sense. var expected = source.LastIndexOf(value, comparison); #if NET472 - if (value == "") + if (value.Length == 0) expected++; #endif source.LastIndexOf(value.AsSpan(), comparison).Should().Be(expected); diff --git a/Tests/SplitTests.cs b/Tests/SplitTests.cs index b6f7624..f9e90a2 100644 --- a/Tests/SplitTests.cs +++ b/Tests/SplitTests.cs @@ -1,5 +1,5 @@ -using System.Text.RegularExpressions; -using System.ComponentModel; +using System.ComponentModel; +using System.Text.RegularExpressions; namespace Open.Text.Tests; diff --git a/Tests/StringBuilderTests.cs b/Tests/StringBuilderTests.cs index 969bf86..715684c 100644 --- a/Tests/StringBuilderTests.cs +++ b/Tests/StringBuilderTests.cs @@ -81,7 +81,7 @@ public static void ToStringBuilderSeparatedChar(string? source, char separator) var span = source.AsSpan(); var a = source.AsSpan().ToArray(); - var joined = string.Join(""+separator, a); + var joined = string.Join("" + separator, a); var list = a.ToList(); list.Insert(0, 'X'); var xValue = string.Join("" + separator, list); diff --git a/Tests/_Global.cs b/Tests/_Global.cs index 7605611..5cc431a 100644 --- a/Tests/_Global.cs +++ b/Tests/_Global.cs @@ -1,3 +1,3 @@ global using FluentAssertions; +global using System.Diagnostics.CodeAnalysis; global using Xunit; -global using System.Diagnostics.CodeAnalysis; \ No newline at end of file