Floating point inaccuracies are really annoying. I understood that in its true sense while developing the next version of Point (this time I'm actually foolproofing my code). Before I upload it for review, I want a class that I wrote, called Numbers
to be reviewed.
Numbers.java
package library.util;
/**
* This class provides a useful interface for easy (approximate) floating
* point equality checks. This is necessary for several other classes.
*
* @author Subhomoy Haldar (ambigram_maker)
* @version 1.0
*/
public class Numbers {
/**
* The tolerance value for comparing the {@code float} values.
*/
public static double FLOAT_TOLERANCE = 5E-8;
/**
* The tolerance value for comparing the {@code double} values.
*/
public static double DOUBLE_TOLERANCE = 5E-16;
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param bytes The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(byte... bytes) {
int length = bytes.length;
checkLength(length);
byte d = bytes[0];
for (int i = 1; i < length; i++) {
if (d != bytes[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param shorts The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(short... shorts) {
int length = shorts.length;
checkLength(length);
short d = shorts[0];
for (int i = 1; i < length; i++) {
if (d != shorts[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param ints The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(int... ints) {
int length = ints.length;
checkLength(length);
int d = ints[0];
for (int i = 1; i < length; i++) {
if (d != ints[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param longs The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(long... longs) {
int length = longs.length;
checkLength(length);
long d = longs[0];
for (int i = 1; i < length; i++) {
if (d != longs[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param chars The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(char... chars) {
int length = chars.length;
checkLength(length);
char d = chars[0];
for (int i = 1; i < length; i++) {
if (d != chars[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param floats The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(float... floats) {
int length = floats.length;
checkLength(length);
float d = floats[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, floats[i])) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param doubles The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(double... doubles) {
int length = doubles.length;
checkLength(length);
double d = doubles[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, doubles[i])) {
return false;
}
}
return true;
}
private static void checkLength(int length) throws
IllegalArgumentException {
if (length < 2) {
throw new IllegalArgumentException
("At least two arguments required.");
}
}
private static boolean areEqual(float f1, float f2) {
// the corner cases first:
if (Float.isNaN(f1) && Float.isNaN(f2)) {
return true;
}
if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
return f1 == f2;
}
float abs;
if (f1 == f2 || (abs = Math.abs(f1 - f2)) <= FLOAT_TOLERANCE) {
return true;
}
// compare using the larger ulp
float ulp1 = Math.ulp(f1);
float ulp2 = Math.ulp(f2);
return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
}
private static boolean areEqual(double d1, double d2) {
// the corner cases first:
if (Double.isNaN(d1) && Double.isNaN(d2)) {
return true;
}
if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
return d1 == d2;
}
double abs;
if (d1 == d2 || (abs = Math.abs(d1 - d2)) <= DOUBLE_TOLERANCE) {
return true;
}
// compare using the larger ulp
double ulp1 = Math.ulp(d1);
double ulp2 = Math.ulp(d2);
return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
}
}
I want you to hit it with your best shot and check the shortcomings of the implementation (especially the float
and double
methods).
Reason (and usage) for Numbers.java
I would recommend using the methods as follows:
boolean example = Numbers.areEqual(60, Math.toDegrees(Math.acos(0.5)));
This is (in my opinion) easy to read and understand what it does.
(1/3)*3
is not 1. \$\endgroup\$tan 90
is notinfinity
. ;-) \$\endgroup\$tan 90
isinfinity
(loosely speaking), as istan π/2
; those are statements about real numbers.tan(3.14159/2)
is probably a large finite value; that's a statement about floating-point arithmetic. \$\endgroup\$