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

NSString+SA_NSStringExtensions.m 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  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 *regex;
  297. static dispatch_once_t onceToken;
  298. dispatch_once(&onceToken, ^{
  299. regex = [@"^\\S*|(?<=\\s)$|\\S+" regularExpression];
  300. });
  301. NSMutableArray <NSString *> *components = [NSMutableArray array];
  302. [regex 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 *regex = [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 regex;
  471. }
  472. /**********************************************/
  473. /* Get matches for a regular expression object.
  474. **********************************************/
  475. -(NSArray <NSString *> *) matchesForRegex:(NSRegularExpression *)regex {
  476. return [self matchesForRegex:regex
  477. all:NO];
  478. }
  479. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegex:(NSRegularExpression *)regex {
  480. return [self matchesForRegex:regex
  481. all:YES];
  482. }
  483. /* Helper method (private).
  484. */
  485. -(NSArray *) matchesForRegex:(NSRegularExpression *)regex
  486. all:(BOOL)all {
  487. NSMutableArray *matches = [NSMutableArray array];
  488. [regex enumerateMatchesInString:self
  489. options:(NSMatchingOptions) 0
  490. range:self.fullRange
  491. usingBlock:^(NSTextCheckingResult * _Nullable result,
  492. NSMatchingFlags flags,
  493. BOOL *stop) {
  494. if (all)
  495. [matches addObject:[NSMutableArray array]];
  496. [NSIndexSet from:0
  497. for:result.numberOfRanges
  498. do:^(NSUInteger idx) {
  499. NSString *resultString = ([result rangeAtIndex:idx].location == NSNotFound
  500. ? @""
  501. : [self substringWithRange:[result rangeAtIndex:idx]]);
  502. if (all) {
  503. [((NSMutableArray *) matches.lastObject) addObject:resultString];
  504. } else {
  505. [matches addObject:resultString];
  506. }
  507. }];
  508. if (!all)
  509. *stop = YES;
  510. }];
  511. return matches;
  512. }
  513. /*************************************************************************/
  514. /* Get matches for a string representing a regular expression (a pattern).
  515. *************************************************************************/
  516. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern {
  517. return [self matchesForRegex:[pattern regularExpression]];
  518. }
  519. -(NSArray <NSString *> *) matchesForRegexPattern:(NSString *)pattern
  520. options:(NSRegularExpressionOptions)options {
  521. return [self matchesForRegex:[pattern regularExpressionWithOptions:options]];
  522. }
  523. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern {
  524. return [self allMatchesForRegex:[pattern regularExpression]];
  525. }
  526. -(NSArray <NSArray <NSString *> *> *) allMatchesForRegexPattern:(NSString *)pattern
  527. options:(NSRegularExpressionOptions)options {
  528. return [self allMatchesForRegex:[pattern regularExpressionWithOptions:options]];
  529. }
  530. /*******************************************************************************/
  531. /* Use a pattern (a string representing a regular expression) to do replacement.
  532. *******************************************************************************/
  533. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  534. withTemplate:(NSString *)template {
  535. return [self stringByReplacingFirstOccurrenceOfPattern:pattern
  536. withTemplate:template
  537. regularExpressionOptions:(NSRegularExpressionOptions) 0
  538. matchingOptions:(NSMatchingOptions) 0];
  539. }
  540. -(NSString *) stringByReplacingFirstOccurrenceOfPattern:(NSString *)pattern
  541. withTemplate:(NSString *)template
  542. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  543. matchingOptions:(NSMatchingOptions)matchingOptions {
  544. NSRegularExpression *regex = [pattern regularExpressionWithOptions:regexpOptions];
  545. NSTextCheckingResult *match = [regex firstMatchInString:self
  546. options:matchingOptions
  547. range:self.fullRange];
  548. if ( match
  549. && match.range.location != NSNotFound) {
  550. return [self stringByReplacingCharactersInRange:match.range
  551. withString:[regex replacementStringForResult:match
  552. inString:self
  553. offset:0
  554. template:template]];
  555. } else {
  556. return self;
  557. }
  558. }
  559. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  560. withTemplate:(NSString *)template {
  561. return [self stringByReplacingAllOccurrencesOfPattern:pattern
  562. withTemplate:template
  563. regularExpressionOptions:(NSRegularExpressionOptions) 0
  564. matchingOptions:(NSMatchingOptions) 0];
  565. }
  566. -(NSString *) stringByReplacingAllOccurrencesOfPattern:(NSString *)pattern
  567. withTemplate:(NSString *)template
  568. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  569. matchingOptions:(NSMatchingOptions)matchingOptions {
  570. return [[pattern regularExpressionWithOptions:regexpOptions] stringByReplacingMatchesInString:self
  571. options:matchingOptions
  572. range:self.fullRange
  573. withTemplate:template];
  574. }
  575. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  576. withTemplates:(NSArray <NSString *> *)replacements {
  577. return [self stringByReplacingAllOccurrencesOfPatterns:patterns
  578. withTemplates:replacements
  579. regularExpressionOptions:(NSRegularExpressionOptions) 0
  580. matchingOptions:(NSMatchingOptions) 0];
  581. }
  582. -(NSString *) stringByReplacingAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  583. withTemplates:(NSArray <NSString *> *)replacements
  584. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  585. matchingOptions:(NSMatchingOptions)matchingOptions {
  586. NSMutableString *workingCopy = [self mutableCopy];
  587. [workingCopy replaceAllOccurrencesOfPatterns:patterns
  588. withTemplates:replacements
  589. regularExpressionOptions:regexpOptions
  590. matchingOptions:matchingOptions];
  591. return [workingCopy copy];
  592. }
  593. @end
  594. /*****************************************************************************/
  595. #pragma mark - SA_NSStringExtensions category implementation (NSMutableString)
  596. /*****************************************************************************/
  597. @implementation NSMutableString (SA_NSStringExtensions)
  598. /*************************************/
  599. #pragma mark - Working with characters
  600. /*************************************/
  601. -(void) removeCharactersInSet:(NSCharacterSet *)characters {
  602. NSRange rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  603. while (rangeOfCharacters.location != NSNotFound) {
  604. [self replaceCharactersInRange:rangeOfCharacters withString:@""];
  605. rangeOfCharacters = [self rangeOfCharacterFromSet:characters];
  606. }
  607. }
  608. -(void) removeCharactersInString:(NSString *)characters {
  609. [self removeCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:characters]];
  610. }
  611. /**********************/
  612. #pragma mark - Trimming
  613. /**********************/
  614. -(void) trimToMaxLengthInBytes:(NSUInteger)maxLengthInBytes
  615. usingEncoding:(NSStringEncoding)encoding
  616. withStringEnumerationOptions:(NSStringEnumerationOptions)enumerationOptions
  617. andStringTrimmingOptions:(SA_NSStringTrimmingOptions)trimmingOptions {
  618. // Trim whitespace.
  619. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  620. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  621. withTemplate:@"$1"];
  622. // Collapse whitespace.
  623. if (trimmingOptions & SA_NSStringTrimming_CollapseWhitespace)
  624. [self replaceAllOccurrencesOfPattern:@"\\s+"
  625. withTemplate:@" "];
  626. // Length of the ellipsis suffix, in bytes.
  627. NSString *ellipsis = @" …";
  628. NSUInteger ellipsisLengthInBytes = [ellipsis lengthOfBytesUsingEncoding:encoding];
  629. // Trim (leaving space for ellipsis, if necessary).
  630. __block NSUInteger cutoffLength = 0;
  631. [self enumerateSubstringsInRange:self.fullRange
  632. options:(enumerationOptions|NSStringEnumerationSubstringNotRequired)
  633. usingBlock:^(NSString * _Nullable substring,
  634. NSRange substringRange,
  635. NSRange enclosingRange,
  636. BOOL * _Nonnull stop) {
  637. NSUInteger endOfEnclosingRange = NSMaxRange(enclosingRange);
  638. NSUInteger endOfEnclosingRangeInBytes = [[self substringToIndex:endOfEnclosingRange] lengthOfBytesUsingEncoding:encoding];
  639. // If we need to append ellipsis when trimming...
  640. if (trimmingOptions & SA_NSStringTrimming_AppendEllipsis) {
  641. if ( self.fullRange.length == endOfEnclosingRange
  642. && endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  643. // Either the ellipsis is not needed, because the string is not cut off...
  644. cutoffLength = endOfEnclosingRange;
  645. } else if (endOfEnclosingRangeInBytes <= (maxLengthInBytes - ellipsisLengthInBytes)) {
  646. // Or there will still be room for the ellipsis after adding this piece...
  647. cutoffLength = endOfEnclosingRange;
  648. } else {
  649. // Or we don’t add this piece.
  650. *stop = YES;
  651. }
  652. } else {
  653. if (endOfEnclosingRangeInBytes <= maxLengthInBytes) {
  654. cutoffLength = endOfEnclosingRange;
  655. } else {
  656. *stop = YES;
  657. }
  658. }
  659. }];
  660. NSUInteger lengthBeforeTrimming = self.length;
  661. [self deleteCharactersInRange:NSMakeRange(cutoffLength, self.length - cutoffLength)];
  662. // Trim whitespace again.
  663. if (trimmingOptions & SA_NSStringTrimming_TrimWhitespace)
  664. [self replaceAllOccurrencesOfPattern:@"^\\s*(.*?)\\s*$"
  665. withTemplate:@"$1"];
  666. // Append ellipsis.
  667. if ( trimmingOptions & SA_NSStringTrimming_AppendEllipsis
  668. && cutoffLength < lengthBeforeTrimming
  669. && maxLengthInBytes >= ellipsisLengthInBytes
  670. && ( cutoffLength > 0
  671. || !(trimmingOptions & SA_NSStringTrimming_ElideEllipsisWhenEmpty))
  672. ) {
  673. [self appendString:ellipsis];
  674. }
  675. }
  676. /*********************/
  677. #pragma mark - Padding
  678. /*********************/
  679. -(void) leftPadTo:(int)width {
  680. [self setString:[NSString stringWithFormat:@"%*s", width, [self stringByAppendingString:@"\0"].dataAsUTF8.bytes]];
  681. }
  682. /****************************************************/
  683. #pragma mark - Regular expression convenience methods
  684. /****************************************************/
  685. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  686. withTemplate:(NSString *)template {
  687. [self replaceFirstOccurrenceOfPattern:pattern
  688. withTemplate:template
  689. regularExpressionOptions:(NSRegularExpressionOptions) 0
  690. matchingOptions:(NSMatchingOptions) 0];
  691. }
  692. -(void) replaceFirstOccurrenceOfPattern:(NSString *)pattern
  693. withTemplate:(NSString *)template
  694. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  695. matchingOptions:(NSMatchingOptions)matchingOptions {
  696. NSRegularExpression *regex = [pattern regularExpressionWithOptions:regexpOptions];
  697. NSTextCheckingResult *match = [regex firstMatchInString:self
  698. options:matchingOptions
  699. range:self.fullRange];
  700. if ( match
  701. && match.range.location != NSNotFound) {
  702. NSString *replacementString = [regex replacementStringForResult:match
  703. inString:self
  704. offset:0
  705. template:template];
  706. [self replaceCharactersInRange:match.range
  707. withString:replacementString];
  708. }
  709. }
  710. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  711. withTemplate:(NSString *)template {
  712. [self replaceAllOccurrencesOfPattern:pattern
  713. withTemplate:template
  714. regularExpressionOptions:(NSRegularExpressionOptions) 0
  715. matchingOptions:(NSMatchingOptions) 0];
  716. }
  717. -(void) replaceAllOccurrencesOfPattern:(NSString *)pattern
  718. withTemplate:(NSString *)template
  719. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  720. matchingOptions:(NSMatchingOptions)matchingOptions {
  721. [[pattern regularExpressionWithOptions:regexpOptions] replaceMatchesInString:self
  722. options:matchingOptions
  723. range:self.fullRange
  724. withTemplate:template];
  725. }
  726. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  727. withTemplates:(NSArray <NSString *> *)replacements {
  728. [self replaceAllOccurrencesOfPatterns:patterns
  729. withTemplates:replacements
  730. regularExpressionOptions:(NSRegularExpressionOptions) 0
  731. matchingOptions:(NSMatchingOptions) 0];
  732. }
  733. -(void) replaceAllOccurrencesOfPatterns:(NSArray <NSString *> *)patterns
  734. withTemplates:(NSArray <NSString *> *)replacements
  735. regularExpressionOptions:(NSRegularExpressionOptions)regexpOptions
  736. matchingOptions:(NSMatchingOptions)matchingOptions {
  737. [patterns enumerateObjectsUsingBlock:^(NSString * _Nonnull pattern,
  738. NSUInteger idx,
  739. BOOL * _Nonnull stop) {
  740. NSString *replacement = (replacements.count > idx
  741. ? replacements[idx]
  742. : @"");
  743. [self replaceAllOccurrencesOfPattern:pattern
  744. withTemplate:replacement
  745. regularExpressionOptions:regexpOptions
  746. matchingOptions:matchingOptions];
  747. }];
  748. }
  749. @end