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://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
|
||||
|
|
|
@ -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 */
|
||||
|
||||
/* 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 = "<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>"; };
|
||||
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>"; };
|
||||
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
/// Draw LCS matrix with two `DiffSequence`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: DiffSequence
|
||||
/// - to: DiffSequence that be compared
|
||||
/// - Returns: 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.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])
|
||||
public class Diff<T: Equatable & Hashable> {
|
||||
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..<target.count).reversed().compactMap { DiffScript.insert(into: $0) }
|
||||
} else if !source.isEmpty, target.isEmpty {
|
||||
scripts = (0..<source.count).reversed().compactMap { DiffScript.delete(at: $0) }
|
||||
} else {
|
||||
let paths = Diff.exploreEditGraph(source: source, target: target)
|
||||
scripts = Diff.reverseTree(paths: paths, sinkVertice: .init(x: target.count, y: source.count))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
typealias DiffIndex = (from: Int, to: Int)
|
||||
|
||||
// swiftlint:disable line_length
|
||||
/// LCS
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: DiffSequence
|
||||
/// - 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
|
||||
// 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)
|
||||
}
|
||||
if lastFrom <= from.count - 1 || lastTo <= to.count - 1 {
|
||||
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)
|
||||
return scripts
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
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 NSAttributedString {
|
||||
/// Get attributedString with `Diff` and attributes
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - diff: Diff
|
||||
/// - attributes: DiffAttributes
|
||||
/// - Returns: NSAttributedString
|
||||
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, 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))
|
||||
extension Array where Element == DiffScript {
|
||||
func reverseIndex<T>(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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
extension NSAttributedString {
|
||||
private static func script<T: Equatable & Hashable>(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()
|
||||
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
|
||||
@testable import Sdifft
|
||||
|
||||
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 }
|
||||
}
|
||||
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 testDiff() {
|
||||
let scripts = Diff(source: .init("b"), target: .init("abcd")).scripts
|
||||
|
||||
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 == []
|
||||
)
|
||||
|
||||
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)
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,24 +8,33 @@ import UIKit
|
|||
typealias Color = UIColor
|
||||
#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 {
|
||||
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)
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue