2 // NSString+SA_NSStringExtensions.m
4 // Copyright 2015-2021 Said Achmiz.
5 // See LICENSE and README.md for more info.
7 #import "NSString+SA_NSStringExtensions.h"
9 #import "NSIndexSet+SA_NSIndexSetExtensions.h"
10 #import "NSArray+SA_NSArrayExtensions.h"
11 #import <CommonCrypto/CommonDigest.h>
13 static BOOL _SA_NSStringExtensions_RaiseRegularExpressionCreateException = YES;
15 /***********************************************************/
16 #pragma mark - SA_NSStringExtensions category implementation
17 /***********************************************************/
19 @implementation NSString (SA_NSStringExtensions)
21 /******************************/
22 #pragma mark - Class properties
23 /******************************/
25 +(void) setSA_NSStringExtensions_RaiseRegularExpressionCreateException:(BOOL)SA_NSStringExtensions_RaiseRegularExpressionCreateException {
26 _SA_NSStringExtensions_RaiseRegularExpressionCreateException = SA_NSStringExtensions_RaiseRegularExpressionCreateException;
29 +(BOOL) SA_NSStringExtensions_RaiseRegularExpressionCreateException {
30 return _SA_NSStringExtensions_RaiseRegularExpressionCreateException;
33 /*************************************/
34 #pragma mark - Working with characters
35 /*************************************/
37 -(BOOL) containsCharactersInSet:(NSCharacterSet *)characters {
38 NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
39 return rangeOfCharacters.location != NSNotFound;
42 -(BOOL) containsCharactersInString:(NSString *)characters {
43 return [self containsCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
46 -(NSString *) stringByRemovingCharactersInSet:(NSCharacterSet *)characters {
47 NSMutableString *workingCopy = [self mutableCopy];
49 [workingCopy removeCharactersInSet:characters];
50 // NSRange rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters];
51 // while (rangeOfCharacters.location != NSNotFound) {
52 // [workingCopy replaceCharactersInRange:rangeOfCharacters withString:@""];
53 // rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters];
56 return [workingCopy copy];
59 -(NSString *) stringByRemovingCharactersInString:(NSString *)characters {
60 return [self stringByRemovingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
63 /**********************/
64 #pragma mark - Trimming
65 /**********************/
67 -(NSString *) stringByTrimmingToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
68 usingEncoding:(NSStringEncoding)encoding
69 withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
70 andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
71 NSMutableString *workingCopy = [self mutableCopy];
73 [workingCopy trimToMaxLengthInBytes:maxLengthInBytes
74 usingEncoding:encoding
75 withStringEnumerationOptions:enumerationOptions
76 andStringTrimmingOptions:trimmingOptions];
78 return [workingCopy copy];
80 // NSString *trimmedString = self;
82 // // Trim whitespace.
83 // if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
84 // trimmedString = [trimmedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
86 // // Collapse whitespace.
87 // if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
88 // trimmedString = [trimmedString stringByReplacingAllOccurrencesOfPattern:@"\\s+"
89 // withTemplate:@" "];
91 // // Length of the ellipsis suffix, in bytes.
92 // NSString *ellipsis = @" …";
93 // NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
95 // // Trim (leaving space for ellipsis, if necessary).
96 // __block NSUInteger cutoffLength = 0;
97 // [trimmedString enumerateSubstringsInRange:trimmedString.fullRange
98 // options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
99 // usingBlock:^(NSString * _Nullable substring,
100 // NSRange substringRange,
101 // NSRange enclosingRange,
102 // BOOL * _Nonnull stop) {
103 // NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
104 // NSUInteger endOfEnclosingRangeInBytes = [[trimmedString substringToIndex:endOfEnclosingRange]
105 // lengthOfBytesUsingEncoding:encoding];
107 // // If we need to append ellipsis when trimming...
108 // if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
109 // if ( trimmedString.fullRange.length == endOfEnclosingRange
110 // && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
111 // // Either the ellipsis is not needed, because the string is not cut off...
112 // cutoffLength = endOfEnclosingRange;
113 // } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
114 // // Or there will still be room for the ellipsis after adding this piece...
115 // cutoffLength = endOfEnclosingRange;
117 // // Or we don’t add this piece.
121 // if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
122 // cutoffLength = endOfEnclosingRange;
128 // NSUInteger lengthBeforeTrimming = trimmedString.length;
129 // trimmedString = [trimmedString substringToIndex:cutoffLength];
131 // // Append ellipsis.
132 // if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
133 // && cutoffLength < lengthBeforeTrimming
134 // && maxLengthInBytes >= ellipsisLengthInBytes
135 // && ( cutoffLength > 0
136 // || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
138 // trimmedString = [trimmedString stringByAppendingString:ellipsis];
141 // return trimmedString;
144 +(instancetype) trimmedStringFromComponents:(NSArray <NSDictionary *> *)components
145 maxLength:(NSUInteger)maxLengthInBytes
146 encoding:(NSStringEncoding)encoding
147 cleanWhitespace:(BOOL)cleanWhitespace {
148 SA_NSStringTrimmingOptions trimmingOptions = (cleanWhitespace
149 ? ( SA_NSStringTrimming_CollapseWhitespace
150 |SA_NSStringTrimming_TrimWhitespace
151 |SA_NSStringTrimming_AppendEllipsis)
152 : SA_NSStringTrimming_AppendEllipsis);
154 NSMutableArray <NSDictionary *> *mutableComponents = [components mutableCopy];
156 // Get the formatted version of the component
157 // (inserting the value into the format string).
158 NSString *(^formatComponent)(NSDictionary *) = ^NSString *(NSDictionary *component) {
159 return (component[@"appendFormat"] != nil
160 ? [NSString stringWithFormat:component[@"appendFormat"], component[@"value"]]
161 : component[@"value"]);
164 // Get the full formatted result.
165 NSString *(^formatResult)(NSArray <NSDictionary *> *) = ^NSString *(NSArray <NSDictionary *> *componentsArray) {
166 return [componentsArray reduce:^NSString *(NSString *resultSoFar,
167 NSDictionary *component) {
168 return [resultSoFar stringByAppendingString:formatComponent(component)];
172 // Clean and trim (if need be) each component.
173 [mutableComponents enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component,
175 BOOL * _Nonnull stop) {
176 NSMutableDictionary *adjustedComponent = [component mutableCopy];
179 if (cleanWhitespace) {
180 adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByReplacingAllOccurrencesOfPatterns:@[ @"^\\s*(.*?)\\s*$", // Trim whitespace.
181 @"(\\s*\\n\\s*)+", // Replace newlines with ‘ / ’.
182 @"\\s+" // Collapse whitespace.
184 withTemplates:@[ @"$1",
190 // If component length is individually limited, trim it.
191 if ( adjustedComponent[@"limit"] != nil
192 && adjustedComponent[@"trimBy"] != nil) {
193 adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByTrimmingToMaxLengthInBytes:[adjustedComponent[@"limit"] unsignedIntegerValue]
194 usingEncoding:encoding
195 withStringEnumerationOptions:[adjustedComponent[@"trimBy"] unsignedIntegerValue]
196 andStringTrimmingOptions:trimmingOptions];
199 [mutableComponents replaceObjectAtIndex:idx
200 withObject:adjustedComponent];
203 // Maybe there’s no length limit? If so, don’t trim; just format and return.
204 if (maxLengthInBytes == 0)
205 return formatResult(mutableComponents);
207 // Get the total (formatted) length of all the components.
208 NSUInteger (^getTotalLength)(NSArray <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *componentsArray) {
209 return ((NSNumber *)[componentsArray reduce:^NSNumber *(NSNumber *lengthSoFar,
210 NSDictionary *component) {
211 return @(lengthSoFar.unsignedIntegerValue + [formatComponent(component) lengthOfBytesUsingEncoding:encoding]);
212 } initial:@(0)]).unsignedIntegerValue;
215 // The “lowest” priority is actually the highest numeric priority value.
216 NSUInteger (^getIndexOfLowestPriorityComponent)(NSArray <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *componentsArray) {
217 // By default, return the index of the last component.
218 __block NSUInteger lowestPriorityComponentIndex = (componentsArray.count - 1);
219 [componentsArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component,
221 BOOL * _Nonnull stop) {
222 if ([component[@"priority"] unsignedIntegerValue] > [componentsArray[lowestPriorityComponentIndex][@"priority"] unsignedIntegerValue])
223 lowestPriorityComponentIndex = idx;
225 return lowestPriorityComponentIndex;
228 // Keep trimming until we’re below the max length.
229 NSInteger excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes);
230 while (excessLength > 0) {
231 NSUInteger lowestPriorityComponentIndex = getIndexOfLowestPriorityComponent(mutableComponents);
232 NSDictionary *lowestPriorityComponent = mutableComponents[lowestPriorityComponentIndex];
233 NSUInteger lowestPriorityComponentValueLengthInBytes = [lowestPriorityComponent[@"value"] lengthOfBytesUsingEncoding:encoding];
235 if ( lowestPriorityComponent[@"trimBy"] == nil
236 || lowestPriorityComponentValueLengthInBytes <= (NSUInteger)excessLength) {
237 [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex];
239 NSMutableDictionary *adjustedComponent = [lowestPriorityComponent mutableCopy];
240 adjustedComponent[@"value"] = [lowestPriorityComponent[@"value"] stringByTrimmingToMaxLengthInBytes:(lowestPriorityComponentValueLengthInBytes - (NSUInteger)excessLength)
241 usingEncoding:encoding
242 withStringEnumerationOptions:[lowestPriorityComponent[@"trimBy"] unsignedIntegerValue]
243 andStringTrimmingOptions:trimmingOptions];
244 // Check to make sure we haven’t trimmed all the way to nothing!
245 // (Actually this can’t happen because the SA_NSStringTrimming_ElideEllipsisWhenEmpty
246 // flag is not set on the trim call above...)
247 if ([adjustedComponent[@"value"] lengthOfBytesUsingEncoding:encoding] == 0) {
248 // ... if we have, just remove the component.
249 [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex];
251 // ... otherwise, update it.
252 [mutableComponents replaceObjectAtIndex:lowestPriorityComponentIndex
253 withObject:adjustedComponent];
257 excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes);
260 // Trimming is done; return.
261 return formatResult(mutableComponents);
264 /****************************************/
265 #pragma mark - Partitioning by whitespace
266 /****************************************/
268 -(NSRange) firstWhitespaceAfterRange:(NSRange)aRange {
269 NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
270 NSRange firstWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]
271 options:(NSStringCompareOptions) 0
274 return firstWhitespace;
277 -(NSRange) firstNonWhitespaceAfterRange:(NSRange)aRange {
278 NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
279 NSRange firstNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
280 options:(NSStringCompareOptions) 0
283 return firstNonWhitespace;
286 -(NSRange) lastWhitespaceBeforeRange:(NSRange)aRange {
287 NSRange stringUntilRange = NSMakeRange(0, aRange.location);
288 NSRange lastWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]
289 options:NSBackwardsSearch
290 range:stringUntilRange];
291 return lastWhitespace;
294 -(NSRange) lastNonWhitespaceBeforeRange:(NSRange)aRange {
295 NSRange stringUntilRange = NSMakeRange(0, aRange.location);
296 NSRange lastNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
297 options:NSBackwardsSearch
298 range:stringUntilRange];
299 return lastNonWhitespace;
302 /********************/
303 #pragma mark - Ranges
304 /********************/
306 -(NSRange) rangeAfterRange:(NSRange)aRange {
307 return NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
310 -(NSRange) rangeFromEndOfRange:(NSRange)aRange {
311 return NSMakeRange(NSMaxRange(aRange) - 1, self.length - NSMaxRange(aRange) + 1);
314 -(NSRange) rangeToEndFrom:(NSRange)aRange {
315 return NSMakeRange(aRange.location, self.length - aRange.location);
318 -(NSRange) startRange {
319 return NSMakeRange(0, 0);
322 -(NSRange) fullRange {
323 return NSMakeRange(0, self.length);
326 -(NSRange) endRange {
327 return NSMakeRange(self.length, 0);
330 /***********************/
331 #pragma mark - Splitting
332 /***********************/
334 -(NSArray <NSString *> *) componentsSplitByWhitespace {
335 return [self componentsSplitByWhitespaceWithMaxSplits:NSUIntegerMax
336 dropEmptyString:YES];
339 -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
340 return [self componentsSplitByWhitespaceWithMaxSplits:maxSplits
341 dropEmptyString:YES];
344 -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits
345 dropEmptyString:(BOOL)dropEmptyString {
346 // No need to do anything fancy in this case.
350 static NSRegularExpression *regex;
351 static dispatch_once_t onceToken;
352 dispatch_once(&onceToken, ^{
353 regex = [@"^\\S*|(?<=\\s)$|\\S+" regularExpression];
356 NSMutableArray <NSString *> *components = [NSMutableArray array];
357 [regex enumerateMatchesInString:self
358 options:(NSMatchingOptions) 0
360 usingBlock:^(NSTextCheckingResult * _Nullable result,
361 NSMatchingFlags flags,
362 BOOL * _Nonnull stop) {
364 && result.range.length == 0) {
366 } else if (components.count < maxSplits) {
367 [components addObject:[self substringWithRange:result.range]];
369 [components addObject:[self substringWithRange:[self rangeToEndFrom:result.range]]];
376 //-(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
377 // if (maxSplits == 0) {
381 // NSMutableArray <NSString *> *components = [NSMutableArray array];
383 // __block NSUInteger tokenStart;
384 // __block NSUInteger tokenEnd;
385 // __block BOOL currentlyInToken = NO;
386 // __block NSRange tokenRange = NSMakeRange(NSNotFound, 0);
388 // __block NSUInteger splits = 0;
390 // [self enumerateSubstringsInRange:self.fullRange
391 // options:NSStringEnumerationByComposedCharacterSequences
392 // usingBlock:^(NSString *character,
393 // NSRange characterRange,
394 // NSRange enclosingRange,
396 // if ( currentlyInToken == NO
397 // && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]]
399 // currentlyInToken = YES;
400 // tokenStart = characterRange.location;
401 // } else if ( currentlyInToken == YES
402 // && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
404 // currentlyInToken = NO;
405 // tokenEnd = characterRange.location;
407 // tokenRange = NSMakeRange(tokenStart,
408 // tokenEnd - tokenStart);
409 // [components addObject:[self substringWithRange:tokenRange]];
411 // if (splits == maxSplits) {
413 // NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]];
414 // if (lastTokenRange.location != NSNotFound) {
415 // [components addObject:[self substringWithRange:lastTokenRange]];
421 // // If we were in a token when we got to the end, add that last token.
422 // if ( splits < maxSplits
423 // && currentlyInToken == YES) {
424 // tokenEnd = self.length;
426 // tokenRange = NSMakeRange(tokenStart,
427 // tokenEnd - tokenStart);
428 // [components addObject:[self substringWithRange:tokenRange]];
431 // return components;
434 -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
435 maxSplits:(NSUInteger)maxSplits {
436 NSArray <NSString *> *components = [self componentsSeparatedByString:separator];
437 if (maxSplits >= (components.count - 1))
440 return [[components subarrayWithRange:NSMakeRange(0, maxSplits)]
441 arrayByAddingObject:[[components
442 subarrayWithRange:NSMakeRange(maxSplits,
443 components.count - maxSplits)]
444 componentsJoinedByString:separator]];
447 -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
448 dropEmptyString:(BOOL)dropEmptyString {
449 NSMutableArray* components = [[self componentsSeparatedByString:separator] mutableCopy];
450 if (dropEmptyString == YES)
451 [components removeObject:@""];
452 return [components copy];
455 /***************************/
456 #pragma mark - Byte encoding
457 /***************************/
459 -(NSUInteger) UTF8length {
460 return [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
463 -(NSData *) dataAsUTF8 {
464 return [self dataUsingEncoding:NSUTF8StringEncoding];
467 +(instancetype) stringWithData:(NSData *)data
468 encoding:(NSStringEncoding)encoding {
469 return [[self alloc] initWithData:data
473 +(instancetype) stringWithUTF8Data:(NSData *)data {
474 return [self stringWithData:data
475 encoding:NSUTF8StringEncoding];
478 /*********************/
479 #pragma mark - Hashing
480 /*********************/
482 -(NSString *) MD5Hash {
483 const char *cStr = [self UTF8String];
484 unsigned char result[CC_MD5_DIGEST_LENGTH];
485 CC_MD5(cStr, (CC_LONG) strlen(cStr), result);
487 return [NSString stringWithFormat:
488 @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
489 result[0], result[1], result[2], result[3],
490 result[4], result[5], result[6], result[7],
491 result[8], result[9], result[10], result[11],
492 result[12], result[13], result[14], result[15]
496 /***********************/
497 #pragma mark - Sentences
498 /***********************/
500 -(NSString *) firstSentence {
501 __block NSString *firstSentence;
502 [self enumerateSubstringsInRange:self.fullRange
503 options:NSStringEnumerationBySentences
504 usingBlock:^(NSString * _Nullable substring,
505 NSRange substringRange,
506 NSRange enclosingRange,
507 BOOL * _Nonnull stop) {
508 firstSentence = substring;
511 return firstSentence;
514 /*********************/
515 #pragma mark - Padding
516 /*********************/
518 -(NSString *) stringLeftPaddedTo:(int)width {
519 return [NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes];
522 /****************************************************/
523 #pragma mark - Regular expression convenience methods
524 /****************************************************/
526 /*********************************************/
527 /* Construct regular expressions from strings.
528 *********************************************/
530 -(NSRegularExpression *) regularExpression {
531 return [self regularExpressionWithOptions:(NSRegularExpressionOptions) 0];
534 -(NSRegularExpression *) regularExpressionWithOptions:(NSRegularExpressionOptions)options {
536 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:self
540 if (NSString.SA_NSStringExtensions_RaiseRegularExpressionCreateException == YES)
541 [NSException raise:@"SA_NSStringExtensions_RegularExpressionCreateException"
542 format:@"%@", error.localizedDescription];
550 /**********************************************/
551 /* Get matches for a regular expression object.
552 **********************************************/
554 -(NSArray <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
555 return [self matchesForRegex:regex
560 -(NSArray <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
561 return [self matchesForRegex:regex
565 /* Helper method (private).
567 -(NSArray *) matchesForRegex:(NSRegularExpression *)regex
569 NSMutableArray *matches = [NSMutableArray array];
570 [regex enumerateMatchesInString:self
571 options:(NSMatchingOptions) 0
573 usingBlock:^(NSTextCheckingResult * _Nullable result,
574 NSMatchingFlags flags,
577 [matches addObject:[NSMutableArray array]];
580 for:result.numberOfRanges
581 do:^(NSUInteger idx) {
582 NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
584 : [self substringWithRange:[result rangeAtIndex:idx]]);
586 [((NSMutableArray *) matches.lastObject) addObject:resultString];
588 [matches addObject:resultString];
598 /*************************************************************************/
599 /* Get matches for a string representing a regular expression (a pattern).
600 *************************************************************************/
602 -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern {
603 return [self matchesForRegex:[pattern regularExpression]];
606 -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern
607 options:(NSRegularExpressionOptions)options {
608 return [self matchesForRegex:[pattern regularExpressionWithOptions:options]];
611 -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern {
612 return [self allMatchesForRegex:[pattern regularExpression]];
615 -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern
616 options:(NSRegularExpressionOptions)options {
617 return [self allMatchesForRegex:[pattern regularExpressionWithOptions:options]];
620 /*******************************************************************************/
621 /* Use a pattern (a string representing a regular expression) to do replacement.
622 *******************************************************************************/
624 -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
625 withTemplate:(NSString *)template {
626 return [self stringByReplacingFirstOccurrenceOfPattern:pattern
627 withTemplate:template
628 regularExpressionOptions:(NSRegularExpressionOptions) 0
629 matchingOptions:(NSMatchingOptions) 0];
632 -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
633 withTemplate:(NSString *)template
634 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
635 matchingOptions:(NSMatchingOptions)matchingOptions {
636 NSRegularExpression *regex = [pattern regularExpressionWithOptions:regexpOptions];
637 NSTextCheckingResult *match = [regex firstMatchInString:self
638 options:matchingOptions
639 range:self.fullRange];
641 && match.range.location != NSNotFound) {
642 return [self stringByReplacingCharactersInRange:match.range
643 withString:[regex replacementStringForResult:match
652 -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
653 withTemplate:(NSString *)template {
654 return [self stringByReplacingAllOccurrencesOfPattern:pattern
655 withTemplate:template
656 regularExpressionOptions:(NSRegularExpressionOptions) 0
657 matchingOptions:(NSMatchingOptions) 0];
660 -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
661 withTemplate:(NSString *)template
662 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
663 matchingOptions:(NSMatchingOptions)matchingOptions {
664 return [[pattern regularExpressionWithOptions:regexpOptions] stringByReplacingMatchesInString:self
665 options:matchingOptions
667 withTemplate:template];
670 -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
671 withTemplates:(NSArray <NSString *> *)replacements {
672 return [self stringByReplacingAllOccurrencesOfPatterns:patterns
673 withTemplates:replacements
674 regularExpressionOptions:(NSRegularExpressionOptions) 0
675 matchingOptions:(NSMatchingOptions) 0];
678 -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
679 withTemplates:(NSArray <NSString *> *)replacements
680 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
681 matchingOptions:(NSMatchingOptions)matchingOptions {
682 NSMutableString *workingCopy = [self mutableCopy];
684 [workingCopy replaceAllOccurrencesOfPatterns:patterns
685 withTemplates:replacements
686 regularExpressionOptions:regexpOptions
687 matchingOptions:matchingOptions];
689 return [workingCopy copy];
694 /*****************************************************************************/
695 #pragma mark - SA_NSStringExtensions category implementation (NSMutableString)
696 /*****************************************************************************/
698 @implementation NSMutableString (SA_NSStringExtensions)
700 /*************************************/
701 #pragma mark - Working with characters
702 /*************************************/
704 -(void) removeCharactersInSet:(NSCharacterSet *)characters {
705 NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
706 while (rangeOfCharacters.location != NSNotFound) {
707 [self replaceCharactersInRange:rangeOfCharacters withString:@""];
708 rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
712 -(void) removeCharactersInString:(NSString *)characters {
713 [self removeCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
716 /**********************/
717 #pragma mark - Trimming
718 /**********************/
720 -(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
721 usingEncoding:(NSStringEncoding)encoding
722 withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
723 andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
725 if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
726 [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
729 // Collapse whitespace.
730 if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
731 [self replaceAllOccurrencesOfPattern:@"\\s+"
734 // Length of the ellipsis suffix, in bytes.
735 NSString *ellipsis = @" …";
736 NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
738 // Trim (leaving space for ellipsis, if necessary).
739 __block NSUInteger cutoffLength = 0;
740 [self enumerateSubstringsInRange:self.fullRange
741 options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
742 usingBlock:^(NSString * _Nullable substring,
743 NSRange substringRange,
744 NSRange enclosingRange,
745 BOOL * _Nonnull stop) {
746 NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
747 NSUInteger endOfEnclosingRangeInBytes = [[self substringToIndex:endOfEnclosingRange] lengthOfBytesUsingEncoding:encoding];
749 // If we need to append ellipsis when trimming...
750 if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
751 if ( self.fullRange.length == endOfEnclosingRange
752 && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
753 // Either the ellipsis is not needed, because the string is not cut off...
754 cutoffLength = endOfEnclosingRange;
755 } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
756 // Or there will still be room for the ellipsis after adding this piece...
757 cutoffLength = endOfEnclosingRange;
759 // Or we don’t add this piece.
763 if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
764 cutoffLength = endOfEnclosingRange;
770 NSUInteger lengthBeforeTrimming = self.length;
771 [self deleteCharactersInRange:NSMakeRange(cutoffLength, self.length - cutoffLength)];
773 // Trim whitespace again.
774 if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
775 [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
779 if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
780 && cutoffLength < lengthBeforeTrimming
781 && maxLengthInBytes >= ellipsisLengthInBytes
782 && ( cutoffLength > 0
783 || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
785 [self appendString:ellipsis];
789 /*********************/
790 #pragma mark - Padding
791 /*********************/
793 -(void) leftPadTo:(int)width {
794 [self setString:[NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]];
797 /****************************************************/
798 #pragma mark - Regular expression convenience methods
799 /****************************************************/
801 -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
802 withTemplate:(NSString *)template {
803 [self replaceFirstOccurrenceOfPattern:pattern
804 withTemplate:template
805 regularExpressionOptions:(NSRegularExpressionOptions) 0
806 matchingOptions:(NSMatchingOptions) 0];
809 -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
810 withTemplate:(NSString *)template
811 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
812 matchingOptions:(NSMatchingOptions)matchingOptions {
813 NSRegularExpression *regex = [pattern regularExpressionWithOptions:regexpOptions];
814 NSTextCheckingResult *match = [regex firstMatchInString:self
815 options:matchingOptions
816 range:self.fullRange];
818 && match.range.location != NSNotFound) {
819 NSString *replacementString = [regex replacementStringForResult:match
823 [self replaceCharactersInRange:match.range
824 withString:replacementString];
828 -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
829 withTemplate:(NSString *)template {
830 [self replaceAllOccurrencesOfPattern:pattern
831 withTemplate:template
832 regularExpressionOptions:(NSRegularExpressionOptions) 0
833 matchingOptions:(NSMatchingOptions) 0];
836 -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
837 withTemplate:(NSString *)template
838 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
839 matchingOptions:(NSMatchingOptions)matchingOptions {
840 [[pattern regularExpressionWithOptions:regexpOptions] replaceMatchesInString:self
841 options:matchingOptions
843 withTemplate:template];
846 -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
847 withTemplates:(NSArray <NSString *> *)replacements {
848 [self replaceAllOccurrencesOfPatterns:patterns
849 withTemplates:replacements
850 regularExpressionOptions:(NSRegularExpressionOptions) 0
851 matchingOptions:(NSMatchingOptions) 0];
854 -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
855 withTemplates:(NSArray <NSString *> *)replacements
856 regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
857 matchingOptions:(NSMatchingOptions)matchingOptions {
858 [patterns enumerateObjectsUsingBlock:^(NSString * _Nonnull pattern,
860 BOOL * _Nonnull stop) {
861 NSString *replacement = (replacements.count > idx
864 [self replaceAllOccurrencesOfPattern:pattern
865 withTemplate:replacement
866 regularExpressionOptions:regexpOptions
867 matchingOptions:matchingOptions];