views:

215

answers:

3

I am writing a text editor and need to provide a live word count. Right now I am using this extension method:

 public static int WordCount(this string s)
    {
        s = s.TrimEnd();
        if (String.IsNullOrEmpty(s)) return 0;
        int count = 0;
        bool lastWasWordChar = false;
        foreach (char c in s)
        {
            if (Char.IsLetterOrDigit(c) || c == '_' || c == '\'' || c == '-')
            {
                lastWasWordChar = true;
                continue;
            }
            if (lastWasWordChar)
            {
                lastWasWordChar = false;
                count++;
            }
        }
        if (!lastWasWordChar) count--;
        return count + 1;
    }

I have it set so that the word count runs on the richtextbox's text every tenth of a second (if the selection start is different from what it was last time the method ran). The problem is that the word count gets slow when working on very long files. To solve this I am thinking about having the word count only run on the current paragraph, recording the word count each time and comparing it against what the word count was last time the word count ran. It would then add the difference between the two to the total word count. Doing this would cause many complications (if the user pastes, if the user deletes a paragraph, ect.) Is this a logical way to go about improving my word count? Or is there something that I don't know about which would make it better?

EDIT: Would it work to run the word count on a different thread? I don't know much about threading, will research.

SAMPLE TEXT THAT I USED:

+8  A: 

You could do a simpler word count based on white-space:

public static int WordCount(this string s)
{
  return s.Split(new char[] {' '}, 
    StringSplitOptions.RemoveEmptyEntries).Length;
}

MSDN provides this example, should give you an accurate word count much faster on large files.

Nick Craver
Thank you for your response, but this method is not accurate enough. Just now I tested both the method in my original post and the method in this post in a document which, according to Microsoft Word, is 2,142 words long. The method in this post counts 2,165 and the method in my original post counts 2,141.
Justin
@highone - Post that sample chunk of text...I'll see what the difference is, it's possible with a slight adjustment we can match it up.
Nick Craver
I had to post the text as an answer since it was too big to fit in a comment. I am new to stackoverflow and am not sure if I should have posted it as a community wiki or not.
Justin
@highone - I updated the answer, I'm getting 2,142 with that method...see it if works for you.
Nick Craver
@highone - I did some benchmarking and this method is minimum twice as fast as any other method. If speed is your main requirement, then this is the method you're going to want to use... +1
John Rasch
@Nick Craver This method is a lot faster than the method I am using right now. However it is still too inaccurate. It sometimes counts punctuation as words. I tested it on an update of the test text and got 2,062 on a file that word says is 1,099 the method I am using right now counted 1,098 (after a slight change). I have updated my question with the updated test text and the update to the method I am using right now.
Justin
@highone - On the text in your current question, I got 2,098 from this method, 2,098 with your method and 2,099 from Word. Can you double check your results? (I did fix the variable in mine though, s != str, woops.)
Nick Craver
@Nick Craver- I apologize, I have no Idea what I was doing wrong. However the problem with punctuation remains. I'm thinking I will try to find a way to do it on a separate thread.
Justin
@highone - Can you give me some punctuation examples that are breaking? There may be an easy fix.
Nick Craver
@Nick Craver- This string returns 4 words "< . . ."" it isn't a huge flaw, but it is a little annoying when it counts non-alphanumeric characters as words.
Justin
@highone - Apologies, thought you wanted the same behavior as microsoft word...you have a list of what you wouldn't count as a word?
Nick Craver
@Nick Craver- I must apologize, I did not realize that microsoft word counted words that way. If it's good enough for Word it's good enough for me. Thank you very much for all your help!
Justin
@highone - Welcome!
Nick Craver
+3  A: 

You could also use a very simple Regex that looks for at least one word character and/or apostrophe to capture the contractions:

public static int WordCount(this string s) 
{
    return Regex.Matches(s, @"[\w']+").Count;
}

This will return 2141 matches (which is actually more correct than Word in this case because Word counts the single asterisk as a word in the sentence "by stabbing a * with her finger").

John Rasch
Unfortunately regex is much slower than the method I am currently using.
Justin
@highone - I pointed this out in the comments of Nick's answer. You have a tradeoff, do you want to sacrifice some speed for accuracy? As I mentioned above, even Word is incorrect when it comes to certain punctuation. It all depends on what you want. Split will always be the fastest method unless it's a new document and you keep track of the word count as the user types.
John Rasch
@John Rasch- What exactly do you mean by keeping track of the word count as the user types?
Justin
A: 

Your method is actually faster than the proposed String.Split method, nearly three times faster on x86 and more than two times faster on x64 in fact. I suspect JIT is messing with your timings, always run your microbenchmarks twice as JIT will occupy the vast majority of the time during your first run. And because String.Split has been NGEN'd, it doesn't need to be compiled to native code and thus will appear to be faster.

Not to mention it's also more accurate, String.Split will count 7 words here:

test :     : this is a test

It also makes sense, String.Split doesn't perform any magic and I would be very surprised if the creation of an array of many strings would be faster than simply iterating over the individual characters in the string. Foreaching over a string has apparently been highly optimized as I tried unsafe pointer arithmetic and it was actually a tiny bit slower than a simple foreach. I really doubt there's any way to do this faster, other than being smart about which sections in your text need word counts.

JulianR