|
| 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