forked from opensearch-project/opensearch-spark
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of relative date time logic and unit tests
Signed-off-by: currantw <[email protected]>
- Loading branch information
Showing
2 changed files
with
393 additions
and
0 deletions.
There are no files selected for viewing
224 changes: 224 additions & 0 deletions
224
ppl-spark-integration/src/main/java/org/opensearch/sql/expression/function/TimeUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.sql.expression.function; | ||
|
||
import lombok.experimental.UtilityClass; | ||
|
||
import java.time.DayOfWeek; | ||
import java.time.Duration; | ||
import java.time.LocalDateTime; | ||
import java.time.Period; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
@UtilityClass | ||
public class TimeUtils { | ||
|
||
private static final String now = "now"; | ||
private static final String negativeSign = "-"; | ||
|
||
// Pattern for relative date time string | ||
private static final String patternStringOffset = "(?<offsetSign>[+-])(?<offsetValue>\\d+)?(?<offsetUnit>\\w+)"; | ||
private static final String patternStringSnap = "[@](?<snapUnit>\\w+)"; | ||
private static final String patternStringRelative = String.format("(?<offset>%s)?(?<snap>%s)?", patternStringOffset, patternStringSnap); | ||
|
||
private static final Pattern pattern = Pattern.compile(patternStringRelative); | ||
|
||
// Time unit constants | ||
private static final Set<String> secondUnits = Set.of("s", "sec", "secs", "second", "seconds"); | ||
private static final Set<String> minuteUnits = Set.of("m", "min", "mins", "minute", "minutes"); | ||
private static final Set<String> hourUnits = Set.of("h", "hr", "hrs", "hour", "hours"); | ||
private static final Set<String> dayUnits = Set.of("d", "day", "days"); | ||
private static final Set<String> weekUnits = Set.of("w", "wk", "wks", "week", "weeks"); | ||
private static final Set<String> monthUnits = Set.of("mon", "month", "months"); | ||
private static final Set<String> quarterUnits = Set.of("q", "qtr", "qtrs", "quarter", "quarters"); | ||
private static final Set<String> yearUnits = Set.of("y", "yr", "yrs", "year", "years"); | ||
|
||
// Map from time unit constants to the corresponding duration. | ||
private static final Duration secondDuration = Duration.ofSeconds(1); | ||
private static final Duration minuteDuration = Duration.ofMinutes(1); | ||
private static final Duration hourDuration = Duration.ofHours(1); | ||
|
||
private static final Map<String, Duration> durationForTimeUnit = Map.ofEntries( | ||
Map.entry("s", secondDuration), | ||
Map.entry("sec", secondDuration), | ||
Map.entry("secs", secondDuration), | ||
Map.entry("second", secondDuration), | ||
Map.entry("seconds", secondDuration), | ||
|
||
Map.entry("m", minuteDuration), | ||
Map.entry("min", minuteDuration), | ||
Map.entry("mins", minuteDuration), | ||
Map.entry("minute", minuteDuration), | ||
Map.entry("minutes", minuteDuration), | ||
|
||
Map.entry("h", hourDuration), | ||
Map.entry("hr", hourDuration), | ||
Map.entry("hrs", hourDuration), | ||
Map.entry("hour", hourDuration), | ||
Map.entry("hours", hourDuration)); | ||
|
||
// Map from time unit constants to the corresponding period. | ||
private static final Period periodDay = Period.ofDays(1); | ||
private static final Period periodWeek = Period.ofWeeks(1); | ||
private static final Period periodMonth = Period.ofMonths(1); | ||
private static final Period periodQuarter = Period.ofMonths(3); | ||
private static final Period periodYear = Period.ofYears(1); | ||
|
||
private static final Map<String, Period> periodForTimeUnit = Map.ofEntries( | ||
Map.entry("d", periodDay), | ||
Map.entry("day", periodDay), | ||
Map.entry("days", periodDay), | ||
|
||
Map.entry("w", periodWeek), | ||
Map.entry("wk", periodWeek), | ||
Map.entry("wks", periodWeek), | ||
Map.entry("week", periodWeek), | ||
Map.entry("weeks", periodWeek), | ||
|
||
Map.entry("mon", periodMonth), | ||
Map.entry("month", periodMonth), | ||
Map.entry("months", periodMonth), | ||
|
||
Map.entry("q", periodQuarter), | ||
Map.entry("qtr", periodQuarter), | ||
Map.entry("qtrs", periodQuarter), | ||
Map.entry("quarter", periodQuarter), | ||
Map.entry("quarters", periodQuarter), | ||
|
||
Map.entry("y", periodYear), | ||
Map.entry("yr", periodYear), | ||
Map.entry("yrs", periodYear), | ||
Map.entry("year", periodYear), | ||
Map.entry("years", periodYear)); | ||
|
||
// Maps from day of the week unit constants to the corresponding day of the week. | ||
private static final Map<String, DayOfWeek> daysOfWeekForUnit = Map.ofEntries( | ||
Map.entry("w0", DayOfWeek.SUNDAY), | ||
Map.entry("w7", DayOfWeek.SUNDAY), | ||
Map.entry("w1", DayOfWeek.MONDAY), | ||
Map.entry("w2", DayOfWeek.TUESDAY), | ||
Map.entry("w3", DayOfWeek.WEDNESDAY), | ||
Map.entry("w4", DayOfWeek.THURSDAY), | ||
Map.entry("w5", DayOfWeek.FRIDAY), | ||
Map.entry("w6", DayOfWeek.SATURDAY)); | ||
|
||
static final int DAYS_PER_WEEK = 7; | ||
static final int MONTHS_PER_QUARTER = 3; | ||
|
||
/** | ||
* Returns the {@link LocalDateTime} corresponding to the given relative date time string and date time. | ||
* Throws {@link RuntimeException} if the relative date time string is not supported. | ||
*/ | ||
public static LocalDateTime getRelativeDateTime(String relativeDateTimeString, LocalDateTime dateTime) { | ||
|
||
if (relativeDateTimeString.equals(now)) { | ||
return dateTime; | ||
} | ||
|
||
Matcher matcher = pattern.matcher(relativeDateTimeString); | ||
if (!matcher.matches()) { | ||
String message = String.format("The relative date time '%s' is not supported.", relativeDateTimeString); | ||
throw new RuntimeException(message); | ||
} | ||
|
||
LocalDateTime relativeDateTime = dateTime; | ||
|
||
if (matcher.group("offset") != null) { | ||
relativeDateTime = applyOffset( | ||
relativeDateTime, | ||
matcher.group("offsetSign"), | ||
matcher.group("offsetValue"), | ||
matcher.group("offsetUnit") | ||
); | ||
} | ||
|
||
if (matcher.group("snap") != null) { | ||
relativeDateTime = applySnap( | ||
relativeDateTime, | ||
matcher.group("snapUnit") | ||
); | ||
} | ||
|
||
return relativeDateTime; | ||
} | ||
|
||
/** | ||
* Applies the offset specified by the offset sign, value, and unit to the given date time, and returns the result. | ||
*/ | ||
private LocalDateTime applyOffset(LocalDateTime dateTime, String offsetSignString, String offsetValueString, String offsetUnitString) { | ||
int offsetValue = Optional.ofNullable(offsetValueString).map(Integer::parseInt).orElse(1); | ||
if (offsetSignString.equals(negativeSign)) { | ||
offsetValue *= -1; | ||
} | ||
|
||
/* {@link Duration} and {@link Period} must be handled separately because, even | ||
though they both inherit from {@link java.time.temporal.TemporalAmount}, they | ||
define separate 'multipliedBy' methods. */ | ||
|
||
if (durationForTimeUnit.containsKey(offsetUnitString)) { | ||
final Duration offsetDuration = durationForTimeUnit.get(offsetUnitString).multipliedBy(offsetValue); | ||
return dateTime.plus(offsetDuration); | ||
} | ||
|
||
if (periodForTimeUnit.containsKey(offsetUnitString)) { | ||
final Period offsetPeriod = periodForTimeUnit.get(offsetUnitString).multipliedBy(offsetValue); | ||
return dateTime.plus(offsetPeriod); | ||
} | ||
|
||
final String message = String.format("The relative date time unit '%s' is not supported.", offsetUnitString); | ||
throw new RuntimeException(message); | ||
} | ||
|
||
/** | ||
* Snaps the given date time to the start of the previous time period specified by the given snap unit, and returns the result. | ||
*/ | ||
private LocalDateTime applySnap(LocalDateTime dateTime, String snapUnit) { | ||
|
||
if (secondUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.SECONDS); | ||
} else if (minuteUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.MINUTES); | ||
} else if (hourUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.HOURS); | ||
} else if (dayUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS); | ||
} else if (weekUnits.contains(snapUnit)) { | ||
return applySnapToDay(dateTime, DayOfWeek.SUNDAY); | ||
} else if (monthUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1); | ||
} else if (quarterUnits.contains(snapUnit)) { | ||
int monthsToSnap = (dateTime.getMonthValue() - 1) % MONTHS_PER_QUARTER; | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1).minusMonths(monthsToSnap); | ||
} else if (yearUnits.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); | ||
} else if (daysOfWeekForUnit.containsKey(snapUnit)) { | ||
return applySnapToDay(dateTime, daysOfWeekForUnit.get(snapUnit)); | ||
} | ||
|
||
final String message = String.format("The relative date time unit '%s' is not supported.", snapUnit); | ||
throw new RuntimeException(message); | ||
} | ||
|
||
/** | ||
* Snaps the given date time to the start of the previous specified day of the week, and returns the result. | ||
*/ | ||
private LocalDateTime applySnapToDay(LocalDateTime dateTime, DayOfWeek snapDay) { | ||
LocalDateTime snapped = dateTime.truncatedTo(ChronoUnit.DAYS); | ||
|
||
DayOfWeek day = dateTime.getDayOfWeek(); | ||
if (day.equals(snapDay)) { | ||
return snapped; | ||
} | ||
|
||
int daysToSnap = DAYS_PER_WEEK - snapDay.getValue() + day.getValue(); | ||
return snapped.minusDays(daysToSnap); | ||
} | ||
} |
169 changes: 169 additions & 0 deletions
169
...spark-integration/src/test/java/org/opensearch/sql/expression/function/TimeUtilsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.sql.expression.function; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertThrows; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
import org.junit.Test; | ||
|
||
public class TimeUtilsTest { | ||
|
||
// Monday, Jan 03, 2000 @ 01:01:01.100 | ||
private final LocalDateTime dateTime = LocalDateTime.parse("2000-01-03T01:01:01.100"); | ||
|
||
@Test | ||
public void testRelative() { | ||
testValid("now", "2000-01-03T01:01:01.100"); | ||
testValid("-60m", "2000-01-03T00:01:01.100"); | ||
testValid("-h", "2000-01-03T00:01:01.100"); | ||
testValid("+2wk", "2000-01-17T01:01:01.100"); | ||
testValid("-1h@h", "2000-01-03T00:00"); | ||
testValid("@d", "2000-01-03T00:00"); | ||
|
||
testInvalid("INVALID", "The relative date time 'INVALID' is not supported."); | ||
} | ||
|
||
@Test | ||
public void testRelativeOffsetSign() { | ||
testValid("+h", "2000-01-03T02:01:01.100"); | ||
testValid("-h", "2000-01-03T00:01:01.100"); | ||
|
||
testInvalid("~h", "The relative date time '~h' is not supported."); | ||
} | ||
|
||
@Test | ||
public void testRelativeOffsetValue() { | ||
testValid("+h", "2000-01-03T02:01:01.100"); | ||
testValid("+0h", "2000-01-03T01:01:01.100"); | ||
testValid("+1h", "2000-01-03T02:01:01.100"); | ||
testValid("+12h", "2000-01-03T13:01:01.100"); | ||
|
||
testInvalid("+1.1h", "The relative date time '+1.1h' is not supported."); | ||
} | ||
|
||
@Test | ||
public void testRelativeOffsetUnit() { | ||
testValid("+s", "2000-01-03T01:01:02.1"); | ||
testValid("+sec", "2000-01-03T01:01:02.1"); | ||
testValid("+secs", "2000-01-03T01:01:02.1"); | ||
testValid("+second", "2000-01-03T01:01:02.1"); | ||
testValid("+seconds", "2000-01-03T01:01:02.1"); | ||
|
||
testValid("+m", "2000-01-03T01:02:01.100"); | ||
testValid("+min", "2000-01-03T01:02:01.100"); | ||
testValid("+mins", "2000-01-03T01:02:01.100"); | ||
testValid("+minute", "2000-01-03T01:02:01.100"); | ||
testValid("+minutes", "2000-01-03T01:02:01.100"); | ||
|
||
testValid("+h", "2000-01-03T02:01:01.100"); | ||
testValid("+hr", "2000-01-03T02:01:01.100"); | ||
testValid("+hrs", "2000-01-03T02:01:01.100"); | ||
testValid("+hour", "2000-01-03T02:01:01.100"); | ||
testValid("+hours", "2000-01-03T02:01:01.100"); | ||
|
||
testValid("+d", "2000-01-04T01:01:01.100"); | ||
testValid("+day", "2000-01-04T01:01:01.100"); | ||
testValid("+days", "2000-01-04T01:01:01.100"); | ||
|
||
testValid("+w", "2000-01-10T01:01:01.100"); | ||
testValid("+wk", "2000-01-10T01:01:01.100"); | ||
testValid("+wks", "2000-01-10T01:01:01.100"); | ||
testValid("+week", "2000-01-10T01:01:01.100"); | ||
testValid("+weeks", "2000-01-10T01:01:01.100"); | ||
|
||
testValid("+mon", "2000-02-03T01:01:01.100"); | ||
testValid("+month", "2000-02-03T01:01:01.100"); | ||
testValid("+months", "2000-02-03T01:01:01.100"); | ||
|
||
testValid("+q", "2000-04-03T01:01:01.100"); | ||
testValid("+qtr", "2000-04-03T01:01:01.100"); | ||
testValid("+qtrs", "2000-04-03T01:01:01.100"); | ||
testValid("+quarter", "2000-04-03T01:01:01.100"); | ||
testValid("+quarters", "2000-04-03T01:01:01.100"); | ||
|
||
testValid("+y", "2001-01-03T01:01:01.100"); | ||
testValid("+yr", "2001-01-03T01:01:01.100"); | ||
testValid("+yrs", "2001-01-03T01:01:01.100"); | ||
testValid("+year", "2001-01-03T01:01:01.100"); | ||
testValid("+years", "2001-01-03T01:01:01.100"); | ||
|
||
testInvalid("+1INVALID", "The relative date time unit 'INVALID' is not supported."); | ||
} | ||
|
||
@Test | ||
public void testRelativeSnap() { | ||
testValid("@s", "2000-01-03T01:01:01"); | ||
testValid("@sec", "2000-01-03T01:01:01"); | ||
testValid("@secs", "2000-01-03T01:01:01"); | ||
testValid("@second", "2000-01-03T01:01:01"); | ||
testValid("@seconds", "2000-01-03T01:01:01"); | ||
|
||
testValid("@m", "2000-01-03T01:01"); | ||
testValid("@min", "2000-01-03T01:01"); | ||
testValid("@mins", "2000-01-03T01:01"); | ||
testValid("@minute", "2000-01-03T01:01"); | ||
testValid("@minutes", "2000-01-03T01:01"); | ||
|
||
testValid("@h", "2000-01-03T01:00"); | ||
testValid("@hr", "2000-01-03T01:00"); | ||
testValid("@hrs", "2000-01-03T01:00"); | ||
testValid("@hour", "2000-01-03T01:00"); | ||
testValid("@hours", "2000-01-03T01:00"); | ||
|
||
testValid("@d", "2000-01-03T00:00"); | ||
testValid("@day", "2000-01-03T00:00"); | ||
testValid("@days", "2000-01-03T00:00"); | ||
|
||
testValid("@w", "2000-01-02T00:00"); | ||
testValid("@wk", "2000-01-02T00:00"); | ||
testValid("@wks", "2000-01-02T00:00"); | ||
testValid("@week", "2000-01-02T00:00"); | ||
testValid("@weeks", "2000-01-02T00:00"); | ||
|
||
testValid("@mon", "2000-01-01T00:00"); | ||
testValid("@month", "2000-01-01T00:00"); | ||
testValid("@months", "2000-01-01T00:00"); | ||
|
||
testValid("@q", "2000-01-01T00:00"); | ||
testValid("@qtr", "2000-01-01T00:00"); | ||
testValid("@qtrs", "2000-01-01T00:00"); | ||
testValid("@quarter", "2000-01-01T00:00"); | ||
testValid("@quarters", "2000-01-01T00:00"); | ||
|
||
testValid("@y", "2000-01-01T00:00"); | ||
testValid("@yr", "2000-01-01T00:00"); | ||
testValid("@yrs", "2000-01-01T00:00"); | ||
testValid("@year", "2000-01-01T00:00"); | ||
testValid("@years", "2000-01-01T00:00"); | ||
|
||
testValid("@w0", "2000-01-02T00:00"); | ||
testValid("@w1", "2000-01-03T00:00"); | ||
testValid("@w2", "1999-12-28T00:00"); | ||
testValid("@w3", "1999-12-29T00:00"); | ||
testValid("@w4", "1999-12-30T00:00"); | ||
testValid("@w5", "1999-12-31T00:00"); | ||
testValid("@w6", "2000-01-01T00:00"); | ||
testValid("@w7", "2000-01-02T00:00"); | ||
|
||
testInvalid("@INVALID", "The relative date time unit 'INVALID' is not supported."); | ||
} | ||
|
||
private void testValid(String relativeDateTimeString, String expectedDateTimeString) { | ||
String testMessage = String.format("\"%s\"", relativeDateTimeString); | ||
LocalDateTime expectedDateTime = LocalDateTime.parse(expectedDateTimeString); | ||
LocalDateTime actualDateTime = TimeUtils.getRelativeDateTime(relativeDateTimeString, dateTime); | ||
assertEquals(testMessage, expectedDateTime, actualDateTime); | ||
} | ||
|
||
private void testInvalid(String relativeDateTimeString, String expectedExceptionMessage) { | ||
String testMessage = String.format("\"%s\"", relativeDateTimeString); | ||
String actualExceptionMessage = assertThrows(testMessage, RuntimeException.class, () -> TimeUtils.getRelativeDateTime(relativeDateTimeString, dateTime)).getMessage(); | ||
assertEquals(expectedExceptionMessage, actualExceptionMessage); | ||
} | ||
} |