You could wrap the Double in another class that provides a 'close enough' aspect to its equals method.
package com.michaelt.so.doub;
import java.util.HashSet;
import java.util.Set;
public class CloseEnough {
private Double d;
protected long masked;
protected Set<Long> similar;
public CloseEnough(Double d) {
this.d = d;
long bits = Double.doubleToLongBits(d);
similar = new HashSet<Long>();
masked = bits & 0xFFFFFFFFFFFFFFF8L; // 111...1000
similar.add(bits);
similar.add(bits + 1);
similar.add(bits - 1);
}
Double getD() {
return d;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CloseEnough)) {
return false;
}
CloseEnough that = (CloseEnough) o;
for(Long bits : this.similar) {
if(that.similar.contains(bits)) { return true; }
}
return false;
}
@Override
public int hashCode() {
return (int) (masked ^ (masked >>> 32));
}
}
And then some code to demonstrate it:
package com.michaelt.so.doub;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<CloseEnough> foo = new ArrayList<CloseEnough>();
foo.add(new CloseEnough(1.38));
foo.add(new CloseEnough(0.02));
foo.add(new CloseEnough(1.40));
foo.add(new CloseEnough(0.20));
System.out.println(foo.contains(new CloseEnough(0.0)));
System.out.println(foo.contains(new CloseEnough(1.37 + 0.01)));
System.out.println(foo.contains(new CloseEnough(0.01 + 0.01)));
System.out.println(foo.contains(new CloseEnough(1.39 + 0.01)));
System.out.println(foo.contains(new CloseEnough(0.19 + 0.01)));
}
}
The output of this code is:
false
true
true
true
true
(that first false is the compare with 0, just to show that it isn't finding things that aren't there)
CloseEnough is just a simple wrapper around the double that masks the lowest three bits for the hash code (enough that and also stores the valid set of similar numbers in a set. When doing an equals comparison, it uses the sets. Two numbers are equal if they contain a common element in their sets.
That said, I am fairly certain that there are some values that would be problematic with a.equals(b)
being true and a.hashCode() == b.hashCode()
being false that may still occur at edge conditions for the proper bit patterns - this would make some things (like HashSet and HashMap) 'unhappy' (and would likely make a good question somewhere.
Probably a better approach to this would instead be to extend ArrayList so that the indexOf
method handles the similarity between the numbers:
package com.michaelt.so.doub;
import java.util.ArrayList;
public class SimilarList extends ArrayList<Double> {
@Override
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < this.size(); i++) {
if (get(i) == null) {
return i;
}
}
} else {
for (int i = 0; i < this.size(); i++) {
if (almostEquals((Double)o, this.get(i))) {
return i;
}
}
}
return -1;
}
private boolean almostEquals(Double a, Double b) {
long abits = Double.doubleToLongBits(a);
long bbits = Double.doubleToLongBits(b);
// Handle +0 == -0
if((abits >> 63) != (bbits >> 63)) {
return a.equals(b);
}
long diff = Math.abs(abits - bbits);
if(diff <= 1) {
return true;
}
return false;
}
}
Working with this code becomes a bit easier (pun not intended):
package com.michaelt.so.doub;
import java.util.ArrayList;
public class ListTest {
public static void main(String[] args) {
ArrayList foo = new SimilarList();
foo.add(1.38);
foo.add(1.40);
foo.add(0.02);
foo.add(0.20);
System.out.println(foo.contains(0.0));
System.out.println(foo.contains(1.37 + 0.01));
System.out.println(foo.contains(1.39 + 0.01));
System.out.println(foo.contains(0.19 + 0.01));
System.out.println(foo.contains(0.01 + 0.01));
}
}
The output of this code is:
false
true
true
true
true
In this case, the bit fiddling is done in the SimilarList based on the code HasMinimalDifference. Again, the numbers are converted into bits, but this time the math is done in the comparison rather than trying to work with the equality and hash code of a wrapper object.