Fixed some bugs
[SA_NSStringExtensions.git] / NSString+SA_NSStringExtensions.m
blobee052890142ac9ecce6379ad39ffb5a2d0f9a6d5
1 //
2 //  NSString+SA_NSStringExtensions.m
3 //
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];
54 //      }
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;
116 //                                                                                 } else {
117 //                                                                                         // Or we don’t add this piece.
118 //                                                                                         *stop = YES;
119 //                                                                                 }
120 //                                                                         } else {
121 //                                                                                 if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
122 //                                                                                         cutoffLength = endOfEnclosingRange;
123 //                                                                                 } else {
124 //                                                                                         *stop = YES;
125 //                                                                                 }
126 //                                                                         }
127 //                                                                 }];
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))
137 //              ) {
138 //              trimmedString = [trimmedString stringByAppendingString:ellipsis];
139 //      }
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"]);
162         };
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)];
169                 } initial:@""];
170         };
172         // Clean and trim (if need be) each component.
173         [mutableComponents enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component,
174                                                                                                         NSUInteger idx,
175                                                                                                         BOOL * _Nonnull stop) {
176                 NSMutableDictionary *adjustedComponent = [component mutableCopy];
178                 // Clean whitespace.
179                 if (cleanWhitespace) {
180                         adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByReplacingAllOccurrencesOfPatterns:@[ @"^\\s*(.*?)\\s*$",             // Trim whitespace.
181                                                                                                                                                                                                                                         @"(\\s*\\n\\s*)+",              // Replace newlines with ‘ / ’.
182                                                                                                                                                                                                                                         @"\\s+"                                 // Collapse whitespace.
183                                                                                                                                                                                                                                         ]
184                                                                                                                                                                                                    withTemplates:@[ @"$1",
185                                                                                                                                                                                                                                         @" / ",
186                                                                                                                                                                                                                                         @" "
187                                                                                                                                                                                                                                         ]];
188                 }
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];
197                 }
199                 [mutableComponents replaceObjectAtIndex:idx
200                                                                          withObject:adjustedComponent];
201         }];
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;
213         };
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,
220                                                                                                           NSUInteger idx,
221                                                                                                           BOOL * _Nonnull stop) {
222                         if ([component[@"priority"] unsignedIntegerValue] > [componentsArray[lowestPriorityComponentIndex][@"priority"] unsignedIntegerValue])
223                                 lowestPriorityComponentIndex = idx;
224                 }];
225                 return lowestPriorityComponentIndex;
226         };
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];
238                 } else {
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];
250                         } else {
251                                 // ... otherwise, update it.
252                                 [mutableComponents replaceObjectAtIndex:lowestPriorityComponentIndex
253                                                                                          withObject:adjustedComponent];
254                         }
255                 }
257                 excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes);
258         }
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
272                                                                                                           range:restOfString];
273         
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
281                                                                                                                  range:restOfString];
282         
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.
347         if (maxSplits == 0)
348                 return @[ self ];
350         static NSRegularExpression *regex;
351         static dispatch_once_t onceToken;
352         dispatch_once(&onceToken, ^{
353                 regex = [@"^\\S*|(?<=\\s)$|\\S+" regularExpression];
354         });
356         NSMutableArray <NSString *> *components = [NSMutableArray array];
357         [regex enumerateMatchesInString:self
358                                                         options:(NSMatchingOptions) 0
359                                                           range:self.fullRange
360                                                  usingBlock:^(NSTextCheckingResult * _Nullable result,
361                                                                           NSMatchingFlags flags,
362                                                                           BOOL * _Nonnull stop) {
363                                                          if (   dropEmptyString
364                                                                  && result.range.length == 0) {
365                                                                  // Nothing.
366                                                          } else if (components.count < maxSplits) {
367                                                                  [components addObject:[self substringWithRange:result.range]];
368                                                          } else {
369                                                                  [components addObject:[self substringWithRange:[self rangeToEndFrom:result.range]]];
370                                                                  *stop = YES;
371                                                          }
372                                                  }];
373         return components;
376 //-(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
377 //      if (maxSplits == 0) {
378 //              return @[ self ];
379 //      }
381 //      NSMutableArray <NSString *> *components = [NSMutableArray array];
382 //      
383 //      __block NSUInteger tokenStart;
384 //      __block NSUInteger tokenEnd;
385 //      __block BOOL currentlyInToken = NO;
386 //      __block NSRange tokenRange = NSMakeRange(NSNotFound, 0);
387 //      
388 //      __block NSUInteger splits = 0;
389 //      
390 //      [self enumerateSubstringsInRange:self.fullRange
391 //                                                       options:NSStringEnumerationByComposedCharacterSequences
392 //                                                usingBlock:^(NSString *character,
393 //                                                                         NSRange characterRange,
394 //                                                                         NSRange enclosingRange,
395 //                                                                         BOOL *stop) {
396 //               if (   currentlyInToken == NO
397 //                       && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]]
398 //                       ) {
399 //                       currentlyInToken = YES;
400 //                       tokenStart = characterRange.location;
401 //               } else if (   currentlyInToken == YES
402 //                                      && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
403 //                                      ) {
404 //                       currentlyInToken = NO;
405 //                       tokenEnd = characterRange.location;
406 //                       
407 //                       tokenRange = NSMakeRange(tokenStart,
408 //                                                                        tokenEnd - tokenStart);
409 //                       [components addObject:[self substringWithRange:tokenRange]];
410 //                       splits++;
411 //                       if (splits == maxSplits) {
412 //                               *stop = YES;
413 //                               NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]];
414 //                               if (lastTokenRange.location != NSNotFound) {
415 //                                       [components addObject:[self substringWithRange:lastTokenRange]];
416 //                               }
417 //                       }
418 //               }
419 //       }];
420 //      
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;
425 //         
426 //              tokenRange = NSMakeRange(tokenStart,
427 //                                                               tokenEnd - tokenStart);
428 //              [components addObject:[self substringWithRange:tokenRange]];
429 //      }
430 //      
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))
438                 return components;
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
470                                                          encoding:encoding];
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;
509                                                           *stop = YES;
510                                                   }];
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 {
535         NSError *error;
536         NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:self
537                                                                                                                                                    options:options
538                                                                                                                                                          error:&error];
539         if (error) {
540                 if (NSString.SA_NSStringExtensions_RaiseRegularExpressionCreateException == YES)
541                         [NSException raise:@"SA_NSStringExtensions_RegularExpressionCreateException"
542                                                 format:@"%@", error.localizedDescription];
544                 return nil;
545         }
547         return regex;
550 /**********************************************/
551 /* Get matches for a regular expression object.
552  **********************************************/
554 -(NSArray <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
555         return [self matchesForRegex:regex
556                                                          all:NO];
560 -(NSArray <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
561         return [self matchesForRegex:regex
562                                                          all:YES];
565 /* Helper method (private).
566  */
567 -(NSArray *) matchesForRegex:(NSRegularExpression *)regex
568                                                  all:(BOOL)all {
569         NSMutableArray *matches = [NSMutableArray array];
570         [regex enumerateMatchesInString:self
571                                                         options:(NSMatchingOptions) 0
572                                                           range:self.fullRange
573                                                  usingBlock:^(NSTextCheckingResult * _Nullable result,
574                                                                           NSMatchingFlags flags,
575                                                                           BOOL *stop) {
576                                                          if (all)
577                                                                  [matches addObject:[NSMutableArray array]];
579                                                          [NSIndexSet from:0
580                                                                                   for:result.numberOfRanges
581                                                                                    do:^(NSUInteger idx) {
582                                                                                            NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
583                                                                                                                                                  ? @""
584                                                                                                                                                  : [self substringWithRange:[result rangeAtIndex:idx]]);
585                                                                                            if (all) {
586                                                                                                    [((NSMutableArray *) matches.lastObject) addObject:resultString];
587                                                                                            } else {
588                                                                                                    [matches addObject:resultString];
589                                                                                            }
590                                                                                    }];
592                                                          if (!all)
593                                                                  *stop = YES;
594                                                  }];
595         return matches;
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];
640         if (   match
641                 && match.range.location != NSNotFound) {
642                 return [self stringByReplacingCharactersInRange:match.range
643                                                                                          withString:[regex replacementStringForResult:match
644                                                                                                                                                                  inString:self
645                                                                                                                                                                    offset:0
646                                                                                                                                                                  template:template]];
647         } else {
648                 return self;
649         }
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
666                                                                                                                                                                                         range:self.fullRange
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];
692 @end
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];
709         }
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 {
724         // Trim whitespace.
725         if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
726                 [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
727                                                                 withTemplate:@"$1"];
729         // Collapse whitespace.
730         if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
731                 [self replaceAllOccurrencesOfPattern:@"\\s+"
732                                                                 withTemplate:@" "];
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;
758                                                                   } else {
759                                                                           // Or we don’t add this piece.
760                                                                           *stop = YES;
761                                                                   }
762                                                           } else {
763                                                                   if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
764                                                                           cutoffLength = endOfEnclosingRange;
765                                                                   } else {
766                                                                           *stop = YES;
767                                                                   }
768                                                           }
769                                                   }];
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*$"
776                                                                 withTemplate:@"$1"];
778         // Append ellipsis.
779         if (   trimmingOptions & SA_NSStringTrimming_AppendEllipsis
780                 && cutoffLength < lengthBeforeTrimming
781                 && maxLengthInBytes >= ellipsisLengthInBytes
782                 && (    cutoffLength > 0
783                         || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
784                 ) {
785                 [self appendString:ellipsis];
786         }
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];
817         if (   match
818                 && match.range.location != NSNotFound) {
819                 NSString *replacementString = [regex replacementStringForResult:match
820                                                                                                                            inString:self
821                                                                                                                                  offset:0
822                                                                                                                            template:template];
823                 [self replaceCharactersInRange:match.range
824                                                         withString:replacementString];
825         }
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
842                                                                                                                                                    range:self.fullRange
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,
859                                                                                    NSUInteger idx,
860                                                                                    BOOL * _Nonnull stop) {
861                 NSString *replacement = (replacements.count > idx
862                                                                  ? replacements[idx]
863                                                                  : @"");
864                 [self replaceAllOccurrencesOfPattern:pattern
865                                                                 withTemplate:replacement
866                                         regularExpressionOptions:regexpOptions
867                                                          matchingOptions:matchingOptions];
868         }];
871 @end