From 951ea045b804bebb24205fc416c27dfa1e635dc7 Mon Sep 17 00:00:00 2001 From: Yum Vin Date: Sun, 22 Jun 2025 08:55:17 +1000 Subject: [PATCH 1/5] Pascalize with option to preserveacronym --- ...Approve_Public_Api.DotNet10_0.verified.txt | 2 +- ....Approve_Public_Api.DotNet8_0.verified.txt | 2 +- ...est.Approve_Public_Api.Net4_8.verified.txt | 2 +- src/Humanizer.Tests/InflectorTests.cs | 20 ++++++- src/Humanizer/InflectorExtensions.cs | 54 ++++++++++++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt index 9a3753c46..c6e7fed80 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt @@ -768,7 +768,7 @@ namespace Humanizer public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } - public static string Pascalize(this string input) { } + public static string Pascalize(this string input, bool preserveAcronym = true) { } [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")] public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { } public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { } diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt index bb3920e7f..68d9de65f 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt @@ -768,7 +768,7 @@ namespace Humanizer public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } - public static string Pascalize(this string input) { } + public static string Pascalize(this string input, bool preserveAcronym = true) { } [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")] public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { } public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { } diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt index 44794d215..6fa4b09ee 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt @@ -549,7 +549,7 @@ namespace Humanizer public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } - public static string Pascalize(this string input) { } + public static string Pascalize(this string input, bool preserveAcronym = true) { } [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")] public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { } public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { } diff --git a/src/Humanizer.Tests/InflectorTests.cs b/src/Humanizer.Tests/InflectorTests.cs index 3518355ec..d3f8cdb22 100644 --- a/src/Humanizer.Tests/InflectorTests.cs +++ b/src/Humanizer.Tests/InflectorTests.cs @@ -110,9 +110,27 @@ public void Hyphenate(string input, string expectedOutput) => [InlineData("customer-first-name", "CustomerFirstName")] [InlineData("_customer-first-name", "CustomerFirstName")] [InlineData(" customer__first--name", "CustomerFirstName")] - public void Pascalize(string input, string expectedOutput) => + [InlineData("HTTP-method IO module RESTful", "HTTPMethodIOModuleRESTful")] + public void PascalizeWithAcronymPreserved(string input, string expectedOutput) => Assert.Equal(expectedOutput, input.Pascalize()); + + [Theory] + [InlineData("customer", "Customer")] + [InlineData("CUSTOMER", "Customer")] + [InlineData("CUStomer", "Customer")] + [InlineData("customer_name", "CustomerName")] + [InlineData("customer_first_name", "CustomerFirstName")] + [InlineData("customer_first_name goes here", "CustomerFirstNameGoesHere")] + [InlineData("customer name", "CustomerName")] + [InlineData("customer name", "CustomerName")] + [InlineData("customer-first-name", "CustomerFirstName")] + [InlineData("_customer-first-name", "CustomerFirstName")] + [InlineData(" customer__first--name", "CustomerFirstName")] + [InlineData("HTTP-method IO module RESTful ", "HttpMethodIoModuleRestful")] + public void PascalizeWithoutAcronymPreserved(string input, string expectedOutput) => + Assert.Equal(expectedOutput, input.Pascalize(false)); + // Same as pascalize, except first char is lowercase [Theory] [InlineData("customer", "customer")] diff --git a/src/Humanizer/InflectorExtensions.cs b/src/Humanizer/InflectorExtensions.cs index b129372e8..c3e6a0065 100644 --- a/src/Humanizer/InflectorExtensions.cs +++ b/src/Humanizer/InflectorExtensions.cs @@ -52,12 +52,64 @@ public static string Titleize(this string input) => /// /// By default, pascalize converts strings to UpperCamelCase also removing underscores + /// For a word that are all upper case, this function by default assumes its an acronym and preserves the casing + /// of all letters in that word + /// In order to treat it as a word and not acronym, set preserveAcronym to false /// - public static string Pascalize(this string input) => + public static string Pascalize(this string input, bool preserveAcronym = true) + { + if (preserveAcronym) + return input.PascalizeWithAcronymPreservation(); + else + return input.PascalizeWithoutAcronymPreservation(); + } + + private static string PascalizeWithAcronymPreservation(this string input) => Regex.Replace(input, @"(?:[ _-]+|^)([a-zA-Z])", match => match .Groups[1] .Value.ToUpper()); + + private static string PascalizeWithoutAcronymPreservation(this string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + var span = input.AsSpan(); + var sb = new StringBuilder(input.Length); + + var wordStart = 0; + for (var index = 0; index <= span.Length; index++) + { + var isSeparator = index == span.Length || span[index] == ' ' || span[index] == '_' || span[index] == '-'; + + if (!isSeparator) + continue; + + var wordSpan = span.Slice(wordStart, index - wordStart); + + if (wordSpan.Length > 0) + { + // Otherwise, capitalize the first letter and append the rest + sb.Append(char.ToUpper(wordSpan[0])); + for (var k = 1; k < wordSpan.Length; k++) + { + sb.Append(char.ToLower(wordSpan[k])); + } + } + + while (index < span.Length && (span[index] == ' ' || span[index] == '_' || span[index] == '-')) + { + index++; + } + + wordStart = index; + } + + return sb.ToString(); + } + + /// /// Same as Pascalize except that the first character is lower case /// From 6e5728407f45350f68b0b510ddb72d34ec90a9a5 Mon Sep 17 00:00:00 2001 From: Yum Vin Date: Wed, 25 Jun 2025 17:53:34 +1000 Subject: [PATCH 2/5] Camelize with acronym use case --- ....Approve_Public_Api.DotNet10_0.verified.txt | 1 + ...t.Approve_Public_Api.DotNet8_0.verified.txt | 1 + ...Test.Approve_Public_Api.Net4_8.verified.txt | 1 + src/Humanizer.Tests/InflectorTests.cs | 18 ++++++++++++++++++ src/Humanizer/InflectorExtensions.cs | 15 +++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt index c6e7fed80..694b8964b 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt @@ -765,6 +765,7 @@ namespace Humanizer public static class InflectorExtensions { public static string Camelize(this string input) { } + public static string CamelizeV2(this string input) { } public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt index 68d9de65f..9706a56de 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt @@ -765,6 +765,7 @@ namespace Humanizer public static class InflectorExtensions { public static string Camelize(this string input) { } + public static string CamelizeV2(this string input) { } public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt index 6fa4b09ee..62815357f 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt @@ -546,6 +546,7 @@ namespace Humanizer public static class InflectorExtensions { public static string Camelize(this string input) { } + public static string CamelizeV2(this string input) { } public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } diff --git a/src/Humanizer.Tests/InflectorTests.cs b/src/Humanizer.Tests/InflectorTests.cs index d3f8cdb22..37de1e9f8 100644 --- a/src/Humanizer.Tests/InflectorTests.cs +++ b/src/Humanizer.Tests/InflectorTests.cs @@ -111,6 +111,7 @@ public void Hyphenate(string input, string expectedOutput) => [InlineData("_customer-first-name", "CustomerFirstName")] [InlineData(" customer__first--name", "CustomerFirstName")] [InlineData("HTTP-method IO module RESTful", "HTTPMethodIOModuleRESTful")] + [InlineData("HTTP SSL IO module RESTful", "HTTPSSLIOModuleRESTful")] public void PascalizeWithAcronymPreserved(string input, string expectedOutput) => Assert.Equal(expectedOutput, input.Pascalize()); @@ -128,6 +129,7 @@ public void PascalizeWithAcronymPreserved(string input, string expectedOutput) = [InlineData("_customer-first-name", "CustomerFirstName")] [InlineData(" customer__first--name", "CustomerFirstName")] [InlineData("HTTP-method IO module RESTful ", "HttpMethodIoModuleRestful")] + [InlineData("HTTP SSL IO module RESTful ", "HttpSslIoModuleRestful")] public void PascalizeWithoutAcronymPreserved(string input, string expectedOutput) => Assert.Equal(expectedOutput, input.Pascalize(false)); @@ -145,6 +147,22 @@ public void PascalizeWithoutAcronymPreserved(string input, string expectedOutput public void Camelize(string input, string expectedOutput) => Assert.Equal(expectedOutput, input.Camelize()); + + // Handles acronyms uniformly, preserving the case of the first letter + [Theory] + [InlineData("customer", "customer")] + [InlineData("CUSTOMER", "customer")] + [InlineData("CUStomer", "customer")] + [InlineData("customer_name", "customerName")] + [InlineData("customer_first_name", "customerFirstName")] + [InlineData("customer_first_name goes here", "customerFirstNameGoesHere")] + [InlineData("customer name", "customerName")] + [InlineData("customer name", "customerName")] + [InlineData("HTTP-method IO module RESTful ", "httpMethodIoModuleRestful")] + [InlineData("", "")] + public void CamelizeV2(string input, string expectedOutput) => + Assert.Equal(expectedOutput, input.CamelizeV2()); + //Makes an underscored lowercase string [Theory] [InlineData("SomeTitle", "some_title")] diff --git a/src/Humanizer/InflectorExtensions.cs b/src/Humanizer/InflectorExtensions.cs index c3e6a0065..60c4b29f7 100644 --- a/src/Humanizer/InflectorExtensions.cs +++ b/src/Humanizer/InflectorExtensions.cs @@ -123,6 +123,21 @@ public static string Camelize(this string input) : word; } + /// + /// Adheres to Google camel case definition https://google.github.io/styleguide/jsguide.html#naming-camel-case-defined + /// that appropriately handles acronyms. + /// + public static string CamelizeV2(this string input) + { + var word = input.Pascalize(false); + return word.Length > 0 + ? StringHumanizeExtensions.Concat( + char.ToLower(word[0]), + word.AsSpan(1)) + : word; + + } + /// /// Separates the input words with underscore /// From 4b1bdc99bdff3298c81306ea8c125b6655205870 Mon Sep 17 00:00:00 2001 From: Yum Vin Date: Wed, 25 Jun 2025 18:05:41 +1000 Subject: [PATCH 3/5] Update README --- readme.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 6359380af..68c8954ff 100644 --- a/readme.md +++ b/readme.md @@ -633,10 +633,11 @@ You can combine `wordForm` argument with gender but passing this argument in whe #### Pascalize -`Pascalize` converts the input words to UpperCamelCase, also removing underscores and spaces: +`Pascalize` converts the input words to UpperCamelCase, also removing underscores and spaces, with optional capability to preserve acronym casing: ```C# -"some_title for something".Pascalize() => "SomeTitleForSomething" +"some_title for some SKU".Pascalize() => "SomeTitleForSomeSKU" +"NBA-Basketball MLS-baseball".Pascalize(false) => "NbaBasketballMlsBaseball" ``` @@ -648,6 +649,14 @@ You can combine `wordForm` argument with gender but passing this argument in whe "some_title for something".Camelize() => "someTitleForSomething" ``` +#### CamelizeV2 + +`Camelize` camelizes acronyms correctly, it is backwards incompatible with the way current Camelize works which is why introducing a new version + +```C# +"HTTP IO module".Camelize() => "httpIoModule" +``` + #### Underscore From ffc3c016b6d8499cae019ec3a64ee97e3f79cc88 Mon Sep 17 00:00:00 2001 From: Yum Vin Date: Wed, 25 Jun 2025 18:26:09 +1000 Subject: [PATCH 4/5] Fix 10.0 api approver verified text --- ...pprovalTest.Approve_Public_Api.DotNet10_0.verified.txt | 2 +- src/Humanizer/Inflections/PossesiveSuffixOverride.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/Humanizer/Inflections/PossesiveSuffixOverride.cs diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt index 694b8964b..85d9cb672 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt @@ -765,7 +765,7 @@ namespace Humanizer public static class InflectorExtensions { public static string Camelize(this string input) { } - public static string CamelizeV2(this string input) { } + public static string CamelizeV2(this string input) { } public static string Dasherize(this string underscoredWord) { } public static string Hyphenate(this string underscoredWord) { } public static string Kebaberize(this string input) { } diff --git a/src/Humanizer/Inflections/PossesiveSuffixOverride.cs b/src/Humanizer/Inflections/PossesiveSuffixOverride.cs new file mode 100644 index 000000000..2ef712bca --- /dev/null +++ b/src/Humanizer/Inflections/PossesiveSuffixOverride.cs @@ -0,0 +1,8 @@ +namespace Humanizer; + +public enum PossesiveSuffixOverride +{ + S_ONLY, + APOSTROPHE_ONLY, + APOSTROPHE_S +} From da19c152c3015e4be83e5b09148c0d8fa9773f34 Mon Sep 17 00:00:00 2001 From: Yum Vin Date: Wed, 25 Jun 2025 18:43:39 +1000 Subject: [PATCH 5/5] Remove accidentally added enum --- src/Humanizer/Inflections/PossesiveSuffixOverride.cs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/Humanizer/Inflections/PossesiveSuffixOverride.cs diff --git a/src/Humanizer/Inflections/PossesiveSuffixOverride.cs b/src/Humanizer/Inflections/PossesiveSuffixOverride.cs deleted file mode 100644 index 2ef712bca..000000000 --- a/src/Humanizer/Inflections/PossesiveSuffixOverride.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Humanizer; - -public enum PossesiveSuffixOverride -{ - S_ONLY, - APOSTROPHE_ONLY, - APOSTROPHE_S -}