Custom diffing strategy for the pointfree.co swift-snapshot library, to compare dictionaries of named floating point values within a given percentage accuracy threshold.
import Foundation | |
import SnapshotTesting | |
import XCTest | |
public typealias NamesToValues = [String : Double] | |
public func approximateValues(tolerance percentage: Double) -> SimplySnapshotting<NamesToValues> { | |
let diffingStrategy = Diffing.approximateValues(tolerance: percentage) | |
return SimplySnapshotting.init(pathExtension: "json", diffing: diffingStrategy) | |
} | |
// Grab a refererence to the standard string diff function | |
// We'll throw away the failure string message, but use the XCTAttachment it returns | |
private let linesDiffer = Diffing.lines.diff | |
public extension Diffing where Value == NamesToValues { | |
static let approximateTo1Percent: Diffing<NamesToValues> = approximateValues(tolerance: 1.0) | |
static let approximateTo3Percent: Diffing<NamesToValues> = approximateValues(tolerance: 3.0) | |
static func approximateValues(tolerance percentage: Double) -> Diffing<NamesToValues> { | |
return Diffing.init(toData: { ntv in | |
return try! encoder.encode(ntv) | |
}, fromData: { data in | |
return try! JSONDecoder().decode(NamesToValues.self, from: data) | |
}) { (oldDict, newDict) -> (String, [XCTAttachment])? in | |
var failureMessage: String = "" | |
// If the dictionary keys don't match, just return the string diff of the keys | |
guard oldDict.keys == newDict.keys else { | |
let (_ , attachment) = linesDiffer(oldDict.keys.joined(separator: "\n"), newDict.keys.joined(separator: "\n"))! | |
return ("Keys did not match", attachment) | |
} | |
// Check values to see if they are more than the threshold percentage apart | |
// If so return the whole dictionary as the diff | |
for (name, oldValue) in oldDict { | |
let newValue = newDict[name]! | |
if let percentage = percentageDifferenceIfOverThreshold(oldValue, newValue, threshold: percentage) { | |
failureMessage.append("\(name) was \(nf.string(from: NSNumber(value: percentage))!)% off from recorded value\n") | |
} | |
} | |
if failureMessage != "" { | |
let (_ , attachment) = diff(oldDict, newDict)! | |
return (failureMessage, attachment) | |
} else { | |
return nil | |
} | |
} | |
} | |
private static let encoder: JSONEncoder = { | |
let e = JSONEncoder() | |
e.outputFormatting = [.prettyPrinted, .sortedKeys] | |
return e | |
}() | |
private static func diff(_ old: NamesToValues, _ new: NamesToValues) -> (String, [XCTAttachment])? { | |
let oldString = try! String(decoding: encoder.encode(old), as: UTF8.self) | |
let newString = try! String(decoding: encoder.encode(new), as: UTF8.self) | |
return linesDiffer(oldString, newString) | |
} | |
private static func percentageDifferenceIfOverThreshold(_ old: Double, _ new: Double, threshold: Double) -> Double? { | |
guard old != new else { return nil } | |
let percentageOff = abs(old - new)/old | |
if percentageOff > (threshold / 100) { | |
return percentageOff * 100 | |
} else { | |
return nil | |
} | |
} | |
private static let nf: NumberFormatter = { | |
let nf = NumberFormatter.init() | |
nf.maximumFractionDigits = 2 | |
return nf | |
}() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment