feature: use myers's difference alogorithm to improve performance
This commit is contained in:
parent
3246d71358
commit
26f1a8940e
56
README.md
56
README.md
|
@ -4,26 +4,56 @@
|
||||||
[](https://codecov.io/gh/Wzxhaha/Sdifft)
|
[](https://codecov.io/gh/Wzxhaha/Sdifft)
|
||||||
[](https://codebeat.co/projects/github-com-wzxhaha-sdifft-master)
|
[](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
|
```swift
|
||||||
impoort Sdifft
|
impoort Sdifft
|
||||||
|
|
||||||
let to = "abcd"
|
let source = "b"
|
||||||
let from = "b"
|
let target = "abcd"
|
||||||
let diff = Diff(from: from, to: to)
|
let diff = Diff(source: from, target: to)
|
||||||
/// Get diff modifications
|
diff.scripts // [.insert(into: 3), .insert(into: 2), .same(into: 1), .insert(into: 0)]
|
||||||
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 }
|
|
||||||
...
|
|
||||||
|
|
||||||
/// Get diff attributedString
|
/// Get diff attributedString
|
||||||
let diffAttributes = DiffAttributes(add: [.backgroundColor: UIColor.green]], delete: [.backgroundColor: UIColor.red], same: [.backgroundColor: UIColor.white])
|
let diffAttributes =
|
||||||
let attributedString = NSAttributedString.attributedString(with: diff, attributes: 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
|
## Installation
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<playground version='5.0' target-platform='ios' executeOnSourceChanges='false'>
|
||||||
|
<timeline fileName='timeline.xctimeline'/>
|
||||||
|
</playground>
|
|
@ -21,7 +21,6 @@
|
||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile 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 */; };
|
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 */; };
|
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 */; };
|
OBJ_21 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Diff.swift */; };
|
||||||
|
@ -48,7 +47,7 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1E4BF61A21708396004C5E1F /* DiffSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSequence.swift; sourceTree = "<group>"; };
|
1E78630721C394C5006F4912 /* Sdifft.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Sdifft.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Diff.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>"; };
|
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>"; };
|
OBJ_12 /* DiffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -107,6 +106,7 @@
|
||||||
OBJ_5 = {
|
OBJ_5 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
1E78630721C394C5006F4912 /* Sdifft.playground */,
|
||||||
OBJ_6 /* Package.swift */,
|
OBJ_6 /* Package.swift */,
|
||||||
OBJ_7 /* Sources */,
|
OBJ_7 /* Sources */,
|
||||||
OBJ_10 /* Tests */,
|
OBJ_10 /* Tests */,
|
||||||
|
@ -126,7 +126,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
OBJ_9 /* Diff.swift */,
|
OBJ_9 /* Diff.swift */,
|
||||||
1E4BF61A21708396004C5E1F /* DiffSequence.swift */,
|
|
||||||
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
|
1EB1AD2620BD5E22004D0450 /* NSAttributedString+Diff.swift */,
|
||||||
);
|
);
|
||||||
name = Sdifft;
|
name = Sdifft;
|
||||||
|
@ -229,7 +228,7 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
@ -238,7 +237,6 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 0;
|
buildActionMask = 0;
|
||||||
files = (
|
files = (
|
||||||
1E4BF61B21708396004C5E1F /* DiffSequence.swift in Sources */,
|
|
||||||
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */,
|
1EB1AD2720BD5E22004D0450 /* NSAttributedString+Diff.swift in Sources */,
|
||||||
OBJ_21 /* Diff.swift in Sources */,
|
OBJ_21 /* Diff.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>classNames</key>
|
||||||
|
<dict>
|
||||||
|
<key>DiffTests</key>
|
||||||
|
<dict>
|
||||||
|
<key>testTime()</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||||
|
<dict>
|
||||||
|
<key>baselineAverage</key>
|
||||||
|
<real>0.36825</real>
|
||||||
|
<key>baselineIntegrationDisplayName</key>
|
||||||
|
<string>Local Baseline</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>classNames</key>
|
||||||
|
<dict>
|
||||||
|
<key>DiffTests</key>
|
||||||
|
<dict>
|
||||||
|
<key>testTime()</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||||
|
<dict>
|
||||||
|
<key>baselineAverage</key>
|
||||||
|
<real>0.8</real>
|
||||||
|
<key>baselineIntegrationDisplayName</key>
|
||||||
|
<string>Local Baseline</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>runDestinationsByUUID</key>
|
||||||
|
<dict>
|
||||||
|
<key>A8E95969-EF02-4FA2-9760-6D628059A4C2</key>
|
||||||
|
<dict>
|
||||||
|
<key>localComputer</key>
|
||||||
|
<dict>
|
||||||
|
<key>busSpeedInMHz</key>
|
||||||
|
<integer>400</integer>
|
||||||
|
<key>cpuCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>cpuKind</key>
|
||||||
|
<string>Intel Core i5</string>
|
||||||
|
<key>cpuSpeedInMHz</key>
|
||||||
|
<integer>2300</integer>
|
||||||
|
<key>logicalCPUCoresPerPackage</key>
|
||||||
|
<integer>8</integer>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>MacBookPro15,2</string>
|
||||||
|
<key>physicalCPUCoresPerPackage</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.macosx</string>
|
||||||
|
</dict>
|
||||||
|
<key>targetArchitecture</key>
|
||||||
|
<string>x86_64</string>
|
||||||
|
<key>targetDevice</key>
|
||||||
|
<dict>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>iPhone10,6</string>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.iphonesimulator</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>DCB9F5A5-E22D-4494-A0DC-4412307D88CA</key>
|
||||||
|
<dict>
|
||||||
|
<key>localComputer</key>
|
||||||
|
<dict>
|
||||||
|
<key>busSpeedInMHz</key>
|
||||||
|
<integer>400</integer>
|
||||||
|
<key>cpuCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>cpuKind</key>
|
||||||
|
<string>Intel Core i5</string>
|
||||||
|
<key>cpuSpeedInMHz</key>
|
||||||
|
<integer>2300</integer>
|
||||||
|
<key>logicalCPUCoresPerPackage</key>
|
||||||
|
<integer>8</integer>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>MacBookPro15,2</string>
|
||||||
|
<key>physicalCPUCoresPerPackage</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.macosx</string>
|
||||||
|
</dict>
|
||||||
|
<key>targetArchitecture</key>
|
||||||
|
<string>x86_64</string>
|
||||||
|
<key>targetDevice</key>
|
||||||
|
<dict>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>iPhone11,8</string>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.iphonesimulator</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -27,104 +27,107 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
typealias Matrix = [[Int]]
|
// swiftlint:disable identifier_name
|
||||||
|
public enum DiffScript {
|
||||||
|
case insert(into: Int)
|
||||||
|
case delete(at: Int)
|
||||||
|
case same(at: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Vertice: Equatable {
|
||||||
|
let x, y: Int
|
||||||
|
static func == (lhs: Vertice, rhs: Vertice) -> Bool {
|
||||||
|
return lhs.x == rhs.x && lhs.y == rhs.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Path {
|
||||||
|
let from, to: Vertice
|
||||||
|
let script: DiffScript
|
||||||
|
}
|
||||||
|
|
||||||
// swiftlint:disable identifier_name
|
// swiftlint:disable identifier_name
|
||||||
/// Draw LCS matrix with two `DiffSequence`
|
public class Diff<T: Equatable & Hashable> {
|
||||||
///
|
let scripts: [DiffScript]
|
||||||
/// - Parameters:
|
|
||||||
/// - from: DiffSequence
|
public init(source: [T], target: [T]) {
|
||||||
/// - to: DiffSequence that be compared
|
if source.isEmpty, target.isEmpty {
|
||||||
/// - Returns: matrix
|
scripts = []
|
||||||
func drawMatrix<T: DiffSequence>(from: T, to: T) -> Matrix {
|
} else if source.isEmpty, !target.isEmpty {
|
||||||
let row = from.count + 1
|
// Under normal circumstances, scripts is a reversed (index) array
|
||||||
let column = to.count + 1
|
// you need to reverse the array youself if need.
|
||||||
var result: [[Int]] = Array(repeating: Array(repeating: 0, count: column), count: row)
|
scripts = (0..<target.count).reversed().compactMap { DiffScript.insert(into: $0) }
|
||||||
for i in 1..<row {
|
} else if !source.isEmpty, target.isEmpty {
|
||||||
for j in 1..<column {
|
scripts = (0..<source.count).reversed().compactMap { DiffScript.delete(at: $0) }
|
||||||
if from.index(of: i - 1) == to.index(of: j - 1) {
|
} else {
|
||||||
result[i][j] = result[i - 1][j - 1] + 1
|
let paths = Diff.exploreEditGraph(source: source, target: target)
|
||||||
} else {
|
scripts = Diff.reverseTree(paths: paths, sinkVertice: .init(x: target.count, y: source.count))
|
||||||
result[i][j] = max(result[i][j - 1], result[i - 1][j])
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 []
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias Position = (row: Int, column: Int)
|
// Search for the path from the back to the front
|
||||||
typealias DiffIndex = (from: Int, to: Int)
|
static func reverseTree(paths: [Path], sinkVertice: Vertice) -> [DiffScript] {
|
||||||
|
var scripts: [DiffScript] = []
|
||||||
// swiftlint:disable line_length
|
var next = sinkVertice
|
||||||
/// LCS
|
paths.reversed().forEach {
|
||||||
///
|
guard $0.to == next else { return }
|
||||||
/// - Parameters:
|
next = $0.from
|
||||||
/// - from: DiffSequence
|
scripts.append($0.script)
|
||||||
/// - to: DiffSequence that be compared
|
|
||||||
/// - position: current position
|
|
||||||
/// - matrix: matrix
|
|
||||||
/// - 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.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Modification<Element: DiffSequence> {
|
|
||||||
public let add, delete, same: Element?
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Array where Element == DiffIndex {
|
|
||||||
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<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 {
|
return scripts
|
||||||
modifications.append(
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return modifications
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Diff<T: DiffSequence> {
|
|
||||||
public let modifications: [Modification<T>]
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,51 +27,116 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
subscript(_ idx: Int) -> String {
|
||||||
|
return String(self[index(startIndex, offsetBy: idx)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct DiffAttributes {
|
public struct DiffAttributes {
|
||||||
public let add, delete, same: [NSAttributedString.Key: Any]
|
public let insert, delete, same: [NSAttributedString.Key: Any]
|
||||||
// swiftlint:disable line_length
|
public init(
|
||||||
public init(add: [NSAttributedString.Key: Any], delete: [NSAttributedString.Key: Any], same: [NSAttributedString.Key: Any]) {
|
insert: [NSAttributedString.Key: Any],
|
||||||
self.add = add
|
delete: [NSAttributedString.Key: Any],
|
||||||
|
same: [NSAttributedString.Key: Any]
|
||||||
|
) {
|
||||||
|
self.insert = insert
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
self.same = same
|
self.same = same
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NSAttributedString {
|
extension Array where Element == DiffScript {
|
||||||
/// Get attributedString with `Diff` and attributes
|
func reverseIndex<T>(source: [T], target: [T]) -> [DiffScript] {
|
||||||
///
|
return map {
|
||||||
/// - Parameters:
|
switch $0 {
|
||||||
/// - diff: Diff
|
case .delete(at: let idx):
|
||||||
/// - attributes: DiffAttributes
|
return DiffScript.delete(at: source.count - 1 - idx)
|
||||||
/// - Returns: NSAttributedString
|
case .insert(into: let idx):
|
||||||
public static func attributedString(with diff: Diff<String>, attributes: DiffAttributes) -> NSAttributedString {
|
return DiffScript.insert(into: target.count - 1 - idx)
|
||||||
let attributedString = NSMutableAttributedString()
|
case .same(at: let idx):
|
||||||
diff.modifications.forEach {
|
return DiffScript.same(at: target.count - 1 - idx)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attributedString
|
}
|
||||||
}
|
}
|
||||||
public static func attributedString(with diff: Diff<[String]>, attributes: DiffAttributes) -> NSAttributedString {
|
|
||||||
let attributedString = NSMutableAttributedString()
|
extension NSAttributedString {
|
||||||
diff.modifications.forEach {
|
private static func script<T: Equatable & Hashable>(withSource source: [T], target: [T]) -> [DiffScript] {
|
||||||
if let add = $0.add {
|
// The results under normal conditions aren't humanable
|
||||||
attributedString.append(NSAttributedString(string: add.joined(), attributes: attributes.add))
|
// because it's `Right-Left`
|
||||||
}
|
// example:
|
||||||
if let delete = $0.delete {
|
// source: "hallo"
|
||||||
attributedString.append(NSAttributedString(string: delete.joined(), attributes: attributes.delete))
|
// target: "typo hello"
|
||||||
}
|
// result: "h{delete}type: he{insert}a{delete}llo{same}"
|
||||||
if let same = $0.same {
|
//
|
||||||
attributedString.append(NSAttributedString(string: same.joined(), attributes: attributes.same))
|
// If we reverse source and target, we will get the results that humanable
|
||||||
}
|
// example:
|
||||||
}
|
// source: "hallo"
|
||||||
return attributedString
|
// 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()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(attributedString: attributedString)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
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 handler = handler {
|
||||||
|
scriptAttributedString = handler($0, scriptAttributedString)
|
||||||
|
}
|
||||||
|
attributedString.append(scriptAttributedString)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(attributedString: attributedString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,146 +1,60 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import Sdifft
|
@testable import Sdifft
|
||||||
|
|
||||||
extension String {
|
extension Array where Element == DiffScript {
|
||||||
subscript(idx: Int) -> String {
|
var description: String {
|
||||||
return index(of: idx)
|
var description = ""
|
||||||
}
|
forEach {
|
||||||
subscript(range: CountableClosedRange<Int>) -> String {
|
switch $0 {
|
||||||
return element(withRange: range)
|
case .delete(at: let idx):
|
||||||
}
|
description += "D{\(idx)}"
|
||||||
}
|
case .insert(into: let idx):
|
||||||
|
description += "I{\(idx)}"
|
||||||
extension Array where Element == Modification<String> {
|
case .same(at: let idx):
|
||||||
var sames: [String] {
|
description += "U{\(idx)}"
|
||||||
return compactMap { $0.same }
|
}
|
||||||
}
|
}
|
||||||
var adds: [String] {
|
return description
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiffTests: XCTestCase {
|
class DiffTests: XCTestCase {
|
||||||
func testMatrix() {
|
func testDiff() {
|
||||||
assert(
|
let scripts = Diff(source: .init("b"), target: .init("abcd")).scripts
|
||||||
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(
|
let expectations = [
|
||||||
"abc"[0] == "a"
|
("abc", "abc", "U{2}U{1}U{0}"),
|
||||||
)
|
("abc", "ab", "D{2}U{1}U{0}"),
|
||||||
assert(
|
("ab", "abc", "I{2}U{1}U{0}"),
|
||||||
"abc"[1] == "b"
|
("", "abc", "I{2}I{1}I{0}"),
|
||||||
)
|
("abc", "", "D{2}D{1}D{0}"),
|
||||||
assert(
|
("b", "ac", "D{0}I{1}I{0}"),
|
||||||
"abc"[2] == "c"
|
("", "", "")
|
||||||
)
|
]
|
||||||
}
|
expectations.forEach {
|
||||||
|
let scripts = Diff(source: .init($0.0), target: .init($0.1)).scripts
|
||||||
func testModification() {
|
XCTAssertTrue(
|
||||||
let to1 = "abcd"
|
scripts.description == $0.2,
|
||||||
let from1 = "b"
|
"\(scripts.description) is no equal to \($0.2)"
|
||||||
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 == []
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable line_length
|
// swiftlint:disable line_length
|
||||||
func testTime() {
|
func testTime() {
|
||||||
// 1000 character * 1000 character: 3.540s
|
// 1000 character * 1000 character: 0.8s
|
||||||
measure {
|
measure {
|
||||||
_ =
|
_ =
|
||||||
Diff(
|
Diff(
|
||||||
from: "abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkbexjabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk123abcdhijkabcdhijkabcdhijkabcdhijk12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123",
|
source: .init("abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkbexjabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk123abcdhijkabcdhijkabcdhijkabcdhijk12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123k12213123"),
|
||||||
to: "abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk"
|
target: .init("abcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijkabcdhijk")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static var allTests = [
|
static var allTests = [
|
||||||
("testMatrix", testMatrix),
|
("testDiff", testDiff),
|
||||||
("testStringRange", testStringRange),
|
|
||||||
("testModification", testModification),
|
|
||||||
("testTime", testTime)
|
("testTime", testTime)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,24 +8,33 @@ import UIKit
|
||||||
typealias Color = UIColor
|
typealias Color = UIColor
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
subscript(range: CountableClosedRange<Int>) -> String {
|
||||||
|
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||||
|
let end = index(startIndex, offsetBy: range.upperBound)
|
||||||
|
return String(self[start...end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension CGColor: CustomStringConvertible {
|
extension CGColor: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
let comps: [CGFloat] = components ?? [0, 0, 0, 0]
|
let comps: [CGFloat] = components ?? [0, 0, 0, 0]
|
||||||
if comps == [1, 0, 0, 1] {
|
if comps == [1, 0, 0, 1] {
|
||||||
return "{red}"
|
return "{D}"
|
||||||
} else if comps == [0, 1, 0, 1] {
|
} else if comps == [0, 1, 0, 1] {
|
||||||
return "{green}"
|
return "{I}"
|
||||||
} else {
|
} else {
|
||||||
return "{black}"
|
return "{U}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NSAttributedString {
|
extension NSAttributedString {
|
||||||
// swiftlint:disable line_length
|
|
||||||
open override var description: String {
|
open override var description: String {
|
||||||
var description = ""
|
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
|
let color = attributes[NSAttributedString.Key.backgroundColor] as? Color ?? Color.black
|
||||||
description += string[range.location...range.location + range.length - 1] + color.cgColor.description
|
description += string[range.location...range.location + range.length - 1] + color.cgColor.description
|
||||||
}
|
}
|
||||||
|
@ -34,40 +43,87 @@ extension NSAttributedString {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NSAttributedStringDiffTests: XCTestCase {
|
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() {
|
func testAttributedString() {
|
||||||
let diffAttributes = DiffAttributes(add: [.backgroundColor: Color.green], delete: [.backgroundColor: Color.red], same: [.backgroundColor: Color.black])
|
let expectations = [
|
||||||
let to1 = "abcdhijk"
|
("abc", "abc", "abc{U}"),
|
||||||
let from1 = "bexj"
|
("abc", "ab", "ab{U}c{D}"),
|
||||||
let diff1 = Diff(from: from1, to: to1)
|
("ab", "abc", "ab{U}c{I}"),
|
||||||
let attributedString1 = NSAttributedString.attributedString(with: diff1, attributes: diffAttributes)
|
("", "abc", "abc{I}"),
|
||||||
assert(
|
("abc", "", "abc{D}"),
|
||||||
attributedString1.debugDescription == "a{green}b{black}cdhi{green}ex{red}j{black}k{green}"
|
("b", "ac", "b{D}ac{I}"),
|
||||||
)
|
("abc", "ac", "a{U}b{D}c{U}"),
|
||||||
let to2 = "bexj"
|
("", "", "")
|
||||||
let from2 = "abcdhijk"
|
]
|
||||||
let diff2 = Diff(from: from2, to: to2)
|
expectations.forEach {
|
||||||
let attributedString2 = NSAttributedString.attributedString(with: diff2, attributes: diffAttributes)
|
let string =
|
||||||
assert(
|
NSAttributedString(
|
||||||
attributedString2.debugDescription == "a{red}b{black}ex{green}cdhi{red}j{black}k{red}"
|
source: $0.0, target: $0.1,
|
||||||
)
|
attributes: DiffAttributes(insert: insertAttributes, delete: deleteAttributes, same: sameAttributes)
|
||||||
let to3 = ["bexj", "abc", "c"]
|
)
|
||||||
let from3 = ["abcdhijk"]
|
XCTAssertTrue(
|
||||||
let diff3 = Diff(from: from3, to: to3)
|
string.description == $0.2,
|
||||||
let attributedString3 = NSAttributedString.attributedString(with: diff3, attributes: diffAttributes)
|
"\(string.description) is no equal to \($0.2)"
|
||||||
assert(
|
)
|
||||||
attributedString3.debugDescription == "bexjabcc{green}abcdhijk{red}"
|
}
|
||||||
)
|
}
|
||||||
let to4 = ["bexj", "abc", "c", "abc"]
|
|
||||||
let from4 = ["abcdhijk", "abc"]
|
func testLines() {
|
||||||
let diff4 = Diff(from: from4, to: to4)
|
let expectations: [([String], [String], [String])] = [
|
||||||
let attributedString4 = NSAttributedString.attributedString(with: diff4, attributes: diffAttributes)
|
(["a", "b", "c"], ["a", "b", "c"], ["a", "b", "c"]),
|
||||||
assert(
|
(["a", "b", "c"], ["a", "b"], ["a", "b", "-c"]),
|
||||||
attributedString4.debugDescription == "bexj{green}abcdhijk{red}abc{black}cabc{green}"
|
(["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 = [
|
static var allTests = [
|
||||||
("testAttributedString", testAttributedString)
|
("testAttributedString", testAttributedString),
|
||||||
|
("testLines", testLines)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue