Last active
July 19, 2024 19:08
-
-
Save kristopherjohnson/543687c763cd6e524c91 to your computer and use it in GitHub Desktop.
Swift code to find differences between strings and display them in a readable way, useful for displaying unit test results
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// Find first differing character between two strings | |
/// | |
/// :param: s1 First String | |
/// :param: s2 Second String | |
/// | |
/// :returns: .DifferenceAtIndex(i) or .NoDifference | |
public func firstDifferenceBetweenStrings(s1: NSString, s2: NSString) -> FirstDifferenceResult { | |
let len1 = s1.length | |
let len2 = s2.length | |
let lenMin = min(len1, len2) | |
for i in 0..<lenMin { | |
if s1.characterAtIndex(i) != s2.characterAtIndex(i) { | |
return .DifferenceAtIndex(i) | |
} | |
} | |
if len1 < len2 { | |
return .DifferenceAtIndex(len1) | |
} | |
if len2 < len1 { | |
return .DifferenceAtIndex(len2) | |
} | |
return .NoDifference | |
} | |
/// Create a formatted String representation of difference between strings | |
/// | |
/// :param: s1 First string | |
/// :param: s2 Second string | |
/// | |
/// :returns: a string, possibly containing significant whitespace and newlines | |
public func prettyFirstDifferenceBetweenStrings(s1: NSString, s2: NSString) -> NSString { | |
let firstDifferenceResult = firstDifferenceBetweenStrings(s1, s2) | |
return prettyDescriptionOfFirstDifferenceResult(firstDifferenceResult, s1, s2) | |
} | |
/// Create a formatted String representation of a FirstDifferenceResult for two strings | |
/// | |
/// :param: firstDifferenceResult FirstDifferenceResult | |
/// :param: s1 First string used in generation of firstDifferenceResult | |
/// :param: s2 Second string used in generation of firstDifferenceResult | |
/// | |
/// :returns: a printable string, possibly containing significant whitespace and newlines | |
public func prettyDescriptionOfFirstDifferenceResult(firstDifferenceResult: FirstDifferenceResult, s1: NSString, s2: NSString) -> NSString { | |
func diffString(index: Int, s1: NSString, s2: NSString) -> NSString { | |
let markerArrow = "\u{2b06}" // "⬆" | |
let ellipsis = "\u{2026}" // "…" | |
/// Given a string and a range, return a string representing that substring. | |
/// | |
/// If the range starts at a position other than 0, an ellipsis | |
/// will be included at the beginning. | |
/// | |
/// If the range ends before the actual end of the string, | |
/// an ellipsis is added at the end. | |
func windowSubstring(s: NSString, range: NSRange) -> String { | |
let validRange = NSMakeRange(range.location, min(range.length, s.length - range.location)) | |
let substring = s.substringWithRange(validRange) | |
let prefix = range.location > 0 ? ellipsis : "" | |
let suffix = (s.length - range.location > range.length) ? ellipsis : "" | |
return "\(prefix)\(substring)\(suffix)" | |
} | |
// Show this many characters before and after the first difference | |
let windowPrefixLength = 10 | |
let windowSuffixLength = 10 | |
let windowLength = windowPrefixLength + 1 + windowSuffixLength | |
let windowIndex = max(index - windowPrefixLength, 0) | |
let windowRange = NSMakeRange(windowIndex, windowLength) | |
let sub1 = windowSubstring(s1, windowRange) | |
let sub2 = windowSubstring(s2, windowRange) | |
let markerPosition = min(windowSuffixLength, index) + (windowIndex > 0 ? 1 : 0) | |
let markerPrefix = String(count: markerPosition, repeatedValue: " " as Character) | |
let markerLine = "\(markerPrefix)\(markerArrow)" | |
return "Difference at index \(index):\n\(sub1)\n\(sub2)\n\(markerLine)" | |
} | |
switch firstDifferenceResult { | |
case .NoDifference: return "No difference" | |
case .DifferenceAtIndex(let index): return diffString(index, s1, s2) | |
} | |
} | |
/// Result type for firstDifferenceBetweenStrings() | |
public enum FirstDifferenceResult { | |
/// Strings are identical | |
case NoDifference | |
/// Strings differ at the specified index. | |
/// | |
/// This could mean that characters at the specified index are different, | |
/// or that one string is longer than the other | |
case DifferenceAtIndex(Int) | |
} | |
extension FirstDifferenceResult: Printable, DebugPrintable { | |
/// Textual representation of a FirstDifferenceResult | |
public var description: String { | |
switch self { | |
case .NoDifference: | |
return "NoDifference" | |
case .DifferenceAtIndex(let index): | |
return "DifferenceAtIndex(\(index))" | |
} | |
} | |
/// Textual representation of a FirstDifferenceResult for debugging purposes | |
public var debugDescription: String { | |
return self.description | |
} | |
} | |
// Examples | |
let noDiff = prettyFirstDifferenceBetweenStrings("foobar", "foobar") | |
println(noDiff) | |
/* Output: | |
No difference | |
*/ | |
let diffEmpty = prettyFirstDifferenceBetweenStrings("xyzzy", "") | |
println(diffEmpty) | |
/* Output: | |
Difference at index 0: | |
xyzzy | |
⬆ | |
*/ | |
let diff1 = prettyFirstDifferenceBetweenStrings("123456", "1234xx") | |
println(diff1) | |
/* Output: | |
Difference at index 4: | |
123456 | |
1234xx | |
⬆ | |
*/ | |
let diff2 = prettyFirstDifferenceBetweenStrings("123456", "123") | |
println(diff2) | |
/* Output: | |
Difference at index 3: | |
123456 | |
123 | |
⬆ | |
*/ | |
let diff3 = prettyFirstDifferenceBetweenStrings("12", "123456") | |
println(diff3) | |
/* Output: | |
Difference at index 2: | |
12 | |
123456 | |
⬆ | |
*/ | |
let diff4 = prettyFirstDifferenceBetweenStrings("To be, or not to be, that is the question", "To be, or not to be, that is the question") | |
println(diff4) | |
/* Output: | |
Difference at index 17: | |
…or not to be, that is… | |
…or not to be, that i… | |
⬆ | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment