Is it possible to set precision for Set<Double>.contains()?

Viewed 160

Let's say we have an implementation of Set<Double>. It contains the following values: [2.0, 5.0, 7.0].

contains(2.0001d) in this case returns false because double values are compared by exact match.

Is it possible to set some double precision for boolean contains(Object o) method?

If it is not possible what workaround can you suggest except storing values in a sequential collection, iterating through it and comparing each value?

4 Answers

Set.contains has a precise definition based upon equality:

 More formally, returns true if and only if this set contains an element e such that (o==null ? e==null : o.equals(e)).

It would violate the contract of the method of it used anything other than equality. And equality has a precise definition which says that it must be transitive (amongst other properties). An equality method which uses a tolerance is not transitive.

As such, there is no way for Set.contains to allow a tolerance.

However, this is not to say that you shouldn't ever check to see if a set contains a value within a tolerance of some value - just don't try to overload the concept of contains to do it.

For example, you could have a method which takes a NavigableSet (for example a TreeSet), and use its subSet method:

static boolean containsApprox(NavigableSet<Double> set, double target, double eps) {
  return !set.subSet(target - eps, true, target + eps, true).isEmpty();
}

This just requests the portion of the set which runs from target-eps to target+eps (inclusive, as indicated by the true parameters). If this is non-empty, there is a value in the set within eps of the target.

This is clearly a separate concept from the standard Set.contains, so it is fine for this to do a contains check which doesn't share the same properties.

You can't do the same subSet trick with a HashMap because it is an unordered map - there is no efficient way to extract the values in a given range. You would have to iterate the entire set, as in Sun's answer, looking for a matching value.

May be you can use anyMatch, for example, compared based on the first two digit after .:

Set<Double> set = Set.of(2.0, 5.0, 7.0);

Double compared = 2.0001d;

System.out.println(
        set.stream().anyMatch(aDouble -> 
                Math.floor(aDouble * 100) / 100 == Math.floor(compared * 100) / 100
));

I can see three options:

  • round the numbers before adding them to the set
  • write a double wrapper class and redefine the equals method
  • use a TreeSet with a custom comparator

Note that the last two options may or may not be satisfactory in the sense that the order in which you populate the set has an impact on which elements are retained. For example if you set the precision to 0.01 and add 0.01 then 0.011 then 0.02, you will have two elements in the set (0.01 and 0.02). If you add 0.011 then 0.01 then 0.02, you will only have one element: 0.011. I don't know if that's a problem for your use case.

The last option could look like this:

static Set<Double> setWithPrecision(double epsilon) {
  return new TreeSet<> ((d1, d2) -> {
    if (d1 <= d2 - epsilon) return -1;
    if (d1 >= d2 + epsilon) return 1;
    return 0;
  });
}

Example use:

Set<Double> set = setWithPrecision(0.01);
set.add(0d);
set.add(0.00001d);
set.add(0.01d);
set.add(0.02d);

System.out.println(set); // [0.0, 0.01, 0.02]

Suppose that your HashSet#contains calling point, is within a method that somehow receives a Double from somewhere, you could simply go ahead and set the precision of the Double. While Double does not offer an out of the box way of rounding a Double object you could very well create a rounding helper that will accept the precision you would like to achieve and return the rounded number.

For example:

public static Double roundToPrecision(Double number, int precision) {
    return BigDecimal.valueOf(number)
        .setScale(precision, RoundingMode.DOWN)
        .doubleValue();
}

Passing in 2.0001d will effectively result in this getting round to 2.0.

Related