| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835 |
- //
- // NSString+SA_NSStringExtensions.m
- //
- // Copyright 2015-2021 Said Achmiz.
- // See LICENSE and README.md for more info.
-
- #import "NSString+SA_NSStringExtensions.h"
-
- #import "NSIndexSet+SA_NSIndexSetExtensions.h"
- #import "NSArray+SA_NSArrayExtensions.h"
- #import <CommonCrypto/CommonDigest.h>
-
- static BOOL _SA_NSStringExtensions_RaiseRegularExpressionCreateException = YES;
-
- /***********************************************************/
- #pragma mark - SA_NSStringExtensions category implementation
- /***********************************************************/
-
- @implementation NSString (SA_NSStringExtensions)
-
- /******************************/
- #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 {
- return [self containsCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
- }
-
- -(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 <NSDictionary *> *)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 <NSDictionary *> *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 <NSDictionary *> *) = ^NSString *(NSArray <NSDictionary *> *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 <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *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 <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *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 = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
- NSRange firstNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
- options:(NSStringCompareOptions) 0
- range:restOfString];
-
- return firstNonWhitespace;
- }
-
- -(NSRange) lastWhitespaceBeforeRange:(NSRange)aRange {
- NSRange stringUntilRange = NSMakeRange(0, aRange.location);
- NSRange lastWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]
- options:NSBackwardsSearch
- range:stringUntilRange];
- return lastWhitespace;
- }
-
- -(NSRange) lastNonWhitespaceBeforeRange:(NSRange)aRange {
- NSRange stringUntilRange = NSMakeRange(0, aRange.location);
- NSRange lastNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
- options:NSBackwardsSearch
- range:stringUntilRange];
- return lastNonWhitespace;
- }
-
- /********************/
- #pragma mark - Ranges
- /********************/
-
- -(NSRange) rangeAfterRange:(NSRange)aRange {
- return NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
- }
-
- -(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 <NSString *> *) componentsSplitByWhitespace {
- return [self componentsSplitByWhitespaceWithMaxSplits:NSUIntegerMax];
- }
-
- -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
- if (maxSplits == 0) {
- return @[ self ];
- }
-
- NSMutableArray <NSString *> *components = [NSMutableArray array];
-
- __block NSUInteger tokenStart;
- __block NSUInteger tokenEnd;
- __block BOOL currentlyInToken = NO;
- __block NSRange tokenRange = NSMakeRange(NSNotFound, 0);
-
- __block NSUInteger splits = 0;
-
- [self enumerateSubstringsInRange:self.fullRange
- options:NSStringEnumerationByComposedCharacterSequences
- 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]]
- ) {
- currentlyInToken = NO;
- tokenEnd = characterRange.location;
-
- tokenRange = NSMakeRange(tokenStart, tokenEnd - tokenStart);
- [components addObject:[self substringWithRange:tokenRange]];
- splits++;
- if (splits == maxSplits) {
- *stop = YES;
- NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]];
- if (lastTokenRange.location != NSNotFound) {
- [components addObject:[self substringWithRange:lastTokenRange]];
- }
- }
- }
- }];
-
- // If we were in a token when we got to the end, add that last token.
- if ( splits < maxSplits
- && currentlyInToken == YES) {
- tokenEnd = self.length;
-
- tokenRange = NSMakeRange(tokenStart,
- tokenEnd - tokenStart);
- [components addObject:[self substringWithRange:tokenRange]];
- }
-
- return components;
- }
-
- -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
- maxSplits:(NSUInteger)maxSplits {
- NSArray <NSString *> *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 <NSString *> *) 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 <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
- NSMutableArray <NSString *> *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 <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
- NSMutableArray <NSMutableArray <NSString *> *> *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 <NSString *> *) matchesForRegexPattern:(NSString *)pattern {
- return [self matchesForRegex:[pattern regularExpression]];
- }
-
- -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern
- options:(NSRegularExpressionOptions)options {
- return [self matchesForRegex:[pattern regularExpressionWithOptions:options]];
- }
-
- -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern {
- return [self allMatchesForRegex:[pattern regularExpression]];
- }
-
- -(NSArray <NSArray <NSString *> *> *) 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 <NSString *> *)patterns
- withTemplates:(NSArray <NSString *> *)replacements {
- return [self stringByReplacingAllOccurrencesOfPatterns:patterns
- withTemplates:replacements
- regularExpressionOptions:(NSRegularExpressionOptions) 0
- matchingOptions:(NSMatchingOptions) 0];
- }
-
- -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
- withTemplates:(NSArray <NSString *> *)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 <NSString *> *)patterns
- withTemplates:(NSArray <NSString *> *)replacements {
- [self replaceAllOccurrencesOfPatterns:patterns
- withTemplates:replacements
- regularExpressionOptions:(NSRegularExpressionOptions) 0
- matchingOptions:(NSMatchingOptions) 0];
- }
-
- -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
- withTemplates:(NSArray <NSString *> *)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
|