diff --git a/CaseConverter.Test/CaseConverterTests.cs b/CaseConverter.Test/CaseConverterTests.cs
index c72694e..217681a 100644
--- a/CaseConverter.Test/CaseConverterTests.cs
+++ b/CaseConverter.Test/CaseConverterTests.cs
@@ -287,6 +287,39 @@ public void IsAllCapsShouldReturnFalseForAnAstralLowercaseLetter()
Assert.IsFalse(result, "A lowercase letter outside the BMP must count as lowercase, not be skipped.");
}
+ [TestMethod]
+ public void ToCamelCaseShouldLowercaseAnAstralFirstLetter()
+ {
+ string input = $"{DeseretCapitalLongI}abc";
+ string result = input.ToCamelCase();
+ Assert.AreEqual($"{DeseretSmallLongI}abc", result, "A first letter outside the BMP must be lowercased like any other.");
+ }
+
+ [TestMethod]
+ public void ToCamelCaseShouldLowercaseAnAstralAllCapsFirstWord()
+ {
+ string input = $"{DeseretCapitalLongI}{DeseretCapitalLongI} foo";
+ string result = input.ToCamelCase();
+ Assert.AreEqual($"{DeseretSmallLongI}{DeseretSmallLongI}Foo", result);
+ Assert.AreEqual("xxFoo", "XX foo".ToCamelCase(), "The BMP analogue this case is matched against.");
+ }
+
+ [TestMethod]
+ public void ToLowercaseFirstCharShouldLowercaseAnAstralFirstLetter()
+ {
+ string input = $"{DeseretCapitalLongI}abc";
+ string result = input.ToLowercaseFirstChar();
+ Assert.AreEqual($"{DeseretSmallLongI}abc", result);
+ }
+
+ [TestMethod]
+ public void ToUppercaseFirstCharShouldUppercaseAnAstralFirstLetter()
+ {
+ string input = $"{DeseretSmallLongI}abc";
+ string result = input.ToUppercaseFirstChar();
+ Assert.AreEqual($"{DeseretCapitalLongI}abc", result);
+ }
+
[TestMethod]
public void ToSnakeCaseShouldStillDropAstralCharactersThatAreNotLetters()
{
diff --git a/CaseConverter/CaseConverter.cs b/CaseConverter/CaseConverter.cs
index 01e9877..ea0bea7 100644
--- a/CaseConverter/CaseConverter.cs
+++ b/CaseConverter/CaseConverter.cs
@@ -126,16 +126,16 @@ private static bool IsWordBoundary(string input, int previousStart, int start, i
///
/// The string to convert.
/// A new string with the first character converted to lowercase.
+ ///
+ /// The first character is the first code point, so a letter outside the Basic Multilingual
+ /// Plane is case-mapped as a whole rather than through its high surrogate alone, which would
+ /// leave it unchanged.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Lowercasing the first character is the point of this method.")]
public static string ToLowercaseFirstChar(this string input)
{
Ensure.NotNull(input);
-#if NETSTANDARD2_0
-#pragma warning disable IDE0057 // Substring cannot be simplified in netstandard2.0
- return CollapseSpaces(input.Length > 0 ? char.ToLowerInvariant(input[0]) + input.Substring(1) : input).Trim();
-#pragma warning restore IDE0057
-#else
- return CollapseSpaces(input.Length > 0 ? char.ToLowerInvariant(input[0]) + input[1..] : input).Trim();
-#endif
+ return CollapseSpaces(MapFirstCodePoint(input, static first => first.ToLowerInvariant())).Trim();
}
///
@@ -143,15 +143,36 @@ public static string ToLowercaseFirstChar(this string input)
///
/// The string to convert.
/// A new string with the first character converted to uppercase.
+ ///
+ /// The first character is the first code point, for the same reason as in
+ /// .
+ ///
public static string ToUppercaseFirstChar(this string input)
{
Ensure.NotNull(input);
+ return CollapseSpaces(MapFirstCodePoint(input, static first => first.ToUpperInvariant())).Trim();
+ }
+
+ ///
+ /// Applies to the first code point of , leaving the rest unchanged.
+ ///
+ /// The string to process.
+ /// The mapping to apply to the first code point, given as a string of one or two UTF-16 code units.
+ /// A new string with the first code point mapped, or if it is empty.
+ private static string MapFirstCodePoint(string input, Func map)
+ {
+ if (input.Length == 0)
+ {
+ return input;
+ }
+
+ int length = CodePointLength(input, 0);
#if NETSTANDARD2_0
#pragma warning disable IDE0057 // Substring cannot be simplified in netstandard2.0
- return CollapseSpaces(input.Length > 0 ? char.ToUpperInvariant(input[0]) + input.Substring(1) : input).Trim();
+ return map(input.Substring(0, length)) + input.Substring(length);
#pragma warning restore IDE0057
#else
- return CollapseSpaces(input.Length > 0 ? char.ToUpperInvariant(input[0]) + input[1..] : input).Trim();
+ return map(input[..length]) + input[length..];
#endif
}