Adds utility methods to NSString, for dealing with whitespace, string splitting, and other things.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

NSString+SA_NSStringExtensions.m 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. //
  2. // NSString+SA_NSStringExtensions.m
  3. //
  4. // Copyright 2015-2021 Said Achmiz.
  5. // See LICENSE and README.md for more info.
  6. #import "NSString+SA_NSStringExtensions.h"
  7. #import "NSIndexSet+SA_NSIndexSetExtensions.h"
  8. #import "NSArray+SA_NSArrayExtensions.h"
  9. #import <CommonCrypto/CommonDigest.h>
  10. static BOOL _SA_NSStringExtensions_RaiseRegularExpressionCreateException = YES;
  11. /***********************************************************/
  12. #pragma mark - SA_NSStringExtensions category implementation
  13. /***********************************************************/
  14. @implementation NSString (SA_NSStringExtensions)
  15. /******************************/
  16. #pragma mark - Class properties
  17. /******************************/
  18. +(void) setSA_NSStringExtensions_RaiseRegularExpressionCreateException:(BOOL)SA_NSStringExtensions_RaiseRegularExpressionCreateException {
  19. _SA_NSStringExtensions_RaiseRegularExpressionCreateException = SA_NSStringExtensions_RaiseRegularExpressionCreateException;
  20. }
  21. +(BOOL) SA_NSStringExtensions_RaiseRegularExpressionCreateException {
  22. return _SA_NSStringExtensions_RaiseRegularExpressionCreateException;
  23. }
  24. /*************************************/
  25. #pragma mark - Working with characters
  26. /*************************************/
  27. -(BOOL) containsCharactersInSet:(NSCharacterSet *)characters {
  28. NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  29. return rangeOfCharacters.location != NSNotFound;
  30. }
  31. -(BOOL) containsCharactersInString:(NSString *)characters {
  32. return [self containsCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
  33. }
  34. -(NSString *) stringByRemovingCharactersInSet:(NSCharacterSet *)characters {
  35. NSMutableString *workingCopy = [self mutableCopy];
  36. [workingCopy removeCharactersInSet:characters];
  37. // NSRange rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters];
  38. // while (rangeOfCharacters.location != NSNotFound) {
  39. // [workingCopy replaceCharactersInRange:rangeOfCharacters withString:@""];
  40. // rangeOfCharacters = [workingCopy rangeOfCharacterFromSet:characters];
  41. // }
  42. return [workingCopy copy];
  43. }
  44. -(NSString *) stringByRemovingCharactersInString:(NSString *)characters {
  45. return [self stringByRemovingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
  46. }
  47. /**********************/
  48. #pragma mark - Trimming
  49. /**********************/
  50. -(NSString *) stringByTrimmingToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
  51. usingEncoding:(NSStringEncoding)encoding
  52. withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
  53. andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
  54. NSMutableString *workingCopy = [self mutableCopy];
  55. [workingCopy trimToMaxLengthInBytes:maxLengthInBytes
  56. usingEncoding:encoding
  57. withStringEnumerationOptions:enumerationOptions
  58. andStringTrimmingOptions:trimmingOptions];
  59. return [workingCopy copy];
  60. // NSString *trimmedString = self;
  61. //
  62. // // Trim whitespace.
  63. // if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  64. // trimmedString = [trimmedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  65. //
  66. // // Collapse whitespace.
  67. // if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
  68. // trimmedString = [trimmedString stringByReplacingAllOccurrencesOfPattern:@"\\s+"
  69. // withTemplate:@" "];
  70. //
  71. // // Length of the ellipsis suffix, in bytes.
  72. // NSString *ellipsis = @" …";
  73. // NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
  74. //
  75. // // Trim (leaving space for ellipsis, if necessary).
  76. // __block NSUInteger cutoffLength = 0;
  77. // [trimmedString enumerateSubstringsInRange:trimmedString.fullRange
  78. // options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
  79. // usingBlock:^(NSString * _Nullable substring,
  80. // NSRange substringRange,
  81. // NSRange enclosingRange,
  82. // BOOL * _Nonnull stop) {
  83. // NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
  84. // NSUInteger endOfEnclosingRangeInBytes = [[trimmedString substringToIndex:endOfEnclosingRange]
  85. // lengthOfBytesUsingEncoding:encoding];
  86. //
  87. // // If we need to append ellipsis when trimming...
  88. // if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
  89. // if ( trimmedString.fullRange.length == endOfEnclosingRange
  90. // && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  91. // // Either the ellipsis is not needed, because the string is not cut off...
  92. // cutoffLength = endOfEnclosingRange;
  93. // } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
  94. // // Or there will still be room for the ellipsis after adding this piece...
  95. // cutoffLength = endOfEnclosingRange;
  96. // } else {
  97. // // Or we don’t add this piece.
  98. // *stop = YES;
  99. // }
  100. // } else {
  101. // if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  102. // cutoffLength = endOfEnclosingRange;
  103. // } else {
  104. // *stop = YES;
  105. // }
  106. // }
  107. // }];
  108. // NSUInteger lengthBeforeTrimming = trimmedString.length;
  109. // trimmedString = [trimmedString substringToIndex:cutoffLength];
  110. //
  111. // // Append ellipsis.
  112. // if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
  113. // && cutoffLength < lengthBeforeTrimming
  114. // && maxLengthInBytes >= ellipsisLengthInBytes
  115. // && ( cutoffLength > 0
  116. // || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
  117. // ) {
  118. // trimmedString = [trimmedString stringByAppendingString:ellipsis];
  119. // }
  120. //
  121. // return trimmedString;
  122. }
  123. +(instancetype) trimmedStringFromComponents:(NSArray <NSDictionary *> *)components
  124. maxLength:(NSUInteger)maxLengthInBytes
  125. encoding:(NSStringEncoding)encoding
  126. cleanWhitespace:(BOOL)cleanWhitespace {
  127. SA_NSStringTrimmingOptions trimmingOptions = (cleanWhitespace
  128. ? ( SA_NSStringTrimming_CollapseWhitespace
  129. |SA_NSStringTrimming_TrimWhitespace
  130. |SA_NSStringTrimming_AppendEllipsis)
  131. : SA_NSStringTrimming_AppendEllipsis);
  132. NSMutableArray <NSDictionary *> *mutableComponents = [components mutableCopy];
  133. // Get the formatted version of the component
  134. // (inserting the value into the format string).
  135. NSString *(^formatComponent)(NSDictionary *) = ^NSString *(NSDictionary *component) {
  136. return (component[@"appendFormat"] != nil
  137. ? [NSString stringWithFormat:component[@"appendFormat"], component[@"value"]]
  138. : component[@"value"]);
  139. };
  140. // Get the full formatted result.
  141. NSString *(^formatResult)(NSArray <NSDictionary *> *) = ^NSString *(NSArray <NSDictionary *> *componentsArray) {
  142. return [componentsArray reduce:^NSString *(NSString *resultSoFar,
  143. NSDictionary *component) {
  144. return [resultSoFar stringByAppendingString:formatComponent(component)];
  145. } initial:@""];
  146. };
  147. // Clean and trim (if need be) each component.
  148. [mutableComponents enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component,
  149. NSUInteger idx,
  150. BOOL * _Nonnull stop) {
  151. NSMutableDictionary *adjustedComponent = [component mutableCopy];
  152. // Clean whitespace.
  153. if (cleanWhitespace) {
  154. adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByReplacingAllOccurrencesOfPatterns:@[ @"^\\s*(.*?)\\s*$", // Trim whitespace.
  155. @"(\\s*\\n\\s*)+", // Replace newlines with ‘ / ’.
  156. @"\\s+" // Collapse whitespace.
  157. ]
  158. withTemplates:@[ @"$1",
  159. @" / ",
  160. @" "
  161. ]];
  162. }
  163. // If component length is individually limited, trim it.
  164. if ( adjustedComponent[@"limit"] != nil
  165. && adjustedComponent[@"trimBy"] != nil) {
  166. adjustedComponent[@"value"] = [adjustedComponent[@"value"] stringByTrimmingToMaxLengthInBytes:[adjustedComponent[@"limit"] unsignedIntegerValue]
  167. usingEncoding:encoding
  168. withStringEnumerationOptions:[adjustedComponent[@"trimBy"] unsignedIntegerValue]
  169. andStringTrimmingOptions:trimmingOptions];
  170. }
  171. [mutableComponents replaceObjectAtIndex:idx
  172. withObject:adjustedComponent];
  173. }];
  174. // Maybe there’s no length limit? If so, don’t trim; just format and return.
  175. if (maxLengthInBytes == 0)
  176. return formatResult(mutableComponents);
  177. // Get the total (formatted) length of all the components.
  178. NSUInteger (^getTotalLength)(NSArray <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *componentsArray) {
  179. return ((NSNumber *)[componentsArray reduce:^NSNumber *(NSNumber *lengthSoFar,
  180. NSDictionary *component) {
  181. return @(lengthSoFar.unsignedIntegerValue + [formatComponent(component) lengthOfBytesUsingEncoding:encoding]);
  182. } initial:@(0)]).unsignedIntegerValue;
  183. };
  184. // The “lowest” priority is actually the highest numeric priority value.
  185. NSUInteger (^getIndexOfLowestPriorityComponent)(NSArray <NSDictionary *> *) = ^NSUInteger(NSArray <NSDictionary *> *componentsArray) {
  186. // By default, return the index of the last component.
  187. __block NSUInteger lowestPriorityComponentIndex = (componentsArray.count - 1);
  188. [componentsArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull component,
  189. NSUInteger idx,
  190. BOOL * _Nonnull stop) {
  191. if ([component[@"priority"] unsignedIntegerValue] > [componentsArray[lowestPriorityComponentIndex][@"priority"] unsignedIntegerValue])
  192. lowestPriorityComponentIndex = idx;
  193. }];
  194. return lowestPriorityComponentIndex;
  195. };
  196. // Keep trimming until we’re below the max length.
  197. NSInteger excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes);
  198. while (excessLength > 0) {
  199. NSUInteger lowestPriorityComponentIndex = getIndexOfLowestPriorityComponent(mutableComponents);
  200. NSDictionary *lowestPriorityComponent = mutableComponents[lowestPriorityComponentIndex];
  201. NSUInteger lowestPriorityComponentValueLengthInBytes = [lowestPriorityComponent[@"value"] lengthOfBytesUsingEncoding:encoding];
  202. if ( lowestPriorityComponent[@"trimBy"] == nil
  203. || lowestPriorityComponentValueLengthInBytes <= (NSUInteger)excessLength) {
  204. [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex];
  205. } else {
  206. NSMutableDictionary *adjustedComponent = [lowestPriorityComponent mutableCopy];
  207. adjustedComponent[@"value"] = [lowestPriorityComponent[@"value"] stringByTrimmingToMaxLengthInBytes:(lowestPriorityComponentValueLengthInBytes - (NSUInteger)excessLength)
  208. usingEncoding:encoding
  209. withStringEnumerationOptions:[lowestPriorityComponent[@"trimBy"] unsignedIntegerValue]
  210. andStringTrimmingOptions:trimmingOptions];
  211. // Check to make sure we haven’t trimmed all the way to nothing!
  212. // (Actually this can’t happen because the SA_NSStringTrimming_ElideEllipsisWhenEmpty
  213. // flag is not set on the trim call above...)
  214. if ([adjustedComponent[@"value"] lengthOfBytesUsingEncoding:encoding] == 0) {
  215. // ... if we have, just remove the component.
  216. [mutableComponents removeObjectAtIndex:lowestPriorityComponentIndex];
  217. } else {
  218. // ... otherwise, update it.
  219. [mutableComponents replaceObjectAtIndex:lowestPriorityComponentIndex
  220. withObject:adjustedComponent];
  221. }
  222. }
  223. excessLength = (NSInteger)(getTotalLength(mutableComponents) - maxLengthInBytes);
  224. }
  225. // Trimming is done; return.
  226. return formatResult(mutableComponents);
  227. }
  228. /****************************************/
  229. #pragma mark - Partitioning by whitespace
  230. /****************************************/
  231. -(NSRange) firstWhitespaceAfterRange:(NSRange)aRange {
  232. NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
  233. NSRange firstWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]
  234. options:(NSStringCompareOptions) 0
  235. range:restOfString];
  236. return firstWhitespace;
  237. }
  238. -(NSRange) firstNonWhitespaceAfterRange:(NSRange)aRange {
  239. NSRange restOfString = NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
  240. NSRange firstNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
  241. options:(NSStringCompareOptions) 0
  242. range:restOfString];
  243. return firstNonWhitespace;
  244. }
  245. -(NSRange) lastWhitespaceBeforeRange:(NSRange)aRange {
  246. NSRange stringUntilRange = NSMakeRange(0, aRange.location);
  247. NSRange lastWhitespace = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]
  248. options:NSBackwardsSearch
  249. range:stringUntilRange];
  250. return lastWhitespace;
  251. }
  252. -(NSRange) lastNonWhitespaceBeforeRange:(NSRange)aRange {
  253. NSRange stringUntilRange = NSMakeRange(0, aRange.location);
  254. NSRange lastNonWhitespace = [self rangeOfCharacterFromSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]
  255. options:NSBackwardsSearch
  256. range:stringUntilRange];
  257. return lastNonWhitespace;
  258. }
  259. /********************/
  260. #pragma mark - Ranges
  261. /********************/
  262. -(NSRange) rangeAfterRange:(NSRange)aRange {
  263. return NSMakeRange(NSMaxRange(aRange), self.length - NSMaxRange(aRange));
  264. }
  265. -(NSRange) rangeFromEndOfRange:(NSRange)aRange {
  266. return NSMakeRange(NSMaxRange(aRange) - 1, self.length - NSMaxRange(aRange) + 1);
  267. }
  268. -(NSRange) rangeToEndFrom:(NSRange)aRange {
  269. return NSMakeRange(aRange.location, self.length - aRange.location);
  270. }
  271. -(NSRange) startRange {
  272. return NSMakeRange(0, 0);
  273. }
  274. -(NSRange) fullRange {
  275. return NSMakeRange(0, self.length);
  276. }
  277. -(NSRange) endRange {
  278. return NSMakeRange(self.length, 0);
  279. }
  280. /***********************/
  281. #pragma mark - Splitting
  282. /***********************/
  283. -(NSArray <NSString *> *) componentsSplitByWhitespace {
  284. return [self componentsSplitByWhitespaceWithMaxSplits:NSUIntegerMax];
  285. }
  286. -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
  287. if (maxSplits == 0) {
  288. return @[ self ];
  289. }
  290. NSMutableArray <NSString *> *components = [NSMutableArray array];
  291. __block NSUInteger tokenStart;
  292. __block NSUInteger tokenEnd;
  293. __block BOOL currentlyInToken = NO;
  294. __block NSRange tokenRange = NSMakeRange(NSNotFound, 0);
  295. __block NSUInteger splits = 0;
  296. [self enumerateSubstringsInRange:self.fullRange
  297. options:NSStringEnumerationByComposedCharacterSequences
  298. usingBlock:^(NSString *character,
  299. NSRange characterRange,
  300. NSRange enclosingRange,
  301. BOOL *stop) {
  302. if ( currentlyInToken == NO
  303. && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]]
  304. ) {
  305. currentlyInToken = YES;
  306. tokenStart = characterRange.location;
  307. } else if ( currentlyInToken == YES
  308. && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
  309. ) {
  310. currentlyInToken = NO;
  311. tokenEnd = characterRange.location;
  312. tokenRange = NSMakeRange(tokenStart, tokenEnd - tokenStart);
  313. [components addObject:[self substringWithRange:tokenRange]];
  314. splits++;
  315. if (splits == maxSplits) {
  316. *stop = YES;
  317. NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]];
  318. if (lastTokenRange.location != NSNotFound) {
  319. [components addObject:[self substringWithRange:lastTokenRange]];
  320. }
  321. }
  322. }
  323. }];
  324. // If we were in a token when we got to the end, add that last token.
  325. if ( splits < maxSplits
  326. && currentlyInToken == YES) {
  327. tokenEnd = self.length;
  328. tokenRange = NSMakeRange(tokenStart,
  329. tokenEnd - tokenStart);
  330. [components addObject:[self substringWithRange:tokenRange]];
  331. }
  332. return components;
  333. }
  334. -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
  335. maxSplits:(NSUInteger)maxSplits {
  336. NSArray <NSString *> *components = [self componentsSeparatedByString:separator];
  337. if (maxSplits >= (components.count - 1))
  338. return components;
  339. return [[components subarrayWithRange:NSMakeRange(0, maxSplits)]
  340. arrayByAddingObject:[[components
  341. subarrayWithRange:NSMakeRange(maxSplits,
  342. components.count - maxSplits)]
  343. componentsJoinedByString:separator]];
  344. }
  345. -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
  346. dropEmptyString:(BOOL)dropEmptyString {
  347. NSMutableArray* components = [[self componentsSeparatedByString:separator] mutableCopy];
  348. if (dropEmptyString == YES)
  349. [components removeObject:@""];
  350. return [components copy];
  351. }
  352. /***************************/
  353. #pragma mark - Byte encoding
  354. /***************************/
  355. -(NSUInteger) UTF8length {
  356. return [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
  357. }
  358. -(NSData *) dataAsUTF8 {
  359. return [self dataUsingEncoding:NSUTF8StringEncoding];
  360. }
  361. +(instancetype) stringWithData:(NSData *)data
  362. encoding:(NSStringEncoding)encoding {
  363. return [[self alloc] initWithData:data
  364. encoding:encoding];
  365. }
  366. +(instancetype) stringWithUTF8Data:(NSData *)data {
  367. return [self stringWithData:data
  368. encoding:NSUTF8StringEncoding];
  369. }
  370. /*********************/
  371. #pragma mark - Hashing
  372. /*********************/
  373. -(NSString *) MD5Hash {
  374. const char *cStr = [self UTF8String];
  375. unsigned char result[CC_MD5_DIGEST_LENGTH];
  376. CC_MD5(cStr, (CC_LONG) strlen(cStr), result);
  377. return [NSString stringWithFormat:
  378. @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
  379. result[0], result[1], result[2], result[3],
  380. result[4], result[5], result[6], result[7],
  381. result[8], result[9], result[10], result[11],
  382. result[12], result[13], result[14], result[15]
  383. ];
  384. }
  385. /***********************/
  386. #pragma mark - Sentences
  387. /***********************/
  388. -(NSString *) firstSentence {
  389. __block NSString *firstSentence;
  390. [self enumerateSubstringsInRange:self.fullRange
  391. options:NSStringEnumerationBySentences
  392. usingBlock:^(NSString * _Nullable substring,
  393. NSRange substringRange,
  394. NSRange enclosingRange,
  395. BOOL * _Nonnull stop) {
  396. firstSentence = substring;
  397. *stop = YES;
  398. }];
  399. return firstSentence;
  400. }
  401. /*********************/
  402. #pragma mark - Padding
  403. /*********************/
  404. -(NSString *) stringLeftPaddedTo:(int)width {
  405. return [NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes];
  406. }
  407. /****************************************************/
  408. #pragma mark - Regular expression convenience methods
  409. /****************************************************/
  410. /*********************************************/
  411. /* Construct regular expressions from strings.
  412. *********************************************/
  413. -(NSRegularExpression *) regularExpression {
  414. return [self regularExpressionWithOptions:(NSRegularExpressionOptions) 0];
  415. }
  416. -(NSRegularExpression *) regularExpressionWithOptions:(NSRegularExpressionOptions)options {
  417. NSError *error;
  418. NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:self
  419. options:options
  420. error:&error];
  421. if (error) {
  422. if (NSString.SA_NSStringExtensions_RaiseRegularExpressionCreateException == YES)
  423. [NSException raise:@"SA_NSStringExtensions_RegularExpressionCreateException"
  424. format:@"%@", error.localizedDescription];
  425. return nil;
  426. }
  427. return regexp;
  428. }
  429. /**********************************************/
  430. /* Get matches for a regular expression object.
  431. **********************************************/
  432. -(NSArray <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
  433. NSMutableArray <NSString *> *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups];
  434. [regex enumerateMatchesInString:self
  435. options:(NSMatchingOptions) 0
  436. range:self.fullRange
  437. usingBlock:^(NSTextCheckingResult * _Nullable result,
  438. NSMatchingFlags flags,
  439. BOOL *stop) {
  440. [NSIndexSet from:0
  441. for:result.numberOfRanges
  442. do:^(NSUInteger idx) {
  443. NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
  444. ? @""
  445. : [self substringWithRange:[result rangeAtIndex:idx]]);
  446. [matches addObject:resultString];
  447. }];
  448. *stop = YES;
  449. }];
  450. return matches;
  451. }
  452. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
  453. NSMutableArray <NSMutableArray <NSString *> *> *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups];
  454. [NSIndexSet from:0
  455. for:regex.numberOfCaptureGroups
  456. do:^(NSUInteger idx) {
  457. [matches addObject:[NSMutableArray array]];
  458. }];
  459. [regex enumerateMatchesInString:self
  460. options:(NSMatchingOptions) 0
  461. range:self.fullRange
  462. usingBlock:^(NSTextCheckingResult * _Nullable result,
  463. NSMatchingFlags flags,
  464. BOOL *stop) {
  465. [NSIndexSet from:0
  466. for:result.numberOfRanges
  467. do:^(NSUInteger idx) {
  468. NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
  469. ? @""
  470. : [self substringWithRange:[result rangeAtIndex:idx]]);
  471. [matches[idx] addObject:resultString];
  472. }];
  473. }];
  474. return matches;
  475. }
  476. /*************************************************************************/
  477. /* Get matches for a string representing a regular expression (a pattern).
  478. *************************************************************************/
  479. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern {
  480. return [self matchesForRegex:[pattern regularExpression]];
  481. }
  482. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern
  483. options:(NSRegularExpressionOptions)options {
  484. return [self matchesForRegex:[pattern regularExpressionWithOptions:options]];
  485. }
  486. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern {
  487. return [self allMatchesForRegex:[pattern regularExpression]];
  488. }
  489. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern
  490. options:(NSRegularExpressionOptions)options {
  491. return [self allMatchesForRegex:[pattern regularExpressionWithOptions:options]];
  492. }
  493. /*******************************************************************************/
  494. /* Use a pattern (a string representing a regular expression) to do replacement.
  495. *******************************************************************************/
  496. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  497. withTemplate:(NSString *)template {
  498. return [self stringByReplacingFirstOccurrenceOfPattern:pattern
  499. withTemplate:template
  500. regularExpressionOptions:(NSRegularExpressionOptions) 0
  501. matchingOptions:(NSMatchingOptions) 0];
  502. }
  503. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  504. withTemplate:(NSString *)template
  505. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  506. matchingOptions:(NSMatchingOptions)matchingOptions {
  507. NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions];
  508. NSTextCheckingResult *match = [regexp firstMatchInString:self
  509. options:matchingOptions
  510. range:self.fullRange];
  511. if ( match
  512. && match.range.location != NSNotFound) {
  513. return [self stringByReplacingCharactersInRange:match.range
  514. withString:[regexp replacementStringForResult:match
  515. inString:self
  516. offset:0
  517. template:template]];
  518. } else {
  519. return self;
  520. }
  521. }
  522. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  523. withTemplate:(NSString *)template {
  524. return [self stringByReplacingAllOccurrencesOfPattern:pattern
  525. withTemplate:template
  526. regularExpressionOptions:(NSRegularExpressionOptions) 0
  527. matchingOptions:(NSMatchingOptions) 0];
  528. }
  529. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  530. withTemplate:(NSString *)template
  531. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  532. matchingOptions:(NSMatchingOptions)matchingOptions {
  533. return [[pattern regularExpressionWithOptions:regexpOptions] stringByReplacingMatchesInString:self
  534. options:matchingOptions
  535. range:self.fullRange
  536. withTemplate:template];
  537. }
  538. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  539. withTemplates:(NSArray <NSString *> *)replacements {
  540. return [self stringByReplacingAllOccurrencesOfPatterns:patterns
  541. withTemplates:replacements
  542. regularExpressionOptions:(NSRegularExpressionOptions) 0
  543. matchingOptions:(NSMatchingOptions) 0];
  544. }
  545. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  546. withTemplates:(NSArray <NSString *> *)replacements
  547. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  548. matchingOptions:(NSMatchingOptions)matchingOptions {
  549. NSMutableString *workingCopy = [self mutableCopy];
  550. [workingCopy replaceAllOccurrencesOfPatterns:patterns
  551. withTemplates:replacements
  552. regularExpressionOptions:regexpOptions
  553. matchingOptions:matchingOptions];
  554. return [workingCopy copy];
  555. }
  556. @end
  557. /*****************************************************************************/
  558. #pragma mark - SA_NSStringExtensions category implementation (NSMutableString)
  559. /*****************************************************************************/
  560. @implementation NSMutableString (SA_NSStringExtensions)
  561. /*************************************/
  562. #pragma mark - Working with characters
  563. /*************************************/
  564. -(void) removeCharactersInSet:(NSCharacterSet *)characters {
  565. NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  566. while (rangeOfCharacters.location != NSNotFound) {
  567. [self replaceCharactersInRange:rangeOfCharacters withString:@""];
  568. rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  569. }
  570. }
  571. -(void) removeCharactersInString:(NSString *)characters {
  572. [self removeCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
  573. }
  574. /**********************/
  575. #pragma mark - Trimming
  576. /**********************/
  577. -(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
  578. usingEncoding:(NSStringEncoding)encoding
  579. withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
  580. andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
  581. // Trim whitespace.
  582. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  583. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  584. withTemplate:@"$1"];
  585. // Collapse whitespace.
  586. if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
  587. [self replaceAllOccurrencesOfPattern:@"\\s+"
  588. withTemplate:@" "];
  589. // Length of the ellipsis suffix, in bytes.
  590. NSString *ellipsis = @" …";
  591. NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
  592. // Trim (leaving space for ellipsis, if necessary).
  593. __block NSUInteger cutoffLength = 0;
  594. [self enumerateSubstringsInRange:self.fullRange
  595. options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
  596. usingBlock:^(NSString * _Nullable substring,
  597. NSRange substringRange,
  598. NSRange enclosingRange,
  599. BOOL * _Nonnull stop) {
  600. NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
  601. NSUInteger endOfEnclosingRangeInBytes = [[self substringToIndex:endOfEnclosingRange] lengthOfBytesUsingEncoding:encoding];
  602. // If we need to append ellipsis when trimming...
  603. if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
  604. if ( self.fullRange.length == endOfEnclosingRange
  605. && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  606. // Either the ellipsis is not needed, because the string is not cut off...
  607. cutoffLength = endOfEnclosingRange;
  608. } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
  609. // Or there will still be room for the ellipsis after adding this piece...
  610. cutoffLength = endOfEnclosingRange;
  611. } else {
  612. // Or we don’t add this piece.
  613. *stop = YES;
  614. }
  615. } else {
  616. if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  617. cutoffLength = endOfEnclosingRange;
  618. } else {
  619. *stop = YES;
  620. }
  621. }
  622. }];
  623. NSUInteger lengthBeforeTrimming = self.length;
  624. [self deleteCharactersInRange:NSMakeRange(cutoffLength, self.length - cutoffLength)];
  625. // Trim whitespace again.
  626. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  627. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  628. withTemplate:@"$1"];
  629. // Append ellipsis.
  630. if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
  631. && cutoffLength < lengthBeforeTrimming
  632. && maxLengthInBytes >= ellipsisLengthInBytes
  633. && ( cutoffLength > 0
  634. || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
  635. ) {
  636. [self appendString:ellipsis];
  637. }
  638. }
  639. /*********************/
  640. #pragma mark - Padding
  641. /*********************/
  642. -(void) leftPadTo:(int)width {
  643. [self setString:[NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]];
  644. }
  645. /****************************************************/
  646. #pragma mark - Regular expression convenience methods
  647. /****************************************************/
  648. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  649. withTemplate:(NSString *)template {
  650. [self replaceFirstOccurrenceOfPattern:pattern
  651. withTemplate:template
  652. regularExpressionOptions:(NSRegularExpressionOptions) 0
  653. matchingOptions:(NSMatchingOptions) 0];
  654. }
  655. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  656. withTemplate:(NSString *)template
  657. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  658. matchingOptions:(NSMatchingOptions)matchingOptions {
  659. NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions];
  660. NSTextCheckingResult *match = [regexp firstMatchInString:self
  661. options:matchingOptions
  662. range:self.fullRange];
  663. if ( match
  664. && match.range.location != NSNotFound) {
  665. NSString *replacementString = [regexp replacementStringForResult:match
  666. inString:self
  667. offset:0
  668. template:template];
  669. [self replaceCharactersInRange:match.range
  670. withString:replacementString];
  671. }
  672. }
  673. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  674. withTemplate:(NSString *)template {
  675. [self replaceAllOccurrencesOfPattern:pattern
  676. withTemplate:template
  677. regularExpressionOptions:(NSRegularExpressionOptions) 0
  678. matchingOptions:(NSMatchingOptions) 0];
  679. }
  680. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  681. withTemplate:(NSString *)template
  682. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  683. matchingOptions:(NSMatchingOptions)matchingOptions {
  684. [[pattern regularExpressionWithOptions:regexpOptions] replaceMatchesInString:self
  685. options:matchingOptions
  686. range:self.fullRange
  687. withTemplate:template];
  688. }
  689. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  690. withTemplates:(NSArray <NSString *> *)replacements {
  691. [self replaceAllOccurrencesOfPatterns:patterns
  692. withTemplates:replacements
  693. regularExpressionOptions:(NSRegularExpressionOptions) 0
  694. matchingOptions:(NSMatchingOptions) 0];
  695. }
  696. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  697. withTemplates:(NSArray <NSString *> *)replacements
  698. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  699. matchingOptions:(NSMatchingOptions)matchingOptions {
  700. [patterns enumerateObjectsUsingBlock:^(NSString * _Nonnull pattern,
  701. NSUInteger idx,
  702. BOOL * _Nonnull stop) {
  703. NSString *replacement = (replacements.count > idx
  704. ? replacements[idx]
  705. : @"");
  706. [self replaceAllOccurrencesOfPattern:pattern
  707. withTemplate:replacement
  708. regularExpressionOptions:regexpOptions
  709. matchingOptions:matchingOptions];
  710. }];
  711. }
  712. @end