Add Regex span startat overloads (#71228)
authorStephen Toub <stoub@microsoft.com>
Mon, 27 Jun 2022 16:28:49 +0000 (12:28 -0400)
committerGitHub <noreply@github.com>
Mon, 27 Jun 2022 16:28:49 +0000 (12:28 -0400)
Also fixes the new Count methods to behave correctly for RightToLeft.

src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Count.cs
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.EnumerateMatches.cs
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.Match.cs
src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Count.Tests.cs
src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.EnumerateMatches.Tests.cs
src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs

index 73c1a7a..3b0b589 100644 (file)
@@ -164,6 +164,7 @@ namespace System.Text.RegularExpressions
         public static void CompileToAssembly(System.Text.RegularExpressions.RegexCompilationInfo[] regexinfos, System.Reflection.AssemblyName assemblyname, System.Reflection.Emit.CustomAttributeBuilder[]? attributes, string? resourceFile) { }
         public int Count(string input) { throw null; }
         public int Count(System.ReadOnlySpan<char> input) { throw null; }
+        public int Count(System.ReadOnlySpan<char> input, int startat) { throw null; }
         public static int Count(string input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] string pattern) { throw null; }
         public static int Count(string input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex, "options")] string pattern, System.Text.RegularExpressions.RegexOptions options) { throw null; }
         public static int Count(string input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex, "options")] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; }
@@ -172,6 +173,7 @@ namespace System.Text.RegularExpressions
         public static int Count(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex, "options")] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; }
         public static string Escape(string str) { throw null; }
         public System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan<char> input) { throw null; }
+        public System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan<char> input, int startat) { throw null; }
         public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex")] string pattern) { throw null; }
         public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", new object[]{ "options"})] string pattern, System.Text.RegularExpressions.RegexOptions options) { throw null; }
         public static System.Text.RegularExpressions.Regex.ValueMatchEnumerator EnumerateMatches(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", new object[]{ "options"})] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; }
@@ -181,6 +183,7 @@ namespace System.Text.RegularExpressions
         public int GroupNumberFromName(string name) { throw null; }
         protected void InitializeReferences() { }
         public bool IsMatch(System.ReadOnlySpan<char> input) { throw null; }
+        public bool IsMatch(System.ReadOnlySpan<char> input, int startat) { throw null; }
         public static bool IsMatch(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex")] string pattern) { throw null; }
         public static bool IsMatch(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", "options")] string pattern, System.Text.RegularExpressions.RegexOptions options) { throw null; }
         public static bool IsMatch(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex", "options")] string pattern, System.Text.RegularExpressions.RegexOptions options, System.TimeSpan matchTimeout) { throw null; }
index 52f8130..025164e 100644 (file)
@@ -20,7 +20,7 @@ namespace System.Text.RegularExpressions
 
             int count = 0;
 
-            RunAllMatchesWithCallback(input, 0, ref count, static (ref int count, Match match) =>
+            RunAllMatchesWithCallback(input, RightToLeft ? input.Length : 0, ref count, static (ref int count, Match match) =>
             {
                 count++;
                 return true;
@@ -34,11 +34,20 @@ namespace System.Text.RegularExpressions
         /// </summary>
         /// <param name="input">The span to search for a match.</param>
         /// <returns>The number of matches.</returns>
-        public int Count(ReadOnlySpan<char> input)
+        public int Count(ReadOnlySpan<char> input) =>
+            Count(input, RightToLeft ? input.Length : 0);
+
+        /// <summary>
+        /// Searches an input span for all occurrences of a regular expression and returns the number of matches.
+        /// </summary>
+        /// <param name="input">The span to search for a match.</param>
+        /// <param name="startat">The zero-based character position at which to start the search.</param>
+        /// <returns>The number of matches.</returns>
+        public int Count(ReadOnlySpan<char> input, int startat)
         {
             int count = 0;
 
-            RunAllMatchesWithCallback(input, 0, ref count, static (ref int count, Match match) =>
+            RunAllMatchesWithCallback(input, startat, ref count, static (ref int count, Match match) =>
             {
                 count++;
                 return true;
index a2e16b8..c9873c6 100644 (file)
@@ -79,6 +79,21 @@ namespace System.Text.RegularExpressions
             new ValueMatchEnumerator(this, input, RightToLeft ? input.Length : 0);
 
         /// <summary>
+        /// Searches an input span for all occurrences of a regular expression and returns a <see cref="ValueMatchEnumerator"/> to iterate over the matches.
+        /// </summary>
+        /// <remarks>
+        /// Each match won't actually happen until <see cref="ValueMatchEnumerator.MoveNext"/> is invoked on the enumerator, with one match being performed per <see cref="ValueMatchEnumerator.MoveNext"/> call.
+        /// Since the evaluation of the match happens lazily, any changes to the passed in input in between calls to <see cref="ValueMatchEnumerator.MoveNext"/> will affect the match results.
+        /// The enumerator returned by this method, as well as the structs returned by the enumerator that wrap each match found in the input are ref structs which
+        /// make this method be amortized allocation free.
+        /// </remarks>
+        /// <param name="input">The span to search for a match.</param>
+        /// <param name="startat">The zero-based character position at which to start the search.</param>
+        /// <returns>A <see cref="ValueMatchEnumerator"/> to iterate over the matches.</returns>
+        public ValueMatchEnumerator EnumerateMatches(ReadOnlySpan<char> input, int startat) =>
+            new ValueMatchEnumerator(this, input, startat);
+
+        /// <summary>
         /// Represents an enumerator containing the set of successful matches found by iteratively applying a regular expression pattern to the input span.
         /// </summary>
         /// <remarks>
index 96f57ac..04e69c6 100644 (file)
@@ -81,15 +81,6 @@ namespace System.Text.RegularExpressions
         }
 
         /// <summary>
-        /// Indicates whether the regular expression specified in the Regex constructor finds a match in a specified input span.
-        /// </summary>
-        /// <param name="input">The span to search for a match.</param>
-        /// <returns><see langword="true"/> if the regular expression finds a match; otherwise, <see langword="false"/>.</returns>
-        /// <exception cref="RegexMatchTimeoutException">A time-out occurred.</exception>
-        public bool IsMatch(ReadOnlySpan<char> input) =>
-            RunSingleMatch(RegexRunnerMode.ExistenceRequired, -1, input, RightToLeft ? input.Length : 0).Success;
-
-        /// <summary>
         /// Searches the input string for one or more matches using the previous pattern and options,
         /// with a new starting position.
         /// </summary>
@@ -104,6 +95,25 @@ namespace System.Text.RegularExpressions
         }
 
         /// <summary>
+        /// Indicates whether the regular expression specified in the Regex constructor finds a match in a specified input span.
+        /// </summary>
+        /// <param name="input">The span to search for a match.</param>
+        /// <returns><see langword="true"/> if the regular expression finds a match; otherwise, <see langword="false"/>.</returns>
+        /// <exception cref="RegexMatchTimeoutException">A time-out occurred.</exception>
+        public bool IsMatch(ReadOnlySpan<char> input) =>
+            IsMatch(input, RightToLeft ? input.Length : 0);
+
+        /// <summary>
+        /// Indicates whether the regular expression specified in the Regex constructor finds a match in a specified input span.
+        /// </summary>
+        /// <param name="input">The span to search for a match.</param>
+        /// <param name="startat">The zero-based character position at which to start the search.</param>
+        /// <returns><see langword="true"/> if the regular expression finds a match; otherwise, <see langword="false"/>.</returns>
+        /// <exception cref="RegexMatchTimeoutException">A time-out occurred.</exception>
+        public bool IsMatch(ReadOnlySpan<char> input, int startat) =>
+            RunSingleMatch(RegexRunnerMode.ExistenceRequired, -1, input, startat).Success;
+
+        /// <summary>
         /// Searches the input string for one or more occurrences of the text
         /// supplied in the pattern parameter.
         /// </summary>
index c7aab09..bef3bb1 100644 (file)
@@ -12,9 +12,19 @@ namespace System.Text.RegularExpressions.Tests
     {
         [Theory]
         [MemberData(nameof(Count_ReturnsExpectedCount_TestData))]
-        public async Task Count_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, RegexOptions options, int expectedCount)
+        public async Task Count_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, int startat, RegexOptions options, int expectedCount)
         {
             Regex r = await RegexHelpers.GetRegexAsync(engine, pattern, options);
+
+            Assert.Equal(expectedCount, r.Count(input.AsSpan(), startat));
+            Assert.Equal(r.Count(input.AsSpan(), startat), r.Matches(input, startat).Count);
+
+            bool isDefaultStartAt = startat == ((options & RegexOptions.RightToLeft) != 0 ? input.Length : 0);
+            if (!isDefaultStartAt)
+            {
+                return;
+            }
+
             Assert.Equal(expectedCount, r.Count(input));
             Assert.Equal(expectedCount, r.Count(input.AsSpan()));
             Assert.Equal(r.Count(input), r.Matches(input).Count);
@@ -44,22 +54,41 @@ namespace System.Text.RegularExpressions.Tests
         {
             foreach (RegexEngine engine in RegexHelpers.AvailableEngines)
             {
-                yield return new object[] { engine, @"", "", RegexOptions.None, 1 };
-                yield return new object[] { engine, @"", "a", RegexOptions.None, 2 };
-                yield return new object[] { engine, @"", "ab", RegexOptions.None, 3 };
-
-                yield return new object[] { engine, @"\w", "", RegexOptions.None, 0 };
-                yield return new object[] { engine, @"\w", "a", RegexOptions.None, 1 };
-                yield return new object[] { engine, @"\w", "ab", RegexOptions.None, 2 };
-
-                yield return new object[] { engine, @"\b\w+\b", "abc def ghi jkl", RegexOptions.None, 4 };
-
-                yield return new object[] { engine, @"A", "", RegexOptions.IgnoreCase, 0 };
-                yield return new object[] { engine, @"A", "a", RegexOptions.IgnoreCase, 1 };
-                yield return new object[] { engine, @"A", "aAaA", RegexOptions.IgnoreCase, 4 };
-
-                yield return new object[] { engine, @".", "\n\n\n", RegexOptions.None, 0 };
-                yield return new object[] { engine, @".", "\n\n\n", RegexOptions.Singleline, 3 };
+                yield return new object[] { engine, @"", "", 0, RegexOptions.None, 1 };
+                yield return new object[] { engine, @"", "a", 0, RegexOptions.None, 2 };
+                yield return new object[] { engine, @"", "ab", 0, RegexOptions.None, 3 };
+                yield return new object[] { engine, @"", "ab", 1, RegexOptions.None, 2 };
+
+                yield return new object[] { engine, @"\w", "", 0, RegexOptions.None, 0 };
+                yield return new object[] { engine, @"\w", "a", 0, RegexOptions.None, 1 };
+                yield return new object[] { engine, @"\w", "ab", 0, RegexOptions.None, 2 };
+                yield return new object[] { engine, @"\w", "ab", 1, RegexOptions.None, 1 };
+                yield return new object[] { engine, @"\w", "ab", 2, RegexOptions.None, 0 };
+
+                yield return new object[] { engine, @"\b\w+\b", "abc def ghi jkl", 0, RegexOptions.None, 4 };
+                yield return new object[] { engine, @"\b\w+\b", "abc def ghi jkl", 7, RegexOptions.None, 2 };
+
+                yield return new object[] { engine, @"A", "", 0, RegexOptions.IgnoreCase, 0 };
+                yield return new object[] { engine, @"A", "a", 0, RegexOptions.IgnoreCase, 1 };
+                yield return new object[] { engine, @"A", "aAaA", 0, RegexOptions.IgnoreCase, 4 };
+
+                yield return new object[] { engine, @".", "\n\n\n", 0, RegexOptions.None, 0 };
+                yield return new object[] { engine, @".", "\n\n\n", 0, RegexOptions.Singleline, 3 };
+
+                if (!RegexHelpers.IsNonBacktracking(engine))
+                {
+                    // Lookbehinds
+                    yield return new object[] { engine, @"(?<=abc)\w", "abcxabcy", 7, RegexOptions.None, 1 };
+
+                    // Starting anchors
+                    yield return new object[] { engine, @"\Gdef", "abcdef", 0, RegexOptions.None, 0 };
+                    yield return new object[] { engine, @"\Gdef", "abcdef", 3, RegexOptions.None, 1 };
+
+                    // RightToLeft
+                    yield return new object[] { engine, @"\b\w+\b", "abc def ghi jkl", 15, RegexOptions.RightToLeft, 4 };
+                    yield return new object[] { RegexEngine.Interpreter, @"(?<=abc)\w", "abcxabcy", 8, RegexOptions.RightToLeft, 2 };
+                    yield return new object[] { engine, @"(?<=abc)\w", "abcxabcy", 7, RegexOptions.RightToLeft, 1 };
+                }
             }
         }
 
index 6defa59..915f072 100644 (file)
@@ -151,16 +151,25 @@ namespace System.Text.RegularExpressions.Tests
     {
         [Theory]
         [MemberData(nameof(Count_ReturnsExpectedCount_TestData))]
-        public void EnumerateMatches_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, RegexOptions options, int expectedCount)
+        public void EnumerateMatches_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, int startat, RegexOptions options, int expectedCount)
         {
             Regex r = RegexHelpers.GetRegexAsync(engine, pattern, options).GetAwaiter().GetResult();
-            int count = 0;
-            foreach (ValueMatch _ in r.EnumerateMatches(input))
+
+            int count;
+
+            count = 0;
+            foreach (ValueMatch _ in r.EnumerateMatches(input, startat))
             {
                 count++;
             }
             Assert.Equal(expectedCount, count);
 
+            bool isDefaultStartAt = startat == ((options & RegexOptions.RightToLeft) != 0 ? input.Length : 0);
+            if (!isDefaultStartAt)
+            {
+                return;
+            }
+
             if (options == RegexOptions.None && engine == RegexEngine.Interpreter)
             {
                 count = 0;
index 0ebe64c..cdd0285 100644 (file)
@@ -1728,6 +1728,9 @@ namespace System.Text.RegularExpressions.Tests
             Regex r = await RegexHelpers.GetRegexAsync(engine, pattern, options);
 
             Assert.Equal(expectedSuccessStartAt, r.IsMatch(input, startat));
+#if NET7_0_OR_GREATER
+            Assert.Equal(expectedSuccessStartAt, r.IsMatch(input.AsSpan(), startat));
+#endif
 
             // Normal matching, but any match before startat is ignored.
             Match match = r.Match(input, startat);