[NO ISSUE][FUN] Add quarter_of_year() function

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

Details:
- Add quarter_of_year() function that returns
  a quarter of the year for datetime and date values
- Add tests and update documentation

Change-Id: I6905b2ec8dbd28556454e37053c0e276730941c7
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/13285
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Dmitry Lychagin <dmitry.lychagin@couchbase.com>
Reviewed-by: Ian Maxon <imaxon@uci.edu>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/TemporalQueries.xml b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/TemporalQueries.xml
index 8fb387e..8b48ca6 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/TemporalQueries.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/TemporalQueries.xml
@@ -83,6 +83,11 @@
       </compilation-unit>
     </test-case>
     <test-case FilePath="temporal">
+      <compilation-unit name="quarter_of_year_01">
+        <output-dir compare="Text">quarter_of_year_01</output-dir>
+      </compilation-unit>
+    </test-case>
+    <test-case FilePath="temporal">
       <compilation-unit name="interval_bin">
         <output-dir compare="Text">interval_bin</output-dir>
       </compilation-unit>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/quarter_of_year_01/quarter_of_year_01.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/quarter_of_year_01/quarter_of_year_01.1.query.sqlpp
new file mode 100644
index 0000000..a11b3c1
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/temporal/quarter_of_year_01/quarter_of_year_01.1.query.sqlpp
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+/*
+ * Quarter of year
+ */
+
+select month, qoy1, qoy2, count(*) cnt
+from range(0, 365) r
+let
+  s = unix_time_from_date_in_days(date("2020-01-01")),
+  d = date_from_unix_time_in_days(s + r),
+  dt = datetime_from_date_time(d, time("01:01:01")),
+  month = get_month(d),
+  qoy1 = quarter_of_year(d),
+  qoy2 = quarter_of_year(dt)
+group by qoy1, qoy2, month
+order by qoy1, qoy2, month;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/quarter_of_year_01/quarter_of_year_01.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/quarter_of_year_01/quarter_of_year_01.1.adm
new file mode 100644
index 0000000..9537fbe
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/temporal/quarter_of_year_01/quarter_of_year_01.1.adm
@@ -0,0 +1,12 @@
+{ "month": 1, "qoy1": 1, "qoy2": 1, "cnt": 31 }
+{ "month": 2, "qoy1": 1, "qoy2": 1, "cnt": 29 }
+{ "month": 3, "qoy1": 1, "qoy2": 1, "cnt": 31 }
+{ "month": 4, "qoy1": 2, "qoy2": 2, "cnt": 30 }
+{ "month": 5, "qoy1": 2, "qoy2": 2, "cnt": 31 }
+{ "month": 6, "qoy1": 2, "qoy2": 2, "cnt": 30 }
+{ "month": 7, "qoy1": 3, "qoy2": 3, "cnt": 31 }
+{ "month": 8, "qoy1": 3, "qoy2": 3, "cnt": 31 }
+{ "month": 9, "qoy1": 3, "qoy2": 3, "cnt": 30 }
+{ "month": 10, "qoy1": 4, "qoy2": 4, "cnt": 31 }
+{ "month": 11, "qoy1": 4, "qoy2": 4, "cnt": 30 }
+{ "month": 12, "qoy1": 4, "qoy2": 4, "cnt": 31 }
\ 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 2e08b77..dcb252b 100644
--- a/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md
+++ b/asterixdb/asterix-doc/src/main/markdown/builtins/7_temporal.md
@@ -394,6 +394,28 @@
 
         { "week_1": 48, "week_2": 49, "week_3": 49, "week_4": 49 }
 
+### quarter_of_year ###
+* Syntax:
+
+        quarter_of_year(date)
+
+* Finds the quarter of the year for a given date
+* Arguments:
+    * `date`: a `date` or a `datetime` value
+* Return Value:
+    * an `bigint` representing the quarter of the year (1_4),
+    * `missing` if the argument is a `missing` value,
+    * `null` if the argument is a `null` value,
+    * any other non-date input value will cause a type error.
+
+* Example:
+
+        quarter_of_year(date("2011-12-31"));
+
+* The expected result is:
+
+        4
+
 ### datetime_from_date_time ###
 * Syntax:
 
diff --git a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/GregorianCalendarSystem.java b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/GregorianCalendarSystem.java
index c8d171f..2f838be 100644
--- a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/GregorianCalendarSystem.java
+++ b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/base/temporal/GregorianCalendarSystem.java
@@ -654,6 +654,14 @@
     }
 
     /**
+     * Get the quarter of the year for the given chronon time and the year.
+     */
+    public int getQuarterOfYear(long millis, int year) {
+        int month = getMonthOfYear(millis, year);
+        return (month - 1) / 3 + 1;
+    }
+
+    /**
      * Get the hour of the day for the given chronon time.
      *
      * @param millis
diff --git a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
index 14b4bab..f9f61fe 100644
--- a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
+++ b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
@@ -1471,6 +1471,8 @@
             new FunctionIdentifier(FunctionConstants.ASTERIX_NS, "day-of-week", 2);
     public static final FunctionIdentifier DAY_OF_YEAR =
             new FunctionIdentifier(FunctionConstants.ASTERIX_NS, "day-of-year", 1);
+    public static final FunctionIdentifier QUARTER_OF_YEAR =
+            new FunctionIdentifier(FunctionConstants.ASTERIX_NS, "quarter-of-year", 1);
     public static final FunctionIdentifier WEEK_OF_YEAR =
             new FunctionIdentifier(FunctionConstants.ASTERIX_NS, "week-of-year", 1);
     public static final FunctionIdentifier WEEK_OF_YEAR2 =
@@ -2383,6 +2385,7 @@
         addFunction(DAY_OF_WEEK, AInt64TypeComputer.INSTANCE, true);
         addFunction(DAY_OF_WEEK2, AInt64TypeComputer.INSTANCE_NULLABLE, true);
         addFunction(DAY_OF_YEAR, AInt64TypeComputer.INSTANCE, true);
+        addFunction(QUARTER_OF_YEAR, AInt64TypeComputer.INSTANCE, true);
         addFunction(WEEK_OF_YEAR, AInt64TypeComputer.INSTANCE, true);
         addFunction(WEEK_OF_YEAR2, AInt64TypeComputer.INSTANCE_NULLABLE, true);
         addFunction(PARSE_DATE, ADateTypeComputer.INSTANCE, true);
diff --git a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/temporal/QuarterOfYearDescriptor.java b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/temporal/QuarterOfYearDescriptor.java
new file mode 100644
index 0000000..bd54db0
--- /dev/null
+++ b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/evaluators/functions/temporal/QuarterOfYearDescriptor.java
@@ -0,0 +1,143 @@
+/*
+ * 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.asterix.runtime.evaluators.functions.temporal;
+
+import java.io.DataOutput;
+import java.io.IOException;
+
+import org.apache.asterix.common.annotations.MissingNullInOutFunction;
+import org.apache.asterix.dataflow.data.nontagged.serde.ADurationSerializerDeserializer;
+import org.apache.asterix.dataflow.data.nontagged.serde.AInt32SerializerDeserializer;
+import org.apache.asterix.dataflow.data.nontagged.serde.AInt64SerializerDeserializer;
+import org.apache.asterix.dataflow.data.nontagged.serde.AYearMonthDurationSerializerDeserializer;
+import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
+import org.apache.asterix.om.base.AInt64;
+import org.apache.asterix.om.base.AMutableInt64;
+import org.apache.asterix.om.base.temporal.GregorianCalendarSystem;
+import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.asterix.om.functions.IFunctionDescriptorFactory;
+import org.apache.asterix.om.types.ATypeTag;
+import org.apache.asterix.om.types.BuiltinType;
+import org.apache.asterix.runtime.evaluators.base.AbstractScalarFunctionDynamicDescriptor;
+import org.apache.asterix.runtime.evaluators.functions.PointableHelper;
+import org.apache.asterix.runtime.exceptions.TypeMismatchException;
+import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.runtime.base.IEvaluatorContext;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluator;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluatorFactory;
+import org.apache.hyracks.api.dataflow.value.ISerializerDeserializer;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.data.std.api.IPointable;
+import org.apache.hyracks.data.std.primitive.VoidPointable;
+import org.apache.hyracks.data.std.util.ArrayBackedValueStorage;
+import org.apache.hyracks.dataflow.common.data.accessors.IFrameTupleReference;
+
+@MissingNullInOutFunction
+public class QuarterOfYearDescriptor extends AbstractScalarFunctionDynamicDescriptor {
+    private static final long serialVersionUID = 1L;
+    private static final FunctionIdentifier FID = BuiltinFunctions.QUARTER_OF_YEAR;
+    public static final IFunctionDescriptorFactory FACTORY = QuarterOfYearDescriptor::new;
+
+    @Override
+    public IScalarEvaluatorFactory createEvaluatorFactory(final IScalarEvaluatorFactory[] args) {
+        return new IScalarEvaluatorFactory() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public IScalarEvaluator createScalarEvaluator(IEvaluatorContext ctx) throws HyracksDataException {
+                return new IScalarEvaluator() {
+                    private final ArrayBackedValueStorage resultStorage = new ArrayBackedValueStorage();
+                    private final DataOutput out = resultStorage.getDataOutput();
+                    private final IPointable argPtr = new VoidPointable();
+                    private final IScalarEvaluator eval = args[0].createScalarEvaluator(ctx);
+
+                    private final GregorianCalendarSystem calSystem = GregorianCalendarSystem.getInstance();
+
+                    // for output: type integer
+                    @SuppressWarnings("unchecked")
+                    private final ISerializerDeserializer<AInt64> intSerde =
+                            SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.AINT64);
+                    private final AMutableInt64 aMutableInt64 = new AMutableInt64(0);
+
+                    @Override
+                    public void evaluate(IFrameTupleReference tuple, IPointable result) throws HyracksDataException {
+                        eval.evaluate(tuple, argPtr);
+
+                        if (PointableHelper.checkAndSetMissingOrNull(result, argPtr)) {
+                            return;
+                        }
+
+                        byte[] bytes = argPtr.getByteArray();
+                        int startOffset = argPtr.getStartOffset();
+
+                        resultStorage.reset();
+                        try {
+                            if (bytes[startOffset] == ATypeTag.SERIALIZED_DURATION_TYPE_TAG) {
+                                int durationMonth = calSystem.getDurationMonth(
+                                        ADurationSerializerDeserializer.getYearMonth(bytes, startOffset + 1));
+                                int durationQuarter = durationMonth / 3;
+                                aMutableInt64.setValue(durationQuarter);
+                                intSerde.serialize(aMutableInt64, out);
+                                result.set(resultStorage);
+                                return;
+                            }
+                            if (bytes[startOffset] == ATypeTag.SERIALIZED_YEAR_MONTH_DURATION_TYPE_TAG) {
+                                int durationMonth = calSystem.getDurationMonth(
+                                        AYearMonthDurationSerializerDeserializer.getYearMonth(bytes, startOffset + 1));
+                                int durationQuarter = durationMonth / 3;
+                                aMutableInt64.setValue(durationQuarter);
+                                intSerde.serialize(aMutableInt64, out);
+                                result.set(resultStorage);
+                                return;
+                            }
+
+                            long chrononTimeInMs;
+                            if (bytes[startOffset] == ATypeTag.SERIALIZED_DATE_TYPE_TAG) {
+                                chrononTimeInMs = AInt32SerializerDeserializer.getInt(bytes, startOffset + 1)
+                                        * GregorianCalendarSystem.CHRONON_OF_DAY;
+                            } else if (bytes[startOffset] == ATypeTag.SERIALIZED_DATETIME_TYPE_TAG) {
+                                chrononTimeInMs = AInt64SerializerDeserializer.getLong(bytes, startOffset + 1);
+                            } else {
+                                throw new TypeMismatchException(sourceLoc, getIdentifier(), 0, bytes[startOffset],
+                                        ATypeTag.SERIALIZED_DURATION_TYPE_TAG,
+                                        ATypeTag.SERIALIZED_YEAR_MONTH_DURATION_TYPE_TAG,
+                                        ATypeTag.SERIALIZED_DATE_TYPE_TAG, ATypeTag.SERIALIZED_DATETIME_TYPE_TAG);
+                            }
+
+                            int year = calSystem.getYear(chrononTimeInMs);
+                            int quarter = calSystem.getQuarterOfYear(chrononTimeInMs, year);
+
+                            aMutableInt64.setValue(quarter);
+                            intSerde.serialize(aMutableInt64, out);
+                        } catch (IOException e) {
+                            throw HyracksDataException.create(e);
+                        }
+                        result.set(resultStorage);
+                    }
+                };
+            }
+        };
+    }
+
+    @Override
+    public FunctionIdentifier getIdentifier() {
+        return FID;
+    }
+}
diff --git a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
index 3677bc8..cc9cca9 100644
--- a/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
+++ b/asterixdb/asterix-runtime/src/main/java/org/apache/asterix/runtime/functions/FunctionCollection.java
@@ -566,6 +566,7 @@
 import org.apache.asterix.runtime.evaluators.functions.temporal.PrintDateDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.temporal.PrintDateTimeDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.temporal.PrintTimeDescriptor;
+import org.apache.asterix.runtime.evaluators.functions.temporal.QuarterOfYearDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.temporal.TimeFromDatetimeDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.temporal.TimeFromUnixTimeInMsDescriptor;
 import org.apache.asterix.runtime.evaluators.functions.temporal.UnixTimeFromDateInDaysDescriptor;
@@ -1188,6 +1189,7 @@
         fc.add(DayOfWeekDescriptor.FACTORY);
         fc.add(DayOfWeek2Descriptor.FACTORY);
         fc.add(DayOfYearDescriptor.FACTORY);
+        fc.add(QuarterOfYearDescriptor.FACTORY);
         fc.add(WeekOfYearDescriptor.FACTORY);
         fc.add(WeekOfYear2Descriptor.FACTORY);
         fc.add(ParseDateDescriptor.FACTORY);