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.
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