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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,16 @@ import org.apache.lucene.document.Fieldable;
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 @@ import java.io.IOException;
* 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 @@ public class DateField extends FieldType {
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 class DateField extends FieldType {
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();
}
}
}

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.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import java.io.IOException;
@ -568,7 +567,42 @@ public class BasicFunctionalityTest extends AbstractSolrTestCase {
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() {

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