Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active July 19, 2024 19:08
Show Gist options
  • Save kristopherjohnson/543687c763cd6e524c91 to your computer and use it in GitHub Desktop.
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
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