Adds utility methods to NSString, for dealing with whitespace, string splitting, and other things.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

NSString+SA_NSStringExtensions.m 32KB

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