JAL-1705 JAL-1686 stronger SequenceFeature.equals() test
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 12 Feb 2016 16:42:34 +0000 (16:42 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 12 Feb 2016 16:42:34 +0000 (16:42 +0000)
src/jalview/datamodel/SequenceFeature.java
test/jalview/datamodel/SequenceFeatureTest.java

index 252f46c..8146400 100755 (executable)
@@ -133,23 +133,93 @@ public class SequenceFeature
     this.featureGroup = featureGroup;
   }
 
-  public boolean equals(SequenceFeature sf)
+  /**
+   * Two features are considered equal if they have the same type, group,
+   * description, start, end, phase, strand, and (if present) 'Name', ID' and
+   * 'Parent' attributes.
+   * 
+   * Note we need to check Parent to distinguish the same exon occurring in
+   * different transcripts (in Ensembl GFF). This allows assembly of transcript
+   * sequences from their component exon regions.
+   */
+  @Override
+  public boolean equals(Object o)
+  {
+    return equals(o, false);
+  }
+
+  /**
+   * Overloaded method allows the equality test to optionally ignore the
+   * 'Parent' attribute of a feature. This supports avoiding adding many
+   * superficially duplicate 'exon' or CDS features to genomic or protein
+   * sequence.
+   * 
+   * @param o
+   * @param ignoreParent
+   * @return
+   */
+  public boolean equals(Object o, boolean ignoreParent)
   {
+    if (o == null || !(o instanceof SequenceFeature))
+    {
+      return false;
+    }
+
+    SequenceFeature sf = (SequenceFeature) o;
     if (begin != sf.begin || end != sf.end || score != sf.score)
     {
       return false;
     }
 
-    if (!(type + description + featureGroup).equals(sf.type
-            + sf.description + sf.featureGroup))
+    if (getStrand() != sf.getStrand())
     {
       return false;
     }
 
+    if (!(type + description + featureGroup + getPhase()).equals(sf.type
+            + sf.description + sf.featureGroup + sf.getPhase()))
+    {
+      return false;
+    }
+    if (!equalAttribute(getValue("ID"), sf.getValue("ID")))
+    {
+      return false;
+    }
+    if (!equalAttribute(getValue("Name"), sf.getValue("Name")))
+    {
+      return false;
+    }
+    if (!ignoreParent)
+    {
+      if (!equalAttribute(getValue("Parent"), sf.getValue("Parent")))
+      {
+        return false;
+      }
+    }
     return true;
   }
 
   /**
+   * Returns true if both values are null, are both non-null and equal
+   * 
+   * @param att1
+   * @param att2
+   * @return
+   */
+  protected static boolean equalAttribute(Object att1, Object att2)
+  {
+    if (att1 == null && att2 == null)
+    {
+      return true;
+    }
+    if (att1 != null)
+    {
+      return att1.equals(att2);
+    }
+    return att2.equals(att1);
+  }
+
+  /**
    * DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
@@ -378,4 +448,18 @@ public class SequenceFeature
     return String.format("%d %d %s %s", getBegin(), getEnd(), getType(),
             getDescription());
   }
+
+  /**
+   * Overridden to ensure that whenever two objects are equal, they have the
+   * same hashCode
+   */
+  @Override
+  public int hashCode()
+  {
+    String s = getType() + getDescription() + getFeatureGroup()
+            + getValue("ID") + getValue("Name") + getValue("Parent")
+            + getPhase();
+    return s.hashCode() + getBegin() + getEnd() + (int) getScore()
+            + getStrand();
+  }
 }
index d488a76..82b260e 100644 (file)
@@ -1,8 +1,10 @@
 package jalview.datamodel;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
 
 import org.testng.annotations.Test;
 
@@ -62,4 +64,90 @@ public class SequenceFeatureTest
     sf.setValue("STRAND", ".");
     assertEquals(0, sf.getStrand());
   }
+
+  /**
+   * Tests for equality, and that equal objects have the same hashCode
+   */
+  @Test(groups = { "Functional" })
+  public void testEqualsAndHashCode()
+  {
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 22, 33,
+            12.5f, "group");
+    sf1.setValue("ID", "id");
+    sf1.setValue("Name", "name");
+    sf1.setValue("Parent", "parent");
+    sf1.setStrand("+");
+    sf1.setPhase("1");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 22, 33,
+            12.5f, "group");
+    sf2.setValue("ID", "id");
+    sf2.setValue("Name", "name");
+    sf2.setValue("Parent", "parent");
+    sf2.setStrand("+");
+    sf2.setPhase("1");
+
+    assertFalse(sf1.equals(null));
+    assertTrue(sf1.equals(sf2));
+    assertTrue(sf2.equals(sf1));
+    assertEquals(sf1.hashCode(), sf2.hashCode());
+
+    // changing type breaks equals:
+    sf2.setType("Type");
+    assertFalse(sf1.equals(sf2));
+
+    // changing description breaks equals:
+    sf2.setType("type");
+    sf2.setDescription("Desc");
+    assertFalse(sf1.equals(sf2));
+
+    // changing start position breaks equals:
+    sf2.setDescription("desc");
+    sf2.setBegin(21);
+    assertFalse(sf1.equals(sf2));
+
+    // changing end position breaks equals:
+    sf2.setBegin(22);
+    sf2.setEnd(32);
+    assertFalse(sf1.equals(sf2));
+
+    // changing feature group breaks equals:
+    sf2.setEnd(33);
+    sf2.setFeatureGroup("Group");
+    assertFalse(sf1.equals(sf2));
+
+    // changing ID breaks equals:
+    sf2.setFeatureGroup("group");
+    sf2.setValue("ID", "id2");
+    assertFalse(sf1.equals(sf2));
+
+    // changing Name breaks equals:
+    sf2.setValue("ID", "id");
+    sf2.setValue("Name", "Name");
+    assertFalse(sf1.equals(sf2));
+
+    // changing Parent breaks equals:
+    sf2.setValue("Name", "name");
+    sf1.setValue("Parent", "Parent");
+    assertFalse(sf1.equals(sf2));
+
+    // changing strand breaks equals:
+    sf1.setValue("Parent", "parent");
+    sf2.setStrand("-");
+    assertFalse(sf1.equals(sf2));
+
+    // changing phase breaks equals:
+    sf2.setStrand("+");
+    sf1.setPhase("2");
+    assertFalse(sf1.equals(sf2));
+
+    // restore equality as sanity check:
+    sf1.setPhase("1");
+    assertTrue(sf1.equals(sf2));
+    assertTrue(sf2.equals(sf1));
+    assertEquals(sf1.hashCode(), sf2.hashCode());
+
+    // changing status doesn't change equals:
+    sf1.setStatus("new");
+    assertTrue(sf1.equals(sf2));
+  }
 }