├── .gitignore ├── NSString+Score.h ├── NSString+Score.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | StringScoreExample/ 17 | -------------------------------------------------------------------------------- /NSString+Score.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Score.h 3 | // 4 | // Created by Nicholas Bruning on 5/12/11. 5 | // Copyright (c) 2011 Involved Pty Ltd. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | enum{ 11 | NSStringScoreOptionNone = 1 << 0, 12 | NSStringScoreOptionFavorSmallerWords = 1 << 1, 13 | NSStringScoreOptionReducedLongStringPenalty = 1 << 2 14 | }; 15 | 16 | typedef NSUInteger NSStringScoreOption; 17 | 18 | @interface NSString (Score) 19 | 20 | - (CGFloat) scoreAgainst:(NSString *)otherString; 21 | - (CGFloat) scoreAgainst:(NSString *)otherString fuzziness:(NSNumber *)fuzziness; 22 | - (CGFloat) scoreAgainst:(NSString *)otherString fuzziness:(NSNumber *)fuzziness options:(NSStringScoreOption)options; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /NSString+Score.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Score.m 3 | // 4 | // Created by Nicholas Bruning on 5/12/11. 5 | // Copyright (c) 2011 Involved Pty Ltd. All rights reserved. 6 | // 7 | 8 | //score reference: http://jsfiddle.net/JrLVD/ 9 | 10 | #import "NSString+Score.h" 11 | 12 | @implementation NSString (Score) 13 | 14 | - (CGFloat) scoreAgainst:(NSString *)otherString{ 15 | return [self scoreAgainst:otherString fuzziness:nil]; 16 | } 17 | 18 | - (CGFloat) scoreAgainst:(NSString *)otherString fuzziness:(NSNumber *)fuzziness{ 19 | return [self scoreAgainst:otherString fuzziness:fuzziness options:NSStringScoreOptionNone]; 20 | } 21 | 22 | - (CGFloat) scoreAgainst:(NSString *)anotherString fuzziness:(NSNumber *)fuzziness options:(NSStringScoreOption)options{ 23 | NSMutableCharacterSet *workingInvalidCharacterSet = [NSCharacterSet lowercaseLetterCharacterSet]; 24 | [workingInvalidCharacterSet formUnionWithCharacterSet:[NSCharacterSet uppercaseLetterCharacterSet]]; 25 | [workingInvalidCharacterSet addCharactersInString:@" "]; 26 | NSCharacterSet *invalidCharacterSet = [workingInvalidCharacterSet invertedSet]; 27 | 28 | NSString *string = [[[self decomposedStringWithCanonicalMapping] componentsSeparatedByCharactersInSet:invalidCharacterSet] componentsJoinedByString:@""]; 29 | NSString *otherString = [[[anotherString decomposedStringWithCanonicalMapping] componentsSeparatedByCharactersInSet:invalidCharacterSet] componentsJoinedByString:@""]; 30 | 31 | // If the string is equal to the abbreviation, perfect match. 32 | if([string isEqualToString:otherString]) return (CGFloat) 1.0f; 33 | 34 | //if it's not a perfect match and is empty return 0 35 | if([otherString length] == 0) return (CGFloat) 0.0f; 36 | 37 | CGFloat totalCharacterScore = 0; 38 | NSUInteger otherStringLength = [otherString length]; 39 | NSUInteger stringLength = [string length]; 40 | BOOL startOfStringBonus = NO; 41 | CGFloat otherStringScore; 42 | CGFloat fuzzies = 1; 43 | CGFloat finalScore; 44 | 45 | // Walk through abbreviation and add up scores. 46 | for(uint index = 0; index < otherStringLength; index++){ 47 | CGFloat characterScore = 0.1; 48 | NSInteger indexInString = NSNotFound; 49 | NSString *chr; 50 | NSRange rangeChrLowercase; 51 | NSRange rangeChrUppercase; 52 | 53 | chr = [otherString substringWithRange:NSMakeRange(index, 1)]; 54 | 55 | //make these next few lines leverage NSNotfound, methinks. 56 | rangeChrLowercase = [string rangeOfString:[chr lowercaseString]]; 57 | rangeChrUppercase = [string rangeOfString:[chr uppercaseString]]; 58 | 59 | if(rangeChrLowercase.location == NSNotFound && rangeChrUppercase.location == NSNotFound){ 60 | if(fuzziness){ 61 | fuzzies += 1 - [fuzziness floatValue]; 62 | } else { 63 | return 0; // this is an error! 64 | } 65 | 66 | } else if (rangeChrLowercase.location != NSNotFound && rangeChrUppercase.location != NSNotFound){ 67 | indexInString = MIN(rangeChrLowercase.location, rangeChrUppercase.location); 68 | 69 | } else if(rangeChrLowercase.location != NSNotFound || rangeChrUppercase.location != NSNotFound){ 70 | indexInString = rangeChrLowercase.location != NSNotFound ? rangeChrLowercase.location : rangeChrUppercase.location; 71 | 72 | } else { 73 | indexInString = MIN(rangeChrLowercase.location, rangeChrUppercase.location); 74 | 75 | } 76 | 77 | // Set base score for matching chr 78 | 79 | // Same case bonus. 80 | if(indexInString != NSNotFound && [[string substringWithRange:NSMakeRange(indexInString, 1)] isEqualToString:chr]){ 81 | characterScore += 0.1; 82 | } 83 | 84 | // Consecutive letter & start-of-string bonus 85 | if(indexInString == 0){ 86 | // Increase the score when matching first character of the remainder of the string 87 | characterScore += 0.6; 88 | if(index == 0){ 89 | // If match is the first character of the string 90 | // & the first character of abbreviation, add a 91 | // start-of-string match bonus. 92 | startOfStringBonus = YES; 93 | } 94 | } else if(indexInString != NSNotFound) { 95 | // Acronym Bonus 96 | // Weighing Logic: Typing the first character of an acronym is as if you 97 | // preceded it with two perfect character matches. 98 | if( [[string substringWithRange:NSMakeRange(indexInString - 1, 1)] isEqualToString:@" "] ){ 99 | characterScore += 0.8; 100 | } 101 | } 102 | 103 | // Left trim the already matched part of the string 104 | // (forces sequential matching). 105 | if(indexInString != NSNotFound){ 106 | string = [string substringFromIndex:indexInString + 1]; 107 | } 108 | 109 | totalCharacterScore += characterScore; 110 | } 111 | 112 | if(NSStringScoreOptionFavorSmallerWords == (options & NSStringScoreOptionFavorSmallerWords)){ 113 | // Weigh smaller words higher 114 | return totalCharacterScore / stringLength; 115 | } 116 | 117 | otherStringScore = totalCharacterScore / otherStringLength; 118 | 119 | if(NSStringScoreOptionReducedLongStringPenalty == (options & NSStringScoreOptionReducedLongStringPenalty)){ 120 | // Reduce the penalty for longer words 121 | CGFloat percentageOfMatchedString = otherStringLength / stringLength; 122 | CGFloat wordScore = otherStringScore * percentageOfMatchedString; 123 | finalScore = (wordScore + otherStringScore) / 2; 124 | 125 | } else { 126 | finalScore = ((otherStringScore * ((CGFloat)(otherStringLength) / (CGFloat)(stringLength))) + otherStringScore) / 2; 127 | } 128 | 129 | finalScore = finalScore / fuzzies; 130 | 131 | if(startOfStringBonus && finalScore + 0.15 < 1){ 132 | finalScore += 0.15; 133 | } 134 | 135 | return finalScore; 136 | } 137 | 138 | @end 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StringScore 2 | 3 | StringScore is an Objective-C library which provides super fast fuzzy string matching/scoring. Based on the [JavaScript library of the same name](https://github.com/joshaven/string_score), by [Joshaven Potter](https://github.com/joshaven). 4 | 5 | 6 | ## Using StringScore 7 | 8 | StringScore adds 3 new methods to `NSString`: 9 | 10 | ```` 11 | - (CGFloat) scoreAgainst:(NSString *)otherString; 12 | 13 | - (CGFloat) scoreAgainst:(NSString *)otherString fuzziness:(NSNumber *)fuzziness; 14 | 15 | - (CGFloat) scoreAgainst:(NSString *)otherString fuzziness:(NSNumber *)fuzziness options:(NSStringScoreOption)options; 16 | ```` 17 | 18 | All three methods return a `CGFloat` representing how closely the string 19 | matched the `otherString` parameter. 20 | 21 | 22 | ## Additional Parameters 23 | 24 | ### Fuzziness 25 | 26 | A number between 0 and 1 which varys how fuzzy/ the calculation is. 27 | Defaults to `nil` (fuzziness disabled). 28 | 29 | 30 | ### Options 31 | 32 | There are 3 options available: `NSStringScoreOptionNone`, `NSStringScoreOptionFavorSmallerWords` and `NSStringScoreOptionReducedLongStringPenalty`. Each of which is pretty self-explanatory, see example below for usage. 33 | 34 | 35 | ## Examples 36 | 37 | Given the following sample application: 38 | 39 | ```` 40 | NSString *testString = @"Hello world!"; 41 | 42 | CGFloat result1 = [testString scoreAgainst:@"Hello world!"]; 43 | CGFloat result2 = [testString scoreAgainst:@"world"]; 44 | CGFloat result3 = [testString scoreAgainst:@"wXrld" fuzziness:[NSNumber numberWithFloat:0.8]]; 45 | CGFloat result4 = [testString scoreAgainst:@"world" fuzziness:nil options:NSStringScoreOptionFavorSmallerWords]; 46 | CGFloat result5 = [testString scoreAgainst:@"world" fuzziness:nil options:(NSStringScoreOptionFavorSmallerWords | NSStringScoreOptionReducedLongStringPenalty)]; 47 | CGFloat result6 = [testString scoreAgainst:@"HW"]; // abbreviation matching example 48 | 49 | NSLog(@"Result 1 = %f", result1); 50 | NSLog(@"Result 2 = %f", result2); 51 | NSLog(@"Result 3 = %f", result3); 52 | NSLog(@"Result 4 = %f", result4); 53 | NSLog(@"Result 5 = %f", result5); 54 | NSLog(@"Result 6 = %f", result6); 55 | ```` 56 | 57 | The resulting output is: 58 | 59 | ```` 60 | 2012-05-14 15:13:38.074 StringScore[13415:18a03] Result 1 = 1.000000 61 | 2012-05-14 15:13:38.075 StringScore[13415:18a03] Result 2 = 0.425000 62 | 2012-05-14 15:13:38.075 StringScore[13415:18a03] Result 3 = 0.271528 63 | 2012-05-14 15:13:38.076 StringScore[13415:18a03] Result 4 = 0.250000 64 | 2012-05-14 15:13:38.077 StringScore[13415:18a03] Result 5 = 0.425000 65 | 2012-05-14 15:13:38.078 StringScore[13415:18a03] Result 6 = 0.645833 66 | ```` 67 | 68 | ## Credits 69 | 70 | Author: [Nicholas Bruning](https://github.com/thetron) 71 | 72 | Special thanks to [Joshaven Potter](https://github.com/joshaven) for 73 | providing the basis for this library. 74 | 75 | 76 | ## License 77 | 78 | Licensed under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 79 | --------------------------------------------------------------------------------