From bad5212e5db93a86a070a1c760f9e3c7e723e243 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Tue, 15 Sep 2026 15:36:41 +0200 Subject: [PATCH] Counting char occurrences: retarget net10.0, fix IndexOf undercount, Regex.Count CountCharsUsingIndex skipped the character after each match, so adjacent occurrences were counted as one ("aab" returned 1, "aaaa" returned 2). Removed the extra increment and added a regression test for adjacent occurrences, which the existing suite could not fail on. Also: - both projects retargeted from net6.0 to net10.0 - CommunityToolkit.HighPerformance removed; MemoryExtensions.Count() supplies the same call from System since .NET 8 - CountCharsUsingRegex uses Regex.Count() instead of building a Regex per call - new CountSubstringsAndFrequencies class with four tests, covering counting a substring (overlapping and non-overlapping) and counting every character - Program.cs lifted to top-level statements - BenchmarkDotNet 0.15.8, Microsoft.NET.Test.Sdk 18.10.1, xunit 2.9.3, xunit.runner.visualstudio 4.0.0, coverlet.collector 10.0.1 --- .../CountingCharOccurences/CountChars.cs | 6 +-- .../CountSubstringsAndFrequencies.cs | 35 +++++++++++++ .../CountingCharOccurences.csproj | 8 +-- .../CountingCharOccurences/Program.cs | 12 +---- .../Test/CountSubstringsAndFrequenciesTest.cs | 52 +++++++++++++++++++ .../CountingCharOccurences/Test/Test.cs | 11 ++++ .../CountingCharOccurences/Test/Test.csproj | 10 ++-- 7 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 strings-csharp/CountingCharOccurences/CountingCharOccurences/CountSubstringsAndFrequencies.cs create mode 100644 strings-csharp/CountingCharOccurences/Test/CountSubstringsAndFrequenciesTest.cs diff --git a/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountChars.cs b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountChars.cs index 2aba88c664..a38319b1de 100644 --- a/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountChars.cs +++ b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountChars.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using System.Text.RegularExpressions; -using CommunityToolkit.HighPerformance; namespace CountingCharOccurences { @@ -59,10 +58,7 @@ public int CountCharsUsingIndex(string source, char toFind) int n = 0; while ((n = source.IndexOf(toFind, n) + 1) != 0) - { - n++; count++; - } return count; } @@ -130,7 +126,7 @@ public int CountCharsUsingReplace(string source, char toFind) [ArgumentsSource(nameof(GenerateStringWithCharArgs))] public int CountCharsUsingRegex(string source, char toFind) { - return new Regex(Regex.Escape(toFind.ToString())).Matches(source).Count; + return Regex.Count(source, Regex.Escape(toFind.ToString())); } [Benchmark] diff --git a/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountSubstringsAndFrequencies.cs b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountSubstringsAndFrequencies.cs new file mode 100644 index 0000000000..9c3b23485e --- /dev/null +++ b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountSubstringsAndFrequencies.cs @@ -0,0 +1,35 @@ +namespace CountingCharOccurences +{ + public class CountSubstringsAndFrequencies + { + public int CountSubstringNonOverlapping(string source, string toFind) + { + return source.AsSpan().Count(toFind); + } + + public int CountSubstringOverlapping(string source, string toFind) + { + var count = 0; + + for (var n = 0; (n = source.IndexOf(toFind, n, StringComparison.Ordinal)) != -1; n++) + count++; + + return count; + } + + public Dictionary CountEveryCharUsingLinq(string source) + { + return source.GroupBy(c => c).ToDictionary(g => g.Key, g => g.Count()); + } + + public Dictionary CountEveryCharUsingLoop(string source) + { + var counted = new Dictionary(); + + foreach (var c in source) + counted[c] = counted.GetValueOrDefault(c) + 1; + + return counted; + } + } +} diff --git a/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountingCharOccurences.csproj b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountingCharOccurences.csproj index 7a93a95341..62be4f867f 100644 --- a/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountingCharOccurences.csproj +++ b/strings-csharp/CountingCharOccurences/CountingCharOccurences/CountingCharOccurences.csproj @@ -2,17 +2,13 @@ Exe - net6.0 + net10.0 enable enable - - + - - - diff --git a/strings-csharp/CountingCharOccurences/CountingCharOccurences/Program.cs b/strings-csharp/CountingCharOccurences/CountingCharOccurences/Program.cs index cf3e092698..091f7f1c4d 100644 --- a/strings-csharp/CountingCharOccurences/CountingCharOccurences/Program.cs +++ b/strings-csharp/CountingCharOccurences/CountingCharOccurences/Program.cs @@ -1,12 +1,4 @@ using BenchmarkDotNet.Running; +using CountingCharOccurences; -namespace CountingCharOccurences -{ - internal class Program - { - static void Main(string[] args) - { - BenchmarkRunner.Run(); - } - } -} \ No newline at end of file +BenchmarkRunner.Run(); diff --git a/strings-csharp/CountingCharOccurences/Test/CountSubstringsAndFrequenciesTest.cs b/strings-csharp/CountingCharOccurences/Test/CountSubstringsAndFrequenciesTest.cs new file mode 100644 index 0000000000..868dee22d2 --- /dev/null +++ b/strings-csharp/CountingCharOccurences/Test/CountSubstringsAndFrequenciesTest.cs @@ -0,0 +1,52 @@ +using CountingCharOccurences; + +namespace Test +{ + public class CountSubstringsAndFrequenciesTest + { + private static readonly CountSubstringsAndFrequencies _counter = new CountSubstringsAndFrequencies(); + + [Fact] + public void WhenCountSubstringNonOverlappingThenMatchesDoNotOverlap() + { + string main = "aaaa"; + string toFind = "aa"; + + int actual = _counter.CountSubstringNonOverlapping(main, toFind); + + Assert.Equal(2, actual); + } + + [Fact] + public void WhenCountSubstringOverlappingThenMatchesOverlap() + { + string main = "aaaa"; + string toFind = "aa"; + + int actual = _counter.CountSubstringOverlapping(main, toFind); + + Assert.Equal(3, actual); + } + + [Fact] + public void WhenCountEveryCharUsingLinqThenReturnFrequencyOfEachChar() + { + string main = "hello world"; + + var actual = _counter.CountEveryCharUsingLinq(main); + + Assert.Equal(3, actual['l']); + Assert.Equal(2, actual['o']); + } + + [Fact] + public void WhenCountEveryCharUsingLoopThenReturnSameFrequenciesAsLinq() + { + string main = "hello world"; + + var actual = _counter.CountEveryCharUsingLoop(main); + + Assert.Equal(_counter.CountEveryCharUsingLinq(main), actual); + } + } +} diff --git a/strings-csharp/CountingCharOccurences/Test/Test.cs b/strings-csharp/CountingCharOccurences/Test/Test.cs index d9ed402a86..3878f71ef8 100644 --- a/strings-csharp/CountingCharOccurences/Test/Test.cs +++ b/strings-csharp/CountingCharOccurences/Test/Test.cs @@ -50,6 +50,17 @@ public void WhenSearchCharUsingIndexThenReturnNumberOfOccurences() Assert.Equal(2, actual); } + [Fact] + public void WhenSearchCharWithAdjacentOccurencesThenReturnNumberOfOccurences() + { + string main = "LLama LLama"; + char toFind = 'L'; + + int actual = _countChars.CountCharsUsingIndex(main, toFind); + + Assert.Equal(4, actual); + } + [Fact] public void WhenSearchCharUsingForThenReturnNumberOfOccurences() { diff --git a/strings-csharp/CountingCharOccurences/Test/Test.csproj b/strings-csharp/CountingCharOccurences/Test/Test.csproj index 481e6febe7..e7256bb28b 100644 --- a/strings-csharp/CountingCharOccurences/Test/Test.csproj +++ b/strings-csharp/CountingCharOccurences/Test/Test.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 enable enable @@ -9,13 +9,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all