1111import static org .opensearch .sql .data .type .ExprCoreType .TIME ;
1212import static org .opensearch .sql .data .type .ExprCoreType .TIMESTAMP ;
1313
14- import java .time .LocalDateTime ;
15- import java .time .temporal .TemporalAccessor ;
1614import java .util .List ;
1715import java .util .Objects ;
1816import java .util .stream .Collectors ;
1917import lombok .EqualsAndHashCode ;
2018import org .opensearch .common .time .DateFormatter ;
21- import org .opensearch .common .time .DateFormatters ;
2219import org .opensearch .common .time .FormatNames ;
2320import org .opensearch .sql .data .type .ExprCoreType ;
2421import org .opensearch .sql .data .type .ExprType ;
@@ -31,13 +28,13 @@ public class OpenSearchDateType extends OpenSearchDataType {
3128
3229 private static final OpenSearchDateType instance = new OpenSearchDateType ();
3330
34- // numeric formats which support full datetime
31+ /** Numeric formats which support full datetime. */
3532 public static final List <FormatNames > SUPPORTED_NAMED_NUMERIC_FORMATS = List .of (
3633 FormatNames .EPOCH_MILLIS ,
3734 FormatNames .EPOCH_SECOND
3835 );
3936
40- // list of named formats which support full datetime
37+ /** List of named formats which support full datetime. */
4138 public static final List <FormatNames > SUPPORTED_NAMED_DATETIME_FORMATS = List .of (
4239 FormatNames .ISO8601 ,
4340 FormatNames .BASIC_DATE_TIME ,
@@ -78,7 +75,7 @@ public class OpenSearchDateType extends OpenSearchDataType {
7875 FormatNames .STRICT_WEEK_DATE_TIME_NO_MILLIS
7976 );
8077
81- // list of named formats that only support year/month/day
78+ /** List of named formats that only support year/month/day. */
8279 public static final List <FormatNames > SUPPORTED_NAMED_DATE_FORMATS = List .of (
8380 FormatNames .BASIC_DATE ,
8481 FormatNames .BASIC_ORDINAL_DATE ,
@@ -94,8 +91,8 @@ public class OpenSearchDateType extends OpenSearchDataType {
9491 FormatNames .STRICT_WEEKYEAR_WEEK_DAY
9592 );
9693
97- // list of named formats which produce incomplete date,
98- // e.g. 1 or 2 are missing from tuple year/month/day
94+ /** list of named formats which produce incomplete date,
95+ * e.g. 1 or 2 are missing from tuple year/month/day. */
9996 public static final List <FormatNames > SUPPORTED_NAMED_INCOMPLETE_DATE_FORMATS = List .of (
10097 FormatNames .YEAR_MONTH ,
10198 FormatNames .STRICT_YEAR_MONTH ,
@@ -108,7 +105,7 @@ public class OpenSearchDateType extends OpenSearchDataType {
108105 FormatNames .STRICT_WEEKYEAR
109106 );
110107
111- // list of named formats that only support hour/minute/second
108+ /** List of named formats that only support hour/minute/second. */
112109 public static final List <FormatNames > SUPPORTED_NAMED_TIME_FORMATS = List .of (
113110 FormatNames .BASIC_TIME ,
114111 FormatNames .BASIC_TIME_NO_MILLIS ,
@@ -134,6 +131,11 @@ public class OpenSearchDateType extends OpenSearchDataType {
134131 FormatNames .STRICT_T_TIME_NO_MILLIS
135132 );
136133
134+ /** Formatter symbols which used to format time or date correspondingly.
135+ * {@link java.time.format.DateTimeFormatter}. */
136+ private static final String CUSTOM_FORMAT_TIME_SYMBOLS = "nNASsmHkKha" ;
137+ private static final String CUSTOM_FORMAT_DATE_SYMBOLS = "FecEWwYqQgdMLDyuG" ;
138+
137139 @ EqualsAndHashCode .Exclude
138140 String formatString ;
139141
@@ -179,7 +181,6 @@ public List<DateFormatter> getAllNamedFormatters() {
179181
180182 /**
181183 * Retrieves a list of numeric formatters that format for dates.
182- *
183184 * @return a list of DateFormatters that can be used to parse a Date.
184185 */
185186 public List <DateFormatter > getNumericNamedFormatters () {
@@ -191,6 +192,26 @@ public List<DateFormatter> getNumericNamedFormatters() {
191192 .map (DateFormatter ::forPattern ).collect (Collectors .toList ());
192193 }
193194
195+ /**
196+ * Retrieves a list of custom formats defined by the user.
197+ * @return a list of formats as strings that can be used to parse a Date/Time/Timestamp.
198+ */
199+ public List <String > getAllCustomFormats () {
200+ return getFormatList ().stream ()
201+ .filter (format -> FormatNames .forName (format ) == null )
202+ .map (format -> {
203+ try {
204+ DateFormatter .forPattern (format );
205+ return format ;
206+ } catch (Exception ignored ) {
207+ // parsing failed
208+ return null ;
209+ }
210+ })
211+ .filter (Objects ::nonNull )
212+ .collect (Collectors .toList ());
213+ }
214+
194215 /**
195216 * Retrieves a list of custom formatters defined by the user.
196217 * @return a list of DateFormatters that can be used to parse a Date/Time/Timestamp.
@@ -212,7 +233,6 @@ public List<DateFormatter> getAllCustomFormatters() {
212233
213234 /**
214235 * Retrieves a list of named formatters that format for dates.
215- *
216236 * @return a list of DateFormatters that can be used to parse a Date.
217237 */
218238 public List <DateFormatter > getDateNamedFormatters () {
@@ -226,7 +246,6 @@ public List<DateFormatter> getDateNamedFormatters() {
226246
227247 /**
228248 * Retrieves a list of named formatters that format for Times.
229- *
230249 * @return a list of DateFormatters that can be used to parse a Time.
231250 */
232251 public List <DateFormatter > getTimeNamedFormatters () {
@@ -240,7 +259,6 @@ public List<DateFormatter> getTimeNamedFormatters() {
240259
241260 /**
242261 * Retrieves a list of named formatters that format for DateTimes.
243- *
244262 * @return a list of DateFormatters that can be used to parse a DateTime.
245263 */
246264 public List <DateFormatter > getDateTimeNamedFormatters () {
@@ -252,39 +270,26 @@ public List<DateFormatter> getDateTimeNamedFormatters() {
252270 .map (DateFormatter ::forPattern ).collect (Collectors .toList ());
253271 }
254272
255- private ExprCoreType getExprTypeFromCustomFormats (List <DateFormatter > formats ) {
273+ private ExprCoreType getExprTypeFromCustomFormats (List <String > formats ) {
256274 boolean isDate = false ;
257275 boolean isTime = false ;
258276
259- LocalDateTime sampleDateTime = LocalDateTime .now ();
260- // Unfortunately, there is no public API to get info from the formatter object,
261- // whether it parses date or time or datetime. The workaround is:
262- // Converting a sample DateTime object by each formatter to string and back.
263- // Double-converted sample will be also DateTime, but if formatter parses
264- // time part only, date part would be lost. And vice versa.
265- // This trick allows us to get matching data type for every custom formatter.
266- // Unfortunately, this involves parsing a string, once per query, per column, per format.
267- // Overhead performance is equal to parsing extra row of result set (extra doc).
268- // Could be cached in scope of #1783 https://github.com/opensearch-project/sql/issues/1783.
269-
270277 for (var format : formats ) {
271- TemporalAccessor ta = format .parse (format .format (sampleDateTime ));
272- LocalDateTime parsedSample = sampleDateTime ;
273- try {
274- // TODO do we need withZoneSameInstant or withZoneSameLocal?
275- parsedSample = DateFormatters .from (ta ).toLocalDateTime ();
276- } catch (Exception ignored ) {
277- // Can't convert to a DateTime - format does not represent a complete date or time
278- continue ;
278+ if (!isTime ) {
279+ for (var symbol : CUSTOM_FORMAT_TIME_SYMBOLS .toCharArray ()) {
280+ if (format .contains (String .valueOf (symbol ))) {
281+ isTime = true ;
282+ break ;
283+ }
284+ }
279285 }
280-
281286 if (!isDate ) {
282- isDate = parsedSample . toLocalDate (). equals ( sampleDateTime . toLocalDate ());
283- }
284- if (! isTime ) {
285- // Second and Second fraction part are optional and may miss in some formats, trim it.
286- isTime = parsedSample . toLocalTime (). withSecond ( 0 ). withNano ( 0 ). equals (
287- sampleDateTime . toLocalTime (). withSecond ( 0 ). withNano ( 0 ));
287+ for ( var symbol : CUSTOM_FORMAT_DATE_SYMBOLS . toCharArray ()) {
288+ if ( format . contains ( String . valueOf ( symbol ))) {
289+ isDate = true ;
290+ break ;
291+ }
292+ }
288293 }
289294 if (isDate && isTime ) {
290295 return TIMESTAMP ;
@@ -298,7 +303,7 @@ private ExprCoreType getExprTypeFromCustomFormats(List<DateFormatter> formats) {
298303 return TIME ;
299304 }
300305
301- // Incomplete formats: can't be converted to DATE nor TIME, for example `year` (year only)
306+ // Incomplete or incorrect formats: can't be converted to DATE nor TIME, for example `year`
302307 return TIMESTAMP ;
303308 }
304309
@@ -316,7 +321,7 @@ private ExprCoreType getExprTypeFromFormatString(String formatString) {
316321 return TIMESTAMP ;
317322 }
318323
319- List <DateFormatter > customFormatters = getAllCustomFormatters ();
324+ List <String > customFormatters = getAllCustomFormats ();
320325 if (!customFormatters .isEmpty ()) {
321326 ExprCoreType customFormatType = getExprTypeFromCustomFormats (customFormatters );
322327 ExprCoreType combinedByDefaultFormats = customFormatType ;
0 commit comments