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:
Chris M. Hostetter 2006-11-21 01:55:05 +00:00
parent d68fbb7bea
commit 57653c5490
8 changed files with 702 additions and 7 deletions

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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&lt;-1 5&lt;-2 6&lt;90%</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> </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

View File

@ -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();
}
}
} }

View File

@ -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)");
}

View File

@ -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() {

View File

@ -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());
}
}
}
}