1414import java .time .format .DateTimeFormatter ;
1515import java .time .format .DateTimeParseException ;
1616import java .time .temporal .ChronoUnit ;
17+ import java .util .Locale ;
1718import java .util .regex .Pattern ;
1819import lombok .experimental .UtilityClass ;
1920import org .opensearch .sql .data .model .ExprTimeValue ;
@@ -162,9 +163,8 @@ public static LocalDate extractDate(ExprValue value, FunctionProperties function
162163
163164 public static ZonedDateTime getRelativeZonedDateTime (String input , ZonedDateTime baseTime ) {
164165 try {
165- Instant localDateTime =
166- LocalDateTime .parse (input , DIRECT_FORMATTER ).toInstant (ZoneOffset .UTC );
167- return localDateTime .atZone (baseTime .getZone ());
166+ Instant parsed = LocalDateTime .parse (input , DIRECT_FORMATTER ).toInstant (ZoneOffset .UTC );
167+ return parsed .atZone (baseTime .getZone ());
168168 } catch (DateTimeParseException ignored ) {
169169 }
170170
@@ -177,85 +177,121 @@ public static ZonedDateTime getRelativeZonedDateTime(String input, ZonedDateTime
177177 while (i < input .length ()) {
178178 char c = input .charAt (i );
179179 if (c == '@' ) {
180- // parse snap
181180 int j = i + 1 ;
182- while (j < input .length () && Character .isLetter (input .charAt (j ))) {
181+ while (j < input .length () && Character .isLetterOrDigit (input .charAt (j ))) {
183182 j ++;
184183 }
185- String snapUnit = input .substring (i + 1 , j );
186- result = applySnap (result , snapUnit );
184+ String rawUnit = input .substring (i + 1 , j );
185+ result = applySnap (result , rawUnit );
187186 i = j ;
188187 } else if (c == '+' || c == '-' ) {
189- // parse offset
190188 int j = i + 1 ;
191189 while (j < input .length () && Character .isDigit (input .charAt (j ))) {
192190 j ++;
193191 }
194- int value = Integer .parseInt (input .substring (i + 1 , j ));
195- // optional unit
192+ String valueStr = input .substring (i + 1 , j );
193+ int value = valueStr .isEmpty () ? 1 : Integer .parseInt (valueStr );
194+
196195 int k = j ;
197196 while (k < input .length () && Character .isLetter (input .charAt (k ))) {
198197 k ++;
199198 }
200- String unit = input .substring (j , k );
201- if (unit .isEmpty ()) {
202- unit = "s" ; // default to seconds
203- }
204- result = applyOffset (result , String .valueOf (c ), value , unit );
199+ String rawUnit = input .substring (j , k );
200+ result = applyOffset (result , String .valueOf (c ), value , rawUnit );
205201 i = k ;
206202 } else {
207- throw new IllegalArgumentException ("Wrong relative time expression: " + input );
203+ throw new IllegalArgumentException (
204+ "Unexpected character '" + c + "' at position " + i + " in input: " + input );
208205 }
209206 }
210207
211208 return result ;
212209 }
213210
214211 private static ZonedDateTime applyOffset (
215- ZonedDateTime base , String sign , int value , String unit ) {
216- ChronoUnit chronoUnit = parseUnit (unit );
212+ ZonedDateTime base , String sign , int value , String rawUnit ) {
213+ String unit = normalizeUnit (rawUnit );
214+ if ("q" .equals (unit )) {
215+ int months = value * 3 ;
216+ return sign .equals ("-" ) ? base .minusMonths (months ) : base .plusMonths (months );
217+ }
218+
219+ ChronoUnit chronoUnit =
220+ switch (unit ) {
221+ case "s" -> ChronoUnit .SECONDS ;
222+ case "m" -> ChronoUnit .MINUTES ;
223+ case "h" -> ChronoUnit .HOURS ;
224+ case "d" -> ChronoUnit .DAYS ;
225+ case "w" -> ChronoUnit .WEEKS ;
226+ case "M" -> ChronoUnit .MONTHS ;
227+ case "y" -> ChronoUnit .YEARS ;
228+ default -> throw new IllegalArgumentException ("Unsupported offset unit: " + rawUnit );
229+ };
230+
217231 return sign .equals ("-" ) ? base .minus (value , chronoUnit ) : base .plus (value , chronoUnit );
218232 }
219233
220- private static ZonedDateTime applySnap (ZonedDateTime base , String unit ) {
221- switch (unit ) {
222- case "s" :
223- return base .truncatedTo (ChronoUnit .SECONDS );
224- case "m" :
225- return base .truncatedTo (ChronoUnit .MINUTES );
226- case "h" :
227- return base .truncatedTo (ChronoUnit .HOURS );
228- case "d" :
229- return base .truncatedTo (ChronoUnit .DAYS );
230- case "w" :
231- return base .minusDays ((base .getDayOfWeek ().getValue () % 7 )).truncatedTo (ChronoUnit .DAYS );
232- case "M" :
233- return base .withDayOfMonth (1 ).truncatedTo (ChronoUnit .DAYS );
234- case "y" :
235- return base .withDayOfYear (1 ).truncatedTo (ChronoUnit .DAYS );
236- default :
237- throw new IllegalArgumentException ("Unsupported snap unit: " + unit );
238- }
234+ private static ZonedDateTime applySnap (ZonedDateTime base , String rawUnit ) {
235+ String unit = normalizeUnit (rawUnit );
236+
237+ return switch (unit ) {
238+ case "s" -> base .truncatedTo (ChronoUnit .SECONDS );
239+ case "m" -> base .truncatedTo (ChronoUnit .MINUTES );
240+ case "h" -> base .truncatedTo (ChronoUnit .HOURS );
241+ case "d" -> base .truncatedTo (ChronoUnit .DAYS );
242+ case "w" -> base .minusDays ((base .getDayOfWeek ().getValue () % 7 )).truncatedTo (ChronoUnit .DAYS );
243+ case "M" -> base .withDayOfMonth (1 ).truncatedTo (ChronoUnit .DAYS );
244+ case "y" -> base .withDayOfYear (1 ).truncatedTo (ChronoUnit .DAYS );
245+ case "q" -> {
246+ int month = base .getMonthValue ();
247+ int quarterStart = ((month - 1 ) / 3 ) * 3 + 1 ;
248+ yield base .withMonth (quarterStart ).withDayOfMonth (1 ).truncatedTo (ChronoUnit .DAYS );
249+ }
250+ default -> {
251+ if (unit .matches ("w[0-7]" )) {
252+ int targetDay =
253+ unit .equals ("w0" ) || unit .equals ("w7" ) ? 7 : Integer .parseInt (unit .substring (1 ));
254+ int diff = (base .getDayOfWeek ().getValue () - targetDay + 7 ) % 7 ;
255+ yield base .minusDays (diff ).truncatedTo (ChronoUnit .DAYS );
256+ } else {
257+ throw new IllegalArgumentException ("Unsupported snap unit: " + rawUnit );
258+ }
259+ }
260+ };
239261 }
240262
241- private static ChronoUnit parseUnit (String unit ) {
242- switch (unit ) {
243- case "s" :
244- return ChronoUnit .SECONDS ;
245- case "m" :
246- return ChronoUnit .MINUTES ;
247- case "h" :
248- return ChronoUnit .HOURS ;
249- case "d" :
250- return ChronoUnit .DAYS ;
251- case "w" :
252- return ChronoUnit .WEEKS ;
253- case "M" :
254- return ChronoUnit .MONTHS ;
255- case "y" :
256- return ChronoUnit .YEARS ;
257- default :
258- throw new IllegalArgumentException ("Unsupported time unit: " + unit );
263+ private static String normalizeUnit (String rawUnit ) {
264+ // strict minute (m or M)
265+ switch (rawUnit .toLowerCase (Locale .ROOT )) {
266+ case "m" , "min" , "mins" , "minute" , "minutes" -> {
267+ return "m" ;
268+ }
269+ case "s" , "sec" , "secs" , "second" , "seconds" -> {
270+ return "s" ;
271+ }
272+ case "h" , "hr" , "hrs" , "hour" , "hours" -> {
273+ return "h" ;
274+ }
275+ case "d" , "day" , "days" -> {
276+ return "d" ;
277+ }
278+ case "w" , "wk" , "wks" , "week" , "weeks" -> {
279+ return "w" ;
280+ }
281+ case "mon" , "month" , "months" -> {
282+ return "M" ; // month
283+ }
284+ case "y" , "yr" , "yrs" , "year" , "years" -> {
285+ return "y" ;
286+ }
287+ case "q" , "qtr" , "qtrs" , "quarter" , "quarters" -> {
288+ return "q" ;
289+ }
290+ default -> {
291+ String lower = rawUnit .toLowerCase ();
292+ if (lower .matches ("w[0-7]" )) return lower ;
293+ throw new IllegalArgumentException ("Unsupported unit alias: " + rawUnit );
294+ }
259295 }
260296 }
261297}
0 commit comments