Adds utility methods to NSString, for dealing with whitespace, string splitting, and other things.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

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