Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Commit

Permalink
Support date and time function: week (#757)
Browse files Browse the repository at this point in the history
* add week

* edge case

* fix case 5 & 7

* add IT

* update doc

* fix table

* rename

* add string type

* nit: add newline

* fix type in comment

* nit

* add unit tests for null, missing values

* nit

* address PR comment

Co-authored-by: Rupal Mahajan <>
  • Loading branch information
rupal-bq authored Sep 30, 2020
1 parent a0e9b5c commit d75b7f1
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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).
*/
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")),

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
63 changes: 63 additions & 0 deletions docs/user/dql/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit d75b7f1

Please sign in to comment.