Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Add maketime and makedate datetime functions #769

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import static org.opensearch.sql.data.type.ExprCoreType.DATE;
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;
import static org.opensearch.sql.data.type.ExprCoreType.INTEGER;
import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL;
import static org.opensearch.sql.data.type.ExprCoreType.LONG;
Expand All @@ -19,6 +20,8 @@
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
Expand All @@ -27,6 +30,7 @@
import org.opensearch.sql.data.model.ExprDatetimeValue;
import org.opensearch.sql.data.model.ExprIntegerValue;
import org.opensearch.sql.data.model.ExprLongValue;
import org.opensearch.sql.data.model.ExprNullValue;
import org.opensearch.sql.data.model.ExprStringValue;
import org.opensearch.sql.data.model.ExprTimeValue;
import org.opensearch.sql.data.model.ExprTimestampValue;
Expand Down Expand Up @@ -64,6 +68,8 @@ public void register(BuiltinFunctionRepository repository) {
repository.register(dayOfYear());
repository.register(from_days());
repository.register(hour());
repository.register(makedate());
repository.register(maketime());
repository.register(microsecond());
repository.register(minute());
repository.register(month());
Expand Down Expand Up @@ -236,6 +242,16 @@ private FunctionResolver hour() {
);
}

private FunctionResolver makedate() {
return define(BuiltinFunctionName.MAKEDATE.getName(),
impl(nullMissingHandling(DateTimeFunction::exprMakeDate), DATE, DOUBLE, DOUBLE));
}

private FunctionResolver maketime() {
return define(BuiltinFunctionName.MAKETIME.getName(),
impl(nullMissingHandling(DateTimeFunction::exprMakeTime), TIME, DOUBLE, DOUBLE, DOUBLE));
}

/**
* MICROSECOND(STRING/TIME/DATETIME/TIMESTAMP). return the microsecond value for time.
*/
Expand Down Expand Up @@ -512,6 +528,50 @@ private ExprValue exprHour(ExprValue time) {
return new ExprIntegerValue(time.timeValue().getHour());
}

/**
* Following MySQL, function receives arguments of type double and rounds them before use.
* Furthermore:
* - zero year interpreted as 2000
* - negative year is not accepted
* - @dayOfYear should be greater than 1
* - if @dayOfYear is greater than 365/366, calculation goes to the next year(s)
*
* @param yearExpr year
* @param dayOfYearExp day of the @year, starting from 1
* @return Date - ExprDateValue object with LocalDate
*/
private ExprValue exprMakeDate(ExprValue yearExpr, ExprValue dayOfYearExp) {
var year = Math.round(yearExpr.doubleValue());
var dayOfYear = Math.round(dayOfYearExp.doubleValue());
// We need to do this to comply with MySQL
if (0 >= dayOfYear || 0 > year) {
return ExprNullValue.of();
}
if (0 == year) {
year = 2000;
}
return new ExprDateValue(LocalDate.ofYearDay((int)year, 1).plusDays(dayOfYear - 1));
}

/**
* Following MySQL, function receives arguments of type double. @hour and @minute are rounded,
* while @second used as is, including fraction part.
* @param hourExpr hour
* @param minuteExpr minute
* @param secondExpr second
* @return Time - ExprTimeValue object with LocalTime
*/
private ExprValue exprMakeTime(ExprValue hourExpr, ExprValue minuteExpr, ExprValue secondExpr) {
var hour = Math.round(hourExpr.doubleValue());
var minute = Math.round(minuteExpr.doubleValue());
var second = secondExpr.doubleValue();
if (0 > hour || 0 > minute || 0 > second) {
return ExprNullValue.of();
}
return new ExprTimeValue(LocalTime.parse(String.format("%02d:%02d:%012.9f",
hour, minute, second), DateTimeFormatter.ISO_TIME));
}

/**
* Microsecond implementation for ExprValue.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public enum BuiltinFunctionName {
DAYOFYEAR(FunctionName.of("dayofyear")),
FROM_DAYS(FunctionName.of("from_days")),
HOUR(FunctionName.of("hour")),
MAKEDATE(FunctionName.of("makedate")),
MAKETIME(FunctionName.of("maketime")),
MICROSECOND(FunctionName.of("microsecond")),
MINUTE(FunctionName.of("minute")),
MONTH(FunctionName.of("month")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static org.opensearch.sql.data.model.ExprValueUtils.stringValue;
import static org.opensearch.sql.data.type.ExprCoreType.DATE;
import static org.opensearch.sql.data.type.ExprCoreType.DATETIME;
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;
import static org.opensearch.sql.data.type.ExprCoreType.INTEGER;
import static org.opensearch.sql.data.type.ExprCoreType.INTERVAL;
import static org.opensearch.sql.data.type.ExprCoreType.LONG;
Expand All @@ -24,7 +25,15 @@
import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP;

import com.google.common.collect.ImmutableList;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Year;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.IntStream;
import lombok.AllArgsConstructor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -43,7 +52,10 @@
import org.opensearch.sql.expression.Expression;
import org.opensearch.sql.expression.ExpressionTestBase;
import org.opensearch.sql.expression.FunctionExpression;
import org.opensearch.sql.expression.config.ExpressionConfig;
import org.opensearch.sql.expression.env.Environment;
import org.opensearch.sql.expression.function.FunctionName;
import org.opensearch.sql.expression.function.FunctionSignature;

@ExtendWith(MockitoExtension.class)
class DateTimeFunctionTest extends ExpressionTestBase {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.mockito.Mockito.when;
import static org.opensearch.sql.data.model.ExprValueUtils.missingValue;
import static org.opensearch.sql.data.model.ExprValueUtils.nullValue;
import static org.opensearch.sql.data.type.ExprCoreType.DOUBLE;

import java.time.LocalDate;
import java.time.Year;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.expression.DSL;
import org.opensearch.sql.expression.Expression;
import org.opensearch.sql.expression.ExpressionTestBase;
import org.opensearch.sql.expression.FunctionExpression;
import org.opensearch.sql.expression.config.ExpressionConfig;
import org.opensearch.sql.expression.env.Environment;
import org.opensearch.sql.expression.function.FunctionName;
import org.opensearch.sql.expression.function.FunctionSignature;

@ExtendWith(MockitoExtension.class)
public class MakeDateTest extends ExpressionTestBase {

@Mock
Environment<Expression, ExprValue> env;

@Mock
Expression nullRef;

@Mock
Expression missingRef;

private FunctionExpression makedate(Expression year, Expression dayOfYear) {
var repo = new ExpressionConfig().functionRepository();
var func = repo.resolve(new FunctionSignature(new FunctionName("makedate"),
List.of(DOUBLE, DOUBLE)));
return (FunctionExpression)func.apply(List.of(year, dayOfYear));
}

private LocalDate makedate(Double year, Double dayOfYear) {
return makedate(DSL.literal(year), DSL.literal(dayOfYear)).valueOf(null).dateValue();
}

@Test
public void checkEdgeCases() {
assertEquals(LocalDate.ofYearDay(2002, 1), makedate(2001., 366.),
"No switch to the next year on getting 366th day of a non-leap year");
assertEquals(LocalDate.ofYearDay(2005, 1), makedate(2004., 367.),
"No switch to the next year on getting 367th day of a leap year");
assertEquals(LocalDate.ofYearDay(2000, 42), makedate(0., 42.),
"0 year is not interpreted as 2000 as in MySQL");
assertEquals(nullValue(), eval(makedate(DSL.literal(-1.), DSL.literal(42.))),
"Negative year doesn't produce NULL");
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(-1.))),
"Negative dayOfYear doesn't produce NULL");
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), DSL.literal(0.))),
"Zero dayOfYear doesn't produce NULL");

assertEquals(LocalDate.of(1999, 3, 1), makedate(1999., 60.),
"Got Feb 29th of a non-lear year");
assertEquals(LocalDate.of(1999, 12, 31), makedate(1999., 365.));
assertEquals(LocalDate.of(2004, 12, 31), makedate(2004., 366.));
}

@Test
public void checkRounding() {
assertEquals(LocalDate.of(42, 1, 1), makedate(42.49, 1.49));
assertEquals(LocalDate.of(43, 1, 2), makedate(42.50, 1.50));
}

@Test
public void checkNullValues() {
when(nullRef.valueOf(env)).thenReturn(nullValue());

assertEquals(nullValue(), eval(makedate(nullRef, DSL.literal(42.))));
assertEquals(nullValue(), eval(makedate(DSL.literal(42.), nullRef)));
assertEquals(nullValue(), eval(makedate(nullRef, nullRef)));
}

@Test
public void checkMissingValues() {
when(missingRef.valueOf(env)).thenReturn(missingValue());

assertEquals(missingValue(), eval(makedate(missingRef, DSL.literal(42.))));
assertEquals(missingValue(), eval(makedate(DSL.literal(42.), missingRef)));
assertEquals(missingValue(), eval(makedate(missingRef, missingRef)));
}

private static Stream<Arguments> getTestData() {
return Stream.of(
Arguments.of(3755.421154, 9.300720),
Arguments.of(3416.922084, 850.832172),
Arguments.of(498.717527, 590.831215),
Arguments.of(1255.402786, 846.041171),
Arguments.of(2491.200868, 832.929840),
Arguments.of(1140.775582, 345.592629),
Arguments.of(2087.208382, 110.392189),
Arguments.of(4582.515870, 763.629197),
Arguments.of(1654.431245, 476.360251),
Arguments.of(1342.494306, 70.108352),
Arguments.of(171.841206, 794.470738),
Arguments.of(5000.103926, 441.461842),
Arguments.of(2957.828371, 273.909052),
Arguments.of(2232.699033, 171.537097),
Arguments.of(4650.163672, 226.857148),
Arguments.of(495.943520, 735.062451),
Arguments.of(4568.187019, 552.394124),
Arguments.of(688.085482, 283.574200),
Arguments.of(4627.662672, 791.729059),
Arguments.of(2812.837393, 397.688304),
Arguments.of(3050.030341, 596.714966),
Arguments.of(3617.452566, 619.795467),
Arguments.of(2210.322073, 106.914268),
Arguments.of(675.757974, 147.702828),
Arguments.of(1101.801820, 40.055318)
);
}

/**
* Test function with given pseudo-random values.
* @param year year
* @param dayOfYear day of year
*/
@ParameterizedTest(name = "year = {0}, dayOfYear = {1}")
@MethodSource("getTestData")
public void checkRandomValues(double year, double dayOfYear) {
LocalDate actual = makedate(year, dayOfYear);
LocalDate expected = getReferenceValue(year, dayOfYear);

assertEquals(expected, actual,
String.format("year = %f, dayOfYear = %f", year, dayOfYear));
}

/**
* Using another algorithm to get reference value.
* We should go to the next year until remaining @dayOfYear is bigger than 365/366.
* @param year Year.
* @param dayOfYear Day of the year.
* @return The calculated date.
*/
private LocalDate getReferenceValue(double year, double dayOfYear) {
var yearL = (int)Math.round(year);
var dayL = (int)Math.round(dayOfYear);
while (true) {
int daysInYear = Year.isLeap(yearL) ? 366 : 365;
if (dayL > daysInYear) {
dayL -= daysInYear;
yearL++;
} else {
break;
}
}
return LocalDate.ofYearDay(yearL, dayL);
}

private ExprValue eval(Expression expression) {
return expression.valueOf(env);
}
}
Loading