Skip to content

Commit

Permalink
Add maketime and makedate (#102) (#755)
Browse files Browse the repository at this point in the history
Signed-off-by: Yury Fridlyand <[email protected]>

Signed-off-by: Yury Fridlyand <[email protected]>
  • Loading branch information
Yury-Fridlyand authored Aug 16, 2022
1 parent 95a24b2 commit 9f602c3
Show file tree
Hide file tree
Showing 13 changed files with 573 additions and 4 deletions.
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

0 comments on commit 9f602c3

Please sign in to comment.