blob: 5b616f15937d94eca7655152f151382522b4cbe3 [file] [log] [blame]
using J2N.Collections.Generic.Extensions;
using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Index.Extensions;
using Lucene.Net.QueryParsers.Flexible.Core.Parser;
using Lucene.Net.QueryParsers.Flexible.Standard.Config;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Support;
using Lucene.Net.Util;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Console = Lucene.Net.Support.SystemConsole;
using JCG = J2N.Collections.Generic;
namespace Lucene.Net.QueryParsers.Flexible.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.
*/
public class TestNumericQueryParser : LuceneTestCase
{
public enum NumberType
{
NEGATIVE, ZERO, POSITIVE
}
private readonly static DateFormat[] DATE_STYLES = {DateFormat.FULL, DateFormat.LONG,
DateFormat.MEDIUM, DateFormat.SHORT};
private readonly static int PRECISION_STEP = 8;
private readonly static String FIELD_NAME = "field";
private static CultureInfo LOCALE;
private static TimeZoneInfo TIMEZONE;
private static IDictionary<String, /*Number*/ object> RANDOM_NUMBER_MAP;
private readonly static IEscapeQuerySyntax ESCAPER = new Standard.Parser.EscapeQuerySyntax();
private readonly static String DATE_FIELD_NAME = "date";
private static DateFormat DATE_STYLE;
private static DateFormat 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 bool checkDateFormatSanity(/*DateFormat*/string dateFormat, long date)
{
DateTime result;
return DateTime.TryParseExact(new DateTime(NumberDateFormat.EPOCH).AddMilliseconds(date).ToString(dateFormat),
dateFormat, CultureInfo.CurrentCulture, DateTimeStyles.RoundtripKind, out result);
}
[OneTimeSetUp]
public override void BeforeClass()
{
base.BeforeClass();
ANALYZER = new MockAnalyzer(Random);
qp = new StandardQueryParser(ANALYZER);
IDictionary<string, /*Number*/object> randomNumberMap = new JCG.Dictionary<string, object>();
/*SimpleDateFormat*/
string dateFormat;
long randomDate;
bool 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 = RandomCulture(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);
// assumes localized date pattern will have at least year, month, day,
// hour, minute
DATE_FORMAT = new NumberDateFormat(DATE_STYLE, TIME_STYLE, LOCALE)
{
TimeZone = TIMEZONE
};
// not all date patterns includes era, full year, timezone and second,
// so we add them here
DATE_FORMAT.SetDateFormat(DATE_FORMAT.GetDateFormat() + " g s z yyyy");
dateFormat = DATE_FORMAT.GetDateFormat();
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);
NUMBER_FORMAT = new NumberFormat(LOCALE);
double randomDouble;
long randomLong;
int randomInt;
float randomFloat;
while ((randomLong = Convert.ToInt64(NormalizeNumber(Math.Abs(Random.nextLong()))
)) == 0L)
;
while ((randomDouble = Convert.ToDouble(NormalizeNumber(Math.Abs(Random.NextDouble()))
)) == 0.0)
;
while ((randomFloat = Convert.ToSingle(NormalizeNumber(Math.Abs(Random.nextFloat()))
)) == 0.0f)
;
while ((randomInt = Convert.ToInt32(NormalizeNumber(Math.Abs(Random.nextInt())))) == 0)
;
randomNumberMap.Put(NumericType.INT64.ToString(), randomLong);
randomNumberMap.Put(NumericType.INT32.ToString(), randomInt);
randomNumberMap.Put(NumericType.SINGLE.ToString(), randomFloat);
randomNumberMap.Put(NumericType.DOUBLE.ToString(), randomDouble);
randomNumberMap.Put(DATE_FIELD_NAME, randomDate);
RANDOM_NUMBER_MAP = randomNumberMap.AsReadOnly();
directory = NewDirectory();
RandomIndexWriter writer = new RandomIndexWriter(Random, directory,
NewIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer(Random))
.SetMaxBufferedDocs(TestUtil.NextInt32(Random, 50, 1000))
.SetMergePolicy(NewLogMergePolicy()));
Document doc = new Document();
IDictionary<String, NumericConfig> numericConfigMap = new JCG.Dictionary<String, NumericConfig>();
IDictionary<String, Field> numericFieldMap = new JCG.Dictionary<String, Field>();
qp.NumericConfigMap = (numericConfigMap);
foreach (NumericType type in Enum.GetValues(typeof(NumericType)))
{
if (type == NumericType.NONE)
{
continue;
}
numericConfigMap.Put(type.ToString(), new NumericConfig(PRECISION_STEP,
NUMBER_FORMAT, type));
FieldType ft2 = new FieldType(Int32Field.TYPE_NOT_STORED);
ft2.NumericType = (type);
ft2.IsStored = (true);
ft2.NumericPrecisionStep = (PRECISION_STEP);
ft2.Freeze();
Field field;
switch (type)
{
case NumericType.INT32:
field = new Int32Field(type.ToString(), 0, ft2);
break;
case NumericType.SINGLE:
field = new SingleField(type.ToString(), 0.0f, ft2);
break;
case NumericType.INT64:
field = new Int64Field(type.ToString(), 0L, ft2);
break;
case NumericType.DOUBLE:
field = new DoubleField(type.ToString(), 0.0, ft2);
break;
default:
fail();
field = null;
break;
}
numericFieldMap.Put(type.ToString(), field);
doc.Add(field);
}
numericConfigMap.Put(DATE_FIELD_NAME, new NumericConfig(PRECISION_STEP,
DATE_FORMAT, NumericType.INT64));
FieldType ft = new FieldType(Int64Field.TYPE_NOT_STORED);
ft.IsStored = (true);
ft.NumericPrecisionStep = (PRECISION_STEP);
Int64Field dateField = new Int64Field(DATE_FIELD_NAME, 0L, ft);
numericFieldMap.Put(DATE_FIELD_NAME, dateField);
doc.Add(dateField);
foreach (NumberType numberType in Enum.GetValues(typeof(NumberType)))
{
setFieldValues(numberType, numericFieldMap);
if (VERBOSE) Console.WriteLine("Indexing document: " + doc);
writer.AddDocument(doc);
}
reader = writer.GetReader();
searcher = NewSearcher(reader);
writer.Dispose();
}
private static /*Number*/ object GetNumberType(NumberType? numberType, String fieldName)
{
if (numberType == null)
{
return null;
}
switch (numberType)
{
case NumberType.POSITIVE:
return RANDOM_NUMBER_MAP[fieldName];
case NumberType.NEGATIVE:
/*Number*/
object number = RANDOM_NUMBER_MAP[fieldName];
if (NumericType.INT64.ToString().Equals(fieldName, StringComparison.Ordinal)
|| DATE_FIELD_NAME.Equals(fieldName, StringComparison.Ordinal))
{
number = -Convert.ToInt64(number);
}
else if (NumericType.DOUBLE.ToString().Equals(fieldName, StringComparison.Ordinal))
{
number = -Convert.ToDouble(number);
}
else if (NumericType.SINGLE.ToString().Equals(fieldName, StringComparison.Ordinal))
{
number = -Convert.ToSingle(number);
}
else if (NumericType.INT32.ToString().Equals(fieldName, StringComparison.Ordinal))
{
number = -Convert.ToInt32(number);
}
else
{
throw new ArgumentException("field name not found: "
+ fieldName);
}
return number;
default:
return 0;
}
}
private static void setFieldValues(NumberType numberType,
IDictionary<String, Field> numericFieldMap)
{
/*Number*/
object number = GetNumberType(numberType, NumericType.DOUBLE
.ToString());
numericFieldMap[NumericType.DOUBLE.ToString()].SetDoubleValue(Convert.ToDouble(
number));
number = GetNumberType(numberType, NumericType.INT32.ToString());
numericFieldMap[NumericType.INT32.ToString()].SetInt32Value(Convert.ToInt32(
number));
number = GetNumberType(numberType, NumericType.INT64.ToString());
numericFieldMap[NumericType.INT64.ToString()].SetInt64Value(Convert.ToInt64(
number));
number = GetNumberType(numberType, NumericType.SINGLE.ToString());
numericFieldMap[NumericType.SINGLE.ToString()].SetSingleValue(Convert.ToSingle(
number));
number = GetNumberType(numberType, DATE_FIELD_NAME);
numericFieldMap[DATE_FIELD_NAME].SetInt64Value(Convert.ToInt64(number));
}
private static DateFormat randomDateStyle(Random random)
{
return DATE_STYLES[random.nextInt(DATE_STYLES.Length)];
}
[Test]
public void TestInclusiveNumericRange()
{
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()
{
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()
{
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()
{
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]
public void TestOpenRangeNumericQuery()
{
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()
{
assertSimpleQuery(NumberType.ZERO, 1);
assertSimpleQuery(NumberType.POSITIVE, 1);
assertSimpleQuery(NumberType.NEGATIVE, 1);
}
public void assertRangeQuery(NumberType? lowerType, NumberType? upperType,
bool lowerInclusive, bool upperInclusive, int expectedDocCount)
{
StringBuilder sb = new StringBuilder();
String lowerInclusiveStr = (lowerInclusive ? "[" : "{");
String upperInclusiveStr = (upperInclusive ? "]" : "}");
foreach (NumericType type in Enum.GetValues(typeof(NumericType)))
{
if (type == NumericType.NONE)
{
continue;
}
String lowerStr = NumberToString(GetNumberType(lowerType, type.ToString()));
String upperStr = NumberToString(GetNumberType(upperType, type.ToString()));
sb.append("+").append(type.ToString()).append(':').append(lowerInclusiveStr)
.append('"').append(lowerStr).append("\" TO \"").append(upperStr)
.append('"').append(upperInclusiveStr).append(' ');
}
/*Number*/
object lowerDateNumber = GetNumberType(lowerType, DATE_FIELD_NAME);
/*Number*/
object upperDateNumber = GetNumberType(upperType, DATE_FIELD_NAME);
String lowerDateStr;
String upperDateStr;
if (lowerDateNumber != null)
{
//lowerDateStr = ESCAPER.Escape(
// DATE_FORMAT.format(new DateTime(lowerDateNumber.longValue())), LOCALE,
// EscapeQuerySyntax.Type.STRING).toString();
lowerDateStr = ESCAPER.Escape(
DATE_FORMAT.Format(Convert.ToInt64(lowerDateNumber, CultureInfo.InvariantCulture)),
LOCALE,
EscapeQuerySyntaxType.STRING).toString();
}
else
{
lowerDateStr = "*";
}
if (upperDateNumber != null)
{
//upperDateStr = ESCAPER.Escape(
// DATE_FORMAT.format(new DateTime(upperDateNumber.longValue())), LOCALE,
// EscapeQuerySyntax.Type.STRING).toString();
upperDateStr = ESCAPER.Escape(
DATE_FORMAT.Format(Convert.ToInt64(upperDateNumber, CultureInfo.InvariantCulture)),
LOCALE,
EscapeQuerySyntaxType.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)
{
StringBuilder sb = new StringBuilder();
foreach (NumericType type in Enum.GetValues(typeof(NumericType)))
{
if (type == NumericType.NONE)
{
continue;
}
String boundStr = NumberToString(GetNumberType(boundType, type.ToString()));
sb.append("+").append(type.ToString()).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();
string boundDateStr = ESCAPER.Escape(
DATE_FORMAT.Format(Convert.ToInt64(GetNumberType(boundType, DATE_FIELD_NAME))),
LOCALE,
EscapeQuerySyntaxType.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)
{
StringBuilder sb = new StringBuilder();
foreach (NumericType type in Enum.GetValues(typeof(NumericType)))
{
if (type == NumericType.NONE)
{
continue;
}
String numberStr = NumberToString(GetNumberType(numberType, type.ToString()));
sb.append('+').append(type.ToString()).append(":\"").append(numberStr)
.append("\" ");
}
//String dateStr = ESCAPER.Escape(
// DATE_FORMAT.format(new DateTime(getNumberType(numberType, DATE_FIELD_NAME)
// .longValue())), LOCALE, EscapeQuerySyntax.Type.STRING).toString();
string dateStr = ESCAPER.Escape(
DATE_FORMAT.Format(Convert.ToInt64(GetNumberType(numberType, DATE_FIELD_NAME))),
LOCALE,
EscapeQuerySyntaxType.STRING).toString();
sb.append('+').append(DATE_FIELD_NAME).append(":\"").append(dateStr)
.append('"');
TestQuery(sb.toString(), expectedDocCount);
}
private void TestQuery(String queryStr, int expectedDocCount)
{
if (VERBOSE) Console.WriteLine("Parsing: " + queryStr);
Query query = qp.Parse(queryStr, FIELD_NAME);
if (VERBOSE) Console.WriteLine("Querying: " + query);
TopDocs topDocs = searcher.Search(query, 1000);
String msg = "Query <" + queryStr + "> retrieved " + topDocs.TotalHits
+ " document(s), " + expectedDocCount + " document(s) expected.";
if (VERBOSE) Console.WriteLine(msg);
assertEquals(msg, expectedDocCount, topDocs.TotalHits);
}
private static String NumberToString(/*Number*/ object number)
{
return number == null ? "*" : ESCAPER.Escape(NUMBER_FORMAT.Format(number),
LOCALE, EscapeQuerySyntaxType.STRING).toString();
}
private static /*Number*/ object NormalizeNumber(/*Number*/ object number)
{
return NUMBER_FORMAT.Parse(NUMBER_FORMAT.Format(number));
}
[OneTimeTearDown]
public override void AfterClass()
{
searcher = null;
reader.Dispose();
reader = null;
directory.Dispose();
directory = null;
qp = null;
base.AfterClass();
}
}
}