You can simply use String.Replace (or StringBuilder.Replace ):
string[] ordinals = { "1St", "2Nd", "3Rd" };
it is not at all elegant. This requires you to have an endless list of ordinals in the first line. I guess someone downvoted you.
This is not elegant, but works better than other simple approaches like regex. You want the headwords in a longer text. But only words that are not ordinals. The sequence number is fe 1, 2 or 3 and 31, but not the 31st. Thus, simple regex calculations will not be fast. You also want to use headings like 10m to 10m (where M can be a million reduction).
Therefore, I do not understand why it is so bad to maintain a list of serial numbers.
You can even generate them automatically with an upper limit, for example:
public static IEnumerable<string> GetTitleCaseOrdinalNumbers() { for (int num = 1; num <= int.MaxValue; num++) { switch (num % 100) { case 11: case 12: case 13: yield return num + "Th"; break; } switch (num % 10) { case 1: yield return num + "St"; break; case 2: yield return num + "Nd"; break; case 3: yield return num + "Rd"; break; default: yield return num + "Th"; break; } } }
So, if you want to check the first 1000 serial numbers:
foreach (string ordinal in GetTitleCaseOrdinalNumbers().Take(1000)) sb.Replace(ordinal, ordinal.ToLowerInvariant());
Update
For what it's worth, here is my attempt to provide an efficient way that really validates words (and not just substrings) and skips ToTitleCase words that really represent ordinal numbers (therefore not 31th , but 31st for example). It also takes care of separator characters that are not white spaces (e.g., periods or commas):
private static readonly char[] separator = { '.', ',', ';', ':', '-', '(', ')', '\\', '{', '}', '[', ']', '/', '\\', '\'', '"', '"', '?', '!', '|' }; public static bool IsOrdinalNumber(string word) { if (word.Any(char.IsWhiteSpace)) return false; // white-spaces are not allowed if (word.Length < 3) return false; var numericPart = word.TakeWhile(char.IsDigit); string numberText = string.Join("", numericPart); if (numberText.Length == 0) return false; int number; if (!int.TryParse(numberText, out number)) return false; // handle unicode digits which are not really numeric like ۵ string ordinalNumber; switch (number % 100) { case 11: case 12: case 13: ordinalNumber = number + "th"; break; } switch (number % 10) { case 1: ordinalNumber = number + "st"; break; case 2: ordinalNumber = number + "nd"; break; case 3: ordinalNumber = number + "rd"; break; default: ordinalNumber = number + "th"; break; } string checkForOrdinalNum = numberText + word.Substring(numberText.Length); return checkForOrdinalNum.Equals(ordinalNumber, StringComparison.CurrentCultureIgnoreCase); } public static string ToTitleCaseIgnoreOrdinalNumbers(string text, TextInfo info) { if(text.Trim().Length < 3) return info.ToTitleCase(text); int whiteSpaceIndex = FindWhiteSpaceIndex(text, 0, separator); if(whiteSpaceIndex == -1) { if(IsOrdinalNumber(text.Trim())) return text; else return info.ToTitleCase(text); } StringBuilder sb = new StringBuilder(); int wordStartIndex = 0; if(whiteSpaceIndex == 0) { // starts with space, find word wordStartIndex = FindNonWhiteSpaceIndex(text, 1, separator); sb.Append(text.Remove(wordStartIndex)); // append leading spaces } while(wordStartIndex >= 0) { whiteSpaceIndex = FindWhiteSpaceIndex(text, wordStartIndex + 1, separator); string word; if(whiteSpaceIndex == -1) word = text.Substring(wordStartIndex); else word = text.Substring(wordStartIndex, whiteSpaceIndex - wordStartIndex); if(IsOrdinalNumber(word)) sb.Append(word); else sb.Append(info.ToTitleCase(word)); wordStartIndex = FindNonWhiteSpaceIndex(text, whiteSpaceIndex + 1, separator); string whiteSpaces; if(wordStartIndex >= 0) whiteSpaces = text.Substring(whiteSpaceIndex, wordStartIndex - whiteSpaceIndex); else whiteSpaces = text.Substring(whiteSpaceIndex); sb.Append(whiteSpaces); // append spaces between words } return sb.ToString(); } public static int FindWhiteSpaceIndex(string text, int startIndex = 0, params char[] separator) { bool checkSeparator = separator != null && separator.Any(); for (int i = startIndex; i < text.Length; i++) { char c = text[i]; if (char.IsWhiteSpace(c) || (checkSeparator && separator.Contains(c))) return i; } return -1; } public static int FindNonWhiteSpaceIndex(string text, int startIndex = 0, params char[] separator) { bool checkSeparator = separator != null && separator.Any(); for (int i = startIndex; i < text.Length; i++) { char c = text[i]; if (!char.IsWhiteSpace(text[i]) && (!checkSeparator || !separator.Contains(c))) return i; } return -1; }
Please note that this has not yet been verified, but should give you an idea.