From fc67f1b69a7101698a93992a9755f2ed0b3b62aa Mon Sep 17 00:00:00 2001 From: Kamil Kaszuba Date: Fri, 31 Oct 2025 06:54:15 +0100 Subject: [PATCH] Fix Critical Round-Trip Conversion Issues --- .../Minerals.StringCases.Tests.csproj | 6 +- .../StringExtensionsTests.cs | 189 +++++++++--------- Minerals.StringCases/StringExtensions.cs | 67 ++++--- 3 files changed, 141 insertions(+), 121 deletions(-) diff --git a/Minerals.StringCases.Tests/Minerals.StringCases.Tests.csproj b/Minerals.StringCases.Tests/Minerals.StringCases.Tests.csproj index 19127cc..9964b02 100644 --- a/Minerals.StringCases.Tests/Minerals.StringCases.Tests.csproj +++ b/Minerals.StringCases.Tests/Minerals.StringCases.Tests.csproj @@ -16,7 +16,11 @@ - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/Minerals.StringCases.Tests/StringExtensionsTests.cs b/Minerals.StringCases.Tests/StringExtensionsTests.cs index 13d1c6b..24bd8e9 100644 --- a/Minerals.StringCases.Tests/StringExtensionsTests.cs +++ b/Minerals.StringCases.Tests/StringExtensionsTests.cs @@ -1,9 +1,8 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; using FluentAssertions; +using Xunit; namespace Minerals.StringCases.Tests { - [TestClass] public class StringExtensionsTests { private const string PascalCase1 = "ExampleVariableName321TestA"; @@ -16,54 +15,6 @@ public class StringExtensionsTests private const string TitleCase1 = "Example Variable Name 321 Test A"; private const string SampleText1 = " _ example Variable - - Name 321 TestA"; - [TestMethod] - public void PascalCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToPascalCase(SampleText1).Should().Be(PascalCase1); - } - - [TestMethod] - public void CamelCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToCamelCase(SampleText1).Should().Be(CamelCase1); - } - - [TestMethod] - public void UnderscoreCamelCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToUnderscoreCamelCase(SampleText1).Should().Be(UnderscoreCamelCase1); - } - - [TestMethod] - public void KebabCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToKebabCase(SampleText1).Should().Be(KebabCase1); - } - - [TestMethod] - public void SnakeCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToSnakeCase(SampleText1).Should().Be(SnakeCase1); - } - - [TestMethod] - public void MacroCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToMacroCase(SampleText1).Should().Be(MacroCase1); - } - - [TestMethod] - public void TrainCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToTrainCase(SampleText1).Should().Be(TrainCase1); - } - - [TestMethod] - public void TitleCase_FromSampleText1() - { - Minerals.StringCases.StringExtensions.ToTitleCase(SampleText1).Should().Be(TitleCase1); - } - private const string PascalCase2 = "ExampleVariableNameAbCd321"; private const string CamelCase2 = "exampleVariableNameAbCd321"; private const string UnderscoreCamelCase2 = "_exampleVariableNameAbCd321"; @@ -74,52 +25,96 @@ public void TitleCase_FromSampleText1() private const string TitleCase2 = "Example Variable Name Ab Cd 321"; private const string SampleText2 = " _ example VARIABLE - - Name AbCd 321"; - [TestMethod] - public void PascalCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToPascalCase(SampleText2).Should().Be(PascalCase2); - } - - [TestMethod] - public void CamelCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToCamelCase(SampleText2).Should().Be(CamelCase2); - } - - [TestMethod] - public void UnderscoreCamelCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToUnderscoreCamelCase(SampleText2).Should().Be(UnderscoreCamelCase2); - } - - [TestMethod] - public void KebabCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToKebabCase(SampleText2).Should().Be(KebabCase2); - } - - [TestMethod] - public void SnakeCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToSnakeCase(SampleText2).Should().Be(SnakeCase2); - } - - [TestMethod] - public void MacroCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToMacroCase(SampleText2).Should().Be(MacroCase2); - } - - [TestMethod] - public void TrainCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToTrainCase(SampleText2).Should().Be(TrainCase2); - } - - [TestMethod] - public void TitleCase_FromSampleText2() - { - Minerals.StringCases.StringExtensions.ToTitleCase(SampleText2).Should().Be(TitleCase2); - } + [Theory] + [InlineData("ToPascalCase", SampleText1, PascalCase1)] + [InlineData("ToPascalCase", SampleText2, PascalCase2)] + [InlineData("ToCamelCase", SampleText1, CamelCase1)] + [InlineData("ToCamelCase", SampleText2, CamelCase2)] + [InlineData("ToUnderscoreCamelCase", SampleText1, UnderscoreCamelCase1)] + [InlineData("ToUnderscoreCamelCase", SampleText2, UnderscoreCamelCase2)] + [InlineData("ToKebabCase", SampleText1, KebabCase1)] + [InlineData("ToKebabCase", SampleText2, KebabCase2)] + [InlineData("ToSnakeCase", SampleText1, SnakeCase1)] + [InlineData("ToSnakeCase", SampleText2, SnakeCase2)] + [InlineData("ToMacroCase", SampleText1, MacroCase1)] + [InlineData("ToMacroCase", SampleText2, MacroCase2)] + [InlineData("ToTrainCase", SampleText1, TrainCase1)] + [InlineData("ToTrainCase", SampleText2, TrainCase2)] + [InlineData("ToTitleCase", SampleText1, TitleCase1)] + [InlineData("ToTitleCase", SampleText2, TitleCase2)] + public void ConversionMethods_ShouldConvertCorrectly(string methodName, string input, string expected) + { + string result = ApplyConversionMethod(input, methodName); + + result.Should().Be(expected); + } + + [Theory] + [InlineData("PascalCase", PascalCase1, "ToKebabCase", "ToPascalCase")] + [InlineData("PascalCase", PascalCase1, "ToSnakeCase", "ToPascalCase")] + [InlineData("PascalCase", PascalCase1, "ToMacroCase", "ToPascalCase")] + [InlineData("PascalCase", PascalCase1, "ToTrainCase", "ToPascalCase")] + [InlineData("CamelCase", CamelCase1, "ToKebabCase", "ToCamelCase")] + [InlineData("CamelCase", CamelCase1, "ToSnakeCase", "ToCamelCase")] + [InlineData("CamelCase", CamelCase1, "ToMacroCase", "ToCamelCase")] + [InlineData("KebabCase", KebabCase1, "ToPascalCase", "ToKebabCase")] + [InlineData("KebabCase", KebabCase1, "ToCamelCase", "ToKebabCase")] + [InlineData("KebabCase", KebabCase1, "ToSnakeCase", "ToKebabCase")] + [InlineData("KebabCase", KebabCase1, "ToMacroCase", "ToKebabCase")] + [InlineData("KebabCase", KebabCase1, "ToTrainCase", "ToKebabCase")] + [InlineData("SnakeCase", SnakeCase1, "ToPascalCase", "ToSnakeCase")] + [InlineData("SnakeCase", SnakeCase1, "ToCamelCase", "ToSnakeCase")] + [InlineData("SnakeCase", SnakeCase1, "ToKebabCase", "ToSnakeCase")] + [InlineData("SnakeCase", SnakeCase1, "ToMacroCase", "ToSnakeCase")] + [InlineData("SnakeCase", SnakeCase1, "ToTrainCase", "ToSnakeCase")] + [InlineData("MacroCase", MacroCase1, "ToPascalCase", "ToMacroCase")] + [InlineData("MacroCase", MacroCase1, "ToCamelCase", "ToMacroCase")] + [InlineData("MacroCase", MacroCase1, "ToKebabCase", "ToMacroCase")] + [InlineData("MacroCase", MacroCase1, "ToSnakeCase", "ToMacroCase")] + [InlineData("MacroCase", MacroCase1, "ToTrainCase", "ToMacroCase")] + [InlineData("TrainCase", TrainCase1, "ToPascalCase", "ToTrainCase")] + [InlineData("TrainCase", TrainCase1, "ToKebabCase", "ToTrainCase")] + [InlineData("TrainCase", TrainCase1, "ToSnakeCase", "ToTrainCase")] + [InlineData("TrainCase", TrainCase1, "ToMacroCase", "ToTrainCase")] + public void RoundTripConversions_ShouldPreserveOriginal(string caseType, string input, string firstMethod, string secondMethod) + { + string intermediate = ApplyConversionMethod(input, firstMethod); + string result = ApplyConversionMethod(intermediate, secondMethod); + + result.Should().Be(input, $"{caseType} -> {firstMethod} -> {secondMethod} should preserve the original value"); + } + + [Fact] + public void RoundTrip_EdgeCase_PascalToTitleToPascal() + { + // This might not work due to space conversion + string result = PascalCase1.ToTitleCase().ToPascalCase(); + // We expect this to still be a valid PascalCase string, but might not match exactly + result.Should().NotBeNullOrEmpty(); + char.IsUpper(result[0]).Should().BeTrue("PascalCase should start with uppercase"); + } + + [Fact] + public void RoundTrip_EdgeCase_CamelToTitleToCamel() + { + // This might not work due to space conversion + string result = CamelCase1.ToTitleCase().ToCamelCase(); + // We expect this to still be a valid camelCase string + result.Should().NotBeNullOrEmpty(); + char.IsLower(result[0]).Should().BeTrue("camelCase should start with lowercase"); + } + + private static string ApplyConversionMethod(string input, string methodName) => methodName switch + { + "ToPascalCase" => input.ToPascalCase(), + "ToCamelCase" => input.ToCamelCase(), + "ToUnderscoreCamelCase" => input.ToUnderscoreCamelCase(), + "ToKebabCase" => input.ToKebabCase(), + "ToSnakeCase" => input.ToSnakeCase(), + "ToMacroCase" => input.ToMacroCase(), + "ToTrainCase" => input.ToTrainCase(), + "ToTitleCase" => input.ToTitleCase(), + _ => throw new ArgumentException($"Unknown method: {methodName}") + }; } } \ No newline at end of file diff --git a/Minerals.StringCases/StringExtensions.cs b/Minerals.StringCases/StringExtensions.cs index c714944..695d18e 100644 --- a/Minerals.StringCases/StringExtensions.cs +++ b/Minerals.StringCases/StringExtensions.cs @@ -1,5 +1,5 @@ -using System.Runtime.CompilerServices; using System.Globalization; +using System.Runtime.CompilerServices; namespace Minerals.StringCases { @@ -16,7 +16,8 @@ public static string ToPascalCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { newString[newIndex] = insertSeparator @@ -26,7 +27,8 @@ public static string ToPascalCase(this string value) newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToCamelCase(this string value) @@ -41,7 +43,8 @@ public static string ToCamelCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { newString[newIndex] = insertSeparator && !isFirstCharacter @@ -52,7 +55,8 @@ public static string ToCamelCase(this string value) newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToUnderscoreCamelCase(this string value) @@ -68,7 +72,8 @@ public static string ToUnderscoreCamelCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { newString[newIndex] = insertSeparator && !isFirstCharacter @@ -79,7 +84,8 @@ public static string ToUnderscoreCamelCase(this string value) newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToKebabCase(this string value) @@ -94,7 +100,8 @@ public static string ToKebabCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { if (insertSeparator && !isFirstCharacter) @@ -102,13 +109,15 @@ public static string ToKebabCase(this string value) newString[newIndex] = '-'; newIndex++; } + newString[newIndex] = char.ToLowerInvariant(value[i]); isFirstCharacter = false; insertSeparator = false; newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToSnakeCase(this string value) @@ -123,7 +132,8 @@ public static string ToSnakeCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { if (insertSeparator && !isFirstCharacter) @@ -131,13 +141,15 @@ public static string ToSnakeCase(this string value) newString[newIndex] = '_'; newIndex++; } + newString[newIndex] = char.ToLowerInvariant(value[i]); isFirstCharacter = false; insertSeparator = false; newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToMacroCase(this string value) @@ -152,7 +164,8 @@ public static string ToMacroCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { if (insertSeparator && !isFirstCharacter) @@ -160,13 +173,15 @@ public static string ToMacroCase(this string value) newString[newIndex] = '_'; newIndex++; } + newString[newIndex] = char.ToUpperInvariant(value[i]); isFirstCharacter = false; insertSeparator = false; newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToTrainCase(this string value) @@ -181,7 +196,8 @@ public static string ToTrainCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { if (insertSeparator && !isFirstCharacter) @@ -189,6 +205,7 @@ public static string ToTrainCase(this string value) newString[newIndex] = '-'; newIndex++; } + newString[newIndex] = insertSeparator ? char.ToUpperInvariant(value[i]) : char.ToLowerInvariant(value[i]); @@ -197,7 +214,8 @@ public static string ToTrainCase(this string value) newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } public static string ToTitleCase(this string value) @@ -212,7 +230,8 @@ public static string ToTitleCase(this string value) { previous = current; current = char.GetUnicodeCategory(value[i]); - insertSeparator = (previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + insertSeparator = (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) || insertSeparator; if (!IsSpecialCharacter(current)) { if (insertSeparator && !isFirstCharacter) @@ -220,6 +239,7 @@ public static string ToTitleCase(this string value) newString[newIndex] = ' '; newIndex++; } + newString[newIndex] = insertSeparator ? char.ToUpperInvariant(value[i]) : char.ToLowerInvariant(value[i]); @@ -228,7 +248,8 @@ public static string ToTitleCase(this string value) newIndex++; } } - return new(newString); + + return new string(newString, 0, newIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -242,7 +263,8 @@ private static int CalculateSpanSizeForKebabOrSnakeCase(string text) { current = char.GetUnicodeCategory(text[i]); skips += IsSpecialCharacter(current) ? 1 : 0; - divs += previous != current && (current is UnicodeCategory.UppercaseLetter || current is UnicodeCategory.DecimalDigitNumber) ? 1 : 0; + divs += (previous != current && current is UnicodeCategory.UppercaseLetter or UnicodeCategory.DecimalDigitNumber) || + (IsSpecialCharacter(previous) && !IsSpecialCharacter(current)) ? 1 : 0; previous = current; } return divs - skips; @@ -262,11 +284,10 @@ private static int CalculateSpanSizeForPascalOrCamelCase(string text) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSpecialCharacter(UnicodeCategory category) - { - return category is not UnicodeCategory.UppercaseLetter + private static bool IsSpecialCharacter(UnicodeCategory category) => + category is not UnicodeCategory.UppercaseLetter and not UnicodeCategory.LowercaseLetter - and not UnicodeCategory.DecimalDigitNumber; - } + and not UnicodeCategory.DecimalDigitNumber + and not UnicodeCategory.OtherPunctuation; } } \ No newline at end of file