You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ds...@apache.org on 2016/05/14 04:26:41 UTC

lucene-solr:branch_6_0: SOLR-9080 SOLR-9085: Fix date math before the year 1582. note: DateMathParser no longer needs a Locale (cherry picked from commit 4193e60) (cherry picked from commit 9d826ff)

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6_0 af39ab800 -> 8309bae5f


SOLR-9080 SOLR-9085: Fix date math before the year 1582.
note: DateMathParser no longer needs a Locale
(cherry picked from commit 4193e60)
(cherry picked from commit 9d826ff)


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/8309bae5
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/8309bae5
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/8309bae5

Branch: refs/heads/branch_6_0
Commit: 8309bae5ff11c6c9e5835c60b6f8b08bd810737d
Parents: af39ab8
Author: David Smiley <ds...@apache.org>
Authored: Fri May 13 10:03:24 2016 -0400
Committer: David Smiley <ds...@apache.org>
Committed: Sat May 14 00:26:30 2016 -0400

----------------------------------------------------------------------
 solr/CHANGES.txt                                |  26 ++-
 .../dataimport/DataImportHandlerException.java  |   4 +-
 .../handler/dataimport/DateFormatEvaluator.java |  45 ++--
 .../dataimport/FileListEntityProcessor.java     |  18 +-
 .../dataimport/TestVariableResolver.java        |  21 +-
 .../apache/solr/core/SolrDeletionPolicy.java    |   3 +-
 .../org/apache/solr/schema/DateRangeField.java  |  40 ++--
 .../org/apache/solr/util/DateMathParser.java    | 166 ++++++--------
 .../org/apache/solr/BasicFunctionalityTest.java |   2 +-
 .../core/src/test/org/apache/solr/TestTrie.java |   4 +-
 .../apache/solr/request/SimpleFacetsTest.java   |  33 ++-
 .../apache/solr/schema/DateRangeFieldTest.java  |  29 +++
 .../apache/solr/util/DateMathParserTest.java    | 222 +++++++++----------
 13 files changed, 325 insertions(+), 288 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 62eac56..871fded 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -18,6 +18,11 @@ See the Quick Start guide at http://lucene.apache.org/solr/quickstart.html
 
 ==================  6.0.1 ==================
 
+Upgrade Notes
+----------------------
+
+* If you use historical dates, specifically on or before the year 1582, you should re-index.
+
 Bug Fixes
 ----------------------
 
@@ -28,6 +33,10 @@ Bug Fixes
 
 * SOLR-8992: Restore Schema API GET method functionality removed in 6.0 (noble, Steve Rowe)
 
+* 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)
+
 ==================  6.0.0 ==================
 
 Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release
@@ -271,7 +280,7 @@ Bug Fixes
   unique field (Erik Hatcher, Anshum Gupta)
 
 * SOLR-8728: ReplicaAssigner throws NPE when a partial list of nodes are only participating in replica
-  placement. splitshard should preassign nodes using rules, if rules are present (noble,Shai Erera)
+  placement. splitshard should preassign nodes using rules, if rules are present (noble, Shai Erera)
 
 * SOLR-8779: Fix missing InterruptedException handling in ZkStateReader.java (Varun Thacker)
 
@@ -285,15 +294,15 @@ Bug Fixes
 
 * SOLR-8135: If a core reload is fired after a core close, it is not a non-recoverable error (noble)
 
+* SOLR-8790: Collections API responses contain node name in the core-level responses that are
+  returned. (Anshum Gupta)
+
 * SOLR-8804: Fix a race condition in the ClusterStatus API call whereby the call would fail when a concurrent delete
   collection api command was executed (Alexey Serba, Varun Thacker)
 
 * SOLR-8835: JSON Facet API: fix faceting exception on multi-valued numeric fields that
   have docValues. (yonik)
 
-* SOLR-8790: Collections API responses contain node name in the core-level responses that are
-  returned. (Anshum Gupta)
-
 * SOLR-8838: Returning non-stored docValues is incorrect for negative floats and doubles.
   (Ishan Chattopadhyaya, Steve Rowe)
 
@@ -490,6 +499,15 @@ Bug Fixes
   hasn't refreshed yet). In this case the reported size of the file is -1.
   (Shai Erera, Alexey Serba, Richard Coggins)
 
+* SOLR-8870: AngularJS Query tab no longer URL-encodes the /select part of the request, fixing possible 404 issue
+  when Solr is behind a proxy. Also, now supports old-style &qt param when handler not prefixed with "/" (janhoy)
+
+* SOLR-8908: Fix to OnReconnect listener registration to allow listeners to deregister, such
+  as when a core is reloaded or deleted to avoid a memory leak. (Timothy Potter)
+
+* SOLR-9007: Remove mention of the managed_schema_configs as valid config directory when creating
+  the collection for the SolrCloud example. (Timothy Potter)
+
 ======================= 5.5.0 =======================
 
 Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DataImportHandlerException.java
----------------------------------------------------------------------
diff --git a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DataImportHandlerException.java b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DataImportHandlerException.java
index 7892c05..e69b3fd 100644
--- a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DataImportHandlerException.java
+++ b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DataImportHandlerException.java
@@ -54,7 +54,7 @@ public class DataImportHandlerException extends RuntimeException {
     return errCode;
   }
 
-  public static void wrapAndThrow(int err, Exception e) {
+  public static DataImportHandlerException wrapAndThrow(int err, Exception e) {
     if (e instanceof DataImportHandlerException) {
       throw (DataImportHandlerException) e;
     } 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) {
       throw (DataImportHandlerException) e;
     } else {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DateFormatEvaluator.java
----------------------------------------------------------------------
diff --git a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DateFormatEvaluator.java b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DateFormatEvaluator.java
index 30e16ce..f4df820 100644
--- a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DateFormatEvaluator.java
+++ b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/DateFormatEvaluator.java
@@ -16,9 +16,6 @@
  */
 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.SimpleDateFormat;
 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.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>Pass three parameters:
@@ -99,7 +99,7 @@ public class DateFormatEvaluator extends Evaluator {
         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) {
       Object tzObj = l.get(3);
       String tzStr = null;
@@ -153,24 +153,19 @@ public class DateFormatEvaluator extends Evaluator {
    * @return the result of evaluating a string
    */
   protected Date evaluateString(String datemathfmt, Locale locale, TimeZone tz) {
-    Date date = null;
-    datemathfmt = datemathfmt.replaceAll("NOW", "");
+    // note: DMP does not use the locale but perhaps a subclass might use it, for e.g. parsing a date in a custom
+    // 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 {
-      DateMathParser parser = getDateMathParser(locale, tz);
-      date = parseMathString(parser,datemathfmt);
+      DateMathParser parser = new DateMathParser(tz);
+      parser.setNow(new Date());// thus do *not* use SolrRequestInfo
+      return parser.parseMath(datemathfmt);
     } catch (ParseException e) {
-      wrapAndThrow(SEVERE, e, "Invalid expression for date");
+      throw 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);
   }
 
   /**
@@ -182,16 +177,4 @@ public class DateFormatEvaluator extends Evaluator {
     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();
-      }
-    };
-  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/FileListEntityProcessor.java
----------------------------------------------------------------------
diff --git a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/FileListEntityProcessor.java b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/FileListEntityProcessor.java
index e86d09d..a03354f 100644
--- a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/FileListEntityProcessor.java
+++ b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/FileListEntityProcessor.java
@@ -20,7 +20,13 @@ import java.io.File;
 import java.io.FilenameFilter;
 import java.text.ParseException;
 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.Pattern;
 
@@ -153,10 +159,14 @@ public class FileListEntityProcessor extends EntityProcessorBase {
     }
     m = Evaluator.IN_SINGLE_QUOTES.matcher(dateStr);
     if (m.find()) {
-      String expr = null;
-      expr = m.group(1).replaceAll("NOW", "");
+      String expr = m.group(1);
+      //TODO refactor DateMathParser.parseMath a bit to have a static method for this logic.
+      if (expr.startsWith("NOW")) {
+        expr = expr.substring("NOW".length());
+      }
       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) {
         throw new DataImportHandlerException(DataImportHandlerException.SEVERE,
                 "Invalid expression for date", exp);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestVariableResolver.java
----------------------------------------------------------------------
diff --git a/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestVariableResolver.java b/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestVariableResolver.java
index e7ff2e6..0028564 100644
--- a/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestVariableResolver.java
+++ b/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestVariableResolver.java
@@ -16,12 +16,19 @@
  */
 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.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>
@@ -103,7 +110,7 @@ public class TestVariableResolver extends AbstractDataImportHandlerTestCase {
         .<Map<String,String>> emptyList()));
     SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
     format.setTimeZone(TimeZone.getTimeZone("UTC"));
-    DateMathParser dmp = new DateMathParser(TimeZone.getDefault(), Locale.ROOT);
+    DateMathParser dmp = new DateMathParser(TimeZone.getDefault());
     
     String s = vri
         .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);
     format.setTimeZone(TimeZone.getTimeZone("UTC"));
-    DateMathParser dmp = new DateMathParser(TimeZone.getDefault(), Locale.ROOT);
+    DateMathParser dmp = new DateMathParser(TimeZone.getDefault());
     
     String s = resolver
         .replaceTokens("${dataimporter.functions.formatDate('NOW/DAY','yyyy-MM-dd HH:mm')}");

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java b/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
index c7a6689..f9da1cf 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
@@ -20,7 +20,6 @@ import java.io.File;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.List;
-import java.util.Locale;
 
 import org.apache.lucene.index.IndexCommit;
 import org.apache.lucene.index.IndexDeletionPolicy;
@@ -174,7 +173,7 @@ public class SolrDeletionPolicy extends IndexDeletionPolicy implements NamedList
         try {
           if (maxCommitAge != null) {
             if (maxCommitAgeTimeStamp==-1) {
-              DateMathParser dmp = new DateMathParser(DateMathParser.UTC, Locale.ROOT);
+              DateMathParser dmp = new DateMathParser(DateMathParser.UTC);
               maxCommitAgeTimeStamp = dmp.parseMath(maxCommitAge).getTime();
             }
             if (IndexDeletionPolicyWrapper.getCommitTimestamp(commit) < maxCommitAgeTimeStamp) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/java/org/apache/solr/schema/DateRangeField.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/DateRangeField.java b/solr/core/src/java/org/apache/solr/schema/DateRangeField.java
index dcda75e..9bc98ae 100644
--- a/solr/core/src/java/org/apache/solr/schema/DateRangeField.java
+++ b/solr/core/src/java/org/apache/solr/schema/DateRangeField.java
@@ -39,16 +39,18 @@ import org.apache.solr.util.DateMathParser;
 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 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 DateRangePrefixTree tree = DateRangePrefixTree.INSTANCE;
+  private static final DateRangePrefixTree tree = new DateRangePrefixTree(DateRangePrefixTree.JAVA_UTIL_TIME_COMPAT_CAL);
 
   @Override
   protected void init(IndexSchema schema, Map<String, String> args) {
@@ -69,17 +71,24 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
 
   @Override
   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) {
       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();
+        //fully precise date. We can be fully compatible with TrieDateField (incl. 'Z')
+        return shape.toString() + 'Z';
       }
     }
     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
   protected NRShape parseShape(String str) {
     if (str.contains(" TO ")) {
@@ -96,9 +105,9 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
   }
 
   private Calendar parseCalendar(String str) {
-    if (str.startsWith("NOW") || str.lastIndexOf('Z') >= 0) {
-      //use Solr standard date format parsing rules.
-      //TODO parse a Calendar instead of a Date, rounded according to DateMath syntax.
+    if (str.startsWith("NOW") || str.lastIndexOf('Z') >= 0) { //  ? but not if Z is last char ?   Ehh, whatever.
+      //use Solr standard date format parsing rules:
+      //TODO add DMP utility to return ZonedDateTime alternative, then set cal fields manually, which is faster?
       Date date = DateMathParser.parseMath(null, str);
       Calendar cal = tree.newCal();
       cal.setTime(date);
@@ -120,19 +129,6 @@ public class DateRangeField extends AbstractSpatialPrefixTreeFieldType<NumberRan
   }
 
   @Override
-  protected 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
   protected SpatialArgs parseSpatialArgs(QParser parser, String externalVal) {
     //We avoid SpatialArgsParser entirely because it isn't very Solr-friendly
     final Shape shape = parseShape(externalVal);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/java/org/apache/solr/util/DateMathParser.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/util/DateMathParser.java b/solr/core/src/java/org/apache/solr/util/DateMathParser.java
index 489a5aa..4a6ec0b 100644
--- a/solr/core/src/java/org/apache/solr/util/DateMathParser.java
+++ b/solr/core/src/java/org/apache/solr/util/DateMathParser.java
@@ -18,9 +18,15 @@ package org.apache.solr.util;
 
 import java.text.ParseException;
 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.DateTimeFormatterBuilder;
 import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
@@ -93,6 +99,11 @@ import org.apache.solr.request.SolrRequestInfo;
  * request param.
  * </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#getNOW
  */
@@ -103,9 +114,6 @@ public class DateMathParser  {
   /** Default TimeZone for DateMath rounding (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.
    * @see #parseNoMath(String)
@@ -114,23 +122,23 @@ public class DateMathParser  {
       .parseCaseInsensitive().parseLenient().appendInstant().toFormatter(Locale.ROOT);
 
   /**
-   * A mapping from (uppercased) String labels idenyifying time units,
-   * to the corresponding Calendar constant used to set/add/roll that unit
-   * of measurement.
+   * A mapping from (uppercased) String labels identifying time units,
+   * to the corresponding {@link ChronoUnit} enum (e.g. "YEARS") 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>)
+   * for convenience (ie: <code>DATE==DAYS</code>,
+   * <code>MILLI==MILLIS</code>)
    * </p>
    *
    * @see Calendar
    */
-  public static final Map<String,Integer> CALENDAR_UNITS = makeUnitsMap();
+  public static final Map<String,ChronoUnit> CALENDAR_UNITS = makeUnitsMap();
 
 
   /** @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,
     // 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 
     // from a param via SolrRequestInfo as well.
     
-    Map<String,Integer> units = new HashMap<>(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);
+    Map<String,ChronoUnit> units = new HashMap<>(13);
+    units.put("YEAR",        ChronoUnit.YEARS);
+    units.put("YEARS",       ChronoUnit.YEARS);
+    units.put("MONTH",       ChronoUnit.MONTHS);
+    units.put("MONTHS",      ChronoUnit.MONTHS);
+    units.put("DAY",         ChronoUnit.DAYS);
+    units.put("DAYS",        ChronoUnit.DAYS);
+    units.put("DATE",        ChronoUnit.DAYS);
+    units.put("HOUR",        ChronoUnit.HOURS);
+    units.put("HOURS",       ChronoUnit.HOURS);
+    units.put("MINUTE",      ChronoUnit.MINUTES);
+    units.put("MINUTES",     ChronoUnit.MINUTES);
+    units.put("SECOND",      ChronoUnit.SECONDS);
+    units.put("SECONDS",     ChronoUnit.SECONDS);
+    units.put("MILLI",       ChronoUnit.MILLIS);
+    units.put("MILLIS",      ChronoUnit.MILLIS);
+    units.put("MILLISECOND", ChronoUnit.MILLIS);
+    units.put("MILLISECONDS",ChronoUnit.MILLIS);
+
+    // NOTE: Maybe eventually support NANOS
 
     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.
    * @see #CALENDAR_UNITS
    */
-  public static void add(Calendar c, int val, String unit) {
-    Integer uu = CALENDAR_UNITS.get(unit);
+  private static LocalDateTime add(LocalDateTime t, int val, String unit) {
+    ChronoUnit uu = CALENDAR_UNITS.get(unit);
     if (null == uu) {
       throw new IllegalArgumentException("Adding Unit not recognized: "
                                          + 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.
    * @see #CALENDAR_UNITS
    */
-  public static void round(Calendar c, String unit) {
-    Integer uu = CALENDAR_UNITS.get(unit);
+  private static LocalDateTime round(LocalDateTime t, String unit) {
+    ChronoUnit 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
-      );
+    // note: OffsetDateTime.truncatedTo does not support >= DAYS units so we handle those
+    switch (uu) {
+      case YEARS:
+        return LocalDateTime.of(LocalDate.of(t.getYear(), 1, 1), LocalTime.MIDNIGHT); // midnight is 00:00:00
+      case MONTHS:
+        return LocalDateTime.of(LocalDate.of(t.getYear(), t.getMonth(), 1), LocalTime.MIDNIGHT);
+      case DAYS:
+        return LocalDateTime.of(t.toLocalDate(), LocalTime.MIDNIGHT);
+      default:
+        assert !uu.isDateBased();// >= DAY
+        return t.truncatedTo(uu);
     }
-
   }
 
   /**
@@ -290,23 +277,19 @@ public class DateMathParser  {
    * otherwise specified in the SolrRequestInfo
    * 
    * @see SolrRequestInfo#getClientTimeZone
-   * @see #DEFAULT_MATH_LOCALE
    */
   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 
-   * 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.
+   * @param tz The TimeZone used for rounding (to determine when hours/days begin).  If null, then this method defaults
+   *           to the value dictated by the SolrRequestInfo if it exists -- otherwise it uses UTC.
    * @see #DEFAULT_MATH_TZ
-   * @see #DEFAULT_MATH_LOCALE
    * @see Calendar#getInstance(TimeZone,Locale)
    * @see SolrRequestInfo#getClientTimeZone
    */
-  public DateMathParser(TimeZone tz, Locale l) {
-    loc = (null != l) ? l : DEFAULT_MATH_LOCALE;
+  public DateMathParser(TimeZone tz) {
     if (null == tz) {
       SolrRequestInfo reqInfo = SolrRequestInfo.getRequestInfo();
       tz = (null != reqInfo) ? reqInfo.getClientTimeZone() : DEFAULT_MATH_TZ;
@@ -321,13 +304,6 @@ public class DateMathParser  {
     return this.zone;
   }
 
-  /**
-   * @return the locale
-   */
-  public Locale getLocale() {
-    return this.loc;
-  }
-
   /** 
    * Defines this instance's concept of "now".
    * @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 
    * 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
         now = new Date();
       } else {
-        now = reqInfo.getNOW();
+        now = reqInfo.getNOW(); // never null
       }
     }
     return (Date) now.clone();
@@ -365,15 +341,15 @@ public class DateMathParser  {
    * @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();
+      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);
     int pos = 0;
     while ( pos < ops.length ) {
@@ -391,7 +367,7 @@ public class DateMathParser  {
             ("Need a unit after command: \"" + command + "\"", pos);
         }
         try {
-          round(cal, ops[pos++]);
+          localDateTime = round(localDateTime, ops[pos++]);
         } catch (IllegalArgumentException e) {
           throw new ParseException
             ("Unit not recognized: \"" + ops[pos-1] + "\"", pos-1);
@@ -415,7 +391,7 @@ public class DateMathParser  {
         }
         try {
           String unit = ops[pos++];
-          add(cal, val, unit);
+          localDateTime = add(localDateTime, val, unit);
         } catch (IllegalArgumentException e) {
           throw new ParseException
             ("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)");

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/test/org/apache/solr/BasicFunctionalityTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/BasicFunctionalityTest.java b/solr/core/src/test/org/apache/solr/BasicFunctionalityTest.java
index 9d45a0d..579ccf0 100644
--- a/solr/core/src/test/org/apache/solr/BasicFunctionalityTest.java
+++ b/solr/core/src/test/org/apache/solr/BasicFunctionalityTest.java
@@ -867,7 +867,7 @@ public class BasicFunctionalityTest extends SolrTestCaseJ4 {
                 
     assertQ("check counts using fixed NOW and TZ rounding",
             req("q", "bday:[NOW/DAY TO NOW/DAY+1DAY]",
-                "TZ", "GMT-23",
+                "TZ", "GMT+01",
                 "NOW", "205369736000" // 1976-07-04T23:08:56.235Z
                 ),
             "*[count(//doc)=0]");

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/test/org/apache/solr/TestTrie.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/TestTrie.java b/solr/core/src/test/org/apache/solr/TestTrie.java
index 07935b2..47c50b2 100644
--- a/solr/core/src/test/org/apache/solr/TestTrie.java
+++ b/solr/core/src/test/org/apache/solr/TestTrie.java
@@ -172,7 +172,7 @@ public class TestTrie extends SolrTestCaseJ4 {
     format.setTimeZone(TimeZone.getTimeZone("UTC"));
 
     assertU(delQ("*:*"));
-    DateMathParser dmp = new DateMathParser(DateMathParser.UTC, Locale.ROOT);
+    DateMathParser dmp = new DateMathParser(DateMathParser.UTC);
     String largestDate = "";
     for (int i = 0; i < 10; i++) {
       // index 10 days starting with today
@@ -221,7 +221,7 @@ public class TestTrie extends SolrTestCaseJ4 {
     // For tdate tests
     SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
     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++) {
       long l = Integer.MAX_VALUE + i*1L;

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java b/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
index 1bb860e..0514918 100644
--- a/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
+++ b/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
@@ -939,7 +939,7 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 {
             ,"*[count("+pre+"/int)=2]"
             ,pre+"/int[@name='1976-07-05T00:00:00Z'][.='2'  ]"
             ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0']"
-            
+
             ,meta+"/int[@name='before' ][.='5']"
             );
     assertQ("check after is not inclusive of lower bound by default (for dates)",
@@ -955,10 +955,10 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 {
             ,"*[count("+pre+"/int)=2]"
             ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='2'  ]"
             ,pre+"/int[@name='1976-07-04T00:00:00Z']" + jul4
-            
+
             ,meta+"/int[@name='after' ][.='9']"
             );
-            
+
 
     assertQ("check hardend=false",
             req( "q", "*:*"
@@ -975,7 +975,7 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 {
             ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='5'  ]"
             ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0'  ]"
             ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='4'  ]"
-            
+
             ,meta+"/int[@name='before' ][.='2']"
             ,meta+"/int[@name='after'  ][.='3']"
             ,meta+"/int[@name='between'][.='9']"
@@ -996,12 +996,33 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 {
             ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='5'  ]"
             ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0'  ]"
             ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='1'  ]"
-            
+
             ,meta+"/int[@name='before' ][.='2']"
             ,meta+"/int[@name='after'  ][.='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

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/test/org/apache/solr/schema/DateRangeFieldTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/schema/DateRangeFieldTest.java b/solr/core/src/test/org/apache/solr/schema/DateRangeFieldTest.java
index 372c9e9..e76f821 100644
--- a/solr/core/src/test/org/apache/solr/schema/DateRangeFieldTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/DateRangeFieldTest.java
@@ -55,6 +55,35 @@ public class DateRangeFieldTest extends SolrTestCaseJ4 {
     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
   public void testMultiValuedDateRanges() {
     assertU(delQ("*:*"));

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/8309bae5/solr/core/src/test/org/apache/solr/util/DateMathParserTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/util/DateMathParserTest.java b/solr/core/src/test/org/apache/solr/util/DateMathParserTest.java
index 726c71c..9614171 100644
--- a/solr/core/src/test/org/apache/solr/util/DateMathParserTest.java
+++ b/solr/core/src/test/org/apache/solr/util/DateMathParserTest.java
@@ -16,11 +16,10 @@
  */
 package org.apache.solr.util;
 
-import java.text.DateFormat;
 import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
-import java.util.Calendar;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.HashMap;
 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
    * refernece in assertion statements
    */
-  private DateFormat fmt;
+  private DateTimeFormatter fmt;
+
   /**
    * A parser for reading in explicit dates that are convinient to type
    * in a test
    */
-  private DateFormat parser;
+  private DateTimeFormatter 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.ROOT);
-    fmt.setTimeZone(UTC);
+    fmt = DateTimeFormatter.ofPattern("G yyyyy MM ww W D dd F E a HH hh mm ss SSS z Z", Locale.ROOT)
+        .withZone(ZoneOffset.UTC);
 
-    parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS",Locale.ROOT);
-    parser.setTimeZone(UTC);
+    parser = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneOffset.UTC); // basically without the 'Z'
   }
 
   /** 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.ROOT);
-    c.setTime(d);
-    DateMathParser.round(c, u);
-    return fmt.format(c.getTime());
+    Date dt = DateMathParser.parseMath(null, s + "Z/" + u);
+    return fmt.format(dt.toInstant());
   }
   
   /** 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.ROOT);
-    c.setTime(d);
-    DateMathParser.add(c, v, u);
-    return fmt.format(c.getTime());
+    char sign = v >= 0 ? '+' : '-';
+    Date dt = DateMathParser.parseMath(null, s + 'Z' + sign + Math.abs(v) + u);
+    return fmt.format(dt.toInstant());
   }
 
   /** MACRO: Expected: parses s, fmts */
   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 {
@@ -85,6 +77,7 @@ public class DateMathParserTest extends LuceneTestCase {
     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 {
     
@@ -97,13 +90,17 @@ public class DateMathParserTest extends LuceneTestCase {
     throws Exception {
     
     String ee = e(e);
-    String aa = fmt.format(p.parseMath(i));
+    String aa = fmt.format(p.parseMath(i).toInstant());
     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 {
-    String input = "2001-07-04T12:08:56.235";
+    String input = "1234-07-04T12:08:56.235";
     for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
       try {
         r(input, u);
@@ -120,20 +117,20 @@ public class DateMathParserTest extends LuceneTestCase {
   
   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("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");
+    assertRound("1234-07-04T12:08:56.000", input, "SECOND");
+    assertRound("1234-07-04T12:08:00.000", input, "MINUTE");
+    assertRound("1234-07-04T12:00:00.000", input, "HOUR");
+    assertRound("1234-07-04T00:00:00.000", input, "DAY");
+    assertRound("1234-07-01T00:00:00.000", input, "MONTH");
+    assertRound("1234-01-01T00:00:00.000", input, "YEAR");
 
   }
 
   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()) {
       assertAdd(input, input, 0, u);
@@ -143,24 +140,24 @@ public class DateMathParserTest extends LuceneTestCase {
   
   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("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");
+    assertAdd("1234-07-04T12:08:56.236", input, 1, "MILLISECOND");
+    assertAdd("1234-07-04T12:08:57.235", input, 1, "SECOND");
+    assertAdd("1234-07-04T12:09:56.235", input, 1, "MINUTE");
+    assertAdd("1234-07-04T13:08:56.235", input, 1, "HOUR");
+    assertAdd("1234-07-05T12:08:56.235", input, 1, "DAY");
+    assertAdd("1234-08-04T12:08:56.235", input, 1, "MONTH");
+    assertAdd("1235-07-04T12:08:56.235", input, 1, "YEAR");
     
   }
   
   public void testParseStatelessness() throws Exception {
 
-    DateMathParser p = new DateMathParser(UTC, Locale.ROOT);
-    p.setNow(parser.parse("2001-07-04T12:08:56.235"));
+    DateMathParser p = new DateMathParser(UTC);
+    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");
     trash = p.parseMath("/MONTH");
@@ -168,90 +165,89 @@ public class DateMathParserTest extends LuceneTestCase {
     Thread.currentThread();
     Thread.sleep(5);
     
-    String a = fmt.format(p.parseMath(""));
+    String a =fmt.format(p.parseMath("").toInstant());
     assertEquals("State of DateMathParser changed", e, a);
   }
-    
+
   public void testParseMath() throws Exception {
 
-    DateMathParser p = new DateMathParser(UTC, Locale.ROOT);
-    p.setNow(parser.parse("2001-07-04T12:08:56.235"));
+    DateMathParser p = new DateMathParser(UTC);
+    setNow(p, "1234-07-04T12:08:56.235");
 
     // No-Op
-    assertMath("2001-07-04T12:08:56.235", p, "");
+    assertMath("1234-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");
+    assertMath("1234-07-04T12:08:56.235", p, "/MILLIS"); // no change
+    assertMath("1234-07-04T12:08:56.000", p, "/SECOND");
+    assertMath("1234-07-04T12:08:00.000", p, "/MINUTE");
+    assertMath("1234-07-04T12:00:00.000", p, "/HOUR");
+    assertMath("1234-07-04T00:00:00.000", p, "/DAY");
+    assertMath("1234-07-01T00:00:00.000", p, "/MONTH");
+    assertMath("1234-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");
+    assertMath("1234-07-04T12:08:56.236", p, "+1MILLISECOND");
+    assertMath("1234-07-04T12:08:57.235", p, "+1SECOND");
+    assertMath("1234-07-04T12:09:56.235", p, "+1MINUTE");
+    assertMath("1234-07-04T13:08:56.235", p, "+1HOUR");
+    assertMath("1234-07-05T12:08:56.235", p, "+1DAY");
+    assertMath("1234-08-04T12:08:56.235", p, "+1MONTH");
+    assertMath("1235-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");
+    assertMath("1234-07-04T12:08:56.234", p, "-1MILLISECOND");
+    assertMath("1234-07-04T12:08:55.235", p, "-1SECOND");
+    assertMath("1234-07-04T12:07:56.235", p, "-1MINUTE");
+    assertMath("1234-07-04T11:08:56.235", p, "-1HOUR");
+    assertMath("1234-07-03T12:08:56.235", p, "-1DAY");
+    assertMath("1234-06-04T12:08:56.235", p, "-1MONTH");
+    assertMath("1233-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");
+    assertMath("1234-07-04T12:08:56.235", p, "+1MILLISECOND-1MILLISECOND");
+    assertMath("1234-07-04T12:08:56.235", p, "+1SECOND-1SECOND");
+    assertMath("1234-07-04T12:08:56.235", p, "+1MINUTE-1MINUTE");
+    assertMath("1234-07-04T12:08:56.235", p, "+1HOUR-1HOUR");
+    assertMath("1234-07-04T12:08:56.235", p, "+1DAY-1DAY");
+    assertMath("1234-07-04T12:08:56.235", p, "+1MONTH-1MONTH");
+    assertMath("1234-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");
+    assertMath("1234-07-04T12:08:56.235", p, "-1MILLISECOND+1MILLISECOND");
+    assertMath("1234-07-04T12:08:56.235", p, "-1SECOND+1SECOND");
+    assertMath("1234-07-04T12:08:56.235", p, "-1MINUTE+1MINUTE");
+    assertMath("1234-07-04T12:08:56.235", p, "-1HOUR+1HOUR");
+    assertMath("1234-07-04T12:08:56.235", p, "-1DAY+1DAY");
+    assertMath("1234-07-04T12:08:56.235", p, "-1MONTH+1MONTH");
+    assertMath("1234-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");
+    assertMath("1233-07-04T12:08:56.236", p, "+1MILLISECOND-1YEAR");
+    assertMath("1233-07-04T12:08:57.235", p, "+1SECOND-1YEAR");
+    assertMath("1233-07-04T12:09:56.235", p, "+1MINUTE-1YEAR");
+    assertMath("1233-07-04T13:08:56.235", p, "+1HOUR-1YEAR");
+    assertMath("1233-07-05T12:08:56.235", p, "+1DAY-1YEAR");
+    assertMath("1233-08-04T12:08:56.235", p, "+1MONTH-1YEAR");
+    assertMath("1233-07-04T12:08:56.236", p, "-1YEAR+1MILLISECOND");
+    assertMath("1233-07-04T12:08:57.235", p, "-1YEAR+1SECOND");
+    assertMath("1233-07-04T12:09:56.235", p, "-1YEAR+1MINUTE");
+    assertMath("1233-07-04T13:08:56.235", p, "-1YEAR+1HOUR");
+    assertMath("1233-07-05T12:08:56.235", p, "-1YEAR+1DAY");
+    assertMath("1233-08-04T12:08:56.235", p, "-1YEAR+1MONTH");
+    assertMath("1233-07-01T00:00:00.000", p, "-1YEAR+1MILLISECOND/MONTH");
+    assertMath("1233-07-04T00:00:00.000", p, "-1YEAR+1SECOND/DAY");
+    assertMath("1233-07-04T00:00:00.000", p, "-1YEAR+1MINUTE/DAY");
+    assertMath("1233-07-04T13:00:00.000", p, "-1YEAR+1HOUR/HOUR");
+    assertMath("1233-07-05T12:08:56.000", p, "-1YEAR+1DAY/SECOND");
+    assertMath("1233-08-04T12:08:56.000", p, "-1YEAR+1MONTH/SECOND");
 
     // "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("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 testParseMathTz() throws Exception {
@@ -267,13 +263,14 @@ public class DateMathParserTest extends LuceneTestCase {
     // US, Positive Offset with DST
 
     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
     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:00.000", p, "/MINUTE");
     assertMath("2001-07-04T12:00:00.000", p, "/HOUR");
@@ -289,8 +286,8 @@ public class DateMathParserTest extends LuceneTestCase {
     // France, Negative Offset with DST
 
     tz = TimeZone.getTimeZone(NEG_TZS);
-    p = new DateMathParser(tz, Locale.ROOT);
-    p.setNow(parser.parse("2001-07-04T12:08:56.235"));
+    p = new DateMathParser(tz);
+    setNow(p, "2001-07-04T12:08:56.235");
 
     assertMath("2001-07-04T12:08:56.000", p, "/SECOND");
     assertMath("2001-07-04T12:08:00.000", p, "/MINUTE");
@@ -306,8 +303,8 @@ public class DateMathParserTest extends LuceneTestCase {
  
   public void testParseMathExceptions() throws Exception {
     
-    DateMathParser p = new DateMathParser(UTC, Locale.ROOT);
-    p.setNow(parser.parse("2001-07-04T12:08:56.235"));
+    DateMathParser p = new DateMathParser(UTC);
+    setNow(p, "1234-07-04T12:08:56.235");
     
     Map<String,Integer> badCommands = new HashMap<>();
     badCommands.put("/", 1);
@@ -373,7 +370,8 @@ public class DateMathParserTest extends LuceneTestCase {
   }
 
   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
   }
 
   /**