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 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..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,10 +765,11 @@ 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) { } - 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..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,10 +765,11 @@ 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) { } - 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..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,10 +546,11 @@ 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) { } - 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..37de1e9f8 100644 --- a/src/Humanizer.Tests/InflectorTests.cs +++ b/src/Humanizer.Tests/InflectorTests.cs @@ -110,9 +110,29 @@ 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")] + [InlineData("HTTP SSL IO module RESTful", "HTTPSSLIOModuleRESTful")] + 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")] + [InlineData("HTTP SSL IO module RESTful ", "HttpSslIoModuleRestful")] + 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")] @@ -127,6 +147,22 @@ public void Pascalize(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 b129372e8..60c4b29f7 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 /// @@ -71,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 ///