/* * 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.lucene.util.LuceneTestCase; import static org.apache.solr.util.DateMathParser.UTC; /** * Tests that the functions in DateMathParser */ public class DateMathParserTest extends LuceneTestCase { /** * 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()) { try { Date out = p.parseMath(command); fail("Didn't generate SyntaxError for: " + command); } catch (ParseException e) { 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); } }