Splitting CamelCase

Viewed 51128

This is all asp.net c#.

I have an enum

public enum ControlSelectionType 
{
    NotApplicable = 1,
    SingleSelectRadioButtons = 2,
    SingleSelectDropDownList = 3,
    MultiSelectCheckBox = 4,
    MultiSelectListBox = 5
}

The numerical value of this is stored in my database. I display this value in a datagrid.

<asp:boundcolumn datafield="ControlSelectionTypeId" headertext="Control Type"></asp:boundcolumn>

The ID means nothing to a user so I have changed the boundcolumn to a template column with the following.

<asp:TemplateColumn>
    <ItemTemplate>
        <%# Enum.Parse(typeof(ControlSelectionType), DataBinder.Eval(Container.DataItem, "ControlSelectionTypeId").ToString()).ToString()%>
    </ItemTemplate>
</asp:TemplateColumn>

This is a lot better... However, it would be great if there was a simple function I can put around the Enum to split it by Camel case so that the words wrap nicely in the datagrid.

Note: I am fully aware that there are better ways of doing all this. This screen is purely used internally and I just want a quick hack in place to display it a little better.

15 Answers

Indeed a regex/replace is the way to go as described in the other answer, however this might also be of use to you if you wanted to go a different direction

    using System.ComponentModel;
    using System.Reflection;

...

    public static string GetDescription(System.Enum value)
    {
        FieldInfo fi = value.GetType().GetField(value.ToString());
        DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (attributes.Length > 0)
            return attributes[0].Description;
        else
            return value.ToString();
    }

this will allow you define your Enums as

public enum ControlSelectionType 
{
    [Description("Not Applicable")]
    NotApplicable = 1,
    [Description("Single Select Radio Buttons")]
    SingleSelectRadioButtons = 2,
    [Description("Completely Different Display Text")]
    SingleSelectDropDownList = 3,
}

Taken from

http://www.codeguru.com/forum/archive/index.php/t-412868.html

If C# 3.0 is an option you can use the following one-liner to do the job:


Regex.Matches(YOUR_ENUM_VALUE_NAME, "[A-Z][a-z]+").OfType<Match>().Select(match => match.Value).Aggregate((acc, b) => acc + " " + b).TrimStart(' ');

Using LINQ:

var chars = ControlSelectionType.NotApplicable.ToString().SelectMany((x, i) => i > 0 && char.IsUpper(x) ? new char[] { ' ', x } : new char[] { x });

Console.WriteLine(new string(chars.ToArray()));

And if you don't fancy using regex - try this:

public static string SeperateByCamelCase(this string text, char splitChar = ' ') {

        var output = new StringBuilder();

        for (int i = 0; i < text.Length; i++)
        {
            var c = text[i];

            //if not the first and the char is upper
            if (i > 0 && char.IsUpper(c)) {

                var wasLastLower = char.IsLower(text[i - 1]);

                if (i + 1 < text.Length) //is there a next
                {
                    var isNextUpper = char.IsUpper(text[i + 1]);

                    if (!isNextUpper) //if next is not upper (start of a word).
                    {
                        output.Append(splitChar);
                    }
                    else if (wasLastLower) //last was lower but i'm upper and my next is an upper (start of an achromin). 'abcdHTTP' 'abcd HTTP'
                    {
                        output.Append(splitChar);
                    }
                }
                else
                {
                    //last letter - if its upper and the last letter was lower 'abcd' to 'abcd A'
                    if (wasLastLower)
                    {
                        output.Append(splitChar);
                    }
                }
            }

            output.Append(c);
        }


        return output.ToString();

    }

Passes these tests, it doesn't like numbers but i didn't need it to.

    [TestMethod()]
    public void ToCamelCaseTest()
    {

        var testData = new string[] { "AAACamel", "AAA", "SplitThisByCamel", "AnA", "doesnothing", "a", "A", "aasdasdAAA" };
        var expectedData = new string[] { "AAA Camel", "AAA", "Split This By Camel", "An A", "doesnothing", "a", "A", "aasdasd AAA" };

        for (int i = 0; i < testData.Length; i++)
        {
            var actual = testData[i].SeperateByCamelCase();
            var expected = expectedData[i];
            Assert.AreEqual(actual, expected);
        }

    }

#JustSayNoToRegex

Takes a C# identifier, with uderscores and numbers, and converts it to space-separated string.

public static class StringExtensions
{
    public static string SplitOnCase(this string identifier)
    {
        if (identifier == null || identifier.Length == 0) return string.Empty;
        var sb = new StringBuilder();

        if (identifier.Length == 1) sb.Append(char.ToUpperInvariant(identifier[0]));

        else if (identifier.Length == 2) sb.Append(char.ToUpperInvariant(identifier[0])).Append(identifier[1]);

        else {
            if (identifier[0] != '_') sb.Append(char.ToUpperInvariant(identifier[0]));
            for (int i = 1; i < identifier.Length; i++) {
                var current = identifier[i];
                var previous = identifier[i - 1];

                if (current == '_' && previous == '_') continue;

                else if (current == '_') {
                    sb.Append(' ');
                }

                else if (char.IsLetter(current) && previous == '_') {
                    sb.Append(char.ToUpperInvariant(current));
                }

                else if (char.IsDigit(current) && char.IsLetter(previous)) {
                    sb.Append(' ').Append(current);
                }

                else if (char.IsLetter(current) && char.IsDigit(previous)) {
                    sb.Append(' ').Append(char.ToUpperInvariant(current));
                }

                else if (char.IsUpper(current) && char.IsLower(previous) 
                    && (i < identifier.Length - 1 && char.IsUpper(identifier[i + 1]) || i == identifier.Length - 1)) {
                        sb.Append(' ').Append(current);
                }

                else if (char.IsUpper(current) && i < identifier.Length - 1 && char.IsLower(identifier[i + 1])) {
                    sb.Append(' ').Append(current);
                }

                else {
                    sb.Append(current);
                }
            }
        }
        return sb.ToString();
    }

}

Tests:

[TestFixture]
static class HelpersTests
{
    [Test]
    public static void Basic()
    {
        Assert.AreEqual("Foo", "foo".SplitOnCase());
        Assert.AreEqual("Foo", "_foo".SplitOnCase());
        Assert.AreEqual("Foo", "__foo".SplitOnCase());
        Assert.AreEqual("Foo", "___foo".SplitOnCase());
        Assert.AreEqual("Foo 2", "foo2".SplitOnCase());
        Assert.AreEqual("Foo 23", "foo23".SplitOnCase());
        Assert.AreEqual("Foo 23 A", "foo23A".SplitOnCase());
        Assert.AreEqual("Foo 23 Ab", "foo23Ab".SplitOnCase());
        Assert.AreEqual("Foo 23 Ab", "foo23_ab".SplitOnCase());
        Assert.AreEqual("Foo 23 Ab", "foo23___ab".SplitOnCase());
        Assert.AreEqual("Foo 23", "foo__23".SplitOnCase());
        Assert.AreEqual("Foo Bar", "Foo_bar".SplitOnCase());
        Assert.AreEqual("Foo Bar", "Foo____bar".SplitOnCase());
        Assert.AreEqual("AAA", "AAA".SplitOnCase());
        Assert.AreEqual("Foo A Aa", "fooAAa".SplitOnCase());
        Assert.AreEqual("Foo AAA", "fooAAA".SplitOnCase());
        Assert.AreEqual("Foo Bar", "FooBar".SplitOnCase());
        Assert.AreEqual("Mn M", "MnM".SplitOnCase());
        Assert.AreEqual("AS", "aS".SplitOnCase());
        Assert.AreEqual("As", "as".SplitOnCase());
        Assert.AreEqual("A", "a".SplitOnCase());
        Assert.AreEqual("_", "_".SplitOnCase());

    }
}

Simple version similar to some of the above, but with logic to not auto-insert the separator (which is by default, a space, but can be any char) if there's already one at the current position.

Uses a StringBuilder rather than 'mutating' strings.

public static string SeparateCamelCase(this string value, char separator = ' ') {

    var sb = new StringBuilder();
    var lastChar = separator;

    foreach (var currentChar in value) {

        if (char.IsUpper(currentChar) && lastChar != separator)
            sb.Append(separator);

        sb.Append(currentChar);

        lastChar = currentChar;
    }

    return sb.ToString();
}

Example:

Input  : 'ThisIsATest'
Output : 'This Is A Test'

Input  : 'This IsATest'
Output : 'This Is A Test' (Note: Still only one space between 'This' and 'Is')

Input  : 'ThisIsATest' (with separator '_')
Output : 'This_Is_A_Test'

Try this:

using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        Console
            .WriteLine(
                SeparateByCamelCase("TestString") == "Test String" // True
            );
    }

    public static string SeparateByCamelCase(string str)
    {
        return String.Join(" ", SplitByCamelCase(str));
    }

    public static IEnumerable<string> SplitByCamelCase(string str) 
    {
        if (str.Length == 0) 
            return new List<string>();

        return 
            new List<string> 
            { 
                Head(str) 
            }
            .Concat(
                SplitByCamelCase(
                    Tail(str)
                )
            );
    }

    public static string Head(string str)
    {
        return new String(
                    str
                        .Take(1)
                        .Concat(
                            str
                                .Skip(1)
                                .TakeWhile(IsLower)
                        )
                        .ToArray()
                );
    }

    public static string Tail(string str)
    {
        return new String(
                    str
                        .Skip(
                            Head(str).Length
                        )
                        .ToArray()
                );
    }

    public static bool IsLower(char ch) 
    {
        return ch >= 'a' && ch <= 'z';
    }
}

See sample online

Related