JAL-2808 revised util.matcher package, 2 filter conditions per feature
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 31 Oct 2017 15:59:04 +0000 (15:59 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 31 Oct 2017 15:59:04 +0000 (15:59 +0000)
type

13 files changed:
resources/lang/Messages.properties
src/jalview/api/FeatureColourI.java
src/jalview/gui/FeatureColourChooser.java
src/jalview/schemes/FeatureColour.java
src/jalview/util/matcher/KeyedMatcher.java
src/jalview/util/matcher/KeyedMatcherI.java
src/jalview/util/matcher/KeyedMatcherSet.java [new file with mode: 0644]
src/jalview/util/matcher/KeyedMatcherSetI.java [new file with mode: 0644]
src/jalview/util/matcher/Matcher.java
test/jalview/util/matcher/ConditionTest.java [new file with mode: 0644]
test/jalview/util/matcher/KeyedMatcherSetTest.java [new file with mode: 0644]
test/jalview/util/matcher/KeyedMatcherTest.java
test/jalview/util/matcher/MatcherTest.java

index daca83b..c950bbc 100644 (file)
@@ -1328,9 +1328,9 @@ label.matchCondition_contains = Contains
 label.matchCondition_notcontains = Does not contain
 label.matchCondition_matches = Matches
 label.matchCondition_notmatches = Does not match
-label.matchCondition_eq = Is equal to
-label.matchCondition_ne = Is not equal to
-label.matchCondition_lt = Is less than
-label.matchCondition_le = Is less than or equal to
-label.matchCondition_gt = Is greater than
-label.matchCondition_ge = Is greater than or equal to
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
index 9644831..3b2313d 100644 (file)
@@ -21,7 +21,7 @@
 package jalview.api;
 
 import jalview.datamodel.SequenceFeature;
-import jalview.util.matcher.KeyedMatcherI;
+import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.Color;
 
@@ -177,7 +177,7 @@ public interface FeatureColourI
    * 
    * @param filter
    */
-  public void setAttributeFilters(KeyedMatcherI filter);
+  public void setAttributeFilters(KeyedMatcherSetI filter);
 
   /**
    * Answers the attribute value filters for the colour scheme, or null if no
@@ -185,5 +185,5 @@ public interface FeatureColourI
    * 
    * @return
    */
-  public KeyedMatcherI getAttributeFilters();
+  public KeyedMatcherSetI getAttributeFilters();
 }
index 937b48e..fbe8437 100644 (file)
@@ -28,8 +28,8 @@ import jalview.util.MessageManager;
 import jalview.util.matcher.Condition;
 import jalview.util.matcher.KeyedMatcher;
 import jalview.util.matcher.KeyedMatcherI;
-import jalview.util.matcher.Matcher;
-import jalview.util.matcher.MatcherI;
+import jalview.util.matcher.KeyedMatcherSet;
+import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -40,6 +40,8 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.util.Iterator;
@@ -244,28 +246,34 @@ public class FeatureColourChooser extends JalviewDialog
   /**
    * Populates the attribute filter fields for the initial display
    * 
-   * @param attributeFilters
+   * @param filters
    */
-  void setInitialFilters(KeyedMatcherI attributeFilters)
+  void setInitialFilters(KeyedMatcherSetI filters)
   {
     // todo generalise to populate N conditions
-    
-    if (attributeFilters != null)
-    {
-      filterAttribute.setSelectedItem(attributeFilters.getKey());
-      filterCondition.setSelectedItem(attributeFilters.getMatcher()
-              .getCondition());
-      filterValue.setText(attributeFilters.getMatcher().getPattern());
-      
-      KeyedMatcherI second = attributeFilters.getSecondMatcher();
-      if (second != null)
-      {
-        // todo add OR/AND condition to gui
-        filterAttribute2.setSelectedItem(second.getKey());
-        filterCondition2
-                .setSelectedItem(second.getMatcher().getCondition());
-        filterValue2.setText(second.getMatcher().getPattern());
-      }
+
+    if (filters == null)
+    {
+      return;
+    }
+
+    Iterator<KeyedMatcherI> theFilters = filters.getMatchers();
+    if (theFilters.hasNext())
+    {
+      KeyedMatcherI filter = theFilters.next();
+      filterAttribute.setSelectedItem(filter.getKey());
+      filterCondition.setSelectedItem(filter.getMatcher().getCondition());
+      filterValue.setText(filter.getMatcher().getPattern());
+    }
+    if (theFilters.hasNext())
+    {
+      KeyedMatcherI filter = theFilters.next();
+      boolean anded = filters.isAnded();
+      // todo add OR/AND condition to gui
+      // - user choice for the second condition, fixed thereafter
+      filterAttribute2.setSelectedItem(filter.getKey());
+      filterCondition2.setSelectedItem(filter.getMatcher().getCondition());
+      filterValue2.setText(filter.getMatcher().getPattern());
     }
   }
 
@@ -684,24 +692,25 @@ public class FeatureColourChooser extends JalviewDialog
     String attribute = (String) filterAttribute.getSelectedItem();
     Condition cond = (Condition) filterCondition.getSelectedItem();
     String pattern = filterValue.getText().trim();
-    if (pattern.length() > 1)
+    if (pattern.length() > 0)
     {
-      MatcherI filter = new Matcher(cond, pattern);
-      KeyedMatcherI km = new KeyedMatcher(attribute, filter);
+      KeyedMatcherSetI filters = new KeyedMatcherSet();
+      KeyedMatcherI km = new KeyedMatcher(attribute, cond, pattern);
+      filters.and(km);
 
       /*
        * is there a second condition?
-       * todo: generalise to N conditions
+       * todo: allow N conditions with choice of AND or OR (but not both!)
        */
       pattern = filterValue2.getText().trim();
       if (pattern.length() > 1)
       {
         attribute = (String) filterAttribute2.getSelectedItem();
         cond = (Condition) filterCondition2.getSelectedItem();
-        filter = new Matcher(cond, pattern);
-        km = km.and(attribute, filter);
+        KeyedMatcherI km2 = new KeyedMatcher(attribute, cond, pattern);
+        filters.and(km2);
       }
-      acg.setAttributeFilters(km);
+      acg.setAttributeFilters(filters);
     }
   }
 
@@ -830,10 +839,10 @@ public class FeatureColourChooser extends JalviewDialog
       filterAttribute.addItem(attName);
       filterAttribute2.addItem(attName);
     }
-    filterAttribute.addActionListener(new ActionListener()
+    filterAttribute.addItemListener(new ItemListener()
     {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void itemStateChanged(ItemEvent e)
       {
         changeColour(true);
       }
@@ -847,10 +856,10 @@ public class FeatureColourChooser extends JalviewDialog
     {
       filterCondition.addItem(cond);
     }
-    filterCondition.addActionListener(new ActionListener()
+    filterCondition.addItemListener(new ItemListener()
     {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void itemStateChanged(ItemEvent e)
       {
         changeColour(true);
       }
index 480522a..2dac7db 100644 (file)
@@ -24,7 +24,7 @@ import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.util.ColorUtils;
 import jalview.util.Format;
-import jalview.util.matcher.KeyedMatcherI;
+import jalview.util.matcher.KeyedMatcherSetI;
 
 import java.awt.Color;
 import java.util.StringTokenizer;
@@ -76,9 +76,9 @@ public class FeatureColour implements FeatureColourI
   final private float deltaBlue;
 
   /*
-   * optional filter by attribute values
+   * optional filter(s) by attribute values
    */
-  private KeyedMatcherI attributeFilters;
+  private KeyedMatcherSetI attributeFilters;
 
   /**
    * Parses a Jalview features file format colour descriptor
@@ -602,8 +602,9 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Answers true if there are any attribute value filters defined, and the
-   * feature matches all of the filter conditions
+   * Answers true if either there are no attribute value filters defined, or the
+   * feature matches all of the filter conditions. Answers false if the feature
+   * fails the filter conditions.
    * 
    * @param feature
    * 
@@ -611,11 +612,15 @@ public class FeatureColour implements FeatureColourI
    */
   boolean matchesFilters(SequenceFeature feature)
   {
+    if (attributeFilters == null)
+    {
+      return true;
+    }
+
     Function<String, String> valueProvider = key -> feature.otherDetails == null ? null
                     : (feature.otherDetails.containsKey(key) ? feature.otherDetails
                             .get(key).toString() : null);
-    return attributeFilters == null ? true : attributeFilters
-            .matches(valueProvider);
+    return attributeFilters.matches(valueProvider);
   }
 
   /**
@@ -711,13 +716,13 @@ public class FeatureColour implements FeatureColourI
    * @param filter
    */
   @Override
-  public void setAttributeFilters(KeyedMatcherI matcher)
+  public void setAttributeFilters(KeyedMatcherSetI matcher)
   {
     attributeFilters = matcher;
   }
 
   @Override
-  public KeyedMatcherI getAttributeFilters()
+  public KeyedMatcherSetI getAttributeFilters()
   {
     return attributeFilters;
   }
index 5655118..474dc31 100644 (file)
@@ -2,93 +2,60 @@ package jalview.util.matcher;
 
 import java.util.function.Function;
 
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
 public class KeyedMatcher implements KeyedMatcherI
 {
-  private String key;
+  final private String key;
 
-  private MatcherI matcher;
+  final private MatcherI matcher;
 
-  /*
-   * an optional second condition
-   */
-  KeyedMatcherI combineWith;
-
-  /*
-   * if true, any second condition is AND-ed with this one
-   * if false,any second condition is OR-ed with this one
+  /**
+   * Constructor given a key, a test condition and a match pattern
+   * 
+   * @param theKey
+   * @param cond
+   * @param pattern
    */
-  boolean combineAnd;
+  public KeyedMatcher(String theKey, Condition cond, String pattern)
+  {
+    key = theKey;
+    matcher = new Matcher(cond, pattern);
+  }
 
   /**
-   * Constructor given a match condition
+   * Constructor given a key, a test condition and a numerical value to compare
+   * to. Note that if a non-numerical condition is specified, the float will be
+   * converted to a string.
    * 
-   * @param m
+   * @param theKey
+   * @param cond
+   * @param value
    */
-  public KeyedMatcher(String theKey, MatcherI m)
+  public KeyedMatcher(String theKey, Condition cond, float value)
   {
     key = theKey;
-    matcher = m;
+    matcher = new Matcher(cond, value);
   }
 
   @Override
   public boolean matches(Function<String, String> valueProvider)
   {
     String value = valueProvider.apply(key);
-    boolean matched = matcher.matches(value);
-
-    /*
-     * apply a second condition if there is one, using 
-     * lazy evalution of AND and OR combinations
-     */
-    if (combineWith != null)
-    {
-      if (combineAnd && matched)
-      {
-        matched = combineWith.matches(valueProvider);
-      }
-      if (!combineAnd && !matched)
-      {
-        matched = combineWith.matches(valueProvider);
-      }
-    }
-
-    return matched;
-  }
-
-  @Override
-  public KeyedMatcherI and(String key2, MatcherI m)
-  {
-    return combineWith(key2, m, true);
-  }
-
-  @Override
-  public KeyedMatcherI or(String key2, MatcherI m)
-  {
-    return combineWith(key2, m, false);
-  }
-
-  /**
-   * Answers a Matcher that is the logical combination of this one with the
-   * given argument. The two matchers are AND-ed if and is true, else OR-ed.
-   * 
-   * @param key2
-   * @param condition2
-   * @param and
-   * @return
-   */
-  KeyedMatcher combineWith(String key2, MatcherI condition2,
-          boolean and)
-  {
-    if (condition2 == null)
-    {
-      return this;
-    }
-
-    KeyedMatcher combined = new KeyedMatcher(key2, condition2);
-    combined.combineWith = this;
-    combined.combineAnd = and;
-
-    return combined;
+    return matcher.matches(value);
   }
 
   @Override
@@ -103,18 +70,6 @@ public class KeyedMatcher implements KeyedMatcherI
     return matcher;
   }
 
-  @Override
-  public KeyedMatcherI getSecondMatcher()
-  {
-    return combineWith;
-  }
-
-  @Override
-  public boolean isAnded()
-  {
-    return combineAnd;
-  }
-
   /**
    * Answers a string description of this matcher, suitable for debugging or
    * logging. The format may change in future.
@@ -126,11 +81,6 @@ public class KeyedMatcher implements KeyedMatcherI
     sb.append(key).append(" ").append(matcher.getCondition().name())
             .append(" ").append(matcher.getPattern());
 
-    if (combineWith != null)
-    {
-      sb.append(" ").append(combineAnd ? "AND (" : "OR (")
-              .append(combineWith.toString()).append(")");
-    }
     return sb.toString();
   }
 }
index a746cd9..e9fe014 100644 (file)
@@ -2,31 +2,35 @@ package jalview.util.matcher;
 
 import java.util.function.Function;
 
+/**
+ * An interface for an object that can apply one or more match conditions, given
+ * a key-value provider. The match conditions are stored against key values, and
+ * applied to the value obtained by a key-value lookup.
+ * 
+ * @author gmcarstairs
+ */
 public interface KeyedMatcherI
 {
-  boolean matches(Function<String, String> valueProvider);
-
   /**
-   * Answers a new object that matches the logical AND of this and m
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
    * 
-   * @param m
+   * @param valueProvider
    * @return
    */
-  KeyedMatcherI and(String key, MatcherI m);
+  boolean matches(Function<String, String> valueProvider);
 
   /**
-   * Answers a new object that matches the logical OR of this and m
+   * Answers the value key this matcher operates on
    * 
-   * @param m
    * @return
    */
-  KeyedMatcherI or(String key, MatcherI m);
-
   String getKey();
 
+  /**
+   * Answers the match condition that is applied
+   * 
+   * @return
+   */
   MatcherI getMatcher();
-
-  KeyedMatcherI getSecondMatcher();
-
-  boolean isAnded();
 }
diff --git a/src/jalview/util/matcher/KeyedMatcherSet.java b/src/jalview/util/matcher/KeyedMatcherSet.java
new file mode 100644 (file)
index 0000000..3c21d50
--- /dev/null
@@ -0,0 +1,116 @@
+package jalview.util.matcher;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+public class KeyedMatcherSet implements KeyedMatcherSetI
+{
+  List<KeyedMatcherI> matchConditions;
+
+  boolean andConditions;
+
+  /**
+   * Constructor
+   */
+  public KeyedMatcherSet()
+  {
+    matchConditions = new ArrayList<>();
+  }
+
+  @Override
+  public boolean matches(Function<String, String> valueProvider)
+  {
+    /*
+     * no conditions matches anything
+     */
+    if (matchConditions.isEmpty())
+    {
+      return true;
+    }
+
+    /*
+     * AND until failure
+     */
+    if (andConditions)
+    {
+      for (KeyedMatcherI m : matchConditions)
+      {
+        if (!m.matches(valueProvider))
+        {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /*
+     * OR until match
+     */
+    for (KeyedMatcherI m : matchConditions)
+    {
+      if (m.matches(valueProvider))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public KeyedMatcherSetI and(KeyedMatcherI m)
+  {
+    if (!andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an AND to OR conditions");
+    }
+    matchConditions.add(m);
+    andConditions = true;
+
+    return this;
+  }
+
+  @Override
+  public KeyedMatcherSetI or(KeyedMatcherI m)
+  {
+    if (andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an OR to AND conditions");
+    }
+    matchConditions.add(m);
+    andConditions = false;
+
+    return this;
+  }
+
+  @Override
+  public boolean isAnded()
+  {
+    return andConditions;
+  }
+
+  @Override
+  public Iterator<KeyedMatcherI> getMatchers()
+  {
+    return matchConditions.iterator();
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (KeyedMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        sb.append(andConditions ? " AND " : " OR ");
+      }
+      first = false;
+      sb.append("(").append(matcher.toString()).append(")");
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/util/matcher/KeyedMatcherSetI.java b/src/jalview/util/matcher/KeyedMatcherSetI.java
new file mode 100644 (file)
index 0000000..09532a4
--- /dev/null
@@ -0,0 +1,58 @@
+package jalview.util.matcher;
+
+import java.util.Iterator;
+import java.util.function.Function;
+
+/**
+ * An interface to describe a set of one or more key-value match conditions,
+ * where all conditions are combined with either AND or OR
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface KeyedMatcherSetI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param valueProvider
+   * @return
+   */
+  boolean matches(Function<String, String> valueProvider);
+
+  /**
+   * Answers a new object that matches the logical AND of this and m
+   * 
+   * @param m
+   * @return
+   * @throws IllegalStateException
+   *           if an attempt is made to AND to existing OR-ed conditions
+   */
+  KeyedMatcherSetI and(KeyedMatcherI m);
+
+  /**
+   * Answers true if any second condition is AND-ed with this one, false if it
+   * is OR-ed
+   * 
+   * @return
+   */
+  boolean isAnded();
+
+  /**
+   * Answers a new object that matches the logical OR of this and m
+   * 
+   * @param m
+   * @return
+   * @throws IllegalStateException
+   *           if an attempt is made to OR to existing AND-ed conditions
+   */
+  KeyedMatcherSetI or(KeyedMatcherI m);
+
+  /**
+   * Answers an iterator over the combined match conditions
+   * 
+   * @return
+   */
+  Iterator<KeyedMatcherI> getMatchers();
+}
index b162d5d..638933d 100644 (file)
@@ -1,5 +1,6 @@
 package jalview.util.matcher;
 
+import java.util.Objects;
 import java.util.regex.Pattern;
 
 /**
@@ -47,9 +48,14 @@ public class Matcher implements MatcherI
     if (cond.isNumeric())
     {
       value = Float.valueOf(compareTo);
+      pattern = String.valueOf(value);
     }
-    // pattern matches will be non-case-sensitive
-    pattern = compareTo.toUpperCase();
+    else
+    {
+      // pattern matches will be non-case-sensitive
+      pattern = compareTo.toUpperCase();
+    }
+
     // if we add regex conditions (e.g. matchesPattern), then
     // pattern should hold the raw regex, and
     // regexPattern = Pattern.compile(compareTo);
@@ -65,9 +71,10 @@ public class Matcher implements MatcherI
    */
   public Matcher(Condition cond, float compareTo)
   {
+    Objects.requireNonNull(cond);
     condition = cond;
     value = compareTo;
-    pattern = String.valueOf(compareTo);
+    pattern = String.valueOf(compareTo).toUpperCase();
   }
 
   /**
@@ -181,8 +188,8 @@ public class Matcher implements MatcherI
       return false;
     }
     Matcher m = (Matcher) obj;
-    return condition != m.condition || value != m.value
-            || !pattern.equals(m.pattern);
+    return condition == m.condition && value == m.value
+            && pattern.equals(m.pattern);
   }
 
   @Override
@@ -202,4 +209,10 @@ public class Matcher implements MatcherI
   {
     return value;
   }
+
+  @Override
+  public String toString()
+  {
+    return condition.name() + " " + pattern;
+  }
 }
diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java
new file mode 100644 (file)
index 0000000..11a0630
--- /dev/null
@@ -0,0 +1,31 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class ConditionTest
+{
+  @Test
+  public void testToString()
+  {
+    Locale.setDefault(Locale.UK);
+    assertEquals(Condition.Contains.toString(), "Contains");
+    assertEquals(Condition.NotContains.toString(), "Does not contain");
+    assertEquals(Condition.Matches.toString(), "Matches");
+    assertEquals(Condition.NotMatches.toString(), "Does not match");
+    assertEquals(Condition.LT.toString(), "Is less than");
+    assertEquals(Condition.LE.toString(), "Is less than or equal to");
+    assertEquals(Condition.GT.toString(), "Is greater than");
+    assertEquals(Condition.GE.toString(), "Is greater than or equal to");
+    assertEquals(Condition.EQ.toString(), "Is equal to");
+    assertEquals(Condition.NE.toString(), "Is not equal to");
+
+    /*
+     * repeat call to get coverage of cached value
+     */
+    assertEquals(Condition.NE.toString(), "Is not equal to");
+  }
+}
diff --git a/test/jalview/util/matcher/KeyedMatcherSetTest.java b/test/jalview/util/matcher/KeyedMatcherSetTest.java
new file mode 100644 (file)
index 0000000..76ae8a5
--- /dev/null
@@ -0,0 +1,117 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.function.Function;
+
+import org.testng.annotations.Test;
+
+public class KeyedMatcherSetTest
+{
+  @Test
+  public void testMatches()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertTrue(km.matches(key -> "-2"));
+    assertTrue(km.matches(key -> "-1"));
+    assertFalse(km.matches(key -> "-3"));
+    assertFalse(km.matches(key -> ""));
+    assertFalse(km.matches(key -> "junk"));
+    assertFalse(km.matches(key -> null));
+
+    /*
+     * a string pattern matcher
+     */
+    km = new KeyedMatcher("AF", Condition.Contains, "Cat");
+    assertTrue(km.matches(key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers"));
+  }
+
+  @Test
+  public void testAnd()
+  {
+    // condition1: AF value contains "dog" (matches)
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains,
+            "how");
+
+    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers";
+    assertTrue(km1.matches(vp));
+    assertFalse(km2.matches(vp));
+
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    assertTrue(kms.matches(vp)); // if no conditions, then 'all' pass
+    kms.and(km1);
+    assertTrue(kms.matches(vp));
+    kms.and(km2);
+    assertFalse(kms.matches(vp));
+  }
+
+  @Test
+  public void testToString()
+  {
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.LT, 1.2f);
+    assertEquals(km1.toString(), "AF LT 1.2");
+
+    KeyedMatcher km2 = new KeyedMatcher("CLIN_SIG", Condition.NotContains, "path");
+    assertEquals(km2.toString(), "CLIN_SIG NotContains PATH");
+
+    /*
+     * AND them
+     */
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    assertEquals(kms.toString(), "");
+    kms.and(km1);
+    assertEquals(kms.toString(), "(AF LT 1.2)");
+    kms.and(km2);
+    assertEquals(kms.toString(),
+            "(AF LT 1.2) AND (CLIN_SIG NotContains PATH)");
+
+    /*
+     * OR them
+     */
+    kms = new KeyedMatcherSet();
+    assertEquals(kms.toString(), "");
+    kms.or(km1);
+    assertEquals(kms.toString(), "(AF LT 1.2)");
+    kms.or(km2);
+    assertEquals(kms.toString(),
+            "(AF LT 1.2) OR (CLIN_SIG NotContains PATH)");
+  }
+
+  /**
+   * @return
+   */
+  protected KeyedMatcher km3()
+  {
+    return new KeyedMatcher("CSQ", Condition.Contains, "benign");
+  }
+
+  @Test
+  public void testOr()
+  {
+    // condition1: AF value contains "dog" (matches)
+    KeyedMatcherI km1 = new KeyedMatcher("AF", Condition.Contains, "dog");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    KeyedMatcherI km2 = new KeyedMatcher("CSQ", Condition.NotContains,
+            "how");
+
+    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+            : "showers";
+    assertTrue(km1.matches(vp));
+    assertFalse(km2.matches(vp));
+
+    KeyedMatcherSetI kms = new KeyedMatcherSet();
+    kms.or(km2);
+    assertFalse(kms.matches(vp));
+    kms.or(km1);
+    assertTrue(kms.matches(vp));
+  }
+}
index ccf2ba2..ebc09c1 100644 (file)
@@ -4,8 +4,6 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
-import java.util.function.Function;
-
 import org.testng.annotations.Test;
 
 public class KeyedMatcherTest
@@ -16,8 +14,7 @@ public class KeyedMatcherTest
     /*
      * a numeric matcher - MatcherTest covers more conditions
      */
-    MatcherI m1 = new Matcher(Condition.GE, -2f);
-    KeyedMatcherI km = new KeyedMatcher("AF", m1);
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
     assertTrue(km.matches(key -> "-2"));
     assertTrue(km.matches(key -> "-1"));
     assertFalse(km.matches(key -> "-3"));
@@ -28,65 +25,31 @@ public class KeyedMatcherTest
     /*
      * a string pattern matcher
      */
-    MatcherI m2 = new Matcher(Condition.Contains, "Cat");
-    km = new KeyedMatcher("AF", m2);
+    km = new KeyedMatcher("AF", Condition.Contains, "Cat");
     assertTrue(km.matches(key -> "AF".equals(key) ? "raining cats and dogs"
             : "showers"));
   }
 
   @Test
-  public void testAnd()
+  public void testToString()
   {
-    // condition1: AF value contains "dog" (matches)
-    KeyedMatcherI km1 = new KeyedMatcher("AF", new Matcher(
-            Condition.Contains, "dog"));
-
-    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
-            : "showers";
-    assertTrue(km1.matches(vp));
-
-    // condition 2: CSQ value does not contain "how" (does not match)
-    KeyedMatcherI km2 = km1.and("CSQ", new Matcher(Condition.NotContains,
-            "how"));
-    assertFalse(km2.matches(vp));
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.LT, 1.2f);
+    assertEquals(km.toString(), "AF LT 1.2");
   }
 
   @Test
-  public void testToString()
+  public void testGetKey()
   {
-    KeyedMatcherI km = new KeyedMatcher("AF",
-            new Matcher(Condition.LT, 1.2f));
-    assertEquals(km.toString(), "AF LT 1.2");
-
-    /*
-     * add an AND condition
-     */
-    km = km.and("CLIN_SIG", new Matcher(Condition.NotContains, "path"));
-    assertEquals(km.toString(), "CLIN_SIG NotContains PATH AND (AF LT 1.2)");
-
-    /*
-     * add an OR condition
-     */
-    km = km.or("CSQ", new Matcher(Condition.Contains, "benign"));
-    assertEquals(km.toString(),
-            "CSQ Contains BENIGN OR (CLIN_SIG NotContains PATH AND (AF LT 1.2))");
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertEquals(km.getKey(), "AF");
   }
 
   @Test
-  public void testOr()
+  public void testGetMatcher()
   {
-    // condition1: AF value contains "dog" (matches)
-    KeyedMatcherI km1 = new KeyedMatcher("AF", new Matcher(
-            Condition.Contains, "dog"));
-  
-    Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
-            : "showers";
-    assertTrue(km1.matches(vp));
-  
-    // condition 2: CSQ value does not contain "how" (does not match)
-    // the OR combination still passes
-    KeyedMatcherI km2 = km1.or("CSQ", new Matcher(Condition.NotContains,
-            "how"));
-    assertTrue(km2.matches(vp));
+    KeyedMatcherI km = new KeyedMatcher("AF", Condition.GE, -2F);
+    assertEquals(km.getMatcher().getCondition(), Condition.GE);
+    assertEquals(km.getMatcher().getFloatValue(), -2F);
+    assertEquals(km.getMatcher().getPattern(), "-2.0");
   }
 }
index ebfb5d2..d988c3a 100644 (file)
@@ -1,12 +1,57 @@
 package jalview.util.matcher;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 import org.testng.annotations.Test;
 
 public class MatcherTest
 {
+  @Test
+  public void testConstructor()
+  {
+    MatcherI m = new Matcher(Condition.Contains, "foo");
+    assertEquals(m.getCondition(), Condition.Contains);
+    assertEquals(m.getPattern(), "FOO"); // all comparisons upper-cased
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GT, -2.1f);
+    assertEquals(m.getCondition(), Condition.GT);
+    assertEquals(m.getPattern(), "-2.1");
+    assertEquals(m.getFloatValue(), -2.1f);
+
+    m = new Matcher(Condition.NotContains, "-1.2f");
+    assertEquals(m.getCondition(), Condition.NotContains);
+    assertEquals(m.getPattern(), "-1.2F");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GE, "-1.2f");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "-1.2");
+    assertEquals(m.getFloatValue(), -1.2f);
+
+    try
+    {
+      new Matcher(null, 0f);
+      fail("Expected exception");
+    } catch (NullPointerException e)
+    {
+      // expected
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123,456");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected
+    }
+  }
+
   /**
    * Tests for float comparison conditions
    */
@@ -120,6 +165,13 @@ public class MatcherTest
     assertTrue(m.matches("MOSTLY BENIGN"));
     assertTrue(m.matches("pathogenic"));
     assertTrue(m.matches(null));
+
+    /*
+     * a float with a string match condition will be treated as string
+     */
+    Matcher m1 = new Matcher(Condition.Contains, "32");
+    assertFalse(m1.matches(-203f));
+    assertTrue(m1.matches(-4321.0f));
   }
 
   /**
@@ -136,4 +188,63 @@ public class MatcherTest
     assertTrue(m.matches("1.0E-7"));
     assertFalse(m.matches("0.0000001f"));
   }
+
+  @Test
+  public void testToString()
+  {
+    MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
+    assertEquals(m.toString(), "LT 1.2E-6");
+
+    m = new Matcher(Condition.NotMatches, "ABC");
+    assertEquals(m.toString(), "NotMatches ABC");
+
+    m = new Matcher(Condition.Contains, -1.2f);
+    assertEquals(m.toString(), "Contains -1.2");
+  }
+
+  @Test
+  public void testEquals()
+  {
+    /*
+     * string condition
+     */
+    MatcherI m = new Matcher(Condition.NotMatches, "ABC");
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC")));
+    // not case-sensitive:
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc")));
+    assertFalse(m.equals(new Matcher(Condition.Matches, "ABC")));
+    assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
+
+    /*
+     * numeric conditions
+     */
+    m = new Matcher(Condition.LT, -1f);
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.LT, -1f)));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1f")));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f")));
+    assertFalse(m.equals(new Matcher(Condition.LE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.GE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+  }
+
+  @Test
+  public void testHashCode()
+  {
+    MatcherI m1 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m2 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m3 = new Matcher(Condition.NotMatches, "AB");
+    MatcherI m4 = new Matcher(Condition.Matches, "ABC");
+    assertEquals(m1.hashCode(), m2.hashCode());
+    assertNotEquals(m1.hashCode(), m3.hashCode());
+    assertNotEquals(m1.hashCode(), m4.hashCode());
+    assertNotEquals(m3.hashCode(), m4.hashCode());
+  }
 }