From 9e21fc57c655660bae7cbe48bc784f1ed915a24b Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Sun, 27 Sep 2026 01:23:50 +0000 Subject: [PATCH] Trim before case-mapping the first char in the first-char helpers ToUppercaseFirstChar and ToLowercaseFirstChar mapped input[0] and only then trimmed, so with leading whitespace the whitespace was "case-mapped" and trimmed away while the first letter was left unchanged (" hello" -> "hello"). They now collapse and trim first, then map the first code point. The existing space collapsing is kept and now stated in the XML summaries. Fixes #79 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013qSqApi9HSiBjyHaJQYPTP --- CaseConverter.Test/CaseConverterTests.cs | 26 ++++++++++++++++++++++++ CaseConverter/CaseConverter.cs | 12 +++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CaseConverter.Test/CaseConverterTests.cs b/CaseConverter.Test/CaseConverterTests.cs index cbebe44..78c6598 100644 --- a/CaseConverter.Test/CaseConverterTests.cs +++ b/CaseConverter.Test/CaseConverterTests.cs @@ -357,6 +357,32 @@ public void ToUppercaseFirstCharShouldUppercaseAnAstralFirstLetter() Assert.AreEqual($"{DeseretCapitalLongI}abc", result); } + [TestMethod] + [DataRow(" hello", "Hello")] + [DataRow("\thello", "Hello")] + [DataRow(" hello ", "Hello")] + public void ToUppercaseFirstCharShouldSkipLeadingWhitespace(string input, string expected) + { + Assert.AreEqual(expected, input.ToUppercaseFirstChar()); + } + + [TestMethod] + [DataRow(" Hello", "hello")] + [DataRow("\tHello", "hello")] + [DataRow(" Hello ", "hello")] + public void ToLowercaseFirstCharShouldSkipLeadingWhitespace(string input, string expected) + { + Assert.AreEqual(expected, input.ToLowercaseFirstChar()); + } + + [TestMethod] + public void FirstCharHelpersShouldCollapseRunsOfSpaces() + { + // Documented behaviour: both helpers collapse runs of spaces to one. + Assert.AreEqual("A b", "a b".ToUppercaseFirstChar()); + Assert.AreEqual("a b", "A b".ToLowercaseFirstChar()); + } + [TestMethod] public void ToSnakeCaseShouldStillDropAstralCharactersThatAreNotLetters() { diff --git a/CaseConverter/CaseConverter.cs b/CaseConverter/CaseConverter.cs index 2644628..4987854 100644 --- a/CaseConverter/CaseConverter.cs +++ b/CaseConverter/CaseConverter.cs @@ -145,11 +145,13 @@ private static bool IsWordBoundary(string input, int previousStart, int start, i } /// - /// Returns a copy of this string with the first character converted to lowercase. + /// Returns a copy of this string, trimmed and with runs of spaces collapsed to one, with the first character converted to lowercase. /// /// The string to convert. /// A new string with the first character converted to lowercase. /// + /// The string is trimmed before the first character is chosen, so leading whitespace does not + /// take the place of the first letter. /// The first character is the first code point, so a letter outside the Basic Multilingual /// Plane is case-mapped as a whole rather than through its high surrogate alone, which would /// leave it unchanged. @@ -158,22 +160,24 @@ private static bool IsWordBoundary(string input, int previousStart, int start, i public static string ToLowercaseFirstChar(this string input) { Ensure.NotNull(input); - return CollapseSpaces(MapFirstCodePoint(input, static first => first.ToLowerInvariant())).Trim(); + return MapFirstCodePoint(CollapseSpaces(input).Trim(), static first => first.ToLowerInvariant()); } /// - /// Returns a copy of this string with the first character converted to uppercase. + /// Returns a copy of this string, trimmed and with runs of spaces collapsed to one, with the first character converted to uppercase. /// /// The string to convert. /// A new string with the first character converted to uppercase. /// + /// The string is trimmed before the first character is chosen, so leading whitespace does not + /// take the place of the first letter. /// The first character is the first code point, for the same reason as in /// . /// public static string ToUppercaseFirstChar(this string input) { Ensure.NotNull(input); - return CollapseSpaces(MapFirstCodePoint(input, static first => first.ToUpperInvariant())).Trim(); + return MapFirstCodePoint(CollapseSpaces(input).Trim(), static first => first.ToUpperInvariant()); } ///