Mark as deprecated methods generated by compiler

I'm afraid that's the crucial question you need to ask and answer yourself first – if those two numbers should appear as a single element as your Set element / Dictionary key – then you are not right trying using them as is as Set elements / Dictionary keys.

You can probably group struct elements together in a "substruct" to keep your EQ / hash implementation short and less error prone. I am not aware of a way to call through the original implementation, pseudocode:

@available(*, deprecated, message:)
static func == (lhs: Self, rhs: Self) -> Bool {
    super . ==(lhs, rhs) // no such thing
}

Have a look, this may be the solution:

struct Num: Hashable, Codable {
    private static let epsilon: Double = 1e-6
    private var val: Double
    
    init(_ value: Double) {
        self.val = value
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.val = try container.decode(Double.self)
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.val)
    }
    
    var value: Double {
        Double(Int(val / Self.epsilon)) * Self.epsilon
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.value == rhs.value
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(value)
    }
}
struct Test: Codable {
    var x: Num
}

let v = Test(x: Num(1.23456789))
print(v)           // Test(x: App.Num(val: 1.23456789))
print(v.x)         // Num(val: 1.23456789)
print(v.x.value)   // 1.234567
let data = try! JSONEncoder().encode(v)
let s = String(data: data, encoding: .utf8)!
print(s)           // {"x":1.2345678899999999}
let obj = try! JSONDecoder().decode(Test.self, from: data)
print(obj)         // Test(x: App.Num(val: 1.23456789))

Adjust epsilon appropriately for your needs. You may further wrap it into a property wrapper.

With this approach there's no need of having a separate isAlmostEqual and no fear of using ==.

Wouldn't hashing those values be just as invalid as trying to compare with ==?

If the key you save into your dict is 5.4999999999999991118215802999, but you later look for 5.5000000000000008881784197001, you won't find it*.

* Unless it just happens to have a hash collision and map into the same bucket.

I strongly suspect that you want a tree-based dictionary instead of a hash-map based one. It's logarithmic look-up compared to constant-time, but it can correctly answer the question: "What's the closest stored key to 5.5000000000000008881784197001?"

2 Likes

Direct access to dictionary is not possible, and for comparison as I wrote we use isAlmostEqual func. Also such close values can not coexist in one Dictionary, so the correct value will be found.

For now we use NonEmpty<OrderedDictionary>, thanks for pointing this out. I will check TreeDictionary.

Only if the numbers compare equal to begin with...

var dict: [Double: String] = [:]
let a = 5.4999999999999991118215802999
let b = 5.5000000000000008881784197001
dict[a] = "foo"
dict[b] = "bar"
print(a == b) // false
print(dict) // [5.499999999999999: "foo", 5.500000000000001: "bar"]

dict = [:]
let c = 5.49999999999999999
let d = 5.50000000000000000
dict[c] = "foo"
dict[d] = "bar"
print(c == d) // true
print(dict) // [5.5: "bar"]
2 Likes