Finding Last Fired time using a Cron Expression in Java

20,344

Solution 1

cron-utils is an opensource Java library to parse, validate, migrate crons that supports the operation you need. To get the previous date from a cron before a given time simply:

//Get date for last execution
DateTime now = DateTime.now();
ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("* * * * * * *"));
DateTime lastExecution = executionTime.lastExecution(now));

Bear in mind that in its current state it is a bit buggy and may not compute correctly for more complex cron expressions.

Solution 2

First, I am not aware of an existing library that supports this. Quartz might, but the standard Java class libraries certainly don't.

Second, strictly speaking what you are asking for originally asked for is impossible. The best that a library can tell you is that the cron expression would have or should have fired. The only thing that could (theoretically) tell you the last time that a cron expression actually fired is the scheduler instance itself.

Solution 3

import org.springframework.scheduling.support.CronSequenceGenerator;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

public class DateCalendarUtil {

    public static Date lastRunOn(String cronExpression) {
        final Date nextExecution = fromLocalDateTime(toLocalDate(nextCronDate(cronExpression)).atTime(23, 59, 59));
        return subtract(nextExecution, numberOfDays(nextExecution, nextCronDate(cronExpression, nextExecution)).intValue());
    }

    /**
     * Converts {@link Date} to {@link LocalDate} with default system {@link ZoneId}
     *
     * @param date to be converted to {@link LocalDate}
     * @return converted {@link Date}
     */
    public static LocalDate toLocalDate(Date date) {
        return toLocalDate(date, ZoneId.systemDefault());
    }

    /**
     * Converts {@link Date} to {@link LocalDate} with provided {@link ZoneId}
     * @param date to be converted to {@link LocalDate}
     * @param zoneId with which {@link Date} will be converted
     * @return converted {@link Date}
     */
    public static LocalDate toLocalDate(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).toLocalDate();
    }

    /**
     * Converts {@link Date} to {@link LocalDateTime} with provided {@link ZoneId}
     * @param date to be converted to {@link LocalDateTime} with provided {@link ZoneId}
     * @param zoneId with which {@link Date} will be converted
     * @return converted {@link Date}
     */
    public static LocalDateTime toLocalDateTime(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).toLocalDateTime();
    }

    /**
     * Converts {@link Date} to {@link LocalDateTime} with system default {@link ZoneId}
     *
     * @param date to be converted to {@link LocalDateTime}
     * @return converted {@link Date}
     */
    public static LocalDateTime toLocalDateTime(Date date) {
        return toLocalDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDate} to {@link Date} with default system {@link ZoneId}
     * @param localDate to be converted to {@link Date}
     * @return converted {@link LocalDate}
     */
    public static Date fromLocalDate(LocalDate localDate) {
        return fromLocalDate(localDate, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDate} to {@link Date} with provided {@link ZoneId}
     * @param localDate to be converted to {@link Date}
     * @param zoneId with which {@link LocalDate} converted
     * @return converted {@link LocalDate}
     */
    public static Date fromLocalDate(LocalDate localDate, ZoneId zoneId) {
        return Date.from(localDate.atStartOfDay(zoneId).toInstant());
    }

    /**
     * Converts {@link LocalDateTime} to {@link Date} with default system {@link ZoneId}
     *
     * @param localDateTime to be converted to {@link Date}
     * @return converted {@link LocalDateTime}
     */
    public static Date fromLocalDateTime(LocalDateTime localDateTime) {
        return fromLocalDateTime(localDateTime, ZoneId.systemDefault());
    }

    /**
     * Converts {@link LocalDateTime} to {@link Date} with provided {@link ZoneId}
     *
     * @param localDateTime to be converted to {@link Date}
     * @param zoneId        with which localDateTime converted to {@link Date}
     * @return converted {@link Date}
     */
    private static Date fromLocalDateTime(LocalDateTime localDateTime, ZoneId zoneId) {
        return Date.from(localDateTime.atZone(zoneId).toInstant());
    }

    public static Date yesterday() {
        return yesterday(TimeZone.getDefault());
    }

    public static Date yesterday(TimeZone timezone) {
        return subtract(new Date(), 1, timezone);
    }

    /**
     * Generates start time of give date with system default {@link TimeZone}
     * @param date Date of which start time to be generated
     * @return Date with start time as 00:00:00
     */
    public static Date startTime(Date date) {
        return startTime(date, TimeZone.getDefault());
    }

    /**
     * Generates start time of give date with provided {@link TimeZone}
     * @param date Date of which start time to be generated
     * @param timeZone with which {@link Calendar} created
     * @return Date with start time as 00:00:00
     */
    public static Date startTime(Date date, TimeZone timeZone) {
        Calendar calendar = Calendar.getInstance(timeZone);
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        return calendar.getTime();
    }

    /**
     * Generates end time of give date with system default {@link TimeZone}
     * @param date Date of which end time to be generated
     * @return Date with end time as 23:59:59
     */
    public static Date endTime(Date date) {
        return endTime(date, TimeZone.getDefault());
    }

    /**
     * Generates end time of give date with provided {@link TimeZone}
     * @param date Date of which end time to be generated
     * @param timeZone with which {@link Calendar} created
     * @return Date with end time as 23:59:59
     */
    public static Date endTime(Date date, TimeZone timeZone) {
        Calendar calendar = Calendar.getInstance(timeZone);
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 23);
        calendar.set(Calendar.MINUTE, 59);
        calendar.set(Calendar.SECOND, 59);
        return calendar.getTime();
    }

    /**
     * Calculates number of days between from and to
     * @param from start Date
     * @param to end date
     * @return number of days including last date
     */
    public static Long numberOfDays(Date from, Date to) {
        return TimeUnit.DAYS.convert(to.getTime() - from.getTime(), TimeUnit.MILLISECONDS) + 1;
    }

    /**
     * Gives next {@link Date} from given cron expression
     * @param cronExpression cron expression
     * @return next {@link Date}
     */
    public static Date nextCronDate(String cronExpression) {
        return nextCronDate(cronExpression, new Date());
    }

    public static Date nextCronDate(String cronExpression, Date date) {
        CronSequenceGenerator generator = new CronSequenceGenerator(cronExpression, TimeZone.getTimeZone("IST"));
        return DateCalendarUtil.fromLocalDate(DateCalendarUtil.toLocalDate(generator.next(date)));
    }
}

Solution 4

Quartz seems to have some library support for cron expressions.

See the Javadoc for the CronExpression class, which has a method called getTimeBefore. I.e.,

CronExpression cron = new CronExpression("0 15 10 ? * *");
Date today = new Date();
Date previousExecution = cron.getTimeBefore(today);

It might depend on the Quartz version whether this works.

Looking at the latest source (version 2.3.0 at time of writing) this method has not been implemented and always returns null.

Solution 5

A solution I have found with Quartz is to go back one time interval for the trigger and calculate what would have been the next firing time. By iterating through all the triggers the most recent time that a trigger should have fired in the past can be determined.


Calculate the interval between each firing:

Date nextFireTime = trigger.getNextFireTime();
Date subsequentFireTime = trigger.getFireTimeAfter(nextFireTime);
long interval = subsequentFireTime.getTime() - nextFireTime.getTime();

Find the next firing time for one time until interval in the past:

Date previousPeriodTime = new Date(System.currentTimeMillis() - interval);
Date previousFireTime = trigger.getFireTimeAfter(previousPeriodTime);

I have found that if you are using a CronTrigger this prevents you asking for a fire time in the past. To work around this I modify the start time, so the above snippet becomes:

Date originalStartTime = trigger.getStartTime(); // save the start time
Date previousPeriodTime = new Date(originalStartTime.getTime() - interval);
trigger.setStartTime(previousPeriodTime);
Date previousFireTime = trigger.getFireTimeAfter(previousPeriodTime);
trigger.setStartTime(originalStartTime); // reset the start time to be nice

Iterate through all of the triggers and find the one that is most recently in the past:

for (String groupName : scheduler.getTriggerGroupNames()) {
    for (String triggerName : scheduler.getTriggerNames(groupName)) {
        Trigger trigger = scheduler.getTrigger(triggerName, groupName);
        // code as detailed above...
        interval = ...
        previousFireTime = ...
    }
}

I'll leave it as an exercise to the reader to refactor this into helper methods or classes. I actually use the above algorithm in a subclassed delegating trigger that I then place in a set sorted by previous firing times.

Share:
20,344
a-sak
Author by

a-sak

Updated on November 21, 2020

Comments

  • a-sak
    a-sak over 3 years

    Is there a way in Java to find the "Last Fired Time" from a Cron Expression?

    E.g. If now = 25-Apr-2010 10PM, and the cron expression is 0 15 10 ? * * (quartz), it should return 25-Apr-2010 10:15AM.

    Note:

    1. I do not care if we use standard cron expressions (like Unix and Quartz) or less popular ones if they can fetch me the correct "Last Fired Time"
    2. Also it is not literally "Last Fire time" as the trigger may not have fired, but logically there should be a way of telling when it (would have) fired last.
  • a-sak
    a-sak about 14 years
    Hi Stephen, Thanks for your feedback. I have added a little more info in notes of my question to clear my problem.
  • Ahamed
    Ahamed over 10 years
    Difference between the subsequent fires will not be same always.
  • João Neves
    João Neves over 8 years
    This will only work for periodic firings. What happens when there's no subsequentNextValidTime or when firings are irregular (e.g. fire at minutes 10, 12, 11, 17, 37)?
  • Marin
    Marin over 7 years
    hi Joao, This works even if job dont run (example: server offline)?
  • João Neves
    João Neves over 7 years
    @NunoMarinho Yes, this simply calculates the previous date from a cron expression. The library itself knows nothing of what you're doing with the cron expression. This library does not schedule actions or jobs to be executed it just calculates dates based on cron expressions. I haven't used this library in a long time though, make sure it suits your needs and thoroughly test your application after using it.
  • L_J
    L_J almost 6 years
    While this code may answer the question, providing information on how and why it solves the problem improves its long-term value.
  • Husain
    Husain over 5 years
    Ahamed is correct. This code is incorrect generally and only correct in a small subset of cases.
  • Jan Larsen
    Jan Larsen about 5 years
    This code has all kinds of crazy assumptions; why would currentDate be exactly in the middle between two executions? Also, this is a good example of how NOT to manipulate dates. What about day time saving and leap seconds?
  • Baldeep Singh Kwatra
    Baldeep Singh Kwatra about 4 years
    Code looks great but there is no definition to the subtract function you are using. Please add that.
  • Scott
    Scott over 2 years
    Answered in 2010, still returns null :(
  • Domenico Campagnolo
    Domenico Campagnolo about 2 years
    today, it still returns null :(