From b50bc1ff6740b8c197ea4f508b5e43d704931f20 Mon Sep 17 00:00:00 2001 From: mohammed Date: Thu, 8 Jan 2026 10:30:20 +0300 Subject: [PATCH 1/2] Fix TitleCase capitalization at sentence boundaries --- src/Humanizer/Transformer/ToTitleCase.cs | 36 ++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Humanizer/Transformer/ToTitleCase.cs b/src/Humanizer/Transformer/ToTitleCase.cs index df506d9ea..c1415ddfa 100644 --- a/src/Humanizer/Transformer/ToTitleCase.cs +++ b/src/Humanizer/Transformer/ToTitleCase.cs @@ -10,7 +10,7 @@ public string Transform(string input) => #if NET7_0_OR_GREATER [GeneratedRegex(WordPattern)] private static partial Regex WordRegexGenerated(); - + private static Regex WordRegex() => WordRegexGenerated(); #else private static readonly Regex WordRegexDefinition = new(WordPattern, RegexOptions.Compiled); @@ -26,11 +26,19 @@ public string Transform(string input, CultureInfo culture) foreach (Match word in matches) { var value = word.Value; - if (AllCapitals(value) || IsArticleOrConjunctionOrPreposition(value)) + var isSentenceStart = IsSentenceStart(input, word.Index); + + if (AllCapitals(value)) + { + continue; + } + + if (IsArticleOrConjunctionOrPreposition(value) && !isSentenceStart) { continue; } + builder[word.Index] = textInfo.ToUpper(value[0]); Overwrite(builder, word.Index + 1, textInfo.ToLower(value[1..])); } @@ -71,4 +79,28 @@ word is // prepositions "as" or "at" or "by" or "for" or "in" or "of" or "off" or "on" or "to" or "up" or "via"; + + + static bool IsSentenceStart(string input, int wordIndex) + { + if (wordIndex == 0) + { + return true; + } + + for (var i = wordIndex - 1; i >= 0; i--) + { + var ch = input[i]; + + if (char.IsWhiteSpace(ch)) + { + continue; + } + + return ch is '.' or '!' or '?'; + } + + return true; + } + } \ No newline at end of file From e8190a5c83bcb7e8fe8e4670fed67a9fdaa60fc4 Mon Sep 17 00:00:00 2001 From: mohammed Date: Mon, 12 Jan 2026 19:22:49 +0300 Subject: [PATCH 2/2] Fix Enum.Humanize for enums known only at runtime --- src/Humanizer.Tests/EnumHumanizeTests.cs | 12 +++++++++++ src/Humanizer/EnumHumanizeExtensions.cs | 27 +++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Humanizer.Tests/EnumHumanizeTests.cs b/src/Humanizer.Tests/EnumHumanizeTests.cs index 277e0f3a1..55a1aa59d 100644 --- a/src/Humanizer.Tests/EnumHumanizeTests.cs +++ b/src/Humanizer.Tests/EnumHumanizeTests.cs @@ -176,4 +176,16 @@ enum DummyEnum First, Second } + [Fact] + public void Humanize_Works_For_Enum_Known_Only_At_Runtime() + { + Enum value = EnumUnderTest.MemberWithoutDescriptionAttribute; + + var result = value.Humanize(); + + Assert.Equal( + EnumTestsResources.MemberWithoutDescriptionAttributeSentence, + result); + } + } \ No newline at end of file diff --git a/src/Humanizer/EnumHumanizeExtensions.cs b/src/Humanizer/EnumHumanizeExtensions.cs index 136e27ff2..c96f74957 100644 --- a/src/Humanizer/EnumHumanizeExtensions.cs +++ b/src/Humanizer/EnumHumanizeExtensions.cs @@ -26,15 +26,15 @@ public static class EnumHumanizeExtensions /// /// enum UserType { AnonymousUser, RegisteredUser } /// UserType.AnonymousUser.Humanize() => "Anonymous user" - /// + /// /// [Flags] /// enum Permission { None = 0, Read = 1, Write = 2, Delete = 4 } /// (Permission.Read | Permission.Write).Humanize() => "Read, Write" - /// - /// enum Status - /// { + /// + /// enum Status + /// { /// [Description("Currently active")] - /// Active + /// Active /// } /// Status.Active.Humanize() => "Currently active" /// @@ -62,6 +62,23 @@ public static class EnumHumanizeExtensions return humanized[input]; } + public static string Humanize(this Enum input) + { + if (input == null) + ArgumentNullException.ThrowIfNull(input); + + var enumType = input.GetType(); + + // Get the generic Humanize method + var method = typeof(EnumHumanizeExtensions) + .GetMethod(nameof(Humanize), new[] { enumType }); + + if (method == null) + throw new InvalidOperationException($"No Humanize method found for enum type {enumType}."); + + return (string)method.Invoke(null, new object[] { input })!; + } + /// /// Converts an enum value to a human-readable string with the specified letter casing applied.