/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program 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 * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.example.set; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Locale; import com.rapidminer.MacroHandler; import com.rapidminer.example.Attribute; import com.rapidminer.example.AttributeTypeException; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.nio.model.DataResultSet.ValueType; import com.rapidminer.parameter.ParameterTypeTupel; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.Tools; /** * The condition is fulfilled if the individual filters are fulfilled. This filter can be * constructed from several conditions of the type {@link CustomFilters} which either must all be * fulfilled (AND) or only one must be fulfilled (OR). * * @author Marco Boeck */ public class CustomFilter implements Condition { /** * Enum for custom filters. */ public static enum CustomFilters { EQUALS_NUMERICAL("gui.comparator.numerical.equals", "eq", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { // special case to handle missing values if (Double.isNaN(filter)) { return Double.isNaN(input); } return input == filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, NOT_EQUALS_NUMERICAL("gui.comparator.numerical.not_equals", "ne", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { // special case to handle missing values if (Double.isNaN(input)) { return !Double.isNaN(filter); } return input != filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, LESS("gui.comparator.numerical.less", "lt", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return input < filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, LESS_EQUALS("gui.comparator.numerical.less_equals", "le", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return input <= filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, GREATER_EQUALS("gui.comparator.numerical.greater_equals", "ge", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return input >= filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, GREATER("gui.comparator.numerical.greater", "gt", Ontology.NUMERICAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return input > filter; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, EQUALS_NOMINAL("gui.comparator.nominal.equals", "equals", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return input.equals(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, NOT_EQUALS_NOMINAL("gui.comparator.nominal.not_equals", "does_not_equal", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return !input.equals(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, IS_IN_NOMINAL("gui.comparator.nominal.is_in", "is_in", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { List<String> filterList = Tools.unescape(filter, ESCAPE_CHAR, new char[] { SEPERATOR_CHAR }, SEPERATOR_CHAR); for (String filterString : filterList) { if (input.equals(filterString)) { return true; } } return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, IS_NOT_IN_NOMINAL("gui.comparator.nominal.is_not_in", "is_not_in", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { List<String> filterList = Tools.unescape(filter, ESCAPE_CHAR, new char[] { SEPERATOR_CHAR }, SEPERATOR_CHAR); for (String filterString : filterList) { if (input.equals(filterString)) { return false; } } return true; } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, CONTAINS("gui.comparator.nominal.contains", "contains", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return input.contains(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, NOT_CONTAINS("gui.comparator.nominal.not_contains", "does_not_contain", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return !input.contains(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, STARTS_WITH("gui.comparator.nominal.starts_with", "starts_with", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return input.startsWith(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, ENDS_WITH("gui.comparator.nominal.ends_with", "ends_with", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return input.endsWith(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, REGEX("gui.comparator.nominal.regex", "matches", Ontology.NOMINAL) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return input.matches(filter); } @Override public boolean isSpecialConditionFulfilled(final double input) { return false; } }, MISSING("gui.comparator.special.is_missing", "is_missing", -1) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return Double.isNaN(input); } }, NOT_MISSING("gui.comparator.special.is_not_missing", "is_not_missing", -1) { @Override public boolean isNumericalConditionFulfilled(final double input, final double filter) { return false; } @Override public boolean isNominalConditionFulfilled(final String input, final String filter) { return false; } @Override public boolean isSpecialConditionFulfilled(final double input) { return !Double.isNaN(input); } }; /** the symbol to seperate strings for IS_IN and IS_NOT_IN input */ public static final char SEPERATOR_CHAR = ';'; /** the symbol to escape seperator symbols in IS_IN and IS_NOT_IN input */ public static final char ESCAPE_CHAR = '\\'; /** the format string for date_time */ public static final String DATE_TIME_FORMAT_STRING = "MM/dd/yyyy h:mm:ss a"; /** the old (bugged) format string for date_time */ public static final String DATE_TIME_FORMAT_STRING_OLD = "MM/dd/yy h:mm:ss a"; /** the format string for date */ public static final String DATE_FORMAT_STRING = "MM/dd/yyyy"; /** the old (bugged) format string for date */ public static final String DATE_FORMAT_STRING_OLD = "MM/dd/yy"; /** the format string for time */ public static final String TIME_FORMAT_STRING = "h:mm:ss a"; // ThreadLocal because DateFormat is NOT threadsafe and creating a new DateFormat is // EXTREMELY expensive /** the format for date_time */ private static final ThreadLocal<DateFormat> FORMAT_DATE_TIME = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(DATE_TIME_FORMAT_STRING, Locale.ENGLISH); } }; /** the old format for date_time */ private static final ThreadLocal<DateFormat> FORMAT_DATE_TIME_OLD = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(DATE_TIME_FORMAT_STRING_OLD, Locale.ENGLISH); } }; // ThreadLocal because DateFormat is NOT threadsafe and creating a new DateFormat is // EXTREMELY expensive /** the format for date */ private static final ThreadLocal<DateFormat> FORMAT_DATE = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.ENGLISH); } }; /** the old format for date */ private static final ThreadLocal<DateFormat> FORMAT_DATE_OLD = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(DATE_FORMAT_STRING_OLD, Locale.ENGLISH); } }; // ThreadLocal because DateFormat is NOT threadsafe and creating a new DateFormat is // EXTREMELY expensive /** the format for time */ private static final ThreadLocal<DateFormat> FORMAT_TIME = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(TIME_FORMAT_STRING, Locale.ENGLISH); } }; /** the label for this filter */ private String label; /** the string representation for this filter */ private String symbol; /** the help text for this filter */ private String helptext; /** the valueType for this filter */ private int valueType; /** * Creates a new {@link CustomFilters} instance which is represented by the specified symbol * and accepts the given {@link ValueType}. * * @param key * @param symbol * @param valueType * the applicable {@link Ontology#ATTRIBUTE_VALUE_TYPE}. If set to -1, denotes a * special filter which is not restricted to any value type */ private CustomFilters(final String key, final String symbol, final int valueType) { this.symbol = symbol; this.label = I18N.getMessage(I18N.getGUIBundle(), key + ".label"); this.helptext = I18N.getMessage(I18N.getGUIBundle(), key + ".tip"); this.valueType = valueType; } /** * Returns the {@link String} label for this comparator. * * @return */ public String getLabel() { return label; } /** * Returns the {@link String} representation for this comparator. * * @return */ public String getSymbol() { return symbol; } /** * Returns the helptext for this comparator. * * @return */ public String getHelptext() { return helptext; } /** * Returns <code>true</code> if this filter is applicable for numerical values; * <code>false</code> otherwise. * * @return */ public boolean isNumericalFilter() { if (isSpecialFilter()) { return false; } if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.NUMERICAL)) { return true; } return false; } /** * Returns <code>true</code> if this filter is applicable for nominal values; * <code>false</code> otherwise. * * @return */ public boolean isNominalFilter() { if (isSpecialFilter()) { return false; } if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.NOMINAL)) { return true; } return false; } /** * Returns <code>true</code> if this filter is a special filter (e.g. missing value filter); * <code>false</code> otherwise. <br/> * These filters are applicable for all attribute types. * * @return */ public boolean isSpecialFilter() { if (valueType == -1) { return true; } return false; } /** * Returns <code>true</code> if the numerical condition for this filter is fulfilled for the * given value. Returns always <code>false</code> if the condition is for nominal values * only. * * @param input * @param filterValue * @return */ public abstract boolean isNumericalConditionFulfilled(double input, double filterValue); /** * Returns <code>true</code> if the nominal condition for this filter is fulfilled for the * given value. Returns always <code>false</code> if the condition is for numerical values * only. * * @param input * @param filterValue * @return */ public abstract boolean isNominalConditionFulfilled(String input, String filterValue); /** * Returns <code>true</code> if the special condition for this filter is fulfilled for the * given value. * * @param input * @return */ public abstract boolean isSpecialConditionFulfilled(double input); /** * Returns the {@link CustomFilters} matching the given label {@link String}. If none can be * found, returns <code>null</code>. * * @param label * @return */ public static CustomFilters getByLabel(final String label) { for (CustomFilters filter : values()) { if (filter.getLabel().equals(label)) { return filter; } } return null; } /** * Returns the {@link CustomFilters} matching the given symbol {@link String}. If none can * be found, returns <code>null</code>. * * @param symbol * @return */ public static CustomFilters getBySymbol(final String symbol) { for (CustomFilters filter : values()) { if (filter.getSymbol().equals(symbol)) { return filter; } } return null; } /** * Returns a list of {@link CustomFilters}s for the given {@link ValueType}. Returns an * empty list if no filter was found. * * @param valueType * @return */ public static List<CustomFilters> getFiltersForValueType(final int valueType) { List<CustomFilters> list = new LinkedList<>(); for (CustomFilters filter : values()) { if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.NOMINAL)) { // only nominal filters if (filter.isSpecialFilter() || filter.isNominalFilter()) { list.add(filter); } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.NUMERICAL)) { // only numerical filters if (filter.isSpecialFilter() || filter.isNumericalFilter()) { list.add(filter); } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.DATE_TIME)) { // only numerical filters if (filter.isSpecialFilter() || filter.isNumericalFilter()) { list.add(filter); } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(valueType, Ontology.ATTRIBUTE_VALUE)) { // unknown value type right now, allow all filters list.add(filter); } else { // filter only defined for numerical or nominal or unknown } } return list; } /** * Returns a {@link Date} parsed via the date {@link String} or <code>null</code> if the * given string could not be parsed. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_DATE_TIME * @param dateTimeString * @return */ public Date parseDateTime(final String dateTimeString) { try { return FORMAT_DATE_TIME.get().parse(dateTimeString); } catch (ParseException e) { return null; } } /** * Old parser for date_time. * * @param dateTimeString * @return */ private Date parseDateTimeOld(final String dateTimeString) { try { return FORMAT_DATE_TIME_OLD.get().parse(dateTimeString); } catch (ParseException e) { return null; } } /** * Returns a {@link Date} parsed via the date {@link String} or <code>null</code> if the * given string could not be parsed. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_DATE * @param dateString * @return */ public Date parseDate(final String dateString) { try { return FORMAT_DATE.get().parse(dateString); } catch (ParseException e) { return null; } } /** * Old parser for date. * * @param dateString * @return */ private Date parseDateOld(final String dateString) { try { return FORMAT_DATE_OLD.get().parse(dateString); } catch (ParseException e) { return null; } } /** * Returns a {@link Date} parsed via the date {@link String} or <code>null</code> if the * given string could not be parsed. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_TIME * @param timeString * @return */ public Date parseTime(final String timeString) { try { return FORMAT_TIME.get().parse(timeString); } catch (ParseException e) { return null; } } /** * Returns a {@link String} formatted from the {@link Date}. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_TIME * @param dateTime * @return */ public String formatDateTime(final Date dateTime) { if (dateTime == null) { throw new IllegalArgumentException("dateTime must not be null!"); } return FORMAT_DATE_TIME.get().format(dateTime); } /** * Old format for date_time. * * @param dateTime * @return */ public String formatDateTimeOld(final Date dateTime) { if (dateTime == null) { throw new IllegalArgumentException("dateTime must not be null!"); } return FORMAT_DATE_TIME_OLD.get().format(dateTime); } /** * Returns a {@link String} formatted from the {@link Date}. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_TIME * @param date * @return */ public String formatDate(final Date date) { if (date == null) { throw new IllegalArgumentException("date must not be null!"); } return FORMAT_DATE.get().format(date); } /** * Returns a {@link String} formatted from the {@link Date}. Uses {@link Locale#ENGLISH}. * <p> * Not static because {@link DateFormat} is NOT threadsafe. * </p> * * @see #FORMAT_TIME * @param time * @return */ public String formatTime(final Date time) { if (time == null) { throw new IllegalArgumentException("time must not be null!"); } return FORMAT_TIME.get().format(time); } } private static final long serialVersionUID = -1369785656210631292L; private static final String WHITESPACE = " "; private static final String BACKSLASH = "/"; private static final int CONDITION_ARRAY_REQUIRED_SIZE = 2; private static final int CONDITION_ARRAY_CONDITION_INDEX = 1; private static final int CONDITION_TUPEL_REQUIRED_SIZE = 3; private static final int CONDITION_TUPEL_ATT_INDEX = 0; private static final int CONDITION_TUPEL_FILTER_INDEX = 1; private static final int CONDITION_TUPEL_VALUE_INDEX = 2; /** the list of all conditions */ private List<String[]> conditions = new LinkedList<>(); /** * an array which indicates if for the ordered filter index the old (bugged) date parsing should * be used */ private boolean[] conditionsOldDateFilter; /** the {@link MacroHandler}, will be used to resolve filter values, can be <code>null</code> */ private MacroHandler macroHandler; private boolean fulfillAllConditions; /** * Creates a new {@link CustomFilter} instance with the {@link CustomFilters} encoded in the * {@link List} of {@link String} arrays. The {@link Boolean} parameter defines if either all * conditions must be fulfilled or only one of them. * * @param exampleSet * @param conditions * @param fulfillAllConditions * @param macroHandler * the macro handler which will be used to resolve macros for the filter value, can * be <code>null</code> * @param version * can be used to force old behaviour. If not needed and current implementation is * desired, can be set to <code>null</code> * */ public CustomFilter(final ExampleSet exampleSet, final List<String[]> conditions, final boolean fulfillAllConditions, final MacroHandler macroHandler) { if (conditions == null) { throw new IllegalArgumentException("typeList must not be null!"); } conditionsOldDateFilter = new boolean[conditions.size()]; // check if given conditions list is well formed and valid! int counter = 0; for (String[] conditionArray : conditions) { if (conditionArray.length != CONDITION_ARRAY_REQUIRED_SIZE) { throw new IllegalArgumentException("conditions must only consist of arrays of length 2!"); } String condition = conditionArray[CONDITION_ARRAY_CONDITION_INDEX]; String[] conditionTupel = ParameterTypeTupel.transformString2Tupel(condition); if (conditionTupel.length != CONDITION_TUPEL_REQUIRED_SIZE) { throw new IllegalArgumentException("Malformed condition tupels! Expected size 3 but was " + conditionTupel.length); } String attName = conditionTupel[CONDITION_TUPEL_ATT_INDEX]; Attribute att = exampleSet.getAttributes().get(attName); String filterSymbol = conditionTupel[CONDITION_TUPEL_FILTER_INDEX]; CustomFilters filter = CustomFilters.getBySymbol(filterSymbol); String filterValue = conditionTupel[CONDITION_TUPEL_VALUE_INDEX]; if (macroHandler != null) { this.macroHandler = macroHandler; filterValue = substituteMacros(filterValue, macroHandler); } if (filter == null) { throw new IllegalArgumentException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.filter_not_found", filterSymbol)); } if (att == null) { throw new IllegalArgumentException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.attribute_not_found", attName)); } // special checks for numerical filters if (filter.isNumericalFilter()) { // check if attribute is numerical if (att.isNominal()) { throw new AttributeTypeException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.numerical_comparator_type_invalid", filter.getLabel(), att.getName())); } if (att.isDateTime()) { // check if filter value works for date attribute if (filterValue == null || "".equals(filterValue) || !isStringValidDoubleValue(filter, filterValue, att)) { throw new IllegalArgumentException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.illegal_date_value", filterValue, att.getName())); } } else if (att.isNumerical()) { // check if filter value works for numerical attribute if (filterValue == null || "".equals(filterValue) || !isStringValidDoubleValue(filter, filterValue, att)) { throw new IllegalArgumentException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.illegal_numerical_value", filterValue, att.getName())); } } // keep compatibility with processes from versions prior to 6.0.004 // only affects DATE and DATE_TIME filters int yearIndex = filterValue.lastIndexOf(BACKSLASH) + 1; int firstWhitespaceIndex = filterValue.indexOf(WHITESPACE); String yearString = null; if (yearIndex > 0 && firstWhitespaceIndex > 0 && yearIndex < firstWhitespaceIndex) { yearString = filterValue.substring(yearIndex, firstWhitespaceIndex); } // if true, the old (bugged) parsing will be used; otherwise the new yyyy parsing // will be used conditionsOldDateFilter[counter] = yearString != null && yearString.length() == 2; } else if (filter.isNominalFilter()) { if (!att.isNominal()) { throw new AttributeTypeException(I18N.getMessageOrNull(I18N.getErrorBundle(), "custom_filters.nominal_comparator_type_invalid", filter.getLabel(), att.getName())); } } counter++; } this.conditions = conditions; this.fulfillAllConditions = fulfillAllConditions; } /** * The sole purpose of this constructor is to provide a constructor that matches the expected * signature for the {@link ConditionedExampleSet} reflection invocation. However, this class * cannot be instantiated by an ExampleSet and a String, so we <b>always</b> throw an * {@link IllegalArgumentException} to signal this filter cannot be instantiated that way. * * @throws IllegalArgumentException * <b>ALWAYS THROWN!</b> */ @Deprecated public CustomFilter(final ExampleSet exampleSet, final String parameterString) throws IllegalArgumentException { throw new IllegalArgumentException("This condition cannot be instantiated this way!"); } /** * Since the condition cannot be altered after creation we can just return the condition object * itself. * * @deprecated Conditions should not be able to be changed dynamically and hence there is no * need for a copy */ @Deprecated @Override public Condition duplicate() { return this; } @Override public String toString() { StringBuilder builder = new StringBuilder(); for (String[] array : conditions) { builder.append(Arrays.toString(array)); builder.append(' '); } return builder.toString(); } @Override public boolean conditionOk(final Example e) { boolean conditionsFulfilled = fulfillAllConditions ? true : false; int counter = 0; for (String[] conditionArray : conditions) { // we checked for malformed conditions in the constructor so no need to do it again String condition = conditionArray[CONDITION_ARRAY_CONDITION_INDEX]; String[] conditionTupel = ParameterTypeTupel.transformString2Tupel(condition); String attName = conditionTupel[CONDITION_TUPEL_ATT_INDEX]; Attribute att = e.getAttributes().get(attName); String filterSymbol = conditionTupel[CONDITION_TUPEL_FILTER_INDEX]; CustomFilters filter = CustomFilters.getBySymbol(filterSymbol); String filterValue = conditionTupel[CONDITION_TUPEL_VALUE_INDEX]; if (macroHandler != null) { filterValue = substituteMacros(filterValue, macroHandler); } // check if condition is fulfilled boolean fulfilled; if (filter.isSpecialFilter()) { fulfilled = filter.isSpecialConditionFulfilled(e.getValue(att)); } else if (filter.isNominalFilter()) { fulfilled = filter.isNominalConditionFulfilled(e.getNominalValue(att), filterValue); } else { fulfilled = checkNumericalCondition(e, att, filter, filterSymbol, filterValue, conditionsOldDateFilter[counter]); } // store result if (fulfillAllConditions) { conditionsFulfilled &= fulfilled; } else { conditionsFulfilled |= fulfilled; } // shortcut for OR - one fulfilled condition is enough if (!fulfillAllConditions && conditionsFulfilled) { return true; } counter++; } return conditionsFulfilled; } /** * Returns <code>true</code> if the given filter is fulfilled for the given value. * * @param e * @param att * @param filter * @param filterSymbol * @param filterValue * @param oldBehavior * if <code>true</code>, old, bugged parsing with format dd/MM/yy will be used * @return */ private boolean checkNumericalCondition(final Example e, final Attribute att, final CustomFilters filter, final String filterSymbol, final String filterValue, final boolean oldBehavior) { // special handling because we can have DATE_TIME, DATE and TIME strings in human readable // format here double doubleOriginalValue = e.getValue(att); double doubleFilterValue; try { doubleFilterValue = Double.parseDouble(filterValue); } catch (NumberFormatException e1) { // if we have a date we are losing precision - therefore we need to convert the original // value back and forth once so both lose the same amount of precision - // otherwise the filters will not work correctly if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.DATE)) { String formattedOriginal = filter.formatDate(new Date((long) doubleOriginalValue)); doubleOriginalValue = filter.parseDate(formattedOriginal).getTime(); // keep compatibility with processes from versions prior to 6.0.004 if (oldBehavior) { // if year consists of 2 chars, use old (bugged) version doubleFilterValue = filter.parseDateOld(filterValue).getTime(); } else { // new behavior doubleFilterValue = filter.parseDate(filterValue).getTime(); } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.TIME)) { String formattedOriginal = filter.formatTime(new Date((long) doubleOriginalValue)); doubleOriginalValue = filter.parseTime(formattedOriginal).getTime(); doubleFilterValue = filter.parseTime(filterValue).getTime(); } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.DATE_TIME)) { String formattedOriginal = filter.formatDateTime(new Date((long) doubleOriginalValue)); doubleOriginalValue = filter.parseDateTime(formattedOriginal).getTime(); // keep compatibility with processes from versions prior to 6.0.004 if (oldBehavior) { // if year consists of 2 chars, use old (bugged) version doubleFilterValue = filter.parseDateTimeOld(filterValue).getTime(); } else { // new behavior doubleFilterValue = filter.parseDateTime(filterValue).getTime(); } } else { // because we have checked the filters in the constructor, this is the only option // left // special handling for ? as missing value doubleFilterValue = Double.NaN; } } return filter.isNumericalConditionFulfilled(doubleOriginalValue, doubleFilterValue); } /** * Tries to parse the given {@link String} to a {@link Double} and returns <code>true</code> if * successful; <code>false</code> otherwise. * * @param value * @param att * @return */ private boolean isStringValidDoubleValue(final CustomFilters filter, final String value, final Attribute att) { try { Double.parseDouble(value); } catch (NumberFormatException e1) { // if we have a date we are losing precision - therefore we need to convert the original // value back and forth once so both lose the same amount of precision - // otherwise the filters will not work correctly if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.DATE)) { if (filter.parseDate(value) == null) { return false; } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.TIME)) { if (filter.parseTime(value) == null) { return false; } } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(att.getValueType(), Ontology.DATE_TIME)) { if (filter.parseDateTime(value) == null) { return false; } } else { if ("?".equals(value)) { // special handling for ? as missing value return true; } else { // all parsing tries failed return false; } } } return true; } /** * Tries to substitute macros with their real value. * * @param value * @param macroHandler * @return */ private static String substituteMacros(String value, final MacroHandler macroHandler) { int startIndex = value.indexOf("%{"); if (startIndex == -1) { return value; } try { StringBuffer result = new StringBuffer(); while (startIndex >= 0) { result.append(value.substring(0, startIndex)); int endIndex = value.indexOf("}", startIndex + 2); String macroString = value.substring(startIndex + 2, endIndex); String macroValue = macroHandler.getMacro(macroString); if (macroValue != null) { result.append(macroValue); } else { result.append("%{" + macroString + "}"); } value = value.substring(endIndex + 1); startIndex = value.indexOf("%{"); } result.append(value); return result.toString(); } catch (Exception e) { return value; } } }