diff --git a/Fluid.Tests/TemplateTests.cs b/Fluid.Tests/TemplateTests.cs index 91e588a3..24055f1a 100644 --- a/Fluid.Tests/TemplateTests.cs +++ b/Fluid.Tests/TemplateTests.cs @@ -1143,6 +1143,48 @@ public void MemberNameStrategiesHandleSuccessiveUppercase() Assert.Equal("uv_index", snakeCase); } + [Fact] + public void SnakeCaseHandlesAcronymsCorrectly() + { + // Test UserName -> user_name + Assert.Equal("user_name", MemberNameStrategies.SnakeCase(typeof(TestClass_UserName).GetProperty("UserName"))); + + // Test OpenAIModel -> open_ai_model + Assert.Equal("open_ai_model", MemberNameStrategies.SnakeCase(typeof(TestClass_OpenAIModel).GetProperty("OpenAIModel"))); + + // Test OEMVendor -> oem_vendor + Assert.Equal("oem_vendor", MemberNameStrategies.SnakeCase(typeof(TestClass_OEMVendor).GetProperty("OEMVendor"))); + + // Test IDSecurity -> id_security + Assert.Equal("id_security", MemberNameStrategies.SnakeCase(typeof(TestClass_IDSecurity).GetProperty("IDSecurity"))); + + // Test ID -> id + Assert.Equal("id", MemberNameStrategies.SnakeCase(typeof(TestClass_ID).GetProperty("ID"))); + + // Test XMLParser -> xml_parser + Assert.Equal("xml_parser", MemberNameStrategies.SnakeCase(typeof(TestClass_XMLParser).GetProperty("XMLParser"))); + + // Test HTMLElement -> html_element + Assert.Equal("html_element", MemberNameStrategies.SnakeCase(typeof(TestClass_HTMLElement).GetProperty("HTMLElement"))); + + // Test IOError -> io_error + Assert.Equal("io_error", MemberNameStrategies.SnakeCase(typeof(TestClass_IOError).GetProperty("IOError"))); + + // Test JSONData -> json_data + Assert.Equal("json_data", MemberNameStrategies.SnakeCase(typeof(TestClass_JSONData).GetProperty("JSONData"))); + } + + private class TestClass_UserName { public string UserName { get; set; } } + private class TestClass_OpenAIModel { public string OpenAIModel { get; set; } } + private class TestClass_OEMVendor { public string OEMVendor { get; set; } } + private class TestClass_IDSecurity { public string IDSecurity { get; set; } } + private class TestClass_ID { public int ID { get; set; } } + private class TestClass_UVIndex { public string UVIndex { get; set; } } + private class TestClass_XMLParser { public string XMLParser { get; set; } } + private class TestClass_HTMLElement { public string HTMLElement { get; set; } } + private class TestClass_IOError { public string IOError { get; set; } } + private class TestClass_JSONData { public string JSONData { get; set; } } + [Fact] public async Task ShouldIterateOnDictionaries() { diff --git a/Fluid/MemberNameStrategies.cs b/Fluid/MemberNameStrategies.cs index ee20fe03..6be13141 100644 --- a/Fluid/MemberNameStrategies.cs +++ b/Fluid/MemberNameStrategies.cs @@ -45,18 +45,30 @@ public static string RenameCamelCase(MemberInfo member) public static string RenameSnakeCase(MemberInfo member) { - var upper = 0; - for (var i = 1; i < member.Name.Length; i++) + // Calculate the exact number of underscores needed + var underscores = 0; + var previousUpper = false; + + for (var i = 0; i < member.Name.Length; i++) { - if (char.IsUpper(member.Name[i])) + var c = member.Name[i]; + if (char.IsUpper(c)) + { + if (i > 0 && (!previousUpper || (i + 1 < member.Name.Length && char.IsLower(member.Name[i + 1])))) + { + underscores++; + } + previousUpper = true; + } + else { - upper++; + previousUpper = false; } } - return String.Create(member.Name.Length + upper, member.Name, (data, name) => + return String.Create(member.Name.Length + underscores, member.Name, (data, name) => { - var previousUpper = false; + previousUpper = false; var k = 0; for (var i = 0; i < name.Length; i++) @@ -64,7 +76,12 @@ public static string RenameSnakeCase(MemberInfo member) var c = name[i]; if (char.IsUpper(c)) { - if (i > 0 && !previousUpper) + // Insert underscore if: + // 1. Not at the start (i > 0) + // 2. Either: + // a. Previous char was not uppercase (transition from lowercase to uppercase) + // b. Previous char was uppercase AND next char is lowercase (end of acronym, start of new word) + if (i > 0 && (!previousUpper || (i + 1 < name.Length && char.IsLower(name[i + 1])))) { data[k++] = '_'; } @@ -103,8 +120,7 @@ public static string RenameSnakeCase(MemberInfo member) return string.Empty; StringBuilder result = new StringBuilder(); - bool wasPrevUpper = false; // Track if the previous character was uppercase - int uppercaseCount = 0; // Count consecutive uppercase letters at the start + bool previousUpper = false; for (int i = 0; i < input.Length; i++) { @@ -112,27 +128,23 @@ public static string RenameSnakeCase(MemberInfo member) if (char.IsUpper(c)) { - if (i > 0 && (!wasPrevUpper || (uppercaseCount > 1 && i < input.Length - 1 && char.IsLower(input[i + 1])))) + // Insert underscore if: + // 1. Not at the start (i > 0) + // 2. Either: + // a. Previous char was not uppercase (transition from lowercase to uppercase) + // b. Previous char was uppercase AND next char is lowercase (end of acronym, start of new word) + if (i > 0 && (!previousUpper || (i + 1 < input.Length && char.IsLower(input[i + 1])))) { result.Append('_'); } result.Append(char.ToLower(c)); - wasPrevUpper = true; - uppercaseCount++; + previousUpper = true; } else { - if (c == ' ' || c == '-') - { - result.Append('_'); // Replace spaces and hyphens with underscores - } - else - { - result.Append(c); - } - - wasPrevUpper = false; + result.Append(c); + previousUpper = false; } }