diff --git a/Sdifft.xcodeproj/project.pbxproj b/Sdifft.xcodeproj/project.pbxproj index 796f883..f7e0045 100644 --- a/Sdifft.xcodeproj/project.pbxproj +++ b/Sdifft.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4BF61A21708396004C5E1F /* DiffSequence.swift */; }; 1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */; }; 1EB1AD2920BD640B004D0450 /* NSAttributedString+DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB1AD2820BD640B004D0450 /* NSAttributedString+DiffTests.swift */; }; OBJ_21 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Diff.swift */; }; @@ -47,6 +48,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1E4BF61A21708396004C5E1F /* DiffSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSequence.swift; sourceTree = ""; }; 1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Diff.swift"; sourceTree = ""; }; 1EB1AD2820BD640B004D0450 /* NSAttributedString+DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+DiffTests.swift"; sourceTree = ""; }; OBJ_12 /* DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = ""; }; @@ -124,6 +126,7 @@ isa = PBXGroup; children = ( OBJ_9 /* Diff.swift */, + 1E4BF61A21708396004C5E1F /* DiffSequence.swift */, 1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */, ); name = Sdifft; @@ -235,6 +238,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + 1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */, 1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */, OBJ_21 /* Diff.swift in Sources */, ); diff --git a/Sources/Sdifft/Diff.swift b/Sources/Sdifft/Diff.swift index d370a9f..f29ea16 100644 --- a/Sources/Sdifft/Diff.swift +++ b/Sources/Sdifft/Diff.swift @@ -25,31 +25,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -extension String { - /// Return character in string - /// - /// - Parameter idx: index - subscript (idx: Int) -> Character { - return self[index(startIndex, offsetBy: idx)] - } -} +import Foundation typealias Matrix = [[Int]] // swiftlint:disable identifier_name -/// Draw LCS matrix with two strings +/// Draw LCS matrix with two `DiffSequence` /// /// - Parameters: -/// - from: string -/// - to: string that be compared +/// - from: DiffSequence +/// - to: DiffSequence that be compared /// - Returns: matrix -func drawMatrix(from: String, to: String) -> Matrix { +func drawMatrix(from: T, to: T) -> Matrix { let row = from.count + 1 let column = to.count + 1 var result: [[Int]] = Array(repeating: Array(repeating: 0, count: column), count: row) for i in 1.. [DiffIndex] { +/// - same: same element's indexes +/// - Returns: same element's indexes +func lcs(from: T, to: T, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] { if position.row == 0 || position.column == 0 { return same } - if from[position.row - 1] == to[position.column - 1] { + if from.index(of: position.row - 1) == to.index(of: position.column - 1) { return lcs(from: from, to: to, position: (position.row - 1, position.column - 1), matrix: matrix, same: same + [(position.row - 1, position.column - 1)]) } else if matrix[position.row - 1][position.column] >= matrix[position.row][position.column - 1] { return lcs(from: from, to: to, position: (position.row - 1, position.column), matrix: matrix, same: same) @@ -85,44 +78,31 @@ func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [Di } } -public struct Modification { - public let add: String? - public let delete: String? - public let same: String? -} - -extension String { - /// Return string with range - /// - /// - Parameter range: range - subscript(_ range: CountableClosedRange) -> String { - let start = index(startIndex, offsetBy: range.lowerBound) - let end = index(startIndex, offsetBy: range.upperBound) - return String(self[start...end]) - } +public struct Modification { + public let add, delete, same: Element? } extension Array where Element == DiffIndex { - func modifications(from: String, to: String) -> [Modification] { - var modifications: [Modification] = [] + func modifications(from: T, to: T) -> [Modification] { + var modifications: [Modification] = [] var lastFrom = 0 var lastTo = 0 modifications += map { let modification = - Modification( - add: lastTo <= $0.to - 1 ? to[lastTo...$0.to - 1] : nil, - delete: lastFrom <= $0.from - 1 ? from[lastFrom...$0.from - 1] : nil, - same: to[$0.to...$0.to] - ) + Modification( + add: lastTo <= $0.to - 1 ? to.element(withRange: lastTo...$0.to - 1) : nil, + delete: lastFrom <= $0.from - 1 ? from.element(withRange: lastFrom...$0.from - 1) : nil, + same: to.element(withRange: $0.to...$0.to) + ) lastFrom = $0.from + 1 lastTo = $0.to + 1 return modification } if lastFrom <= from.count - 1 || lastTo <= to.count - 1 { modifications.append( - Modification( - add: lastTo <= to.count - 1 ? to[lastTo...to.count - 1] : nil, - delete: lastFrom <= from.count - 1 ? from[lastFrom...from.count - 1] : nil, + Modification( + add: lastTo <= to.count - 1 ? to.element(withRange: lastTo...to.count - 1) : nil, + delete: lastFrom <= from.count - 1 ? from.element(withRange: lastFrom...from.count - 1) : nil, same: nil ) ) @@ -131,21 +111,20 @@ extension Array where Element == DiffIndex { } } -public struct Diff { - public let modifications: [Modification] +public struct Diff { + public let modifications: [Modification] let matrix: Matrix - let from: String - let to: String - public init(from: String, to: String) { - // because LCS is 'bottom-up' - // so them need be reversed to get the normal sequence + let from, to: T + public init(from: T, to: T) { self.from = from self.to = to - let reversedFrom = String(from.reversed()) - let reversedTo = String(to.reversed()) + // because LCS is 'bottom-up' + // so them need be reversed to get the normal sequence + let reversedFrom = from.reversedElement() + let reversedTo = to.reversedElement() matrix = drawMatrix(from: reversedFrom, to: reversedTo) var same = lcs(from: reversedFrom, to: reversedTo, position: (from.count, to.count), matrix: matrix, same: []) - same = same.map({ (from.count - 1 - $0, to.count - 1 - $1) }) + same = same.map { (from.count - 1 - $0, to.count - 1 - $1) } modifications = same.modifications(from: from, to: to) } } diff --git a/Sources/Sdifft/DiffSequence.swift b/Sources/Sdifft/DiffSequence.swift new file mode 100644 index 0000000..7b45923 --- /dev/null +++ b/Sources/Sdifft/DiffSequence.swift @@ -0,0 +1,61 @@ +// +// DiffSequence.swift +// Sdifft +// +// Created by WzxJiang on 18/5/23. +// Copyright © 2018年 WzxJiang. All rights reserved. +// +// https://github.com/Wzxhaha/Sdifft +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public protocol DiffSequence: Equatable { + func index(of idx: Int) -> Self + func element(withRange range: CountableClosedRange) -> Self + func reversedElement() -> Self + var count: Int { get } +} + +extension String: DiffSequence { + public func index(of idx: Int) -> String { + return String(self[index(startIndex, offsetBy: idx)]) + } + public func element(withRange range: CountableClosedRange) -> String { + let start = index(startIndex, offsetBy: range.lowerBound) + let end = index(startIndex, offsetBy: range.upperBound) + return String(self[start...end]) + } + public func reversedElement() -> String { + return String(reversed()) + } +} + +extension Array: DiffSequence where Element == String { + public func index(of idx: Int) -> [Element] { + return [self[idx]] + } + public func element(withRange range: CountableClosedRange) -> [Element] { + return Array(self[range]) + } + public func reversedElement() -> [Element] { + return reversed() + } +} diff --git a/Sources/Sdifft/NSAttributedString+Diff.swift b/Sources/Sdifft/NSAttributedString+Diff.swift index 130d0fb..af36e9c 100644 --- a/Sources/Sdifft/NSAttributedString+Diff.swift +++ b/Sources/Sdifft/NSAttributedString+Diff.swift @@ -28,9 +28,7 @@ import Foundation public struct DiffAttributes { - public let add: [NSAttributedString.Key: Any] - public let delete: [NSAttributedString.Key: Any] - public let same: [NSAttributedString.Key: Any] + public let add, delete, same: [NSAttributedString.Key: Any] // swiftlint:disable line_length public init(add: [NSAttributedString.Key: Any], delete: [NSAttributedString.Key: Any], same: [NSAttributedString.Key: Any]) { self.add = add @@ -46,7 +44,7 @@ extension NSAttributedString { /// - diff: Diff /// - attributes: DiffAttributes /// - Returns: NSAttributedString - public static func attributedString(with diff: Diff, attributes: DiffAttributes) -> NSAttributedString { + public static func attributedString(with diff: Diff, attributes: DiffAttributes) -> NSAttributedString { let attributedString = NSMutableAttributedString() diff.modifications.forEach { if let add = $0.add { @@ -61,4 +59,19 @@ extension NSAttributedString { } return attributedString } + public static func attributedString(with diff: Diff<[String]>, attributes: DiffAttributes) -> NSAttributedString { + let attributedString = NSMutableAttributedString() + diff.modifications.forEach { + if let add = $0.add { + attributedString.append(NSAttributedString(string: add.joined(), attributes: attributes.add)) + } + if let delete = $0.delete { + attributedString.append(NSAttributedString(string: delete.joined(), attributes: attributes.delete)) + } + if let same = $0.same { + attributedString.append(NSAttributedString(string: same.joined(), attributes: attributes.same)) + } + } + return attributedString + } } diff --git a/Tests/SdifftTests/DiffTests.swift b/Tests/SdifftTests/DiffTests.swift index fed025c..029c197 100644 --- a/Tests/SdifftTests/DiffTests.swift +++ b/Tests/SdifftTests/DiffTests.swift @@ -1,7 +1,16 @@ import XCTest @testable import Sdifft -extension Array where Element == Modification { +extension String { + subscript(idx: Int) -> String { + return index(of: idx) + } + subscript(range: CountableClosedRange) -> String { + return element(withRange: range) + } +} + +extension Array where Element == Modification { var sames: [String] { return compactMap { $0.same } } @@ -13,6 +22,18 @@ extension Array where Element == Modification { } } +extension Array where Element == Modification<[String]> { + var sames: [[String]] { + return compactMap { $0.same } + } + var adds: [[String]] { + return compactMap { $0.add } + } + var deletes: [[String]] { + return compactMap { $0.delete } + } +} + class DiffTests: XCTestCase { func testMatrix() { assert( @@ -94,6 +115,14 @@ class DiffTests: XCTestCase { diff3.modifications.adds == [] && diff3.modifications.deletes == ["\r\n", "\r\n"] ) + let to4 = ["a", "bc", "d", "c"] + let from4 = ["d"] + let diff4 = Diff(from: from4, to: to4) + assert( + diff4.modifications.sames == [["d"]] && + diff4.modifications.adds == [["a", "bc"], ["c"]] && + diff4.modifications.deletes == [] + ) } // swiftlint:disable line_length diff --git a/Tests/SdifftTests/NSAttributedString+DiffTests.swift b/Tests/SdifftTests/NSAttributedString+DiffTests.swift index 214e7c3..5d3ef41 100644 --- a/Tests/SdifftTests/NSAttributedString+DiffTests.swift +++ b/Tests/SdifftTests/NSAttributedString+DiffTests.swift @@ -51,6 +51,20 @@ class NSAttributedStringDiffTests: XCTestCase { assert( attributedString2.debugDescription == "a{red}b{black}ex{green}cdhi{red}j{black}k{red}" ) + let to3 = ["bexj", "abc", "c"] + let from3 = ["abcdhijk"] + let diff3 = Diff(from: from3, to: to3) + let attributedString3 = NSAttributedString.attributedString(with: diff3, attributes: diffAttributes) + assert( + attributedString3.debugDescription == "bexjabcc{green}abcdhijk{red}" + ) + let to4 = ["bexj", "abc", "c", "abc"] + let from4 = ["abcdhijk", "abc"] + let diff4 = Diff(from: from4, to: to4) + let attributedString4 = NSAttributedString.attributedString(with: diff4, attributes: diffAttributes) + assert( + attributedString4.debugDescription == "bexj{green}abcdhijk{red}abc{black}cabc{green}" + ) } static var allTests = [