20

This removes white space from both ends of a string:

NSString *newString = [oldString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

How do I remove white space from just the right end of a string?

UPDATE: The code from the accepted answer is now part of SSToolkit. Yay!

Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651

4 Answers4

32

UPDATE: A quick benchmark showed that Matt's own adaption, based on Max' & mine, performs best.

@implementation NSString (TrimmingAdditions)

- (NSString *)stringByTrimmingLeadingCharactersInSet:(NSCharacterSet *)characterSet {
    NSUInteger location = 0;
    NSUInteger length = [self length];
    unichar charBuffer[length];    
    [self getCharacters:charBuffer];

    for (location; location < length; location++) {
        if (![characterSet characterIsMember:charBuffer[location]]) {
            break;
        }
    }

    return [self substringWithRange:NSMakeRange(location, length - location)];
}

- (NSString *)stringByTrimmingTrailingCharactersInSet:(NSCharacterSet *)characterSet {
    NSUInteger location = 0;
    NSUInteger length = [self length];
    unichar charBuffer[length];    
    [self getCharacters:charBuffer];

    for (length; length > 0; length--) {
        if (![characterSet characterIsMember:charBuffer[length - 1]]) {
            break;
        }
    }

    return [self substringWithRange:NSMakeRange(location, length - location)];
}

@end

and then:

NSString *trimmedString = [yourString stringByTrimmingTrailingCharactersInSet:[NSCharacterset whitespaceAndNewlineCharacterSet]];

or for leading whitespace:

NSString *trimmedString = [yourString stringByTrimmingLeadingCharactersInSet:[NSCharacterset whitespaceAndNewlineCharacterSet]];

It's implemented in an abstract fashion so you can use it with any possible NSCharacterSet, whitespaceAndNewlineCharacterSet being just one of them.

For convenience you might want to add these wrapper methods:

- (NSString *)stringByTrimmingLeadingWhitespace {
    return [self stringByTrimmingLeadingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}

- (NSString *)stringByTrimmingTrailingWhitespace {
    return [self stringByTrimmingTrailingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}

- (NSString *)stringByTrimmingLeadingWhitespaceAndNewline {
    return [self stringByTrimmingLeadingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

- (NSString *)stringByTrimmingTrailingWhitespaceAndNewline {
    return [self stringByTrimmingTrailingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

Edit: reverted back to initial version using charBuffer for better performance.

Community
  • 1
  • 1
Regexident
  • 29,441
  • 10
  • 93
  • 100
  • Wow! Thanks! I love the use of the Cocoa category pattern and the modular architecture of this code, but I'm looking for a solution that performs better. The ["Optimize Your Text Manipulations" section of the "Cocoa Performance Guidelines" in the Xcode Documentation](http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CocoaPerformance/Articles/StringDrawing.html#//apple_ref/doc/uid/TP40001445-112378) discourages using the `characterAtIndex:` method to retrieve each character separately. – ma11hew28 Apr 17 '11 at 04:09
  • See this [Stack Overflow answer on the Most efficient way to iterate over all the chars in an NSString](http://stackoverflow.com/questions/4158646/most-efficient-way-to-iterate-over-all-the-chars-in-an-nsstring/5691341#5691341). – ma11hew28 Apr 17 '11 at 09:47
  • @MattDiPasquale: Ironically my first (pre-edited) answer used a `unichar charBuffer`. Dunno why I removed it in favor of `characterAtIndex:`. Just made a quick benchmark and your version is slightly faster than my initial byte buffered version. – Regexident Apr 17 '11 at 14:48
  • @Regexident, nice! Cool to see how to do this with a `char` buffer. 1) Does your code address multibyte text? (See the first comment on this [Stack Overflow answer to the Most efficient way to iterate over all the chars in an NSString](http://j.mp/eWjBnY). 2) For `stringByTrimmingTrailingCharactersInSet:characterSet`, shouldn't `length >= 0` be changed to `length > 0` and `|| length == 0` be removed? Otherwise, `charBuffer[-1]` will be executed when `length == 0`. 3) Instead of `substringWithRange`, I'd use `substringFromIndex` & `substringToIndex`. – ma11hew28 Apr 21 '11 at 04:48
  • You're right about `length > 0`, of course. Fixed. As to whether it supports multibyte strings, I don't know. Would need some testing. Some quick benchmarks showed your code to be superior to mine anyway. Yours won't allocate additional memory, supports multibytes and is actually slightly faster than mine. To be honest I'm actually thinking about deleting my answer as its votes (compared to those of your answer) give the wrong impression about what's the best method. And I assume most people don't read the comments (well, screw them anyway, but still…). Would you support deletion, Matt? – Regexident Apr 21 '11 at 21:03
  • Haha... Thanks for the feedback. I was starting to think that I had egotistically accepted my own answer. I moved the "worth noting" part up to the top of your answer. I think that should be good enough. That way, people can see your contribution and also get a little schooling on how to use a char buffer. But, if you'd prefer to delete it (or edit it in a different way), that's fine by me. Whatever you think's best. – ma11hew28 Jun 19 '11 at 19:42
18

Building on the answers by @Regexident & @Max, I came up with the following methods:

@implementation NSString (SSToolkitAdditions)

#pragma mark Trimming Methods

- (NSString *)stringByTrimmingLeadingCharactersInSet:(NSCharacterSet *)characterSet {
    NSRange rangeOfFirstWantedCharacter = [self rangeOfCharacterFromSet:[characterSet invertedSet]];
    if (rangeOfFirstWantedCharacter.location == NSNotFound) {
        return @"";
    }
    return [self substringFromIndex:rangeOfFirstWantedCharacter.location];
}

- (NSString *)stringByTrimmingLeadingWhitespaceAndNewlineCharacters {
    return [self stringByTrimmingLeadingCharactersInSet:
            [NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

- (NSString *)stringByTrimmingTrailingCharactersInSet:(NSCharacterSet *)characterSet {
    NSRange rangeOfLastWantedCharacter = [self rangeOfCharacterFromSet:[characterSet invertedSet]
                                                               options:NSBackwardsSearch];
    if (rangeOfLastWantedCharacter.location == NSNotFound) {
        return @"";
    }
    return [self substringToIndex:rangeOfLastWantedCharacter.location+1]; // non-inclusive
}

- (NSString *)stringByTrimmingTrailingWhitespaceAndNewlineCharacters {
    return [self stringByTrimmingTrailingCharactersInSet:
            [NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

@end

And here are the GHUnit tests, which all pass of course:

@interface StringCategoryTest : GHTestCase
@end

@implementation StringCategoryTest

- (void)testStringByTrimmingLeadingCharactersInSet {
    NSCharacterSet *letterCharSet = [NSCharacterSet letterCharacterSet];
    GHAssertEqualObjects([@"zip90210zip" stringByTrimmingLeadingCharactersInSet:letterCharSet], @"90210zip", nil);
}

- (void)testStringByTrimmingLeadingWhitespaceAndNewlineCharacters {
    GHAssertEqualObjects([@"" stringByTrimmingLeadingWhitespaceAndNewlineCharacters], @"", nil);
    GHAssertEqualObjects([@"\n \n " stringByTrimmingLeadingWhitespaceAndNewlineCharacters], @"", nil);
    GHAssertEqualObjects([@"\n hello \n" stringByTrimmingLeadingWhitespaceAndNewlineCharacters], @"hello \n", nil);
}

- (void)testStringByTrimmingTrailingCharactersInSet {
    NSCharacterSet *letterCharSet = [NSCharacterSet letterCharacterSet];
    GHAssertEqualObjects([@"zip90210zip" stringByTrimmingTrailingCharactersInSet:letterCharSet], @"zip90210", nil);
}

- (void)testStringByTrimmingTrailingWhitespaceAndNewlineCharacters {
    GHAssertEqualObjects([@"" stringByTrimmingLeadingWhitespaceAndNewlineCharacters], @"", nil);
    GHAssertEqualObjects([@"\n \n " stringByTrimmingLeadingWhitespaceAndNewlineCharacters], @"", nil);
    GHAssertEqualObjects([@"\n hello \n" stringByTrimmingTrailingWhitespaceAndNewlineCharacters], @"\n hello", nil);
}

@end

I submitted a GitHub pull request to SSToolkit with these methods added.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • Your first method fails if the string does not actually start with any of the characters from the set, but they show up later in the string (it will remove characters from the beginning that it shouldn't). For instance, in your first test, if you used "90210zip" as your input, it would return "90210" instead of "90210zip". (Actually, the same is true for your trailing method as well). – lnafziger May 25 '13 at 04:10
7
NSString* str = @"hdskfh   dsakjfh akhf kasdhfk asdfkjash fkadshf1234        ";
NSRange rng = [str rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: [str stringByReplacingOccurrencesOfString: @" " withString: @""]] options: NSBackwardsSearch];
str = [str substringToIndex: rng.location+1];
Max
  • 16,679
  • 4
  • 44
  • 57
1

Needs a slight change to account for the case when last non-trailing character is multibyte:

- (NSString *)stringByTrimmingTrailingCharactersInSet:(NSCharacterSet *)characterSet {
    NSRange rangeOfLastWantedCharacter = [self rangeOfCharacterFromSet:[characterSet invertedSet]
                                                               options:NSBackwardsSearch];
    if (rangeOfLastWantedCharacter.location == NSNotFound) {
        return @"";
    }
    return [self substringToIndex:rangeOfLastWantedCharacter.location + rangeOfLastWantedCharacter.length]; // non-inclusive
}
Jim Conroy
  • 341
  • 4
  • 8