Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
You can’t perform that action at this time.