Skip to content

Commit

Permalink
Add functions ADDTIME and SUBTIME. (#132) (#1194)
Browse files Browse the repository at this point in the history
* Add functions `ADDTIME` and `SUBTIME`. (#132)

Signed-off-by: Yury-Fridlyand <[email protected]>
(cherry picked from commit 7630f87)
  • Loading branch information
Yury-Fridlyand authored and github-actions[bot] committed Jan 9, 2023
1 parent 628be97 commit 2a1794f
Show file tree
Hide file tree
Showing 13 changed files with 626 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_SHORT_YEAR;
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_STRICT_WITH_TZ;
import static org.opensearch.sql.utils.DateTimeUtils.extractDate;
import static org.opensearch.sql.utils.DateTimeUtils.extractDateTime;

import java.math.BigDecimal;
import java.math.RoundingMode;
Expand Down Expand Up @@ -95,6 +96,7 @@ public class DateTimeFunction {
*/
public void register(BuiltinFunctionRepository repository) {
repository.register(adddate());
repository.register(addtime());
repository.register(convert_tz());
repository.register(curtime());
repository.register(curdate());
Expand Down Expand Up @@ -132,6 +134,7 @@ public void register(BuiltinFunctionRepository repository) {
repository.register(second(BuiltinFunctionName.SECOND));
repository.register(second(BuiltinFunctionName.SECOND_OF_MINUTE));
repository.register(subdate());
repository.register(subtime());
repository.register(sysdate());
repository.register(time());
repository.register(time_to_sec());
Expand Down Expand Up @@ -247,6 +250,52 @@ private DefaultFunctionResolver adddate() {
return add_date(BuiltinFunctionName.ADDDATE.getName());
}

/**
* Adds expr2 to expr1 and returns the result.
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
* TODO: MySQL has these signatures too
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
* (x, STRING) -> NULL // second arg - string with timestamp
* (x, STRING/DATE) -> x // second arg - string with date only
*/
private DefaultFunctionResolver addtime() {
return define(BuiltinFunctionName.ADDTIME.getName(),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
TIME, TIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATETIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, DATE, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
DATETIME, TIMESTAMP, TIMESTAMP)
);
}

/**
* Converts date/time from a specified timezone to another specified timezone.
* The supported signatures:
Expand Down Expand Up @@ -573,6 +622,52 @@ private DefaultFunctionResolver subdate() {
return sub_date(BuiltinFunctionName.SUBDATE.getName());
}

/**
* Subtracts expr2 from expr1 and returns the result.
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
* TODO: MySQL has these signatures too
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
* (x, STRING) -> NULL // second arg - string with timestamp
* (x, STRING/DATE) -> x // second arg - string with date only
*/
private DefaultFunctionResolver subtime() {
return define(BuiltinFunctionName.SUBTIME.getName(),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
TIME, TIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATETIME, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, DATE, TIMESTAMP),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, TIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, DATE),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, DATETIME),
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
DATETIME, TIMESTAMP, TIMESTAMP)
);
}

/**
* Extracts the time part of a date and time value.
* Also to construct a time type. The supported signatures:
Expand Down Expand Up @@ -752,6 +847,39 @@ private ExprValue exprAddDateDays(ExprValue date, ExprValue days) {
: exprValue);
}

/**
* Adds or subtracts time to/from date and returns the result.
*
* @param functionProperties A FunctionProperties object.
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add/subtract time from.
* @param isAdd A flag: true to add, false to subtract.
* @return A value calculated.
*/
private ExprValue exprApplyTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta, Boolean isAdd) {
var interval = Duration.between(LocalTime.MIN, temporalDelta.timeValue());
var result = isAdd
? extractDateTime(temporal, functionProperties).plus(interval)
: extractDateTime(temporal, functionProperties).minus(interval);
return temporal.type() == TIME
? new ExprTimeValue(result.toLocalTime())
: new ExprDatetimeValue(result);
}

/**
* Adds time to date and returns the result.
*
* @param functionProperties A FunctionProperties object.
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add time from.
* @return A value calculated.
*/
private ExprValue exprAddTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta) {
return exprApplyTime(functionProperties, temporal, temporalDelta, true);
}

/**
* CONVERT_TZ function implementation for ExprValue.
* Returns null for time zones outside of +13:00 and -12:00.
Expand Down Expand Up @@ -1164,6 +1292,18 @@ private ExprValue exprSubDateInterval(ExprValue date, ExprValue expr) {
: exprValue);
}

/**
* Subtracts expr2 from expr1 and returns the result.
*
* @param temporal A Date/Time/Datetime/Timestamp value to change.
* @param temporalDelta A Date/Time/Datetime/Timestamp to subtract time from.
* @return A value calculated.
*/
private ExprValue exprSubTime(FunctionProperties functionProperties,
ExprValue temporal, ExprValue temporalDelta) {
return exprApplyTime(functionProperties, temporal, temporalDelta, false);
}

/**
* Time implementation for ExprValue.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public enum BuiltinFunctionName {
* Date and Time Functions.
*/
ADDDATE(FunctionName.of("adddate")),
ADDTIME(FunctionName.of("addtime")),
CONVERT_TZ(FunctionName.of("convert_tz")),
DATE(FunctionName.of("date")),
DATEDIFF(FunctionName.of("datediff")),
Expand Down Expand Up @@ -88,6 +89,7 @@ public enum BuiltinFunctionName {
SECOND(FunctionName.of("second")),
SECOND_OF_MINUTE(FunctionName.of("second_of_minute")),
SUBDATE(FunctionName.of("subdate")),
SUBTIME(FunctionName.of("subtime")),
TIME(FunctionName.of("time")),
TIMEDIFF(FunctionName.of("timediff")),
TIME_TO_SEC(FunctionName.of("time_to_sec")),
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ public Boolean isValidMySqlTimeZoneId(ZoneId zone) {
|| passedTzValidator.isEqual(minTzValidator));
}

/**
* Extracts LocalDateTime from a datetime ExprValue.
* Uses `FunctionProperties` for `ExprTimeValue`.
*/
public static LocalDateTime extractDateTime(ExprValue value,
FunctionProperties functionProperties) {
return value instanceof ExprTimeValue
? ((ExprTimeValue) value).datetimeValue(functionProperties)
: value.datetimeValue();
}

/**
* Extracts LocalDate from a datetime ExprValue.
* Uses `FunctionProperties` for `ExprTimeValue`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.datetime;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
import static org.opensearch.sql.data.type.ExprCoreType.TIME;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.Temporal;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class AddTimeAndSubTimeTest extends DateTimeTestBase {

@Test
// (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
public void return_time_when_first_arg_is_time() {
var res = addtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(21, 5), res.timeValue());

res = subtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(20, 55), res.timeValue());

res = addtime(LocalTime.of(12, 20), Instant.ofEpochSecond(42));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(12, 20, 42), res.timeValue());

res = subtime(LocalTime.of(10, 0), Instant.ofEpochSecond(42));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 59, 18), res.timeValue());

res = addtime(LocalTime.of(2, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(11, 10, 4), res.timeValue());

res = subtime(LocalTime.of(12, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(2, 56, 4), res.timeValue());

res = addtime(LocalTime.of(9, 7), LocalDate.now());
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 7), res.timeValue());

res = subtime(LocalTime.of(9, 7), LocalDate.of(1961, 4, 12));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(9, 7), res.timeValue());
}

@Test
public void time_limited_by_24_hours() {
var res = addtime(LocalTime.of(21, 0), LocalTime.of(14, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(11, 5), res.timeValue());

res = subtime(LocalTime.of(14, 0), LocalTime.of(21, 5));
assertEquals(TIME, res.type());
assertEquals(LocalTime.of(16, 55), res.timeValue());
}

// Function signature is:
// (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
private static Stream<Arguments> getTestData() {
return Stream.of(
// DATETIME and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalTime.of(1, 48),
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDate.of(2000, 1, 1),
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 12, 9, 7)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1235, 5, 6, 1, 48),
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), Instant.ofEpochSecond(42),
LocalDateTime.of(1961, 4, 12, 9, 7, 42), LocalDateTime.of(1961, 4, 12, 9, 6, 18)),
// DATE and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(LocalDate.of(1961, 4, 12), LocalTime.of(9, 7),
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 11, 14, 53)),
Arguments.of(LocalDate.of(1961, 4, 12), LocalDate.of(2000, 1, 1),
LocalDateTime.of(1961, 4, 12, 0, 0), LocalDateTime.of(1961, 4, 12, 0, 0)),
Arguments.of(LocalDate.of(1961, 4, 12), LocalDateTime.of(1235, 5, 6, 1, 48),
LocalDateTime.of(1961, 4, 12, 1, 48), LocalDateTime.of(1961, 4, 11, 22, 12)),
Arguments.of(LocalDate.of(1961, 4, 12), Instant.ofEpochSecond(42),
LocalDateTime.of(1961, 4, 12, 0, 0, 42), LocalDateTime.of(1961, 4, 11, 23, 59, 18)),
// TIMESTAMP and TIME/DATE/DATETIME/TIMESTAMP
Arguments.of(Instant.ofEpochSecond(42), LocalTime.of(9, 7),
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
Arguments.of(Instant.ofEpochSecond(42), LocalDate.of(1961, 4, 12),
LocalDateTime.of(1970, 1, 1, 0, 0, 42), LocalDateTime.of(1970, 1, 1, 0, 0, 42)),
Arguments.of(Instant.ofEpochSecond(42), LocalDateTime.of(1961, 4, 12, 9, 7),
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
Arguments.of(Instant.ofEpochSecond(42), Instant.ofEpochMilli(42),
LocalDateTime.of(1970, 1, 1, 0, 0, 42, 42000000),
LocalDateTime.of(1970, 1, 1, 0, 0, 41, 958000000))
);
}

/**
* Check that `ADDTIME` and `SUBTIME` functions result value and type.
* @param arg1 First argument.
* @param arg2 Second argument.
* @param addTimeExpectedResult Expected result for `ADDTIME`.
* @param subTimeExpectedResult Expected result for `SUBTIME`.
*/
@ParameterizedTest
@MethodSource("getTestData")
public void return_datetime_when_first_arg_is_not_time(Temporal arg1, Temporal arg2,
LocalDateTime addTimeExpectedResult,
LocalDateTime subTimeExpectedResult) {
var res = addtime(arg1, arg2);
assertEquals(DATETIME, res.type());
assertEquals(addTimeExpectedResult, res.datetimeValue());

res = subtime(arg1, arg2);
assertEquals(DATETIME, res.type());
assertEquals(subTimeExpectedResult, res.datetimeValue());
}
}
Loading

0 comments on commit 2a1794f

Please sign in to comment.