Adds utility methods to NSString, for dealing with whitespace, string splitting, and other things.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

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