+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ *
+ * This file is part of Jalview.
+ *
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *
+ * Jalview is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
package jalview.util.matcher;
+import java.util.Locale;
+
import java.util.Objects;
-import java.util.regex.Pattern;
/**
* A bean to describe one attribute-based filter
*/
public class Matcher implements MatcherI
{
+ public enum PatternType
+ {
+ String, Integer, Float
+ }
+
/*
* the comparison condition
*/
- Condition condition;
+ private final Condition condition;
+
+ /*
+ * the string pattern as entered, to compare to
+ */
+ private String pattern;
/*
- * the string value (upper-cased), or the regex, to compare to
- * also holds the string form of float value if a numeric condition
+ * the pattern in upper case, for non-case-sensitive matching
*/
- String pattern;
+ private final String uppercasePattern;
/*
* the compiled regex if using a pattern match condition
- * (reserved for possible future enhancement)
+ * (possible future enhancement)
*/
- Pattern regexPattern;
+ // private Pattern regexPattern;
/*
- * the value to compare to for a numerical condition
+ * the value to compare to for a numerical condition with a float pattern
*/
- float value;
+ private float floatValue = 0F;
+
+ /*
+ * the value to compare to for a numerical condition with an integer pattern
+ */
+ private long longValue = 0L;
+
+ private PatternType patternType;
/**
* Constructor
* @param compareTo
* @return
* @throws NumberFormatException
- * if a numerical condition is specified with a non-numeric comparison
- * value
+ * if a numerical condition is specified with a non-numeric
+ * comparison value
* @throws NullPointerException
* if a null condition or comparison string is specified
*/
public Matcher(Condition cond, String compareTo)
{
+ Objects.requireNonNull(cond);
condition = cond;
+
if (cond.isNumeric())
{
- value = Float.valueOf(compareTo);
- pattern = String.valueOf(value);
+ try
+ {
+ longValue = Long.valueOf(compareTo);
+ pattern = String.valueOf(longValue);
+ patternType = PatternType.Integer;
+ } catch (NumberFormatException e)
+ {
+ floatValue = Float.valueOf(compareTo);
+ pattern = String.valueOf(floatValue);
+ patternType = PatternType.Float;
+ }
}
else
{
- // pattern matches will be non-case-sensitive
- pattern = compareTo == null ? null : compareTo.toUpperCase();
+ pattern = compareTo;
+ patternType = PatternType.String;
}
+ uppercasePattern = pattern == null ? null
+ : pattern.toUpperCase(Locale.ROOT);
+
// if we add regex conditions (e.g. matchesPattern), then
// pattern should hold the raw regex, and
// regexPattern = Pattern.compile(compareTo);
}
/**
- * Constructor for a numerical match condition. Note that if a string
- * comparison condition is specified, this will be converted to a comparison
- * with the float value as string
+ * Constructor for a float-valued numerical match condition. Note that if a
+ * string comparison condition is specified, this will be converted to a
+ * comparison with the float value as string
*
* @param cond
* @param compareTo
*/
public Matcher(Condition cond, float compareTo)
{
- Objects.requireNonNull(cond);
- condition = cond;
- value = compareTo;
- pattern = String.valueOf(compareTo).toUpperCase();
+ this(cond, String.valueOf(compareTo));
+ }
+
+ /**
+ * Constructor for an integer-valued numerical match condition. Note that if a
+ * string comparison condition is specified, this will be converted to a
+ * comparison with the integer value as string
+ *
+ * @param cond
+ * @param compareTo
+ */
+ public Matcher(Condition cond, long compareTo)
+ {
+ this(cond, String.valueOf(compareTo));
}
/**
* {@inheritDoc}
*/
- @SuppressWarnings("incomplete-switch")
@Override
- public boolean matches(String val)
+ public boolean matches(String compareTo)
{
- if (condition.isNumeric())
+ if (compareTo == null)
{
- try
- {
- /*
- * treat a null value (no such attribute) as
- * failing any numerical filter condition
- */
- return val == null ? false : matches(Float.valueOf(val));
- } catch (NumberFormatException e)
- {
- return false;
- }
+ return matchesNull();
}
-
- /*
- * a null value matches a negative condition, fails a positive test
- */
- if (val == null)
+
+ boolean matched = false;
+ switch (patternType)
{
- return condition == Condition.NotContains
- || condition == Condition.NotMatches
- || condition == Condition.NotPresent;
+ case Float:
+ matched = matchesFloat(compareTo, floatValue);
+ break;
+ case Integer:
+ matched = matchesLong(compareTo);
+ break;
+ default:
+ matched = matchesString(compareTo);
+ break;
}
-
- String upper = val.toUpperCase().trim();
+ return matched;
+ }
+
+ /**
+ * Executes a non-case-sensitive string comparison to the given value, after
+ * trimming it. Returns true if the test passes, false if it fails.
+ *
+ * @param compareTo
+ * @return
+ */
+ boolean matchesString(String compareTo)
+ {
boolean matched = false;
- switch(condition) {
+ String upper = compareTo.toUpperCase(Locale.ROOT).trim();
+ switch (condition)
+ {
case Matches:
- matched = upper.equals(pattern);
+ matched = upper.equals(uppercasePattern);
break;
case NotMatches:
- matched = !upper.equals(pattern);
+ matched = !upper.equals(uppercasePattern);
break;
case Contains:
- matched = upper.indexOf(pattern) > -1;
+ matched = upper.indexOf(uppercasePattern) > -1;
break;
case NotContains:
- matched = upper.indexOf(pattern) == -1;
+ matched = upper.indexOf(uppercasePattern) == -1;
break;
case Present:
matched = true;
}
/**
- * Applies a numerical comparison match condition
+ * Performs a numerical comparison match condition test against a float value
*
- * @param f
+ * @param testee
+ * @param compareTo
* @return
*/
- @SuppressWarnings("incomplete-switch")
- boolean matches(float f)
+ boolean matchesFloat(String testee, float compareTo)
{
if (!condition.isNumeric())
{
- return matches(String.valueOf(f));
+ // failsafe, shouldn't happen
+ return matches(testee);
+ }
+
+ float f = 0f;
+ try
+ {
+ f = Float.valueOf(testee);
+ } catch (NumberFormatException e)
+ {
+ return false;
}
-
+
boolean matched = false;
- switch (condition) {
+ switch (condition)
+ {
case LT:
- matched = f < value;
+ matched = f < compareTo;
break;
case LE:
- matched = f <= value;
+ matched = f <= compareTo;
break;
case EQ:
- matched = f == value;
+ matched = f == compareTo;
break;
case NE:
- matched = f != value;
+ matched = f != compareTo;
break;
case GT:
- matched = f > value;
+ matched = f > compareTo;
break;
case GE:
- matched = f >= value;
+ matched = f >= compareTo;
break;
default:
break;
@Override
public int hashCode()
{
- return pattern.hashCode() + condition.hashCode() + (int) value;
+ return pattern.hashCode() + condition.hashCode() + (int) floatValue;
}
/**
* equals is overridden so that we can safely remove Matcher objects from
- * collections (e.g. delete an attribut match condition for a feature colour)
+ * collections (e.g. delete an attribute match condition for a feature colour)
*/
@Override
public boolean equals(Object obj)
return false;
}
Matcher m = (Matcher) obj;
- return condition == m.condition && value == m.value
- && pattern.equals(m.pattern);
+ if (condition != m.condition || floatValue != m.floatValue
+ || longValue != m.longValue)
+ {
+ return false;
+ }
+ if (pattern == null)
+ {
+ return m.pattern == null;
+ }
+ return uppercasePattern.equals(m.uppercasePattern);
}
@Override
}
@Override
- public float getFloatValue()
- {
- return value;
- }
-
- @Override
public String toString()
{
StringBuilder sb = new StringBuilder();
- sb.append(condition.name()).append(" ");
+ sb.append(condition.toString()).append(" ");
if (condition.isNumeric())
{
sb.append(pattern);
return sb.toString();
}
+
+ /**
+ * Performs a numerical comparison match condition test against an integer
+ * value
+ *
+ * @param compareTo
+ * @return
+ */
+ boolean matchesLong(String compareTo)
+ {
+ if (!condition.isNumeric())
+ {
+ // failsafe, shouldn't happen
+ return matches(String.valueOf(compareTo));
+ }
+
+ long val = 0L;
+ try
+ {
+ val = Long.valueOf(compareTo);
+ } catch (NumberFormatException e)
+ {
+ /*
+ * try the presented value as a float instead
+ */
+ return matchesFloat(compareTo, longValue);
+ }
+
+ boolean matched = false;
+ switch (condition)
+ {
+ case LT:
+ matched = val < longValue;
+ break;
+ case LE:
+ matched = val <= longValue;
+ break;
+ case EQ:
+ matched = val == longValue;
+ break;
+ case NE:
+ matched = val != longValue;
+ break;
+ case GT:
+ matched = val > longValue;
+ break;
+ case GE:
+ matched = val >= longValue;
+ break;
+ default:
+ break;
+ }
+
+ return matched;
+ }
+
+ /**
+ * Tests whether a null value matches the condition. The rule is that any
+ * numeric condition is failed, and only 'negative' string conditions are
+ * matched. So for example <br>
+ * {@code null contains "damaging"}<br>
+ * fails, but <br>
+ * {@code null does not contain "damaging"}</br>
+ * passes.
+ */
+ boolean matchesNull()
+ {
+ if (condition.isNumeric())
+ {
+ return false;
+ }
+ else
+ {
+ return condition == Condition.NotContains
+ || condition == Condition.NotMatches
+ || condition == Condition.NotPresent;
+ }
+ }
}