How to remove whitespace from right end of NSString?

20,447

Solution 1

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.

Solution 2

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.

Solution 3

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];

Solution 4

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
}
Share:
20,447
ma11hew28
Author by

ma11hew28

Updated on July 09, 2022

Comments

  • ma11hew28
    ma11hew28 almost 2 years

    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!

  • ma11hew28
    ma11hew28 about 13 years
    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 discourages using the characterAtIndex: method to retrieve each character separately.
  • ma11hew28
    ma11hew28 about 13 years
  • Regexident
    Regexident about 13 years
    @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.
  • ma11hew28
    ma11hew28 about 13 years
    @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. 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.
  • Regexident
    Regexident about 13 years
    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?
  • ma11hew28
    ma11hew28 about 13 years
    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.
  • lnafziger
    lnafziger about 11 years
    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).