From 7abf649e2dd3f284e04a98972e35e774ef75ca1e Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Tue, 15 Sep 2026 15:33:32 +0200 Subject: [PATCH] First letter to upper case: retarget net10.0, source-generated regex, tests that can fail Retarget both projects from net6.0 to net10.0 and move the packages to current versions: BenchmarkDotNet and BenchmarkDotNet.Annotations 0.15.8, Microsoft.NET.Test.Sdk 18.10.1, MSTest.TestAdapter and MSTest.TestFramework 4.4.0 (from 2.2.8), coverlet.collector 10.0.1. No source change was needed for the MSTest major jump: this file has no Assert.ThrowsException call site. FirstCharToUpperRegex moves from Regex.Replace(input, "^[a-z]", ...) to a [GeneratedRegex("^\p{Ll}")] partial method. The ASCII class silently passed through any non-ASCII first letter that every other method here capitalizes. FirstCharToUpperUnsafeCode keeps its body and gains an XML doc comment and an inline comment saying what it really does: it mutates the instance it was given, so an interned literal changes for every reference to it. It is demonstration code. The unit tests could not fail before. All eight shared one interned literal, so the unsafe test upper-cased the input of every test that ran after it, and every assertion only checked char.IsUpper on the first character. The assertions are now AreEqual on the whole expected string, the unsafe tests build their own non-interned instances, and two tests are added: one proving the mutation reaches a second variable, one covering a non-ASCII first letter. Idiom lift: file-scoped namespaces, and the test class field becomes private readonly FirstLetterToUpperMethods _upperCase = new(). Build 0 warnings 0 errors, 10 of 10 tests passing on SDK 10.0.302, runtime 10.0.10. --- .../FirstLetterToUpper.csproj | 6 +- .../FirstLetterToUpperMethods.cs | 217 +++++++++--------- .../FirstLetterToUpperTests.csproj | 10 +- .../FirstLetterToUpperUnitTests.cs | 173 +++++++------- 4 files changed, 218 insertions(+), 188 deletions(-) diff --git a/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpper.csproj b/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpper.csproj index 05f194ae53..315bf66695 100644 --- a/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpper.csproj +++ b/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpper.csproj @@ -2,15 +2,15 @@ Exe - net6.0 + net10.0 enable enable true - - + + diff --git a/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpperMethods.cs b/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpperMethods.cs index 0e5c4424b1..395647d7e6 100644 --- a/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpperMethods.cs +++ b/strings-csharp/FirstLetterToUpper/FirstLetterToUpper/FirstLetterToUpperMethods.cs @@ -1,145 +1,156 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using System.Text.RegularExpressions; -namespace FirstLetterToUpper +namespace FirstLetterToUpper; + +[MemoryDiagnoser] +[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] +[RankColumn] +public partial class FirstLetterToUpperMethods { - [MemoryDiagnoser] - [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] - [RankColumn] - public class FirstLetterToUpperMethods + public IEnumerable SampleStrings() + { + yield return new object[] { GenerateRandomString(2000)}; + } + + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharSubstring(string input) { - public IEnumerable SampleStrings() + if (string.IsNullOrEmpty(input)) { - yield return new object[] { GenerateRandomString(2000)}; + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharSubstring(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + return $"{input[0].ToString().ToUpper()}{input.Substring(1)}"; + } - return $"{input[0].ToString().ToUpper()}{input.Substring(1)}"; + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpper(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpper(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + return $"{char.ToUpper(input[0])}{input[1..]}"; + } - return $"{char.ToUpper(input[0])}{input[1..]}"; + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToCharArray(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToCharArray(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + var stringArray = input.ToCharArray(); - var stringArray = input.ToCharArray(); + if (char.IsLower(stringArray[0])) + { + stringArray[0] = char.ToUpper(stringArray[0]); + } - if (char.IsLower(stringArray[0])) - { - stringArray[0] = char.ToUpper(stringArray[0]); - } + return new string(stringArray); + } - return new string(stringArray); + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpperAsSpan(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpperAsSpan(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + Span destination = stackalloc char[1]; - Span destination = stackalloc char[1]; + input.AsSpan(0, 1).ToUpperInvariant(destination); - input.AsSpan(0, 1).ToUpperInvariant(destination); + return $"{destination}{input.AsSpan(1)}"; + } - return $"{destination}{input.AsSpan(1)}"; - } + [GeneratedRegex("^\\p{Ll}")] + private static partial Regex FirstLowerLetter(); - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpperRegex(string input) + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpperRegex(string input) + { + if (string.IsNullOrEmpty(input)) { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - - return Regex.Replace(input, "^[a-z]", c => c.Value.ToUpper()); + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpperLinq(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + return FirstLowerLetter().Replace(input, c => c.Value.ToUpper()); + } - return $"{input.FirstOrDefault().ToString().ToUpper()}{input.Substring(1)}"; + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpperLinq(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; } + return $"{input.FirstOrDefault().ToString().ToUpper()}{input.Substring(1)}"; + } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpperStringCreate(string input) - { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } - return string.Create(input.Length, input, static (Span chars, string str) => - { - chars[0] = char.ToUpperInvariant(str[0]); - str.AsSpan(1).CopyTo(chars[1..]); - }); + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpperStringCreate(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; } - [Benchmark] - [ArgumentsSource(nameof(SampleStrings))] - public string FirstCharToUpperUnsafeCode(string input) + return string.Create(input.Length, input, static (Span chars, string str) => { - if (string.IsNullOrEmpty(input)) - { - return string.Empty; - } + chars[0] = char.ToUpperInvariant(str[0]); + str.AsSpan(1).CopyTo(chars[1..]); + }); + } + + /// + /// Demonstration code only. This method does not build a new string: it pins the string it was + /// given and overwrites its first character in place, so every other variable, field or + /// dictionary key pointing at the same instance changes with it. Identical string literals in an + /// assembly share one interned instance, which makes the damage reach code this method never saw. + /// Do not use it in production. + /// + [Benchmark] + [ArgumentsSource(nameof(SampleStrings))] + public string FirstCharToUpperUnsafeCode(string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } - unsafe + unsafe + { + // Mutates the instance itself. If input is an interned literal, every reference to + // that literal now reads the modified value. See the XML comment above. + fixed (char* p = input) { - fixed (char* p = input) - { - *p = char.ToUpper(*p); - } + *p = char.ToUpper(*p); } - - return input; } - private string GenerateRandomString(int size) - { - var random = new Random(); + return input; + } - var charOptions = "abcdefghijklmnopqrstuvwxyz"; + private string GenerateRandomString(int size) + { + var random = new Random(); - return new string(Enumerable.Repeat(charOptions, size).Select(s => s[random.Next(s.Length)]).ToArray()).ToLower(); - } + var charOptions = "abcdefghijklmnopqrstuvwxyz"; + + return new string(Enumerable.Repeat(charOptions, size).Select(s => s[random.Next(s.Length)]).ToArray()).ToLower(); } } diff --git a/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperTests.csproj b/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperTests.csproj index 8223e9ede4..564c26c729 100644 --- a/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperTests.csproj +++ b/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 enable enable @@ -9,10 +9,10 @@ - - - - + + + + diff --git a/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperUnitTests.cs b/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperUnitTests.cs index 2140bcbd71..bf6c764475 100644 --- a/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperUnitTests.cs +++ b/strings-csharp/FirstLetterToUpper/FirstLetterToUpperTests/FirstLetterToUpperUnitTests.cs @@ -1,82 +1,101 @@ using FirstLetterToUpper; -namespace FirstLetterToUpperTests +namespace FirstLetterToUpperTests; + +[TestClass] +public class FirstLetterToUpperUnitTests { - [TestClass] - public class FirstLetterToUpperUnitTests + private readonly FirstLetterToUpperMethods _upperCase = new(); + + [TestMethod] + public void GivenALowerCaseString_WhenUsingSubStringTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharSubstring(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingCharToUpperTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToUpper(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingCharArrayTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToCharArray(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingAsSpanTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToUpperAsSpan(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingStringCreateTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToUpperStringCreate(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingRegexTechnique_VerifyFirstCharIsUpper() + { + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToUpperRegex(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenANonAsciiLowerCaseString_WhenUsingRegexTechnique_VerifyFirstCharIsUpper() + { + var testString = "élan vital"; + var returnedString = _upperCase.FirstCharToUpperRegex(testString); + + Assert.AreEqual("Élan vital", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingLinqTechnique_VerifyFirstCharIsUpper() { - FirstLetterToUpperMethods upperCase = new FirstLetterToUpperMethods(); - - [TestMethod] - public void GivenALowerCaseString_WhenUsingSubStringTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharSubstring(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingCharToUpperTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpper(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingCharArrayTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToCharArray(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingAsSpanTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpperAsSpan(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingStringCreateTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpperStringCreate(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingRegexTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpperRegex(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingLinqTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpperLinq(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } - - [TestMethod] - public void GivenALowerCaseString_WhenUsingUnsafeCodeTechnique_VerifyFirstCharIsUpper() - { - var testString = "this is a test string"; - var returnedString = upperCase.FirstCharToUpperUnsafeCode(testString); - - Assert.IsTrue(char.IsUpper(returnedString[0])); - } + var testString = "this is a test string"; + var returnedString = _upperCase.FirstCharToUpperLinq(testString); + + Assert.AreEqual("This is a test string", returnedString); + } + + [TestMethod] + public void GivenALowerCaseString_WhenUsingUnsafeCodeTechnique_VerifyFirstCharIsUpper() + { + var testString = new string("unsafe demo input".ToCharArray()); + var returnedString = _upperCase.FirstCharToUpperUnsafeCode(testString); + + Assert.AreEqual("Unsafe demo input", returnedString); + } + + [TestMethod] + public void GivenTwoVariablesHoldingTheSameString_WhenUsingUnsafeCode_ThenBothStringsChange() + { + var first = new string("mutation demo".ToCharArray()); + var second = first; + + _upperCase.FirstCharToUpperUnsafeCode(first); + + Assert.AreEqual("Mutation demo", second); } -} \ No newline at end of file +}