diff --git a/src/main/java/com/cedarsoftware/util/DeepEquals.java b/src/main/java/com/cedarsoftware/util/DeepEquals.java
index 9986c159..17c84aa4 100644
--- a/src/main/java/com/cedarsoftware/util/DeepEquals.java
+++ b/src/main/java/com/cedarsoftware/util/DeepEquals.java
@@ -12,6 +12,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -652,7 +653,7 @@ private static boolean compareFloatingPointNumbers(Object a, Object b, double ep
}
/**
- * Correctly handles floating point comparisions.
+ * Correctly handles floating point comparisons.
* source: http://floating-point-gui.de/errors/comparison/
*
* @param a first number
@@ -728,79 +729,118 @@ public static boolean hasCustomEquals(Class> c)
*
* This method will handle cycles correctly (A->B->C->A). In this case,
* Starting with object A, B, or C would yield the same hashCode. If an
- * object encountered (root, suboject, etc.) has a hashCode() method on it
+ * object encountered (root, sub-object, etc.) has a hashCode() method on it
* (that is not Object.hashCode()), that hashCode() method will be called
* and it will stop traversal on that branch.
* @param obj Object who hashCode is desired.
* @return the 'deep' hashCode value for the passed in object.
*/
- public static int deepHashCode(Object obj)
- {
+ public static int deepHashCode(Object obj) {
Set visited = new HashSet<>();
+ return deepHashCode(obj, visited);
+ }
+
+ private static int deepHashCode(Object obj, Set visited) {
LinkedList stack = new LinkedList<>();
stack.addFirst(obj);
int hash = 0;
- while (!stack.isEmpty())
- {
+ while (!stack.isEmpty()) {
obj = stack.removeFirst();
- if (obj == null || visited.contains(obj))
- {
+ if (obj == null || visited.contains(obj)) {
continue;
}
visited.add(obj);
- if (obj.getClass().isArray())
- {
+ // Ensure array order matters to hash
+ if (obj.getClass().isArray()) {
final int len = Array.getLength(obj);
- for (int i = 0; i < len; i++)
- {
- stack.addFirst(Array.get(obj, i));
+ long result = 1;
+
+ for (int i = 0; i < len; i++) {
+ Object element = Array.get(obj, i);
+ result = 31 * result + deepHashCode(element, visited); // recursive
}
+ hash += (int) result;
continue;
}
- if (obj instanceof Collection)
- {
- stack.addAll(0, (Collection>)obj);
+ // Ensure list order matters to hash
+ if (obj instanceof List) {
+ List> list = (List>) obj;
+ long result = 1;
+
+ for (Object element : list) {
+ result = 31 * result + deepHashCode(element, visited); // recursive
+ }
+ hash += (int) result;
continue;
}
- if (obj instanceof Map)
- {
- stack.addAll(0, ((Map, ?>)obj).keySet());
- stack.addAll(0, ((Map, ?>)obj).values());
+ if (obj instanceof Collection) {
+ stack.addAll(0, (Collection>) obj);
continue;
}
- if (obj instanceof Double || obj instanceof Float)
- {
- // just take the integral value for hashcode
- // equality tests things more comprehensively
- stack.add(Math.round(((Number) obj).doubleValue()));
+ if (obj instanceof Map) {
+ stack.addAll(0, ((Map, ?>) obj).keySet());
+ stack.addAll(0, ((Map, ?>) obj).values());
continue;
}
- if (hasCustomHashCode(obj.getClass()))
- { // A real hashCode() method exists, call it.
+ // Protects Floats and Doubles from causing inequality, even if there are within an epsilon distance
+ // of one another. It does this by marshalling values of IEEE 754 numbers to coarser grained resolution,
+ // allowing for dynamic range on obviously different values, but identical values for IEEE 754 values
+ // that are near each other. Since hashes do not have to be unique, this upholds the hashCode()
+ // contract...two hash values that are not the same guarantee the objects are not equal, however, two
+ // values that are the same mean the two objects COULD be equals.
+ if (obj instanceof Float) {
+ hash += hashFloat((Float) obj);
+ continue;
+ } else if (obj instanceof Double) {
+ hash += hashDouble((Double) obj);
+ continue;
+ }
+
+ if (hasCustomHashCode(obj.getClass())) { // A real hashCode() method exists, call it.
hash += obj.hashCode();
continue;
}
Collection fields = ReflectionUtils.getDeepDeclaredFields(obj.getClass());
- for (Field field : fields)
- {
- try
- {
+ for (Field field : fields) {
+ try {
stack.addFirst(field.get(obj));
+ } catch (Exception ignored) {
}
- catch (Exception ignored) { }
}
}
return hash;
}
+ private static final double SCALE_DOUBLE = Math.pow(10, 10);
+
+ private static int hashDouble(double value) {
+ // Normalize the value to a fixed precision
+ double normalizedValue = Math.round(value * SCALE_DOUBLE) / SCALE_DOUBLE;
+ // Convert to long for hashing
+ long bits = Double.doubleToLongBits(normalizedValue);
+ // Standard way to hash a long in Java
+ return (int)(bits ^ (bits >>> 32));
+ }
+
+ private static final float SCALE_FLOAT = (float)Math.pow(10, 5); // Scale according to epsilon for float
+
+ private static int hashFloat(float value) {
+ // Normalize the value to a fixed precision
+ float normalizedValue = Math.round(value * SCALE_FLOAT) / SCALE_FLOAT;
+ // Convert to int for hashing, as float bits can be directly converted
+ int bits = Float.floatToIntBits(normalizedValue);
+ // Return the hash
+ return bits;
+ }
+
/**
* Determine if the passed in class has a non-Object.hashCode() method. This
* method caches its results in static ConcurrentHashMap to benefit
diff --git a/src/test/java/com/cedarsoftware/util/TestDeepEquals.java b/src/test/java/com/cedarsoftware/util/TestDeepEquals.java
index 1c7f1043..40c7e3a2 100644
--- a/src/test/java/com/cedarsoftware/util/TestDeepEquals.java
+++ b/src/test/java/com/cedarsoftware/util/TestDeepEquals.java
@@ -283,6 +283,19 @@ public void testPrimitiveArrays()
assertFalse(DeepEquals.deepEquals(array1, array4));
}
+ @Test
+ public void testArrayOrder()
+ {
+ int array1[] = { 3, 4, 7 };
+ int array2[] = { 7, 3, 4 };
+
+ int x = DeepEquals.deepHashCode(array1);
+ int y = DeepEquals.deepHashCode(array2);
+ assertNotEquals(x, y);
+
+ assertFalse(DeepEquals.deepEquals(array1, array2));
+ }
+
@Test
public void testOrderedCollection()
{
@@ -304,7 +317,27 @@ public void testOrderedCollection()
assertTrue(DeepEquals.deepEquals(x1, x2));
}
- @Test
+ @Test
+ public void testOrderedDoubleCollection() {
+ List aa = asList(log(pow(E, 2)), tan(PI / 4));
+ List bb = asList(2.0, 1.0);
+ List cc = asList(1.0, 2.0);
+ assertEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(bb));
+ assertNotEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(cc));
+ assertNotEquals(DeepEquals.deepHashCode(bb), DeepEquals.deepHashCode(cc));
+ }
+
+ @Test
+ public void testOrderedFloatCollection() {
+ List aa = asList((float)log(pow(E, 2)), (float)tan(PI / 4));
+ List bb = asList(2.0f, 1.0f);
+ List cc = asList(1.0f, 2.0f);
+ assertEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(bb));
+ assertNotEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(cc));
+ assertNotEquals(DeepEquals.deepHashCode(bb), DeepEquals.deepHashCode(cc));
+ }
+
+ @Test
public void testUnorderedCollection()
{
Set a = new HashSet<>(asList("one", "two", "three", "four", "five"));
@@ -317,10 +350,20 @@ public void testUnorderedCollection()
Set d = new HashSet<>(asList(4, 2, 6));
assertFalse(DeepEquals.deepEquals(c, d));
- Set x1 = new HashSet<>(asList(new Class1(true, log(pow(E, 2)), 6), new Class1(true, tan(PI / 4), 1)));
- Set x2 = new HashSet<>(asList(new Class1(true, 1, 1), new Class1(true, 2, 6)));
- assertTrue(DeepEquals.deepEquals(x1, x2));
+ Set x1 = new LinkedHashSet<>();
+ x1.add(new Class1(true, log(pow(E, 2)), 6));
+ x1.add(new Class1(true, tan(PI / 4), 1));
+
+ Set x2 = new HashSet<>();
+ x2.add(new Class1(true, 1, 1));
+ x2.add(new Class1(true, 2, 6));
+ int x = DeepEquals.deepHashCode(x1);
+ int y = DeepEquals.deepHashCode(x2);
+
+ assertEquals(x, y);
+ assertTrue(DeepEquals.deepEquals(x1, x2));
+
// Proves that objects are being compared against the correct objects in each collection (all objects have same
// hash code, so the unordered compare must handle checking item by item for hash-collided items)
Set d1 = new LinkedHashSet<>();
@@ -341,6 +384,21 @@ public void testUnorderedCollection()
assert !DeepEquals.deepEquals(d2, d1);
}
+ @Test
+ public void testSetOrder() {
+ Set a = new LinkedHashSet<>();
+ Set b = new LinkedHashSet<>();
+ a.add("a");
+ a.add("b");
+ a.add("c");
+
+ b.add("c");
+ b.add("a");
+ b.add("b");
+ assertEquals(DeepEquals.deepHashCode(a), DeepEquals.deepHashCode(b));
+ assertTrue(DeepEquals.deepEquals(a, b));
+ }
+
@SuppressWarnings("unchecked")
@Test
public void testEquivalentMaps()
diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java
index 74ccc2db..eefe3f6f 100644
--- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java
+++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java
@@ -192,6 +192,11 @@ private static void loadMapTests() {
{(byte)1, mapOf(VALUE, (byte)1)},
{(byte)2, mapOf(VALUE, (byte)2)}
});
+ TEST_DB.put(pair(Integer.class, Map.class), new Object[][]{
+ {-1, mapOf(VALUE, -1)},
+ {0, mapOf(VALUE, 0)},
+ {1, mapOf(VALUE, 1)}
+ });
TEST_DB.put(pair(Float.class, Map.class), new Object[][]{
{1.0f, mapOf(VALUE, 1.0f)},
{2.0f, mapOf(VALUE, 2.0f)}
@@ -261,6 +266,23 @@ private static void loadAtomicBooleanTests() {
TEST_DB.put(pair(Void.class, AtomicBoolean.class), new Object[][]{
{null, null}
});
+ TEST_DB.put(pair(Integer.class, AtomicBoolean.class), new Object[][]{
+ {-1, new AtomicBoolean(true)},
+ {0, new AtomicBoolean(false), true},
+ {1, new AtomicBoolean(true), true},
+ });
+ TEST_DB.put(pair(Float.class, AtomicBoolean.class), new Object[][]{
+ {1.9f, new AtomicBoolean(true)},
+ {1.0f, new AtomicBoolean(true), true},
+ {-1.0f, new AtomicBoolean(true)},
+ {0.0f, new AtomicBoolean(false), true},
+ });
+ TEST_DB.put(pair(Double.class, AtomicBoolean.class), new Object[][]{
+ {1.1, new AtomicBoolean(true)},
+ {1.0, new AtomicBoolean(true), true},
+ {-1.0, new AtomicBoolean(true)},
+ {0.0, new AtomicBoolean(false), true},
+ });
TEST_DB.put(pair(AtomicBoolean.class, AtomicBoolean.class), new Object[][] {
{ new AtomicBoolean(false), new AtomicBoolean(false)},
{ new AtomicBoolean(true), new AtomicBoolean(true)},
@@ -275,18 +297,6 @@ private static void loadAtomicBooleanTests() {
{ new AtomicLong((byte)0), new AtomicBoolean(false), true},
{ new AtomicLong((byte)1), new AtomicBoolean(true), true},
});
- TEST_DB.put(pair(Float.class, AtomicBoolean.class), new Object[][]{
- {1.9f, new AtomicBoolean(true)},
- {1.0f, new AtomicBoolean(true), true},
- {-1.0f, new AtomicBoolean(true)},
- {0.0f, new AtomicBoolean(false), true},
- });
- TEST_DB.put(pair(Double.class, AtomicBoolean.class), new Object[][]{
- {1.1, new AtomicBoolean(true)},
- {1.0, new AtomicBoolean(true), true},
- {-1.0, new AtomicBoolean(true)},
- {0.0, new AtomicBoolean(false), true},
- });
TEST_DB.put(pair(BigInteger.class, AtomicBoolean.class), new Object[][] {
{ BigInteger.valueOf(-1), new AtomicBoolean(true)},
{ BigInteger.ZERO, new AtomicBoolean(false), true},
@@ -326,6 +336,13 @@ private static void loadAtomicIntegerTests() {
TEST_DB.put(pair(Void.class, AtomicInteger.class), new Object[][]{
{null, null}
});
+ TEST_DB.put(pair(Integer.class, AtomicInteger.class), new Object[][]{
+ {-1, new AtomicInteger(-1)},
+ {0, new AtomicInteger(0), true},
+ {1, new AtomicInteger(1), true},
+ {Integer.MIN_VALUE, new AtomicInteger(-2147483648)},
+ {Integer.MAX_VALUE, new AtomicInteger(2147483647)},
+ });
TEST_DB.put(pair(AtomicInteger.class, AtomicInteger.class), new Object[][] {
{ new AtomicInteger(1), new AtomicInteger((byte)1), true}
});
@@ -712,7 +729,16 @@ private static void loadLocalDateTimeTests() {
cal.set(2024, Calendar.MARCH, 2, 22, 54, 17);
cal.set(Calendar.MILLISECOND, 0);
return cal;
- }, LocalDateTime.of(2024, Month.MARCH, 2, 22, 54, 17), true }
+ }, LocalDateTime.of(2024, Month.MARCH, 2, 22, 54, 17), true}
+ });
+ TEST_DB.put(pair(java.sql.Date.class, LocalDateTime.class), new Object[][]{
+ {new java.sql.Date(-62167219200000L), ZonedDateTime.parse("0000-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(-62167219199999L), ZonedDateTime.parse("0000-01-01T00:00:00.001Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(-1000L), ZonedDateTime.parse("1969-12-31T23:59:59Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(-1L), ZonedDateTime.parse("1969-12-31T23:59:59.999Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(0L), ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(1L), ZonedDateTime.parse("1970-01-01T00:00:00.001Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
+ {new java.sql.Date(999L), ZonedDateTime.parse("1970-01-01T00:00:00.999Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
});
TEST_DB.put(pair(Instant.class, LocalDateTime.class), new Object[][] {
{Instant.parse("0000-01-01T00:00:00Z"), ZonedDateTime.parse("0000-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDateTime(), true},
@@ -1240,6 +1266,17 @@ private static void loadSqlDateTests() {
{new Date(1), new java.sql.Date(1), true },
{new Date(Long.MAX_VALUE), new java.sql.Date(Long.MAX_VALUE), true },
});
+ TEST_DB.put(pair(LocalDate.class, java.sql.Date.class), new Object[][] {
+ {ZonedDateTime.parse("0000-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-62167252739000L), true},
+ {ZonedDateTime.parse("0000-01-01T00:00:00.001Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-62167252739000L), true},
+ {ZonedDateTime.parse("1969-12-31T14:59:59.999Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-118800000L), true},
+ {ZonedDateTime.parse("1969-12-31T15:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ {ZonedDateTime.parse("1969-12-31T23:59:59.999Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ {ZonedDateTime.parse("1970-01-01T00:00:00Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ {ZonedDateTime.parse("1970-01-01T00:00:00.001Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ {ZonedDateTime.parse("1970-01-01T00:00:00.999Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ {ZonedDateTime.parse("1970-01-01T00:00:00.999Z").withZoneSameInstant(TOKYO_Z).toLocalDate(), new java.sql.Date(-32400000L), true},
+ });
TEST_DB.put(pair(Calendar.class, java.sql.Date.class), new Object[][] {
{(Supplier) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
@@ -2600,17 +2637,6 @@ private static void loadIntegerTests() {
{-2147483648.0, Integer.MIN_VALUE},
{2147483647.0, Integer.MAX_VALUE},
});
- TEST_DB.put(pair(AtomicBoolean.class, Integer.class), new Object[][]{
- {new AtomicBoolean(true), 1},
- {new AtomicBoolean(false), 0},
- });
- TEST_DB.put(pair(AtomicInteger.class, Integer.class), new Object[][]{
- {new AtomicInteger(-1), -1},
- {new AtomicInteger(0), 0},
- {new AtomicInteger(1), 1},
- {new AtomicInteger(-2147483648), Integer.MIN_VALUE},
- {new AtomicInteger(2147483647), Integer.MAX_VALUE},
- });
TEST_DB.put(pair(AtomicLong.class, Integer.class), new Object[][]{
{new AtomicLong(-1), -1, true},
{new AtomicLong(0), 0, true},
@@ -2628,17 +2654,17 @@ private static void loadIntegerTests() {
{new BigInteger("2147483648"), Integer.MIN_VALUE},
});
TEST_DB.put(pair(BigDecimal.class, Integer.class), new Object[][]{
- {new BigDecimal("-1"), -1},
+ {new BigDecimal("-1"), -1, true},
{new BigDecimal("-1.1"), -1},
{new BigDecimal("-1.9"), -1},
- {BigDecimal.ZERO, 0},
- {new BigDecimal("1"), 1},
+ {BigDecimal.ZERO, 0, true},
+ {new BigDecimal("1"), 1, true},
{new BigDecimal("1.1"), 1},
{new BigDecimal("1.9"), 1},
- {new BigDecimal("-2147483648"), Integer.MIN_VALUE},
- {new BigDecimal("2147483647"), Integer.MAX_VALUE},
- {new BigDecimal("-2147483649"), Integer.MAX_VALUE},
- {new BigDecimal("2147483648"), Integer.MIN_VALUE},
+ {new BigDecimal("-2147483648"), Integer.MIN_VALUE, true},
+ {new BigDecimal("2147483647"), Integer.MAX_VALUE, true},
+ {new BigDecimal("-2147483649"), Integer.MAX_VALUE}, // wrap around test
+ {new BigDecimal("2147483648"), Integer.MIN_VALUE}, // wrap around test
});
TEST_DB.put(pair(Number.class, Integer.class), new Object[][]{
{-2L, -2},