diff --git a/HeckelDiff.xcodeproj/project.pbxproj b/HeckelDiff.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3b5b367 --- /dev/null +++ b/HeckelDiff.xcodeproj/project.pbxproj @@ -0,0 +1,431 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D829E61D1DE5039500560BD4 /* HeckelDiff.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D829E6131DE5039500560BD4 /* HeckelDiff.framework */; }; + D829E6241DE5039500560BD4 /* HeckelDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = D829E6161DE5039500560BD4 /* HeckelDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D829E62E1DE5047600560BD4 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829E62D1DE5047600560BD4 /* Diff.swift */; }; + D829E6301DE504A400560BD4 /* DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829E62F1DE504A400560BD4 /* DiffTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D829E61E1DE5039500560BD4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D829E60A1DE5039500560BD4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D829E6121DE5039500560BD4; + remoteInfo = HeckelDiff; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + D829E6131DE5039500560BD4 /* HeckelDiff.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HeckelDiff.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D829E6161DE5039500560BD4 /* HeckelDiff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HeckelDiff.h; sourceTree = ""; }; + D829E6171DE5039500560BD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HeckelDiffTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D829E6231DE5039500560BD4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D829E62D1DE5047600560BD4 /* Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diff.swift; sourceTree = ""; }; + D829E62F1DE504A400560BD4 /* DiffTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D829E60F1DE5039500560BD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D829E6191DE5039500560BD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D829E61D1DE5039500560BD4 /* HeckelDiff.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D829E6091DE5039500560BD4 = { + isa = PBXGroup; + children = ( + D829E6151DE5039500560BD4 /* Source */, + D829E6201DE5039500560BD4 /* Tests */, + D829E6141DE5039500560BD4 /* Products */, + ); + sourceTree = ""; + }; + D829E6141DE5039500560BD4 /* Products */ = { + isa = PBXGroup; + children = ( + D829E6131DE5039500560BD4 /* HeckelDiff.framework */, + D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D829E6151DE5039500560BD4 /* Source */ = { + isa = PBXGroup; + children = ( + D829E6161DE5039500560BD4 /* HeckelDiff.h */, + D829E6171DE5039500560BD4 /* Info.plist */, + D829E62D1DE5047600560BD4 /* Diff.swift */, + ); + path = Source; + sourceTree = ""; + }; + D829E6201DE5039500560BD4 /* Tests */ = { + isa = PBXGroup; + children = ( + D829E6231DE5039500560BD4 /* Info.plist */, + D829E62F1DE504A400560BD4 /* DiffTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D829E6101DE5039500560BD4 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D829E6241DE5039500560BD4 /* HeckelDiff.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D829E6121DE5039500560BD4 /* HeckelDiff */ = { + isa = PBXNativeTarget; + buildConfigurationList = D829E6271DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiff" */; + buildPhases = ( + D829E60E1DE5039500560BD4 /* Sources */, + D829E60F1DE5039500560BD4 /* Frameworks */, + D829E6101DE5039500560BD4 /* Headers */, + D829E6111DE5039500560BD4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HeckelDiff; + productName = HeckelDiff; + productReference = D829E6131DE5039500560BD4 /* HeckelDiff.framework */; + productType = "com.apple.product-type.framework"; + }; + D829E61B1DE5039500560BD4 /* HeckelDiffTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D829E62A1DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiffTests" */; + buildPhases = ( + D829E6181DE5039500560BD4 /* Sources */, + D829E6191DE5039500560BD4 /* Frameworks */, + D829E61A1DE5039500560BD4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D829E61F1DE5039500560BD4 /* PBXTargetDependency */, + ); + name = HeckelDiffTests; + productName = HeckelDiffTests; + productReference = D829E61C1DE5039500560BD4 /* HeckelDiffTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D829E60A1DE5039500560BD4 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0810; + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = "Matias Cudich"; + TargetAttributes = { + D829E6121DE5039500560BD4 = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = 46UKZ786J3; + LastSwiftMigration = 0810; + ProvisioningStyle = Automatic; + }; + D829E61B1DE5039500560BD4 = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = 46UKZ786J3; + LastSwiftMigration = 0810; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = D829E60D1DE5039500560BD4 /* Build configuration list for PBXProject "HeckelDiff" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D829E6091DE5039500560BD4; + productRefGroup = D829E6141DE5039500560BD4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D829E6121DE5039500560BD4 /* HeckelDiff */, + D829E61B1DE5039500560BD4 /* HeckelDiffTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D829E6111DE5039500560BD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D829E61A1DE5039500560BD4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D829E60E1DE5039500560BD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D829E62E1DE5047600560BD4 /* Diff.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D829E6181DE5039500560BD4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D829E6301DE504A400560BD4 /* DiffTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D829E61F1DE5039500560BD4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D829E6121DE5039500560BD4 /* HeckelDiff */; + targetProxy = D829E61E1DE5039500560BD4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + D829E6251DE5039500560BD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D829E6261DE5039500560BD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D829E6281DE5039500560BD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 46UKZ786J3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Source/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiff; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + D829E6291DE5039500560BD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 46UKZ786J3; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Source/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiff; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + D829E62B1DE5039500560BD4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = 46UKZ786J3; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiffTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + D829E62C1DE5039500560BD4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = 46UKZ786J3; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.matiascudich.HeckelDiffTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D829E60D1DE5039500560BD4 /* Build configuration list for PBXProject "HeckelDiff" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D829E6251DE5039500560BD4 /* Debug */, + D829E6261DE5039500560BD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D829E6271DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiff" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D829E6281DE5039500560BD4 /* Debug */, + D829E6291DE5039500560BD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D829E62A1DE5039500560BD4 /* Build configuration list for PBXNativeTarget "HeckelDiffTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D829E62B1DE5039500560BD4 /* Debug */, + D829E62C1DE5039500560BD4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D829E60A1DE5039500560BD4 /* Project object */; +} diff --git a/HeckelDiff.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/HeckelDiff.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1a4fae4 --- /dev/null +++ b/HeckelDiff.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Source/Diff.swift b/Source/Diff.swift new file mode 100644 index 0000000..0a11750 --- /dev/null +++ b/Source/Diff.swift @@ -0,0 +1,207 @@ +// +// Diff.swift +// HeckelDiff +// +// Created by Matias Cudich on 11/22/16. +// Copyright © 2016 Matias Cudich. All rights reserved. +// + +import Foundation + +public enum Operation: Equatable { + case insert(Int) + case delete(Int) + case move(Int, Int) + case update(Int) +} + +public func ==(lhs: Operation, rhs: Operation) -> Bool { + switch (lhs, rhs) { + case let (.insert(lhsIndex), .insert(rhsIndex)): + return lhsIndex == rhsIndex + case let (.delete(lhsIndex), .delete(rhsIndex)): + return lhsIndex == rhsIndex + case let (.move(lhsFromIndex, lhsToIndex), .move(rhsFromIndex, rhsToIndex)): + return lhsFromIndex == rhsFromIndex && lhsToIndex == rhsToIndex + case let (.update(lhsIndex), .update(rhsIndex)): + return lhsIndex == rhsIndex + default: + return false + } +} + +enum Counter { + case zero + case one + case many + + mutating func increment() { + switch self { + case .zero: + self = .one + case .one: + self = .many + case .many: + break + } + } +} + +class SymbolEntry { + var oc: Counter = .zero + var nc: Counter = .zero + var olno = [Int]() + + var occursInBoth: Bool { + return oc != .zero && nc != .zero + } +} + +enum Entry { + case symbol(SymbolEntry) + case index(Int) +} + +// Based on http://dl.acm.org/citation.cfm?id=359467. +// +// And other similar implementations at: +// * https://github.com/Instagram/IGListKit +// * https://github.com/andre-alves/PHDiff +// +public func diff(_ old: T, _ new: T) -> [Operation] where T.Iterator.Element: Hashable, T.IndexDistance == Int, T.Index == Int { + var table = [Int: SymbolEntry]() + var oa = [Entry]() + var na = [Entry]() + + // Pass 1 comprises the following: (a) each line i of file N is read in sequence; (b) a symbol + // table entry for each line i is created if it does not already exist; (c) NC for the line's + // symbol table entry is incremented; and (d) NA [i] is set to point to the symbol table entry of + // line i. + for item in new { + let entry = table[item.hashValue] ?? SymbolEntry() + table[item.hashValue] = entry + entry.nc.increment() + na.append(.symbol(entry)) + } + + // Pass 2 is identical to pass 1 except that it acts on file O, array OA, and counter OC, + // and OLNO for the symbol table entry is set to the line's number. + for (index, item) in old.enumerated() { + let entry = table[item.hashValue] ?? SymbolEntry() + table[item.hashValue] = entry + entry.oc.increment() + entry.olno.append(index) + oa.append(.symbol(entry)) + } + + // In pass 3 we use observation 1 and process only those lines having NC = OC = 1. Since each + // represents (we assume) the same unmodified line, for each we replace the symbol table pointers + // in NA and OA by the number of the line in the other file. For example, if NA[i] corresponds to + // such a line, we look NA[i] up in the symbol table and set NA[i] to OLNO and OA[OLNO] to i. + // In pass 3 we also "find" unique virtual lines immediately before the first and immediately + // after the last lines of the files. + for (index, item) in na.enumerated() { + if case .symbol(let entry) = item, entry.occursInBoth { + guard entry.olno.count > 0 else { continue } + + let oldIndex = entry.olno.removeFirst() + na[index] = .index(oldIndex) + oa[oldIndex] = .index(index) + } + } + + // In pass 4, we apply observation 2 and process each line in NA in ascending order: If NA[i] + // points to OA[j] and NA[i + 1] and OA[j + 1] contain identical symbol table entry pointers, then + // OA[j + 1] is set to line i + 1 and NA[i + 1] is set to line j + 1. + var i = 1 + while (i < na.count - 1) { + if case .index(let j) = na[i], j + 1 < oa.count { + if case .symbol(let newEntry) = na[i + 1], case .symbol(let oldEntry) = oa[j + 1], newEntry === oldEntry { + na[i + 1] = .index(j + 1) + oa[j + 1] = .index(i + 1) + } + } + i += 1 + } + + // In pass 5, we also apply observation 2 and process each entry in descending order: if NA[i] + // points to OA[j] and NA[i - 1] and OA[j - 1] contain identical symbol table pointers, then + // NA[i - 1] is replaced by j - 1 and OA[j - 1] is replaced by i - 1. + i = na.count - 1 + while (i > 0) { + if case .index(let j) = na[i], j - 1 >= 0 { + if case .symbol(let newEntry) = na[i - 1], case .symbol(let oldEntry) = oa[j - 1], newEntry === oldEntry { + na[i - 1] = .index(j - 1) + oa[j - 1] = .index(i - 1) + } + } + i -= 1 + } + + var steps = [Operation]() + + var deleteOffsets = Array(repeating: 0, count: old.count) + var runningOffset = 0 + for (index, item) in oa.enumerated() { + deleteOffsets[index] = runningOffset + if case .symbol(_) = item { + steps.append(.delete(index)) + runningOffset += 1 + } + } + + runningOffset = 0 + + for (index, item) in na.enumerated() { + switch item { + case .symbol(_): + steps.append(.insert(index)) + runningOffset += 1 + case .index(let oldIndex): + // The object has changed, so it should be updated. + if old[oldIndex] != new[index] { + steps.append(.update(index)) + } + + let deleteOffset = deleteOffsets[oldIndex] + // The object is not at the expected position, so move it. + if (oldIndex - deleteOffset + runningOffset) != index { + steps.append(.move(oldIndex, index)) + } + } + } + + return steps +} + +public func orderedDiff(_ old: T, _ new: T) -> [Operation] where T.Iterator.Element: Hashable, T.IndexDistance == Int, T.Index == Int { + let steps = diff(old, new) + + var insertions = [Operation]() + var updates = [Operation]() + var possibleDeletions: [Operation?] = Array(repeating: nil, count: old.count) + + let trackDeletion = { (fromIndex: Int, step: Operation) in + if possibleDeletions[fromIndex] == nil { + possibleDeletions[fromIndex] = step + } + } + + for step in steps { + switch step { + case .insert: + insertions.append(step) + case let .delete(fromIndex): + trackDeletion(fromIndex, step) + case let .move(fromIndex, toIndex): + insertions.append(.insert(toIndex)) + trackDeletion(fromIndex, .delete(fromIndex)) + case .update: + updates.append(step) + } + } + + let deletions = possibleDeletions.flatMap { $0 }.reversed() + + return deletions + insertions + updates +} diff --git a/Source/HeckelDiff.h b/Source/HeckelDiff.h new file mode 100644 index 0000000..b256de8 --- /dev/null +++ b/Source/HeckelDiff.h @@ -0,0 +1,19 @@ +// +// HeckelDiff.h +// HeckelDiff +// +// Created by Matias Cudich on 11/22/16. +// Copyright © 2016 Matias Cudich. All rights reserved. +// + +#import + +//! Project version number for HeckelDiff. +FOUNDATION_EXPORT double HeckelDiffVersionNumber; + +//! Project version string for HeckelDiff. +FOUNDATION_EXPORT const unsigned char HeckelDiffVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Source/Info.plist b/Source/Info.plist new file mode 100644 index 0000000..fbe1e6b --- /dev/null +++ b/Source/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Tests/DiffTests.swift b/Tests/DiffTests.swift new file mode 100644 index 0000000..5f36ba1 --- /dev/null +++ b/Tests/DiffTests.swift @@ -0,0 +1,160 @@ +// +// DiffTests.swift +// HeckelDiff +// +// Created by Matias Cudich on 11/22/16. +// Copyright © 2016 Matias Cudich. All rights reserved. +// + +import XCTest +import HeckelDiff + +struct FakeItem: Hashable { + let value: Int + let eValue: Int + + var hashValue: Int { + return value.hashValue + } +} + +func ==(lhs: FakeItem, rhs: FakeItem) -> Bool { + return lhs.eValue == rhs.eValue +} + +func ==(lhs: (from: Int, to: Int), rhs: (from: Int, to: Int)) -> Bool { + return lhs.0 == rhs.0 && lhs.1 == rhs.1 +} + +class DiffTests: XCTestCase { + func testEmptyArrays() { + let o = [Int]() + let n = [Int]() + let result = diff(o, n) + XCTAssertEqual(0, result.count) + } + + func testDiffingFromEmptyArray() { + let o = [Int]() + let n = [1] + let result = diff(o, n) + XCTAssertEqual(.insert(0), result[0]) + XCTAssertEqual(1, result.count) + } + + func testDiffingToEmptyArray() { + let o = [1] + let n = [Int]() + let result = diff(o, n) + XCTAssertEqual(.delete(0), result[0]) + XCTAssertEqual(1, result.count) + } + + func testSwapHasMoves() { + let o = [1, 2, 3] + let n = [2, 3, 1] + let result = diff(o, n) + XCTAssertEqual([.move(1, 0), .move(2, 1), .move(0, 2)], result) + } + + func testSwapHasMovesWithOrder() { + let o = [1, 2, 3] + let n = [2, 3, 1] + let result = orderedDiff(o, n) + XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result) + } + + func testMovingTogether() { + let o = [1, 2, 3, 3, 4] + let n = [2, 3, 1, 3, 4] + let result = diff(o, n) + XCTAssertEqual([.move(1, 0), .move(2, 1), .move(0, 2)], result) + } + + func testMovingTogetherWithOrder() { + let o = [1, 2, 3, 3, 4] + let n = [2, 3, 1, 3, 4] + let result = orderedDiff(o, n) + XCTAssertEqual([.delete(2), .delete(1), .delete(0), .insert(0), .insert(1), .insert(2)], result) + } + + func testSwappedValuesHaveMoves() { + let o = [1, 2, 3, 4] + let n = [2, 4, 5, 3] + let result = diff(o, n) + XCTAssertEqual([.delete(0), .move(3, 1), .insert(2), .move(2, 3)], result) + } + + func testSwappedValuesHaveMovesWithOrder() { + let o = [1, 2, 3, 4] + let n = [2, 4, 5, 3] + let result = orderedDiff(o, n) + XCTAssertEqual([.delete(3), .delete(2), .delete(0), .insert(1), .insert(2), .insert(3)], result) + } + + func testUpdates() { + let o = [ + FakeItem(value: 0, eValue: 0), + FakeItem(value: 1, eValue: 1), + FakeItem(value: 2, eValue: 2) + ] + let n = [ + FakeItem(value: 0, eValue: 1), + FakeItem(value: 1, eValue: 2), + FakeItem(value: 2, eValue: 3) + ] + let result = diff(o, n) + XCTAssertEqual([.update(0), .update(1), .update(2)], result) + } + + func testDeletionLeadingToInsertionDeletionMoves() { + let o = [0, 1, 2, 3, 4, 5, 6, 7, 8] + let n = [0, 2, 3, 4, 7, 6, 9, 5, 10] + let result = diff(o, n) + XCTAssertEqual([.delete(1), .delete(8), .move(7, 4), .insert(6), .move(5, 7), .insert(8)], result) + } + + func testDeletionLeadingToInsertionDeletionMovesWithOrder() { + let o = [0, 1, 2, 3, 4, 5, 6, 7, 8] + let n = [0, 2, 3, 4, 7, 6, 9, 5, 10] + let result = orderedDiff(o, n) + XCTAssertEqual([.delete(8), .delete(7), .delete(5), .delete(1), .insert(4), .insert(6), .insert(7), .insert(8)], result) + } + + func testMovingWithEqualityChanges() { + let o = [ + FakeItem(value: 0, eValue: 0), + FakeItem(value: 1, eValue: 1), + FakeItem(value: 2, eValue: 2) + ] + let n = [ + FakeItem(value: 2, eValue: 3), + FakeItem(value: 1, eValue: 1), + FakeItem(value: 0, eValue: 0) + ] + let result = orderedDiff(o, n) + XCTAssertEqual([.delete(2), .delete(0), .insert(0), .insert(2), .update(0)], result) + } + + func testDeletingEqualObjects() { + let o = [0, 0, 0, 0] + let n = [0, 0] + let result = diff(o, n) + XCTAssertEqual(2, result.count) + } + + func testInsertingEqualObjects() { + let o = [0, 0] + let n = [0, 0, 0, 0] + let result = diff(o, n) + XCTAssertEqual(2, result.count) + } + + func testInsertingWithOldArrayHavingMultipleCopies() { + let o = [NSObject(), NSObject(), NSObject(), 49, 33, "cat", "cat", 0, 14] as [AnyHashable] + var n = o + n.insert("cat", at: 5) + let result = diff(o, n) + XCTAssertEqual(1, result.count) + } +} diff --git a/Tests/HeckelDiffTests.swift b/Tests/HeckelDiffTests.swift new file mode 100644 index 0000000..9577fe7 --- /dev/null +++ b/Tests/HeckelDiffTests.swift @@ -0,0 +1,36 @@ +// +// HeckelDiffTests.swift +// HeckelDiffTests +// +// Created by Matias Cudich on 11/22/16. +// Copyright © 2016 Matias Cudich. All rights reserved. +// + +import XCTest +@testable import HeckelDiff + +class HeckelDiffTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Tests/Info.plist b/Tests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + +