--- /dev/null
+package org.apache.lucene.queryParser.standard;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.TimeZone;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericField;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.queryParser.core.QueryNodeException;
+import org.apache.lucene.queryParser.core.parser.EscapeQuerySyntax;
+import org.apache.lucene.queryParser.standard.config.NumberDateFormat;
+import org.apache.lucene.queryParser.standard.config.NumericConfig;
+import org.apache.lucene.queryParser.standard.parser.EscapeQuerySyntaxImpl;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util._TestUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestNumericQueryParser extends LuceneTestCase {
+
+ private static enum NumberType {
+ NEGATIVE, ZERO, POSITIVE;
+ }
+
+ final private static int[] DATE_STYLES = {DateFormat.FULL, DateFormat.LONG,
+ DateFormat.MEDIUM, DateFormat.SHORT};
+
+ final private static int PRECISION_STEP = 8;
+ final private static String FIELD_NAME = "field";
+ private static Locale LOCALE;
+ private static TimeZone TIMEZONE;
+ private static Map<String,Number> RANDOM_NUMBER_MAP;
+ final private static EscapeQuerySyntax ESCAPER = new EscapeQuerySyntaxImpl();
+ final private static String DATE_FIELD_NAME = "date";
+ private static int DATE_STYLE;
+ private static int TIME_STYLE;
+
+ private static Analyzer ANALYZER;
+
+ private static NumberFormat NUMBER_FORMAT;
+
+ private static StandardQueryParser qp;
+
+ private static NumberDateFormat DATE_FORMAT;
+
+ private static Directory directory = null;
+ private static IndexReader reader = null;
+ private static IndexSearcher searcher = null;
+
+ private static boolean checkDateFormatSanity(DateFormat dateFormat, long date)
+ throws ParseException {
+ return date == dateFormat.parse(dateFormat.format(new Date(date)))
+ .getTime();
+ }
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ ANALYZER = new MockAnalyzer(random);
+
+ qp = new StandardQueryParser(ANALYZER);
+
+ final HashMap<String,Number> randomNumberMap = new HashMap<String,Number>();
+
+ SimpleDateFormat dateFormat;
+ long randomDate;
+ boolean dateFormatSanityCheckPass;
+ int count = 0;
+ do {
+ if (count > 100) {
+ fail("This test has problems to find a sane random DateFormat/NumberFormat. Stopped trying after 100 iterations.");
+ }
+
+ dateFormatSanityCheckPass = true;
+ LOCALE = randomLocale(random);
+ TIMEZONE = randomTimeZone(random);
+ DATE_STYLE = randomDateStyle(random);
+ TIME_STYLE = randomDateStyle(random);
+
+ // assumes localized date pattern will have at least year, month, day,
+ // hour, minute
+ dateFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance(
+ DATE_STYLE, TIME_STYLE, LOCALE);
+
+ // not all date patterns includes era, full year, timezone and second,
+ // so we add them here
+ dateFormat.applyPattern(dateFormat.toPattern() + " G s Z yyyy");
+ dateFormat.setTimeZone(TIMEZONE);
+
+ DATE_FORMAT = new NumberDateFormat(dateFormat);
+
+ do {
+ randomDate = random.nextLong();
+
+ // prune date value so it doesn't pass in insane values to some
+ // calendars.
+ randomDate = randomDate % 3400000000000l;
+
+ // truncate to second
+ randomDate = (randomDate / 1000L) * 1000L;
+
+ // only positive values
+ randomDate = Math.abs(randomDate);
+ } while (randomDate == 0L);
+
+ dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat, randomDate);
+
+ dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat, 0);
+
+ dateFormatSanityCheckPass &= checkDateFormatSanity(dateFormat,
+ -randomDate);
+
+ count++;
+ } while (!dateFormatSanityCheckPass);
+
+ NUMBER_FORMAT = NumberFormat.getNumberInstance(LOCALE);
+ NUMBER_FORMAT.setMaximumFractionDigits((random.nextInt() & 20) + 1);
+ NUMBER_FORMAT.setMinimumFractionDigits((random.nextInt() & 20) + 1);
+ NUMBER_FORMAT.setMaximumIntegerDigits((random.nextInt() & 20) + 1);
+ NUMBER_FORMAT.setMinimumIntegerDigits((random.nextInt() & 20) + 1);
+
+ double randomDouble;
+ long randomLong;
+ int randomInt;
+ float randomFloat;
+
+ while ((randomLong = normalizeNumber(Math.abs(random.nextLong()))
+ .longValue()) == 0L)
+ ;
+ while ((randomDouble = normalizeNumber(Math.abs(random.nextDouble()))
+ .doubleValue()) == 0.0)
+ ;
+ while ((randomFloat = normalizeNumber(Math.abs(random.nextFloat()))
+ .floatValue()) == 0.0f)
+ ;
+ while ((randomInt = normalizeNumber(Math.abs(random.nextInt())).intValue()) == 0)
+ ;
+
+ randomNumberMap.put(NumericField.DataType.LONG.name(), randomLong);
+ randomNumberMap.put(NumericField.DataType.INT.name(), randomInt);
+ randomNumberMap.put(NumericField.DataType.FLOAT.name(), randomFloat);
+ randomNumberMap.put(NumericField.DataType.DOUBLE.name(), randomDouble);
+ randomNumberMap.put(DATE_FIELD_NAME, randomDate);
+
+ RANDOM_NUMBER_MAP = Collections.unmodifiableMap(randomNumberMap);
+
+ directory = newDirectory();
+ RandomIndexWriter writer = new RandomIndexWriter(random, directory,
+ newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer(random))
+ .setMaxBufferedDocs(_TestUtil.nextInt(random, 50, 1000))
+ .setMergePolicy(newLogMergePolicy()));
+
+ Document doc = new Document();
+ HashMap<String,NumericConfig> numericConfigMap = new HashMap<String,NumericConfig>();
+ HashMap<String,NumericField> numericFieldMap = new HashMap<String,NumericField>();
+ qp.setNumericConfigMap(numericConfigMap);
+
+ for (NumericField.DataType type : NumericField.DataType.values()) {
+ numericConfigMap.put(type.name(), new NumericConfig(PRECISION_STEP,
+ NUMBER_FORMAT, type));
+
+ NumericField field = new NumericField(type.name(), PRECISION_STEP,
+ Field.Store.YES, true);
+
+ numericFieldMap.put(type.name(), field);
+ doc.add(field);
+
+ }
+
+ numericConfigMap.put(DATE_FIELD_NAME, new NumericConfig(PRECISION_STEP,
+ DATE_FORMAT, NumericField.DataType.LONG));
+ NumericField dateField = new NumericField(DATE_FIELD_NAME, PRECISION_STEP,
+ Field.Store.YES, true);
+ numericFieldMap.put(DATE_FIELD_NAME, dateField);
+ doc.add(dateField);
+
+ for (NumberType numberType : NumberType.values()) {
+ setFieldValues(numberType, numericFieldMap);
+ if (VERBOSE) System.out.println("Indexing document: " + doc);
+ writer.addDocument(doc);
+ }
+
+ reader = writer.getReader();
+ searcher = newSearcher(reader);
+ writer.close();
+
+ }
+
+ private static Number getNumberType(NumberType numberType, String fieldName) {
+
+ if (numberType == null) {
+ return null;
+ }
+
+ switch (numberType) {
+
+ case POSITIVE:
+ return RANDOM_NUMBER_MAP.get(fieldName);
+
+ case NEGATIVE:
+ Number number = RANDOM_NUMBER_MAP.get(fieldName);
+
+ if (NumericField.DataType.LONG.name().equals(fieldName)
+ || DATE_FIELD_NAME.equals(fieldName)) {
+ number = -number.longValue();
+
+ } else if (NumericField.DataType.DOUBLE.name().equals(fieldName)) {
+ number = -number.doubleValue();
+
+ } else if (NumericField.DataType.FLOAT.name().equals(fieldName)) {
+ number = -number.floatValue();
+
+ } else if (NumericField.DataType.INT.name().equals(fieldName)) {
+ number = -number.intValue();
+
+ } else {
+ throw new IllegalArgumentException("field name not found: "
+ + fieldName);
+ }
+
+ return number;
+
+ default:
+ return 0;
+
+ }
+
+ }
+
+ private static void setFieldValues(NumberType numberType,
+ HashMap<String,NumericField> numericFieldMap) {
+
+ Number number = getNumberType(numberType, NumericField.DataType.DOUBLE
+ .name());
+ numericFieldMap.get(NumericField.DataType.DOUBLE.name()).setDoubleValue(
+ number.doubleValue());
+
+ number = getNumberType(numberType, NumericField.DataType.INT.name());
+ numericFieldMap.get(NumericField.DataType.INT.name()).setIntValue(
+ number.intValue());
+
+ number = getNumberType(numberType, NumericField.DataType.LONG.name());
+ numericFieldMap.get(NumericField.DataType.LONG.name()).setLongValue(
+ number.longValue());
+
+ number = getNumberType(numberType, NumericField.DataType.FLOAT.name());
+ numericFieldMap.get(NumericField.DataType.FLOAT.name()).setFloatValue(
+ number.floatValue());
+
+ number = getNumberType(numberType, DATE_FIELD_NAME);
+ numericFieldMap.get(DATE_FIELD_NAME).setLongValue(number.longValue());
+
+ }
+
+ private static int randomDateStyle(Random random) {
+ return DATE_STYLES[random.nextInt(DATE_STYLES.length)];
+ }
+
+ @Test
+ public void testInclusiveNumericRange() throws Exception {
+ assertRangeQuery(NumberType.ZERO, NumberType.ZERO, true, true, 1);
+ assertRangeQuery(NumberType.ZERO, NumberType.POSITIVE, true, true, 2);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.ZERO, true, true, 2);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.POSITIVE, true, true, 3);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.NEGATIVE, true, true, 1);
+ }
+
+// @Test
+// // test disabled since standard syntax parser does not work with inclusive and
+// // exclusive at the same time
+// public void testInclusiveLowerNumericRange() throws Exception {
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.ZERO, false, true, 1);
+// assertRangeQuery(NumberType.ZERO, NumberType.POSITIVE, false, true, 1);
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.POSITIVE, false, true, 2);
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.NEGATIVE, false, true, 0);
+// }
+//
+// @Test
+// // test disabled since standard syntax parser does not work with inclusive and
+// // exclusive at the same time
+// public void testInclusiveUpperNumericRange() throws Exception {
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.ZERO, true, false, 1);
+// assertRangeQuery(NumberType.ZERO, NumberType.POSITIVE, true, false, 1);
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.POSITIVE, true, false, 2);
+// assertRangeQuery(NumberType.NEGATIVE, NumberType.NEGATIVE, true, false, 0);
+// }
+
+ @Test
+ public void testExclusiveNumericRange() throws Exception {
+ assertRangeQuery(NumberType.ZERO, NumberType.ZERO, false, false, 0);
+ assertRangeQuery(NumberType.ZERO, NumberType.POSITIVE, false, false, 0);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.ZERO, false, false, 0);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.POSITIVE, false, false, 1);
+ assertRangeQuery(NumberType.NEGATIVE, NumberType.NEGATIVE, false, false, 0);
+ }
+
+// @Test
+//// test disabled since standard syntax parser does not work with open range
+// public void testOpenRangeNumericQuery() throws Exception {
+// assertOpenRangeQuery(NumberType.ZERO, "<", 1);
+// assertOpenRangeQuery(NumberType.POSITIVE, "<", 2);
+// assertOpenRangeQuery(NumberType.NEGATIVE, "<", 0);
+//
+// assertOpenRangeQuery(NumberType.ZERO, "<=", 2);
+// assertOpenRangeQuery(NumberType.POSITIVE, "<=", 3);
+// assertOpenRangeQuery(NumberType.NEGATIVE, "<=", 1);
+//
+// assertOpenRangeQuery(NumberType.ZERO, ">", 1);
+// assertOpenRangeQuery(NumberType.POSITIVE, ">", 0);
+// assertOpenRangeQuery(NumberType.NEGATIVE, ">", 2);
+//
+// assertOpenRangeQuery(NumberType.ZERO, ">=", 2);
+// assertOpenRangeQuery(NumberType.POSITIVE, ">=", 1);
+// assertOpenRangeQuery(NumberType.NEGATIVE, ">=", 3);
+//
+// assertOpenRangeQuery(NumberType.NEGATIVE, "=", 1);
+// assertOpenRangeQuery(NumberType.ZERO, "=", 1);
+// assertOpenRangeQuery(NumberType.POSITIVE, "=", 1);
+//
+// assertRangeQuery(NumberType.NEGATIVE, null, true, true, 3);
+// assertRangeQuery(NumberType.NEGATIVE, null, false, true, 2);
+// assertRangeQuery(NumberType.POSITIVE, null, true, false, 1);
+// assertRangeQuery(NumberType.ZERO, null, false, false, 1);
+//
+// assertRangeQuery(null, NumberType.POSITIVE, true, true, 3);
+// assertRangeQuery(null, NumberType.POSITIVE, true, false, 2);
+// assertRangeQuery(null, NumberType.NEGATIVE, false, true, 1);
+// assertRangeQuery(null, NumberType.ZERO, false, false, 1);
+//
+// assertRangeQuery(null, null, false, false, 3);
+// assertRangeQuery(null, null, true, true, 3);
+//
+// }
+
+ @Test
+ public void testSimpleNumericQuery() throws Exception {
+ assertSimpleQuery(NumberType.ZERO, 1);
+ assertSimpleQuery(NumberType.POSITIVE, 1);
+ assertSimpleQuery(NumberType.NEGATIVE, 1);
+ }
+
+ public void assertRangeQuery(NumberType lowerType, NumberType upperType,
+ boolean lowerInclusive, boolean upperInclusive, int expectedDocCount)
+ throws QueryNodeException, IOException {
+
+ StringBuilder sb = new StringBuilder();
+
+ String lowerInclusiveStr = (lowerInclusive ? "[" : "{");
+ String upperInclusiveStr = (upperInclusive ? "]" : "}");
+
+ for (NumericField.DataType type : NumericField.DataType.values()) {
+ String lowerStr = numberToString(getNumberType(lowerType, type.name()));
+ String upperStr = numberToString(getNumberType(upperType, type.name()));
+
+ sb.append("+").append(type.name()).append(':').append(lowerInclusiveStr)
+ .append('"').append(lowerStr).append("\" TO \"").append(upperStr)
+ .append('"').append(upperInclusiveStr).append(' ');
+ }
+
+ Number lowerDateNumber = getNumberType(lowerType, DATE_FIELD_NAME);
+ Number upperDateNumber = getNumberType(upperType, DATE_FIELD_NAME);
+ String lowerDateStr;
+ String upperDateStr;
+
+ if (lowerDateNumber != null) {
+ lowerDateStr = ESCAPER.escape(
+ DATE_FORMAT.format(new Date(lowerDateNumber.longValue())), LOCALE,
+ EscapeQuerySyntax.Type.STRING).toString();
+
+ } else {
+ lowerDateStr = "*";
+ }
+
+ if (upperDateNumber != null) {
+ upperDateStr = ESCAPER.escape(
+ DATE_FORMAT.format(new Date(upperDateNumber.longValue())), LOCALE,
+ EscapeQuerySyntax.Type.STRING).toString();
+
+ } else {
+ upperDateStr = "*";
+ }
+
+ sb.append("+").append(DATE_FIELD_NAME).append(':')
+ .append(lowerInclusiveStr).append('"').append(lowerDateStr).append(
+ "\" TO \"").append(upperDateStr).append('"').append(
+ upperInclusiveStr);
+
+ testQuery(sb.toString(), expectedDocCount);
+
+ }
+
+ public void assertOpenRangeQuery(NumberType boundType, String operator, int expectedDocCount)
+ throws QueryNodeException, IOException {
+
+ StringBuilder sb = new StringBuilder();
+
+ for (NumericField.DataType type : NumericField.DataType.values()) {
+ String boundStr = numberToString(getNumberType(boundType, type.name()));
+
+ sb.append("+").append(type.name()).append(operator).append('"').append(boundStr).append('"').append(' ');
+ }
+
+ String boundDateStr = ESCAPER.escape(
+ DATE_FORMAT.format(new Date(getNumberType(boundType, DATE_FIELD_NAME)
+ .longValue())), LOCALE, EscapeQuerySyntax.Type.STRING).toString();
+
+ sb.append("+").append(DATE_FIELD_NAME).append(operator).append('"').append(boundDateStr).append('"');
+
+ testQuery(sb.toString(), expectedDocCount);
+ }
+
+ public void assertSimpleQuery(NumberType numberType, int expectedDocCount)
+ throws QueryNodeException, IOException {
+ StringBuilder sb = new StringBuilder();
+
+ for (NumericField.DataType type : NumericField.DataType.values()) {
+ String numberStr = numberToString(getNumberType(numberType, type.name()));
+ sb.append('+').append(type.name()).append(":\"").append(numberStr)
+ .append("\" ");
+ }
+
+ String dateStr = ESCAPER.escape(
+ DATE_FORMAT.format(new Date(getNumberType(numberType, DATE_FIELD_NAME)
+ .longValue())), LOCALE, EscapeQuerySyntax.Type.STRING).toString();
+
+ sb.append('+').append(DATE_FIELD_NAME).append(":\"").append(dateStr)
+ .append('"');
+
+ testQuery(sb.toString(), expectedDocCount);
+
+ }
+
+ private void testQuery(String queryStr, int expectedDocCount)
+ throws QueryNodeException, IOException {
+ if (VERBOSE) System.out.println("Parsing: " + queryStr);
+
+ Query query = qp.parse(queryStr, FIELD_NAME);
+ if (VERBOSE) System.out.println("Querying: " + query);
+ TopDocs topDocs = searcher.search(query, 1000);
+
+ String msg = "Query <" + queryStr + "> retrieved " + topDocs.totalHits
+ + " document(s), " + expectedDocCount + " document(s) expected.";
+
+ if (VERBOSE) System.out.println(msg);
+
+ assertEquals(msg, expectedDocCount, topDocs.totalHits);
+ }
+
+ private static String numberToString(Number number) {
+ return number == null ? "*" : ESCAPER.escape(NUMBER_FORMAT.format(number),
+ LOCALE, EscapeQuerySyntax.Type.STRING).toString();
+ }
+
+ private static Number normalizeNumber(Number number) throws ParseException {
+ return NUMBER_FORMAT.parse(NUMBER_FORMAT.format(number));
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ searcher.close();
+ searcher = null;
+ reader.close();
+ reader = null;
+ directory.close();
+ directory = null;
+ }
+
+}