// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.prelude.query; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.Objects; /** * This represents either * * * If a range is specified in brackets, it is also permissible to add a third number specifying the number of hits this * will match on each node - [from;to;hitLimit] * * @author bratseth */ public class IntItem extends TermItem { /** The inclusive lower end of this range */ private Limit from; /** The inclusive upper end of this range */ private Limit to; private int hitLimit = 0; /** The number expression of this */ private String expression; /** * Creates an int item which must be equal to the given int number - * that is both the lower and upper limit is this number */ public IntItem(int number, String indexName) { this(new Limit(number, true), new Limit(number, true), indexName); } /** * Creates an int item which must be equal to the given long number - * that is both the lower and upper limit is this number */ public IntItem(long number, String indexName) { this(new Limit(number, true), new Limit(number, true), indexName); } public IntItem(Limit from, Limit to, String indexName) { super(indexName, false); this.from = from; this.to = to; expression = toExpression(from, to, 0); } public IntItem(String expression) { this(expression, ""); } public IntItem(String expression, boolean isFromQuery) { this(expression, "", isFromQuery); } public IntItem(String expression, String indexName) { this(expression, indexName, false); } public IntItem(String expression, String indexName, boolean isFromQuery) { super(indexName, isFromQuery); setNumber(expression); } public IntItem(Limit from, Limit to, int hitLimit, String indexName, boolean isFromQuery) { super(indexName, isFromQuery); setLimits(from, to); this.hitLimit = hitLimit; this.expression = toExpression(from, to, hitLimit); } /** Returns the simplest expression matching this */ private String toExpression(Limit from, Limit to, int hitLimit) { if (from.equals(to) && hitLimit == 0) return from.number().toString(); String expression = from.toRangeStart() + ";" + to.toRangeEnd(); if (hitLimit == 0) return expression; // Insert ;hitLimit at the end inside the brackets return expression.substring(0, expression.length()-1) + ";" + hitLimit + expression.substring(expression.length()-1); } /** Sets limit and flip them if "from" is greater than "to" */ private void setLimits(Limit from, Limit to) { if (from.number().doubleValue() > to.number().doubleValue()) { this.from = to; this.to = from; } else { this.from = from; this.to = to; } } /** Sets the number expression of this - a number or range following the syntax specified in the class javadoc */ public void setNumber(String expression) { try { this.expression = expression; parseAndAssignLimits(expression.trim()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("'" + expression + "' is not an int item expression: " + "Expected NUMBER, '<'NUMBER, '>'NUMBER or ('['|'<')NUMBER;NUMBER(;NUMBER)?(']'|'>')", e); } } private void parseAndAssignLimits(String e) { if (e.startsWith("<") && ! e.contains(";")) { from = Limit.NEGATIVE_INFINITY; to = new Limit(asNumber(e.substring(1)), false); } else if (e.startsWith(">")) { from = new Limit(asNumber(e.substring(1)), false); to = Limit.POSITIVE_INFINITY; } else if (e.startsWith("[") || e.startsWith("<")) { if ( ! (e.endsWith("]") || e.endsWith(">"))) throw new IllegalArgumentException("No closing bracket"); boolean inclusiveStart = e.startsWith("["); boolean inclusiveEnd = e.startsWith("["); String[] limits = e.substring(1, e.length()-1).split(";"); if (limits.length < 1 || limits.length > 3) throw new IllegalArgumentException("Unexpected bracket content"); Limit from = new Limit(getOr(Double.NEGATIVE_INFINITY, 0, limits), inclusiveStart); Limit to = new Limit(getOr(Double.POSITIVE_INFINITY, 1, limits), inclusiveEnd); setLimits(from, to); hitLimit = getOr(0, 2, limits).intValue(); } else { to = from = new Limit(asNumber(e), true); } } private Number getOr(Number defaultValue, int valueIndex, String[] values) { if (valueIndex >= values.length) return defaultValue; if (values[valueIndex] == null) return defaultValue; if (values[valueIndex].isEmpty()) return defaultValue; return asNumber(values[valueIndex]); } private Number asNumber(String numberString) { try { if (!numberString.contains(".")) return Long.valueOf(numberString); } catch (NumberFormatException e) { return new BigInteger(numberString); } return Double.valueOf(numberString); } /** Sets the number expression of this - a number or range */ public String getNumber() { return expression; } /** Returns the lower limit of this range, which may be negative infinity */ public final Limit getFromLimit() { return from; } /** Returns the upper limit of this range, which may be positive infinity */ public final Limit getToLimit() { return to; } /** * Returns the number of hits this will match, or 0 if all should be matched. * If this number is positive, the hits closest to from are returned, and if * this number is negative the hits closest to to are returned. */ public final int getHitLimit() { return hitLimit; } /** * Sets the number of hits this will match, or 0 if all should be * matched. If this number is positive, the hits closest to * from are returned, and if this number is negative the hits * closest to to are returned. * * @param hitLimit number of hits to match for this operator */ public final void setHitLimit(int hitLimit) { this.hitLimit = hitLimit; this.expression = toExpression(from, to, hitLimit); } @Override public String getRawWord() { return getNumber(); } @Override public ItemType getItemType() { return ItemType.INT; } @Override public String getName() { return "INT"; } @Override public String stringValue() { return expression; } /** Same as {@link #setNumber} */ @Override public void setValue(String value) { setNumber(value); } /** Int items uses a empty heading instead of "INT " */ protected void appendHeadingString(StringBuilder buffer) {} @Override public int hashCode() { return Objects.hash(super.hashCode(), expression); } @Override public boolean equals(Object object) { if ( ! super.equals(object)) return false; IntItem other = (IntItem) object; // Ensured by superclass if ( ! getFromLimit().equals(other.getFromLimit())) return false; if ( ! getToLimit().equals(other.getToLimit())) return false; if ( getHitLimit() != other.getHitLimit()) return false; return true; } /** Returns the number for encoding; the number expression as-is. */ protected String getEncodedInt() { return getIndexedString(); } @Override public String getIndexedString() { return expression; } @Override protected void encodeThis(ByteBuffer buffer) { super.encodeThis(buffer); // takes care of index bytes putString(getEncodedInt(), buffer); } @Override public int getNumWords() { return 1; } @Override public boolean isStemmed() { return true; } @Override public boolean isWords() { return false; } /** * Creates an int item from arguments. * This will return an instance of the RankItem subclass if either hitLimit or both from * and to is set to a value other than defaults (respectively 0, double negative and positive infinity). * And different from each other. * * @param indexName the index this searches * @param from the lower limit (inclusive) on hits * @param to the higher limit (inclusive) on hits * @param hitLimit the number of hits to match, or 0 to return all */ public static IntItem from(String indexName, Limit from, Limit to, int hitLimit) { if (hitLimit == 0 && (from.equals(Limit.NEGATIVE_INFINITY) || to.equals(Limit.POSITIVE_INFINITY) || from.equals(to))) return new IntItem(from, to, indexName); else { return new RangeItem(from, to, hitLimit, indexName, false); } } }