mirror of https://github.com/apache/lucene.git
SOLR-71: Date Math for DateField
git-svn-id: https://svn.apache.org/repos/asf/incubator/solr/trunk@477465 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
d68fbb7bea
commit
57653c5490
|
@ -65,7 +65,10 @@ New Features
|
||||||
29. autoCommit can be specified every so many documents added (klaas, SOLR-65)
|
29. autoCommit can be specified every so many documents added (klaas, SOLR-65)
|
||||||
30. ${solr.home}/lib directory can now be used for specifying "plugin" jars
|
30. ${solr.home}/lib directory can now be used for specifying "plugin" jars
|
||||||
(hossman, SOLR-68)
|
(hossman, SOLR-68)
|
||||||
|
31. Support for "Date Math" relative "NOW" when specifying values of a
|
||||||
|
DateField in a query -- or when adding a document.
|
||||||
|
(hossman, SOLR-71)
|
||||||
|
|
||||||
Changes in runtime behavior
|
Changes in runtime behavior
|
||||||
1. classes reorganized into different packages, package names changed to Apache
|
1. classes reorganized into different packages, package names changed to Apache
|
||||||
2. force read of document stored fields in QuerySenderListener
|
2. force read of document stored fields in QuerySenderListener
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
<field name="price">0</field>
|
<field name="price">0</field>
|
||||||
<field name="popularity">10</field>
|
<field name="popularity">10</field>
|
||||||
<field name="inStock">true</field>
|
<field name="inStock">true</field>
|
||||||
|
<field name="incubationdate_dt">2006-01-17T00:00:00.000Z</field>
|
||||||
</doc>
|
</doc>
|
||||||
</add>
|
</add>
|
||||||
|
|
||||||
|
|
|
@ -83,11 +83,25 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- The format for this date field is of the form 1995-12-31T23:59:59Z, and
|
<!-- The format for this date field is of the form 1995-12-31T23:59:59Z, and
|
||||||
is a more restricted form of the canonical representation of dateTime
|
Is a more restricted form of the canonical representation of dateTime
|
||||||
http://www.w3.org/TR/xmlschema-2/#dateTime
|
http://www.w3.org/TR/xmlschema-2/#dateTime
|
||||||
The trailing "Z" designates UTC time and is mandatory.
|
The trailing "Z" designates UTC time and is mandatory.
|
||||||
Optional fractional seconds are allowed: 1995-12-31T23:59:59.999Z
|
Optional fractional seconds are allowed: 1995-12-31T23:59:59.999Z
|
||||||
All other components are mandatory. -->
|
All other components are mandatory.
|
||||||
|
|
||||||
|
Expressions can also be used to denote calculations which should be
|
||||||
|
performed relative "NOW" to determine the value, ie...
|
||||||
|
|
||||||
|
NOW/HOUR
|
||||||
|
... Round to the start of the current hour
|
||||||
|
NOW-1DAY
|
||||||
|
... Exactly 1 day prior to now
|
||||||
|
NOW/DAY+6MONTHS+3DAYS
|
||||||
|
... 6 months and 3 days in the future from the start of
|
||||||
|
the current day
|
||||||
|
|
||||||
|
Consult the DateField javadocs for more information.
|
||||||
|
-->
|
||||||
<fieldtype name="date" class="solr.DateField" sortMissingLast="true"/>
|
<fieldtype name="date" class="solr.DateField" sortMissingLast="true"/>
|
||||||
|
|
||||||
<!-- solr.TextField allows the specification of custom text analyzers
|
<!-- solr.TextField allows the specification of custom text analyzers
|
||||||
|
|
|
@ -250,6 +250,10 @@
|
||||||
<lst name="defaults">
|
<lst name="defaults">
|
||||||
<str name="qf">text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0</str>
|
<str name="qf">text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0</str>
|
||||||
<str name="mm">2<-1 5<-2 6<90%</str>
|
<str name="mm">2<-1 5<-2 6<90%</str>
|
||||||
|
<!-- This is an example of using Date Math to specify a constantly
|
||||||
|
moving date range in a config...
|
||||||
|
-->
|
||||||
|
<str name="bq">incubationdate_dt:[* TO NOW/DAY-1MONTH]^2.2</str>
|
||||||
</lst>
|
</lst>
|
||||||
<!-- In addition to defaults, "appends" params can be specified
|
<!-- In addition to defaults, "appends" params can be specified
|
||||||
to identify values which should be appended to the list of
|
to identify values which should be appended to the list of
|
||||||
|
|
|
@ -25,9 +25,16 @@ import org.apache.lucene.document.Fieldable;
|
||||||
import org.apache.lucene.search.SortField;
|
import org.apache.lucene.search.SortField;
|
||||||
import org.apache.solr.search.function.ValueSource;
|
import org.apache.solr.search.function.ValueSource;
|
||||||
import org.apache.solr.search.function.OrdFieldSource;
|
import org.apache.solr.search.function.OrdFieldSource;
|
||||||
|
import org.apache.solr.util.DateMathParser;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
|
||||||
// TODO: make a FlexibleDateField that can accept dates in multiple
|
// TODO: make a FlexibleDateField that can accept dates in multiple
|
||||||
// formats, better for human entered dates.
|
// formats, better for human entered dates.
|
||||||
|
@ -62,12 +69,22 @@ import java.io.IOException;
|
||||||
* acronym UTC was chosen as a compromise."
|
* acronym UTC was chosen as a compromise."
|
||||||
* </blockquote>
|
* </blockquote>
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
|
* This FieldType also supports incoming "Date Math" strings for computing
|
||||||
|
* values by adding/rounding internals of time relative "NOW",
|
||||||
|
* ie: "NOW+1YEAR", "NOW/DAY", etc.. -- see {@link DateMathParser}
|
||||||
|
* for more examples.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
* @author yonik
|
* @author yonik
|
||||||
* @version $Id$
|
* @version $Id$
|
||||||
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML schema part 2</a>
|
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML schema part 2</a>
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public class DateField extends FieldType {
|
public class DateField extends FieldType {
|
||||||
|
|
||||||
|
public static TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||||
|
|
||||||
// The XML (external) date format will sort correctly, except if
|
// The XML (external) date format will sort correctly, except if
|
||||||
// fractions of seconds are present (because '.' is lower than 'Z').
|
// fractions of seconds are present (because '.' is lower than 'Z').
|
||||||
// The easiest fix is to simply remove the 'Z' for the internal
|
// The easiest fix is to simply remove the 'Z' for the internal
|
||||||
|
@ -80,8 +97,20 @@ public class DateField extends FieldType {
|
||||||
int len=val.length();
|
int len=val.length();
|
||||||
if (val.charAt(len-1)=='Z') {
|
if (val.charAt(len-1)=='Z') {
|
||||||
return val.substring(0,len-1);
|
return val.substring(0,len-1);
|
||||||
|
} else if (val.startsWith("NOW")) {
|
||||||
|
/* :TODO: let Locale/TimeZone come from init args for rounding only */
|
||||||
|
DateMathParser p = new DateMathParser(UTC, Locale.US);
|
||||||
|
try {
|
||||||
|
return toInternal(p.parseMath(val.substring(3)));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new SolrException(400,"Invalid Date Math String:'" +val+'\'',e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new SolrException(1,"Invalid Date String:'" +val+'\'');
|
throw new SolrException(400,"Invalid Date String:'" +val+'\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toInternal(Date val) {
|
||||||
|
return getThreadLocalDateFormat().format(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String indexedToReadable(String indexedForm) {
|
public String indexedToReadable(String indexedForm) {
|
||||||
|
@ -107,4 +136,32 @@ public class DateField extends FieldType {
|
||||||
public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException {
|
public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException {
|
||||||
writer.writeDate(name, toExternal(f));
|
writer.writeDate(name, toExternal(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a formatter that can be use by the current thread if needed to
|
||||||
|
* convert Date objects to the Internal representation.
|
||||||
|
*/
|
||||||
|
protected DateFormat getThreadLocalDateFormat() {
|
||||||
|
|
||||||
|
return fmtThreadLocal.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThreadLocalDateFormat fmtThreadLocal
|
||||||
|
= new ThreadLocalDateFormat();
|
||||||
|
|
||||||
|
private static class ThreadLocalDateFormat extends ThreadLocal<DateFormat> {
|
||||||
|
DateFormat proto;
|
||||||
|
public ThreadLocalDateFormat() {
|
||||||
|
super();
|
||||||
|
SimpleDateFormat tmp =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
|
||||||
|
tmp.setTimeZone(UTC);
|
||||||
|
proto = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DateFormat initialValue() {
|
||||||
|
return (DateFormat) proto.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.solr.util;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Simple Utility class for parsing "math" like strings relating to Dates.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The basic syntax support addition, subtraction and rounding at various
|
||||||
|
* levels of granularity (or "units"). Commands can be chained together
|
||||||
|
* and are parsed from left to right. '+' and '-' denote addition and
|
||||||
|
* subtraction, while '/' denotes "round". Round requires only a unit, while
|
||||||
|
* addition/subtraction require an integer value and a unit.
|
||||||
|
* Command strings must not include white space, but the "No-Op" command
|
||||||
|
* (empty string) is allowed....
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* /HOUR
|
||||||
|
* ... Round to the start of the current hour
|
||||||
|
* /DAY
|
||||||
|
* ... Round to the start of the current day
|
||||||
|
* +2YEARS
|
||||||
|
* ... Exactly two years in the future from now
|
||||||
|
* -1DAY
|
||||||
|
* ... Exactly 1 day prior to now
|
||||||
|
* /DAY+6MONTHS+3DAYS
|
||||||
|
* ... 6 months and 3 days in the future from the start of
|
||||||
|
* the current day
|
||||||
|
* +6MONTHS+3DAYS/DAY
|
||||||
|
* ... 6 months and 3 days in the future from now, rounded
|
||||||
|
* down to nearest day
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* All commands are relative to a "now" which is fixed in an instance of
|
||||||
|
* DateMathParser such that
|
||||||
|
* <code>p.parseMath("+0MILLISECOND").equals(p.parseMath("+0MILLISECOND"))</code>
|
||||||
|
* no matter how many wall clock milliseconds elapse between the two
|
||||||
|
* distinct calls to parse (Assuming no other thread calls
|
||||||
|
* "<code>setNow</code>" in the interim)
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Multiple aliases exist for the various units of time (ie:
|
||||||
|
* <code>MINUTE</code> and <code>MINUTES</code>; <code>MILLI</code>,
|
||||||
|
* <code>MILLIS</code>, <code>MILLISECOND</code>, and
|
||||||
|
* <code>MILLISECONDS</code>.) The complete list can be found by
|
||||||
|
* inspecting the keySet of <code>CALENDAR_UNITS</code>.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @version $Id:$
|
||||||
|
*/
|
||||||
|
public class DateMathParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping from (uppercased) String labels idenyifying time units,
|
||||||
|
* to the corresponding Calendar constant used to set/add/roll that unit
|
||||||
|
* of measurement.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A single logical unit of time might be represented by multiple labels
|
||||||
|
* for convenience (ie: <code>DATE==DAY</code>,
|
||||||
|
* <code>MILLI==MILLISECOND</code>)
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see Calendar
|
||||||
|
*/
|
||||||
|
public static final Map<String,Integer> CALENDAR_UNITS = makeUnitsMap();
|
||||||
|
|
||||||
|
/** @see #CALENDAR_UNITS */
|
||||||
|
private static Map<String,Integer> makeUnitsMap() {
|
||||||
|
|
||||||
|
// NOTE: consciously choosing not to support WEEK at this time,
|
||||||
|
// because of complexity in rounding down to the nearest week
|
||||||
|
// arround a month/year boundry.
|
||||||
|
// (Not to mention: it's not clear what people would *expect*)
|
||||||
|
|
||||||
|
Map<String,Integer> units = new HashMap<String,Integer>(13);
|
||||||
|
units.put("YEAR", Calendar.YEAR);
|
||||||
|
units.put("YEARS", Calendar.YEAR);
|
||||||
|
units.put("MONTH", Calendar.MONTH);
|
||||||
|
units.put("MONTHS", Calendar.MONTH);
|
||||||
|
units.put("DAY", Calendar.DATE);
|
||||||
|
units.put("DAYS", Calendar.DATE);
|
||||||
|
units.put("DATE", Calendar.DATE);
|
||||||
|
units.put("HOUR", Calendar.HOUR_OF_DAY);
|
||||||
|
units.put("HOURS", Calendar.HOUR_OF_DAY);
|
||||||
|
units.put("MINUTE", Calendar.MINUTE);
|
||||||
|
units.put("MINUTES", Calendar.MINUTE);
|
||||||
|
units.put("SECOND", Calendar.SECOND);
|
||||||
|
units.put("SECONDS", Calendar.SECOND);
|
||||||
|
units.put("MILLI", Calendar.MILLISECOND);
|
||||||
|
units.put("MILLIS", Calendar.MILLISECOND);
|
||||||
|
units.put("MILLISECOND", Calendar.MILLISECOND);
|
||||||
|
units.put("MILLISECONDS",Calendar.MILLISECOND);
|
||||||
|
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the specified Calendar by "adding" the specified value of units
|
||||||
|
*
|
||||||
|
* @exception IllegalArgumentException if unit isn't recognized.
|
||||||
|
* @see #CALENDAR_UNITS
|
||||||
|
*/
|
||||||
|
public static void add(Calendar c, int val, String unit) {
|
||||||
|
Integer uu = CALENDAR_UNITS.get(unit);
|
||||||
|
if (null == uu) {
|
||||||
|
throw new IllegalArgumentException("Adding Unit not recognized: "
|
||||||
|
+ unit);
|
||||||
|
}
|
||||||
|
c.add(uu.intValue(), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the specified Calendar by "rounding" down to the specified unit
|
||||||
|
*
|
||||||
|
* @exception IllegalArgumentException if unit isn't recognized.
|
||||||
|
* @see #CALENDAR_UNITS
|
||||||
|
*/
|
||||||
|
public static void round(Calendar c, String unit) {
|
||||||
|
Integer uu = CALENDAR_UNITS.get(unit);
|
||||||
|
if (null == uu) {
|
||||||
|
throw new IllegalArgumentException("Rounding Unit not recognized: "
|
||||||
|
+ unit);
|
||||||
|
}
|
||||||
|
int u = uu.intValue();
|
||||||
|
|
||||||
|
switch (u) {
|
||||||
|
|
||||||
|
case Calendar.YEAR:
|
||||||
|
c.clear(Calendar.MONTH);
|
||||||
|
/* fall through */
|
||||||
|
case Calendar.MONTH:
|
||||||
|
c.clear(Calendar.DAY_OF_MONTH);
|
||||||
|
c.clear(Calendar.DAY_OF_WEEK);
|
||||||
|
c.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
|
||||||
|
c.clear(Calendar.DAY_OF_YEAR);
|
||||||
|
c.clear(Calendar.WEEK_OF_MONTH);
|
||||||
|
c.clear(Calendar.WEEK_OF_YEAR);
|
||||||
|
/* fall through */
|
||||||
|
case Calendar.DATE:
|
||||||
|
c.clear(Calendar.HOUR_OF_DAY);
|
||||||
|
c.clear(Calendar.HOUR);
|
||||||
|
c.clear(Calendar.AM_PM);
|
||||||
|
/* fall through */
|
||||||
|
case Calendar.HOUR_OF_DAY:
|
||||||
|
c.clear(Calendar.MINUTE);
|
||||||
|
/* fall through */
|
||||||
|
case Calendar.MINUTE:
|
||||||
|
c.clear(Calendar.SECOND);
|
||||||
|
/* fall through */
|
||||||
|
case Calendar.SECOND:
|
||||||
|
c.clear(Calendar.MILLISECOND);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException
|
||||||
|
("No logic for rounding value ("+u+") " + unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private TimeZone zone;
|
||||||
|
private Locale loc;
|
||||||
|
private Date now;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tz The TimeZone used for rounding (to determine when hours/days begin)
|
||||||
|
* @param l The Locale used for rounding (to determine when weeks begin)
|
||||||
|
* @see Calendar#getInstance(TimeZone,Locale)
|
||||||
|
*/
|
||||||
|
public DateMathParser(TimeZone tz, Locale l) {
|
||||||
|
zone = tz;
|
||||||
|
loc = l;
|
||||||
|
setNow(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Redefines this instance's concept of "now" */
|
||||||
|
public void setNow(Date n) {
|
||||||
|
now = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a cloned of this instance's concept of "now" */
|
||||||
|
public Date getNow() {
|
||||||
|
return (Date) now.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string of commands relative "now" are returns the resulting Date.
|
||||||
|
*
|
||||||
|
* @exception ParseException positions in ParseExceptions are token positions, not character positions.
|
||||||
|
*/
|
||||||
|
public Date parseMath(String math) throws ParseException {
|
||||||
|
|
||||||
|
Calendar cal = Calendar.getInstance(zone, loc);
|
||||||
|
cal.setTime(getNow());
|
||||||
|
|
||||||
|
/* check for No-Op */
|
||||||
|
if (0==math.length()) {
|
||||||
|
return cal.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] ops = splitter.split(math);
|
||||||
|
int pos = 0;
|
||||||
|
while ( pos < ops.length ) {
|
||||||
|
|
||||||
|
if (1 != ops[pos].length()) {
|
||||||
|
throw new ParseException
|
||||||
|
("Multi character command found: \"" + ops[pos] + "\"", pos);
|
||||||
|
}
|
||||||
|
char command = ops[pos++].charAt(0);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case '/':
|
||||||
|
if (ops.length < pos + 1) {
|
||||||
|
throw new ParseException
|
||||||
|
("Need a unit after command: \"" + command + "\"", pos);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
round(cal, ops[pos++]);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ParseException
|
||||||
|
("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '+': /* fall through */
|
||||||
|
case '-':
|
||||||
|
if (ops.length < pos + 2) {
|
||||||
|
throw new ParseException
|
||||||
|
("Need a value and unit for command: \"" + command + "\"", pos);
|
||||||
|
}
|
||||||
|
int val = 0;
|
||||||
|
try {
|
||||||
|
val = Integer.valueOf(ops[pos++]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParseException
|
||||||
|
("Not a Number: \"" + ops[pos-1] + "\"", pos-1);
|
||||||
|
}
|
||||||
|
if ('-' == command) {
|
||||||
|
val = 0 - val;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String unit = ops[pos++];
|
||||||
|
add(cal, val, unit);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ParseException
|
||||||
|
("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ParseException
|
||||||
|
("Unrecognized command: \"" + command + "\"", pos-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cal.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern splitter = Pattern.compile("\\b|(?<=\\d)(?=\\D)");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import org.apache.solr.util.*;
|
||||||
import org.apache.solr.schema.*;
|
import org.apache.solr.schema.*;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -568,7 +567,42 @@ public class BasicFunctionalityTest extends AbstractSolrTestCase {
|
||||||
assertTrue(luf.isStored());
|
assertTrue(luf.isStored());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @see org.apache.solr.util.DateMathParserTest */
|
||||||
|
public void testDateMath() {
|
||||||
|
|
||||||
|
// testing everything from query level is hard because
|
||||||
|
// time marches on ... and there is no easy way to reach into the
|
||||||
|
// bowels of DateField and muck with the definition of "now"
|
||||||
|
// ...
|
||||||
|
// BUT: we can test that crazy combinations of "NOW" all work correctly,
|
||||||
|
// assuming the test doesn't take too long to run...
|
||||||
|
|
||||||
|
assertU(adoc("id", "1", "bday", "1976-07-04T12:08:56.235Z"));
|
||||||
|
assertU(adoc("id", "2", "bday", "NOW"));
|
||||||
|
assertU(adoc("id", "3", "bday", "NOW/HOUR"));
|
||||||
|
assertU(adoc("id", "4", "bday", "NOW-30MINUTES"));
|
||||||
|
assertU(adoc("id", "5", "bday", "NOW+30MINUTES"));
|
||||||
|
assertU(adoc("id", "6", "bday", "NOW+2YEARS"));
|
||||||
|
assertU(commit());
|
||||||
|
|
||||||
|
assertQ("check count for before now",
|
||||||
|
req("q", "bday:[* TO NOW]"), "*[count(//doc)=4]");
|
||||||
|
|
||||||
|
assertQ("check count for after now",
|
||||||
|
req("q", "bday:[NOW TO *]"), "*[count(//doc)=2]");
|
||||||
|
|
||||||
|
assertQ("check count for old stuff",
|
||||||
|
req("q", "bday:[* TO NOW-2YEARS]"), "*[count(//doc)=1]");
|
||||||
|
|
||||||
|
assertQ("check count for future stuff",
|
||||||
|
req("q", "bday:[NOW+1MONTH TO *]"), "*[count(//doc)=1]");
|
||||||
|
|
||||||
|
assertQ("check count for near stuff",
|
||||||
|
req("q", "bday:[NOW-1MONTH TO NOW+2HOURS]"), "*[count(//doc)=4]");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// /** this doesn't work, but if it did, this is how we'd test it. */
|
// /** this doesn't work, but if it did, this is how we'd test it. */
|
||||||
// public void testOverwriteFalse() {
|
// public void testOverwriteFalse() {
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.solr.util;
|
||||||
|
|
||||||
|
import org.apache.solr.util.DateMathParser;
|
||||||
|
|
||||||
|
import junit.framework.Test;
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import junit.framework.TestSuite;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.text.ParseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the functions in DateMathParser
|
||||||
|
*/
|
||||||
|
public class DateMathParserTest extends TestCase {
|
||||||
|
|
||||||
|
public static TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A formatter for specifying every last nuance of a Date for easy
|
||||||
|
* refernece in assertion statements
|
||||||
|
*/
|
||||||
|
private DateFormat fmt;
|
||||||
|
/**
|
||||||
|
* A parser for reading in explicit dates that are convinient to type
|
||||||
|
* in a test
|
||||||
|
*/
|
||||||
|
private DateFormat parser;
|
||||||
|
|
||||||
|
public DateMathParserTest() {
|
||||||
|
super();
|
||||||
|
fmt = new SimpleDateFormat
|
||||||
|
("G yyyyy MM ww WW DD dd F E aa HH hh mm ss SSS z Z",Locale.US);
|
||||||
|
fmt.setTimeZone(UTC);
|
||||||
|
|
||||||
|
parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",Locale.US);
|
||||||
|
parser.setTimeZone(UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MACRO: Round: parses s, rounds with u, fmts */
|
||||||
|
protected String r(String s, String u) throws Exception {
|
||||||
|
Date d = parser.parse(s);
|
||||||
|
Calendar c = Calendar.getInstance(UTC, Locale.US);
|
||||||
|
c.setTime(d);
|
||||||
|
DateMathParser.round(c, u);
|
||||||
|
return fmt.format(c.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MACRO: Add: parses s, adds v u, fmts */
|
||||||
|
protected String a(String s, int v, String u) throws Exception {
|
||||||
|
Date d = parser.parse(s);
|
||||||
|
Calendar c = Calendar.getInstance(UTC, Locale.US);
|
||||||
|
c.setTime(d);
|
||||||
|
DateMathParser.add(c, v, u);
|
||||||
|
return fmt.format(c.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MACRO: Expected: parses s, fmts */
|
||||||
|
protected String e(String s) throws Exception {
|
||||||
|
return fmt.format(parser.parse(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertRound(String e, String i, String u) throws Exception {
|
||||||
|
String ee = e(e);
|
||||||
|
String rr = r(i,u);
|
||||||
|
assertEquals(ee + " != " + rr + " round:" + i + ":" + u, ee, rr);
|
||||||
|
}
|
||||||
|
protected void assertAdd(String e, String i, int v, String u)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
String ee = e(e);
|
||||||
|
String aa = a(i,v,u);
|
||||||
|
assertEquals(ee + " != " + aa + " add:" + i + "+" + v + ":" + u, ee, aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertMath(String e, DateMathParser p, String i)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
String ee = e(e);
|
||||||
|
String aa = fmt.format(p.parseMath(i));
|
||||||
|
assertEquals(ee + " != " + aa + " math:" +
|
||||||
|
parser.format(p.getNow()) + ":" + i, ee, aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCalendarUnitsConsistency() throws Exception {
|
||||||
|
String input = "2001-07-04T12:08:56.235";
|
||||||
|
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
|
||||||
|
try {
|
||||||
|
r(input, u);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertNotNull("no logic for rounding: " + u, e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
a(input, 1, u);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertNotNull("no logic for rounding: " + u, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testRound() throws Exception {
|
||||||
|
|
||||||
|
String input = "2001-07-04T12:08:56.235";
|
||||||
|
|
||||||
|
assertRound("2001-07-04T12:08:56.000", input, "SECOND");
|
||||||
|
assertRound("2001-07-04T12:08:00.000", input, "MINUTE");
|
||||||
|
assertRound("2001-07-04T12:00:00.000", input, "HOUR");
|
||||||
|
assertRound("2001-07-04T00:00:00.000", input, "DAY");
|
||||||
|
assertRound("2001-07-01T00:00:00.000", input, "MONTH");
|
||||||
|
assertRound("2001-01-01T00:00:00.000", input, "YEAR");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAddZero() throws Exception {
|
||||||
|
|
||||||
|
String input = "2001-07-04T12:08:56.235";
|
||||||
|
|
||||||
|
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
|
||||||
|
assertAdd(input, input, 0, u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testAdd() throws Exception {
|
||||||
|
|
||||||
|
String input = "2001-07-04T12:08:56.235";
|
||||||
|
|
||||||
|
assertAdd("2001-07-04T12:08:56.236", input, 1, "MILLISECOND");
|
||||||
|
assertAdd("2001-07-04T12:08:57.235", input, 1, "SECOND");
|
||||||
|
assertAdd("2001-07-04T12:09:56.235", input, 1, "MINUTE");
|
||||||
|
assertAdd("2001-07-04T13:08:56.235", input, 1, "HOUR");
|
||||||
|
assertAdd("2001-07-05T12:08:56.235", input, 1, "DAY");
|
||||||
|
assertAdd("2001-08-04T12:08:56.235", input, 1, "MONTH");
|
||||||
|
assertAdd("2002-07-04T12:08:56.235", input, 1, "YEAR");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseStatelessness() throws Exception {
|
||||||
|
|
||||||
|
DateMathParser p = new DateMathParser(UTC, Locale.US);
|
||||||
|
p.setNow(parser.parse("2001-07-04T12:08:56.235"));
|
||||||
|
|
||||||
|
String e = fmt.format(p.parseMath(""));
|
||||||
|
|
||||||
|
Date trash = p.parseMath("+7YEARS");
|
||||||
|
trash = p.parseMath("/MONTH");
|
||||||
|
trash = p.parseMath("-5DAYS+20MINUTES");
|
||||||
|
Thread.currentThread().sleep(5);
|
||||||
|
|
||||||
|
String a = fmt.format(p.parseMath(""));
|
||||||
|
assertEquals("State of DateMathParser changed", e, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseMath() throws Exception {
|
||||||
|
|
||||||
|
DateMathParser p = new DateMathParser(UTC, Locale.US);
|
||||||
|
p.setNow(parser.parse("2001-07-04T12:08:56.235"));
|
||||||
|
|
||||||
|
// No-Op
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "");
|
||||||
|
|
||||||
|
// simple round
|
||||||
|
assertMath("2001-07-04T12:08:56.000", p, "/SECOND");
|
||||||
|
assertMath("2001-07-04T12:08:00.000", p, "/MINUTE");
|
||||||
|
assertMath("2001-07-04T12:00:00.000", p, "/HOUR");
|
||||||
|
assertMath("2001-07-04T00:00:00.000", p, "/DAY");
|
||||||
|
assertMath("2001-07-01T00:00:00.000", p, "/MONTH");
|
||||||
|
assertMath("2001-01-01T00:00:00.000", p, "/YEAR");
|
||||||
|
|
||||||
|
// simple addition
|
||||||
|
assertMath("2001-07-04T12:08:56.236", p, "+1MILLISECOND");
|
||||||
|
assertMath("2001-07-04T12:08:57.235", p, "+1SECOND");
|
||||||
|
assertMath("2001-07-04T12:09:56.235", p, "+1MINUTE");
|
||||||
|
assertMath("2001-07-04T13:08:56.235", p, "+1HOUR");
|
||||||
|
assertMath("2001-07-05T12:08:56.235", p, "+1DAY");
|
||||||
|
assertMath("2001-08-04T12:08:56.235", p, "+1MONTH");
|
||||||
|
assertMath("2002-07-04T12:08:56.235", p, "+1YEAR");
|
||||||
|
|
||||||
|
// simple subtraction
|
||||||
|
assertMath("2001-07-04T12:08:56.234", p, "-1MILLISECOND");
|
||||||
|
assertMath("2001-07-04T12:08:55.235", p, "-1SECOND");
|
||||||
|
assertMath("2001-07-04T12:07:56.235", p, "-1MINUTE");
|
||||||
|
assertMath("2001-07-04T11:08:56.235", p, "-1HOUR");
|
||||||
|
assertMath("2001-07-03T12:08:56.235", p, "-1DAY");
|
||||||
|
assertMath("2001-06-04T12:08:56.235", p, "-1MONTH");
|
||||||
|
assertMath("2000-07-04T12:08:56.235", p, "-1YEAR");
|
||||||
|
|
||||||
|
// simple '+/-'
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1MILLISECOND-1MILLISECOND");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1SECOND-1SECOND");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1MINUTE-1MINUTE");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1HOUR-1HOUR");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1DAY-1DAY");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1MONTH-1MONTH");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "+1YEAR-1YEAR");
|
||||||
|
|
||||||
|
// simple '-/+'
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1MILLISECOND+1MILLISECOND");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1SECOND+1SECOND");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1MINUTE+1MINUTE");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1HOUR+1HOUR");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1DAY+1DAY");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1MONTH+1MONTH");
|
||||||
|
assertMath("2001-07-04T12:08:56.235", p, "-1YEAR+1YEAR");
|
||||||
|
|
||||||
|
// more complex stuff
|
||||||
|
assertMath("2000-07-04T12:08:56.236", p, "+1MILLISECOND-1YEAR");
|
||||||
|
assertMath("2000-07-04T12:08:57.235", p, "+1SECOND-1YEAR");
|
||||||
|
assertMath("2000-07-04T12:09:56.235", p, "+1MINUTE-1YEAR");
|
||||||
|
assertMath("2000-07-04T13:08:56.235", p, "+1HOUR-1YEAR");
|
||||||
|
assertMath("2000-07-05T12:08:56.235", p, "+1DAY-1YEAR");
|
||||||
|
assertMath("2000-08-04T12:08:56.235", p, "+1MONTH-1YEAR");
|
||||||
|
assertMath("2000-07-04T12:08:56.236", p, "-1YEAR+1MILLISECOND");
|
||||||
|
assertMath("2000-07-04T12:08:57.235", p, "-1YEAR+1SECOND");
|
||||||
|
assertMath("2000-07-04T12:09:56.235", p, "-1YEAR+1MINUTE");
|
||||||
|
assertMath("2000-07-04T13:08:56.235", p, "-1YEAR+1HOUR");
|
||||||
|
assertMath("2000-07-05T12:08:56.235", p, "-1YEAR+1DAY");
|
||||||
|
assertMath("2000-08-04T12:08:56.235", p, "-1YEAR+1MONTH");
|
||||||
|
assertMath("2000-07-01T00:00:00.000", p, "-1YEAR+1MILLISECOND/MONTH");
|
||||||
|
assertMath("2000-07-04T00:00:00.000", p, "-1YEAR+1SECOND/DAY");
|
||||||
|
assertMath("2000-07-04T00:00:00.000", p, "-1YEAR+1MINUTE/DAY");
|
||||||
|
assertMath("2000-07-04T13:00:00.000", p, "-1YEAR+1HOUR/HOUR");
|
||||||
|
assertMath("2000-07-05T12:08:56.000", p, "-1YEAR+1DAY/SECOND");
|
||||||
|
assertMath("2000-08-04T12:08:56.000", p, "-1YEAR+1MONTH/SECOND");
|
||||||
|
|
||||||
|
// "tricky" cases
|
||||||
|
p.setNow(parser.parse("2006-01-31T17:09:59.999"));
|
||||||
|
assertMath("2006-02-28T17:09:59.999", p, "+1MONTH");
|
||||||
|
assertMath("2008-02-29T17:09:59.999", p, "+25MONTH");
|
||||||
|
assertMath("2006-02-01T00:00:00.000", p, "/MONTH+35DAYS/MONTH");
|
||||||
|
assertMath("2006-01-31T17:10:00.000", p, "+3MILLIS/MINUTE");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseMathExceptions() throws Exception {
|
||||||
|
|
||||||
|
DateMathParser p = new DateMathParser(UTC, Locale.US);
|
||||||
|
p.setNow(parser.parse("2001-07-04T12:08:56.235"));
|
||||||
|
|
||||||
|
Map<String,Integer> badCommands = new HashMap<String,Integer>();
|
||||||
|
badCommands.put("/", 1);
|
||||||
|
badCommands.put("+", 1);
|
||||||
|
badCommands.put("-", 1);
|
||||||
|
badCommands.put("/BOB", 1);
|
||||||
|
badCommands.put("+SECOND", 1);
|
||||||
|
badCommands.put("-2MILLI/", 4);
|
||||||
|
badCommands.put(" +BOB", 0);
|
||||||
|
badCommands.put("+2SECONDS ", 3);
|
||||||
|
badCommands.put("/4", 1);
|
||||||
|
badCommands.put("?SECONDS", 0);
|
||||||
|
|
||||||
|
for (String command : badCommands.keySet()) {
|
||||||
|
try {
|
||||||
|
Date out = p.parseMath(command);
|
||||||
|
fail("Didn't generate ParseException for: " + command);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
assertEquals("Wrong pos for: " + command + " => " + e.getMessage(),
|
||||||
|
badCommands.get(command).intValue(), e.getErrorOffset());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue