Skip to content

Commit

Permalink
feature: support on line model
Browse files Browse the repository at this point in the history
  • Loading branch information
wzxha committed Oct 12, 2018
1 parent e7ce841 commit 3246d71
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 59 deletions.
4 changes: 4 additions & 0 deletions Sdifft.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -47,6 +48,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
1E4BF61A21708396004C5E1F /* DiffSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSequence.swift; sourceTree = "<group>"; };
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Diff.swift"; sourceTree = "<group>"; };
1EB1AD2820BD640B004D0450 /* NSAttributedString+DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+DiffTests.swift"; sourceTree = "<group>"; };
OBJ_12 /* DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -124,6 +126,7 @@
isa = PBXGroup;
children = (
OBJ_9 /* Diff.swift */,
1E4BF61A21708396004C5E1F /* DiffSequence.swift */,
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
);
name = Sdifft;
Expand Down Expand Up @@ -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 */,
);
Expand Down
87 changes: 33 additions & 54 deletions Sources/Sdifft/Diff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: DiffSequence>(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..<row {
for j in 1..<column {
if from[i - 1] == to[j - 1] {
if from.index(of: i - 1) == to.index(of: j - 1) {
result[i][j] = result[i - 1][j - 1] + 1
} else {
result[i][j] = max(result[i][j - 1], result[i - 1][j])
Expand All @@ -66,17 +59,17 @@ typealias DiffIndex = (from: Int, to: Int)
/// LCS
///
/// - Parameters:
/// - from: string
/// - to: string that be compared
/// - from: DiffSequence
/// - to: DiffSequence that be compared
/// - position: current position
/// - matrix: matrix
/// - same: same character's indexes
/// - Returns: same character's indexes
func lcs(from: String, to: String, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
/// - same: same element's indexes
/// - Returns: same element's indexes
func lcs<T: DiffSequence>(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)
Expand All @@ -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<Int>) -> String {
let start = index(startIndex, offsetBy: range.lowerBound)
let end = index(startIndex, offsetBy: range.upperBound)
return String(self[start...end])
}
public struct Modification<Element: DiffSequence> {
public let add, delete, same: Element?
}

extension Array where Element == DiffIndex {
func modifications(from: String, to: String) -> [Modification] {
var modifications: [Modification] = []
func modifications<T: DiffSequence>(from: T, to: T) -> [Modification<T>] {
var modifications: [Modification<T>] = []
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<T>(
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<T>(
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
)
)
Expand All @@ -131,21 +111,20 @@ extension Array where Element == DiffIndex {
}
}

public struct Diff {
public let modifications: [Modification]
public struct Diff<T: DiffSequence> {
public let modifications: [Modification<T>]
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)
}
}
61 changes: 61 additions & 0 deletions Sources/Sdifft/DiffSequence.swift
Original file line number Diff line number Diff line change
@@ -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<Int>) -> 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<Int>) -> 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<Int>) -> [Element] {
return Array(self[range])
}
public func reversedElement() -> [Element] {
return reversed()
}
}
21 changes: 17 additions & 4 deletions Sources/Sdifft/NSAttributedString+Diff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>, attributes: DiffAttributes) -> NSAttributedString {
let attributedString = NSMutableAttributedString()
diff.modifications.forEach {
if let add = $0.add {
Expand All @@ -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
}
}
31 changes: 30 additions & 1 deletion Tests/SdifftTests/DiffTests.swift
Original file line number Diff line number Diff line change
@@ -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<Int>) -> String {
return element(withRange: range)
}
}

extension Array where Element == Modification<String> {
var sames: [String] {
return compactMap { $0.same }
}
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Tests/SdifftTests/NSAttributedString+DiffTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down

0 comments on commit 3246d71

Please sign in to comment.