-
Notifications
You must be signed in to change notification settings - Fork 33
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
#991 Support Relative Date Times #1006
Open
currantw
wants to merge
16
commits into
opensearch-project:main
Choose a base branch
from
Bit-Quill:#991_relative_datetime
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
f35326c
Unrelated typos
currantw f4eed01
Initial implementation of relative date time logic and unit tests
currantw c01690a
Minor cleanup
currantw f6a2444
Update to make relative time case-insensitive, and add corresponding …
currantw 6afee83
Add a few more unit test cases.
currantw 83f2063
Split pattern into smaller strings.
currantw fd4df7b
Add `relativeDateTimeFunction` to `SerializableUdf`, along with corre…
currantw 93446bb
Fix dangling Javadoc
currantw c78c0a5
Initial implementation of `relative_timestamp` UDF.
currantw 68ec760
Add integration tests, refactor to use Timestamp
currantw a117ba2
Add documentation
currantw 6eb2b60
Review comments: add more documentation, add ignored tests for earlie…
currantw a8d084a
Minor clean up
currantw 592a07a
Update to use Instant and ZoneId. For some reason, timestamp can be r…
currantw 0ea69ed
Remove unused import
currantw cad87e3
Address review comments
currantw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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 NEGATIVE_SIGN = "-"; | ||
|
||
// Pattern for relative date time string. | ||
private static final Pattern RELATIVE_DATE_TIME_PATTERN = Pattern.compile(String.format( | ||
"(?<offset>%s)?(?<snap>%s)?", | ||
"(?<offsetSign>[+-])(?<offsetValue>\\d+)?(?<offsetUnit>\\w+)", | ||
"[@](?<snapUnit>\\w+)")); | ||
|
||
// Supported time units. | ||
private static final Set<String> SECOND_UNITS_SET = Set.of("s", "sec", "secs", "second", "seconds"); | ||
private static final Set<String> MINUTE_UNITS_SET = Set.of("m", "min", "mins", "minute", "minutes"); | ||
private static final Set<String> HOUR_UNITS_SET = Set.of("h", "hr", "hrs", "hour", "hours"); | ||
private static final Set<String> DAY_UNITS_SET = Set.of("d", "day", "days"); | ||
private static final Set<String> WEEK_UNITS_SET = Set.of("w", "wk", "wks", "week", "weeks"); | ||
private static final Set<String> MONTH_UNITS_SET = Set.of("mon", "month", "months"); | ||
private static final Set<String> QUARTER_UNITS_SET = Set.of("q", "qtr", "qtrs", "quarter", "quarters"); | ||
private static final Set<String> YEAR_UNITS_SET = Set.of("y", "yr", "yrs", "year", "years"); | ||
|
||
// Map from time unit to the corresponding duration. | ||
private static final Duration DURATION_SECOND = Duration.ofSeconds(1); | ||
private static final Duration DURATION_MINUTE = Duration.ofMinutes(1); | ||
private static final Duration DURATION_HOUR = Duration.ofHours(1); | ||
|
||
private static final Map<String, Duration> DURATION_FOR_TIME_UNIT_MAP = Map.ofEntries( | ||
Map.entry("s", DURATION_SECOND), | ||
Map.entry("sec", DURATION_SECOND), | ||
Map.entry("secs", DURATION_SECOND), | ||
Map.entry("second", DURATION_SECOND), | ||
Map.entry("seconds", DURATION_SECOND), | ||
|
||
Map.entry("m", DURATION_MINUTE), | ||
Map.entry("min", DURATION_MINUTE), | ||
Map.entry("mins", DURATION_MINUTE), | ||
Map.entry("minute", DURATION_MINUTE), | ||
Map.entry("minutes", DURATION_MINUTE), | ||
|
||
Map.entry("h", DURATION_HOUR), | ||
Map.entry("hr", DURATION_HOUR), | ||
Map.entry("hrs", DURATION_HOUR), | ||
Map.entry("hour", DURATION_HOUR), | ||
Map.entry("hours", DURATION_HOUR)); | ||
|
||
// Map from time unit to the corresponding period. | ||
private static final Period PERIOD_DAY = Period.ofDays(1); | ||
private static final Period PERIOD_WEEK = Period.ofWeeks(1); | ||
private static final Period PERIOD_MONTH = Period.ofMonths(1); | ||
private static final Period PERIOD_QUARTER = Period.ofMonths(3); | ||
private static final Period PERIOD_YEAR = Period.ofYears(1); | ||
|
||
private static final Map<String, Period> PERIOD_FOR_TIME_UNIT_MAP = Map.ofEntries( | ||
Map.entry("d", PERIOD_DAY), | ||
Map.entry("day", PERIOD_DAY), | ||
Map.entry("days", PERIOD_DAY), | ||
|
||
Map.entry("w", PERIOD_WEEK), | ||
Map.entry("wk", PERIOD_WEEK), | ||
Map.entry("wks", PERIOD_WEEK), | ||
Map.entry("week", PERIOD_WEEK), | ||
Map.entry("weeks", PERIOD_WEEK), | ||
|
||
Map.entry("mon", PERIOD_MONTH), | ||
Map.entry("month", PERIOD_MONTH), | ||
Map.entry("months", PERIOD_MONTH), | ||
|
||
Map.entry("q", PERIOD_QUARTER), | ||
Map.entry("qtr", PERIOD_QUARTER), | ||
Map.entry("qtrs", PERIOD_QUARTER), | ||
Map.entry("quarter", PERIOD_QUARTER), | ||
Map.entry("quarters", PERIOD_QUARTER), | ||
|
||
Map.entry("y", PERIOD_YEAR), | ||
Map.entry("yr", PERIOD_YEAR), | ||
Map.entry("yrs", PERIOD_YEAR), | ||
Map.entry("year", PERIOD_YEAR), | ||
Map.entry("years", PERIOD_YEAR)); | ||
|
||
// Map from snap unit to the corresponding day of the week. | ||
private static final Map<String, DayOfWeek> DAY_OF_THE_WEEK_FOR_SNAP_UNIT_MAP = 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 = RELATIVE_DATE_TIME_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(NEGATIVE_SIGN)) { | ||
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 (DURATION_FOR_TIME_UNIT_MAP.containsKey(offsetUnitString)) { | ||
final Duration offsetDuration = DURATION_FOR_TIME_UNIT_MAP.get(offsetUnitString).multipliedBy(offsetValue); | ||
return dateTime.plus(offsetDuration); | ||
} | ||
|
||
if (PERIOD_FOR_TIME_UNIT_MAP.containsKey(offsetUnitString)) { | ||
final Period offsetPeriod = PERIOD_FOR_TIME_UNIT_MAP.get(offsetUnitString).multipliedBy(offsetValue); | ||
return dateTime.plus(offsetPeriod); | ||
} | ||
|
||
String message = String.format("The relative date time unit '%s' is not supported.", offsetUnitString); | ||
throw new RuntimeException(message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, updated ✅ |
||
} | ||
|
||
/** | ||
* 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 (SECOND_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.SECONDS); | ||
} else if (MINUTE_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.MINUTES); | ||
} else if (HOUR_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.HOURS); | ||
} else if (DAY_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS); | ||
} else if (WEEK_UNITS_SET.contains(snapUnit)) { | ||
return applySnapToDayOfWeek(dateTime, DayOfWeek.SUNDAY); | ||
} else if (MONTH_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1); | ||
} else if (QUARTER_UNITS_SET.contains(snapUnit)) { | ||
int monthsToSnap = (dateTime.getMonthValue() - 1) % MONTHS_PER_QUARTER; | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1).minusMonths(monthsToSnap); | ||
} else if (YEAR_UNITS_SET.contains(snapUnit)) { | ||
return dateTime.truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); | ||
} else if (DAY_OF_THE_WEEK_FOR_SNAP_UNIT_MAP.containsKey(snapUnit)) { | ||
return applySnapToDayOfWeek(dateTime, DAY_OF_THE_WEEK_FOR_SNAP_UNIT_MAP.get(snapUnit)); | ||
} | ||
|
||
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 applySnapToDayOfWeek(LocalDateTime dateTime, DayOfWeek snapDayOfWeek) { | ||
LocalDateTime snappedDateTime = dateTime.truncatedTo(ChronoUnit.DAYS); | ||
|
||
DayOfWeek dayOfWeek = dateTime.getDayOfWeek(); | ||
if (dayOfWeek.equals(snapDayOfWeek)) { | ||
return snappedDateTime; | ||
} | ||
|
||
int daysToSnap = DAYS_PER_WEEK - snapDayOfWeek.getValue() + dayOfWeek.getValue(); | ||
return snappedDateTime.minusDays(daysToSnap); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
IllegalArgumentException
installThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done ✅