Skip to content

Instantly share code, notes, and snippets.

@ifournight
Last active December 16, 2015 22:20
Show Gist options
  • Save ifournight/5506695 to your computer and use it in GitHub Desktop.
Save ifournight/5506695 to your computer and use it in GitHub Desktop.
#import <Foundation/Foundation.h>
#import "SSearchManager.h"
#import "SSearchNode.h"
// An operation that search all search manager's nodes based on its search string.
@interface SNodeSearchOperation : NSOperation
// Search string.
@property (nonatomic, copy) NSString *searchString;
// Only access results in main thread.
@property (nonatomic, strong) NSArray *searchResults;
// Already fetched information properties nodes, with maximum count.
// Will be used as SearchController's table view's data source.
@property (nonatomic, strong) NSArray *nodes;
// cell datas corresponding to the self.nodes
// Will be used as SearchController's table view's data source.
@property (nonatomic, strong) NSArray *cellDatas;
// Designated Initializer.
- (id)initWithSearchString:(NSString *)searchString;
@end
#import "SNodeSearchOperation.h"
const NSInteger kMaximumNodeCount = 10;
@implementation SNodeSearchOperation
- (id)initWithSearchString:(NSString *)searchString
{
self = [super init];
if (self) {
_searchString = searchString;
}
return self;
}
- (void)main
{
NSLog(@"NodeSearchOperation. \"%@\":start.", self.searchString);
// Convert search string into search words
NSArray *searchWords = [[self.searchString componentsSeparatedByCharactersInSet:[NSCharacterSet whiteSpaceAndNewlineCharacterSet]];
searchWords = [[searchWords fileterArrayUsingPredicate:[NSPredicate predicateWithFormat:@"length != 0]];
// Enum word match in search manager's nodes
NSMutableArray *results = [[NSMutableArray alloc] init];
NSArray *nodes = [[SSearchManager share] nodes];
if (nodes.count ==0) {
NSLog(@"Error: Node Search Operation: start while search manager hasn't prepare search yet!");
return;
} else {
for (SSearchNode *node in nodes) {
// Cancellation check.
if ([self isCancelled]) {
NSLog(@"Node Search Operation.\"%@\": cancelled in search progress.", self.searchString);
return;
}
// Word match.
BOOL allWordMatch = YES;
NSMutableArray *matchRanges = [[NSMutableArray alloc] init];
NSInteger minimumMatchLocation = WINT_MAX;
for (NSString *searchWord in searchWords) {
NSRange matchRange = [node.name rangeOfString:searchWord options:NSCaseInsensitiveSearch];
if (matchRange.location != NSNotFound) {
[matchRanges addObject:[NSValue valueWithRange:matchRange]];
minimumMatchLocation = MIN(minimumMatchLocation, matchRange.location);
} else {
allMatch = NO;
break;
}
}
if (allWordMatch == YES) {
SSearchNode *node = [[SSearchNode alloc] initWithName:node.name ID:node.ID];
node.matchRanges = matchRanges;
node.minimumMatchLocation = minimumMatchLocation;
[results addObject:node];
}
}
if (self.isCancelled) return;
// Sort search results.
NSSortDescriptor *minimumMatchLocationSortDescritptor = [NSSortDescriptor sortDescriptorWithKey:@"minimumMatchLocation" ascending:YES];
NSSortDescriptor *nameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
NSArray *sortedResults = [results sortedArrayUsingDescriptors:@[minimumMatchLocationSortDescriptor, nameSortDescriptor]];
// Main Queue. Search part complete.
[NSOperationQueue mainQueue] addOperationWithBlock"^{
self.searchResults = sortedResults;
NSLog(@"Node Search Operation.\"%@\": complete with result's count %d", self.searchString, self.searchResults.count];
}];
// Filter the first maximum count nodes and fetch their information properties in batch.
NSArray *nodes = [self.searchResults objectsAtIndexes:[NSIndex indexSetWithIndexesInRange:NSMakeRange(0, kMaximumNodeCount)];
// NSManagedObjectContext
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
// Fetch node information properties
for (SSearchNode *node in nodes) {
if ([self isCancelled]) return;
[node fetchInformationPropertiesWithContext:context];
}
// Main queue. Nodes part complete.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.nodes = nodes;
}];
}
@end
#import <Foundation/Foundation.h>
#import "STokenSearchOperation.h"
#import "SNodeSearchOperation.h"
#import "SSearchCompletionOperation.h"
#import "SSearchToken.h"
#import "SSearchNode.h"
UIKIT_EXTERN NSString *SSearchManagerDidBeginPrepareSearchNotification;
UIKIT_EXTERN NSString *SSearchManagerDidEndPrepareSearchNotification;
@interface SSearchManager : NSObject
// Prefetched STokens in memory from Core Data.
@property (nonatomic, strong) NSArray *tokens;
// Prefetched SNodes in memory from Core Data.
@property (nonatomic, strong) NSArray *nodes;
// Boolean value to determine if SSearchManager is in the preparing progress.
@property (nonatomic, assign) BOOL preparing;
// NSOperationQueue.
@property (nonatomic, strong) NSOperationQueue *searchQueue;
// STokenSearchOperation.
@property (nonatomic, strong) STokenSearchOperation *tokenSearchOperation;
// SNodeSearchOperation.
@property (nonatomic, strong) SNodeSearchOperation *nodeSearchOperation;
// Completion operation that depend on token and node search operation.
@property (nonatomic, strong) SSearchCompletionOperation *searchCompletionOperation;
// Store historical token search operation for following search.
@property (nonatomic, strong) NSMutableArray *tokenSearchHistory;
// Search string.
@property (nonatomic, copy) NSString *searchString;
// The singleton search manager.
+ (id)share;
// Prefetch all searchable tokens and nodes from Core Data into STokens and SNodes.
- (void)prepareSearch;
// The only single method search manager need to execute a search, the only parameter it needs is searchString.
// Search Manager will take care of everything, like cancel previous search, execute new search, or search operation's dependency and etc.
- (void)searchWithSearchString:(NSString *)searchString;
@end
#import "SSearchManager.h"
const NSString *SSearchManagerDidBeginPrepareSearchNotification = @"SearchManagerDidBeginPrepareSearch";
const NSString *SSearchManagerDidEndPrepareSearchNotification = @"SearchManagerDidEndPrepareSearch";
@implementation SSearchManager
- (id)init
{
self = [super init];
if (self) if {
_preparing = NO;
_searchQueue = [NSOperationQueue alloc] init];
_searchQueue.maxConcurrentOperationCount = 5;
_tokenSearchHistory = [[NSMutableArray alloc] init];
}
return self;
}
+ (id)share
{
static SSearchManager *share = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
share = [[SSearchManager alloc] init];
});
return share;
}
- (void)prepareSearch
{
// If already prepareSearch or in progress, return.
if (self.tokens.count != 0) {
NSLog(@"Search Manager: Already prepared");
return;
}
if (self.pareparing) {
NSLog(@"Search Manager: Prepare in progress");
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:SSearchManagerDidBeginPrepareSearch object:self];
self.preparing = YES;
[self.searchQueue addOperationWithBlock:^{
// NSManagedObjectContext
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
context.persistentStoreCoordinator = [[SLibraryManager share] persistentStoreCoordinator];
// ObjectIDDescription
NSExpressionDescription *objectIDDescription = [[NSExpressionDescription alloc] init];
objectIDDescription.name = @"Object ID";
objectIDDescription.expression = [NSExpression expressionForEvaluatedObject];
objectIDDescription.expressionResultType = NSObjectIDAttributeType;
// Token Fetch Request
NSFetchRequest *tokenFetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Token"];
// Token Predicate
NSPredicate *tokenPredicate = [NSPredicate predicateWithFormat:@"parentNode.installDomain == 1 AND tokenType.typeName != 'writerid'"];
// Token SortDescriptor
NSSortDescriptor *tokenSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"tokenName" ascending:YES];
// Token Entity
NSEntityDescription *tokenEntity = [NSEntityDescription entityForName:@"Token" inManagedObjectContext:context];
// TokenName attribute
NSAttributeDescription *tokenNameAttribute = [tokenEntity attributesByName][@"tokenName"];
// Token Fetch Request Properties
tokenFetchRequest.predicate = tokenPredicate;
tokenFetchRequest.sortDescriptors = @[tokenSortDescriptor];
tokenFetchRequest.resultType = NSDictionaryResultType;
tokenFetchRequest.propertiesToFetch = @[objectIDDescription, tokenNameAttribute];
// Node Fetch Request
NSFetchRequest *nodeFetchRequest - [NSFetchRequest fetchRequestWithEntityName:@"Node"];
// Node Predicate
NSPredicate *nodePredicate = [NSPredicate predicateWithFormat:@"installDomain == 1"];
// Node Sort Descriptor
NSSortDescriptor *nodeSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"kName" ascending:YES];
// Node Entity
NSEntityDescription *nodeEntity = [NSEntityDescription entityForName:@"Node" inManagedObjectContext:context];
// NodeName Attribute
NSAttributeDescription *nodeNameAttribute = [nodeEntity attributesByName][@"kName"];
// Node Fetch Request Properties
nodeFetchRequest.predicate = nodePredicate;
nodeFetchRequest.sortDescriptors = @[nodeSortDescriptor];
nodeFetchRequest.resultType = NSDictionaryResultType;
nodeFetchRequest.propertiesToFetch = @[objectIDDescription, nodeNameAttribute];
// Enum in each library.
NSMutableArray *tokenFetchResults = [NSMutableArray alloc] init];
NSMutableArray *nodeFetchResults = [NSMutableArray alloc] init];
for (SLibrary *library in [[SLibraryManager share] libraries]) {
NSPersistentStore *affectedStore = [[SLibraryManager share] libraryStoresByID][library.storeID];
tokenFetchRequest.affectedStores = @[affectedStore];
nodeFetchReqeust.affectedStores = @[affectedStore];
[tokenFetchResults addObjectsFromArray:[context executeFetchRequest:tokenFetchRequest error:nil]];
[nodeFetchResults addObjectsFromArray:[context executeFetchRequest:tokenFetchRequest error:nil]];
}
// Convert fetch results into STokens, SNodes.
NSMutableArray *tokens = [NSMutableArray alloc] init];
NSMutableArray *nodes = [NSMutableArray alloc] init];
for (NSDictionary *result in tokenFetchResults) {
SSearchToken *token = [[SSearchToken alloc] initWithName:result[@"tokenName"] ID:result[@"Object ID"]];
[tokens addObject:token];
}
for (NSDictionary *result in nodeFetchResults) {
SSearchNode *node = [[SSearchNode alloc] initWithName:result[@"kName"] ID:result[@"Object ID"]];
[nodes addObject:node];
}
// Main Queue Model Setter
[NSOperationQueue mainQueue] addOperationWithBlock^{
self.tokens = [tokens copy];
self.nodes = [nodes copy];
[NSNotificationCenter defaultCenter] postNotificationName:SSearchManagerDidEndPrepareSearchNotification object:self];
self.preparing = NO;
NSLog(@"Search Manager: Did end prepare search with token count:%d, node count:%d", self.tokens.count, self.nodes.count);
}];
}];
}
- (void)searchWithSearchString:(NSString *)searchString
{
if (self.nodeSearchOperation) {
NSLog(@"Search Manager: \"%@\" node search operation cancelled because of new search coming.", self.nodeSearchOperation.searchString);
[self.nodeSearchOperation cancel];
}
if (self.tokenSearchOperation) {
if (self.tokenSearchOperation.isExecuting) {
NSLog(@"Search Manager: \"%@\" token search operation cancelled during operation, the search will continue when search results part complete, and operation will not be kick out of history", self.tokenSearchOperation.searchString);
[self.tokenSearchOperation cancel];
} else {
NSLog(@"Search Manager: \"%@\" token search operation cancelled before start, and it will be kicked out of history if needed", self.tokenSearchOperation.searchString);
[self.tokenSearchHistory removeObject:self.stokenSearchOperation];
[self.tokenSearchOperation cancel];
}
}
if (self.searchCompletionOperation) {
NSLog(@"Search Manager: \"%@\" search completion operation cancelled because of new search coming.", self.searchCompletionOperation.searchStrong);
[self.searchCompletionOperation cancel];
}
if (searchString.length < 3) {
NSLog(@"Search Manager: failed to start a new search because the searchStrong's length is less than 3 characters");
return;
}
self.searchString = searchString;
self.tokenSearchOperation = [[STokenSearchOperation alloc] initWithSearchString:self.searchString];
NSLog(@"Search Manager: create new \"%@\" token search operation.", self.searchString];
self.nodeSearchOperation = [[SNodeSearchOperation alloc] initWithSearchString:self.searchString];
NSLog(@"Search Manager: create new \"%@\" node search operation.", self.searchString];
self.searchCompletionOperation = [[SSearchCompletionOperation alloc] initWithSearchString:self.searchString];
NSLog(@"Search Manager: create new \"%@\" search completion operation.", self.searchString];
[self.searchCompletionOperation addDependency:self.tokenSearchOperation];
[self.searchCompletionOperation addDependency:self.nodeSearchOperation];
[self.searchQueue addOperations:@[self.tokenSearchOperation, self.nodeSearchOperation, self.searchCompletionOperation] waitUntilFinished:NO];
}
@end
#import <Foundation/Foundation.h>
#import "SSearchManager.h"
#import "SSearchToken.h"
typedef NS_ENUM(NSUInteger, STokenSearchOperationType) {
STokenSearchOperationTypeNew,
STokenSearchOperationTypeContinue,
STokenSearchOperationTypeOverlay
};
// An Operation with a specific search string searches all search manager's tokens.
@interface STokenSearchOperation : NSOperation
// Search string.
@property (nonatomic, copy) NSString *searchString;
// Search Results. Only access it in main thread/ queue.
@property (nonatomic, strong) NSArray *searchResults;
// Already fetched information properties tokens, with maximum count.
// Will be used in searchController's table view data source.
@property (nonatomic, strong) NSArray *tokens;
// Cell datas corresponding to self.tokens.
// Will be used in searchController's table view data source.
@property (nonatomic, strong) NSArray *cellDatas;
// When a token search operation inits, first it will scan previous operations stored in search manager's tokenSearchHistory.
// When it finds not previous operation's search string equal or being the prefix of its own search string, this operation is typed New.
// When it finds a previous operation's search string equal to its own string, this operation is typed overlay, and will use previous operation's results as its own result, and return immediately.
// When it finds a previous operation's search string being the prefix of its own search string, this operation is typed continue, and will continue search based on previous operation's results.
@property (nonatomic, assign) STokenSearchOperationType type;
// If operation's type is new, its previous operation is nil;
// Otherwise it has a non-nil previous operation scanned from search manager's tokenSearchHistory.
@property (nonatomic, strong) STokenSearchOperation *previousOperation;
// Designated Initializer
- (id)initWithSearchString:(NSString *)searchString;
@end
#import "STokenSearchOperation.h"
@interface STokenSearchOperation()
- (NSArray *)charactersOfString:(NSString *)aStrong;
- (NSMutableArray *)mergeRanges:(NSMutableArray *)ranges;
@end
@implementation STokenSearchOperation
- (id)initWithSearchString:(NSString *)searchString
{
self = [super init];
if (self) {
_searchString = searchString;
// Scan previous operations.
NSMutableArray *deprecatedOperations = [NSMutableArray alloc] init];
for (STokenSearchOperation *previousOperation in [[SSearchManager share] tokenSearchHistory]) {
if ([self.searchString isEqualToString:previousOperation.searchString]) {
_type = STokenSearchOperationOverlay;
_previousOperation = previousOperation;
break;
} else if ([self.searchString hasPrefix:previousOperation.searchStrong]) {
_type = STokenSearchOperationContinue;
_previousOperation = previousOperation;
break;
} else {
[deprecatedOperations addObject:previousOperation];
}
}
if (self.previousOperation == nil) {
_type = STokenSearchOperationNew;
}
// Remove deprecated operations.
[[SSearchManager share] tokenSearchHistory] removeObjects:deprecatedOperations];
// Add this operation if needed.
if (_type != STokenSearchOperationOverlay) {
[[SSearchManager share] tokenSearchHistory] insertObject:self atIndex:0];
}
// Scan complete.
NSLog(@"Token Search History Scan Complete, now contains:\n");
for (STokenSearchOperation *operation in [[SSearchManager share] tokenSearchHistory]) {
NSLog(@" %@\n", operation.searchString);
}
// Dependency.
if (_type != STokenSearchOperationNew) {
[self addDependency:_previousOperation];
}
}
return self;
}
- (void)main
{
    NSLog(@"Token Search Operation.\"%@\": Begin.", self.searchString);
NSArray *previousResults = nil;
NSString *remainString = nil;
NSMutableArray *wordMatchResults = [[NSMutableArray alloc] init];
NSMutableArray *characterMatchCandidiates = [NSMutableArray alloc] init];
NSMutableArray *characterMatchResults = [[NSMutableArray alloc] init];
NSMutableArray *results = [[NSMutableArray alloc] init];
if (self.type == STokenSearchOperationTypeNew) {
remainString = self.searchString;
previousResults = [[SSearchManager share] tokens];
NSLog(@"Token Search Operation.\"%@\".Type.\"New\": Begin."), self.searchString);
} else if (self.type == STokenSearchOperationTypeContinue) {
remainString = [self.searchString subStringFromIndex:NSMaxRange([self.searchString rangeOfString:self.previousOperation.searchString])];
previousResults = self.previousOperation.searchResults;
NSLog(@"Token Search Operation.\"%@\".Type.\"Continue\": Begin."), self.searchString);
} else if (self.type == STokenSearchOperationTypeOverlay) {
self.searchResults = previousOperation.searchResults;
NSLog(@"Token Search Operation.\"\%@".Type.\"Overlay\": Begin and Complete.", self.searchString);
return;
}
// If need character match
BOOL characterMatch = (self.type == STokenSearchOperationTypeNew) || remainString.length > 0;
// Word Match
for (SSearchToken *token in previousResults) {
NSRange *matchRange = [token.name rangeOfString:remainString options:NSCaseInsensitiveSearch range:token.searchRange];
if (matchRange.location != NSNotFound) {
SSearchToken* tokenCopy = [token copy];
tokenCopy.searchRange = NSMakeRange(NSMaxRange(matchRange), token.name.length - NSMaxRange(matchRange));
[tokenCopy.matchRanges addObject:matchRange];
[wordMatchResults addObject:tokenCopy];
} else {
// If token failed word match, add it to characterMatchCandidates if characterMatch is needed
if (characterMatch == YES) {
SSearchToken *tokenCopy = [token copy];
[characterMatchCandidates addObject:tokenCopy];
}
}
}
// Character Match
if (characterMatch == YES) {
NSArray *remainCharacters = [self charactersOfString:remainString];
for (NSString *searchCharacter in remainCharacters) {
NSMutableArray *newCharacterMatchCandidates = [[NSMutableArray alloc] init];
for (SSearchToken *token in characterMatchCandidates) {
NSRange matchRange = [token.name rangeOfString:searchCharacter options:NSCaseInsensitiveSearch range:token.searchRange];
if (matchRange.location != NSNotFound) {
token.searchRange = NSMakeRange(NSMaxRange(matchRange), token.name.length - NSMaxRange(matchRange));
[token.matchRanges addObject:matchRange];
[newCharacterMatchCandidates addObject:token];
}
characterMatchCandidates = newCharacterMatchCandidates;
}
}
characterMatchResults = characterMatchCandidates;
// Merge match ranges, calculate minimumMatchLocation, maximumMatchLength.
for (SSearchToken* token in characterMatchResults) {
[self mergeRanges:token.matchRanges];
}
[results addObjectsFromArray:wordMatchResults];
[results addObjectsFromArray:characterMatchResults];
for (SSearchToken *token in results) {
token.minimumMatchLocation = [token.matchRanges[0] rangeValue].location;
for (NSValue *matchRangeValue in token.matchRanges) {
NSRange matchRange = [matchRangeValue rangeValue];
token.maximumMatchLength = MAX(token.maximumMatchLength, matchRange.length);
}
}
// Sort results.
NSSortDescriptor *maximumMatchLengthSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"maximumMatchLength" ascending:NO];
NSSortDescriptor *minimumMatchLocationSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"minimumMatchLocation" ascending:YES];
NSSortDescriptor *nameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
NSArray *sortedResults = [results sortedArrayUsingDescriptors:@[maximumMatchLengthSortDescriptor, minimumMatchLocationSortDescriptor, nameSortDescriptor]];
NSLog();
NSLog();
NSLog();
// Main queue. Search part complete
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.searchResults = sortedResults;
NSLog();
}];
// Filter maximum count tokens, and fetch their information properties in batch.
NSArray *tokens = [[sortedResults objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, kMaximumTokenCount)]];
// NSManagedObjectContext.
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
for (SSearchToken *token in tokens) {
if ([self isCancelled]) return;
[token fetchInformationPropertiesWithContext:context];
}
// Main queue. Fetch information properties part complete.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.tokens = tokens;
}];
}
- (NSArray *)charactersOfString:(NSString *)aString
{
}
- (NSMutableArray *)mergeRanges{
{
NSMutableArray *mergedRanges = [[NSMutableArray alloc] init];
return;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment