Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Fluid.Tests/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
56 changes: 34 additions & 22 deletions Fluid/MemberNameStrategies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,43 @@ 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++)
{
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++] = '_';
}
Expand Down Expand Up @@ -103,36 +120,31 @@ 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++)
{
char c = input[i];

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;
}
}

Expand Down