diff --git a/README.md b/README.md
index 1dd97ea..d90a33d 100644
--- a/README.md
+++ b/README.md
@@ -4,26 +4,56 @@
[![codecov](https://codecov.io/gh/Wzxhaha/Sdifft/branch/master/graph/badge.svg)](https://codecov.io/gh/Wzxhaha/Sdifft)
[![codebeat badge](https://codebeat.co/badges/d37a19b5-3d38-45ae-a7c5-5e453826188d)](https://codebeat.co/projects/github-com-wzxhaha-sdifft-master)
-Using the LCS to compare differences between two strings
+Using [`the Myers's Difference Algorithm`](http://www.xmailserver.org/diff2.pdf) to compare differences between two equatable element
-## Example
+## Example(String)
```swift
impoort Sdifft
-let to = "abcd"
-let from = "b"
-let diff = Diff(from: from, to: to)
-/// Get diff modifications
-diff.modifications // [(add: "a", delete: nil, same: "b"), (add: "cd", delete: nil, same: nil)]
-
-/// Get same/add/delete
-let same = diff.modifications.compactMap { $0.same }
-...
+let source = "b"
+let target = "abcd"
+let diff = Diff(source: from, target: to)
+diff.scripts // [.insert(into: 3), .insert(into: 2), .same(into: 1), .insert(into: 0)]
/// Get diff attributedString
-let diffAttributes = DiffAttributes(add: [.backgroundColor: UIColor.green]], delete: [.backgroundColor: UIColor.red], same: [.backgroundColor: UIColor.white])
-let attributedString = NSAttributedString.attributedString(with: diff, attributes: diffAttributes)
+let diffAttributes =
+ DiffAttributes(
+ insert: [.backgroundColor: UIColor.green]],
+ delete: [.backgroundColor: UIColor.red],
+ same: [.backgroundColor: UIColor.white]
+ )
+let attributedString = NSAttributedString(source: source, target: target, attributes: diffAttributes)
+
+// output ->
+// a{green}b{black}cd{green}
+```
+
+## Example(Line)
+
+```swift
+impoort Sdifft
+let source = ["Hello"]
+let target = ["Hello", "World", "!"]
+let attributedString =
+ NSAttributedString(source: source, target: target, attributes: diffAttributes) {
+ let string = NSMutableAttributedString(attributedString: string)
+ string.append(NSAttributedString(string: "\n"))
+ switch script {
+ case .delete:
+ string.insert(NSAttributedString(string: "- "), at: 0)
+ case .insert:
+ string.insert(NSAttributedString(string: "+ "), at: 0)
+ case .same:
+ string.insert(NSAttributedString(string: " "), at: 0)
+ }
+ return string
+ }
+
+// output ->
+// Hello
+// + World{green}
+// + !{green}
```
## Installation
diff --git a/Sdifft.playground/Contents.swift b/Sdifft.playground/Contents.swift
new file mode 100644
index 0000000..cf64635
--- /dev/null
+++ b/Sdifft.playground/Contents.swift
@@ -0,0 +1,40 @@
+import UIKit
+import Sdifft
+
+// Difference between two strings
+let source = "Hallo world"
+let target = "typo: Hello World!"
+
+let font = UIFont.systemFont(ofSize: 20)
+let insertAttributes: [NSAttributedString.Key: Any] = [
+ .backgroundColor: UIColor.green,
+ .font: font
+]
+let deleteAttributes: [NSAttributedString.Key: Any] = [
+ .backgroundColor: UIColor.red,
+ .font: font,
+ .strikethroughStyle: NSUnderlineStyle.single.rawValue,
+ .strikethroughColor: UIColor.red,
+ .baselineOffset: 0
+]
+
+let sameAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor.black,
+ .font: font
+]
+
+let attributedString1 =
+ NSAttributedString(
+ source: source, target: target,
+ attributes: DiffAttributes(insert: insertAttributes, delete: deleteAttributes, same: sameAttributes)
+ )
+
+// Difference between two lines
+let sourceLines = ["I'm coding with Swift"]
+let targetLines = ["Today", "I'm coding with Swift", "lol"]
+
+let attributedString2 =
+ NSAttributedString(
+ source: sourceLines, target: targetLines,
+ attributes: DiffAttributes(insert: insertAttributes, delete: deleteAttributes, same: sameAttributes)
+ )
diff --git a/Sdifft.playground/contents.xcplayground b/Sdifft.playground/contents.xcplayground
new file mode 100644
index 0000000..9f5f2f4
--- /dev/null
+++ b/Sdifft.playground/contents.xcplayground
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Sdifft.xcodeproj/project.pbxproj b/Sdifft.xcodeproj/project.pbxproj
index f7e0045..3653b70 100644
--- a/Sdifft.xcodeproj/project.pbxproj
+++ b/Sdifft.xcodeproj/project.pbxproj
@@ -21,7 +21,6 @@
/* 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 */; };
@@ -48,7 +47,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 1E4BF61A21708396004C5E1F /* DiffSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSequence.swift; sourceTree = ""; };
+ 1E78630721C394C5006F4912 /* Sdifft.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Sdifft.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
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 = ""; };
@@ -107,6 +106,7 @@
OBJ_5 = {
isa = PBXGroup;
children = (
+ 1E78630721C394C5006F4912 /* Sdifft.playground */,
OBJ_6 /* Package.swift */,
OBJ_7 /* Sources */,
OBJ_10 /* Tests */,
@@ -126,7 +126,6 @@
isa = PBXGroup;
children = (
OBJ_9 /* Diff.swift */,
- 1E4BF61A21708396004C5E1F /* DiffSequence.swift */,
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
);
name = Sdifft;
@@ -229,7 +228,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if which swiftlint >/dev/null;\nthen\nswiftlint\n#cd Teambition&&swiftlint\nelse\necho \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi";
+ shellScript = "if which swiftlint >/dev/null;\nthen\nswiftlint\nelse\necho \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -238,7 +237,6 @@
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/Sdifft.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Sdifft.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/Sdifft.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/A8E95969-EF02-4FA2-9760-6D628059A4C2.plist b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/A8E95969-EF02-4FA2-9760-6D628059A4C2.plist
new file mode 100644
index 0000000..ef8329f
--- /dev/null
+++ b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/A8E95969-EF02-4FA2-9760-6D628059A4C2.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ classNames
+
+ DiffTests
+
+ testTime()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.36825
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+
+
+
diff --git a/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/DCB9F5A5-E22D-4494-A0DC-4412307D88CA.plist b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/DCB9F5A5-E22D-4494-A0DC-4412307D88CA.plist
new file mode 100644
index 0000000..b9d9122
--- /dev/null
+++ b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/DCB9F5A5-E22D-4494-A0DC-4412307D88CA.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ classNames
+
+ DiffTests
+
+ testTime()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.8
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+
+
+
diff --git a/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/Info.plist b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/Info.plist
new file mode 100644
index 0000000..4a34ded
--- /dev/null
+++ b/Sdifft.xcodeproj/xcshareddata/xcbaselines/Sdifft::SdifftTests.xcbaseline/Info.plist
@@ -0,0 +1,71 @@
+
+
+
+
+ runDestinationsByUUID
+
+ A8E95969-EF02-4FA2-9760-6D628059A4C2
+
+ localComputer
+
+ busSpeedInMHz
+ 400
+ cpuCount
+ 1
+ cpuKind
+ Intel Core i5
+ cpuSpeedInMHz
+ 2300
+ logicalCPUCoresPerPackage
+ 8
+ modelCode
+ MacBookPro15,2
+ physicalCPUCoresPerPackage
+ 4
+ platformIdentifier
+ com.apple.platform.macosx
+
+ targetArchitecture
+ x86_64
+ targetDevice
+
+ modelCode
+ iPhone10,6
+ platformIdentifier
+ com.apple.platform.iphonesimulator
+
+
+ DCB9F5A5-E22D-4494-A0DC-4412307D88CA
+
+ localComputer
+
+ busSpeedInMHz
+ 400
+ cpuCount
+ 1
+ cpuKind
+ Intel Core i5
+ cpuSpeedInMHz
+ 2300
+ logicalCPUCoresPerPackage
+ 8
+ modelCode
+ MacBookPro15,2
+ physicalCPUCoresPerPackage
+ 4
+ platformIdentifier
+ com.apple.platform.macosx
+
+ targetArchitecture
+ x86_64
+ targetDevice
+
+ modelCode
+ iPhone11,8
+ platformIdentifier
+ com.apple.platform.iphonesimulator
+
+
+
+
+
diff --git a/Sources/Sdifft/Diff.swift b/Sources/Sdifft/Diff.swift
index f29ea16..c3e0c70 100644
--- a/Sources/Sdifft/Diff.swift
+++ b/Sources/Sdifft/Diff.swift
@@ -27,104 +27,107 @@
import Foundation
-typealias Matrix = [[Int]]
-
// swiftlint:disable identifier_name
-/// Draw LCS matrix with two `DiffSequence`
-///
-/// - Parameters:
-/// - from: DiffSequence
-/// - to: DiffSequence that be compared
-/// - Returns: 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..(from: T, to: T, position: Position, matrix: Matrix, same: [DiffIndex]) -> [DiffIndex] {
- if position.row == 0 || position.column == 0 {
- return same
- }
- 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)
- } else {
- return lcs(from: from, to: to, position: (position.row, position.column - 1), matrix: matrix, same: same)
+struct Vertice: Equatable {
+ let x, y: Int
+ static func == (lhs: Vertice, rhs: Vertice) -> Bool {
+ return lhs.x == rhs.x && lhs.y == rhs.y
}
}
-public struct Modification {
- public let add, delete, same: Element?
+struct Path {
+ let from, to: Vertice
+ let script: DiffScript
}
-extension Array where Element == DiffIndex {
- 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.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
+// swiftlint:disable identifier_name
+public class Diff {
+ let scripts: [DiffScript]
+
+ public init(source: [T], target: [T]) {
+ if source.isEmpty, target.isEmpty {
+ scripts = []
+ } else if source.isEmpty, !target.isEmpty {
+ // Under normal circumstances, scripts is a reversed (index) array
+ // you need to reverse the array youself if need.
+ scripts = (0..(
- 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
+ }
+
+ static func exploreEditGraph(source: [T], target: [T]) -> [Path] {
+ let max = source.count + target.count
+ var furthest = Array(repeating: 0, count: 2 * max + 1)
+ var paths: [Path] = []
+
+ let snake: (Int, Int, Int) -> Int = { x, d, k in
+ var _x = x
+ var y: Int { return _x - k }
+ while _x < target.count && y < source.count && source[y] == target[_x] {
+ _x += 1
+ paths.append(
+ Path(from: .init(x: _x - 1, y: y - 1), to: .init(x: _x, y: y), script: .same(at: _x - 1))
)
- )
+ }
+ return _x
}
- return modifications
+
+ for d in 0...max {
+ for k in stride(from: -d, through: d, by: 2) {
+ let index = k + max
+ var x = 0
+ var y: Int { return x - k }
+ // swiftlint:disable statement_position
+ if d == 0 { }
+ else if k == -d || k != d && furthest[index - 1] < furthest[index + 1] {
+ // moving bottom
+ x = furthest[index + 1]
+ paths.append(
+ Path(
+ from: .init(x: x, y: y - 1), to: .init(x: x, y: y),
+ script: .delete(at: y - 1)
+ )
+ )
+ } else {
+ // moving right
+ x = furthest[index - 1] + 1
+ paths.append(
+ Path(
+ from: .init(x: x - 1, y: y), to: .init(x: x, y: y),
+ script: .insert(into: x - 1)
+ )
+ )
+ }
+ x = snake(x, d, k)
+ if x == target.count, y == source.count {
+ return paths
+ }
+ furthest[index] = x
+ }
+ }
+
+ return []
}
-}
-public struct Diff {
- public let modifications: [Modification]
- let matrix: Matrix
- let from, to: T
- public init(from: T, to: T) {
- self.from = from
- self.to = to
- // 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) }
- modifications = same.modifications(from: from, to: to)
+ // Search for the path from the back to the front
+ static func reverseTree(paths: [Path], sinkVertice: Vertice) -> [DiffScript] {
+ var scripts: [DiffScript] = []
+ var next = sinkVertice
+ paths.reversed().forEach {
+ guard $0.to == next else { return }
+ next = $0.from
+ scripts.append($0.script)
+ }
+ return scripts
}
}
diff --git a/Sources/Sdifft/DiffSequence.swift b/Sources/Sdifft/DiffSequence.swift
deleted file mode 100644
index 7b45923..0000000
--- a/Sources/Sdifft/DiffSequence.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-// 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 af36e9c..c0a1a80 100644
--- a/Sources/Sdifft/NSAttributedString+Diff.swift
+++ b/Sources/Sdifft/NSAttributedString+Diff.swift
@@ -27,51 +27,116 @@
import Foundation
+extension String {
+ subscript(_ idx: Int) -> String {
+ return String(self[index(startIndex, offsetBy: idx)])
+ }
+}
+
public struct DiffAttributes {
- 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
+ public let insert, delete, same: [NSAttributedString.Key: Any]
+ public init(
+ insert: [NSAttributedString.Key: Any],
+ delete: [NSAttributedString.Key: Any],
+ same: [NSAttributedString.Key: Any]
+ ) {
+ self.insert = insert
self.delete = delete
self.same = same
}
}
+extension Array where Element == DiffScript {
+ func reverseIndex(source: [T], target: [T]) -> [DiffScript] {
+ return map {
+ switch $0 {
+ case .delete(at: let idx):
+ return DiffScript.delete(at: source.count - 1 - idx)
+ case .insert(into: let idx):
+ return DiffScript.insert(into: target.count - 1 - idx)
+ case .same(at: let idx):
+ return DiffScript.same(at: target.count - 1 - idx)
+ }
+ }
+ }
+}
+
extension NSAttributedString {
- /// Get attributedString with `Diff` and attributes
- ///
- /// - Parameters:
- /// - diff: Diff
- /// - attributes: DiffAttributes
- /// - Returns: NSAttributedString
- public static func attributedString(with diff: Diff, attributes: DiffAttributes) -> NSAttributedString {
+ private static func script(withSource source: [T], target: [T]) -> [DiffScript] {
+ // The results under normal conditions aren't humanable
+ // because it's `Right-Left`
+ // example:
+ // source: "hallo"
+ // target: "typo hello"
+ // result: "h{delete}type: he{insert}a{delete}llo{same}"
+ //
+ // If we reverse source and target, we will get the results that humanable
+ // example:
+ // source: "hallo"
+ // target: "typo hello"
+ // result: "type: {insert}h{same}a{delete}e{insert}llo"
+ return
+ Diff(source: source.reversed(), target: target.reversed())
+ .scripts
+ .reverseIndex(source: source, target: target)
+ }
+
+ public convenience init(source: String, target: String, attributes: DiffAttributes) {
let attributedString = NSMutableAttributedString()
- diff.modifications.forEach {
- if let add = $0.add {
- attributedString.append(NSAttributedString(string: add, attributes: attributes.add))
- }
- if let delete = $0.delete {
- attributedString.append(NSAttributedString(string: delete, attributes: attributes.delete))
- }
- if let same = $0.same {
- attributedString.append(NSAttributedString(string: same, attributes: attributes.same))
+ let scripts = NSAttributedString.script(withSource: .init(source), target: .init(target))
+
+ scripts.forEach {
+ switch $0 {
+ case .insert(into: let idx):
+ attributedString.append(NSAttributedString(string: target[idx], attributes: attributes.insert))
+ case .delete(at: let idx):
+ attributedString.append(NSAttributedString(string: source[idx], attributes: attributes.delete))
+ case .same(at: let idx):
+ attributedString.append(NSAttributedString(string: target[idx], attributes: attributes.same))
}
}
- return attributedString
+
+ self.init(attributedString: attributedString)
}
- public static func attributedString(with diff: Diff<[String]>, attributes: DiffAttributes) -> NSAttributedString {
+
+ /// Difference between two lines
+ ///
+ /// - Parameters:
+ /// - source: source
+ /// - target: target
+ /// - attributes: attributes
+ /// - handler: handler of each script's attributedString
+ ///
+ /// For example:
+ /// let attributedString =
+ /// NSAttributedString(source: source, target: target, attributes: attributes) { script, string in
+ /// let string = NSMutableAttributedString(attributedString: string)
+ /// string.append(NSAttributedString(string: "\n"))
+ /// return string
+ /// }
+ public convenience init(
+ source: [String], target: [String],
+ attributes: DiffAttributes,
+ handler: ((DiffScript, NSAttributedString) -> NSAttributedString)? = nil
+ ) {
let attributedString = NSMutableAttributedString()
- diff.modifications.forEach {
- if let add = $0.add {
- attributedString.append(NSAttributedString(string: add.joined(), attributes: attributes.add))
+ let scripts = NSAttributedString.script(withSource: source, target: target)
+ scripts.forEach {
+ var scriptAttributedString: NSAttributedString
+ switch $0 {
+ case .insert(into: let idx):
+ scriptAttributedString = NSAttributedString(string: target[idx], attributes: attributes.insert)
+ case .delete(at: let idx):
+ scriptAttributedString = NSAttributedString(string: source[idx], attributes: attributes.delete)
+ case .same(at: let idx):
+ scriptAttributedString = NSAttributedString(string: target[idx], attributes: attributes.same)
}
- 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))
+ if let handler = handler {
+ scriptAttributedString = handler($0, scriptAttributedString)
}
+ attributedString.append(scriptAttributedString)
}
- return attributedString
+
+ self.init(attributedString: attributedString)
}
}
diff --git a/Tests/SdifftTests/DiffTests.swift b/Tests/SdifftTests/DiffTests.swift
index 029c197..45896af 100644
--- a/Tests/SdifftTests/DiffTests.swift
+++ b/Tests/SdifftTests/DiffTests.swift
@@ -1,146 +1,60 @@
import XCTest
@testable import Sdifft
-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 }
- }
- var adds: [String] {
- return compactMap { $0.add }
- }
- var deletes: [String] {
- return compactMap { $0.delete }
- }
-}
-
-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 }
+extension Array where Element == DiffScript {
+ var description: String {
+ var description = ""
+ forEach {
+ switch $0 {
+ case .delete(at: let idx):
+ description += "D{\(idx)}"
+ case .insert(into: let idx):
+ description += "I{\(idx)}"
+ case .same(at: let idx):
+ description += "U{\(idx)}"
+ }
+ }
+ return description
}
}
class DiffTests: XCTestCase {
- func testMatrix() {
- assert(
- drawMatrix(from: "abcd", to: "acd") == [
- [0, 0, 0, 0],
- [0, 1, 1, 1],
- [0, 1, 1, 1],
- [0, 1, 2, 2],
- [0, 1, 2, 3]
- ]
- )
- assert(
- drawMatrix(from: "abcdegh", to: "ae") == [
- [0, 0, 0],
- [0, 1, 1],
- [0, 1, 1],
- [0, 1, 1],
- [0, 1, 1],
- [0, 1, 2],
- [0, 1, 2],
- [0, 1, 2]
- ]
- )
- assert(
- drawMatrix(from: "adf", to: "d") == [
- [0, 0],
- [0, 0],
- [0, 1],
- [0, 1]
- ]
- )
- assert(
- drawMatrix(from: "d", to: "adf") == [
- [0, 0, 0, 0],
- [0, 0, 1, 1]
- ]
- )
- assert(
- drawMatrix(from: "", to: "") == [
- [0]
- ]
- )
- }
-
- func testStringRange() {
- assert(
- "abc"[0] == "a"
- )
- assert(
- "abc"[1] == "b"
- )
- assert(
- "abc"[2] == "c"
- )
- }
-
- func testModification() {
- let to1 = "abcd"
- let from1 = "b"
- let diff1 = Diff(from: from1, to: to1)
- assert(
- diff1.modifications.sames == ["b"] &&
- diff1.modifications.adds == ["a", "cd"] &&
- diff1.modifications.deletes == []
- )
- let to2 = "abcd"
- let from2 = "bx"
- let diff2 = Diff(from: from2, to: to2)
- assert(
- diff2.modifications.sames == ["b"] &&
- diff2.modifications.adds == ["a", "cd"] &&
- diff2.modifications.deletes == ["x"]
- )
- let to3 = "A\r\nB\r\nC"
- let from3 = "A\r\n\r\nB\r\n\r\nC"
- let diff3 = Diff(from: from3, to: to3)
- assert(
- diff3.modifications.sames == ["A", "\r\n", "B", "\r\n", "C"] &&
- 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 == []
- )
+ func testDiff() {
+ let scripts = Diff(source: .init("b"), target: .init("abcd")).scripts
+
+
+ let expectations = [
+ ("abc", "abc", "U{2}U{1}U{0}"),
+ ("abc", "ab", "D{2}U{1}U{0}"),
+ ("ab", "abc", "I{2}U{1}U{0}"),
+ ("", "abc", "I{2}I{1}I{0}"),
+ ("abc", "", "D{2}D{1}D{0}"),
+ ("b", "ac", "D{0}I{1}I{0}"),
+ ("", "", "")
+ ]
+ expectations.forEach {
+ let scripts = Diff(source: .init($0.0), target: .init($0.1)).scripts
+ XCTAssertTrue(
+ scripts.description == $0.2,
+ "\(scripts.description) is no equal to \($0.2)"
+ )
+ }
}
// swiftlint:disable line_length
func testTime() {
- // 1000 character * 1000 character: 3.540s
+ // 1000 character * 1000 character: 0.8s
measure {
_ =
Diff(
- from: "abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkbexjabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk123abcdhijkabcdhijkabcdhijkabcdhijk12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123",
- to: "abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk"
+ source: .init("abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkbexjabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk123abcdhijkabcdhijkabcdhijkabcdhijk12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123"),
+ target: .init("abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk")
)
}
}
static var allTests = [
- ("testMatrix", testMatrix),
- ("testStringRange", testStringRange),
- ("testModification", testModification),
+ ("testDiff", testDiff),
("testTime", testTime)
]
}
diff --git a/Tests/SdifftTests/NSAttributedString+DiffTests.swift b/Tests/SdifftTests/NSAttributedString+DiffTests.swift
index 5d3ef41..4ba2405 100644
--- a/Tests/SdifftTests/NSAttributedString+DiffTests.swift
+++ b/Tests/SdifftTests/NSAttributedString+DiffTests.swift
@@ -8,24 +8,33 @@ import UIKit
typealias Color = UIColor
#endif
+extension String {
+ subscript(range: CountableClosedRange) -> String {
+ let start = index(startIndex, offsetBy: range.lowerBound)
+ let end = index(startIndex, offsetBy: range.upperBound)
+ return String(self[start...end])
+ }
+}
+
extension CGColor: CustomStringConvertible {
public var description: String {
let comps: [CGFloat] = components ?? [0, 0, 0, 0]
if comps == [1, 0, 0, 1] {
- return "{red}"
+ return "{D}"
} else if comps == [0, 1, 0, 1] {
- return "{green}"
+ return "{I}"
} else {
- return "{black}"
+ return "{U}"
}
}
}
extension NSAttributedString {
- // swiftlint:disable line_length
open override var description: String {
var description = ""
- enumerateAttributes(in: NSRange(location: 0, length: string.count), options: .longestEffectiveRangeNotRequired) { (attributes, range, _) in
+ enumerateAttributes(
+ in: NSRange(location: 0, length: string.count),
+ options: .longestEffectiveRangeNotRequired) { (attributes, range, _) in
let color = attributes[NSAttributedString.Key.backgroundColor] as? Color ?? Color.black
description += string[range.location...range.location + range.length - 1] + color.cgColor.description
}
@@ -34,40 +43,87 @@ extension NSAttributedString {
}
class NSAttributedStringDiffTests: XCTestCase {
- // swiftlint:disable line_length
+ let insertAttributes: [NSAttributedString.Key: Any] = [
+ .backgroundColor: UIColor.green
+ ]
+
+ let deleteAttributes: [NSAttributedString.Key: Any] = [
+ .backgroundColor: UIColor.red,
+ .strikethroughStyle: NSUnderlineStyle.single.rawValue,
+ .strikethroughColor: UIColor.red,
+ .baselineOffset: 0
+ ]
+
+ let sameAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor.black
+ ]
+
func testAttributedString() {
- let diffAttributes = DiffAttributes(add: [.backgroundColor: Color.green], delete: [.backgroundColor: Color.red], same: [.backgroundColor: Color.black])
- let to1 = "abcdhijk"
- let from1 = "bexj"
- let diff1 = Diff(from: from1, to: to1)
- let attributedString1 = NSAttributedString.attributedString(with: diff1, attributes: diffAttributes)
- assert(
- attributedString1.debugDescription == "a{green}b{black}cdhi{green}ex{red}j{black}k{green}"
- )
- let to2 = "bexj"
- let from2 = "abcdhijk"
- let diff2 = Diff(from: from2, to: to2)
- let attributedString2 = NSAttributedString.attributedString(with: diff2, attributes: diffAttributes)
- 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}"
- )
+ let expectations = [
+ ("abc", "abc", "abc{U}"),
+ ("abc", "ab", "ab{U}c{D}"),
+ ("ab", "abc", "ab{U}c{I}"),
+ ("", "abc", "abc{I}"),
+ ("abc", "", "abc{D}"),
+ ("b", "ac", "b{D}ac{I}"),
+ ("abc", "ac", "a{U}b{D}c{U}"),
+ ("", "", "")
+ ]
+ expectations.forEach {
+ let string =
+ NSAttributedString(
+ source: $0.0, target: $0.1,
+ attributes: DiffAttributes(insert: insertAttributes, delete: deleteAttributes, same: sameAttributes)
+ )
+ XCTAssertTrue(
+ string.description == $0.2,
+ "\(string.description) is no equal to \($0.2)"
+ )
+ }
+ }
+
+ func testLines() {
+ let expectations: [([String], [String], [String])] = [
+ (["a", "b", "c"], ["a", "b", "c"], ["a", "b", "c"]),
+ (["a", "b", "c"], ["a", "b"], ["a", "b", "-c"]),
+ (["a", "b"], ["a", "b", "c"], ["a", "b", "+c"]),
+ ([], ["a", "b", "c"], ["+a", "+b", "+c"]),
+ (["a", "b", "c"], [], ["-a", "-b", "-c"]),
+ (["b"], ["a", "c"], ["-b", "+a", "+c"]),
+ (["a", "b", "c"], ["a", "c"], ["a", "-b", "c"]),
+ ([], [], [])
+ ]
+ expectations.forEach {
+ let string =
+ NSAttributedString(
+ source: $0.0, target: $0.1,
+ attributes: DiffAttributes(
+ insert: insertAttributes,
+ delete: deleteAttributes,
+ same: sameAttributes)
+ ) { script, string in
+ let string = NSMutableAttributedString(attributedString: string)
+ string.append(NSAttributedString(string: "\n"))
+ switch script {
+ case .delete:
+ string.insert(NSAttributedString(string: "-"), at: 0)
+ case .insert:
+ string.insert(NSAttributedString(string: "+"), at: 0)
+ case .same:
+ break
+ }
+ return string
+ }
+ let result = string.string.split(separator: "\n").map { String($0) }
+ XCTAssertTrue(
+ result == $0.2,
+ "\(result) is no equal to \($0.2)"
+ )
+ }
}
static var allTests = [
- ("testAttributedString", testAttributedString)
+ ("testAttributedString", testAttributedString),
+ ("testLines", testLines)
]
}