You are viewing a plain text version of this content. The canonical link for it is here.
Posted to solr-commits@lucene.apache.org by ho...@apache.org on 2006/11/21 02:55:06 UTC
svn commit: r477465 - in /incubator/solr/trunk: ./ example/exampledocs/
example/solr/conf/ src/java/org/apache/solr/schema/
src/java/org/apache/solr/util/ src/test/org/apache/solr/
src/test/org/apache/solr/util/
Author: hossman
Date: Mon Nov 20 17:55:05 2006
New Revision: 477465
URL: http://svn.apache.org/viewvc?view=rev&rev=477465
Log:
SOLR-71: Date Math for DateField
Added:
incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java (with props)
incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java (with props)
Modified:
incubator/solr/trunk/CHANGES.txt
incubator/solr/trunk/example/exampledocs/solr.xml
incubator/solr/trunk/example/solr/conf/schema.xml
incubator/solr/trunk/example/solr/conf/solrconfig.xml
incubator/solr/trunk/src/java/org/apache/solr/schema/DateField.java
incubator/solr/trunk/src/test/org/apache/solr/BasicFunctionalityTest.java
Modified: incubator/solr/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/CHANGES.txt?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/CHANGES.txt (original)
+++ incubator/solr/trunk/CHANGES.txt Mon Nov 20 17:55:05 2006
@@ -65,7 +65,10 @@
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
(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
1. classes reorganized into different packages, package names changed to Apache
2. force read of document stored fields in QuerySenderListener
Modified: incubator/solr/trunk/example/exampledocs/solr.xml
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/example/exampledocs/solr.xml?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/example/exampledocs/solr.xml (original)
+++ incubator/solr/trunk/example/exampledocs/solr.xml Mon Nov 20 17:55:05 2006
@@ -32,6 +32,7 @@
<field name="price">0</field>
<field name="popularity">10</field>
<field name="inStock">true</field>
+ <field name="incubationdate_dt">2006-01-17T00:00:00.000Z</field>
</doc>
</add>
Modified: incubator/solr/trunk/example/solr/conf/schema.xml
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/example/solr/conf/schema.xml?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/example/solr/conf/schema.xml (original)
+++ incubator/solr/trunk/example/solr/conf/schema.xml Mon Nov 20 17:55:05 2006
@@ -83,11 +83,25 @@
<!-- 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
The trailing "Z" designates UTC time and is mandatory.
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"/>
<!-- solr.TextField allows the specification of custom text analyzers
Modified: incubator/solr/trunk/example/solr/conf/solrconfig.xml
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/example/solr/conf/solrconfig.xml?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/example/solr/conf/solrconfig.xml (original)
+++ incubator/solr/trunk/example/solr/conf/solrconfig.xml Mon Nov 20 17:55:05 2006
@@ -250,6 +250,10 @@
<lst name="defaults">
<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>
+ <!-- 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>
<!-- In addition to defaults, "appends" params can be specified
to identify values which should be appended to the list of
Modified: incubator/solr/trunk/src/java/org/apache/solr/schema/DateField.java
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/src/java/org/apache/solr/schema/DateField.java?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/src/java/org/apache/solr/schema/DateField.java (original)
+++ incubator/solr/trunk/src/java/org/apache/solr/schema/DateField.java Mon Nov 20 17:55:05 2006
@@ -25,9 +25,16 @@
import org.apache.lucene.search.SortField;
import org.apache.solr.search.function.ValueSource;
import org.apache.solr.search.function.OrdFieldSource;
-
+import org.apache.solr.util.DateMathParser;
+
import java.util.Map;
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
// formats, better for human entered dates.
@@ -62,12 +69,22 @@
* acronym UTC was chosen as a compromise."
* </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
* @version $Id$
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML schema part 2</a>
+ *
*/
public class DateField extends FieldType {
+ public static TimeZone UTC = TimeZone.getTimeZone("UTC");
+
// The XML (external) date format will sort correctly, except if
// fractions of seconds are present (because '.' is lower than 'Z').
// The easiest fix is to simply remove the 'Z' for the internal
@@ -80,8 +97,20 @@
int len=val.length();
if (val.charAt(len-1)=='Z') {
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) {
@@ -107,4 +136,32 @@
public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException {
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();
+ }
+ }
+
}
Added: incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java?view=auto&rev=477465
==============================================================================
--- incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java (added)
+++ incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java Mon Nov 20 17:55:05 2006
@@ -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)");
+
+}
+
Propchange: incubator/solr/trunk/src/java/org/apache/solr/util/DateMathParser.java
------------------------------------------------------------------------------
svn:eol-style = native
Modified: incubator/solr/trunk/src/test/org/apache/solr/BasicFunctionalityTest.java
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/src/test/org/apache/solr/BasicFunctionalityTest.java?view=diff&rev=477465&r1=477464&r2=477465
==============================================================================
--- incubator/solr/trunk/src/test/org/apache/solr/BasicFunctionalityTest.java (original)
+++ incubator/solr/trunk/src/test/org/apache/solr/BasicFunctionalityTest.java Mon Nov 20 17:55:05 2006
@@ -26,7 +26,6 @@
import org.apache.solr.schema.*;
import org.w3c.dom.Document;
-
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import java.io.IOException;
@@ -568,7 +567,42 @@
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. */
// public void testOverwriteFalse() {
Added: incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java
URL: http://svn.apache.org/viewvc/incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java?view=auto&rev=477465
==============================================================================
--- incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java (added)
+++ incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java Mon Nov 20 17:55:05 2006
@@ -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());
+
+ }
+ }
+
+ }
+
+}
+
Propchange: incubator/solr/trunk/src/test/org/apache/solr/util/DateMathParserTest.java
------------------------------------------------------------------------------
svn:eol-style = native