Skip to content

Commit 7630f87

Browse files
Add functions ADDTIME and SUBTIME. (#132) (#1194)
* Add functions `ADDTIME` and `SUBTIME`. (#132) Signed-off-by: Yury-Fridlyand <[email protected]>
1 parent 151f4cc commit 7630f87

File tree

13 files changed

+626
-12
lines changed

13 files changed

+626
-12
lines changed

core/src/main/java/org/opensearch/sql/expression/datetime/DateTimeFunction.java

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_SHORT_YEAR;
3131
import static org.opensearch.sql.utils.DateTimeFormatters.DATE_TIME_FORMATTER_STRICT_WITH_TZ;
3232
import static org.opensearch.sql.utils.DateTimeUtils.extractDate;
33+
import static org.opensearch.sql.utils.DateTimeUtils.extractDateTime;
3334

3435
import java.math.BigDecimal;
3536
import java.math.RoundingMode;
@@ -95,6 +96,7 @@ public class DateTimeFunction {
9596
*/
9697
public void register(BuiltinFunctionRepository repository) {
9798
repository.register(adddate());
99+
repository.register(addtime());
98100
repository.register(convert_tz());
99101
repository.register(curtime());
100102
repository.register(curdate());
@@ -134,6 +136,7 @@ public void register(BuiltinFunctionRepository repository) {
134136
repository.register(second(BuiltinFunctionName.SECOND));
135137
repository.register(second(BuiltinFunctionName.SECOND_OF_MINUTE));
136138
repository.register(subdate());
139+
repository.register(subtime());
137140
repository.register(sysdate());
138141
repository.register(time());
139142
repository.register(time_to_sec());
@@ -249,6 +252,52 @@ private DefaultFunctionResolver adddate() {
249252
return add_date(BuiltinFunctionName.ADDDATE.getName());
250253
}
251254

255+
/**
256+
* Adds expr2 to expr1 and returns the result.
257+
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
258+
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
259+
* TODO: MySQL has these signatures too
260+
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
261+
* (x, STRING) -> NULL // second arg - string with timestamp
262+
* (x, STRING/DATE) -> x // second arg - string with date only
263+
*/
264+
private DefaultFunctionResolver addtime() {
265+
return define(BuiltinFunctionName.ADDTIME.getName(),
266+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
267+
TIME, TIME, TIME),
268+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
269+
TIME, TIME, DATE),
270+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
271+
TIME, TIME, DATETIME),
272+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
273+
TIME, TIME, TIMESTAMP),
274+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
275+
DATETIME, DATETIME, TIME),
276+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
277+
DATETIME, DATETIME, DATE),
278+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
279+
DATETIME, DATETIME, DATETIME),
280+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
281+
DATETIME, DATETIME, TIMESTAMP),
282+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
283+
DATETIME, DATE, TIME),
284+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
285+
DATETIME, DATE, DATE),
286+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
287+
DATETIME, DATE, DATETIME),
288+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
289+
DATETIME, DATE, TIMESTAMP),
290+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
291+
DATETIME, TIMESTAMP, TIME),
292+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
293+
DATETIME, TIMESTAMP, DATE),
294+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
295+
DATETIME, TIMESTAMP, DATETIME),
296+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprAddTime),
297+
DATETIME, TIMESTAMP, TIMESTAMP)
298+
);
299+
}
300+
252301
/**
253302
* Converts date/time from a specified timezone to another specified timezone.
254303
* The supported signatures:
@@ -579,6 +628,52 @@ private DefaultFunctionResolver subdate() {
579628
return sub_date(BuiltinFunctionName.SUBDATE.getName());
580629
}
581630

631+
/**
632+
* Subtracts expr2 from expr1 and returns the result.
633+
* (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
634+
* (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
635+
* TODO: MySQL has these signatures too
636+
* (STRING, STRING/TIME) -> STRING // second arg - string with time only
637+
* (x, STRING) -> NULL // second arg - string with timestamp
638+
* (x, STRING/DATE) -> x // second arg - string with date only
639+
*/
640+
private DefaultFunctionResolver subtime() {
641+
return define(BuiltinFunctionName.SUBTIME.getName(),
642+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
643+
TIME, TIME, TIME),
644+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
645+
TIME, TIME, DATE),
646+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
647+
TIME, TIME, DATETIME),
648+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
649+
TIME, TIME, TIMESTAMP),
650+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
651+
DATETIME, DATETIME, TIME),
652+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
653+
DATETIME, DATETIME, DATE),
654+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
655+
DATETIME, DATETIME, DATETIME),
656+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
657+
DATETIME, DATETIME, TIMESTAMP),
658+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
659+
DATETIME, DATE, TIME),
660+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
661+
DATETIME, DATE, DATE),
662+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
663+
DATETIME, DATE, DATETIME),
664+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
665+
DATETIME, DATE, TIMESTAMP),
666+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
667+
DATETIME, TIMESTAMP, TIME),
668+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
669+
DATETIME, TIMESTAMP, DATE),
670+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
671+
DATETIME, TIMESTAMP, DATETIME),
672+
implWithProperties(nullMissingHandlingWithProperties(DateTimeFunction::exprSubTime),
673+
DATETIME, TIMESTAMP, TIMESTAMP)
674+
);
675+
}
676+
582677
/**
583678
* Extracts the time part of a date and time value.
584679
* Also to construct a time type. The supported signatures:
@@ -768,6 +863,39 @@ private ExprValue exprAddDateDays(ExprValue date, ExprValue days) {
768863
: exprValue);
769864
}
770865

866+
/**
867+
* Adds or subtracts time to/from date and returns the result.
868+
*
869+
* @param functionProperties A FunctionProperties object.
870+
* @param temporal A Date/Time/Datetime/Timestamp value to change.
871+
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add/subtract time from.
872+
* @param isAdd A flag: true to add, false to subtract.
873+
* @return A value calculated.
874+
*/
875+
private ExprValue exprApplyTime(FunctionProperties functionProperties,
876+
ExprValue temporal, ExprValue temporalDelta, Boolean isAdd) {
877+
var interval = Duration.between(LocalTime.MIN, temporalDelta.timeValue());
878+
var result = isAdd
879+
? extractDateTime(temporal, functionProperties).plus(interval)
880+
: extractDateTime(temporal, functionProperties).minus(interval);
881+
return temporal.type() == TIME
882+
? new ExprTimeValue(result.toLocalTime())
883+
: new ExprDatetimeValue(result);
884+
}
885+
886+
/**
887+
* Adds time to date and returns the result.
888+
*
889+
* @param functionProperties A FunctionProperties object.
890+
* @param temporal A Date/Time/Datetime/Timestamp value to change.
891+
* @param temporalDelta A Date/Time/Datetime/Timestamp object to add time from.
892+
* @return A value calculated.
893+
*/
894+
private ExprValue exprAddTime(FunctionProperties functionProperties,
895+
ExprValue temporal, ExprValue temporalDelta) {
896+
return exprApplyTime(functionProperties, temporal, temporalDelta, true);
897+
}
898+
771899
/**
772900
* CONVERT_TZ function implementation for ExprValue.
773901
* Returns null for time zones outside of +13:00 and -12:00.
@@ -1181,6 +1309,18 @@ private ExprValue exprSubDateInterval(ExprValue date, ExprValue expr) {
11811309
: exprValue);
11821310
}
11831311

1312+
/**
1313+
* Subtracts expr2 from expr1 and returns the result.
1314+
*
1315+
* @param temporal A Date/Time/Datetime/Timestamp value to change.
1316+
* @param temporalDelta A Date/Time/Datetime/Timestamp to subtract time from.
1317+
* @return A value calculated.
1318+
*/
1319+
private ExprValue exprSubTime(FunctionProperties functionProperties,
1320+
ExprValue temporal, ExprValue temporalDelta) {
1321+
return exprApplyTime(functionProperties, temporal, temporalDelta, false);
1322+
}
1323+
11841324
/**
11851325
* Time implementation for ExprValue.
11861326
*

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public enum BuiltinFunctionName {
5959
* Date and Time Functions.
6060
*/
6161
ADDDATE(FunctionName.of("adddate")),
62+
ADDTIME(FunctionName.of("addtime")),
6263
CONVERT_TZ(FunctionName.of("convert_tz")),
6364
DATE(FunctionName.of("date")),
6465
DATEDIFF(FunctionName.of("datediff")),
@@ -90,6 +91,7 @@ public enum BuiltinFunctionName {
9091
SECOND(FunctionName.of("second")),
9192
SECOND_OF_MINUTE(FunctionName.of("second_of_minute")),
9293
SUBDATE(FunctionName.of("subdate")),
94+
SUBTIME(FunctionName.of("subtime")),
9395
TIME(FunctionName.of("time")),
9496
TIMEDIFF(FunctionName.of("timediff")),
9597
TIME_TO_SEC(FunctionName.of("time_to_sec")),

core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ public Boolean isValidMySqlTimeZoneId(ZoneId zone) {
130130
|| passedTzValidator.isEqual(minTzValidator));
131131
}
132132

133+
/**
134+
* Extracts LocalDateTime from a datetime ExprValue.
135+
* Uses `FunctionProperties` for `ExprTimeValue`.
136+
*/
137+
public static LocalDateTime extractDateTime(ExprValue value,
138+
FunctionProperties functionProperties) {
139+
return value instanceof ExprTimeValue
140+
? ((ExprTimeValue) value).datetimeValue(functionProperties)
141+
: value.datetimeValue();
142+
}
143+
133144
/**
134145
* Extracts LocalDate from a datetime ExprValue.
135146
* Uses `FunctionProperties` for `ExprTimeValue`.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.datetime;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
10+
import static org.opensearch.sql.data.type.ExprCoreType.TIME;
11+
12+
import java.time.Instant;
13+
import java.time.LocalDate;
14+
import java.time.LocalDateTime;
15+
import java.time.LocalTime;
16+
import java.time.temporal.Temporal;
17+
import java.util.stream.Stream;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.Arguments;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
23+
public class AddTimeAndSubTimeTest extends DateTimeTestBase {
24+
25+
@Test
26+
// (TIME, TIME/DATE/DATETIME/TIMESTAMP) -> TIME
27+
public void return_time_when_first_arg_is_time() {
28+
var res = addtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
29+
assertEquals(TIME, res.type());
30+
assertEquals(LocalTime.of(21, 5), res.timeValue());
31+
32+
res = subtime(LocalTime.of(21, 0), LocalTime.of(0, 5));
33+
assertEquals(TIME, res.type());
34+
assertEquals(LocalTime.of(20, 55), res.timeValue());
35+
36+
res = addtime(LocalTime.of(12, 20), Instant.ofEpochSecond(42));
37+
assertEquals(TIME, res.type());
38+
assertEquals(LocalTime.of(12, 20, 42), res.timeValue());
39+
40+
res = subtime(LocalTime.of(10, 0), Instant.ofEpochSecond(42));
41+
assertEquals(TIME, res.type());
42+
assertEquals(LocalTime.of(9, 59, 18), res.timeValue());
43+
44+
res = addtime(LocalTime.of(2, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
45+
assertEquals(TIME, res.type());
46+
assertEquals(LocalTime.of(11, 10, 4), res.timeValue());
47+
48+
res = subtime(LocalTime.of(12, 3, 4), LocalDateTime.of(1961, 4, 12, 9, 7));
49+
assertEquals(TIME, res.type());
50+
assertEquals(LocalTime.of(2, 56, 4), res.timeValue());
51+
52+
res = addtime(LocalTime.of(9, 7), LocalDate.now());
53+
assertEquals(TIME, res.type());
54+
assertEquals(LocalTime.of(9, 7), res.timeValue());
55+
56+
res = subtime(LocalTime.of(9, 7), LocalDate.of(1961, 4, 12));
57+
assertEquals(TIME, res.type());
58+
assertEquals(LocalTime.of(9, 7), res.timeValue());
59+
}
60+
61+
@Test
62+
public void time_limited_by_24_hours() {
63+
var res = addtime(LocalTime.of(21, 0), LocalTime.of(14, 5));
64+
assertEquals(TIME, res.type());
65+
assertEquals(LocalTime.of(11, 5), res.timeValue());
66+
67+
res = subtime(LocalTime.of(14, 0), LocalTime.of(21, 5));
68+
assertEquals(TIME, res.type());
69+
assertEquals(LocalTime.of(16, 55), res.timeValue());
70+
}
71+
72+
// Function signature is:
73+
// (DATE/DATETIME/TIMESTAMP, TIME/DATE/DATETIME/TIMESTAMP) -> DATETIME
74+
private static Stream<Arguments> getTestData() {
75+
return Stream.of(
76+
// DATETIME and TIME/DATE/DATETIME/TIMESTAMP
77+
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalTime.of(1, 48),
78+
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
79+
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDate.of(2000, 1, 1),
80+
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 12, 9, 7)),
81+
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1235, 5, 6, 1, 48),
82+
LocalDateTime.of(1961, 4, 12, 10, 55), LocalDateTime.of(1961, 4, 12, 7, 19)),
83+
Arguments.of(LocalDateTime.of(1961, 4, 12, 9, 7), Instant.ofEpochSecond(42),
84+
LocalDateTime.of(1961, 4, 12, 9, 7, 42), LocalDateTime.of(1961, 4, 12, 9, 6, 18)),
85+
// DATE and TIME/DATE/DATETIME/TIMESTAMP
86+
Arguments.of(LocalDate.of(1961, 4, 12), LocalTime.of(9, 7),
87+
LocalDateTime.of(1961, 4, 12, 9, 7), LocalDateTime.of(1961, 4, 11, 14, 53)),
88+
Arguments.of(LocalDate.of(1961, 4, 12), LocalDate.of(2000, 1, 1),
89+
LocalDateTime.of(1961, 4, 12, 0, 0), LocalDateTime.of(1961, 4, 12, 0, 0)),
90+
Arguments.of(LocalDate.of(1961, 4, 12), LocalDateTime.of(1235, 5, 6, 1, 48),
91+
LocalDateTime.of(1961, 4, 12, 1, 48), LocalDateTime.of(1961, 4, 11, 22, 12)),
92+
Arguments.of(LocalDate.of(1961, 4, 12), Instant.ofEpochSecond(42),
93+
LocalDateTime.of(1961, 4, 12, 0, 0, 42), LocalDateTime.of(1961, 4, 11, 23, 59, 18)),
94+
// TIMESTAMP and TIME/DATE/DATETIME/TIMESTAMP
95+
Arguments.of(Instant.ofEpochSecond(42), LocalTime.of(9, 7),
96+
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
97+
Arguments.of(Instant.ofEpochSecond(42), LocalDate.of(1961, 4, 12),
98+
LocalDateTime.of(1970, 1, 1, 0, 0, 42), LocalDateTime.of(1970, 1, 1, 0, 0, 42)),
99+
Arguments.of(Instant.ofEpochSecond(42), LocalDateTime.of(1961, 4, 12, 9, 7),
100+
LocalDateTime.of(1970, 1, 1, 9, 7, 42), LocalDateTime.of(1969, 12, 31, 14, 53, 42)),
101+
Arguments.of(Instant.ofEpochSecond(42), Instant.ofEpochMilli(42),
102+
LocalDateTime.of(1970, 1, 1, 0, 0, 42, 42000000),
103+
LocalDateTime.of(1970, 1, 1, 0, 0, 41, 958000000))
104+
);
105+
}
106+
107+
/**
108+
* Check that `ADDTIME` and `SUBTIME` functions result value and type.
109+
* @param arg1 First argument.
110+
* @param arg2 Second argument.
111+
* @param addTimeExpectedResult Expected result for `ADDTIME`.
112+
* @param subTimeExpectedResult Expected result for `SUBTIME`.
113+
*/
114+
@ParameterizedTest
115+
@MethodSource("getTestData")
116+
public void return_datetime_when_first_arg_is_not_time(Temporal arg1, Temporal arg2,
117+
LocalDateTime addTimeExpectedResult,
118+
LocalDateTime subTimeExpectedResult) {
119+
var res = addtime(arg1, arg2);
120+
assertEquals(DATETIME, res.type());
121+
assertEquals(addTimeExpectedResult, res.datetimeValue());
122+
123+
res = subtime(arg1, arg2);
124+
assertEquals(DATETIME, res.type());
125+
assertEquals(subTimeExpectedResult, res.datetimeValue());
126+
}
127+
}

0 commit comments

Comments
 (0)