11package com .xxl .job .admin .scheduler .cron ;
2+ /*
3+ * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
4+ * Copyright IBM Corp. 2024, 2025
5+ *
6+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7+ * use this file except in compliance with the License. You may obtain a copy
8+ * of the License at
9+ *
10+ * http://www.apache.org/licenses/LICENSE-2.0
11+ *
12+ * Unless required by applicable law or agreed to in writing, software
13+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+ * License for the specific language governing permissions and limitations
16+ * under the License.
17+ *
18+ * Borrowed from quartz v2.5.2
19+ */
220
321import java .io .Serializable ;
422import java .text .ParseException ;
6381 * <tr>
6482 * <td><code>Month</code></td>
6583 * <td> </td>
66- * <td><code>0-11 or JAN-DEC</code></td>
84+ * <td><code>1-12 or JAN-DEC</code></td>
6785 * <td> </td>
6886 * <td><code>, - * /</code></td>
6987 * </tr>
186204 * @author Sharada Jambula, James House
187205 * @author Contributions from Mads Henderson
188206 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
189- *
190- * Borrowed from quartz v2.5.0
191207 */
192208public final class CronExpression implements Serializable , Cloneable {
193209
@@ -649,6 +665,10 @@ protected int storeExpressionVals(int pos, String s, int type)
649665 addToSet (ALL_SPEC_INT , -1 , incr , type );
650666 return i ;
651667 } else if (c == 'L' ) {
668+
669+ if (type < DAY_OF_MONTH )
670+ throw new ParseException ("'L' not expected in seconds, minutes or hours fields." , i );
671+
652672 i ++;
653673 if (type == DAY_OF_WEEK ) {
654674 addToSet (7 , 7 , 0 , type );
@@ -705,15 +725,15 @@ protected int storeExpressionVals(int pos, String s, int type)
705725
706726 private void checkIncrementRange (int incr , int type , int idxPos ) throws ParseException {
707727 if (incr > 59 && (type == SECOND || type == MINUTE )) {
708- throw new ParseException ("Increment > 60 : " + incr , idxPos );
728+ throw new ParseException ("Increment >= 60 : " + incr , idxPos );
709729 } else if (incr > 23 && (type == HOUR )) {
710- throw new ParseException ("Increment > 24 : " + incr , idxPos );
730+ throw new ParseException ("Increment >= 24 : " + incr , idxPos );
711731 } else if (incr > 31 && (type == DAY_OF_MONTH )) {
712- throw new ParseException ("Increment > 31 : " + incr , idxPos );
732+ throw new ParseException ("Increment >= 31 : " + incr , idxPos );
713733 } else if (incr > 7 && (type == DAY_OF_WEEK )) {
714- throw new ParseException ("Increment > 7 : " + incr , idxPos );
734+ throw new ParseException ("Increment >= 7 : " + incr , idxPos );
715735 } else if (incr > 12 && (type == MONTH )) {
716- throw new ParseException ("Increment > 12 : " + incr , idxPos );
736+ throw new ParseException ("Increment >= 12 : " + incr , idxPos );
717737 }
718738 }
719739
@@ -1293,15 +1313,20 @@ public Date getTimeAfter(Date afterTime) {
12931313 day = -1 ;
12941314 }
12951315 }
1316+
1317+ boolean needAdvance = false ;
12961318 if (smallestDay .isPresent ()) {
12971319 if (day == -1 || smallestDay .get () < day ) {
12981320 day = smallestDay .get ();
1321+ needAdvance = true ;
12991322 }
13001323 } else if (day == -1 ) {
13011324 day = 1 ;
13021325 mon ++;
1326+ needAdvance = true ;
13031327 }
1304- if (day != t || mon != tmon ) {
1328+
1329+ if (needAdvance && (day != t || mon != tmon )) {
13051330 cl .set (Calendar .SECOND , 0 );
13061331 cl .set (Calendar .MINUTE , 0 );
13071332 cl .set (Calendar .HOUR_OF_DAY , 0 );
@@ -1311,6 +1336,7 @@ public Date getTimeAfter(Date afterTime) {
13111336 // are 1-based
13121337 continue ;
13131338 }
1339+
13141340 } else if (dayOfWSpec && !dayOfMSpec ) { // get day by day of week rule
13151341 if (lastDayOfWeek ) { // are we looking for the last XXX day of
13161342 // the month?
@@ -1523,12 +1549,53 @@ protected void setCalendarHour(Calendar cal, int hour) {
15231549 }
15241550
15251551 /**
1526- * NOT YET IMPLEMENTED: Returns the time before the given time
1552+ * Returns the time before the given time
15271553 * that the <code>CronExpression</code> matches.
1554+ *
1555+ * @param endTime a time for which the previous
1556+ * matching time is returned
1557+ * @return the previous matching time before the given end time,
1558+ * or null if there are no previous matching times
15281559 */
15291560 public Date getTimeBefore (Date endTime ) {
1530- // FUTURE_TODO: implement QUARTZ-423
1531- return null ;
1561+ // the current implementation is not a direct calculation, but rather
1562+ // uses getTimeAfter with a binary search to find the previous match time
1563+ long end = endTime .getTime ();
1564+ long min = 0 ; // the epoch date is the minimum supported by this class
1565+ long max = end ;
1566+ // check if it's satisfiable at all
1567+ Date date = new Date (min );
1568+ Date after = getTimeAfter (date );
1569+ if (after == null || after .getTime () >= end )
1570+ return null ; // there are no after-times before end
1571+ // from this point forward min's time-after is always less than end,
1572+ // and max's time-after is always equal to or greater than end
1573+ // so we just need to shrink the interval until they meet.
1574+ // optimization - perform inverse binary search to find a tighter lower bound
1575+ long interval = 60 * 60 * 1000 ; // start with a reasonable interval
1576+ while (interval < max ) {
1577+ date .setTime (max - interval );
1578+ after = getTimeAfter (date );
1579+ if (after != null && after .getTime () < max ) {
1580+ min = date .getTime (); // found a closer min
1581+ break ;
1582+ }
1583+ interval *= 2 ;
1584+ }
1585+ // perform a regular binary search to find the earliest moment
1586+ // whose time-after is equal to or greater than the end time -
1587+ // this moment is the previous match time itself
1588+ while (max - min > 1000 ) { // we can stop at 1 second resolution
1589+ long mid = (min + max ) >>> 1 ;
1590+ date .setTime (mid );
1591+ after = getTimeAfter (date );
1592+ if (after != null && after .getTime () < end )
1593+ min = mid ;
1594+ else
1595+ max = mid ;
1596+ }
1597+ date .setTime (max - max % 1000 ); // round to second
1598+ return date ;
15321599 }
15331600
15341601 /**
@@ -1585,18 +1652,22 @@ private Optional<Integer> findSmallestDay(int day, int mon, int year, TreeSet<In
15851652
15861653 final int lastDay = getLastDayOfMonth (mon , year );
15871654 // For "L", "L-1", etc.
1588- int smallestDay = Optional .ofNullable (set .ceiling (LAST_DAY_OFFSET_END - (lastDay - day )))
1655+ final int smallestDay = Optional .ofNullable (set .ceiling (LAST_DAY_OFFSET_END - (lastDay - day )))
15891656 .map (d -> d - LAST_DAY_OFFSET_START + 1 )
15901657 .orElse (Integer .MAX_VALUE );
15911658
15921659 // For "1", "2", etc.
15931660 SortedSet <Integer > st = set .subSet (day , LAST_DAY_OFFSET_START );
15941661 // make sure we don't over-run a short month, such as february
15951662 if (!st .isEmpty () && st .first () < smallestDay && st .first () <= lastDay ) {
1596- smallestDay = st .first ();
1663+ return Optional . of ( st .first () );
15971664 }
15981665
1599- return smallestDay == Integer .MAX_VALUE ? Optional .empty () : Optional .of (smallestDay );
1666+ if (smallestDay == Integer .MAX_VALUE ) {
1667+ return Optional .empty ();
1668+ } else {
1669+ return Optional .of (smallestDay + lastDay - LAST_DAY_OFFSET_START + 1 );
1670+ }
16001671 }
16011672
16021673 private void readObject (java .io .ObjectInputStream stream )
0 commit comments