tags:

views:

140

answers:

7

I am looking for a way to split PascalCase strings, e.g. "MyString", into separate words - "My", "String". Another user posed the question for bash, but I want to know how to do it with general regular expressions or at least in .NET.

Bonus if you can find a way to also split (and optionally capitalize) camelCase strings: e.g. "myString" becomes "my" and "String", with the option to capitalize/lowercase either or both of the strings.

A: 
var regex = new Regex("([A-Z]+[^A-Z]+)");
var matches = regex.Matches("aCamelCaseWord")
    .Cast<Match>()
    .Select(match => match.Value);
foreach (var element in matches)
{
    Console.WriteLine(element);
}

Prints

Camel
Case
Word

(As you can see, it doesn't handle camelCase - it dropped the leading "a".)

Pat
1) Compile the regexp for some speed. 2) It'll still be slower than doing it by hand.
Steven Sudit
@Steven I agree that it should be compiled for speed, but it's the functionality I'm going after for now. What do you mean it will be "slower than doing it by hand"? If I reflect over an object with a bunch of public properties and convert the names from PascalCase to separate words, it will be much faster (development and maintenance time) doing it programmatically than by hand.
Pat
I didn't see speed mentioned as a requirement. Also I think "doing it by hand" refers to writing your own string parsing code which *may* be faster but *will* be significantly more code and more testing.
Ron Warholic
Where'd the "a" go?
Ken Bloom
@Ken This method doesn't handle camelCase, so the "a" was dropped (see edit to the answer).
Pat
@Pat: what Ron said is correct: "by hand" means writing your own code to loop over the string, character by character, building up each word into a StringBuilder and outputting as needed.
Steven Sudit
A: 

Check that a non-word character comes at the beginning of your regex with \W and keep the individual strings together, then split the words.

Something like: \W([A-Z][A-Za-z]+)+

For: sdcsds sd aCamelCaseWord as dasd as aSscdcacdcdc PascelCase DfsadSsdd sd Outputs:

48: PascelCase
59: DfsadSsdd
Aaron Harun
Hmmm. That doesn't work straight-up for .NET's regex, but maybe with a little documentation digging...
Pat
Updated with an actual working regex.
Aaron Harun
You should use `\b` (word boundary) to match the beginning of the word, not `\W`.
Alan Moore
A: 

In Ruby:

"aCamelCaseWord".split /(?=[[:upper:]])/
=> ["a", "Camel", "Case", "Word"]

I'm using positive lookahead here, so that I can split the string right before each uppercase letter. This lets me save any initial lowercase part as well.

Ken Bloom
That's a positive lookahead, isn't it? I can't get an equivalent to work for .NET, even when I replace `[[:upper:]]` with `[A-Z]` (http://en.wikipedia.org/wiki/Regular_expression).
Pat
.NET regex doesn't support the POSIX character class syntax. You could use `\p{Lu}` instead, but `[A-Z]` will probably suffice. Anyway, this approach is way too simplistic. Check out the other question, especially the `split` regex @poly came up with. It really is that complicated.
Alan Moore
@Pat: that Wikipedia article is not meant to be used as a reference; too general and too theoretical. This site is much more useful: http://www.regular-expressions.info/
Alan Moore
+3  A: 

Answered in a different question:

void Main()
{
    "aCamelCaseWord".ToFriendlyCase().Dump();
}

public static class Extensions
{
    public static string ToFriendlyCase(this string PascalString)
    {
        return Regex.Replace(PascalString, "(?!^)([A-Z])", " $1");
    }
}

Outputs a Camel Case Word (.Dump() just prints to the console).

Pat
What must happen for the strings like this: `aCamelCaseXML`? Reading the question, I would expect `a Camel Case XML`. Instead, it gives `a Camel Case X M L`.
MainMa
@MainMa That's true. Following .NET naming standards, any acronyms three letters or longer (e.g. XML) would be in proper case (i.e. Xml), but two-letter acronyms (e.g. IP for IPAddress) would still cause a problem. It would be better to have the algorithm handle this case.
Pat
Is there any out-the-box funtion that does this?
Shimmy
+6  A: 

See this question: Is there a elegant way to parse a word and add spaces before capital letters? Its accepted answer covers what you want, including numbers and several uppercase letters in a row. While this sample has words starting in uppercase, it it equally valid when the first word is in lowercase.

string[] tests = {
   "AutomaticTrackingSystem",
   "XMLEditor",
   "AnXMLAndXSLT2.0Tool",
};


Regex r = new Regex(
    @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])"
  );

foreach (string s in tests)
  r.Replace(s, " ");

The above will output:

[Automatic][Tracking][System]
[XML][Editor]
[An][XML][And][XSLT][2.0][Tool]
chilltemp
The accepted answer is yet another RegExp-based solution.
Steven Sudit
@Steven Sudit: Yes. RegEx is one of the best tools for this type of problem. The other question is just got flushed out with a larger set of sample use cases.
chilltemp
@chilltemp, do you know of a built-in function for it?
Shimmy
@Shimmy: No. I'd recommend that you use the information in the linked question to create a reusable library.
chilltemp
I made my own function that doesn't use regex.
Shimmy
@Shimmy: Ok, but why?
chilltemp
@chilltemp, I think it costs less performance.If I am wrong correct me and I'll use the regex way.
Shimmy
@Shimmy: Performance varies greatly depending upon many factors including the how complex the RegEx is and if it is compiled. Just like the performance of C# varies depending upon how you use it. That being said, I've always found RegEx in .NET to be fast enough for my needs (real-time transactional system with high throughput). The only ways to really compare is to look at the generated IL and/or do timed test runs.
chilltemp
Agreed. I went to your function. BTW, I edited your answer so users don't have to say "So?".
Shimmy
@Shimmy: Ok, thanks.
chilltemp
+2  A: 

How about:

static IEnumerable<string> SplitPascalCase(this string text)
{
    var sb = new StringBuilder();
    using (var reader = new StringReader(text))
    {
        while (reader.Peek() != -1)
        {
            char c = (char)reader.Read();
            if (char.IsUpper(c) && sb.Length > 0)
            {
                yield return sb.ToString();
                sb.Length = 0;
            }

            sb.Append(c);
        }
    }

    if (sb.Length > 0)
        yield return sb.ToString();
}
Dan Tao
This would be a "by hand" solution.
Steven Sudit
@Steven Sudit: Yeah... was that forbidden or something?
Dan Tao
@Dan: No, no, not at all. There was just some confusion about what "by hand" meant, when I suggested that to Pat as an alternative to RegExp. In fact, I think that RegExp, for all its power, is overused. For many jobs, it's a bad fit, leading to cryptic code and poor performance.
Steven Sudit