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&lt;-1 5&lt;-2 6&lt;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