Skip to content

Commit 0bf7e14

Browse files
authored
Add Regex.Count string overloads (#64289)
1 parent bdc0d1a commit 0bf7e14

File tree

5 files changed

+190
-0
lines changed

5 files changed

+190
-0
lines changed

src/libraries/System.Text.RegularExpressions/ref/System.Text.RegularExpressions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ public static void CompileToAssembly(System.Text.RegularExpressions.RegexCompila
161161
public static void CompileToAssembly(System.Text.RegularExpressions.RegexCompilationInfo[] regexinfos, System.Reflection.AssemblyName assemblyname, System.Reflection.Emit.CustomAttributeBuilder[]? attributes) { }
162162
[System.ObsoleteAttribute("Regex.CompileToAssembly is obsolete and not supported. Use the RegexGeneratorAttribute with the regular expression source generator instead.", DiagnosticId = "SYSLIB0036", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")]
163163
public static void CompileToAssembly(System.Text.RegularExpressions.RegexCompilationInfo[] regexinfos, System.Reflection.AssemblyName assemblyname, System.Reflection.Emit.CustomAttributeBuilder[]? attributes, string? resourceFile) { }
164+
public int Count(string input) { throw null; }
165+
public static int Count(string input, [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] string pattern) { throw null; }
166+
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; }
167+
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; }
164168
public static string Escape(string str) { throw null; }
165169
public string[] GetGroupNames() { throw null; }
166170
public int[] GetGroupNumbers() { throw null; }

src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Compile Include="System\Text\RegularExpressions\MatchCollection.cs" />
1919
<Compile Include="System\Text\RegularExpressions\Regex.cs" />
2020
<Compile Include="System\Text\RegularExpressions\Regex.Cache.cs" />
21+
<Compile Include="System\Text\RegularExpressions\Regex.Count.cs" />
2122
<Compile Include="System\Text\RegularExpressions\Regex.Debug.cs" />
2223
<Compile Include="System\Text\RegularExpressions\Regex.Match.cs" />
2324
<Compile Include="System\Text\RegularExpressions\Regex.Replace.cs" />
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace System.Text.RegularExpressions
7+
{
8+
public partial class Regex
9+
{
10+
/// <summary>Searches an input string for all occurrences of a regular expression and returns the number of matches.</summary>
11+
/// <param name="input">The string to search for a match.</param>
12+
/// <returns>The number of matches.</returns>
13+
/// <exception cref="ArgumentNullException"><paramref name="input"/> is null.</exception>
14+
public int Count(string input)
15+
{
16+
if (input is null)
17+
{
18+
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.input);
19+
}
20+
21+
int count = 0;
22+
23+
Run(input, 0, ref count, static (ref int count, Match match) =>
24+
{
25+
count++;
26+
return true;
27+
}, reuseMatchObject: true);
28+
29+
return count;
30+
}
31+
32+
/// <summary>Searches an input string for all occurrences of a regular expression and returns the number of matches.</summary>
33+
/// <param name="input">The string to search for a match.</param>
34+
/// <param name="pattern">The regular expression pattern to match.</param>
35+
/// <returns>The number of matches.</returns>
36+
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="pattern"/> is null.</exception>
37+
/// <exception cref="RegexParseException">A regular expression parsing error occurred.</exception>
38+
public static int Count(string input, [StringSyntax(StringSyntaxAttribute.Regex)] string pattern) =>
39+
RegexCache.GetOrAdd(pattern).Count(input);
40+
41+
/// <summary>Searches an input string for all occurrences of a regular expression and returns the number of matches.</summary>
42+
/// <param name="input">The string to search for a match.</param>
43+
/// <param name="pattern">The regular expression pattern to match.</param>
44+
/// <param name="options">A bitwise combination of the enumeration values that specify options for matching.</param>
45+
/// <returns>The number of matches.</returns>
46+
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="pattern"/> is null.</exception>
47+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="options"/> is not a valid bitwise combination of RegexOptions values.</exception>
48+
/// <exception cref="RegexParseException">A regular expression parsing error occurred.</exception>
49+
public static int Count(string input, [StringSyntax(StringSyntaxAttribute.Regex, "options")] string pattern, RegexOptions options) =>
50+
RegexCache.GetOrAdd(pattern, options, s_defaultMatchTimeout).Count(input);
51+
52+
/// <summary>Searches an input string for all occurrences of a regular expression and returns the number of matches.</summary>
53+
/// <param name="input">The string to search for a match.</param>
54+
/// <param name="pattern">The regular expression pattern to match.</param>
55+
/// <param name="options">A bitwise combination of the enumeration values that specify options for matching.</param>
56+
/// <param name="matchTimeout">A time-out interval, or <see cref="InfiniteMatchTimeout"/> to indicate that the method should not time out.</param>
57+
/// <returns>The number of matches.</returns>
58+
/// <exception cref="ArgumentNullException"><paramref name="input"/> or <paramref name="pattern"/> is null.</exception>
59+
/// <exception cref="ArgumentOutOfRangeException"><paramref name="options"/> is not a valid bitwise combination of RegexOptions values, or <paramref name="matchTimeout"/> is negative, zero, or greater than approximately 24 days.</exception>
60+
/// <exception cref="RegexParseException">A regular expression parsing error occurred.</exception>
61+
public static int Count(string input, [StringSyntax(StringSyntaxAttribute.Regex, "options")] string pattern, RegexOptions options, TimeSpan matchTimeout) =>
62+
RegexCache.GetOrAdd(pattern, options, matchTimeout).Count(input);
63+
}
64+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
9+
namespace System.Text.RegularExpressions.Tests
10+
{
11+
public class RegexCountTests
12+
{
13+
[Theory]
14+
[MemberData(nameof(Count_ReturnsExpectedCount_TestData))]
15+
public async Task Count_ReturnsExpectedCount(RegexEngine engine, string pattern, string input, RegexOptions options, int expectedCount)
16+
{
17+
Regex r = await RegexHelpers.GetRegexAsync(engine, pattern, options);
18+
Assert.Equal(expectedCount, r.Count(input));
19+
Assert.Equal(r.Count(input), r.Matches(input).Count);
20+
21+
if (options == RegexOptions.None && engine == RegexEngine.Interpreter)
22+
{
23+
Assert.Equal(expectedCount, Regex.Count(input, pattern));
24+
}
25+
26+
switch (engine)
27+
{
28+
case RegexEngine.Interpreter:
29+
case RegexEngine.Compiled:
30+
case RegexEngine.NonBacktracking:
31+
RegexOptions engineOptions = RegexHelpers.OptionsFromEngine(engine);
32+
Assert.Equal(expectedCount, Regex.Count(input, pattern, options | engineOptions));
33+
Assert.Equal(expectedCount, Regex.Count(input, pattern, options | engineOptions, Regex.InfiniteMatchTimeout));
34+
break;
35+
}
36+
}
37+
38+
public static IEnumerable<object[]> Count_ReturnsExpectedCount_TestData()
39+
{
40+
foreach (RegexEngine engine in RegexHelpers.AvailableEngines)
41+
{
42+
yield return new object[] { engine, @"", "", RegexOptions.None, 1 };
43+
yield return new object[] { engine, @"", "a", RegexOptions.None, 2 };
44+
yield return new object[] { engine, @"", "ab", RegexOptions.None, 3 };
45+
46+
yield return new object[] { engine, @"\w", "", RegexOptions.None, 0 };
47+
yield return new object[] { engine, @"\w", "a", RegexOptions.None, 1 };
48+
yield return new object[] { engine, @"\w", "ab", RegexOptions.None, 2 };
49+
50+
yield return new object[] { engine, @"\b\w+\b", "abc def ghi jkl", RegexOptions.None, 4 };
51+
52+
yield return new object[] { engine, @"A", "", RegexOptions.IgnoreCase, 0 };
53+
yield return new object[] { engine, @"A", "a", RegexOptions.IgnoreCase, 1 };
54+
yield return new object[] { engine, @"A", "aAaA", RegexOptions.IgnoreCase, 4 };
55+
56+
yield return new object[] { engine, @".", "\n\n\n", RegexOptions.None, 0 };
57+
yield return new object[] { engine, @".", "\n\n\n", RegexOptions.Singleline, 3 };
58+
}
59+
}
60+
61+
[Fact]
62+
public void Count_InvalidArguments_Throws()
63+
{
64+
// input is null
65+
AssertExtensions.Throws<ArgumentNullException>("input", () => new Regex("pattern").Count(null));
66+
AssertExtensions.Throws<ArgumentNullException>("input", () => Regex.Count(null, @"pattern"));
67+
AssertExtensions.Throws<ArgumentNullException>("input", () => Regex.Count(null, @"pattern", RegexOptions.None));
68+
AssertExtensions.Throws<ArgumentNullException>("input", () => Regex.Count(null, @"pattern", RegexOptions.None, TimeSpan.FromMilliseconds(1)));
69+
70+
// pattern is null
71+
AssertExtensions.Throws<ArgumentNullException>("pattern", () => Regex.Count("input", null));
72+
AssertExtensions.Throws<ArgumentNullException>("pattern", () => Regex.Count("input", null, RegexOptions.None));
73+
AssertExtensions.Throws<ArgumentNullException>("pattern", () => Regex.Count("input", null, RegexOptions.None, TimeSpan.FromMilliseconds(1)));
74+
75+
// pattern is invalid
76+
#pragma warning disable RE0001 // invalid regex pattern
77+
AssertExtensions.Throws<RegexParseException>(() => Regex.Count("input", @"[abc"));
78+
AssertExtensions.Throws<RegexParseException>(() => Regex.Count("input", @"[abc", RegexOptions.None));
79+
AssertExtensions.Throws<RegexParseException>(() => Regex.Count("input", @"[abc", RegexOptions.None, TimeSpan.FromMilliseconds(1)));
80+
#pragma warning restore RE0001
81+
82+
// options is invalid
83+
AssertExtensions.Throws<ArgumentOutOfRangeException>("options", () => Regex.Count("input", @"[abc]", (RegexOptions)(-1)));
84+
AssertExtensions.Throws<ArgumentOutOfRangeException>("options", () => Regex.Count("input", @"[abc]", (RegexOptions)(-1), TimeSpan.FromMilliseconds(1)));
85+
86+
// matchTimeout is invalid
87+
AssertExtensions.Throws<ArgumentOutOfRangeException>("matchTimeout", () => Regex.Count("input", @"[abc]", RegexOptions.None, TimeSpan.FromMilliseconds(-2)));
88+
}
89+
90+
[Theory]
91+
[MemberData(nameof(RegexHelpers.AvailableEngines_MemberData), MemberType = typeof(RegexHelpers))]
92+
public async Task Count_Timeout_ThrowsAfterTooLongExecution(RegexEngine engine)
93+
{
94+
if (RegexHelpers.IsNonBacktracking(engine))
95+
{
96+
// Test relies on backtracking taking a long time
97+
return;
98+
}
99+
100+
const string Pattern = @"^(\w+\s?)*$";
101+
const string Input = "An input string that takes a very very very very very very very very very very very long time!";
102+
103+
Regex r = await RegexHelpers.GetRegexAsync(engine, Pattern, RegexOptions.None, TimeSpan.FromMilliseconds(1));
104+
105+
Stopwatch sw = Stopwatch.StartNew();
106+
Assert.Throws<RegexMatchTimeoutException>(() => r.Count(Input));
107+
Assert.InRange(sw.Elapsed.TotalSeconds, 0, 10); // arbitrary upper bound that should be well above what's needed with a 1ms timeout
108+
109+
switch (engine)
110+
{
111+
case RegexEngine.Interpreter:
112+
case RegexEngine.Compiled:
113+
sw = Stopwatch.StartNew();
114+
Assert.Throws<RegexMatchTimeoutException>(() => Regex.Count(Input, Pattern, RegexHelpers.OptionsFromEngine(engine), TimeSpan.FromMilliseconds(1)));
115+
Assert.InRange(sw.Elapsed.TotalSeconds, 0, 10); // arbitrary upper bound that should be well above what's needed with a 1ms timeout
116+
break;
117+
}
118+
}
119+
}
120+
}

src/libraries/System.Text.RegularExpressions/tests/System.Text.RegularExpressions.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
4242
</ItemGroup>
4343
<ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
44+
<Compile Include="Regex.Count.Tests.cs" />
4445
<Compile Include="RegexAssert.netcoreapp.cs" />
4546
<Compile Include="RegexParserTests.netcoreapp.cs" />
4647
<Compile Include="GroupCollectionReadOnlyDictionaryTests.cs" />

0 commit comments

Comments
 (0)