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
33 changes: 33 additions & 0 deletions CaseConverter.Test/CaseConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
39 changes: 30 additions & 9 deletions CaseConverter/CaseConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,32 +126,53 @@ private static bool IsWordBoundary(string input, int previousStart, int start, i
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>A new string with the first character converted to lowercase.</returns>
/// <remarks>
/// 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.
/// </remarks>
[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();
}

/// <summary>
/// Returns a copy of this string with the first character converted to uppercase.
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>A new string with the first character converted to uppercase.</returns>
/// <remarks>
/// The first character is the first code point, for the same reason as in
/// <see cref="ToLowercaseFirstChar(string)"/>.
/// </remarks>
public static string ToUppercaseFirstChar(this string input)
{
Ensure.NotNull(input);
return CollapseSpaces(MapFirstCodePoint(input, static first => first.ToUpperInvariant())).Trim();
}

/// <summary>
/// Applies <paramref name="map"/> to the first code point of <paramref name="input"/>, leaving the rest unchanged.
/// </summary>
/// <param name="input">The string to process.</param>
/// <param name="map">The mapping to apply to the first code point, given as a string of one or two UTF-16 code units.</param>
/// <returns>A new string with the first code point mapped, or <paramref name="input"/> if it is empty.</returns>
private static string MapFirstCodePoint(string input, Func<string, string> 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
}

Expand Down
Loading