Last active
March 4, 2019 01:42
-
-
Save zoejessica/074d4bf7f378afbc1979e3316539eaff to your computer and use it in GitHub Desktop.
Custom diffing strategy for the pointfree.co swift-snapshot library, to compare dictionaries of named floating point values within a given percentage accuracy threshold.
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 | |
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