diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java index 495e429767..1a816ad017 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/DSL.java @@ -333,6 +333,10 @@ public FunctionExpression to_days(Expression... expressions) { return function(BuiltinFunctionName.TO_DAYS, expressions); } + public FunctionExpression week(Expression... expressions) { + return function(BuiltinFunctionName.WEEK, expressions); + } + public FunctionExpression year(Expression... expressions) { return function(BuiltinFunctionName.YEAR, expressions); } diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java new file mode 100644 index 0000000000..9956a93201 --- /dev/null +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/CalendarLookup.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.sql.expression.datetime; + +import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; +import com.google.common.collect.ImmutableList; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Calendar; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +class CalendarLookup { + + /** + * Get a calendar for the specific mode. + * @param mode Mode to get calendar for. + * @param date Date to get calendar for. + */ + private static Calendar getCalendar(int mode, LocalDate date) { + if ((mode < 0) || (mode > 7)) { + throw new SemanticCheckException( + String.format("mode:%s is invalid, please use mode value between 0-7", mode)); + } + int day = (mode % 2 == 0) ? Calendar.SUNDAY : Calendar.MONDAY; + if (ImmutableList.of(1, 3).contains(mode)) { + return getCalendar(day, 5, date); + } else if (ImmutableList.of(4, 6).contains(mode)) { + return getCalendar(day, 4, date); + } else { + return getCalendar(day, 7, date); + } + } + + /** + * Set first day of week, minimal days in first week and date in calendar. + * @param firstDayOfWeek the given first day of the week. + * @param minimalDaysInWeek the given minimal days required in the first week of the year. + * @param date the given date. + */ + private static Calendar getCalendar(int firstDayOfWeek, int minimalDaysInWeek, LocalDate date) { + Calendar calendar = Calendar.getInstance(); + calendar.setFirstDayOfWeek(firstDayOfWeek); + calendar.setMinimalDaysInFirstWeek(minimalDaysInWeek); + calendar.set(date.getYear(), date.getMonthValue() - 1, date.getDayOfMonth()); + return calendar; + } + + /** + * Returns week number for date according to mode. + * @param mode Integer for mode. Valid mode values are 0 to 7. + * @param date LocalDate for date. + */ + static int getWeekNumber(int mode, LocalDate date) { + Calendar calendar = getCalendar(mode, date); + int weekNumber = calendar.get(Calendar.WEEK_OF_YEAR); + if ((weekNumber > 51) + && (calendar.get(Calendar.DAY_OF_MONTH) < 7) + && Arrays.asList(0, 1, 4, 5).contains(mode)) { + weekNumber = 0; + } + return weekNumber; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java index bddf583530..b0d145f9af 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunction.java @@ -86,6 +86,7 @@ public void register(BuiltinFunctionRepository repository) { repository.register(time_to_sec()); repository.register(timestamp()); repository.register(to_days()); + repository.register(week()); repository.register(year()); } @@ -371,6 +372,22 @@ private FunctionResolver to_days() { impl(nullMissingHandling(DateTimeFunction::exprToDays), LONG, DATETIME)); } + /** + * WEEK(DATE[,mode]). return the week number for date. + */ + private FunctionResolver week() { + return define(BuiltinFunctionName.WEEK.getName(), + impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATE), + impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, DATETIME), + impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, TIMESTAMP), + impl(nullMissingHandling(DateTimeFunction::exprWeekWithoutMode), INTEGER, STRING), + impl(nullMissingHandling(DateTimeFunction::exprWeek), INTEGER, DATE, INTEGER), + impl(nullMissingHandling(DateTimeFunction::exprWeek), INTEGER, DATETIME, INTEGER), + impl(nullMissingHandling(DateTimeFunction::exprWeek), INTEGER, TIMESTAMP, INTEGER), + impl(nullMissingHandling(DateTimeFunction::exprWeek), INTEGER, STRING, INTEGER) + ); + } + /** * YEAR(STRING/DATE/DATETIME/TIMESTAMP). return the year for date (1000-9999). */ @@ -621,6 +638,26 @@ private ExprValue exprToDays(ExprValue date) { return new ExprLongValue(date.dateValue().toEpochDay() + DAYS_0000_TO_1970); } + /** + * Week for date implementation for ExprValue. + * @param date ExprValue of Date/Datetime/Timestamp/String type. + * @param mode ExprValue of Integer type. + */ + private ExprValue exprWeek(ExprValue date, ExprValue mode) { + return new ExprIntegerValue( + CalendarLookup.getWeekNumber(mode.integerValue(), date.dateValue())); + } + + /** + * Week for date implementation for ExprValue. + * When mode is not specified default value mode 0 is used for default_week_format. + * @param date ExprValue of Date/Datetime/Timestamp/String type. + * @return ExprValue. + */ + private ExprValue exprWeekWithoutMode(ExprValue date) { + return exprWeek(date, new ExprIntegerValue(0)); + } + /** * Year for date implementation for ExprValue. * diff --git a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java index f693779e2f..c403ee88fc 100644 --- a/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/com/amazon/opendistroforelasticsearch/sql/expression/function/BuiltinFunctionName.java @@ -73,6 +73,7 @@ public enum BuiltinFunctionName { TIME_TO_SEC(FunctionName.of("time_to_sec")), TIMESTAMP(FunctionName.of("timestamp")), TO_DAYS(FunctionName.of("to_days")), + WEEK(FunctionName.of("week")), YEAR(FunctionName.of("year")), /** diff --git a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java index 3d60a256b5..88188cebb1 100644 --- a/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java +++ b/core/src/test/java/com/amazon/opendistroforelasticsearch/sql/expression/datetime/DateTimeFunctionTest.java @@ -31,6 +31,7 @@ import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIME; import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.TIMESTAMP; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprDateValue; @@ -39,6 +40,8 @@ import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTimeValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTimestampValue; import com.amazon.opendistroforelasticsearch.sql.data.model.ExprValue; +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType; +import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; import com.amazon.opendistroforelasticsearch.sql.expression.DSL; import com.amazon.opendistroforelasticsearch.sql.expression.Expression; import com.amazon.opendistroforelasticsearch.sql.expression.ExpressionTestBase; @@ -726,6 +729,103 @@ public void timestamp() { assertEquals("timestamp(TIMESTAMP '2020-08-17 01:01:01')", expr.toString()); } + private void testWeek(String date, int mode, int expectedResult) { + FunctionExpression expression = dsl + .week(DSL.literal(new ExprDateValue(date)), DSL.literal(mode)); + assertEquals(INTEGER, expression.type()); + assertEquals(String.format("week(DATE '%s', %d)", date, mode), expression.toString()); + assertEquals(integerValue(expectedResult), eval(expression)); + } + + private void testNullMissingWeek(ExprCoreType date) { + when(nullRef.type()).thenReturn(date); + when(missingRef.type()).thenReturn(date); + assertEquals(nullValue(), eval(dsl.week(nullRef))); + assertEquals(missingValue(), eval(dsl.week(missingRef))); + } + + @Test + public void week() { + testNullMissingWeek(DATE); + testNullMissingWeek(DATETIME); + testNullMissingWeek(TIMESTAMP); + testNullMissingWeek(STRING); + + when(nullRef.type()).thenReturn(INTEGER); + when(missingRef.type()).thenReturn(INTEGER); + assertEquals(nullValue(), eval(dsl.week(DSL.literal("2019-01-05"), nullRef))); + assertEquals(missingValue(), eval(dsl.week(DSL.literal("2019-01-05"), missingRef))); + + when(nullRef.type()).thenReturn(DATE); + when(missingRef.type()).thenReturn(INTEGER); + assertEquals(missingValue(), eval(dsl.week(nullRef, missingRef))); + + FunctionExpression expression = dsl + .week(DSL.literal(new ExprTimestampValue("2019-01-05 01:02:03"))); + assertEquals(INTEGER, expression.type()); + assertEquals("week(TIMESTAMP '2019-01-05 01:02:03')", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + + expression = dsl.week(DSL.literal("2019-01-05")); + assertEquals(INTEGER, expression.type()); + assertEquals("week(\"2019-01-05\")", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + + expression = dsl.week(DSL.literal("2019-01-05 00:01:00")); + assertEquals(INTEGER, expression.type()); + assertEquals("week(\"2019-01-05 00:01:00\")", expression.toString()); + assertEquals(integerValue(0), eval(expression)); + + testWeek("2019-01-05", 0, 0); + testWeek("2019-01-05", 1, 1); + testWeek("2019-01-05", 2, 52); + testWeek("2019-01-05", 3, 1); + testWeek("2019-01-05", 4, 1); + testWeek("2019-01-05", 5, 0); + testWeek("2019-01-05", 6, 1); + testWeek("2019-01-05", 7, 53); + + testWeek("2019-01-06", 0, 1); + testWeek("2019-01-06", 1, 1); + testWeek("2019-01-06", 2, 1); + testWeek("2019-01-06", 3, 1); + testWeek("2019-01-06", 4, 2); + testWeek("2019-01-06", 5, 0); + testWeek("2019-01-06", 6, 2); + testWeek("2019-01-06", 7, 53); + + testWeek("2019-01-07", 0, 1); + testWeek("2019-01-07", 1, 2); + testWeek("2019-01-07", 2, 1); + testWeek("2019-01-07", 3, 2); + testWeek("2019-01-07", 4, 2); + testWeek("2019-01-07", 5, 1); + testWeek("2019-01-07", 6, 2); + testWeek("2019-01-07", 7, 1); + + testWeek("2000-01-01", 0, 0); + testWeek("2000-01-01", 2, 52); + testWeek("1999-12-31", 0, 52); + } + + @Test + public void modeInUnsupportedFormat() { + testNullMissingWeek(DATE); + + FunctionExpression expression1 = dsl + .week(DSL.literal(new ExprDateValue("2019-01-05")), DSL.literal(8)); + SemanticCheckException exception = + assertThrows(SemanticCheckException.class, () -> eval(expression1)); + assertEquals("mode:8 is invalid, please use mode value between 0-7", + exception.getMessage()); + + FunctionExpression expression2 = dsl + .week(DSL.literal(new ExprDateValue("2019-01-05")), DSL.literal(-1)); + exception = assertThrows(SemanticCheckException.class, () -> eval(expression2)); + assertEquals("mode:-1 is invalid, please use mode value between 0-7", + exception.getMessage()); + } + @Test public void to_days() { when(nullRef.type()).thenReturn(DATE); diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index f6a37a901f..72dd446594 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -1357,6 +1357,69 @@ Example:: +------------------------------+ +WEEK +---- + +Description +>>>>>>>>>>> + +Usage: week(date[, mode]) returns the week number for date. If the mode argument is omitted, the default mode 0 is used. + +.. list-table:: The following table describes how the mode argument works. + :widths: 25 50 25 75 + :header-rows: 1 + + * - Mode + - First day of week + - Range + - Week 1 is the first week … + * - 0 + - Sunday + - 0-53 + - with a Sunday in this year + * - 1 + - Monday + - 0-53 + - with 4 or more days this year + * - 2 + - Sunday + - 1-53 + - with a Sunday in this year + * - 3 + - Monday + - 1-53 + - with 4 or more days this year + * - 4 + - Sunday + - 0-53 + - with 4 or more days this year + * - 5 + - Monday + - 0-53 + - with a Monday in this year + * - 6 + - Sunday + - 1-53 + - with 4 or more days this year + * - 7 + - Monday + - 1-53 + - with a Monday in this year + +Argument type: DATE/DATETIME/TIMESTAMP/STRING + +Return type: INTEGER + +Example:: + + >od SELECT WEEK(DATE('2008-02-20')), WEEK(DATE('2008-02-20'), 1) + fetched rows / total rows = 1/1 + +----------------------------+-------------------------------+ + | WEEK(DATE('2008-02-20')) | WEEK(DATE('2008-02-20'), 1) | + |----------------------------|-------------------------------| + | 7 | 8 | + +----------------------------+-------------------------------+ + YEAR ---- diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java index bcac70a94f..46891b482d 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/ppl/DateTimeFunctionIT.java @@ -370,6 +370,27 @@ public void testToDays() throws IOException { verifySome(result.getJSONArray("datarows"), rows(738049)); } + private void week(String date, int mode, int expectedResult) throws IOException { + JSONObject result = executeQuery(String.format( + "source=%s | eval f = week(date('%s'), %d) | fields f", TEST_INDEX_DATE, date, mode)); + verifySchema(result, schema("f", null, "integer")); + verifySome(result.getJSONArray("datarows"), rows(expectedResult)); + } + + @Test + public void testWeek() throws IOException { + JSONObject result = executeQuery(String.format( + "source=%s | eval f = week(date('2008-02-20')) | fields f", TEST_INDEX_DATE)); + verifySchema(result, schema("f", null, "integer")); + verifySome(result.getJSONArray("datarows"), rows(7)); + + week("2008-02-20", 0, 7); + week("2008-02-20", 1, 8); + week("2008-12-31", 1, 53); + week("2000-01-01", 0, 0); + week("2000-01-01", 2, 52); + } + @Test public void testYear() throws IOException { JSONObject result = executeQuery(String.format( diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java index 9d90ffec02..52af0c80c4 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java @@ -353,6 +353,26 @@ public void testYear() throws IOException { verifyDataRows(result, rows(2020)); } + private void week(String date, int mode, int expectedResult) throws IOException { + JSONObject result = executeQuery(String.format("select week(date('%s'), %d)", date, mode)); + verifySchema(result, + schema(String.format("week(date('%s'), %d)", date, mode), null, "integer")); + verifyDataRows(result, rows(expectedResult)); + } + + @Test + public void testWeek() throws IOException { + JSONObject result = executeQuery("select week(date('2008-02-20'))"); + verifySchema(result, schema("week(date('2008-02-20'))", null, "integer")); + verifyDataRows(result, rows(7)); + + week("2008-02-20", 0, 7); + week("2008-02-20", 1, 8); + week("2008-12-31", 1, 53); + week("2000-01-01", 0, 0); + week("2000-01-01", 2, 52); + } + protected JSONObject executeQuery(String query) throws IOException { Request request = new Request("POST", QUERY_API_ENDPOINT); request.setJsonEntity(String.format(Locale.ROOT, "{\n" + " \"query\": \"%s\"\n" + "}", query)); diff --git a/ppl/src/main/antlr/OpenDistroPPLParser.g4 b/ppl/src/main/antlr/OpenDistroPPLParser.g4 index 0fb63c6923..1f822ac007 100644 --- a/ppl/src/main/antlr/OpenDistroPPLParser.g4 +++ b/ppl/src/main/antlr/OpenDistroPPLParser.g4 @@ -235,7 +235,7 @@ trigonometricFunctionName dateAndTimeFunctionBase : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC - | TIMESTAMP | TO_DAYS | YEAR + | TIMESTAMP | TO_DAYS | WEEK | YEAR ; textFunctionBase diff --git a/sql/src/main/antlr/OpenDistroSQLParser.g4 b/sql/src/main/antlr/OpenDistroSQLParser.g4 index 3dd86ad11b..eced4246a9 100644 --- a/sql/src/main/antlr/OpenDistroSQLParser.g4 +++ b/sql/src/main/antlr/OpenDistroSQLParser.g4 @@ -238,7 +238,7 @@ trigonometricFunctionName dateTimeFunctionName : ADDDATE | DATE | DATE_ADD | DATE_SUB | DAY | DAYNAME | DAYOFMONTH | DAYOFWEEK | DAYOFYEAR | FROM_DAYS | HOUR | MICROSECOND | MINUTE | MONTH | MONTHNAME | QUARTER | SECOND | SUBDATE | TIME | TIME_TO_SEC - | TIMESTAMP | TO_DAYS | YEAR + | TIMESTAMP | TO_DAYS | WEEK | YEAR ; textFunctionName