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 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  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. dropEmptyString:YES];
  286. }
  287. -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
  288. return [self componentsSplitByWhitespaceWithMaxSplits:maxSplits
  289. dropEmptyString:YES];
  290. }
  291. -(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits
  292. dropEmptyString:(BOOL)dropEmptyString {
  293. // No need to do anything fancy in this case.
  294. if (maxSplits == 0)
  295. return @[ self ];
  296. static NSRegularExpression *regexp;
  297. static dispatch_once_t onceToken;
  298. dispatch_once(&onceToken, ^{
  299. regexp = [@"(?:^|\\S|$)+" regularExpression];
  300. });
  301. NSMutableArray <NSString *> *components = [NSMutableArray array];
  302. [regexp enumerateMatchesInString:self
  303. options:(NSMatchingOptions) 0
  304. range:self.fullRange
  305. usingBlock:^(NSTextCheckingResult * _Nullable result,
  306. NSMatchingFlags flags,
  307. BOOL * _Nonnull stop) {
  308. if ( dropEmptyString
  309. && result.range.length == 0) {
  310. // Nothing.
  311. } else if (components.count < maxSplits) {
  312. [components addObject:[self substringWithRange:result.range]];
  313. } else {
  314. [components addObject:[self substringWithRange:[self rangeToEndFrom:result.range]]];
  315. *stop = YES;
  316. }
  317. }];
  318. return components;
  319. }
  320. //-(NSArray <NSString *> *) componentsSplitByWhitespaceWithMaxSplits:(NSUInteger)maxSplits {
  321. // if (maxSplits == 0) {
  322. // return @[ self ];
  323. // }
  324. //
  325. // NSMutableArray <NSString *> *components = [NSMutableArray array];
  326. //
  327. // __block NSUInteger tokenStart;
  328. // __block NSUInteger tokenEnd;
  329. // __block BOOL currentlyInToken = NO;
  330. // __block NSRange tokenRange = NSMakeRange(NSNotFound, 0);
  331. //
  332. // __block NSUInteger splits = 0;
  333. //
  334. // [self enumerateSubstringsInRange:self.fullRange
  335. // options:NSStringEnumerationByComposedCharacterSequences
  336. // usingBlock:^(NSString *character,
  337. // NSRange characterRange,
  338. // NSRange enclosingRange,
  339. // BOOL *stop) {
  340. // if ( currentlyInToken == NO
  341. // && [character containsCharactersInSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]]
  342. // ) {
  343. // currentlyInToken = YES;
  344. // tokenStart = characterRange.location;
  345. // } else if ( currentlyInToken == YES
  346. // && [character containsCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
  347. // ) {
  348. // currentlyInToken = NO;
  349. // tokenEnd = characterRange.location;
  350. //
  351. // tokenRange = NSMakeRange(tokenStart,
  352. // tokenEnd - tokenStart);
  353. // [components addObject:[self substringWithRange:tokenRange]];
  354. // splits++;
  355. // if (splits == maxSplits) {
  356. // *stop = YES;
  357. // NSRange lastTokenRange = [self rangeToEndFrom:[self firstNonWhitespaceAfterRange:tokenRange]];
  358. // if (lastTokenRange.location != NSNotFound) {
  359. // [components addObject:[self substringWithRange:lastTokenRange]];
  360. // }
  361. // }
  362. // }
  363. // }];
  364. //
  365. // // If we were in a token when we got to the end, add that last token.
  366. // if ( splits < maxSplits
  367. // && currentlyInToken == YES) {
  368. // tokenEnd = self.length;
  369. //
  370. // tokenRange = NSMakeRange(tokenStart,
  371. // tokenEnd - tokenStart);
  372. // [components addObject:[self substringWithRange:tokenRange]];
  373. // }
  374. //
  375. // return components;
  376. //}
  377. -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
  378. maxSplits:(NSUInteger)maxSplits {
  379. NSArray <NSString *> *components = [self componentsSeparatedByString:separator];
  380. if (maxSplits >= (components.count - 1))
  381. return components;
  382. return [[components subarrayWithRange:NSMakeRange(0, maxSplits)]
  383. arrayByAddingObject:[[components
  384. subarrayWithRange:NSMakeRange(maxSplits,
  385. components.count - maxSplits)]
  386. componentsJoinedByString:separator]];
  387. }
  388. -(NSArray <NSString *> *) componentsSeparatedByString:(NSString *)separator
  389. dropEmptyString:(BOOL)dropEmptyString {
  390. NSMutableArray* components = [[self componentsSeparatedByString:separator] mutableCopy];
  391. if (dropEmptyString == YES)
  392. [components removeObject:@""];
  393. return [components copy];
  394. }
  395. /***************************/
  396. #pragma mark - Byte encoding
  397. /***************************/
  398. -(NSUInteger) UTF8length {
  399. return [self lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
  400. }
  401. -(NSData *) dataAsUTF8 {
  402. return [self dataUsingEncoding:NSUTF8StringEncoding];
  403. }
  404. +(instancetype) stringWithData:(NSData *)data
  405. encoding:(NSStringEncoding)encoding {
  406. return [[self alloc] initWithData:data
  407. encoding:encoding];
  408. }
  409. +(instancetype) stringWithUTF8Data:(NSData *)data {
  410. return [self stringWithData:data
  411. encoding:NSUTF8StringEncoding];
  412. }
  413. /*********************/
  414. #pragma mark - Hashing
  415. /*********************/
  416. -(NSString *) MD5Hash {
  417. const char *cStr = [self UTF8String];
  418. unsigned char result[CC_MD5_DIGEST_LENGTH];
  419. CC_MD5(cStr, (CC_LONG) strlen(cStr), result);
  420. return [NSString stringWithFormat:
  421. @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
  422. result[0], result[1], result[2], result[3],
  423. result[4], result[5], result[6], result[7],
  424. result[8], result[9], result[10], result[11],
  425. result[12], result[13], result[14], result[15]
  426. ];
  427. }
  428. /***********************/
  429. #pragma mark - Sentences
  430. /***********************/
  431. -(NSString *) firstSentence {
  432. __block NSString *firstSentence;
  433. [self enumerateSubstringsInRange:self.fullRange
  434. options:NSStringEnumerationBySentences
  435. usingBlock:^(NSString * _Nullable substring,
  436. NSRange substringRange,
  437. NSRange enclosingRange,
  438. BOOL * _Nonnull stop) {
  439. firstSentence = substring;
  440. *stop = YES;
  441. }];
  442. return firstSentence;
  443. }
  444. /*********************/
  445. #pragma mark - Padding
  446. /*********************/
  447. -(NSString *) stringLeftPaddedTo:(int)width {
  448. return [NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes];
  449. }
  450. /****************************************************/
  451. #pragma mark - Regular expression convenience methods
  452. /****************************************************/
  453. /*********************************************/
  454. /* Construct regular expressions from strings.
  455. *********************************************/
  456. -(NSRegularExpression *) regularExpression {
  457. return [self regularExpressionWithOptions:(NSRegularExpressionOptions) 0];
  458. }
  459. -(NSRegularExpression *) regularExpressionWithOptions:(NSRegularExpressionOptions)options {
  460. NSError *error;
  461. NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:self
  462. options:options
  463. error:&error];
  464. if (error) {
  465. if (NSString.SA_NSStringExtensions_RaiseRegularExpressionCreateException == YES)
  466. [NSException raise:@"SA_NSStringExtensions_RegularExpressionCreateException"
  467. format:@"%@", error.localizedDescription];
  468. return nil;
  469. }
  470. return regexp;
  471. }
  472. /**********************************************/
  473. /* Get matches for a regular expression object.
  474. **********************************************/
  475. -(NSArray <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
  476. NSMutableArray <NSString *> *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups];
  477. [regex enumerateMatchesInString:self
  478. options:(NSMatchingOptions) 0
  479. range:self.fullRange
  480. usingBlock:^(NSTextCheckingResult * _Nullable result,
  481. NSMatchingFlags flags,
  482. BOOL *stop) {
  483. [NSIndexSet from:0
  484. for:result.numberOfRanges
  485. do:^(NSUInteger idx) {
  486. NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
  487. ? @""
  488. : [self substringWithRange:[result rangeAtIndex:idx]]);
  489. [matches addObject:resultString];
  490. }];
  491. *stop = YES;
  492. }];
  493. return matches;
  494. }
  495. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
  496. NSMutableArray <NSMutableArray <NSString *> *> *matches = [NSMutableArray arrayWithCapacity:regex.numberOfCaptureGroups];
  497. [NSIndexSet from:0
  498. for:regex.numberOfCaptureGroups
  499. do:^(NSUInteger idx) {
  500. [matches addObject:[NSMutableArray array]];
  501. }];
  502. [regex enumerateMatchesInString:self
  503. options:(NSMatchingOptions) 0
  504. range:self.fullRange
  505. usingBlock:^(NSTextCheckingResult * _Nullable result,
  506. NSMatchingFlags flags,
  507. BOOL *stop) {
  508. [NSIndexSet from:0
  509. for:result.numberOfRanges
  510. do:^(NSUInteger idx) {
  511. NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
  512. ? @""
  513. : [self substringWithRange:[result rangeAtIndex:idx]]);
  514. [matches[idx] addObject:resultString];
  515. }];
  516. }];
  517. return matches;
  518. }
  519. /*************************************************************************/
  520. /* Get matches for a string representing a regular expression (a pattern).
  521. *************************************************************************/
  522. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern {
  523. return [self matchesForRegex:[pattern regularExpression]];
  524. }
  525. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern
  526. options:(NSRegularExpressionOptions)options {
  527. return [self matchesForRegex:[pattern regularExpressionWithOptions:options]];
  528. }
  529. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern {
  530. return [self allMatchesForRegex:[pattern regularExpression]];
  531. }
  532. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern
  533. options:(NSRegularExpressionOptions)options {
  534. return [self allMatchesForRegex:[pattern regularExpressionWithOptions:options]];
  535. }
  536. /*******************************************************************************/
  537. /* Use a pattern (a string representing a regular expression) to do replacement.
  538. *******************************************************************************/
  539. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  540. withTemplate:(NSString *)template {
  541. return [self stringByReplacingFirstOccurrenceOfPattern:pattern
  542. withTemplate:template
  543. regularExpressionOptions:(NSRegularExpressionOptions) 0
  544. matchingOptions:(NSMatchingOptions) 0];
  545. }
  546. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  547. withTemplate:(NSString *)template
  548. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  549. matchingOptions:(NSMatchingOptions)matchingOptions {
  550. NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions];
  551. NSTextCheckingResult *match = [regexp firstMatchInString:self
  552. options:matchingOptions
  553. range:self.fullRange];
  554. if ( match
  555. && match.range.location != NSNotFound) {
  556. return [self stringByReplacingCharactersInRange:match.range
  557. withString:[regexp replacementStringForResult:match
  558. inString:self
  559. offset:0
  560. template:template]];
  561. } else {
  562. return self;
  563. }
  564. }
  565. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  566. withTemplate:(NSString *)template {
  567. return [self stringByReplacingAllOccurrencesOfPattern:pattern
  568. withTemplate:template
  569. regularExpressionOptions:(NSRegularExpressionOptions) 0
  570. matchingOptions:(NSMatchingOptions) 0];
  571. }
  572. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  573. withTemplate:(NSString *)template
  574. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  575. matchingOptions:(NSMatchingOptions)matchingOptions {
  576. return [[pattern regularExpressionWithOptions:regexpOptions] stringByReplacingMatchesInString:self
  577. options:matchingOptions
  578. range:self.fullRange
  579. withTemplate:template];
  580. }
  581. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  582. withTemplates:(NSArray <NSString *> *)replacements {
  583. return [self stringByReplacingAllOccurrencesOfPatterns:patterns
  584. withTemplates:replacements
  585. regularExpressionOptions:(NSRegularExpressionOptions) 0
  586. matchingOptions:(NSMatchingOptions) 0];
  587. }
  588. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  589. withTemplates:(NSArray <NSString *> *)replacements
  590. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  591. matchingOptions:(NSMatchingOptions)matchingOptions {
  592. NSMutableString *workingCopy = [self mutableCopy];
  593. [workingCopy replaceAllOccurrencesOfPatterns:patterns
  594. withTemplates:replacements
  595. regularExpressionOptions:regexpOptions
  596. matchingOptions:matchingOptions];
  597. return [workingCopy copy];
  598. }
  599. @end
  600. /*****************************************************************************/
  601. #pragma mark - SA_NSStringExtensions category implementation (NSMutableString)
  602. /*****************************************************************************/
  603. @implementation NSMutableString (SA_NSStringExtensions)
  604. /*************************************/
  605. #pragma mark - Working with characters
  606. /*************************************/
  607. -(void) removeCharactersInSet:(NSCharacterSet *)characters {
  608. NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  609. while (rangeOfCharacters.location != NSNotFound) {
  610. [self replaceCharactersInRange:rangeOfCharacters withString:@""];
  611. rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  612. }
  613. }
  614. -(void) removeCharactersInString:(NSString *)characters {
  615. [self removeCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
  616. }
  617. /**********************/
  618. #pragma mark - Trimming
  619. /**********************/
  620. -(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
  621. usingEncoding:(NSStringEncoding)encoding
  622. withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
  623. andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
  624. // Trim whitespace.
  625. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  626. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  627. withTemplate:@"$1"];
  628. // Collapse whitespace.
  629. if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
  630. [self replaceAllOccurrencesOfPattern:@"\\s+"
  631. withTemplate:@" "];
  632. // Length of the ellipsis suffix, in bytes.
  633. NSString *ellipsis = @" …";
  634. NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
  635. // Trim (leaving space for ellipsis, if necessary).
  636. __block NSUInteger cutoffLength = 0;
  637. [self enumerateSubstringsInRange:self.fullRange
  638. options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
  639. usingBlock:^(NSString * _Nullable substring,
  640. NSRange substringRange,
  641. NSRange enclosingRange,
  642. BOOL * _Nonnull stop) {
  643. NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
  644. NSUInteger endOfEnclosingRangeInBytes = [[self substringToIndex:endOfEnclosingRange] lengthOfBytesUsingEncoding:encoding];
  645. // If we need to append ellipsis when trimming...
  646. if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
  647. if ( self.fullRange.length == endOfEnclosingRange
  648. && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  649. // Either the ellipsis is not needed, because the string is not cut off...
  650. cutoffLength = endOfEnclosingRange;
  651. } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
  652. // Or there will still be room for the ellipsis after adding this piece...
  653. cutoffLength = endOfEnclosingRange;
  654. } else {
  655. // Or we don’t add this piece.
  656. *stop = YES;
  657. }
  658. } else {
  659. if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  660. cutoffLength = endOfEnclosingRange;
  661. } else {
  662. *stop = YES;
  663. }
  664. }
  665. }];
  666. NSUInteger lengthBeforeTrimming = self.length;
  667. [self deleteCharactersInRange:NSMakeRange(cutoffLength, self.length - cutoffLength)];
  668. // Trim whitespace again.
  669. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  670. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  671. withTemplate:@"$1"];
  672. // Append ellipsis.
  673. if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
  674. && cutoffLength < lengthBeforeTrimming
  675. && maxLengthInBytes >= ellipsisLengthInBytes
  676. && ( cutoffLength > 0
  677. || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
  678. ) {
  679. [self appendString:ellipsis];
  680. }
  681. }
  682. /*********************/
  683. #pragma mark - Padding
  684. /*********************/
  685. -(void) leftPadTo:(int)width {
  686. [self setString:[NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]];
  687. }
  688. /****************************************************/
  689. #pragma mark - Regular expression convenience methods
  690. /****************************************************/
  691. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  692. withTemplate:(NSString *)template {
  693. [self replaceFirstOccurrenceOfPattern:pattern
  694. withTemplate:template
  695. regularExpressionOptions:(NSRegularExpressionOptions) 0
  696. matchingOptions:(NSMatchingOptions) 0];
  697. }
  698. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  699. withTemplate:(NSString *)template
  700. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  701. matchingOptions:(NSMatchingOptions)matchingOptions {
  702. NSRegularExpression *regexp = [pattern regularExpressionWithOptions:regexpOptions];
  703. NSTextCheckingResult *match = [regexp firstMatchInString:self
  704. options:matchingOptions
  705. range:self.fullRange];
  706. if ( match
  707. && match.range.location != NSNotFound) {
  708. NSString *replacementString = [regexp replacementStringForResult:match
  709. inString:self
  710. offset:0
  711. template:template];
  712. [self replaceCharactersInRange:match.range
  713. withString:replacementString];
  714. }
  715. }
  716. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  717. withTemplate:(NSString *)template {
  718. [self replaceAllOccurrencesOfPattern:pattern
  719. withTemplate:template
  720. regularExpressionOptions:(NSRegularExpressionOptions) 0
  721. matchingOptions:(NSMatchingOptions) 0];
  722. }
  723. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  724. withTemplate:(NSString *)template
  725. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  726. matchingOptions:(NSMatchingOptions)matchingOptions {
  727. [[pattern regularExpressionWithOptions:regexpOptions] replaceMatchesInString:self
  728. options:matchingOptions
  729. range:self.fullRange
  730. withTemplate:template];
  731. }
  732. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  733. withTemplates:(NSArray <NSString *> *)replacements {
  734. [self replaceAllOccurrencesOfPatterns:patterns
  735. withTemplates:replacements
  736. regularExpressionOptions:(NSRegularExpressionOptions) 0
  737. matchingOptions:(NSMatchingOptions) 0];
  738. }
  739. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  740. withTemplates:(NSArray <NSString *> *)replacements
  741. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  742. matchingOptions:(NSMatchingOptions)matchingOptions {
  743. [patterns enumerateObjectsUsingBlock:^(NSString * _Nonnull pattern,
  744. NSUInteger idx,
  745. BOOL * _Nonnull stop) {
  746. NSString *replacement = (replacements.count > idx
  747. ? replacements[idx]
  748. : @"");
  749. [self replaceAllOccurrencesOfPattern:pattern
  750. withTemplate:replacement
  751. regularExpressionOptions:regexpOptions
  752. matchingOptions:matchingOptions];
  753. }];
  754. }
  755. @end