Skip to content

Commit f8f01f4

Browse files
committed
Simplify date/time expr constructors & parsers
Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>
1 parent 28d2461 commit f8f01f4

6 files changed

Lines changed: 57 additions & 130 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/utils/datetime/DateTimeParser.java

Lines changed: 14 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,129 +5,38 @@
55

66
package org.opensearch.sql.calcite.utils.datetime;
77

8-
import static org.opensearch.sql.utils.DateTimeFormatters.STRICT_DATE_FORMATTER;
9-
import static org.opensearch.sql.utils.DateTimeFormatters.STRICT_TIMESTAMP_FORMATTER;
10-
import static org.opensearch.sql.utils.DateTimeFormatters.STRICT_TIME_FORMATTER;
11-
12-
import com.google.common.collect.ImmutableList;
138
import java.time.LocalDate;
149
import java.time.LocalDateTime;
1510
import java.time.LocalTime;
1611
import java.time.ZoneId;
17-
import java.time.format.DateTimeFormatter;
18-
import java.util.List;
19-
import org.opensearch.sql.exception.SemanticCheckException;
12+
import java.time.format.DateTimeParseException;
13+
import java.util.Locale;
14+
import org.opensearch.sql.utils.DateTimeFormatters;
2015

2116
public interface DateTimeParser {
2217
/**
23-
* Parse a string into a LocalDateTime If only date is found, time is set to 00:00:00. If only
24-
* time is found, date is set to today.
18+
* Parse a string into a LocalDateTime. If only date is found, time is set to 00:00:00. If only
19+
* time is found, date is set to today at UTC.
2520
*
2621
* @param input A date/time/timestamp string
2722
* @return A LocalDateTime
2823
* @throws IllegalArgumentException if parsing fails
2924
*/
3025
static LocalDateTime parse(String input) {
31-
32-
if (input == null || input.trim().isEmpty()) {
33-
throw new SemanticCheckException("Cannot parse a null/empty date-time string.");
34-
}
35-
36-
if (input.contains(":")) {
37-
try {
38-
return parseTimestamp(input);
39-
} catch (Exception ignored) {
40-
}
41-
42-
try {
43-
LocalTime t = parseTime(input);
44-
return LocalDateTime.of(LocalDate.now(ZoneId.of("UTC")), t);
45-
} catch (Exception ignored) {
46-
}
47-
} else {
48-
try {
49-
LocalDate d = parseDate(input);
50-
return d.atStartOfDay();
51-
} catch (Exception ignored) {
52-
}
53-
}
54-
throw new SemanticCheckException(String.format("Unable to parse %s as datetime", input));
55-
}
56-
57-
static LocalDateTime parseTimeOrTimestamp(String input) {
58-
if (input == null || input.trim().isEmpty()) {
59-
throw new SemanticCheckException("Cannot parse a null/empty date-time string.");
60-
}
61-
62-
try {
63-
return parseTime(input).atDate(LocalDate.now(ZoneId.of("UTC")));
64-
} catch (Exception ignored) {
65-
}
66-
67-
try {
68-
return parseTimestamp(input);
69-
} catch (Exception ignored) {
70-
}
71-
72-
throw new SemanticCheckException(
73-
String.format("time:%s in unsupported format, please use 'HH:mm:ss[.SSSSSSSSS]'", input));
74-
}
75-
76-
static LocalDateTime parseDateOrTimestamp(String input) {
77-
if (input == null || input.trim().isEmpty()) {
78-
throw new SemanticCheckException("Cannot parse a null/empty date-time string.");
79-
}
80-
8126
try {
82-
return parseDate(input).atStartOfDay();
83-
} catch (Exception ignored) {
27+
return LocalDateTime.parse(input, DateTimeFormatters.DATE_TIMESTAMP_FORMATTER);
28+
} catch (DateTimeParseException ignored) {
8429
}
8530

8631
try {
87-
return parseTimestamp(input);
32+
LocalTime t = LocalTime.parse(input, DateTimeFormatters.TIME_TIMESTAMP_FORMATTER);
33+
return LocalDateTime.of(LocalDate.now(ZoneId.of("UTC")), t);
8834
} catch (Exception ignored) {
35+
throw new IllegalArgumentException(
36+
String.format(
37+
Locale.ROOT,
38+
"timestamp:%s in unsupported format, please use 'yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]'",
39+
input));
8940
}
90-
91-
throw new SemanticCheckException(
92-
String.format("date:%s in unsupported format, please use 'yyyy-MM-dd'", input));
93-
}
94-
95-
static LocalDateTime parseTimestamp(String input) {
96-
List<DateTimeFormatter> dateTimeFormatters = ImmutableList.of(STRICT_TIMESTAMP_FORMATTER);
97-
98-
for (DateTimeFormatter fmt : dateTimeFormatters) {
99-
try {
100-
return LocalDateTime.parse(input, fmt);
101-
} catch (Exception ignored) {
102-
}
103-
}
104-
throw new SemanticCheckException(
105-
String.format(
106-
"timestamp:%s in unsupported format, please use 'yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]'",
107-
input));
108-
}
109-
110-
static LocalTime parseTime(String input) {
111-
List<DateTimeFormatter> timeFormatters = ImmutableList.of(STRICT_TIME_FORMATTER);
112-
for (DateTimeFormatter fmt : timeFormatters) {
113-
try {
114-
return LocalTime.parse(input, fmt);
115-
} catch (Exception ignored) {
116-
}
117-
}
118-
throw new SemanticCheckException(
119-
String.format("time:%s in unsupported format, please use 'HH:mm:ss[.SSSSSSSSS]'", input));
120-
}
121-
122-
static LocalDate parseDate(String input) {
123-
List<DateTimeFormatter> dateFormatters = ImmutableList.of(STRICT_DATE_FORMATTER);
124-
for (DateTimeFormatter fmt : dateFormatters) {
125-
try {
126-
return LocalDate.parse(input, fmt);
127-
} catch (Exception ignored) {
128-
}
129-
}
130-
throw new SemanticCheckException(
131-
String.format("date:%s in unsupported format, please use 'yyyy-MM-dd'", input));
13241
}
13342
}

core/src/main/java/org/opensearch/sql/data/model/ExprDateValue.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,28 @@
1212
import java.time.ZoneOffset;
1313
import java.time.ZonedDateTime;
1414
import java.time.format.DateTimeFormatter;
15+
import java.time.format.DateTimeParseException;
1516
import lombok.RequiredArgsConstructor;
16-
import org.opensearch.sql.calcite.utils.datetime.DateTimeParser;
1717
import org.opensearch.sql.data.type.ExprCoreType;
1818
import org.opensearch.sql.data.type.ExprType;
1919
import org.opensearch.sql.exception.SemanticCheckException;
20+
import org.opensearch.sql.utils.DateTimeFormatters;
2021

2122
/** Expression Date Value. */
2223
@RequiredArgsConstructor
2324
public class ExprDateValue extends AbstractExprValue {
2425

2526
private final LocalDate date;
2627

27-
/** Constructor of ExprDateValue. */
28+
/**
29+
* Constructor with date string.
30+
*
31+
* @param date a date or timestamp string (does not accept time string)
32+
*/
2833
public ExprDateValue(String date) {
2934
try {
30-
this.date = DateTimeParser.parseDateOrTimestamp(date).toLocalDate();
31-
} catch (SemanticCheckException e) {
35+
this.date = LocalDate.parse(date, DateTimeFormatters.DATE_TIMESTAMP_FORMATTER);
36+
} catch (DateTimeParseException e) {
3237
throw new SemanticCheckException(
3338
String.format("date:%s in unsupported format, please use 'yyyy-MM-dd'", date));
3439
}

core/src/main/java/org/opensearch/sql/data/model/ExprTimeValue.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,30 @@
1212
import java.time.LocalTime;
1313
import java.time.ZoneOffset;
1414
import java.time.ZonedDateTime;
15+
import java.time.format.DateTimeParseException;
1516
import java.util.Objects;
1617
import lombok.RequiredArgsConstructor;
17-
import org.opensearch.sql.calcite.utils.datetime.DateTimeParser;
1818
import org.opensearch.sql.data.type.ExprCoreType;
1919
import org.opensearch.sql.data.type.ExprType;
2020
import org.opensearch.sql.exception.SemanticCheckException;
2121
import org.opensearch.sql.expression.function.FunctionProperties;
22+
import org.opensearch.sql.utils.DateTimeFormatters;
2223

2324
/** Expression Time Value. */
2425
@RequiredArgsConstructor
2526
public class ExprTimeValue extends AbstractExprValue {
2627

2728
private final LocalTime time;
2829

29-
/** Constructor of ExprTimeValue. */
30+
/**
31+
* Constructor with time string.
32+
*
33+
* @param time a time or timestamp string (does not accept date string)
34+
*/
3035
public ExprTimeValue(String time) {
3136
try {
32-
this.time = DateTimeParser.parseTimeOrTimestamp(time).toLocalTime();
33-
} catch (SemanticCheckException e) {
37+
this.time = LocalTime.parse(time, DateTimeFormatters.TIME_TIMESTAMP_FORMATTER);
38+
} catch (DateTimeParseException e) {
3439
throw new SemanticCheckException(
3540
String.format("time:%s in unsupported format, please use 'HH:mm:ss[.SSSSSSSSS]'", time));
3641
}

core/src/main/java/org/opensearch/sql/data/model/ExprTimestampValue.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,32 @@
1313
import java.time.LocalDateTime;
1414
import java.time.LocalTime;
1515
import java.time.ZoneOffset;
16+
import java.time.format.DateTimeParseException;
1617
import java.time.temporal.ChronoUnit;
1718
import java.util.Objects;
1819
import lombok.RequiredArgsConstructor;
19-
import org.opensearch.sql.calcite.utils.datetime.DateTimeParser;
2020
import org.opensearch.sql.data.type.ExprCoreType;
2121
import org.opensearch.sql.data.type.ExprType;
2222
import org.opensearch.sql.exception.SemanticCheckException;
23+
import org.opensearch.sql.utils.DateTimeFormatters;
2324

2425
/** Expression Timestamp Value. */
2526
@RequiredArgsConstructor
2627
public class ExprTimestampValue extends AbstractExprValue {
2728

2829
private final Instant timestamp;
2930

30-
/** Constructor. */
31+
/**
32+
* Constructor with timestamp string.
33+
*
34+
* @param timestamp a date or timestamp string (does not accept time string)
35+
*/
3136
public ExprTimestampValue(String timestamp) {
3237
try {
3338
this.timestamp =
34-
DateTimeParser.parseDateOrTimestamp(timestamp).atZone(ZoneOffset.UTC).toInstant();
35-
} catch (SemanticCheckException e) {
39+
LocalDateTime.parse(timestamp, DateTimeFormatters.DATE_TIMESTAMP_FORMATTER)
40+
.toInstant(ZoneOffset.UTC);
41+
} catch (DateTimeParseException e) {
3642
throw new SemanticCheckException(
3743
String.format(
3844
"timestamp:%s in unsupported format, please use 'yyyy-MM-dd HH:mm:ss[.SSSSSSSSS]'",

core/src/main/java/org/opensearch/sql/utils/DateTimeFormatters.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,25 +129,23 @@ public class DateTimeFormatters {
129129
.toFormatter(Locale.ROOT)
130130
.withResolverStyle(ResolverStyle.STRICT);
131131

132-
public static final DateTimeFormatter STRICT_TIMESTAMP_FORMATTER =
132+
public static final DateTimeFormatter DATE_TIMESTAMP_FORMATTER =
133133
new DateTimeFormatterBuilder()
134-
.appendPattern("[uuuu-MM-dd HH:mm:ss][uuuu-MM-dd HH:mm]")
134+
.appendPattern("[uuuu-MM-dd HH:mm:ss][uuuu-MM-dd HH:mm][uuuu-MM-dd]")
135135
.appendFraction(
136136
ChronoField.NANO_OF_SECOND, MIN_FRACTION_SECONDS, MAX_FRACTION_SECONDS, true)
137+
.parseDefaulting(HOUR_OF_DAY, 0)
138+
.parseDefaulting(MINUTE_OF_HOUR, 0)
139+
.parseDefaulting(SECOND_OF_MINUTE, 0)
137140
.toFormatter(Locale.ROOT)
138141
.withResolverStyle(ResolverStyle.STRICT);
139142

140-
public static final DateTimeFormatter STRICT_DATE_FORMATTER =
143+
public static final DateTimeFormatter TIME_TIMESTAMP_FORMATTER =
141144
new DateTimeFormatterBuilder()
142-
.appendPattern("[uuuu-MM-dd]")
143-
.toFormatter(Locale.ROOT)
144-
.withResolverStyle(ResolverStyle.STRICT);
145-
146-
public static final DateTimeFormatter STRICT_TIME_FORMATTER =
147-
new DateTimeFormatterBuilder()
148-
.appendPattern("[HH:mm:ss][HH:mm]")
145+
.appendPattern("[uuuu-MM-dd HH:mm:ss][uuuu-MM-dd HH:mm][HH:mm:ss][HH:mm]")
149146
.appendFraction(
150147
ChronoField.NANO_OF_SECOND, MIN_FRACTION_SECONDS, MAX_FRACTION_SECONDS, true)
148+
.parseDefaulting(SECOND_OF_MINUTE, 0)
151149
.toFormatter(Locale.ROOT)
152150
.withResolverStyle(ResolverStyle.STRICT);
153151

integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,13 @@ protected JSONObject executeQuery(String query) throws IOException {
13961396
return new JSONObject(getResponseBody(response));
13971397
}
13981398

1399+
@Test
1400+
public void testDateStringAsTimestamp() throws IOException {
1401+
JSONObject result = executeQuery("select {timestamp '2025-07-10'} as t");
1402+
verifySchema(result, schema("{timestamp '2025-07-10'}", "t", "timestamp"));
1403+
verifyDataRows(result, rows("2025-07-10 00:00:00"));
1404+
}
1405+
13991406
@Test
14001407
public void testTimestampBracket() throws IOException {
14011408
JSONObject result = executeQuery("select {timestamp '2020-09-16 17:30:00'}");
@@ -1471,9 +1478,6 @@ public void testBracketFails() {
14711478
assertThrows(ResponseException.class, () -> executeQuery("select {t '2020-09-16'}"));
14721479
assertThrows(ResponseException.class, () -> executeQuery("select {date '17:30:00'}"));
14731480
assertThrows(ResponseException.class, () -> executeQuery("select {d '17:30:00'}"));
1474-
// The following test case is removed because they will be parsed to proper timestamps
1475-
// assertThrows(ResponseException.class, () -> executeQuery("select {timestamp '2020-09-16'}"));
1476-
// assertThrows(ResponseException.class, () -> executeQuery("select {ts '2020-09-16'}"));
14771481
assertThrows(ResponseException.class, () -> executeQuery("select {timestamp '17:30:00'}"));
14781482
assertThrows(ResponseException.class, () -> executeQuery("select {ts '17:30:00'}"));
14791483
}

0 commit comments

Comments
 (0)