Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 629
Andrii Lisun Avatar asked Sep 01 '18 11:09

Andrii Lisun


3 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.

like image 126
Andy Turner Avatar answered Nov 12 '22 11:11

Andy Turner


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
));
like image 45
xingbin Avatar answered Nov 12 '22 12:11

xingbin


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]
like image 36
assylias Avatar answered Nov 12 '22 11:11

assylias