1212import org .elasticsearch .common .io .stream .StreamInput ;
1313import org .elasticsearch .common .io .stream .StreamOutput ;
1414import org .elasticsearch .common .time .DateFormatter ;
15+ import org .elasticsearch .common .util .LocaleUtils ;
1516import org .elasticsearch .compute .ann .Evaluator ;
1617import org .elasticsearch .compute .ann .Fixed ;
1718import org .elasticsearch .compute .operator .EvalOperator .ExpressionEvaluator ;
1819import org .elasticsearch .xpack .esql .core .InvalidArgumentException ;
1920import org .elasticsearch .xpack .esql .core .expression .Expression ;
21+ import org .elasticsearch .xpack .esql .core .expression .MapExpression ;
2022import org .elasticsearch .xpack .esql .core .tree .NodeInfo ;
2123import org .elasticsearch .xpack .esql .core .tree .Source ;
2224import org .elasticsearch .xpack .esql .core .type .DataType ;
2325import org .elasticsearch .xpack .esql .expression .function .Example ;
2426import org .elasticsearch .xpack .esql .expression .function .FunctionInfo ;
27+ import org .elasticsearch .xpack .esql .expression .function .MapParam ;
28+ import org .elasticsearch .xpack .esql .expression .function .Options ;
2529import org .elasticsearch .xpack .esql .expression .function .Param ;
26- import org .elasticsearch .xpack .esql .expression .function .ThreeOptionalArguments ;
30+ import org .elasticsearch .xpack .esql .expression .function .TwoOptionalArguments ;
2731import org .elasticsearch .xpack .esql .expression .function .scalar .EsqlScalarFunction ;
2832import org .elasticsearch .xpack .esql .io .stream .PlanStreamInput ;
2933
3034import java .io .IOException ;
35+ import java .time .ZoneId ;
36+ import java .time .zone .ZoneRulesException ;
3137import java .util .ArrayList ;
38+ import java .util .HashMap ;
3239import java .util .List ;
3340import java .util .Locale ;
34- import java .util .TimeZone ;
41+ import java .util .Map ;
3542
43+ import static java .util .Map .entry ;
3644import static org .elasticsearch .common .time .DateFormatter .forPattern ;
3745import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .FIRST ;
3846import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .SECOND ;
47+ import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .ParamOrdinal .THIRD ;
3948import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isString ;
49+ import static org .elasticsearch .xpack .esql .core .type .DataType .KEYWORD ;
4050import static org .elasticsearch .xpack .esql .expression .EsqlTypeResolutions .isStringAndExact ;
4151import static org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter .DEFAULT_DATE_TIME_FORMATTER ;
4252import static org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter .dateTimeToLong ;
4353
44- public class DateParse extends EsqlScalarFunction implements ThreeOptionalArguments {
54+ public class DateParse extends EsqlScalarFunction implements TwoOptionalArguments {
4555 public static final NamedWriteableRegistry .Entry ENTRY = new NamedWriteableRegistry .Entry (
4656 Expression .class ,
4757 "DateParse" ,
4858 DateParse ::new
4959 );
5060
61+ private static final String TIME_ZONE_PARAM_NAME = "time_zone" ;
62+ private static final String LOCALE_PARAM_NAME = "locale" ;
63+
5164 private final Expression field ;
5265 private final Expression format ;
53- private final Expression locale ;
54- private final Expression timezone ;
66+ private final Expression options ;
5567
5668 @ FunctionInfo (
5769 returnType = "date" ,
@@ -69,27 +81,40 @@ public DateParse(
6981 type = { "keyword" , "text" },
7082 description = "Date expression as a string. If `null` or an empty string, the function returns `null`."
7183 ) Expression second ,
72- @ Param (name = "dateLocale" , type = { "keyword" , "text" }, description = "The locale to parse with" ) Expression third ,
73- @ Param (name = "dateTimezone" , type = { "keyword" , "text" }, description = "The timezone to parse with" ) Expression forth
84+ @ MapParam (
85+ name = "options" ,
86+ params = {
87+ @ MapParam .MapParamEntry (
88+ name = TIME_ZONE_PARAM_NAME ,
89+ type = "keyword" ,
90+ valueHint = { "standard" },
91+ description = "Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the "
92+ + "query string to UTC."
93+ ),
94+ @ MapParam .MapParamEntry (
95+ name = LOCALE_PARAM_NAME ,
96+ type = "keyword" ,
97+ valueHint = { "standard" },
98+ description = "The locale to use when parsing the date, relevant when parsing month names or week days."
99+ )
100+ },
101+ description = "(Optional) Additional options for date parsing as <<esql-function-named-params,function named parameters>>." ,
102+ optional = true ) Expression options
74103 ) {
75- super (source , fields (first , second , third , forth ));
104+ super (source , fields (first , second , options ));
76105 this .field = second != null ? second : first ;
77106 this .format = second != null ? first : null ;
78- this .locale = third ;
79- this .timezone = forth ;
107+ this .options = options ;
80108 }
81109
82- private static List <Expression > fields (Expression field , Expression format , Expression locale , Expression timezone ) {
110+ private static List <Expression > fields (Expression field , Expression format , Expression options ) {
83111 List <Expression > list = new ArrayList <>(3 );
84112 list .add (field );
85113 if (format != null ) {
86114 list .add (format );
87115 }
88- if (locale != null ) {
89- list .add (locale );
90- }
91- if (timezone != null ) {
92- list .add (timezone );
116+ if (options != null ) {
117+ list .add (options );
93118 }
94119 return list ;
95120 }
@@ -99,7 +124,6 @@ private DateParse(StreamInput in) throws IOException {
99124 Source .readFrom ((PlanStreamInput ) in ),
100125 in .readNamedWriteable (Expression .class ),
101126 in .readOptionalNamedWriteable (Expression .class ),
102- in .readOptionalNamedWriteable (Expression .class ),
103127 in .readOptionalNamedWriteable (Expression .class )
104128 );
105129 }
@@ -160,6 +184,21 @@ static long process(BytesRef val, BytesRef formatter) throws IllegalArgumentExce
160184 return dateTimeToLong (val .utf8ToString (), toFormatter (formatter ));
161185 }
162186
187+ public static final Map <String , DataType > ALLOWED_OPTIONS = Map .ofEntries (
188+ entry (TIME_ZONE_PARAM_NAME , KEYWORD ),
189+ entry (LOCALE_PARAM_NAME , KEYWORD )
190+ );
191+
192+ private Map <String , Object > parseOptions () throws InvalidArgumentException {
193+ Map <String , Object > matchOptions = new HashMap <>();
194+ if (this .options == null ) {
195+ return matchOptions ;
196+ }
197+
198+ Options .populateMap ((MapExpression ) this .options , matchOptions , source (), THIRD , ALLOWED_OPTIONS );
199+ return matchOptions ;
200+ }
201+
163202 @ Override
164203 public ExpressionEvaluator .Factory toEvaluator (ToEvaluator toEvaluator ) {
165204 ExpressionEvaluator .Factory fieldEvaluator = toEvaluator .apply (field );
@@ -169,22 +208,28 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
169208 if (DataType .isString (format .dataType ()) == false ) {
170209 throw new IllegalArgumentException ("unsupported data type for date_parse [" + format .dataType () + "]" );
171210 }
172- String localeAsString = locale == null ? null : ((BytesRef ) locale .fold (toEvaluator .foldCtx ())).utf8ToString ();
173- Locale locale = localeAsString == null ? null : Locale .forLanguageTag (localeAsString );
174- if (localeAsString != null && locale == null ) {
175- throw new IllegalArgumentException ("unsupported locale [" + localeAsString + "]" );
211+ var parsedOptions = this .parseOptions ();
212+ String localeAsString = (String )parsedOptions .get (LOCALE_PARAM_NAME );
213+ Locale locale = localeAsString == null ? null : LocaleUtils .parse (localeAsString );
214+
215+ String timezoneAsString = (String )parsedOptions .get (TIME_ZONE_PARAM_NAME );
216+ ZoneId timezone = null ;
217+ try {
218+ if (timezoneAsString != null ) {
219+ timezone = ZoneId .of (timezoneAsString );
220+ }
221+ } catch (ZoneRulesException e ) {
222+ throw new IllegalArgumentException ("unsupported timezone [" + timezoneAsString + "]" );
176223 }
177224
178- String timezoneAsString = timezone == null ? null : ((BytesRef ) timezone .fold (toEvaluator .foldCtx ())).utf8ToString ();
179- TimeZone timezone = timezoneAsString == null ? null : TimeZone .getTimeZone (timezoneAsString );
180225 if (format .foldable ()) {
181226 try {
182227 DateFormatter formatter = toFormatter (format .fold (toEvaluator .foldCtx ()));
183228 if (locale != null ) {
184229 formatter = formatter .withLocale (locale );
185230 }
186231 if (timezone != null ) {
187- formatter = formatter .withZone (timezone . toZoneId () );
232+ formatter = formatter .withZone (timezone );
188233 }
189234 return new DateParseConstantEvaluator .Factory (source (), fieldEvaluator , formatter );
190235 } catch (IllegalArgumentException e ) {
@@ -205,15 +250,14 @@ public Expression replaceChildren(List<Expression> newChildren) {
205250 source (),
206251 newChildren .get (0 ),
207252 newChildren .size () > 1 ? newChildren .get (1 ) : null ,
208- newChildren .size () > 2 ? newChildren .get (2 ) : null ,
209- newChildren .size () > 3 ? newChildren .get (3 ) : null
253+ newChildren .size () > 2 ? newChildren .get (2 ) : null
210254 );
211255 }
212256
213257 @ Override
214258 protected NodeInfo <? extends Expression > info () {
215259 Expression first = format != null ? format : field ;
216260 Expression second = format != null ? field : null ;
217- return NodeInfo .create (this , DateParse ::new , first , second , locale , timezone );
261+ return NodeInfo .create (this , DateParse ::new , first , second , options );
218262 }
219263}
0 commit comments