diff --git common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java index 9443e8ec78..514f627df6 100644 --- common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java +++ common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java @@ -74,7 +74,7 @@ * - "Delimiter" for numeric tokens means any non-numeric character or end of input. * - The words token and pattern are used interchangeably. * - * A. Temporal tokens + * A.1. Numeric temporal tokens * YYYY * 4-digit year * - For string to datetime conversion, prefix digits for 1, 2, and 3-digit inputs are obtained @@ -199,6 +199,48 @@ * - 1st week starts on the 1st of the month and ends on the 7th, and so on. * - Not allowed in string to datetime conversion. * + * IYYY + * 4-digit ISO 8601 week-numbering year + * - Returns the year relating to the ISO week number (IW), which is the full week (Monday to + * Sunday) which contains January 4 of the Gregorian year. + * - Behaves similarly to YYYY in that for datetime to string conversion, prefix digits for 1, 2, + * and 3-digit inputs are obtained from current week-numbering year. + * - For string to datetime conversion, requires IW and ID|DAY|DY. Conflicts with all other date + * patterns (see "List of Date-Based Patterns"). + * + * IYY + * Last 3 digits of ISO 8601 week-numbering year + * - See IYYY. + * - Behaves similarly to YYY in that for datetime to string conversion, prefix digit is obtained + * from current week-numbering year and can accept 1 or 2-digit input. + * + * IY + * Last 2 digits of ISO 8601 week-numbering year + * - See IYYY. + * - Behaves similarly to YY in that for datetime to string conversion, prefix digits are obtained + * from current week-numbering year and can accept 1-digit input. + * + * I + * Last digit of ISO 8601 week-numbering year + * - See IYYY. + * - Behaves similarly to Y in that for datetime to string conversion, prefix digits are obtained + * from current week-numbering year. + * + * IW + * ISO 8601 week of year (1-53) + * - Begins on the Monday closest to January 1 of the year. + * - For string to datetime conversion, if the input week does not exist in the input year, an + * error will be thrown. e.g. the 2019 week-year has 52 weeks; with pattern="iyyy-iw-id" + * input="2019-53-2" is not accepted. + * - For string to datetime conversion, requires IYYY|IYY|IY|I and ID|DAY|DY. Conflicts with all other + * date patterns (see "List of Date-Based Patterns"). + * + * ID + * ISO 8601 day of week (1-7) + * - 1 is Monday, and so on. + * - For string to datetime conversion, requires IYYY|IYY|IY|I and IW. Conflicts with all other + * date patterns (see "List of Date-Based Patterns"). + * * A.2. Character temporals * Temporal elements, but spelled out. * - For datetime to string conversion, the pattern's case must match one of the listed formats @@ -241,7 +283,7 @@ * - day = sunday * - For string to datetime conversion, neither the case of the pattern nor the case of the input * are taken into account. - * - Not allowed in string to datetime conversion. + * - Not allowed in string to datetime conversion except with IYYY|IYY|IY|I and IW. * * DY|Dy|dy * Abbreviated name of day of week @@ -252,7 +294,7 @@ * - dy = sun * - For string to datetime conversion, neither the case of the pattern nor the case of the input * are taken into account. - * - Not allowed in string to datetime conversion. + * - Not allowed in string to datetime conversion except with IYYY|IYY|IY|I and IW. * * B. Time zone tokens * TZH @@ -346,6 +388,7 @@ public static final int AM = 0; public static final int PM = 1; private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("MMM"); + public static final DateTimeFormatter DAY_OF_WEEK_FORMATTER = DateTimeFormatter.ofPattern("EEE"); private String pattern; private List tokens = new ArrayList<>(); private boolean formatExact = false; @@ -374,6 +417,9 @@ .put("p.m.", ChronoField.AMPM_OF_DAY).put("pm", ChronoField.AMPM_OF_DAY) .put("ww", ChronoField.ALIGNED_WEEK_OF_YEAR).put("w", ChronoField.ALIGNED_WEEK_OF_MONTH) .put("q", IsoFields.QUARTER_OF_YEAR) + .put("iyyy", IsoFields.WEEK_BASED_YEAR).put("iyy", IsoFields.WEEK_BASED_YEAR) + .put("iy", IsoFields.WEEK_BASED_YEAR).put("i", IsoFields.WEEK_BASED_YEAR) + .put("iw", IsoFields.WEEK_OF_WEEK_BASED_YEAR).put("id", ChronoField.DAY_OF_WEEK) .build(); private static final Map CHARACTER_TEMPORAL_TOKENS = @@ -404,6 +450,11 @@ .put("month", 9).put("day", 9).put("dy", 3) .build(); + private static final List ISO_8601_TEMPORAL_FIELDS = + ImmutableList.of(ChronoField.DAY_OF_WEEK, + IsoFields.WEEK_OF_WEEK_BASED_YEAR, + IsoFields.WEEK_BASED_YEAR); + /** * Represents broad categories of tokens. */ @@ -697,6 +748,7 @@ private void verifyForParse() { ArrayList temporalFields = new ArrayList<>(); ArrayList timeZoneTemporalUnits = new ArrayList<>(); int roundYearCount=0, yearCount=0; + boolean containsIsoFields=false, containsGregorianFields=false; for (Token token : tokens) { if (token.temporalField != null) { temporalFields.add(token.temporalField); @@ -707,6 +759,13 @@ private void verifyForParse() { yearCount += 1; } } + if (token.temporalField.isDateBased() && token.temporalField != ChronoField.DAY_OF_WEEK) { + if (ISO_8601_TEMPORAL_FIELDS.contains(token.temporalField)) { + containsIsoFields = true; + } else { + containsGregorianFields = true; + } + } } else if (token.temporalUnit != null) { timeZoneTemporalUnits.add(token.temporalUnit); } @@ -719,7 +778,7 @@ private void verifyForParse() { if (temporalFields.contains(WeekFields.SUNDAY_START.dayOfWeek())) { throw new IllegalArgumentException("Illegal field: d (" + WeekFields.SUNDAY_START.dayOfWeek() + ")"); } - if (temporalFields.contains(ChronoField.DAY_OF_WEEK)) { + if (temporalFields.contains(ChronoField.DAY_OF_WEEK) && containsGregorianFields) { throw new IllegalArgumentException("Illegal field: dy/day (" + ChronoField.DAY_OF_WEEK + ")"); } if (temporalFields.contains(ChronoField.ALIGNED_WEEK_OF_MONTH)) { @@ -729,15 +788,25 @@ private void verifyForParse() { throw new IllegalArgumentException("Illegal field: ww (" + ChronoField.ALIGNED_WEEK_OF_YEAR + ")"); } - if (!(temporalFields.contains(ChronoField.YEAR))) { + if (containsGregorianFields && containsIsoFields) { + throw new IllegalArgumentException("Pattern cannot contain both ISO and Gregorian tokens"); + } + if (!(temporalFields.contains(ChronoField.YEAR) + || temporalFields.contains(IsoFields.WEEK_BASED_YEAR))) { throw new IllegalArgumentException("Missing year token."); } - if (!(temporalFields.contains(ChronoField.MONTH_OF_YEAR) && + if (containsGregorianFields && + !(temporalFields.contains(ChronoField.MONTH_OF_YEAR) && temporalFields.contains(ChronoField.DAY_OF_MONTH) || temporalFields.contains(ChronoField.DAY_OF_YEAR))) { throw new IllegalArgumentException("Missing day of year or (month of year + day of month)" + " tokens."); } + if (containsIsoFields && + !(temporalFields.contains(IsoFields.WEEK_OF_WEEK_BASED_YEAR) && + temporalFields.contains(ChronoField.DAY_OF_WEEK))) { + throw new IllegalArgumentException("Missing week of year (iw) or day of week (id) tokens."); + } if (roundYearCount > 0 && yearCount > 0) { throw new IllegalArgumentException("Invalid duplication of format element: Both year and" + "round year are provided"); @@ -934,6 +1003,7 @@ public Timestamp parseTimestamp(String fullInput){ int index = 0; int value; int timeZoneSign = 0, timeZoneHours = 0, timeZoneMinutes = 0; + int iyyy = 0, iw = 0; for (Token token : tokens) { switch (token.type) { @@ -952,6 +1022,8 @@ public Timestamp parseTimestamp(String fullInput){ throw new IllegalArgumentException( "Value " + value + " not valid for token " + token.toString()); } + iyyy = updateVar(value, iyyy, token.temporalField == IsoFields.WEEK_BASED_YEAR); + iw = updateVar(value, iw, token.temporalField == IsoFields.WEEK_OF_WEEK_BASED_YEAR); index += substring.length(); break; case TIMEZONE: @@ -1004,10 +1076,35 @@ public Timestamp parseTimestamp(String fullInput){ throw new IllegalArgumentException("Leftover input after parsing: " + fullInput.substring(index) + " in string " + fullInput); } + checkForInvalidIsoWeek(iyyy, iw); return Timestamp.ofEpochSecond(ldt.toEpochSecond(ZoneOffset.UTC), ldt.getNano()); } + private int updateVar(int input, int var, boolean doUpdateIf) { + if (doUpdateIf) { + var = input; + } + return var; + } + + /** + * Check for WEEK_OF_WEEK_BASED_YEAR (iw) value 53 when WEEK_BASED_YEAR (iyyy) does not have 53 + * weeks. + */ + private void checkForInvalidIsoWeek(int iyyy, int iw) { + if (iyyy == 0) { + return; + } + + LocalDateTime ldt = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC); + ldt = ldt.with(IsoFields.WEEK_BASED_YEAR, iyyy); + ldt = ldt.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, iw); + if (ldt.getYear() != iyyy) { + throw new IllegalArgumentException("ISO year " + iyyy + " does not have " + iw + " weeks."); + } + } + public Date parseDate(String input){ return Date.ofEpochMilli(parseTimestamp(input).toEpochMilli()); } @@ -1060,8 +1157,16 @@ private int parseNumericTemporal(String substring, Token token){ } else if (token.temporalField == ChronoField.HOUR_OF_AMPM && "12".equals(substring)) { substring = "0"; - } else if (token.temporalField == ChronoField.YEAR) { - String currentYearString = String.valueOf(LocalDateTime.now().getYear()); + } else if (token.temporalField == ChronoField.YEAR + || token.temporalField == IsoFields.WEEK_BASED_YEAR) { + + String currentYearString; + if (token.temporalField == ChronoField.YEAR) { + currentYearString = String.valueOf(LocalDateTime.now().getYear()); + } else { + currentYearString = String.valueOf(LocalDateTime.now().get(IsoFields.WEEK_BASED_YEAR)); + } + //deal with round years if (token.string.startsWith("r") && substring.length() == 2) { int currFirst2Digits = Integer.parseInt(currentYearString.substring(0, 2)); @@ -1092,6 +1197,7 @@ private int parseNumericTemporal(String substring, Token token){ } private static final String MONTH_REGEX; + private static final String DAY_OF_WEEK_REGEX; static { StringBuilder sb = new StringBuilder(); String or = ""; @@ -1100,6 +1206,13 @@ private int parseNumericTemporal(String substring, Token token){ or = "|"; } MONTH_REGEX = sb.toString(); + sb = new StringBuilder(); + or = ""; + for (DayOfWeek dayOfWeek : DayOfWeek.values()) { + sb.append(or).append(dayOfWeek); + or = "|"; + } + DAY_OF_WEEK_REGEX = sb.toString(); } private String getNextCharacterSubstring(String fullInput, int index, Token token) { @@ -1112,7 +1225,17 @@ private String getNextCharacterSubstring(String fullInput, int index, Token toke return substring; } - Matcher matcher = Pattern.compile(MONTH_REGEX, Pattern.CASE_INSENSITIVE).matcher(substring); + // patterns day, month + String regex; + if (token.temporalField == ChronoField.MONTH_OF_YEAR) { + regex = MONTH_REGEX; + } else if (token.temporalField == ChronoField.DAY_OF_WEEK) { + regex = DAY_OF_WEEK_REGEX; + } else { + throw new IllegalArgumentException("Error at index " + index + ": " + token + " not a " + + "character temporal with length not 3"); + } + Matcher matcher = Pattern.compile(regex, Pattern.CASE_INSENSITIVE).matcher(substring); if (matcher.find()) { return substring.substring(0, matcher.end()); } @@ -1128,6 +1251,12 @@ private int parseCharacterTemporal(String substring, Token token) { } else { return Month.valueOf(substring.toUpperCase()).getValue(); } + } else if (token.temporalField == ChronoField.DAY_OF_WEEK) { + if (token.length == 3) { + return DayOfWeek.from(DAY_OF_WEEK_FORMATTER.parse(capitalize(substring))).getValue(); + } else { + return DayOfWeek.valueOf(substring.toUpperCase()).getValue(); + } } } catch (Exception e) { throw new IllegalArgumentException( diff --git common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java index ff41534fce..ea60a31088 100644 --- common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java +++ common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java @@ -95,6 +95,11 @@ public void testSetPatternWithBadPatterns() { verifyBadPattern("yyyy mm-MONTH dd", true); verifyBadPattern("yyyy MON, month dd", true); + verifyBadPattern("iyyy-mm-dd", true); // can't mix iso and Gregorian + verifyBadPattern("iyyy-id", true); // missing iyyy, iw, or id + verifyBadPattern("iyyy-iw", true); + verifyBadPattern("iw-id", true); + verifyBadPattern("tzm", false); verifyBadPattern("tzh", false); @@ -145,6 +150,23 @@ public void testFormatTimestamp() { checkFormatTs("YYYY-mm-dd: Q WW W", "2019-03-31 00:00:00", "2019-03-31: 1 13 5"); checkFormatTs("YYYY-mm-dd: Q WW W", "2019-04-01 00:00:00", "2019-04-01: 2 13 1"); checkFormatTs("YYYY-mm-dd: Q WW W", "2019-12-31 00:00:00", "2019-12-31: 4 53 5"); + + //ISO 8601 + checkFormatTs("YYYY-MM-DD : IYYY-IW-ID", "2018-12-31 00:00:00", "2018-12-31 : 2019-01-01"); + checkFormatTs("YYYY-MM-DD : IYYY-IW-ID", "2019-01-06 00:00:00", "2019-01-06 : 2019-01-07"); + checkFormatTs("YYYY-MM-DD : IYYY-IW-ID", "2019-01-07 00:00:00", "2019-01-07 : 2019-02-01"); + checkFormatTs("YYYY-MM-DD : IYYY-IW-ID", "2019-12-29 00:00:00", "2019-12-29 : 2019-52-07"); + checkFormatTs("YYYY-MM-DD : IYYY-IW-ID", "2019-12-30 00:00:00", "2019-12-30 : 2020-01-01"); + checkFormatTs("YYYY-MM-DD : IYY-IW-ID", "2019-12-30 00:00:00", "2019-12-30 : 020-01-01"); + checkFormatTs("YYYY-MM-DD : IY-IW-ID", "2019-12-30 00:00:00", "2019-12-30 : 20-01-01"); + checkFormatTs("YYYY-MM-DD : I-IW-ID", "2019-12-30 00:00:00", "2019-12-30 : 0-01-01"); + checkFormatTs("id: Day", "2018-12-31 00:00:00", "01: Monday "); + checkFormatTs("id: Day", "2019-01-01 00:00:00", "02: Tuesday "); + checkFormatTs("id: Day", "2019-01-02 00:00:00", "03: Wednesday"); + checkFormatTs("id: Day", "2019-01-03 00:00:00", "04: Thursday "); + checkFormatTs("id: Day", "2019-01-04 00:00:00", "05: Friday "); + checkFormatTs("id: Day", "2019-01-05 00:00:00", "06: Saturday "); + checkFormatTs("id: Day", "2019-01-06 00:00:00", "07: Sunday "); } private void checkFormatTs(String pattern, String input, String expectedOutput) { @@ -236,9 +258,33 @@ public void testParseTimestamp() { checkParseTimestamp("yyyy-MON-dd", "2018-FEB-28", "2018-02-28 00:00:00"); checkParseTimestamp("yyyy-moN-dd", "2018-FeB-28", "2018-02-28 00:00:00"); checkParseTimestamp("yyyy-mon-dd", "2018-FEB-28", "2018-02-28 00:00:00"); + verifyBadParseString("yyyy-MON-dd", "2018-FEBRUARY-28"); + verifyBadParseString("yyyy-MON-dd", "2018-FEBR-28"); + verifyBadParseString("yyyy-MONTH-dd", "2018-FEB-28"); //letters and numbers are delimiters to each other, respectively checkParseDate("yyyy-ddMONTH", "2018-4March", "2018-03-04"); checkParseDate("yyyy-MONTHdd", "2018-March4", "2018-03-04"); + //ISO 8601 + checkParseTimestamp("IYYY-IW-ID", "2019-01-01", "2018-12-31 00:00:00"); + checkParseTimestamp("IYYY-IW-ID", "2019-01-07", "2019-01-06 00:00:00"); + checkParseTimestamp("IYYY-IW-ID", "2019-02-01", "2019-01-07 00:00:00"); + checkParseTimestamp("IYYY-IW-ID", "2019-52-07", "2019-12-29 00:00:00"); + checkParseTimestamp("IYYY-IW-ID", "2020-01-01", "2019-12-30 00:00:00"); + checkParseTimestamp("IYYY-IW-ID", "020-01-04", thisYearString.substring(0, 1) + "020-01-02 00:00:00"); + checkParseTimestamp("IYY-IW-ID", "020-01-04", thisYearString.substring(0, 1) + "020-01-02 00:00:00"); + checkParseTimestamp("IYY-IW-ID", "20-01-04", thisYearString.substring(0, 2) + "20-01-02 00:00:00"); + checkParseTimestamp("IY-IW-ID", "20-01-04", thisYearString.substring(0, 2) + "20-01-02 00:00:00"); + checkParseTimestamp("IYYY-IW-DAY", "2019-01-monday", "2018-12-31 00:00:00"); + checkParseTimestamp("IYYY-IW-Day", "2019-01-Sunday", "2019-01-06 00:00:00"); + checkParseTimestamp("IYYY-IW-Dy", "2019-02-MON", "2019-01-07 00:00:00"); + checkParseTimestamp("IYYY-IW-DY", "2019-52-sun", "2019-12-29 00:00:00"); + checkParseTimestamp("IYYY-IW-dy", "2020-01-Mon", "2019-12-30 00:00:00"); + //Tests for these patterns would need changing every decade if done in the above way. + //Thursday of the first week in an ISO year always matches the Gregorian year. + checkParseTimestampIso("IY-IW-ID", "0-01-04", "iw, yyyy", "01, " + thisYearString.substring(0, 3) + "0"); + checkParseTimestampIso("I-IW-ID", "0-01-04", "iw, yyyy", "01, " + thisYearString.substring(0, 3) + "0"); + //time patterns are allowed; date patterns are not + checkParseTimestamp("IYYY-IW-ID hh24:mi:ss", "2019-01-01 01:02:03", "2018-12-31 01:02:03"); } private int getFirstTwoDigits() { @@ -256,6 +302,14 @@ private void checkParseTimestamp(String pattern, String input, String expectedOu Timestamp.valueOf(expectedOutput), formatter.parseTimestamp(input)); } + private void checkParseTimestampIso(String parsePattern, String input, String formatPattern, + String expectedOutput) { + formatter = new HiveSqlDateTimeFormatter(parsePattern, true); + Timestamp ts = formatter.parseTimestamp(input); + formatter = new HiveSqlDateTimeFormatter(formatPattern, false); + assertEquals(expectedOutput, formatter.format(ts)); + } + @Test public void testParseDate() { @@ -283,6 +337,15 @@ public void testParseDate() { checkParseDate("dd/MonthT/yyyy", "31/AugustT/2020", "2020-08-31"); checkParseDate("dd/MonthT/yyyy", "31/MarchT/2020", "2020-03-31"); + + //ISO 8601 + checkParseDate("IYYY-IW-ID", "2019-01-01", "2018-12-31"); + checkParseDate("IW-ID-IYYY", "01-02-2019", "2019-01-01"); + checkParseDate("ID-IW-IYYY", "02-01-2019", "2019-01-01"); + checkParseDate("IYYY-IW-ID", "2019-01-07", "2019-01-06"); + checkParseDate("IYYY-IW-ID", "2019-02-01", "2019-01-07"); + checkParseDate("IYYY-IW-ID", "2019-52-07", "2019-12-29"); + checkParseDate("IYYY-IW-ID", "2020-01-01", "2019-12-30"); } private void checkParseDate(String pattern, String input, String expectedOutput) { @@ -306,6 +369,12 @@ public void testParseTimestampError() { verifyBadParseString("yyyy-MON-dd", "2018-FEBRUARY-28"); // can't mix and match mon and month verifyBadParseString("yyyy-MON-dd", "2018-FEBR-28"); verifyBadParseString("yyyy-MONTH-dd", "2018-FEB-28"); + verifyBadParseString("iyyy-iw-id", "2019-00-01"); //ISO 8601 week number out of range for year + verifyBadParseString("iyyy-iw-id", "2019-53-01"); //ISO 8601 week number out of range for year + verifyBadParseString("iw-iyyy-id", "53-2019-01"); //ISO 8601 week number out of range for year + verifyBadParseString("iw-iyyy-id", "54-2019-01"); //ISO 8601 week number out of range + verifyBadParseString("iyyy-iw-id", "2019-52-00"); //ISO 8601 day of week out of range + verifyBadParseString("iyyy-iw-id", "2019-52-08"); //ISO 8601 day of week out of range } private void verifyBadPattern(String string, boolean forParsing) { @@ -322,6 +391,7 @@ private void verifyBadPattern(String string, boolean forParsing) { public void testFm() { //year (019) becomes 19 even if pattern is yyy checkFormatTs("FMyyy-FMmm-dd FMHH12:MI:FMSS", "2019-01-01 01:01:01", "19-1-01 1:01:1"); + checkFormatTs("FMiyy-FMiw-id FMHH12:MI:FMSS", "2018-12-31 01:01:01", "19-1-01 1:01:1"); //ff[1-9] shouldn't be affected, because leading zeroes hold information checkFormatTs("FF5/FMFF5", "2019-01-01 01:01:01.0333", "03330/03330"); checkFormatTs("FF/FMFF", "2019-01-01 01:01:01.0333", "0333/0333"); @@ -357,6 +427,7 @@ public void testFx() { //enforce correct amount of leading zeroes verifyBadParseString("FXyyyy-mm-dd hh24:miss", "2018-01-01 17:005"); verifyBadParseString("FXyyyy-mm-dd sssss", "2019-01-01 003"); + verifyBadParseString("FXiyyy-iw-id hh24:mi:ss", "019-01-02 17:00:05"); //text case does not matter checkParseTimestamp("\"the DATE is\" yyyy-mm-dd", "the date is 2018-01-01", "2018-01-01 00:00:00"); //AM/PM length has to match, but case doesn't @@ -372,6 +443,8 @@ public void testFx() { public void testFmFx() { checkParseTimestamp("FXDD-FMMM-YYYY hh12 am", "01-1-1998 12 PM", "1998-01-01 12:00:00"); checkParseTimestamp("FXFMDD-MM-YYYY hh12 am", "1-01-1998 12 PM", "1998-01-01 12:00:00"); + checkParseTimestamp("FXFMiyyy-iw-id hh24:mi:ss", "019-01-02 17:00:05", "2019-01-01 17:00:05"); + verifyBadParseString("FXFMiyyy-iw-id hh24:mi:ss", "019-01-02 17:0:05"); //ff[1-9] unaffected checkParseTimestamp("FXFMDD-MM-YYYY FMff2", "1-01-1998 4", "1998-01-01 00:00:00.4"); checkParseTimestamp("FXFMDD-MM-YYYY ff2", "1-01-1998 4", "1998-01-01 00:00:00.4"); @@ -450,9 +523,9 @@ private void verifyPatternParsing(String pattern, int expectedPatternLength, private void verifyBadParseString(String pattern, String string) { formatter = new HiveSqlDateTimeFormatter(pattern, true); try { - formatter.parseTimestamp(string); + Timestamp output = formatter.parseTimestamp(string); fail("Parse string to timestamp should have failed.\nString: " + string + "\nPattern: " - + pattern); + + pattern + ", output = " + output); } catch (Exception e) { assertEquals("Expected IllegalArgumentException, got another exception.", e.getClass().getName(), IllegalArgumentException.class.getName());