blob: d3ea248277137046035a427decae4f315f173dfa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.util;
import java.text.ParseException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.apache.solr.SolrTestCaseJ4;
import static org.apache.solr.util.DateMathParser.UTC;
/**
* Tests that the functions in DateMathParser
*/
public class DateMathParserTest extends SolrTestCaseJ4 {
/**
* A formatter for specifying every last nuance of a Date for easy
* reference in assertion statements
*/
private DateTimeFormatter fmt;
/**
* A parser for reading in explicit dates that are convenient to type
* in a test
*/
private DateTimeFormatter parser;
public DateMathParserTest() {
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 = 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 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 {
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, Instant::from));
}
protected void assertRound(String e, String i, String u) throws Exception {
String ee = e(e);
String rr = r(i,u);
assertEquals(ee + " != " + rr + " round:" + i + ":" + u, ee, rr);
}
protected void assertAdd(String e, String i, int v, String u)
throws Exception {
String ee = e(e);
String aa = a(i,v,u);
assertEquals(ee + " != " + aa + " add:" + i + "+" + v + ":" + u, ee, aa);
}
protected void assertMath(String e, DateMathParser p, String i)
throws Exception {
String ee = e(e);
String aa = fmt.format(p.parseMath(i).toInstant());
assertEquals(ee + " != " + aa + " math:" +
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 = "1234-07-04T12:08:56.235";
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
try {
r(input, u);
} catch (IllegalStateException e) {
assertNotNull("no logic for rounding: " + u, e);
}
try {
a(input, 1, u);
} catch (IllegalStateException e) {
assertNotNull("no logic for rounding: " + u, e);
}
}
}
public void testRound() throws Exception {
String input = "1234-07-04T12:08:56.235";
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 = "1234-07-04T12:08:56.235";
for (String u : DateMathParser.CALENDAR_UNITS.keySet()) {
assertAdd(input, input, 0, u);
}
}
public void testAdd() throws Exception {
String input = "1234-07-04T12:08:56.235";
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);
setNow(p, "1234-07-04T12:08:56.235");
String e = fmt.format(p.parseMath("").toInstant());
Date trash = p.parseMath("+7YEARS");
trash = p.parseMath("/MONTH");
trash = p.parseMath("-5DAYS+20MINUTES");
Thread.currentThread();
Thread.sleep(5);
String a =fmt.format(p.parseMath("").toInstant());
assertEquals("State of DateMathParser changed", e, a);
}
public void testParseMath() throws Exception {
DateMathParser p = new DateMathParser(UTC);
setNow(p, "1234-07-04T12:08:56.235");
// No-Op
assertMath("1234-07-04T12:08:56.235", p, "");
// simple round
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("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("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("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("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("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
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 {
final String PLUS_TZS = "America/Los_Angeles";
final String NEG_TZS = "Europe/Paris";
assumeTrue("Test requires JVM to know about about TZ: " + PLUS_TZS,
TimeZoneUtils.KNOWN_TIMEZONE_IDS.contains(PLUS_TZS));
assumeTrue("Test requires JVM to know about about TZ: " + NEG_TZS,
TimeZoneUtils.KNOWN_TIMEZONE_IDS.contains(NEG_TZS));
// US, Positive Offset with DST
TimeZone tz = TimeZone.getTimeZone(PLUS_TZS);
DateMathParser p = new DateMathParser(tz);
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");
assertMath("2001-07-04T07:00:00.000", p, "/DAY");
assertMath("2001-07-01T07:00:00.000", p, "/MONTH");
// no DST in jan
assertMath("2001-01-01T08:00:00.000", p, "/YEAR");
// no DST in nov 2001
assertMath("2001-11-04T08:00:00.000", p, "+4MONTH/DAY");
// yes DST in nov 2010
assertMath("2010-11-04T07:00:00.000", p, "+9YEAR+4MONTH/DAY");
// France, Negative Offset with DST
tz = TimeZone.getTimeZone(NEG_TZS);
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");
assertMath("2001-07-04T12:00:00.000", p, "/HOUR");
assertMath("2001-07-03T22:00:00.000", p, "/DAY");
assertMath("2001-06-30T22:00:00.000", p, "/MONTH");
// no DST in dec
assertMath("2000-12-31T23:00:00.000", p, "/YEAR");
// no DST in nov
assertMath("2001-11-03T23:00:00.000", p, "+4MONTH/DAY");
}
public void testParseMathExceptions() throws Exception {
DateMathParser p = new DateMathParser(UTC);
setNow(p, "1234-07-04T12:08:56.235");
Map<String,Integer> badCommands = new HashMap<>();
badCommands.put("/", 1);
badCommands.put("+", 1);
badCommands.put("-", 1);
badCommands.put("/BOB", 1);
badCommands.put("+SECOND", 1);
badCommands.put("-2MILLI/", 4);
badCommands.put(" +BOB", 0);
badCommands.put("+2SECONDS ", 3);
badCommands.put("/4", 1);
badCommands.put("?SECONDS", 0);
for (String command : badCommands.keySet()) {
ParseException e = expectThrows(ParseException.class, () -> p.parseMath(command));
assertEquals("Wrong pos for: " + command + " => " + e.getMessage(),
badCommands.get(command).intValue(), e.getErrorOffset());
}
}
/*
PARSING / FORMATTING (without date math) Formerly in DateFieldTest.
*/
public void testFormatter() {
assertFormat("1995-12-31T23:59:59.999Z", 820454399999l);
assertFormat("1995-12-31T23:59:59.990Z", 820454399990l);
assertFormat("1995-12-31T23:59:59.900Z", 820454399900l);
assertFormat("1995-12-31T23:59:59Z", 820454399000l);
// just after epoch
assertFormat("1970-01-01T00:00:00.005Z", 5L);
assertFormat("1970-01-01T00:00:00Z", 0L);
assertFormat("1970-01-01T00:00:00.370Z", 370L);
assertFormat("1970-01-01T00:00:00.900Z", 900L);
// well after epoch
assertFormat("1999-12-31T23:59:59.005Z", 946684799005L);
assertFormat("1999-12-31T23:59:59Z", 946684799000L);
assertFormat("1999-12-31T23:59:59.370Z", 946684799370L);
assertFormat("1999-12-31T23:59:59.900Z", 946684799900L);
// waaaay after epoch ('+' is required for more than 4 digits in a year)
assertFormat("+12345-12-31T23:59:59.005Z", 327434918399005L);
assertFormat("+12345-12-31T23:59:59Z", 327434918399000L);
assertFormat("+12345-12-31T23:59:59.370Z", 327434918399370L);
assertFormat("+12345-12-31T23:59:59.900Z", 327434918399900L);
// well before epoch
assertFormat("0299-12-31T23:59:59Z", -52700112001000L);
assertFormat("0299-12-31T23:59:59.123Z", -52700112000877L);
assertFormat("0299-12-31T23:59:59.090Z", -52700112000910L);
// BC (negative years)
assertFormat("-12021-12-01T02:02:02Z", Instant.parse("-12021-12-01T02:02:02Z").toEpochMilli());
}
private void assertFormat(final String expected, final long millis) {
assertEquals(expected, Instant.ofEpochMilli(millis).toString()); // assert same as ISO_INSTANT
assertEquals(millis, DateMathParser.parseMath(null, expected).getTime()); // assert DMP has same result
}
/**
* Using dates in the canonical format, verify that parsing+formatting
* is an identify function
*/
public void testRoundTrip() throws Exception {
// NOTE: the 2nd arg is what the round trip result looks like (may be null if same as input)
assertParseFormatEquals("1995-12-31T23:59:59.999666Z", "1995-12-31T23:59:59.999Z"); // beyond millis is truncated
assertParseFormatEquals("1995-12-31T23:59:59.999Z", "1995-12-31T23:59:59.999Z");
assertParseFormatEquals("1995-12-31T23:59:59.99Z", "1995-12-31T23:59:59.990Z");
assertParseFormatEquals("1995-12-31T23:59:59.9Z", "1995-12-31T23:59:59.900Z");
assertParseFormatEquals("1995-12-31T23:59:59Z", "1995-12-31T23:59:59Z");
// here the input isn't in the canonical form, but we should be forgiving
assertParseFormatEquals("1995-12-31T23:59:59.990Z", "1995-12-31T23:59:59.990Z");
assertParseFormatEquals("1995-12-31T23:59:59.900Z", "1995-12-31T23:59:59.900Z");
assertParseFormatEquals("1995-12-31T23:59:59.90Z", "1995-12-31T23:59:59.900Z");
assertParseFormatEquals("1995-12-31T23:59:59.000Z", "1995-12-31T23:59:59Z");
assertParseFormatEquals("1995-12-31T23:59:59.00Z", "1995-12-31T23:59:59Z");
assertParseFormatEquals("1995-12-31T23:59:59.0Z", "1995-12-31T23:59:59Z");
// kind of kludgy, but we have other tests for the actual date math
//assertParseFormatEquals("NOW/DAY", p.parseMath("/DAY").toInstant().toString());
// as of Solr 1.3
assertParseFormatEquals("1995-12-31T23:59:59Z/DAY", "1995-12-31T00:00:00Z");
assertParseFormatEquals("1995-12-31T23:59:59.123Z/DAY", "1995-12-31T00:00:00Z");
assertParseFormatEquals("1995-12-31T23:59:59.123999Z/DAY", "1995-12-31T00:00:00Z");
// typical dates, various precision (0,1,2,3 digits of millis)
assertParseFormatEquals("1995-12-31T23:59:59.987Z", null);
assertParseFormatEquals("1995-12-31T23:59:59.98Z", "1995-12-31T23:59:59.980Z");//add 0 ms
assertParseFormatEquals("1995-12-31T23:59:59.9Z", "1995-12-31T23:59:59.900Z");//add 00 ms
assertParseFormatEquals("1995-12-31T23:59:59Z", null);
assertParseFormatEquals("1976-03-06T03:06:00Z", null);
assertParseFormatEquals("1995-12-31T23:59:59.987654Z", "1995-12-31T23:59:59.987Z");//truncate nanoseconds off
// dates with atypical years
assertParseFormatEquals("0001-01-01T01:01:01Z", null);
assertParseFormatEquals("+12021-12-01T03:03:03Z", null);
assertParseFormatEquals("0000-04-04T04:04:04Z", null); // note: 0 AD is also known as 1 BC
// dates with negative years (BC)
assertParseFormatEquals("-0005-05-05T05:05:05Z", null);
assertParseFormatEquals("-2021-12-01T04:04:04Z", null);
assertParseFormatEquals("-12021-12-01T02:02:02Z", null);
}
public void testParseLenient() throws Exception {
// dates that only parse thanks to lenient mode of DateTimeFormatter
assertParseFormatEquals("10995-12-31T23:59:59.990Z", "+10995-12-31T23:59:59.990Z"); // missing '+' 5 digit year
assertParseFormatEquals("995-1-2T3:4:5Z", "0995-01-02T03:04:05Z"); // wasn't 0 padded
}
private void assertParseFormatEquals(String inputStr, String expectedStr) {
if (expectedStr == null) {
expectedStr = inputStr;
}
Date inputDate = DateMathParser.parseMath(null, inputStr);
String resultStr = inputDate.toInstant().toString();
assertEquals("d:" + inputDate.getTime(), expectedStr, resultStr);
}
}