Hello,
Although I am still fairly new to Cocoa, I thought I would try my hand at a live word count for a text view (yes, I am ploughing through some Cocoa books to learn properly

). I am using NSScanner to scan up to the whitespace and newline character set to do this - that part is based on some code I found in the mail archives and works quite nicely (I do need to refine it a little, for instance by using formUnionWithCharacterSet to stop it counting hyphens and so forth as words, but that is fairly straightforward). At first I calculated my word count for the whole text view every time the text was edited using the textDidChange delegate method, updating the count in a text field. This worked fine up to a point, but as you might expect, as soon as large chunks of text were pasted into the text view (ie. 10,000 words+), performance took a major hit because of the scanner having to parse so many characters.
I therefore want to implement a more efficient method, and Chuckit on this forum very helpfully suggested that using "editable blocks" would help optimise a word count. I thought about this, and figured that I could limit the word count to calculating lines that are being edited on by using the following approach:
1) Keep a word count as an instance variable that keep tally of all changes.
2) Whenever text is about to change, count the words in the line(s) containing the text that is about to be changed (using lineRangeForRange) *before* it is changed using textView:shouldChangeTextInRange:replacementString :, and subtract this number from the running total.
3) After the text has changed, count the words in the line(s) that just changed using textDidChange: and add this number to the running total.
I implemented this (code below), and it is almost working perfectly - almost. I can paste in over 100,000 words of text and there is no performance hit, bacause parsing is only ever done on the line currently being worked on (problems would only occur if someone wanted to write a single paragraph of over 10,000 words...). However, my code is still giving me some problems. The main problem is that if I delete blank lines (ie. containing invisible newline characters), the word count seems to go *up*. For instance, if I typed in my text view:
And now
this
The word count gives 3, as it should. But if I then place the cursor at the beginning of "this" and hit backspace, so that the text looks like this:
And now
this
the word count goes *up* to 4. If I then hit backspace again to put it all on one line, like this:
And nowthis
the word count remains at 4. If I then hit space, so that it reads "And now this", the word count goes *up* again to 5. If I then delete the whole line, the word count remains at 2, even though there is nothing there. I sat down and worked all this out on paper, and realised that there is some skewed logic in my code, as in the above case it works like this:
1) Calculate word count of affected line about to be changed = blank line = 0
2) Delete this from running total -> total unchanged
3) Calculate word count of the line containing the text that has replaced it = "this" = 1
4) Add that to the running total -> 1 is added to the total because the "this" line was counted again
Unfortunately, I have been staring at this for two days now and cannot find a solution that works. I tried asking on the Apple mailing list, but didn't have much joy. I know that I somehow need to expand the blocks that are being counted to cover all the affected lines, but just can't seem to find a way of doing it. Has anybody got any suggestions?
If anyone can cast some fresh eyes on this code and point out the errors in my logic I would be very grateful. Sorry for the long mail.
Many thanks,
KB
Code:
/*
// Relevant instance variables:
IBOutlet NSTextField wordCountText; // text field for displaying word count
int words; // word count running total (initialized to 0)
NSRange changedTextRange; // for storing the range of the edited string (initialized to {0,0})
*/
-(unsigned) wordCountForString: (NSString *) textString range: (NSRange) range
{
NSString *lineString = [textString substringWithRange: range];
// WORD COUNT
NSScanner *wordScanner = [NSScanner scannerWithString: lineString];
NSCharacterSet *whiteSpace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
unsigned wordCount = 0;
while ([wordScanner scanUpToCharactersFromSet: whiteSpace intoString: nil])
{
wordCount++;
}
return wordCount;
}
// DELEGATE METHODS
-(BOOL) textView: (NSTextView *) aTextView shouldChangeTextInRange: (NSRange)
affectedCharRange replacementString: (NSString *) replacementString
{
// Get the line that is currently being worked on:
NSRange currLineRange = [[aTextView string] lineRangeForRange: affectedCharRange];
// Now subtract the number of words in that line from the running total:
if (currLineRange.length > 0) // ...but only if there is something to subtract
{
words -= [self wordCountForString: [aTextView string] range: currLineRange];
}
// Now calculate the range of replacementString so that we can calculate the new
// word count of the line after text has changed...
changedTextRange = NSMakeRange (affectedCharRange.location, [replacementString length]);
return YES;
}
-(void) textDidChange: (NSNotification *) notification
{
// Get the line(s) currently being worked on:
NSRange currLineRange = [[mainTextView string] lineRangeForRange: changedTextRange];
// Now add the number of words in the line to the running total:
if (currLineRange.length > 0) // ...but only if there is something to add
{
words += [self wordCountForString: [mainTextView string] range: currLineRange];
}
[wordCountText setIntValue: words];
}