'Finding Last Fired time using a Cron Expression in Java

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.


Solution 1:[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:[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:[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:[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:[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.

Solution 6:[6]

In case you are using org.quartz, is possible to use context.getPreviousFireTime();

Example:

public class TestJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {

        context.getPreviousFireTime(); 
    }
}

If you are using context.getTrigger().getPreviousFireTime(), you will have the triggered time of the Job that is running at the moment.

Solution 7:[7]

It's amazing that there is still no quartz method for getting the previous fired time based on a CronExpression...

How to get last fired time ?

If you are manipulating basic CRON like 0 0 0 * * ? (At 00:00:00am every day) you can use João Neves' solution (using com.cronutils.model.time.ExecutionTime).

Otherwise if you are manipulating complex CRON like 0 30 11 ? * MON,THU * it will not works. You'll get random results (I had a Wednesday for this one...).

Edit: I did other tests and it looks working better with the latest version (previous tests were made with version < 3.1.6). Note: You need Java 8 if you want to use version > 3.1.6.

The solution you can use is to store it when your job is triggered.


How to verify that the job has been triggered ?

The solution I found is to use getNextValidTimeAfter from Quartz (CronExpression). This one works fine. You'll ask me what I'm talking about as we are looking for the previous valid time ! You are right, give me one second !

Let's imagine we have a once a month CRON (0 0 16 1 * ? = At 16:00:00pm, on the 1st day, every month) and we want to check everyday that the previous execution worked. You'll have to store the getNextValidTime at each execution and compare this date with today's date. eg (format DD/MM/YYYY):

• 01/01/2019 ? job triggered, we store next fire time (let's call it nextFireTime):

CronExpression trigger = new CronExpression("0 0 16 1 * ?");
Date nextFireTime = trigger.getNextValidTimeAfter(new Date());
// = 01/02/2019

• 02/01/2019 ? verification of the day: 02/01/2019 < 01/02/2019 OK

...

• 01/02/2019 ? let's imagine server is down, the job is not triggered.

• 02/02/2019 ? server on, verification of the day: 02/02/2019 > 01/02/2019 KO !

? We know the previous fire time hasn't worked. You can know do what you want (trigger the job and store the new nextFireTime).


Another option that may interest you, see MISFIRE_INSTRUCTION_FIRE_NOW.

The job is executed immediately after the scheduler discovers misfire situation. This is the smart policy. Example scenario: you have scheduled some system clean up at 2 AM. Unfortunately the application was down due to maintenance by that time and brought back on 3 AM. So the trigger misfired and the scheduler tries to save the situation by running it as soon as it can - at 3 AM.

From (https://dzone.com/articles/quartz-scheduler-misfire)

e.g.:

Trigger trigger = newTrigger().
 withSchedule(
  cronSchedule("0 0 9-17 ? * MON-FRI").
   withMisfireHandlingInstructionFireAndProceed()
 ).
 build();

Official doc: https://www.quartz-scheduler.org/api/2.1.7/org/quartz/CronTrigger.html

Solution 8:[8]

I am using cron-utils 9.1.6 to get the last execution time based on 'now' and the 'cron expression', and the expected end time adding the expected duration in seconds.

The code's language is Scala.

import java.time.ZonedDateTime
import com.cronutils.model.definition.CronDefinitionBuilder
import com.cronutils.model.time.ExecutionTime
import com.cronutils.parser.CronParser
import com.cronutils.model.CronType.QUARTZ
import java.time.ZoneOffset.UTC

val parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(QUARTZ))
val cronExpression = "0 0 23 ? * *"
val now = ZonedDateTime.now(UTC)
val expectedDurationSec = 1500

println(s"now: $now")
val executionTime = ExecutionTime.forCron(parser.parse(cronExpression))
val lastExecution = executionTime.lastExecution(now)
println(s"lastExecution: ${lastExecution.get()}")
val expectedEnd = lastExecution.get().plusSeconds(expectedDurationSec)
println(s"expectedEnd: $expectedEnd")

the output is:

now: 2022-04-21T01:08:27.499Z
lastExecution: 2022-04-20T23:00Z
expectedEnd: 2022-04-20T23:25Z

Solution 9:[9]

public class CronMircoUtils {

    /**
     * Use this method to calculate previous valid date for cron
     * @param ce CronExpression object with cron expression
     * @param date Date for find previous valid date
     * @return
     */
    public static Date getPreviousValidDate(CronExpression ce, Date date) {
        try {
            Date nextValidTime = ce.getNextValidTimeAfter(date);
            Date subsequentNextValidTime = ce.getNextValidTimeAfter(nextValidTime);
            long interval = subsequentNextValidTime.getTime() - nextValidTime.getTime();
            return new Date(nextValidTime.getTime() - interval);
        } catch (Exception e) {
            throw new IllegalArgumentException("Unsupported cron or date", e);
        }
    }
}

Source Code in https://github.com/devbhuwan/cron-micro-utils

Solution 10:[10]

Its working solution

   pom :
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.0</version>
        </dependency>

   javaCode:
        //Provide your format of date  want
        String format="dd-MMM-YYYY hh:mm:ss";
        //Provide your cron expression you want
        String cronExpression="* * 12 ? * FRI *";
        SimpleDateFormat sdf=new SimpleDateFormat(format);
        CronExpression expression = new CronExpression(cronExpression);
        Date  currentDate=new Date();
        Date nextDate=expression.getNextValidTimeAfter(currentDate);
        long interval = nextDate.getTime()-currentDate.getTime();
        Date previousDate= new Date(currentDate.getTime() - interval);
        System.out.Println(sdf.format(previousDate));

Here im getting the difference between the current time and next fire time and exluding this difference time from current time so that we will get last fired time

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 João Neves
Solution 2
Solution 3 iPhrends
Solution 4 Aldo
Solution 5
Solution 6 Kurt Van den Branden
Solution 7
Solution 8
Solution 9
Solution 10