JAL-2446 pseudo-random generator unit test, and fixes arising
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 3 Apr 2017 09:07:03 +0000 (10:07 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 3 Apr 2017 09:07:03 +0000 (10:07 +0100)
src/jalview/datamodel/features/NCList.java
src/jalview/datamodel/features/NCNode.java
test/jalview/datamodel/features/NCListTest.java

index 7046670..40b062a 100644 (file)
@@ -14,6 +14,10 @@ import java.util.List;
  */
 public class NCList<T extends ContiguousI>
 {
+  /*
+   * the number of ranges represented
+   */
+  private int size;
 
   /*
    * a list, in start position order, of sublists of ranges ordered so 
@@ -38,6 +42,7 @@ public class NCList<T extends ContiguousI>
    */
   public NCList(List<T> ranges)
   {
+    this();
     build(ranges);
   }
 
@@ -54,8 +59,6 @@ public class NCList<T extends ContiguousI>
 
     List<SubList> sublists = findSubranges(ranges);
 
-    subranges = new ArrayList<NCNode<T>>();
-
     /*
      * convert each subrange to an NCNode consisting of a range and
      * (possibly) its contained NCList
@@ -65,16 +68,23 @@ public class NCList<T extends ContiguousI>
       subranges.add(new NCNode<T>(ranges.subList(sublist.startIndex,
               sublist.endIndex + 1)));
     }
-    sublists.clear();
+
+    size = ranges.size();
   }
 
   public NCList(T entry)
   {
+    this();
     List<T> ranges = new ArrayList<T>();
     ranges.add(entry);
     build(ranges);
   }
 
+  public NCList()
+  {
+    subranges = new ArrayList<NCNode<T>>();
+  }
+
   /**
    * Traverses the sorted ranges to identify sublists, within which each
    * interval contains the one that follows it
@@ -117,6 +127,7 @@ public class NCList<T extends ContiguousI>
    */
   public synchronized void add(T entry)
   {
+    size++;
     long start = entry.getBegin();
     long end = entry.getEnd();
 
@@ -146,15 +157,16 @@ public class NCList<T extends ContiguousI>
      * search for maximal span of subranges i-k that the new entry
      * encloses; or a subrange that encloses the new entry
      */
-    int i = candidateIndex;
     boolean enclosing = false;
+    int firstEnclosed = 0;
+    int lastEnclosed = 0;
     boolean overlapping = false;
 
     for (int j = candidateIndex; j < subranges.size(); j++)
     {
       NCNode<T> subrange = subranges.get(j);
 
-      if (end < subrange.getStart() && !overlapping)
+      if (end < subrange.getStart() && !overlapping && !enclosing)
       {
         /*
          * new entry lies between subranges j-1 j
@@ -180,6 +192,11 @@ public class NCList<T extends ContiguousI>
            * new entry encloses this subrange (and possibly preceding ones);
            * continue to find the maximal list it encloses
            */
+          if (!enclosing)
+          {
+            firstEnclosed = j;
+          }
+          lastEnclosed = j;
           enclosing = true;
           continue;
         }
@@ -193,7 +210,7 @@ public class NCList<T extends ContiguousI>
             /*
              * entry encloses one or more preceding subranges
              */
-            addEnclosingRange(entry, i, j - 1);
+            addEnclosingRange(entry, firstEnclosed, lastEnclosed);
             return;
           }
           else
@@ -207,22 +224,41 @@ public class NCList<T extends ContiguousI>
           }
         }
       }
+      else
+      {
+        overlapping = true;
+      }
+    }
+
+    /*
+     * drops through to here if new range encloses all others
+     */
+    if (enclosing)
+    {
+      addEnclosingRange(entry, firstEnclosed, lastEnclosed);
     }
   }
   
   /**
    * Update the tree so that the range of the new entry encloses subranges i to
-   * j (inclusive). That is, replace subranges i-j with a new subrange that
-   * contains them.
+   * j (inclusive). That is, replace subranges i-j (inclusive) with a new
+   * subrange that contains them.
    * 
    * @param entry
    * @param i
    * @param j
    */
-  protected synchronized void addEnclosingRange(T entry, int i, int j)
+  protected synchronized void addEnclosingRange(T entry, final int i,
+          final int j)
   {
-    // TODO how to rebuild the subranges as an NCList?
-
+    NCList<T> newNCList = new NCList<T>();
+    newNCList.subranges.addAll(subranges.subList(i, j + 1));
+    NCNode<T> newNode = new NCNode<T>(entry, newNCList);
+    for (int k = j; k >= i; k--)
+    {
+      subranges.remove(k);
+    }
+    subranges.add(i, newNode);
   }
 
   /**
@@ -321,4 +357,108 @@ public class NCList<T extends ContiguousI>
   {
     return subranges.toString();
   }
+
+  /**
+   * Returns a string representation of the data where containment is shown bgy
+   * indentation on new lines
+   * 
+   * @return
+   */
+  public String prettyPrint()
+  {
+    StringBuilder sb = new StringBuilder(512);
+    int offset = 0;
+    int indent = 2;
+    prettyPrint(sb, offset, indent);
+    return sb.toString();
+  }
+
+  /**
+   * @param sb
+   * @param offset
+   * @param indent
+   */
+  void prettyPrint(StringBuilder sb, int offset, int indent)
+  {
+    boolean first = true;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (!first)
+      {
+        sb.append(System.lineSeparator());
+      }
+      first = false;
+      subrange.prettyPrint(sb, offset, indent);
+    }
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  public boolean isValid()
+  {
+    return isValid(Integer.MIN_VALUE, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList bounded within the given start-end range, else false.
+   * <p>
+   * Each subrange must lie within start-end (inclusive). Subranges must be
+   * ordered by start position ascending.
+   * <p>
+   * 
+   * @param start
+   * @param end
+   * @return
+   */
+  boolean isValid(final int start, final int end)
+  {
+    int lastStart = start;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (subrange.getStart() < lastStart)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " starts before " + lastStart);
+        return false;
+      }
+      if (subrange.getEnd() > end)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " ends after " + end);
+        return false;
+      }
+      lastStart = subrange.getStart();
+
+      if (!subrange.isValid())
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Answers the lowest start position enclosed by the ranges
+   * 
+   * @return
+   */
+  public int getStart()
+  {
+    return subranges.isEmpty() ? 0 : subranges.get(0).getStart();
+  }
+
+  /**
+   * Returns the number of ranges held (deep count)
+   * 
+   * @return
+   */
+  public int getSize()
+  {
+    return size;
+  }
 }
index d4c7b0c..ab10f67 100644 (file)
@@ -21,7 +21,7 @@ class NCNode<V extends ContiguousI>
   private NCList<V> subregions;
 
   /**
-   * Constructor
+   * Constructor given a list of ranges
    * 
    * @param ranges
    */
@@ -31,7 +31,7 @@ class NCNode<V extends ContiguousI>
   }
 
   /**
-   * Constructo
+   * Constructor given a single range
    * 
    * @param range
    */
@@ -42,6 +42,13 @@ class NCNode<V extends ContiguousI>
     build(ranges);
   }
 
+  NCNode(V entry, NCList<V> newNCList)
+  {
+    region = entry;
+    subregions = newNCList;
+    // size = 1 + newNCList.size();
+  }
+
   /**
    * @param ranges
    */
@@ -69,6 +76,13 @@ class NCNode<V extends ContiguousI>
     return region.getEnd();
   }
 
+  /**
+   * Formats the node as a bracketed list e.g.
+   * 
+   * <pre>
+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * </pre>
+   */
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder(10 * size);
@@ -80,6 +94,17 @@ class NCNode<V extends ContiguousI>
     return sb.toString();
   }
 
+  void prettyPrint(StringBuilder sb, int offset, int indent) {
+    for (int i = 0 ; i < offset ; i++) {
+      sb.append(" ");
+    }
+    sb.append(region.getBegin()).append("-").append(region.getEnd());
+    if (subregions != null)
+    {
+      sb.append(System.lineSeparator());
+      subregions.prettyPrint(sb, offset + 2, indent);
+    }
+  }
   /**
    * Add any ranges that overlap the from-to range to the result list
    * 
@@ -104,7 +129,7 @@ class NCNode<V extends ContiguousI>
    * 
    * @param entry
    */
-  public synchronized void add(V entry)
+  synchronized void add(V entry)
   {
     if (entry.getBegin() < region.getBegin() || entry.getEnd() > region.getEnd()) {
       throw new IllegalArgumentException(String.format(
@@ -121,4 +146,19 @@ class NCNode<V extends ContiguousI>
       subregions.add(entry);
     }
   }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  boolean isValid()
+  {
+    if (subregions == null)
+    {
+      return true;
+    }
+    return subregions.isValid(getStart(), getEnd());
+  }
 }
index 38227c1..367e424 100644 (file)
@@ -1,10 +1,12 @@
 package jalview.datamodel.features;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Random;
 
 import org.testng.annotations.Test;
 
@@ -58,10 +60,12 @@ public class NCListTest
     NCList<Range> ncl = new NCList<Range>(ranges);
     String expected = "[1-100 [10-30 [10-20 [10-20 [11-19]]]], 15-30 [20-20]]";
     assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
 
     Collections.reverse(ranges);
     ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
   }
 
   @Test(groups = "Functional")
@@ -98,9 +102,11 @@ public class NCListTest
     ranges.add(new Range(20, 50));
     NCList<Range> ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
 
     ncl.add(new Range(60, 70));
     assertEquals(ncl.toString(), "[20-50, 60-70]");
+    assertTrue(ncl.isValid());
   }
 
   @Test(groups = "Functional")
@@ -110,6 +116,7 @@ public class NCListTest
     ranges.add(new Range(20, 50));
     NCList<Range> ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
 
     ncl.add(new Range(30, 40));
     assertEquals(ncl.toString(), "[20-50 [30-40]]");
@@ -122,9 +129,11 @@ public class NCListTest
     ranges.add(new Range(20, 50));
     NCList<Range> ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
 
     ncl.add(new Range(5, 15));
     assertEquals(ncl.toString(), "[5-15, 20-50]");
+    assertTrue(ncl.isValid());
   }
 
   @Test(groups = "Functional")
@@ -135,9 +144,12 @@ public class NCListTest
     ranges.add(new Range(30, 60));
     NCList<Range> ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), "[20-50, 30-60]");
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.getStart(), 20);
 
     ncl.add(new Range(10, 70));
-    assertEquals(ncl.toString(), "[10-70 [20-50, 30-60]");
+    assertEquals(ncl.toString(), "[10-70 [20-50, 30-60]]");
+    assertTrue(ncl.isValid());
   }
 
   @Test(groups = "Functional")
@@ -148,11 +160,46 @@ public class NCListTest
     ranges.add(new Range(60, 70));
     NCList<Range> ncl = new NCList<Range>(ranges);
     assertEquals(ncl.toString(), "[20-40, 60-70]");
+    assertTrue(ncl.isValid());
 
     ncl.add(new Range(30, 50));
     assertEquals(ncl.toString(), "[20-40, 30-50, 60-70]");
+    assertTrue(ncl.isValid());
 
     ncl.add(new Range(40, 65));
     assertEquals(ncl.toString(), "[20-40, 30-50, 40-65, 60-70]");
+    assertTrue(ncl.isValid());
+  }
+
+  /**
+   * Do a number of pseudo-random (reproducible) builds of an NCList, with
+   * checks for validity of the data structure, for greater (but not perfect!)
+   * confidence that corner cases are being handled correctly.
+   */
+  @Test(groups = "Functional")
+  public void testAdd_pseudoRandom()
+  {
+    Random r = new Random(108); // well why not?
+    int[] scales = new int[] { 10, 100, 1000 };
+
+    for (int scale : scales)
+    {
+      NCList<Range> ncl = new NCList<Range>();
+      int size = 0;
+      for (int i = 0; i < 100; i++)
+      {
+        int r1 = r.nextInt(scale);
+        int r2 = r.nextInt(scale);
+        int nextFrom = Math.min(r1, r2);
+        int nextTo = Math.max(r1, r2);
+        Range range = new Range(nextFrom, nextTo);
+        ncl.add(range);
+        assertTrue(ncl.isValid(),
+                String.format("Failed for scale = %d, i=%d", scale, i));
+        size++;
+        assertEquals(ncl.getSize(), size);
+      }
+      System.out.println(ncl.prettyPrint());
+    }
   }
 }