// // NSArray+Diffing.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import #import #import #import @implementation NSArray (Diffing) typedef BOOL (^compareBlock)(id _Nonnull lhs, id _Nonnull rhs); - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions { [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:[NSArray defaultCompareBlock]]; } - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(compareBlock)comparison { [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:comparison]; } - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions moves:(NSArray **)moves { [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:moves compareBlock:[NSArray defaultCompareBlock]]; } - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions moves:(NSArray **)moves compareBlock:(compareBlock)comparison { struct NSObjectHash { std::size_t operator()(id k) const { return (std::size_t) [k hash]; }; }; struct NSObjectCompare { bool operator()(id lhs, id rhs) const { return (bool) [lhs isEqual:rhs]; }; }; std::unordered_multimap potentialMoves; NSAssert(comparison != nil, @"Comparison block is required"); NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)"); NSMutableArray *moveIndexPaths = nil; NSMutableIndexSet *insertionIndexes = nil, *deletionIndexes = nil; if (moves) { moveIndexPaths = [NSMutableArray new]; } NSMutableIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison]; if (deletions || moves) { deletionIndexes = [NSMutableIndexSet indexSet]; NSUInteger i = 0; for (id element in self) { if (![commonIndexes containsIndex:i]) { [deletionIndexes addIndex:i]; } if (moves) { potentialMoves.insert(std::pair(element, i)); } ++i; } } if (insertions || moves) { insertionIndexes = [NSMutableIndexSet indexSet]; NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; for (NSUInteger i = 0, j = 0; j < array.count; j++) { auto moveFound = potentialMoves.find(array[j]); NSUInteger movedFrom = NSNotFound; if (moveFound != potentialMoves.end() && moveFound->second != j) { movedFrom = moveFound->second; potentialMoves.erase(moveFound); [moveIndexPaths addObject:[NSIndexPath indexPathForItem:j inSection:movedFrom]]; } if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) { i++; } else { if (movedFrom != NSNotFound) { // moves will coalesce a delete / insert - the insert is just not done, and here we remove the delete: [deletionIndexes removeIndex:movedFrom]; // OR a move will have come from the LCS: if ([commonIndexes containsIndex:movedFrom]) { [commonIndexes removeIndex:movedFrom]; commonObjects = [self objectsAtIndexes:commonIndexes]; } } else { [insertionIndexes addIndex:j]; } } } } if (moves) {*moves = moveIndexPaths;} if (deletions) {*deletions = deletionIndexes;} if (insertions) {*insertions = insertionIndexes;} } // https://github.com/raywenderlich/swift-algorithm-club/tree/master/Longest%20Common%20Subsequence is not exactly this code (obviously), but // is a good commentary on the algorithm. - (NSMutableIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison { NSAssert(comparison != nil, @"Comparison block is required"); NSInteger selfCount = self.count; NSInteger arrayCount = array.count; // Allocate the diff map in the heap so we don't blow the stack for large arrays. NSInteger **lengths = NULL; lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1)); if (lengths == NULL) { ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); return nil; } // Fill in a LCS length matrix: for (NSInteger i = 0; i <= selfCount; i++) { lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1)); if (lengths[i] == NULL) { ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); return nil; } id selfObj = i > 0 ? self[i-1] : nil; for (NSInteger j = 0; j <= arrayCount; j++) { if (i == 0 || j == 0) { lengths[i][j] = 0; } else if (comparison(selfObj, array[j-1])) { lengths[i][j] = 1 + lengths[i-1][j-1]; } else { lengths[i][j] = MAX(lengths[i-1][j], lengths[i][j-1]); } } } // Backtrack to fill in indices based on length matrix: NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; NSInteger i = selfCount, j = arrayCount; while(i > 0 && j > 0) { if (comparison(self[i-1], array[j-1])) { [common addIndex:(i-1)]; i--; j--; } else if (lengths[i-1][j] > lengths[i][j-1]) { i--; } else { j--; } } for (NSInteger i = 0; i <= selfCount; i++) { free(lengths[i]); } free(lengths); return common; } static compareBlock defaultCompare = nil; + (compareBlock)defaultCompareBlock { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultCompare = ^BOOL(id lhs, id rhs) { return [lhs isEqual:rhs]; }; }); return defaultCompare; } @end