You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hive.apache.org by ku...@apache.org on 2019/08/22 09:16:53 UTC

[hive] branch master updated: HIVE-21580: Introduce ISO 8601 week numbering SQL:2016 formats (Karen Coppage via Marta Kuczora)

This is an automated email from the ASF dual-hosted git repository.

kuczoram pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new db59ec6  HIVE-21580: Introduce ISO 8601 week numbering SQL:2016 formats (Karen Coppage via Marta Kuczora)
db59ec6 is described below

commit db59ec6ecf57dad72588b7593fb516c3e810b5a2
Author: Karen Coppage <kc...@gmail.com>
AuthorDate: Thu Aug 22 11:16:02 2019 +0200

    HIVE-21580: Introduce ISO 8601 week numbering SQL:2016 formats (Karen Coppage via Marta Kuczora)
---
 .../format/datetime/HiveSqlDateTimeFormatter.java  | 147 +++++++++++++++++++--
 .../datetime/TestHiveSqlDateTimeFormatter.java     |  77 ++++++++++-
 2 files changed, 213 insertions(+), 11 deletions(-)

diff --git a/common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java b/common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java
index 9443e8e..9167a8d 100644
--- a/common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java
+++ b/common/src/java/org/apache/hadoop/hive/common/format/datetime/HiveSqlDateTimeFormatter.java
@@ -74,7 +74,7 @@ import java.util.regex.Pattern;
  *       - "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 @@ import java.util.regex.Pattern;
  * - 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 @@ import java.util.regex.Pattern;
  *   - 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 @@ import java.util.regex.Pattern;
  *   - 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 class HiveSqlDateTimeFormatter implements Serializable {
   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<Token> tokens = new ArrayList<>();
   private boolean formatExact = false;
@@ -374,6 +417,9 @@ public class HiveSqlDateTimeFormatter implements Serializable {
           .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<String, TemporalField> CHARACTER_TEMPORAL_TOKENS =
@@ -404,6 +450,11 @@ public class HiveSqlDateTimeFormatter implements Serializable {
       .put("month", 9).put("day", 9).put("dy", 3)
       .build();
 
+  private static final List<TemporalField> 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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
     ArrayList<TemporalField> temporalFields = new ArrayList<>();
     ArrayList<TemporalUnit> 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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
             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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
     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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
       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 class HiveSqlDateTimeFormatter implements Serializable {
     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,15 @@ public class HiveSqlDateTimeFormatter implements Serializable {
           throw new IllegalArgumentException(
               "Value " + value + " not valid for token " + token.toString());
         }
+
+        //update IYYY and IW if necessary
+        if (token.temporalField == IsoFields.WEEK_BASED_YEAR) {
+          iyyy = value;
+        }
+        if (token.temporalField == IsoFields.WEEK_OF_WEEK_BASED_YEAR) {
+          iw = value;
+        }
+
         index += substring.length();
         break;
       case TIMEZONE:
@@ -1004,10 +1083,28 @@ public class HiveSqlDateTimeFormatter implements Serializable {
       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());
   }
 
+  /**
+   * 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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
     } 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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
   }
 
   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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
       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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
       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 @@ public class HiveSqlDateTimeFormatter implements Serializable {
         } 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 a/common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java b/common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java
index ff41534..ea60a31 100644
--- a/common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java
+++ b/common/src/test/org/apache/hadoop/hive/common/format/datetime/TestHiveSqlDateTimeFormatter.java
@@ -95,6 +95,11 @@ public class TestHiveSqlDateTimeFormatter {
     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 class TestHiveSqlDateTimeFormatter {
     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 class TestHiveSqlDateTimeFormatter {
     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 @@ public class TestHiveSqlDateTimeFormatter {
         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 class TestHiveSqlDateTimeFormatter {
 
     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 class TestHiveSqlDateTimeFormatter {
     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 @@ public class TestHiveSqlDateTimeFormatter {
   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 class TestHiveSqlDateTimeFormatter {
     //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 class TestHiveSqlDateTimeFormatter {
   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 @@ public class TestHiveSqlDateTimeFormatter {
   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());