diff --git a/NSString+SA_NSStringExtensions.h b/NSString+SA_NSStringExtensions.h index 7279b98..ea6dd6a 100644 --- a/NSString+SA_NSStringExtensions.h +++ b/NSString+SA_NSStringExtensions.h @@ -1,26 +1,284 @@ // // NSString+SA_NSStringExtensions.h +// RPGBot +// +// Created by Said Achmiz on 12/31/15. // -// Copyright (c) 2016 Said Achmiz. // -// This software is licensed under the MIT license. -// See the file "LICENSE" for more information. #import +/********************************************************/ +#pragma mark - SA_NSStringExtensions category declaration +/********************************************************/ + @interface NSString (SA_NSStringExtensions) -- (BOOL)containsCharactersInSet:(NSCharacterSet *)characters; -- (BOOL)containsCharactersInString:(NSString *)characters; +/******************************/ +#pragma mark - Class properties +/******************************/ -- (NSRange)firstWhitespaceAfterRange:(NSRange)aRange; -- (NSRange)firstNonWhitespaceAfterRange:(NSRange)aRange; +@property (class) BOOL SA_NSStringExtensions_RaiseRegularExpressionCreateException; -- (NSRange)rangeAfterRange:(NSRange)aRange; -- (NSRange)rangeFromEndOfRange:(NSRange)aRange; -- (NSRange)rangeToEndFrom:(NSRange)aRange; +/*************************************/ +#pragma mark - Working with characters +/*************************************/ -- (NSArray *)componentsSplitByWhitespace; -- (NSArray *)componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits; +/* Testing for occurrences of specified characters. + */ +-(BOOL) containsCharactersInSet:(NSCharacterSet *)characters; +-(BOOL) containsCharactersInString:(NSString *)characters; + +/* Removing occurrences of specified characters. + */ +-(NSString *) stringByRemovingCharactersInSet:(NSCharacterSet *)characters; +-(NSString *) stringByRemovingCharactersInString:(NSString *)characters; + +/**********************/ +#pragma mark - Trimming +/**********************/ + +/* “Smart” trimming. + */ +typedef NS_OPTIONS(NSUInteger, SA_NSStringTrimmingOptions) { + SA_NSStringTrimming_CollapseWhitespace = 1 << 0, + SA_NSStringTrimming_TrimWhitespace = 1 << 1, + SA_NSStringTrimming_AppendEllipsis = 1 << 2, + SA_NSStringTrimming_ElideEllipsisWhenEmpty = 1 << 3 +}; +-(NSString *) stringByTrimmingToMaxLengthInBytes:(NSUInteger)maxLengthInBytes + usingEncoding:(NSStringEncoding)encoding + withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions + andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions; + +/******************************************************************************/ +/* Construct trimmed and cleaned string from component dictionary array. + * + * Component dictionary keys: + * + * value + * The string value of the component. + * + * appendFormat (optional) + * A format string, with exactly one format specifier, which must be + * ‘%@’; this will be replaced with the string value. + * + * priority + * Priority. Lower values mean higher priority. Must be an integer. + * (Equal-priority components are NOT guaranteed to be trimmed in + * array order.) + * + * limit (optional) + * Individual length limit for the given component. If present, the + * string value (NOT the full formatted component!) will be trimmed to + * the given length limit *before* the components are formatted and + * combined. + * + * trimBy (optional) + * Wrapped NSStringEnumerationOptions flags value. Specifies the unit + * by which to trim (character, word, etc.) Components without a value + * for this key are not trimmed, only (if need be) removed entirely. + * NOTE: This means that if you specify a value for they ‘limit’ key, + * you MUST specify a value for this key also, otherwise the limit + * will be ignored!) + ******************************************************************************/ ++(instancetype) trimmedStringFromComponents:(NSArray *)components + maxLength:(NSUInteger)maxLengthInBytes + encoding:(NSStringEncoding)encoding + cleanWhitespace:(BOOL)cleanWhitespace; + +/****************************************/ +#pragma mark - Partitioning by whitespace +/****************************************/ + +/* Finding runs of whitespace. + */ +-(NSRange) firstWhitespaceAfterRange:(NSRange)aRange; +-(NSRange) firstNonWhitespaceAfterRange:(NSRange)aRange; +-(NSRange) lastWhitespaceBeforeRange:(NSRange)aRange; +-(NSRange) lastNonWhitespaceBeforeRange:(NSRange)aRange; + +/********************/ +#pragma mark - Ranges +/********************/ + +/* Range manipulation. + */ +-(NSRange) rangeAfterRange:(NSRange)aRange; +-(NSRange) rangeFromEndOfRange:(NSRange)aRange; +-(NSRange) rangeToEndFrom:(NSRange)aRange; + +@property (readonly) NSRange startRange; +@property (readonly) NSRange fullRange; +@property (readonly) NSRange endRange; + +/***********************/ +#pragma mark - Splitting +/***********************/ + +/* Splitting by whitespace. + */ +-(NSArray *) componentsSplitByWhitespace; +-(NSArray *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits; + +/* Splitting by specified delimiter string. + */ +-(NSArray *) componentsSeparatedByString:(NSString *)separator + maxSplits:(NSUInteger)maxSplits; +-(NSArray *) componentsSeparatedByString:(NSString *)separator + dropEmptyString:(BOOL)dropEmptyString; + +/***************************/ +#pragma mark - Byte encoding +/***************************/ + +/* Convenience method; alias for lengthOfBytesUsingEncoding:NSUTF8StringEncoding. + */ +@property (readonly) NSUInteger UTF8length; + +/* Convenience method; alias for dataWithEncoding:NSUTF8StringEncoding. + */ +@property (nonatomic, readonly) NSData *dataAsUTF8; + +/* Convenience method; alias for [[foo alloc] initWithData:data encoding:encoding]. + */ ++(instancetype) stringWithData:(NSData *)data + encoding:(NSStringEncoding)encoding; + +/* Convenience method; alias for stringWithData:data encoding:NSUTF8StringEncoding. + */ ++(instancetype) stringWithUTF8Data:(NSData *)data; + +/*********************/ +#pragma mark - Hashing +/*********************/ + +/* MD5 hashing. + */ +@property (nonatomic, readonly) NSString *MD5Hash; + +/***********************/ +#pragma mark - Sentences +/***********************/ + +/* Get first sentence. + */ +@property (nonatomic, readonly) NSString *firstSentence; + +/*********************/ +#pragma mark - Padding +/*********************/ + +/* Left-pad. + */ +-(NSString *) stringLeftPaddedTo:(int)width; + +/****************************************************/ +#pragma mark - Regular expression convenience methods +/****************************************************/ + +-(NSRegularExpression *) regularExpression; +-(NSRegularExpression *) regularExpressionWithOptions:(NSRegularExpressionOptions)options; + +-(NSArray *) matchesForRegex:(NSRegularExpression *)regex; + +-(NSArray *> *) allMatchesForRegex:(NSRegularExpression *)regex; + +-(NSArray *) matchesForRegexPattern:(NSString *)pattern; +-(NSArray *) matchesForRegexPattern:(NSString *)pattern + options:(NSRegularExpressionOptions)options; + +-(NSArray *> *) allMatchesForRegexPattern:(NSString *)pattern; +-(NSArray *> *) allMatchesForRegexPattern:(NSString *)pattern + options:(NSRegularExpressionOptions)options; + +-(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement; + +-(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; + +-(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement; + +-(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; + +-(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements; + +-(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; @end + +/**************************************************************************/ +#pragma mark - SA_NSStringExtensions category declaration (NSMutableString) +/**************************************************************************/ + +@interface NSMutableString (SA_NSStringExtensions) + +/*************************************/ +#pragma mark - Working with characters +/*************************************/ + +/* Removing occurrences of specified characters. + */ +-(void) removeCharactersInSet:(NSCharacterSet *)characters; +-(void) removeCharactersInString:(NSString *)characters; + +/**********************/ +#pragma mark - Trimming +/**********************/ + +/* “Smart” trimming. + */ +-(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes + usingEncoding:(NSStringEncoding)encoding + withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions + andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions; + +/*********************/ +#pragma mark - Padding +/*********************/ + +/* Left-pad. + */ +-(void) leftPadTo:(int)width; + +/****************************************************/ +#pragma mark - Regular expression convenience methods +/****************************************************/ + +-(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement; + +-(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; + +-(void) replaceAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement; + +-(void) replaceAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)replacement + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; + +-(void) replaceAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements; + +-(void) replaceAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions; + +@end + diff --git a/NSString+SA_NSStringExtensions.m b/NSString+SA_NSStringExtensions.m index 2edf729..cb61876 100644 --- a/NSString+SA_NSStringExtensions.m +++ b/NSString+SA_NSStringExtensions.m @@ -1,103 +1,380 @@ // // NSString+SA_NSStringExtensions.m +// RPGBot +// +// Created by Said Achmiz on 12/31/15. // -// Copyright (c) 2016 Said Achmiz. // -// This software is licensed under the MIT license. -// See the file "LICENSE" for more information. #import "NSString+SA_NSStringExtensions.h" -#import "NSRange-Conventional.h" +#import "NSIndexSet+SA_NSIndexSetExtensions.h" +#import "NSArray+SA_NSArrayExtensions.h" +#import + +static BOOL _SA_NSStringExtensions_RaiseRegularExpressionCreateException = YES; + +/***********************************************************/ +#pragma mark - SA_NSStringExtensions category implementation +/***********************************************************/ @implementation NSString (SA_NSStringExtensions) -- (BOOL)containsCharactersInSet:(NSCharacterSet *)characters -{ +/******************************/ +#pragma mark - Class properties +/******************************/ + ++(void) setSA_NSStringExtensions_RaiseRegularExpressionCreateException:(BOOL)SA_NSStringExtensions_RaiseRegularExpressionCreateException { + _SA_NSStringExtensions_RaiseRegularExpressionCreateException = SA_NSStringExtensions_RaiseRegularExpressionCreateException; +} + ++(BOOL) SA_NSStringExtensions_RaiseRegularExpressionCreateException { + return _SA_NSStringExtensions_RaiseRegularExpressionCreateException; +} + +/*************************************/ +#pragma mark - Working with characters +/*************************************/ + +-(BOOL) containsCharactersInSet:(NSCharacterSet *)characters { NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters]; return rangeOfCharacters.location != NSNotFound; } -- (BOOL)containsCharactersInString:(NSString *)characters -{ +-(BOOL) containsCharactersInString:(NSString *)characters { return [self containsCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]]; } -- (NSRange)firstWhitespaceAfterRange:(NSRange)aRange -{ - NSRange restOfString = NSRangeMake(NSRangeMax(aRange), self.length - NSRangeMax(aRange)); - NSRange firstWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:0 range:restOfString]; +-(NSString *) stringByRemovingCharactersInSet:(NSCharacterSet *)characters { + NSMutableString *workingCopy = [self mutableCopy]; + + [workingCopy removeCharactersInSet:characters]; +// NSRange rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters]; +// while (rangeOfCharacters.location != NSNotFound) { +// [workingCopy replaceCharactersInRange:rangeOfCharacters withString:@""]; +// rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters]; +// } + + return [workingCopy copy]; +} + +-(NSString *) stringByRemovingCharactersInString:(NSString *)characters { + return [self stringByRemovingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]]; +} + +/**********************/ +#pragma mark - Trimming +/**********************/ + +-(NSString *) stringByTrimmingToMaxLengthInBytes:(NSUInteger)maxLengthInBytes + usingEncoding:(NSStringEncoding)encoding + withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions + andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions { + NSMutableString *workingCopy = [self mutableCopy]; + + [workingCopy trimToMaxLengthInBytes:maxLengthInBytes + usingEncoding:encoding + withStringEnumerationOptions:enumerationOptions + andStringTrimmingOptions:trimmingOptions]; + + return [workingCopy copy]; + +// NSString *trimmedString = self; +// +// // Trim whitespace. +// if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace) +// trimmedString = [trimmedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; +// +// // Collapse whitespace. +// if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace) +// trimmedString = [trimmedString stringByReplacingAllOccurrencesOfPattern:@"\\s+" +// withTemplate:@" "]; +// +// // Length of the ellipsis suffix, in bytes. +// NSString *ellipsis = @" …"; +// NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding]; +// +// // Trim (leaving space for ellipsis, if necessary). +// __block NSUInteger cutoffLength = 0; +// [trimmedString enumerateSubstringsInRange:trimmedString.fullRange +// options:(enumerationOptions|NSStringEnumerationSubstringNotRequired) +// usingBlock:^(NSString * _Nullable substring, +// NSRange substringRange, +// NSRange enclosingRange, +// BOOL * _Nonnull stop) { +// NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange); +// NSUInteger endOfEnclosingRangeInBytes = [[trimmedString substringToIndex:endOfEnclosingRange] +// lengthOfBytesUsingEncoding:encoding]; +// +// // If we need to append ellipsis when trimming... +// if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) { +// if ( trimmedString.fullRange.length == endOfEnclosingRange +// && endOfEnclosingRangeInBytes <= maxLengthInBytes) { +// // Either the ellipsis is not needed, because the string is not cut off... +// cutoffLength = endOfEnclosingRange; +// } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) { +// // Or there will still be room for the ellipsis after adding this piece... +// cutoffLength = endOfEnclosingRange; +// } else { +// // Or we don’t add this piece. +// *stop = YES; +// } +// } else { +// if (endOfEnclosingRangeInBytes <= maxLengthInBytes) { +// cutoffLength = endOfEnclosingRange; +// } else { +// *stop = YES; +// } +// } +// }]; +// NSUInteger lengthBeforeTrimming = trimmedString.length; +// trimmedString = [trimmedString substringToIndex:cutoffLength]; +// +// // Append ellipsis. +// if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis +// && cutoffLength < lengthBeforeTrimming +// && maxLengthInBytes >= ellipsisLengthInBytes +// && ( cutoffLength > 0 +// || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty)) +// ) { +// trimmedString = [trimmedString stringByAppendingString:ellipsis]; +// } +// +// return trimmedString; +} + ++(instancetype) trimmedStringFromComponents:(NSArray *)components + maxLength:(NSUInteger)maxLengthInBytes + encoding:(NSStringEncoding)encoding + cleanWhitespace:(BOOL)cleanWhitespace { + SA_NSStringTrimmingOptions trimmingOptions = (cleanWhitespace + ? ( SA_NSStringTrimming_CollapseWhitespace + |SA_NSStringTrimming_TrimWhitespace + |SA_NSStringTrimming_AppendEllipsis) + : SA_NSStringTrimming_AppendEllipsis); + + NSMutableArray *mutableComponents = [components mutableCopy]; + + // Get the formatted version of the component + // (inserting the value into the format string). + NSString *(^formatComponent)(NSDictionary *) = ^NSString *(NSDictionary *component) { + return (component[@"appendFormat"] != nil + ? [NSString stringWithFormat:component[@"appendFormat"], component[@"value"]] + : component[@"value"]); + }; + + // Get the full formatted result. + NSString *(^formatResult)(NSArray *) = ^NSString *(NSArray *componentsArray) { + return [componentsArray reduce:^NSString *(NSString *resultSoFar, + NSDictionary *component) { + return [resultSoFar stringByAppendingString:formatComponent(component)]; + } initial:@""]; + }; + + // Clean and trim (if need be) each component. + [mutableComponents enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component, + NSUInteger idx, + BOOL * _Nonnull stop) { + NSMutableDictionary *adjustedComponent = [component mutableCopy]; + + // Clean whitespace. + if (cleanWhitespace) { + adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByReplacingAllOccurrencesOfPatterns:@[ @"^\\s*(.*?)\\s*$", // Trim whitespace. + @"(\\s*\\n\\s*)+", // Replace newlines with ‘ / ’. + @"\\s+" // Collapse whitespace. + ] + withTemplates:@[ @"$1", + @" / ", + @" " + ]]; + } + + // If component length is individually limited, trim it. + if ( adjustedComponent[@"limit"] != nil + && adjustedComponent[@"trimBy"] != nil) { + adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByTrimmingToMaxLengthInBytes:[adjustedComponent[@"limit"] unsignedIntegerValue] + usingEncoding:encoding + withStringEnumerationOptions:[adjustedComponent[@"trimBy"] unsignedIntegerValue] + andStringTrimmingOptions:trimmingOptions]; + } + + [mutableComponents replaceObjectAtIndex:idx + withObject:adjustedComponent]; + }]; + + // Maybe there’s no length limit? If so, don’t trim; just format and return. + if (maxLengthInBytes == 0) + return formatResult(mutableComponents); + + // Get the total (formatted) length of all the components. + NSUInteger (^getTotalLength)(NSArray *) = ^NSUInteger(NSArray *componentsArray) { + return ((NSNumber *)[componentsArray reduce:^NSNumber *(NSNumber *lengthSoFar, + NSDictionary *component) { + return @(lengthSoFar.unsignedIntegerValue + [formatComponent(component) lengthOfBytesUsingEncoding:encoding]); + } initial:@(0)]).unsignedIntegerValue; + }; + + // The “lowest” priority is actually the highest numeric priority value. + NSUInteger (^getIndexOfLowestPriorityComponent)(NSArray *) = ^NSUInteger(NSArray *componentsArray) { + // By default, return the index of the last component. + __block NSUInteger lowestPriorityComponentIndex = (componentsArray.count - 1); + [componentsArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component, + NSUInteger idx, + BOOL * _Nonnull stop) { + if ([component[@"priority"] unsignedIntegerValue] > [componentsArray[lowestPriorityComponentIndex][@"priority"] unsignedIntegerValue]) + lowestPriorityComponentIndex = idx; + }]; + return lowestPriorityComponentIndex; + }; + + // Keep trimming until we’re below the max length. + NSInteger excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes); + while (excessLength > 0) { + NSUInteger lowestPriorityComponentIndex = getIndexOfLowestPriorityComponent(mutableComponents); + NSDictionary *lowestPriorityComponent = mutableComponents[lowestPriorityComponentIndex]; + NSUInteger lowestPriorityComponentValueLengthInBytes = [lowestPriorityComponent[@"value"] lengthOfBytesUsingEncoding:encoding]; + + if ( lowestPriorityComponent[@"trimBy"] == nil + || lowestPriorityComponentValueLengthInBytes <= (NSUInteger)excessLength) { + [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex]; + } else { + NSMutableDictionary *adjustedComponent = [lowestPriorityComponent mutableCopy]; + adjustedComponent[@"value"] = [lowestPriorityComponent[@"value"] stringByTrimmingToMaxLengthInBytes:(lowestPriorityComponentValueLengthInBytes - (NSUInteger)excessLength) + usingEncoding:encoding + withStringEnumerationOptions:[lowestPriorityComponent[@"trimBy"] unsignedIntegerValue] + andStringTrimmingOptions:trimmingOptions]; + // Check to make sure we haven’t trimmed all the way to nothing! + // (Actually this can’t happen because the SA_NSStringTrimming_ElideEllipsisWhenEmpty + // flag is not set on the trim call above...) + if ([adjustedComponent[@"value"] lengthOfBytesUsingEncoding:encoding] == 0) { + // ... if we have, just remove the component. + [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex]; + } else { + // ... otherwise, update it. + [mutableComponents replaceObjectAtIndex:lowestPriorityComponentIndex + withObject:adjustedComponent]; + } + } + + excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes); + } + + // Trimming is done; return. + return formatResult(mutableComponents); +} + +/****************************************/ +#pragma mark - Partitioning by whitespace +/****************************************/ + +-(NSRange) firstWhitespaceAfterRange:(NSRange)aRange { + NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange)); + NSRange firstWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] + options:(NSStringCompareOptions) 0 + range:restOfString]; return firstWhitespace; } -- (NSRange)firstNonWhitespaceAfterRange:(NSRange)aRange -{ - NSRange restOfString = NSRangeMake(NSRangeMax(aRange), self.length - NSRangeMax(aRange)); - NSRange firstNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet] options:0 range:restOfString]; +-(NSRange) firstNonWhitespaceAfterRange:(NSRange)aRange { + NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange)); + NSRange firstNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet] + options:(NSStringCompareOptions) 0 + range:restOfString]; return firstNonWhitespace; } -- (NSRange)rangeAfterRange:(NSRange)aRange -{ - return NSRangeMake(NSRangeMax(aRange), self.length - NSRangeMax(aRange)); +-(NSRange) lastWhitespaceBeforeRange:(NSRange)aRange { + NSRange stringUntilRange = NSMakeRange(0, aRange.location); + NSRange lastWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] + options:NSBackwardsSearch + range:stringUntilRange]; + return lastWhitespace; } -- (NSRange)rangeFromEndOfRange:(NSRange)aRange -{ - return NSRangeMake(NSRangeMax(aRange) - 1, self.length - NSRangeMax(aRange) + 1); +-(NSRange) lastNonWhitespaceBeforeRange:(NSRange)aRange { + NSRange stringUntilRange = NSMakeRange(0, aRange.location); + NSRange lastNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet] + options:NSBackwardsSearch + range:stringUntilRange]; + return lastNonWhitespace; } -- (NSRange)rangeToEndFrom:(NSRange)aRange -{ - return NSRangeMake(aRange.location, self.length - aRange.location); +/********************/ +#pragma mark - Ranges +/********************/ + +-(NSRange) rangeAfterRange:(NSRange)aRange { + return NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange)); } -- (NSArray *)componentsSplitByWhitespace -{ +-(NSRange) rangeFromEndOfRange:(NSRange)aRange { + return NSMakeRange(NSMaxRange(aRange) - 1, self.length - NSMaxRange(aRange) + 1); +} + +-(NSRange) rangeToEndFrom:(NSRange)aRange { + return NSMakeRange(aRange.location, self.length - aRange.location); +} + +-(NSRange) startRange { + return NSMakeRange(0, 0); +} + +-(NSRange) fullRange { + return NSMakeRange(0, self.length); +} + +-(NSRange) endRange { + return NSMakeRange(self.length, 0); +} + +/***********************/ +#pragma mark - Splitting +/***********************/ + +-(NSArray *) componentsSplitByWhitespace { return [self componentsSplitByWhitespaceWithMaxSplits:NSUIntegerMax]; } -- (NSArray *)componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits -{ - if(maxSplits == 0) - { - return [NSMutableArray arrayWithObject:self]; +-(NSArray *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits { + if (maxSplits == 0) { + return @[ self ]; } - __block NSMutableArray *components = [NSMutableArray array]; + NSMutableArray *components = [NSMutableArray array]; __block NSUInteger tokenStart; __block NSUInteger tokenEnd; __block BOOL currentlyInToken = NO; - __block NSRange tokenRange = NSRangeMake(NSNotFound, 0); + __block NSRange tokenRange = NSMakeRange(NSNotFound, 0); __block NSUInteger splits = 0; - [self enumerateSubstringsInRange:NSRangeMake(0, self.length) + [self enumerateSubstringsInRange:self.fullRange options:NSStringEnumerationByComposedCharacterSequences - usingBlock:^(NSString *character, NSRange characterRange, NSRange enclosingRange, BOOL *stop) - { - if(currentlyInToken == NO && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]]) - { + usingBlock:^(NSString *character, + NSRange characterRange, + NSRange enclosingRange, + BOOL *stop) { + if ( currentlyInToken == NO + && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]] + ) { currentlyInToken = YES; tokenStart = characterRange.location; - } - else if(currentlyInToken == YES && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]) - { + } else if ( currentlyInToken == YES + && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] + ) { currentlyInToken = NO; tokenEnd = characterRange.location; - tokenRange = NSRangeMake(tokenStart, tokenEnd - tokenStart); + tokenRange = NSMakeRange(tokenStart, tokenEnd - tokenStart); [components addObject:[self substringWithRange:tokenRange]]; splits++; - if(splits == maxSplits) - { + if (splits == maxSplits) { *stop = YES; NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]]; - if(lastTokenRange.location != NSNotFound) - { + if (lastTokenRange.location != NSNotFound) { [components addObject:[self substringWithRange:lastTokenRange]]; } } @@ -105,15 +382,456 @@ }]; // If we were in a token when we got to the end, add that last token. - if(splits < maxSplits && currentlyInToken == YES) - { + if ( splits < maxSplits + && currentlyInToken == YES) { tokenEnd = self.length; - tokenRange = NSRangeMake(tokenStart, tokenEnd - tokenStart); + tokenRange = NSMakeRange(tokenStart, + tokenEnd - tokenStart); [components addObject:[self substringWithRange:tokenRange]]; } return components; } +-(NSArray *) componentsSeparatedByString:(NSString *)separator + maxSplits:(NSUInteger)maxSplits { + NSArray *components = [self componentsSeparatedByString:separator]; + if (maxSplits >= (components.count - 1)) + return components; + + return [[components subarrayWithRange:NSMakeRange(0, maxSplits)] + arrayByAddingObject:[[components + subarrayWithRange:NSMakeRange(maxSplits, + components.count - maxSplits)] + componentsJoinedByString:separator]]; +} + +-(NSArray *) componentsSeparatedByString:(NSString *)separator + dropEmptyString:(BOOL)dropEmptyString { + NSMutableArray* components = [[self componentsSeparatedByString:separator] mutableCopy]; + if (dropEmptyString == YES) + [components removeObject:@""]; + return [components copy]; +} + +/***************************/ +#pragma mark - Byte encoding +/***************************/ + +-(NSUInteger) UTF8length { + return [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; +} + +-(NSData *) dataAsUTF8 { + return [self dataUsingEncoding:NSUTF8StringEncoding]; +} + ++(instancetype) stringWithData:(NSData *)data + encoding:(NSStringEncoding)encoding { + return [[self alloc] initWithData:data + encoding:encoding]; +} + ++(instancetype) stringWithUTF8Data:(NSData *)data { + return [self stringWithData:data + encoding:NSUTF8StringEncoding]; +} + +/*********************/ +#pragma mark - Hashing +/*********************/ + +-(NSString *) MD5Hash { + const char *cStr = [self UTF8String]; + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5(cStr, (CC_LONG) strlen(cStr), result); + + return [NSString stringWithFormat: + @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + result[0], result[1], result[2], result[3], + result[4], result[5], result[6], result[7], + result[8], result[9], result[10], result[11], + result[12], result[13], result[14], result[15] +]; +} + +/***********************/ +#pragma mark - Sentences +/***********************/ + +-(NSString *) firstSentence { + __block NSString *firstSentence; + [self enumerateSubstringsInRange:self.fullRange + options:NSStringEnumerationBySentences + usingBlock:^(NSString * _Nullable substring, + NSRange substringRange, + NSRange enclosingRange, + BOOL * _Nonnull stop) { + firstSentence = substring; + *stop = YES; + }]; + return firstSentence; +} + +/*********************/ +#pragma mark - Padding +/*********************/ + +-(NSString *) stringLeftPaddedTo:(int)width { + return [NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]; +} + +/****************************************************/ +#pragma mark - Regular expression convenience methods +/****************************************************/ + +/*********************************************/ +/* Construct regular expressions from strings. + *********************************************/ + +-(NSRegularExpression *) regularExpression { + return [self regularExpressionWithOptions:(NSRegularExpressionOptions) 0]; +} + +-(NSRegularExpression *) regularExpressionWithOptions:(NSRegularExpressionOptions)options { + NSError *error; + NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:self + options:options + error:&error]; + if (error) { + if (NSString.SA_NSStringExtensions_RaiseRegularExpressionCreateException == YES) + [NSException raise:@"SA_NSStringExtensions_RegularExpressionCreateException" + format:@"%@", error.localizedDescription]; + + return nil; + } + + return regexp; +} + +/**********************************************/ +/* Get matches for a regular expression object. + **********************************************/ + +-(NSArray *) matchesForRegex:(NSRegularExpression *)regex { + NSMutableArray *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups]; + [regex enumerateMatchesInString:self + options:(NSMatchingOptions) 0 + range:self.fullRange + usingBlock:^(NSTextCheckingResult * _Nullable result, + NSMatchingFlags flags, + BOOL *stop) { + [NSIndexSet from:0 + for:result.numberOfRanges + do:^(NSUInteger idx) { + NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound + ? @"" + : [self substringWithRange:[result rangeAtIndex:idx]]); + [matches addObject:resultString]; + }]; + + *stop = YES; + }]; + return matches; +} + +-(NSArray *> *) allMatchesForRegex:(NSRegularExpression *)regex { + NSMutableArray *> *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups]; + [NSIndexSet from:0 + for:regex.numberOfCaptureGroups + do:^(NSUInteger idx) { + [matches addObject:[NSMutableArray array]]; + }]; + [regex enumerateMatchesInString:self + options:(NSMatchingOptions) 0 + range:self.fullRange + usingBlock:^(NSTextCheckingResult * _Nullable result, + NSMatchingFlags flags, + BOOL *stop) { + [NSIndexSet from:0 + for:result.numberOfRanges + do:^(NSUInteger idx) { + NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound + ? @"" + : [self substringWithRange:[result rangeAtIndex:idx]]); + [matches[idx] addObject:resultString]; + }]; + }]; + return matches; +} + +/*************************************************************************/ +/* Get matches for a string representing a regular expression (a pattern). + *************************************************************************/ + +-(NSArray *) matchesForRegexPattern:(NSString *)pattern { + return [self matchesForRegex:[pattern regularExpression]]; +} + +-(NSArray *) matchesForRegexPattern:(NSString *)pattern + options:(NSRegularExpressionOptions)options { + return [self matchesForRegex:[pattern regularExpressionWithOptions:options]]; +} + +-(NSArray *> *) allMatchesForRegexPattern:(NSString *)pattern { + return [self allMatchesForRegex:[pattern regularExpression]]; +} + +-(NSArray *> *) allMatchesForRegexPattern:(NSString *)pattern + options:(NSRegularExpressionOptions)options { + return [self allMatchesForRegex:[pattern regularExpressionWithOptions:options]]; +} + +/*******************************************************************************/ +/* Use a pattern (a string representing a regular expression) to do replacement. + *******************************************************************************/ + +-(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)template { + return [self stringByReplacingFirstOccurrenceOfPattern:pattern + withTemplate:template + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)template + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions]; + NSTextCheckingResult *match = [regexp firstMatchInString:self + options:matchingOptions + range:self.fullRange]; + if ( match + && match.range.location != NSNotFound) { + return [self stringByReplacingCharactersInRange:match.range + withString:[regexp replacementStringForResult:match + inString:self + offset:0 + template:template]]; + } else { + return self; + } +} + +-(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)template { + return [self stringByReplacingAllOccurrencesOfPattern:pattern + withTemplate:template + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)template + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + return [[pattern regularExpressionWithOptions:regexpOptions] stringByReplacingMatchesInString:self + options:matchingOptions + range:self.fullRange + withTemplate:template]; +} + +-(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements { + return [self stringByReplacingAllOccurrencesOfPatterns:patterns + withTemplates:replacements + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + NSMutableString *workingCopy = [self mutableCopy]; + + [workingCopy replaceAllOccurrencesOfPatterns:patterns + withTemplates:replacements + regularExpressionOptions:regexpOptions + matchingOptions:matchingOptions]; + + return [workingCopy copy]; +} + +@end + +/*****************************************************************************/ +#pragma mark - SA_NSStringExtensions category implementation (NSMutableString) +/*****************************************************************************/ + +@implementation NSMutableString (SA_NSStringExtensions) + +/*************************************/ +#pragma mark - Working with characters +/*************************************/ + +-(void) removeCharactersInSet:(NSCharacterSet *)characters { + NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters]; + while (rangeOfCharacters.location != NSNotFound) { + [self replaceCharactersInRange:rangeOfCharacters withString:@""]; + rangeOfCharacters = [self rangeOfCharacterFromSet:characters]; + } +} + +-(void) removeCharactersInString:(NSString *)characters { + [self removeCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]]; +} + +/**********************/ +#pragma mark - Trimming +/**********************/ + +-(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes + usingEncoding:(NSStringEncoding)encoding + withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions + andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions { + // Trim whitespace. + if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace) + [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$" + withTemplate:@"$1"]; + + // Collapse whitespace. + if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace) + [self replaceAllOccurrencesOfPattern:@"\\s+" + withTemplate:@" "]; + + // Length of the ellipsis suffix, in bytes. + NSString *ellipsis = @" …"; + NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding]; + + // Trim (leaving space for ellipsis, if necessary). + __block NSUInteger cutoffLength = 0; + [self enumerateSubstringsInRange:self.fullRange + options:(enumerationOptions|NSStringEnumerationSubstringNotRequired) + usingBlock:^(NSString * _Nullable substring, + NSRange substringRange, + NSRange enclosingRange, + BOOL * _Nonnull stop) { + NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange); + NSUInteger endOfEnclosingRangeInBytes = [[self substringToIndex:endOfEnclosingRange] lengthOfBytesUsingEncoding:encoding]; + + // If we need to append ellipsis when trimming... + if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) { + if ( self.fullRange.length == endOfEnclosingRange + && endOfEnclosingRangeInBytes <= maxLengthInBytes) { + // Either the ellipsis is not needed, because the string is not cut off... + cutoffLength = endOfEnclosingRange; + } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) { + // Or there will still be room for the ellipsis after adding this piece... + cutoffLength = endOfEnclosingRange; + } else { + // Or we don’t add this piece. + *stop = YES; + } + } else { + if (endOfEnclosingRangeInBytes <= maxLengthInBytes) { + cutoffLength = endOfEnclosingRange; + } else { + *stop = YES; + } + } + }]; + NSUInteger lengthBeforeTrimming = self.length; + [self deleteCharactersInRange:NSMakeRange(cutoffLength, self.length - cutoffLength)]; + + // Trim whitespace again. + if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace) + [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$" + withTemplate:@"$1"]; + + // Append ellipsis. + if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis + && cutoffLength < lengthBeforeTrimming + && maxLengthInBytes >= ellipsisLengthInBytes + && ( cutoffLength > 0 + || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty)) + ) { + [self appendString:ellipsis]; + } +} + +/*********************/ +#pragma mark - Padding +/*********************/ + +-(void) leftPadTo:(int)width { + [self setString:[NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]]; +} + +/****************************************************/ +#pragma mark - Regular expression convenience methods +/****************************************************/ + +-(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)template { + [self replaceFirstOccurrenceOfPattern:pattern + withTemplate:template + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern + withTemplate:(NSString *)template + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions]; + NSTextCheckingResult *match = [regexp firstMatchInString:self + options:matchingOptions + range:self.fullRange]; + if ( match + && match.range.location != NSNotFound) { + NSString *replacementString = [regexp replacementStringForResult:match + inString:self + offset:0 + template:template]; + [self replaceCharactersInRange:match.range + withString:replacementString]; + } +} + +-(void) replaceAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)template { + [self replaceAllOccurrencesOfPattern:pattern + withTemplate:template + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(void) replaceAllOccurrencesOfPattern:(NSString *)pattern + withTemplate:(NSString *)template + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + [[pattern regularExpressionWithOptions:regexpOptions] replaceMatchesInString:self + options:matchingOptions + range:self.fullRange + withTemplate:template]; +} + +-(void) replaceAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements { + [self replaceAllOccurrencesOfPatterns:patterns + withTemplates:replacements + regularExpressionOptions:(NSRegularExpressionOptions) 0 + matchingOptions:(NSMatchingOptions) 0]; +} + +-(void) replaceAllOccurrencesOfPatterns:(NSArray *)patterns + withTemplates:(NSArray *)replacements + regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions + matchingOptions:(NSMatchingOptions)matchingOptions { + [patterns enumerateObjectsUsingBlock:^(NSString * _Nonnull pattern, + NSUInteger idx, + BOOL * _Nonnull stop) { + NSString *replacement = (replacements.count > idx + ? replacements[idx] + : @""); + [self replaceAllOccurrencesOfPattern:pattern + withTemplate:replacement + regularExpressionOptions:regexpOptions + matchingOptions:matchingOptions]; + }]; +} + @end