[NO ISSUE][FUN] Improve quarter printing/parsing in date functions

- user model changes: no
- storage format changes: no
- interface changes: no

Details:
- Add 'QQ' format to print_date/print_datetime()
  functions to print quarter of year with leading 0
- Add 'Q' and 'QQ' formats to parse_date/parse_datetime()
  functions to parse quarter of year
- Add testcases and update documentation

Change-Id: Ie71a1f59ab96ed0382255109d1f59bfa6e50ad82
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/13544
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Ian Maxon <imaxon@uci.edu>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/parse_03/parse_03.6.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/parse_03/parse_03.6.query.sqlpp
new file mode 100644
index 0000000..522a8dc
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/parse_03/parse_03.6.query.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/*
+ * Parse year and quarter
+ */
+
+select r, d, dt
+from range(1, 4) r
+let d = parse_date("2020-" || string(r) , "YYYY-Q"),
+    dt = parse_datetime("2021-0" || string(r), "YYYY-QQ")
+order by r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/print_01/print_01.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/print_01/print_01.1.query.sqlpp
index ae1db50..1579c95 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/print_01/print_01.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/print_01/print_01.1.query.sqlpp
@@ -19,8 +19,9 @@
 
 /* Print quarter */
 
-select p, count(*) cnt
+select p1, p2, count(*) cnt
 from range(0, 365) r
 let d = date_from_unix_time_in_days(unix_time_from_date_in_days(date("2020-01-01")) + r),
-  p = print_date(d, "YYYY-Q")
-group by p;
+  p1 = print_date(d, "YYYY-Q"),
+  p2 = print_date(d, "YYYY-QQ")
+group by p1, p2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/parse_03/parse_03.6.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/parse_03/parse_03.6.adm
new file mode 100644
index 0000000..5a7c485
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/parse_03/parse_03.6.adm
@@ -0,0 +1,4 @@
+{ "r": 1, "d": date("2020-01-01"), "dt": datetime("2021-01-01T00:00:00.000") }
+{ "r": 2, "d": date("2020-04-01"), "dt": datetime("2021-04-01T00:00:00.000") }
+{ "r": 3, "d": date("2020-07-01"), "dt": datetime("2021-07-01T00:00:00.000") }
+{ "r": 4, "d": date("2020-10-01"), "dt": datetime("2021-10-01T00:00:00.000") }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/print_01/print_01.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/print_01/print_01.1.adm
index facff48..83e0610 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/print_01/print_01.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/print_01/print_01.1.adm
@@ -1,4 +1,4 @@
-{ "p": "2020-1", "cnt": 91 }
-{ "p": "2020-2", "cnt": 91 }
-{ "p": "2020-3", "cnt": 92 }
-{ "p": "2020-4", "cnt": 92 }
\ No newline at end of file
+{ "p1": "2020-1", "p2": "2020-01", "cnt": 91 }
+{ "p1": "2020-2", "p2": "2020-02", "cnt": 91 }
+{ "p1": "2020-3", "p2": "2020-03", "cnt": 92 }
+{ "p1": "2020-4", "p2": "2020-04", "cnt": 92 }
\ No newline at end of file
diff --git a/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md b/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md
index 5c11b46..a1cdda2 100644
--- a/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md
+++ b/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md
@@ -648,6 +648,8 @@
        * `a` am/pm
        * `z` timezone (parsed and ignored)
        * `Y` year
+       * `Q` quarter of year (1-4)
+       * `QQ` quarter of year (01-04)
        * `M` month
        * `D` day
        * `EEE` weekday (abbreviated name, parsed and ignored)
@@ -685,6 +687,8 @@
        * `n` (or `S`) milliseconds
        * `a` am/pm
        * `Y` year
+       * `Q` quarter of year (1-4)
+       * `QQ` quarter of year (01-04)
        * `M` month
        * `MMM` month (abbreviated name)
        * `MMMM` month (full name)
diff --git a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/DateTimeFormatUtils.java b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/DateTimeFormatUtils.java
index 73ba3d8..db771f9 100644
--- a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/DateTimeFormatUtils.java
+++ b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/DateTimeFormatUtils.java
@@ -37,7 +37,7 @@
  * <p/>
  * - <b>Y</b>: a digit for the year field. At most 4 year format characters are allowed for a valid format string.<br/>
  * - <b>M</b>: a digit or character for the month field. At most 3 month format characters are allowed for a valid format string. When three month format characters are used, the shorten month names (like JAN, FEB etc.) are expected in the string to be parsed. Otherwise digits are expected.<br/>
- * - <b>Q</b>: (print-only) a digit for the quarter field (1-4). At most 1 format character is allowed.<br/>
+ * - <b>Q</b>: a digit for the quarter field (1-4). At most 2 format characters are allowed.<br/>
  * - <b>D</b>: a digit for the day field. At most 2 day format characters are allowed.<br/>
  * - <b>h</b>: a digit for the hour field. At most 2 hour format characters are allowed.<br/>
  * - <b>m</b>: a digit for the minute field. At most 2 minute format characters are allowed.<br/>
@@ -100,7 +100,7 @@
     private static final char WEEKDAY_CHAR = 'E';
 
     private static final int MAX_YEAR_CHARS = 4;
-    private static final int MAX_QUARTER_CHARS = 1;
+    private static final int MAX_QUARTER_CHARS = 2;
     private static final int MAX_MONTH_CHARS = 4;
     private static final int MAX_DAY_CHARS_PARSE = 2;
     private static final int MAX_DAY_CHARS_PRINT = 3; // + DDD = Day of Year
@@ -341,6 +341,13 @@
                     formatPointer += pointerMove;
                     formatCharCopies += pointerMove;
                     break;
+                case QUARTER_CHAR:
+                    processState = DateTimeProcessState.QUARTER;
+                    pointerMove = parseFormatField(format, formatStart, formatLength, formatPointer, QUARTER_CHAR,
+                            MAX_QUARTER_CHARS);
+                    formatPointer += pointerMove;
+                    formatCharCopies += pointerMove;
+                    break;
                 case MONTH_CHAR:
                     processState = DateTimeProcessState.MONTH;
                     pointerMove = parseFormatField(format, formatStart, formatLength, formatPointer, MONTH_CHAR,
@@ -452,6 +459,7 @@
 
             switch (processState) {
                 case YEAR:
+                case QUARTER:
                 case MONTH:
                 case DAY:
                     if (parseMode == DateTimeParseMode.TIME_ONLY) {
@@ -529,6 +537,52 @@
                         day = parsedValue;
                     }
                     break;
+                case QUARTER:
+                    // the month is in the number format
+                    parsedValue = 0;
+                    int processedQuarterFieldsCount = 0;
+                    for (int i = 0; i < formatCharCopies; i++) {
+                        if (data[dataStart + dataStringPointer] < '0' || data[dataStart + dataStringPointer] > '9') {
+                            if (raiseParseDataError) {
+                                throw new AsterixTemporalTypeParseException("Unexpected char for quarter field at "
+                                        + (dataStart + dataStringPointer) + ": " + data[dataStart + dataStringPointer]);
+                            } else {
+                                return false;
+                            }
+                        }
+                        parsedValue = parsedValue * 10 + (data[dataStart + dataStringPointer] - '0');
+                        dataStringPointer++;
+                        if (processedQuarterFieldsCount++ > 2) {
+                            if (raiseParseDataError) {
+                                throw new AsterixTemporalTypeParseException("Unexpected char for quarter field at "
+                                        + (dataStart + dataStringPointer) + ": " + data[dataStart + dataStringPointer]);
+                            } else {
+                                return false;
+                            }
+                        }
+                    }
+                    // if there are more than 2 digits for the quarter string
+                    while (processedQuarterFieldsCount < 2 && dataStringPointer < dataLength
+                            && data[dataStart + dataStringPointer] >= '0'
+                            && data[dataStart + dataStringPointer] <= '9') {
+                        parsedValue = parsedValue * 10 + (data[dataStart + dataStringPointer] - '0');
+                        dataStringPointer++;
+                        processedQuarterFieldsCount++;
+                    }
+                    if (parsedValue == 0) {
+                        if (raiseParseDataError) {
+                            throw new AsterixTemporalTypeParseException(
+                                    "Incorrect quarter value at " + (dataStart + dataStringPointer));
+                        } else {
+                            return false;
+                        }
+                    }
+                    month = (parsedValue - 1) * 3 + 1;
+                    // Allow day to be missing if we parsed quarter
+                    if (day == 0) {
+                        day = GregorianCalendarSystem.FIELD_MINS[GregorianCalendarSystem.Fields.DAY.ordinal()];
+                    }
+                    break;
                 case MONTH:
                     if (formatCharCopies >= 3) {
                         // the month is in the text format