SOLR-9080 SOLR-9085: Fix date math before the year 1582.

note: DateMathParser no longer needs a Locale
This commit is contained in:
David Smiley 2016-05-13 10:03:24 -04:00
parent 927454b8a2
commit 4193e60b9f
13 changed files with 313 additions and 285 deletions

View File

@ -47,6 +47,11 @@ Optimizations
================== 6.1.0 ================== ================== 6.1.0 ==================
Upgrading from Solr any prior release
----------------------
* If you use historical dates, specifically on or before the year 1582, you should re-index.
Detailed Change List Detailed Change List
---------------------- ----------------------
@ -206,6 +211,10 @@ Bug Fixes
* SOLR-8970: Change SSLTestConfig to use a keystore file that is included as a resource in the * SOLR-8970: Change SSLTestConfig to use a keystore file that is included as a resource in the
test-framework jar so users subclassing SolrTestCaseJ4 don't need to preserve magic paths (hossman) test-framework jar so users subclassing SolrTestCaseJ4 don't need to preserve magic paths (hossman)
* SOLR-9080, SOLR-9085: (6.0 bug) For years <= 1582, date math (round,add,sub) introduced error. Range faceting
on such dates was also affected. With this fixed, this is the first release range faceting works on BC years.
(David Smiley)
Optimizations Optimizations
---------------------- ----------------------
* SOLR-8722: Don't force a full ZkStateReader refresh on every Overseer operation. * SOLR-8722: Don't force a full ZkStateReader refresh on every Overseer operation.

View File

@ -54,7 +54,7 @@ public class DataImportHandlerException extends RuntimeException {
return errCode; return errCode;
} }
public static void wrapAndThrow(int err, Exception e) { public static DataImportHandlerException wrapAndThrow(int err, Exception e) {
if (e instanceof DataImportHandlerException) { if (e instanceof DataImportHandlerException) {
throw (DataImportHandlerException) e; throw (DataImportHandlerException) e;
} else { } else {
@ -62,7 +62,7 @@ public class DataImportHandlerException extends RuntimeException {
} }
} }
public static void wrapAndThrow(int err, Exception e, String msg) { public static DataImportHandlerException wrapAndThrow(int err, Exception e, String msg) {
if (e instanceof DataImportHandlerException) { if (e instanceof DataImportHandlerException) {
throw (DataImportHandlerException) e; throw (DataImportHandlerException) e;
} else { } else {

View File

@ -16,9 +16,6 @@
*/ */
package org.apache.solr.handler.dataimport; package org.apache.solr.handler.dataimport;
import static org.apache.solr.handler.dataimport.DataImportHandlerException.SEVERE;
import static org.apache.solr.handler.dataimport.DataImportHandlerException.wrapAndThrow;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -35,6 +32,9 @@ import org.apache.solr.common.util.SuppressForbidden;
import org.apache.solr.handler.dataimport.config.EntityField; import org.apache.solr.handler.dataimport.config.EntityField;
import org.apache.solr.util.DateMathParser; import org.apache.solr.util.DateMathParser;
import static org.apache.solr.handler.dataimport.DataImportHandlerException.SEVERE;
import static org.apache.solr.handler.dataimport.DataImportHandlerException.wrapAndThrow;
/** /**
* <p>Formats values using a given date format. </p> * <p>Formats values using a given date format. </p>
* <p>Pass three parameters: * <p>Pass three parameters:
@ -99,7 +99,7 @@ public class DateFormatEvaluator extends Evaluator {
throw new DataImportHandlerException(SEVERE, "Malformed / non-existent locale: " + localeStr, ex); throw new DataImportHandlerException(SEVERE, "Malformed / non-existent locale: " + localeStr, ex);
} }
} }
TimeZone tz = TimeZone.getDefault(); TimeZone tz = TimeZone.getDefault(); // DWS TODO: is this the right default for us? Deserves explanation if so.
if(l.size()==4) { if(l.size()==4) {
Object tzObj = l.get(3); Object tzObj = l.get(3);
String tzStr = null; String tzStr = null;
@ -153,24 +153,19 @@ public class DateFormatEvaluator extends Evaluator {
* @return the result of evaluating a string * @return the result of evaluating a string
*/ */
protected Date evaluateString(String datemathfmt, Locale locale, TimeZone tz) { protected Date evaluateString(String datemathfmt, Locale locale, TimeZone tz) {
Date date = null; // note: DMP does not use the locale but perhaps a subclass might use it, for e.g. parsing a date in a custom
datemathfmt = datemathfmt.replaceAll("NOW", ""); // string that doesn't necessarily have date math?
//TODO refactor DateMathParser.parseMath a bit to have a static method for this logic.
if (datemathfmt.startsWith("NOW")) {
datemathfmt = datemathfmt.substring("NOW".length());
}
try { try {
DateMathParser parser = getDateMathParser(locale, tz); DateMathParser parser = new DateMathParser(tz);
date = parseMathString(parser,datemathfmt); parser.setNow(new Date());// thus do *not* use SolrRequestInfo
} catch (ParseException e) {
wrapAndThrow(SEVERE, e, "Invalid expression for date");
}
return date;
}
/**
* NOTE: declared as a method to allow for extensibility
* @lucene.experimental
* @return the result of resolving the variable wrapper
*/
protected Date parseMathString(DateMathParser parser, String datemathfmt) throws ParseException {
return parser.parseMath(datemathfmt); return parser.parseMath(datemathfmt);
} catch (ParseException e) {
throw wrapAndThrow(SEVERE, e, "Invalid expression for date");
}
} }
/** /**
@ -182,16 +177,4 @@ public class DateFormatEvaluator extends Evaluator {
return variableWrapper.resolve(); return variableWrapper.resolve();
} }
/**
* @lucene.experimental
* @return a DateMathParser
*/
protected DateMathParser getDateMathParser(Locale l, TimeZone tz) {
return new DateMathParser(tz, l) {
@Override
public Date getNow() {
return new Date();
}
};
}
} }

View File

@ -20,7 +20,13 @@ import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -153,10 +159,14 @@ public class FileListEntityProcessor extends EntityProcessorBase {
} }
m = Evaluator.IN_SINGLE_QUOTES.matcher(dateStr); m = Evaluator.IN_SINGLE_QUOTES.matcher(dateStr);
if (m.find()) { if (m.find()) {
String expr = null; String expr = m.group(1);
expr = m.group(1).replaceAll("NOW", ""); //TODO refactor DateMathParser.parseMath a bit to have a static method for this logic.
if (expr.startsWith("NOW")) {
expr = expr.substring("NOW".length());
}
try { try {
return new DateMathParser(TimeZone.getDefault(), Locale.ROOT).parseMath(expr); // DWS TODO: is this TimeZone the right default for us? Deserves explanation if so.
return new DateMathParser(TimeZone.getDefault()).parseMath(expr);
} catch (ParseException exp) { } catch (ParseException exp) {
throw new DataImportHandlerException(DataImportHandlerException.SEVERE, throw new DataImportHandlerException(DataImportHandlerException.SEVERE,
"Invalid expression for date", exp); "Invalid expression for date", exp);

View File

@ -16,12 +16,19 @@
*/ */
package org.apache.solr.handler.dataimport; package org.apache.solr.handler.dataimport;
import org.junit.Ignore;
import org.junit.Test;
import org.apache.solr.util.DateMathParser;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import org.apache.solr.util.DateMathParser;
import org.junit.Test;
/** /**
* <p> * <p>
@ -103,7 +110,7 @@ public class TestVariableResolver extends AbstractDataImportHandlerTestCase {
.<Map<String,String>> emptyList())); .<Map<String,String>> emptyList()));
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
format.setTimeZone(TimeZone.getTimeZone("UTC")); format.setTimeZone(TimeZone.getTimeZone("UTC"));
DateMathParser dmp = new DateMathParser(TimeZone.getDefault(), Locale.ROOT); DateMathParser dmp = new DateMathParser(TimeZone.getDefault());
String s = vri String s = vri
.replaceTokens("${dataimporter.functions.formatDate('NOW/DAY','yyyy-MM-dd HH:mm')}"); .replaceTokens("${dataimporter.functions.formatDate('NOW/DAY','yyyy-MM-dd HH:mm')}");
@ -144,7 +151,7 @@ public class TestVariableResolver extends AbstractDataImportHandlerTestCase {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
format.setTimeZone(TimeZone.getTimeZone("UTC")); format.setTimeZone(TimeZone.getTimeZone("UTC"));
DateMathParser dmp = new DateMathParser(TimeZone.getDefault(), Locale.ROOT); DateMathParser dmp = new DateMathParser(TimeZone.getDefault());
String s = resolver String s = resolver
.replaceTokens("${dataimporter.functions.formatDate('NOW/DAY','yyyy-MM-dd HH:mm')}"); .replaceTokens("${dataimporter.functions.formatDate('NOW/DAY','yyyy-MM-dd HH:mm')}");

View File

@ -20,7 +20,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexDeletionPolicy; import org.apache.lucene.index.IndexDeletionPolicy;
@ -174,7 +173,7 @@ public class SolrDeletionPolicy extends IndexDeletionPolicy implements NamedList
try { try {
if (maxCommitAge != null) { if (maxCommitAge != null) {
if (maxCommitAgeTimeStamp==-1) { if (maxCommitAgeTimeStamp==-1) {
DateMathParser dmp = new DateMathParser(DateMathParser.UTC, Locale.ROOT); DateMathParser dmp = new DateMathParser(DateMathParser.UTC);
maxCommitAgeTimeStamp = dmp.parseMath(maxCommitAge).getTime(); maxCommitAgeTimeStamp = dmp.parseMath(maxCommitAge).getTime();
} }
if (IndexDeletionPolicyWrapper.getCommitTimestamp(commit) < maxCommitAgeTimeStamp) { if (IndexDeletionPolicyWrapper.getCommitTimestamp(commit) < maxCommitAgeTimeStamp) {

View File

@ -39,16 +39,18 @@ import org.apache.solr.util.DateMathParser;
import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.Shape;
/** /**
* A field for indexed dates and date ranges. It's mostly compatible with TrieDateField. * A field for indexed dates and date ranges. It's mostly compatible with TrieDateField. It has the potential to allow
* efficient faceting, similar to facet.enum.
* *
* @see NumberRangePrefixTreeStrategy * @see NumberRangePrefixTreeStrategy
* @see DateRangePrefixTree * @see DateRangePrefixTree
*/ */
public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRangePrefixTreeStrategy> { public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRangePrefixTreeStrategy>
implements DateValueFieldType { // used by ParseDateFieldUpdateProcessorFactory
private static final String OP_PARAM = "op";//local-param to resolve SpatialOperation private static final String OP_PARAM = "op";//local-param to resolve SpatialOperation
private static final DateRangePrefixTree tree = DateRangePrefixTree.INSTANCE; private static final DateRangePrefixTree tree = new DateRangePrefixTree(DateRangePrefixTree.JAVA_UTIL_TIME_COMPAT_CAL);
@Override @Override
protected void init(IndexSchema schema, Map<String, String> args) { protected void init(IndexSchema schema, Map<String, String> args) {
@ -69,17 +71,24 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
@Override @Override
protected String getStoredValue(Shape shape, String shapeStr) { protected String getStoredValue(Shape shape, String shapeStr) {
// even if shapeStr is set, it might have included some dateMath, so see if we can resolve it first:
if (shape instanceof UnitNRShape) { if (shape instanceof UnitNRShape) {
UnitNRShape unitShape = (UnitNRShape) shape; UnitNRShape unitShape = (UnitNRShape) shape;
if (unitShape.getLevel() == tree.getMaxLevels()) { if (unitShape.getLevel() == tree.getMaxLevels()) {
//fully precise date. We can be fully compatible with TrieDateField. //fully precise date. We can be fully compatible with TrieDateField (incl. 'Z')
Date date = tree.toCalendar(unitShape).getTime(); return shape.toString() + 'Z';
return date.toInstant().toString();
} }
} }
return (shapeStr == null ? shape.toString() : shapeStr);//we don't normalize ranges here; should we? return (shapeStr == null ? shape.toString() : shapeStr);//we don't normalize ranges here; should we?
} }
// Won't be called because we override getStoredValue? any way; easy to implement in terms of that
@Override
public String shapeToString(Shape shape) {
return getStoredValue(shape, null);
}
@Override @Override
public NRShape parseShape(String str) { public NRShape parseShape(String str) {
if (str.contains(" TO ")) { if (str.contains(" TO ")) {
@ -96,9 +105,9 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
} }
private Calendar parseCalendar(String str) { private Calendar parseCalendar(String str) {
if (str.startsWith("NOW") || str.lastIndexOf('Z') >= 0) { if (str.startsWith("NOW") || str.lastIndexOf('Z') >= 0) { // ? but not if Z is last char ? Ehh, whatever.
//use Solr standard date format parsing rules. //use Solr standard date format parsing rules:
//TODO parse a Calendar instead of a Date, rounded according to DateMath syntax. //TODO add DMP utility to return ZonedDateTime alternative, then set cal fields manually, which is faster?
Date date = DateMathParser.parseMath(null, str); Date date = DateMathParser.parseMath(null, str);
Calendar cal = tree.newCal(); Calendar cal = tree.newCal();
cal.setTime(date); cal.setTime(date);
@ -119,19 +128,6 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
return DateMathParser.parseMath(now, rawval); return DateMathParser.parseMath(now, rawval);
} }
@Override
public String shapeToString(Shape shape) {
if (shape instanceof UnitNRShape) {
UnitNRShape unitShape = (UnitNRShape) shape;
if (unitShape.getLevel() == tree.getMaxLevels()) {
//fully precise date. We can be fully compatible with TrieDateField.
Date date = tree.toCalendar(unitShape).getTime();
return date.toInstant().toString();
}
}
return shape.toString();//range shape
}
@Override @Override
protected SpatialArgs parseSpatialArgs(QParser parser, String externalVal) { protected SpatialArgs parseSpatialArgs(QParser parser, String externalVal) {
//We avoid SpatialArgsParser entirely because it isn't very Solr-friendly //We avoid SpatialArgsParser entirely because it isn't very Solr-friendly

View File

@ -18,9 +18,15 @@ package org.apache.solr.util;
import java.text.ParseException; import java.text.ParseException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -93,6 +99,11 @@ import org.apache.solr.request.SolrRequestInfo;
* request param. * request param.
* </p> * </p>
* *
* <p>
* Historical dates: The calendar computation is completely done with the
* Gregorian system/algorithm. It does <em>not</em> switch to Julian or
* anything else, unlike the default {@link java.util.GregorianCalendar}.
* </p>
* @see SolrRequestInfo#getClientTimeZone * @see SolrRequestInfo#getClientTimeZone
* @see SolrRequestInfo#getNOW * @see SolrRequestInfo#getNOW
*/ */
@ -103,9 +114,6 @@ public class DateMathParser {
/** Default TimeZone for DateMath rounding (UTC) */ /** Default TimeZone for DateMath rounding (UTC) */
public static final TimeZone DEFAULT_MATH_TZ = UTC; public static final TimeZone DEFAULT_MATH_TZ = UTC;
/** Default Locale for DateMath rounding (Locale.ROOT) */
public static final Locale DEFAULT_MATH_LOCALE = Locale.ROOT;
/** /**
* Differs by {@link DateTimeFormatter#ISO_INSTANT} in that it's lenient. * Differs by {@link DateTimeFormatter#ISO_INSTANT} in that it's lenient.
* @see #parseNoMath(String) * @see #parseNoMath(String)
@ -115,22 +123,22 @@ public class DateMathParser {
/** /**
* A mapping from (uppercased) String labels identifying time units, * A mapping from (uppercased) String labels identifying time units,
* to the corresponding Calendar constant used to set/add/roll that unit * to the corresponding {@link ChronoUnit} enum (e.g. "YEARS") used to
* of measurement. * set/add/roll that unit of measurement.
* *
* <p> * <p>
* A single logical unit of time might be represented by multiple labels * A single logical unit of time might be represented by multiple labels
* for convenience (ie: <code>DATE==DAY</code>, * for convenience (ie: <code>DATE==DAYS</code>,
* <code>MILLI==MILLISECOND</code>) * <code>MILLI==MILLIS</code>)
* </p> * </p>
* *
* @see Calendar * @see Calendar
*/ */
public static final Map<String,Integer> CALENDAR_UNITS = makeUnitsMap(); public static final Map<String,ChronoUnit> CALENDAR_UNITS = makeUnitsMap();
/** @see #CALENDAR_UNITS */ /** @see #CALENDAR_UNITS */
private static Map<String,Integer> makeUnitsMap() { private static Map<String,ChronoUnit> makeUnitsMap() {
// NOTE: consciously choosing not to support WEEK at this time, // NOTE: consciously choosing not to support WEEK at this time,
// because of complexity in rounding down to the nearest week // because of complexity in rounding down to the nearest week
@ -141,90 +149,69 @@ public class DateMathParser {
// we probably need to change "Locale loc" to default to something // we probably need to change "Locale loc" to default to something
// from a param via SolrRequestInfo as well. // from a param via SolrRequestInfo as well.
Map<String,Integer> units = new HashMap<>(13); Map<String,ChronoUnit> units = new HashMap<>(13);
units.put("YEAR", Calendar.YEAR); units.put("YEAR", ChronoUnit.YEARS);
units.put("YEARS", Calendar.YEAR); units.put("YEARS", ChronoUnit.YEARS);
units.put("MONTH", Calendar.MONTH); units.put("MONTH", ChronoUnit.MONTHS);
units.put("MONTHS", Calendar.MONTH); units.put("MONTHS", ChronoUnit.MONTHS);
units.put("DAY", Calendar.DATE); units.put("DAY", ChronoUnit.DAYS);
units.put("DAYS", Calendar.DATE); units.put("DAYS", ChronoUnit.DAYS);
units.put("DATE", Calendar.DATE); units.put("DATE", ChronoUnit.DAYS);
units.put("HOUR", Calendar.HOUR_OF_DAY); units.put("HOUR", ChronoUnit.HOURS);
units.put("HOURS", Calendar.HOUR_OF_DAY); units.put("HOURS", ChronoUnit.HOURS);
units.put("MINUTE", Calendar.MINUTE); units.put("MINUTE", ChronoUnit.MINUTES);
units.put("MINUTES", Calendar.MINUTE); units.put("MINUTES", ChronoUnit.MINUTES);
units.put("SECOND", Calendar.SECOND); units.put("SECOND", ChronoUnit.SECONDS);
units.put("SECONDS", Calendar.SECOND); units.put("SECONDS", ChronoUnit.SECONDS);
units.put("MILLI", Calendar.MILLISECOND); units.put("MILLI", ChronoUnit.MILLIS);
units.put("MILLIS", Calendar.MILLISECOND); units.put("MILLIS", ChronoUnit.MILLIS);
units.put("MILLISECOND", Calendar.MILLISECOND); units.put("MILLISECOND", ChronoUnit.MILLIS);
units.put("MILLISECONDS",Calendar.MILLISECOND); units.put("MILLISECONDS",ChronoUnit.MILLIS);
// NOTE: Maybe eventually support NANOS
return units; return units;
} }
/** /**
* Modifies the specified Calendar by "adding" the specified value of units * Returns a modified time by "adding" the specified value of units
* *
* @exception IllegalArgumentException if unit isn't recognized. * @exception IllegalArgumentException if unit isn't recognized.
* @see #CALENDAR_UNITS * @see #CALENDAR_UNITS
*/ */
public static void add(Calendar c, int val, String unit) { private static LocalDateTime add(LocalDateTime t, int val, String unit) {
Integer uu = CALENDAR_UNITS.get(unit); ChronoUnit uu = CALENDAR_UNITS.get(unit);
if (null == uu) { if (null == uu) {
throw new IllegalArgumentException("Adding Unit not recognized: " throw new IllegalArgumentException("Adding Unit not recognized: "
+ unit); + unit);
} }
c.add(uu.intValue(), val); return t.plus(val, uu);
} }
/** /**
* Modifies the specified Calendar by "rounding" down to the specified unit * Returns a modified time by "rounding" down to the specified unit
* *
* @exception IllegalArgumentException if unit isn't recognized. * @exception IllegalArgumentException if unit isn't recognized.
* @see #CALENDAR_UNITS * @see #CALENDAR_UNITS
*/ */
public static void round(Calendar c, String unit) { private static LocalDateTime round(LocalDateTime t, String unit) {
Integer uu = CALENDAR_UNITS.get(unit); ChronoUnit uu = CALENDAR_UNITS.get(unit);
if (null == uu) { if (null == uu) {
throw new IllegalArgumentException("Rounding Unit not recognized: " throw new IllegalArgumentException("Rounding Unit not recognized: "
+ unit); + unit);
} }
int u = uu.intValue(); // note: OffsetDateTime.truncatedTo does not support >= DAYS units so we handle those
switch (uu) {
switch (u) { case YEARS:
return LocalDateTime.of(LocalDate.of(t.getYear(), 1, 1), LocalTime.MIDNIGHT); // midnight is 00:00:00
case Calendar.YEAR: case MONTHS:
c.clear(Calendar.MONTH); return LocalDateTime.of(LocalDate.of(t.getYear(), t.getMonth(), 1), LocalTime.MIDNIGHT);
/* fall through */ case DAYS:
case Calendar.MONTH: return LocalDateTime.of(t.toLocalDate(), LocalTime.MIDNIGHT);
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: default:
throw new IllegalStateException( assert !uu.isDateBased();// >= DAY
"No logic for rounding value ("+u+") " + unit return t.truncatedTo(uu);
);
} }
} }
/** /**
@ -290,23 +277,19 @@ public class DateMathParser {
* otherwise specified in the SolrRequestInfo * otherwise specified in the SolrRequestInfo
* *
* @see SolrRequestInfo#getClientTimeZone * @see SolrRequestInfo#getClientTimeZone
* @see #DEFAULT_MATH_LOCALE
*/ */
public DateMathParser() { public DateMathParser() {
this(null, DEFAULT_MATH_LOCALE); this(null);
} }
/** /**
* @param tz The TimeZone used for rounding (to determine when hours/days begin). If null, then this method defaults to the value dicated by the SolrRequestInfo if it * @param tz The TimeZone used for rounding (to determine when hours/days begin). If null, then this method defaults
* exists -- otherwise it uses UTC. * to the value dictated by the SolrRequestInfo if it exists -- otherwise it uses UTC.
* @param l The Locale used for rounding (to determine when weeks begin). If null, then this method defaults to en_US.
* @see #DEFAULT_MATH_TZ * @see #DEFAULT_MATH_TZ
* @see #DEFAULT_MATH_LOCALE
* @see Calendar#getInstance(TimeZone,Locale) * @see Calendar#getInstance(TimeZone,Locale)
* @see SolrRequestInfo#getClientTimeZone * @see SolrRequestInfo#getClientTimeZone
*/ */
public DateMathParser(TimeZone tz, Locale l) { public DateMathParser(TimeZone tz) {
loc = (null != l) ? l : DEFAULT_MATH_LOCALE;
if (null == tz) { if (null == tz) {
SolrRequestInfo reqInfo = SolrRequestInfo.getRequestInfo(); SolrRequestInfo reqInfo = SolrRequestInfo.getRequestInfo();
tz = (null != reqInfo) ? reqInfo.getClientTimeZone() : DEFAULT_MATH_TZ; tz = (null != reqInfo) ? reqInfo.getClientTimeZone() : DEFAULT_MATH_TZ;
@ -321,13 +304,6 @@ public class DateMathParser {
return this.zone; return this.zone;
} }
/**
* @return the locale
*/
public Locale getLocale() {
return this.loc;
}
/** /**
* Defines this instance's concept of "now". * Defines this instance's concept of "now".
* @see #getNow * @see #getNow
@ -337,7 +313,7 @@ public class DateMathParser {
} }
/** /**
* Returns a cloned of this instance's concept of "now". * Returns a clone of this instance's concept of "now" (never null).
* *
* If setNow was never called (or if null was specified) then this method * If setNow was never called (or if null was specified) then this method
* first defines 'now' as the value dictated by the SolrRequestInfo if it * first defines 'now' as the value dictated by the SolrRequestInfo if it
@ -353,7 +329,7 @@ public class DateMathParser {
// fall back to current time if no request info set // fall back to current time if no request info set
now = new Date(); now = new Date();
} else { } else {
now = reqInfo.getNOW(); now = reqInfo.getNOW(); // never null
} }
} }
return (Date) now.clone(); return (Date) now.clone();
@ -365,15 +341,15 @@ public class DateMathParser {
* @exception ParseException positions in ParseExceptions are token positions, not character positions. * @exception ParseException positions in ParseExceptions are token positions, not character positions.
*/ */
public Date parseMath(String math) throws ParseException { public Date parseMath(String math) throws ParseException {
Calendar cal = Calendar.getInstance(zone, loc);
cal.setTime(getNow());
/* check for No-Op */ /* check for No-Op */
if (0==math.length()) { if (0==math.length()) {
return cal.getTime(); return getNow();
} }
ZoneId zoneId = zone.toZoneId();
// localDateTime is a date and time local to the timezone specified
LocalDateTime localDateTime = ZonedDateTime.ofInstant(getNow().toInstant(), zoneId).toLocalDateTime();
String[] ops = splitter.split(math); String[] ops = splitter.split(math);
int pos = 0; int pos = 0;
while ( pos < ops.length ) { while ( pos < ops.length ) {
@ -391,7 +367,7 @@ public class DateMathParser {
("Need a unit after command: \"" + command + "\"", pos); ("Need a unit after command: \"" + command + "\"", pos);
} }
try { try {
round(cal, ops[pos++]); localDateTime = round(localDateTime, ops[pos++]);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ParseException throw new ParseException
("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1); ("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1);
@ -415,7 +391,7 @@ public class DateMathParser {
} }
try { try {
String unit = ops[pos++]; String unit = ops[pos++];
add(cal, val, unit); localDateTime = add(localDateTime, val, unit);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new ParseException throw new ParseException
("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1); ("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1);
@ -427,7 +403,7 @@ public class DateMathParser {
} }
} }
return cal.getTime(); return Date.from(ZonedDateTime.of(localDateTime, zoneId).toInstant());
} }
private static Pattern splitter = Pattern.compile("\\b|(?<=\\d)(?=\\D)"); private static Pattern splitter = Pattern.compile("\\b|(?<=\\d)(?=\\D)");

View File

@ -867,7 +867,7 @@ public class BasicFunctionalityTest extends SolrTestCaseJ4 {
assertQ("check counts using fixed NOW and TZ rounding", assertQ("check counts using fixed NOW and TZ rounding",
req("q", "bday:[NOW/DAY TO NOW/DAY+1DAY]", req("q", "bday:[NOW/DAY TO NOW/DAY+1DAY]",
"TZ", "GMT-23", "TZ", "GMT+01",
"NOW", "205369736000" // 1976-07-04T23:08:56.235Z "NOW", "205369736000" // 1976-07-04T23:08:56.235Z
), ),
"*[count(//doc)=0]"); "*[count(//doc)=0]");

View File

@ -172,7 +172,7 @@ public class TestTrie extends SolrTestCaseJ4 {
format.setTimeZone(TimeZone.getTimeZone("UTC")); format.setTimeZone(TimeZone.getTimeZone("UTC"));
assertU(delQ("*:*")); assertU(delQ("*:*"));
DateMathParser dmp = new DateMathParser(DateMathParser.UTC, Locale.ROOT); DateMathParser dmp = new DateMathParser(DateMathParser.UTC);
String largestDate = ""; String largestDate = "";
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
// index 10 days starting with today // index 10 days starting with today
@ -221,7 +221,7 @@ public class TestTrie extends SolrTestCaseJ4 {
// For tdate tests // For tdate tests
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
format.setTimeZone(TimeZone.getTimeZone("UTC")); format.setTimeZone(TimeZone.getTimeZone("UTC"));
DateMathParser dmp = new DateMathParser(DateMathParser.UTC, Locale.ROOT); DateMathParser dmp = new DateMathParser(DateMathParser.UTC);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
long l = Integer.MAX_VALUE + i*1L; long l = Integer.MAX_VALUE + i*1L;

View File

@ -1002,6 +1002,27 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 {
,meta+"/int[@name='between'][.='6']" ,meta+"/int[@name='between'][.='6']"
); );
//Fixed by SOLR-9080 related to the Gregorian Change Date
assertQ("check BC era",
req( "q", "*:*"
,"rows", "0"
,"facet", "true"
,p, f
,p+".start", "-0200-01-01T00:00:00Z" // BC
,p+".end", "+0200-01-01T00:00:00Z" // AD
,p+".gap", "+100YEARS"
,p+".other", "all"
)
,pre+"/int[@name='-0200-01-01T00:00:00Z'][.='0']"
,pre+"/int[@name='-0100-01-01T00:00:00Z'][.='0']"
,pre+"/int[@name='0000-01-01T00:00:00Z'][.='0']"
,pre+"/int[@name='0100-01-01T00:00:00Z'][.='0']"
,meta+"/int[@name='before' ][.='0']"
,meta+"/int[@name='after' ][.='14']"
,meta+"/int[@name='between'][.='0']"
);
} }
@Test @Test

View File

@ -55,6 +55,35 @@ public class DateRangeFieldTest extends SolrTestCaseJ4 {
assertQ(req("q", "dateRange:[1999 TO 2001]"), xpathMatches(0, 2)); assertQ(req("q", "dateRange:[1999 TO 2001]"), xpathMatches(0, 2));
} }
public void testBeforeGregorianChangeDate() { // GCD is the year 1582
assertU(delQ("*:*"));
assertU(adoc("id", "0", "dateRange", "1500-01-01T00:00:00Z"));
assertU(adoc("id", "1", "dateRange", "-1500-01-01T00:00:00Z")); // BC
assertU(adoc("id", "2", "dateRange", "1400-01-01T00:00:00Z/YEAR")); // date math of month or year can cause issues
assertU(adoc("id", "3", "dateRange", "1300")); // the whole year of 1300
assertU(commit());
//ensure round-trip toString
assertQ(req("q", "id:0", "fl", "dateRange"), "//result/doc/arr[@name='dateRange']/str[.='1500-01-01T00:00:00Z']");
assertQ(req("q", "id:1", "fl", "dateRange"), "//result/doc/arr[@name='dateRange']/str[.='-1500-01-01T00:00:00Z']");
// note: fixed by SOLR-9080, would instead find "1399-01-09T00:00:00Z"
assertQ(req("q", "id:2", "fl", "dateRange"), "//result/doc/arr[@name='dateRange']/str[.='1400-01-01T00:00:00Z']");
assertQ(req("q", "id:3", "fl", "dateRange"), "//result/doc/arr[@name='dateRange']/str[.='1300']");
//ensure range syntax works
assertQ(req("q", "dateRange:[1450-01-01T00:00:00Z TO 1499-12-31T23:59:59Z]"), xpathMatches());// before
assertQ(req("q", "dateRange:[1500-01-01T00:00:00Z TO 1500-01-01T00:00:00Z]"), xpathMatches(0));// spot on
assertQ(req("q", "dateRange:[1500-01-01T00:00:01Z TO 1550-01-01T00:00:00Z]"), xpathMatches());// after
assertQ(req("q", "dateRange:[-1500-01-01T00:00:00Z TO -1500-01-01T00:00:00Z]"), xpathMatches(1));
// do range queries in the vicinity of docId=3 val:"1300"
assertQ(req("q", "dateRange:[1299 TO 1299-12-31T23:59:59Z]"), xpathMatches());//adjacent
assertQ(req("q", "dateRange:[1299 TO 1300-01-01T00:00:00Z]"), xpathMatches(3));// expand + 1 sec
assertQ(req("q", "dateRange:1301"), xpathMatches()); // adjacent
assertQ(req("q", "dateRange:[1300-12-31T23:59:59Z TO 1301]"), xpathMatches(3)); // expand + 1 sec
}
@Test @Test
public void testMultiValuedDateRanges() { public void testMultiValuedDateRanges() {
assertU(delQ("*:*")); assertU(delQ("*:*"));

View File

@ -16,11 +16,10 @@
*/ */
package org.apache.solr.util; package org.apache.solr.util;
import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant; import java.time.Instant;
import java.util.Calendar; import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@ -40,44 +39,37 @@ public class DateMathParserTest extends LuceneTestCase {
* A formatter for specifying every last nuance of a Date for easy * A formatter for specifying every last nuance of a Date for easy
* reference in assertion statements * reference in assertion statements
*/ */
private DateFormat fmt; private DateTimeFormatter fmt;
/** /**
* A parser for reading in explicit dates that are convenient to type * A parser for reading in explicit dates that are convenient to type
* in a test * in a test
*/ */
private DateFormat parser; private DateTimeFormatter parser;
public DateMathParserTest() { public DateMathParserTest() {
super(); fmt = DateTimeFormatter.ofPattern("G yyyyy MM ww W D dd F E a HH hh mm ss SSS z Z", Locale.ROOT)
fmt = new SimpleDateFormat .withZone(ZoneOffset.UTC);
("G yyyyy MM ww WW DD dd F E aa HH hh mm ss SSS z Z",Locale.ROOT);
fmt.setTimeZone(UTC);
parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",Locale.ROOT); parser = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneOffset.UTC); // basically without the 'Z'
parser.setTimeZone(UTC);
} }
/** MACRO: Round: parses s, rounds with u, fmts */ /** MACRO: Round: parses s, rounds with u, fmts */
protected String r(String s, String u) throws Exception { protected String r(String s, String u) throws Exception {
Date d = parser.parse(s); Date dt = DateMathParser.parseMath(null, s + "Z/" + u);
Calendar c = Calendar.getInstance(UTC, Locale.ROOT); return fmt.format(dt.toInstant());
c.setTime(d);
DateMathParser.round(c, u);
return fmt.format(c.getTime());
} }
/** MACRO: Add: parses s, adds v u, fmts */ /** MACRO: Add: parses s, adds v u, fmts */
protected String a(String s, int v, String u) throws Exception { protected String a(String s, int v, String u) throws Exception {
Date d = parser.parse(s); char sign = v >= 0 ? '+' : '-';
Calendar c = Calendar.getInstance(UTC, Locale.ROOT); Date dt = DateMathParser.parseMath(null, s + 'Z' + sign + Math.abs(v) + u);
c.setTime(d); return fmt.format(dt.toInstant());
DateMathParser.add(c, v, u);
return fmt.format(c.getTime());
} }
/** MACRO: Expected: parses s, fmts */ /** MACRO: Expected: parses s, fmts */
protected String e(String s) throws Exception { protected String e(String s) throws Exception {
return fmt.format(parser.parse(s)); return fmt.format(parser.parse(s, Instant::from));
} }
protected void assertRound(String e, String i, String u) throws Exception { protected void assertRound(String e, String i, String u) throws Exception {
@ -85,6 +77,7 @@ public class DateMathParserTest extends LuceneTestCase {
String rr = r(i,u); String rr = r(i,u);
assertEquals(ee + " != " + rr + " round:" + i + ":" + u, ee, rr); assertEquals(ee + " != " + rr + " round:" + i + ":" + u, ee, rr);
} }
protected void assertAdd(String e, String i, int v, String u) protected void assertAdd(String e, String i, int v, String u)
throws Exception { throws Exception {
@ -97,13 +90,17 @@ public class DateMathParserTest extends LuceneTestCase {
throws Exception { throws Exception {
String ee = e(e); String ee = e(e);
String aa = fmt.format(p.parseMath(i)); String aa = fmt.format(p.parseMath(i).toInstant());
assertEquals(ee + " != " + aa + " math:" + assertEquals(ee + " != " + aa + " math:" +
parser.format(p.getNow()) + ":" + i, ee, aa); parser.format(p.getNow().toInstant()) + ":" + i, ee, aa);
}
private void setNow(DateMathParser p, String text) {
p.setNow(Date.from(parser.parse(text, Instant::from)));
} }
public void testCalendarUnitsConsistency() throws Exception { public void testCalendarUnitsConsistency() throws Exception {
String input = "2001-07-04T12:08:56.235"; String input = "1234-07-04T12:08:56.235";
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) { for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
try { try {
r(input, u); r(input, u);
@ -120,20 +117,20 @@ public class DateMathParserTest extends LuceneTestCase {
public void testRound() throws Exception { public void testRound() throws Exception {
String input = "2001-07-04T12:08:56.235"; String input = "1234-07-04T12:08:56.235";
assertRound("2001-07-04T12:08:56.000", input, "SECOND"); assertRound("1234-07-04T12:08:56.000", input, "SECOND");
assertRound("2001-07-04T12:08:00.000", input, "MINUTE"); assertRound("1234-07-04T12:08:00.000", input, "MINUTE");
assertRound("2001-07-04T12:00:00.000", input, "HOUR"); assertRound("1234-07-04T12:00:00.000", input, "HOUR");
assertRound("2001-07-04T00:00:00.000", input, "DAY"); assertRound("1234-07-04T00:00:00.000", input, "DAY");
assertRound("2001-07-01T00:00:00.000", input, "MONTH"); assertRound("1234-07-01T00:00:00.000", input, "MONTH");
assertRound("2001-01-01T00:00:00.000", input, "YEAR"); assertRound("1234-01-01T00:00:00.000", input, "YEAR");
} }
public void testAddZero() throws Exception { public void testAddZero() throws Exception {
String input = "2001-07-04T12:08:56.235"; String input = "1234-07-04T12:08:56.235";
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) { for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
assertAdd(input, input, 0, u); assertAdd(input, input, 0, u);
@ -143,24 +140,24 @@ public class DateMathParserTest extends LuceneTestCase {
public void testAdd() throws Exception { public void testAdd() throws Exception {
String input = "2001-07-04T12:08:56.235"; String input = "1234-07-04T12:08:56.235";
assertAdd("2001-07-04T12:08:56.236", input, 1, "MILLISECOND"); assertAdd("1234-07-04T12:08:56.236", input, 1, "MILLISECOND");
assertAdd("2001-07-04T12:08:57.235", input, 1, "SECOND"); assertAdd("1234-07-04T12:08:57.235", input, 1, "SECOND");
assertAdd("2001-07-04T12:09:56.235", input, 1, "MINUTE"); assertAdd("1234-07-04T12:09:56.235", input, 1, "MINUTE");
assertAdd("2001-07-04T13:08:56.235", input, 1, "HOUR"); assertAdd("1234-07-04T13:08:56.235", input, 1, "HOUR");
assertAdd("2001-07-05T12:08:56.235", input, 1, "DAY"); assertAdd("1234-07-05T12:08:56.235", input, 1, "DAY");
assertAdd("2001-08-04T12:08:56.235", input, 1, "MONTH"); assertAdd("1234-08-04T12:08:56.235", input, 1, "MONTH");
assertAdd("2002-07-04T12:08:56.235", input, 1, "YEAR"); assertAdd("1235-07-04T12:08:56.235", input, 1, "YEAR");
} }
public void testParseStatelessness() throws Exception { public void testParseStatelessness() throws Exception {
DateMathParser p = new DateMathParser(UTC, Locale.ROOT); DateMathParser p = new DateMathParser(UTC);
p.setNow(parser.parse("2001-07-04T12:08:56.235")); setNow(p, "1234-07-04T12:08:56.235");
String e = fmt.format(p.parseMath("")); String e = fmt.format(p.parseMath("").toInstant());
Date trash = p.parseMath("+7YEARS"); Date trash = p.parseMath("+7YEARS");
trash = p.parseMath("/MONTH"); trash = p.parseMath("/MONTH");
@ -168,90 +165,89 @@ public class DateMathParserTest extends LuceneTestCase {
Thread.currentThread(); Thread.currentThread();
Thread.sleep(5); Thread.sleep(5);
String a = fmt.format(p.parseMath("")); String a =fmt.format(p.parseMath("").toInstant());
assertEquals("State of DateMathParser changed", e, a); assertEquals("State of DateMathParser changed", e, a);
} }
public void testParseMath() throws Exception { public void testParseMath() throws Exception {
DateMathParser p = new DateMathParser(UTC, Locale.ROOT); DateMathParser p = new DateMathParser(UTC);
p.setNow(parser.parse("2001-07-04T12:08:56.235")); setNow(p, "1234-07-04T12:08:56.235");
// No-Op // No-Op
assertMath("2001-07-04T12:08:56.235", p, ""); assertMath("1234-07-04T12:08:56.235", p, "");
// simple round // simple round
assertMath("2001-07-04T12:08:56.000", p, "/SECOND"); assertMath("1234-07-04T12:08:56.235", p, "/MILLIS"); // no change
assertMath("2001-07-04T12:08:00.000", p, "/MINUTE"); assertMath("1234-07-04T12:08:56.000", p, "/SECOND");
assertMath("2001-07-04T12:00:00.000", p, "/HOUR"); assertMath("1234-07-04T12:08:00.000", p, "/MINUTE");
assertMath("2001-07-04T00:00:00.000", p, "/DAY"); assertMath("1234-07-04T12:00:00.000", p, "/HOUR");
assertMath("2001-07-01T00:00:00.000", p, "/MONTH"); assertMath("1234-07-04T00:00:00.000", p, "/DAY");
assertMath("2001-01-01T00:00:00.000", p, "/YEAR"); assertMath("1234-07-01T00:00:00.000", p, "/MONTH");
assertMath("1234-01-01T00:00:00.000", p, "/YEAR");
// simple addition // simple addition
assertMath("2001-07-04T12:08:56.236", p, "+1MILLISECOND"); assertMath("1234-07-04T12:08:56.236", p, "+1MILLISECOND");
assertMath("2001-07-04T12:08:57.235", p, "+1SECOND"); assertMath("1234-07-04T12:08:57.235", p, "+1SECOND");
assertMath("2001-07-04T12:09:56.235", p, "+1MINUTE"); assertMath("1234-07-04T12:09:56.235", p, "+1MINUTE");
assertMath("2001-07-04T13:08:56.235", p, "+1HOUR"); assertMath("1234-07-04T13:08:56.235", p, "+1HOUR");
assertMath("2001-07-05T12:08:56.235", p, "+1DAY"); assertMath("1234-07-05T12:08:56.235", p, "+1DAY");
assertMath("2001-08-04T12:08:56.235", p, "+1MONTH"); assertMath("1234-08-04T12:08:56.235", p, "+1MONTH");
assertMath("2002-07-04T12:08:56.235", p, "+1YEAR"); assertMath("1235-07-04T12:08:56.235", p, "+1YEAR");
// simple subtraction // simple subtraction
assertMath("2001-07-04T12:08:56.234", p, "-1MILLISECOND"); assertMath("1234-07-04T12:08:56.234", p, "-1MILLISECOND");
assertMath("2001-07-04T12:08:55.235", p, "-1SECOND"); assertMath("1234-07-04T12:08:55.235", p, "-1SECOND");
assertMath("2001-07-04T12:07:56.235", p, "-1MINUTE"); assertMath("1234-07-04T12:07:56.235", p, "-1MINUTE");
assertMath("2001-07-04T11:08:56.235", p, "-1HOUR"); assertMath("1234-07-04T11:08:56.235", p, "-1HOUR");
assertMath("2001-07-03T12:08:56.235", p, "-1DAY"); assertMath("1234-07-03T12:08:56.235", p, "-1DAY");
assertMath("2001-06-04T12:08:56.235", p, "-1MONTH"); assertMath("1234-06-04T12:08:56.235", p, "-1MONTH");
assertMath("2000-07-04T12:08:56.235", p, "-1YEAR"); assertMath("1233-07-04T12:08:56.235", p, "-1YEAR");
// simple '+/-' // simple '+/-'
assertMath("2001-07-04T12:08:56.235", p, "+1MILLISECOND-1MILLISECOND"); assertMath("1234-07-04T12:08:56.235", p, "+1MILLISECOND-1MILLISECOND");
assertMath("2001-07-04T12:08:56.235", p, "+1SECOND-1SECOND"); assertMath("1234-07-04T12:08:56.235", p, "+1SECOND-1SECOND");
assertMath("2001-07-04T12:08:56.235", p, "+1MINUTE-1MINUTE"); assertMath("1234-07-04T12:08:56.235", p, "+1MINUTE-1MINUTE");
assertMath("2001-07-04T12:08:56.235", p, "+1HOUR-1HOUR"); assertMath("1234-07-04T12:08:56.235", p, "+1HOUR-1HOUR");
assertMath("2001-07-04T12:08:56.235", p, "+1DAY-1DAY"); assertMath("1234-07-04T12:08:56.235", p, "+1DAY-1DAY");
assertMath("2001-07-04T12:08:56.235", p, "+1MONTH-1MONTH"); assertMath("1234-07-04T12:08:56.235", p, "+1MONTH-1MONTH");
assertMath("2001-07-04T12:08:56.235", p, "+1YEAR-1YEAR"); assertMath("1234-07-04T12:08:56.235", p, "+1YEAR-1YEAR");
// simple '-/+' // simple '-/+'
assertMath("2001-07-04T12:08:56.235", p, "-1MILLISECOND+1MILLISECOND"); assertMath("1234-07-04T12:08:56.235", p, "-1MILLISECOND+1MILLISECOND");
assertMath("2001-07-04T12:08:56.235", p, "-1SECOND+1SECOND"); assertMath("1234-07-04T12:08:56.235", p, "-1SECOND+1SECOND");
assertMath("2001-07-04T12:08:56.235", p, "-1MINUTE+1MINUTE"); assertMath("1234-07-04T12:08:56.235", p, "-1MINUTE+1MINUTE");
assertMath("2001-07-04T12:08:56.235", p, "-1HOUR+1HOUR"); assertMath("1234-07-04T12:08:56.235", p, "-1HOUR+1HOUR");
assertMath("2001-07-04T12:08:56.235", p, "-1DAY+1DAY"); assertMath("1234-07-04T12:08:56.235", p, "-1DAY+1DAY");
assertMath("2001-07-04T12:08:56.235", p, "-1MONTH+1MONTH"); assertMath("1234-07-04T12:08:56.235", p, "-1MONTH+1MONTH");
assertMath("2001-07-04T12:08:56.235", p, "-1YEAR+1YEAR"); assertMath("1234-07-04T12:08:56.235", p, "-1YEAR+1YEAR");
// more complex stuff // more complex stuff
assertMath("2000-07-04T12:08:56.236", p, "+1MILLISECOND-1YEAR"); assertMath("1233-07-04T12:08:56.236", p, "+1MILLISECOND-1YEAR");
assertMath("2000-07-04T12:08:57.235", p, "+1SECOND-1YEAR"); assertMath("1233-07-04T12:08:57.235", p, "+1SECOND-1YEAR");
assertMath("2000-07-04T12:09:56.235", p, "+1MINUTE-1YEAR"); assertMath("1233-07-04T12:09:56.235", p, "+1MINUTE-1YEAR");
assertMath("2000-07-04T13:08:56.235", p, "+1HOUR-1YEAR"); assertMath("1233-07-04T13:08:56.235", p, "+1HOUR-1YEAR");
assertMath("2000-07-05T12:08:56.235", p, "+1DAY-1YEAR"); assertMath("1233-07-05T12:08:56.235", p, "+1DAY-1YEAR");
assertMath("2000-08-04T12:08:56.235", p, "+1MONTH-1YEAR"); assertMath("1233-08-04T12:08:56.235", p, "+1MONTH-1YEAR");
assertMath("2000-07-04T12:08:56.236", p, "-1YEAR+1MILLISECOND"); assertMath("1233-07-04T12:08:56.236", p, "-1YEAR+1MILLISECOND");
assertMath("2000-07-04T12:08:57.235", p, "-1YEAR+1SECOND"); assertMath("1233-07-04T12:08:57.235", p, "-1YEAR+1SECOND");
assertMath("2000-07-04T12:09:56.235", p, "-1YEAR+1MINUTE"); assertMath("1233-07-04T12:09:56.235", p, "-1YEAR+1MINUTE");
assertMath("2000-07-04T13:08:56.235", p, "-1YEAR+1HOUR"); assertMath("1233-07-04T13:08:56.235", p, "-1YEAR+1HOUR");
assertMath("2000-07-05T12:08:56.235", p, "-1YEAR+1DAY"); assertMath("1233-07-05T12:08:56.235", p, "-1YEAR+1DAY");
assertMath("2000-08-04T12:08:56.235", p, "-1YEAR+1MONTH"); assertMath("1233-08-04T12:08:56.235", p, "-1YEAR+1MONTH");
assertMath("2000-07-01T00:00:00.000", p, "-1YEAR+1MILLISECOND/MONTH"); assertMath("1233-07-01T00:00:00.000", p, "-1YEAR+1MILLISECOND/MONTH");
assertMath("2000-07-04T00:00:00.000", p, "-1YEAR+1SECOND/DAY"); assertMath("1233-07-04T00:00:00.000", p, "-1YEAR+1SECOND/DAY");
assertMath("2000-07-04T00:00:00.000", p, "-1YEAR+1MINUTE/DAY"); assertMath("1233-07-04T00:00:00.000", p, "-1YEAR+1MINUTE/DAY");
assertMath("2000-07-04T13:00:00.000", p, "-1YEAR+1HOUR/HOUR"); assertMath("1233-07-04T13:00:00.000", p, "-1YEAR+1HOUR/HOUR");
assertMath("2000-07-05T12:08:56.000", p, "-1YEAR+1DAY/SECOND"); assertMath("1233-07-05T12:08:56.000", p, "-1YEAR+1DAY/SECOND");
assertMath("2000-08-04T12:08:56.000", p, "-1YEAR+1MONTH/SECOND"); assertMath("1233-08-04T12:08:56.000", p, "-1YEAR+1MONTH/SECOND");
// "tricky" cases // "tricky" cases
p.setNow(parser.parse("2006-01-31T17:09:59.999")); setNow(p, "2006-01-31T17:09:59.999");
assertMath("2006-02-28T17:09:59.999", p, "+1MONTH"); assertMath("2006-02-28T17:09:59.999", p, "+1MONTH");
assertMath("2008-02-29T17:09:59.999", p, "+25MONTH"); assertMath("2008-02-29T17:09:59.999", p, "+25MONTH");
assertMath("2006-02-01T00:00:00.000", p, "/MONTH+35DAYS/MONTH"); assertMath("2006-02-01T00:00:00.000", p, "/MONTH+35DAYS/MONTH");
assertMath("2006-01-31T17:10:00.000", p, "+3MILLIS/MINUTE"); assertMath("2006-01-31T17:10:00.000", p, "+3MILLIS/MINUTE");
} }
public void testParseMathTz() throws Exception { public void testParseMathTz() throws Exception {
@ -267,12 +263,13 @@ public class DateMathParserTest extends LuceneTestCase {
// US, Positive Offset with DST // US, Positive Offset with DST
TimeZone tz = TimeZone.getTimeZone(PLUS_TZS); TimeZone tz = TimeZone.getTimeZone(PLUS_TZS);
DateMathParser p = new DateMathParser(tz, Locale.ROOT); DateMathParser p = new DateMathParser(tz);
p.setNow(parser.parse("2001-07-04T12:08:56.235")); setNow(p, "2001-07-04T12:08:56.235");
// No-Op // No-Op
assertMath("2001-07-04T12:08:56.235", p, ""); assertMath("2001-07-04T12:08:56.235", p, "");
assertMath("2001-07-04T12:08:56.235", p, "/MILLIS");
assertMath("2001-07-04T12:08:56.000", p, "/SECOND"); assertMath("2001-07-04T12:08:56.000", p, "/SECOND");
assertMath("2001-07-04T12:08:00.000", p, "/MINUTE"); assertMath("2001-07-04T12:08:00.000", p, "/MINUTE");
@ -289,8 +286,8 @@ public class DateMathParserTest extends LuceneTestCase {
// France, Negative Offset with DST // France, Negative Offset with DST
tz = TimeZone.getTimeZone(NEG_TZS); tz = TimeZone.getTimeZone(NEG_TZS);
p = new DateMathParser(tz, Locale.ROOT); p = new DateMathParser(tz);
p.setNow(parser.parse("2001-07-04T12:08:56.235")); setNow(p, "2001-07-04T12:08:56.235");
assertMath("2001-07-04T12:08:56.000", p, "/SECOND"); assertMath("2001-07-04T12:08:56.000", p, "/SECOND");
assertMath("2001-07-04T12:08:00.000", p, "/MINUTE"); assertMath("2001-07-04T12:08:00.000", p, "/MINUTE");
@ -306,8 +303,8 @@ public class DateMathParserTest extends LuceneTestCase {
public void testParseMathExceptions() throws Exception { public void testParseMathExceptions() throws Exception {
DateMathParser p = new DateMathParser(UTC, Locale.ROOT); DateMathParser p = new DateMathParser(UTC);
p.setNow(parser.parse("2001-07-04T12:08:56.235")); setNow(p, "1234-07-04T12:08:56.235");
Map<String,Integer> badCommands = new HashMap<>(); Map<String,Integer> badCommands = new HashMap<>();
badCommands.put("/", 1); badCommands.put("/", 1);
@ -373,7 +370,8 @@ public class DateMathParserTest extends LuceneTestCase {
} }
private void assertFormat(final String expected, final long millis) { private void assertFormat(final String expected, final long millis) {
assertEquals(expected, Instant.ofEpochMilli(millis).toString()); assertEquals(expected, Instant.ofEpochMilli(millis).toString()); // assert same as ISO_INSTANT
assertEquals(millis, DateMathParser.parseMath(null, expected).getTime()); // assert DMP has same result
} }
/** /**