aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/prelude/query/IntItem.java
blob: 5664ecfb3d253f49d1dbfba6d15a03e35c1bcdea (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// 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
 * <ul>
 * <li>a number (integer or floating point)
 * <li>a partial range, given by "&lt;number" or "&gt;number", where the numbers are exclusive, or
 * <li>a full or open range "[number;number], "[number;]" or "[;number]" where the numbers are inclusive,
 * or exclusive if a square bracket is replaced with a pointy one
 * </ul>
 *
 * 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 <code>from</code> are returned, and if
     * this number is negative the hits closest to <code>to</code> 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
     * <code>from</code> are returned, and if this number is negative the hits
     * closest to <code>to</code> 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 <code>hitLimit</code> or both <code>from</code>
     * and <code>to</code> 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);
        }
    }

}