Skip to content

Commit 9f602c3

Browse files
Add maketime and makedate (#102) (#755)
Signed-off-by: Yury Fridlyand <[email protected]> Signed-off-by: Yury Fridlyand <[email protected]>
1 parent 95a24b2 commit 9f602c3

File tree

13 files changed

+573
-4
lines changed

13 files changed

+573
-4
lines changed

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import static org.opensearch.sql.data.type.ExprCoreType.DATE;
1010
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
11+
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;
1112
import static org.opensearch.sql.data.type.ExprCoreType.INTEGER;
1213
import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL;
1314
import static org.opensearch.sql.data.type.ExprCoreType.LONG;
@@ -19,6 +20,8 @@
1920
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;
2021

2122
import java.time.LocalDate;
23+
import java.time.LocalTime;
24+
import java.time.format.DateTimeFormatter;
2225
import java.time.format.TextStyle;
2326
import java.util.Locale;
2427
import java.util.concurrent.TimeUnit;
@@ -27,6 +30,7 @@
2730
import org.opensearch.sql.data.model.ExprDatetimeValue;
2831
import org.opensearch.sql.data.model.ExprIntegerValue;
2932
import org.opensearch.sql.data.model.ExprLongValue;
33+
import org.opensearch.sql.data.model.ExprNullValue;
3034
import org.opensearch.sql.data.model.ExprStringValue;
3135
import org.opensearch.sql.data.model.ExprTimeValue;
3236
import org.opensearch.sql.data.model.ExprTimestampValue;
@@ -64,6 +68,8 @@ public void register(BuiltinFunctionRepository repository) {
6468
repository.register(dayOfYear());
6569
repository.register(from_days());
6670
repository.register(hour());
71+
repository.register(makedate());
72+
repository.register(maketime());
6773
repository.register(microsecond());
6874
repository.register(minute());
6975
repository.register(month());
@@ -236,6 +242,16 @@ private FunctionResolver hour() {
236242
);
237243
}
238244

245+
private FunctionResolver makedate() {
246+
return define(BuiltinFunctionName.MAKEDATE.getName(),
247+
impl(nullMissingHandling(DateTimeFunction::exprMakeDate), DATE, DOUBLE, DOUBLE));
248+
}
249+
250+
private FunctionResolver maketime() {
251+
return define(BuiltinFunctionName.MAKETIME.getName(),
252+
impl(nullMissingHandling(DateTimeFunction::exprMakeTime), TIME, DOUBLE, DOUBLE, DOUBLE));
253+
}
254+
239255
/**
240256
* MICROSECOND(STRING/TIME/DATETIME/TIMESTAMP). return the microsecond value for time.
241257
*/
@@ -512,6 +528,50 @@ private ExprValue exprHour(ExprValue time) {
512528
return new ExprIntegerValue(time.timeValue().getHour());
513529
}
514530

531+
/**
532+
* Following MySQL, function receives arguments of type double and rounds them before use.
533+
* Furthermore:
534+
* - zero year interpreted as 2000
535+
* - negative year is not accepted
536+
* - @dayOfYear should be greater than 1
537+
* - if @dayOfYear is greater than 365/366, calculation goes to the next year(s)
538+
*
539+
* @param yearExpr year
540+
* @param dayOfYearExp day of the @year, starting from 1
541+
* @return Date - ExprDateValue object with LocalDate
542+
*/
543+
private ExprValue exprMakeDate(ExprValue yearExpr, ExprValue dayOfYearExp) {
544+
var year = Math.round(yearExpr.doubleValue());
545+
var dayOfYear = Math.round(dayOfYearExp.doubleValue());
546+
// We need to do this to comply with MySQL
547+
if (0 >= dayOfYear || 0 > year) {
548+
return ExprNullValue.of();
549+
}
550+
if (0 == year) {
551+
year = 2000;
552+
}
553+
return new ExprDateValue(LocalDate.ofYearDay((int)year, 1).plusDays(dayOfYear - 1));
554+
}
555+
556+
/**
557+
* Following MySQL, function receives arguments of type double. @hour and @minute are rounded,
558+
* while @second used as is, including fraction part.
559+
* @param hourExpr hour
560+
* @param minuteExpr minute
561+
* @param secondExpr second
562+
* @return Time - ExprTimeValue object with LocalTime
563+
*/
564+
private ExprValue exprMakeTime(ExprValue hourExpr, ExprValue minuteExpr, ExprValue secondExpr) {
565+
var hour = Math.round(hourExpr.doubleValue());
566+
var minute = Math.round(minuteExpr.doubleValue());
567+
var second = secondExpr.doubleValue();
568+
if (0 > hour || 0 > minute || 0 > second) {
569+
return ExprNullValue.of();
570+
}
571+
return new ExprTimeValue(LocalTime.parse(String.format("%02d:%02d:%012.9f",
572+
hour, minute, second), DateTimeFormatter.ISO_TIME));
573+
}
574+
515575
/**
516576
* Microsecond implementation for ExprValue.
517577
*

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
@@ -68,6 +68,8 @@ public enum BuiltinFunctionName {
6868
DAYOFYEAR(FunctionName.of("dayofyear")),
6969
FROM_DAYS(FunctionName.of("from_days")),
7070
HOUR(FunctionName.of("hour")),
71+
MAKEDATE(FunctionName.of("makedate")),
72+
MAKETIME(FunctionName.of("maketime")),
7173
MICROSECOND(FunctionName.of("microsecond")),
7274
MINUTE(FunctionName.of("minute")),
7375
MONTH(FunctionName.of("month")),

core/src/test/java/org/opensearch/sql/expression/datetime/DateTimeFunctionTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static org.opensearch.sql.data.model.ExprValueUtils.stringValue;
1717
import static org.opensearch.sql.data.type.ExprCoreType.DATE;
1818
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
19+
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;
1920
import static org.opensearch.sql.data.type.ExprCoreType.INTEGER;
2021
import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL;
2122
import static org.opensearch.sql.data.type.ExprCoreType.LONG;
@@ -24,7 +25,15 @@
2425
import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP;
2526

2627
import com.google.common.collect.ImmutableList;
28+
import java.time.Duration;
29+
import java.time.LocalDate;
30+
import java.time.LocalTime;
31+
import java.time.Year;
32+
import java.util.HashSet;
2733
import java.util.List;
34+
import java.util.Random;
35+
import java.util.Set;
36+
import java.util.stream.IntStream;
2837
import lombok.AllArgsConstructor;
2938
import org.junit.jupiter.api.BeforeEach;
3039
import org.junit.jupiter.api.Test;
@@ -43,7 +52,10 @@
4352
import org.opensearch.sql.expression.Expression;
4453
import org.opensearch.sql.expression.ExpressionTestBase;
4554
import org.opensearch.sql.expression.FunctionExpression;
55+
import org.opensearch.sql.expression.config.ExpressionConfig;
4656
import org.opensearch.sql.expression.env.Environment;
57+
import org.opensearch.sql.expression.function.FunctionName;
58+
import org.opensearch.sql.expression.function.FunctionSignature;
4759

4860
@ExtendWith(MockitoExtension.class)
4961
class DateTimeFunctionTest extends ExpressionTestBase {
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
7+
package org.opensearch.sql.expression.datetime;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.mockito.Mockito.when;
11+
import static org.opensearch.sql.data.model.ExprValueUtils.missingValue;
12+
import static org.opensearch.sql.data.model.ExprValueUtils.nullValue;
13+
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;
14+
15+
import java.time.LocalDate;
16+
import java.time.Year;
17+
import java.util.List;
18+
import java.util.stream.Stream;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.Arguments;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.jupiter.MockitoExtension;
26+
import org.opensearch.sql.data.model.ExprValue;
27+
import org.opensearch.sql.expression.DSL;
28+
import org.opensearch.sql.expression.Expression;
29+
import org.opensearch.sql.expression.ExpressionTestBase;
30+
import org.opensearch.sql.expression.FunctionExpression;
31+
import org.opensearch.sql.expression.config.ExpressionConfig;
32+
import org.opensearch.sql.expression.env.Environment;
33+
import org.opensearch.sql.expression.function.FunctionName;
34+
import org.opensearch.sql.expression.function.FunctionSignature;
35+
36+
@ExtendWith(MockitoExtension.class)
37+
public class MakeDateTest extends ExpressionTestBase {
38+
39+
@Mock
40+
Environment<Expression, ExprValue> env;
41+
42+
@Mock
43+
Expression nullRef;
44+
45+
@Mock
46+
Expression missingRef;
47+
48+
private FunctionExpression makedate(Expression year, Expression dayOfYear) {
49+
var repo = new ExpressionConfig().functionRepository();
50+
var func = repo.resolve(new FunctionSignature(new FunctionName("makedate"),
51+
List.of(DOUBLE, DOUBLE)));
52+
return (FunctionExpression)func.apply(List.of(year, dayOfYear));
53+
}
54+
55+
private LocalDate makedate(Double year, Double dayOfYear) {
56+
return makedate(DSL.literal(year), DSL.literal(dayOfYear)).valueOf(null).dateValue();
57+
}
58+
59+
@Test
60+
public void checkEdgeCases() {
61+
assertEquals(LocalDate.ofYearDay(2002, 1), makedate(2001., 366.),
62+
"No switch to the next year on getting 366th day of a non-leap year");
63+
assertEquals(LocalDate.ofYearDay(2005, 1), makedate(2004., 367.),
64+
"No switch to the next year on getting 367th day of a leap year");
65+
assertEquals(LocalDate.ofYearDay(2000, 42), makedate(0., 42.),
66+
"0 year is not interpreted as 2000 as in MySQL");
67+
assertEquals(nullValue(), eval(makedate(DSL.literal(-1.), DSL.literal(42.))),
68+
"Negative year doesn't produce NULL");
69+
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(-1.))),
70+
"Negative dayOfYear doesn't produce NULL");
71+
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(0.))),
72+
"Zero dayOfYear doesn't produce NULL");
73+
74+
assertEquals(LocalDate.of(1999, 3, 1), makedate(1999., 60.),
75+
"Got Feb 29th of a non-lear year");
76+
assertEquals(LocalDate.of(1999, 12, 31), makedate(1999., 365.));
77+
assertEquals(LocalDate.of(2004, 12, 31), makedate(2004., 366.));
78+
}
79+
80+
@Test
81+
public void checkRounding() {
82+
assertEquals(LocalDate.of(42, 1, 1), makedate(42.49, 1.49));
83+
assertEquals(LocalDate.of(43, 1, 2), makedate(42.50, 1.50));
84+
}
85+
86+
@Test
87+
public void checkNullValues() {
88+
when(nullRef.valueOf(env)).thenReturn(nullValue());
89+
90+
assertEquals(nullValue(), eval(makedate(nullRef, DSL.literal(42.))));
91+
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), nullRef)));
92+
assertEquals(nullValue(), eval(makedate(nullRef, nullRef)));
93+
}
94+
95+
@Test
96+
public void checkMissingValues() {
97+
when(missingRef.valueOf(env)).thenReturn(missingValue());
98+
99+
assertEquals(missingValue(), eval(makedate(missingRef, DSL.literal(42.))));
100+
assertEquals(missingValue(), eval(makedate(DSL.literal(42.), missingRef)));
101+
assertEquals(missingValue(), eval(makedate(missingRef, missingRef)));
102+
}
103+
104+
private static Stream<Arguments> getTestData() {
105+
return Stream.of(
106+
Arguments.of(3755.421154, 9.300720),
107+
Arguments.of(3416.922084, 850.832172),
108+
Arguments.of(498.717527, 590.831215),
109+
Arguments.of(1255.402786, 846.041171),
110+
Arguments.of(2491.200868, 832.929840),
111+
Arguments.of(1140.775582, 345.592629),
112+
Arguments.of(2087.208382, 110.392189),
113+
Arguments.of(4582.515870, 763.629197),
114+
Arguments.of(1654.431245, 476.360251),
115+
Arguments.of(1342.494306, 70.108352),
116+
Arguments.of(171.841206, 794.470738),
117+
Arguments.of(5000.103926, 441.461842),
118+
Arguments.of(2957.828371, 273.909052),
119+
Arguments.of(2232.699033, 171.537097),
120+
Arguments.of(4650.163672, 226.857148),
121+
Arguments.of(495.943520, 735.062451),
122+
Arguments.of(4568.187019, 552.394124),
123+
Arguments.of(688.085482, 283.574200),
124+
Arguments.of(4627.662672, 791.729059),
125+
Arguments.of(2812.837393, 397.688304),
126+
Arguments.of(3050.030341, 596.714966),
127+
Arguments.of(3617.452566, 619.795467),
128+
Arguments.of(2210.322073, 106.914268),
129+
Arguments.of(675.757974, 147.702828),
130+
Arguments.of(1101.801820, 40.055318)
131+
);
132+
}
133+
134+
/**
135+
* Test function with given pseudo-random values.
136+
* @param year year
137+
* @param dayOfYear day of year
138+
*/
139+
@ParameterizedTest(name = "year = {0}, dayOfYear = {1}")
140+
@MethodSource("getTestData")
141+
public void checkRandomValues(double year, double dayOfYear) {
142+
LocalDate actual = makedate(year, dayOfYear);
143+
LocalDate expected = getReferenceValue(year, dayOfYear);
144+
145+
assertEquals(expected, actual,
146+
String.format("year = %f, dayOfYear = %f", year, dayOfYear));
147+
}
148+
149+
/**
150+
* Using another algorithm to get reference value.
151+
* We should go to the next year until remaining @dayOfYear is bigger than 365/366.
152+
* @param year Year.
153+
* @param dayOfYear Day of the year.
154+
* @return The calculated date.
155+
*/
156+
private LocalDate getReferenceValue(double year, double dayOfYear) {
157+
var yearL = (int)Math.round(year);
158+
var dayL = (int)Math.round(dayOfYear);
159+
while (true) {
160+
int daysInYear = Year.isLeap(yearL) ? 366 : 365;
161+
if (dayL > daysInYear) {
162+
dayL -= daysInYear;
163+
yearL++;
164+
} else {
165+
break;
166+
}
167+
}
168+
return LocalDate.ofYearDay(yearL, dayL);
169+
}
170+
171+
private ExprValue eval(Expression expression) {
172+
return expression.valueOf(env);
173+
}
174+
}

0 commit comments

Comments
 (0)