6

guys,

I have monthly jobs scheduled(using Quartz) by users. Users provide starting date f or first job to run, it could be any day of month 1-31

My question is how to schedule this using cron trigger, having in mind that not all month have 31,30,29th days. In such case job should run closest previous day of the month. So, lets say April has only 30 days, so job has to run on 30th of April.

Can it be done using single cron trigger? Or should it be combination of triggers? I tried to play with CronExpression to see how it handles such cases:

CronExpression ce = new CronExpression("0 0 0 30 JAN-DEC ? *");
Date nextValidTime = ce.getNextValidTimeAfter(//**27th of February**//);

I've got nextValidTime equal to 30th of March, so cron just "skipped" February. Any help would be highly appreciated. Thanks in advance.

ilu
  • 133
  • 2
  • 7

4 Answers4

6

The 'L' character is allowed for the day-of-month and day-of-week fields. This character > is short-hand for "last", but it has different meaning in each of the two fields. For example, the value "L" in the day-of-month field means "the last day of the month" - day 31 for January, day 28 for February on non-leap years. If used in the day-of-week field by itself, it simply means "7" or "SAT". But if used in the day-of-week field after another value, it means "the last xxx day of the month" - for example "6L" means "the last friday of the month". You can also specify an offset from the last day of the month, such as "L-3" which would mean the third-to-last day of the calendar month. When using the 'L' option, it is important not to specify lists, or ranges of values, as you'll get confusing/unexpected results.

http://quartz-scheduler.org/api/2.0.0/org/quartz/CronExpression.html

new CronExpression("0 0 0 L JAN-DEC ? *");

Edit:

I would just do something like this then

Calendar tCalendar = Calendar.getInstance();
tCalendar.set(2009, Calendar.FEBRUARY/*int*/, 1); // for example Feb, 2009 -- day doesn't matter here
if(userSelectedDay > tCalendar.getActualMaximum(Calendar.DAY_OF_MONTH) ){
    //Fix user day
    //fixedDay = tCalendar.getActualMaximum(Calendar.DAY_OF_MONTH)

    // Or, for that month 
    //new CronExpression("0 0 0 L JAN-DEC ? *");
}
Sully
  • 14,672
  • 5
  • 54
  • 79
  • 6
    Using L (or any variants L-x) does not help in my situation. User can specify any starting day for job to be run first time. Day-of-month from this starting date has to be used to run jobs monthly. So, it could be anything from 1-31. The problem is with 29-31 as not all months have them. So, if user selected 30th of Janurary, job has to run on 29 of February, 30th March, 30th of April and so on. Using L-3 will fire trigger on 25th of February(for non leap year), 28th of March - but this not what I need. Job has to run on specified day-of-month or previous closest date. – ilu Jul 12 '12 at 18:52
1

Here is my solution. The idea is trying to build a set of cron expressions which will be passed in to a trigger builder.

  • If input day < 28. Use a single expression:
  • If input day = 29 or = 30. Use 2 expressions: The last day of Feb, and the specific day of the other months.
  • If input day = 31. Use the last day of the month.

    public static Set<String> byDay(int day) {
        if (day < 1 || day > 31) {
            throw new IllegalArgumentException("The input day must be in range: 1 <= day <= 31");
        }
        if (day <= 28) {
            return Collections.singleton(String.format("0 0 0 %d JAN-DEC ? *", day));
        }
        if (day == 29 || day == 30) {
            Set<String> expressions = new HashSet<String>();
            expressions.add("0 0 0 L FEB ? *");
            expressions.add(String.format("0 0 0 %d JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC ? *", day));
            return Collections.unmodifiableSet(expressions);
        }
        // day == 31
        return Collections.singleton("0 0 0 L * ? *");
    }
    
An Do Tran
  • 11
  • 3
0

Try this snippet, it creates from 1 to 3 triggers (covers a whole year) depending on day:

        Set<Trigger> triggers = new HashSet<>(3);

        CronScheduleBuilder interval = CronScheduleBuilder.monthlyOnDayAndHourAndMinute(dayNumber, 0, 0);

        if (dayNumber > 28) {
            CronTrigger trigger28 = TriggerBuilder.newTrigger()
                    .withIdentity("payment_trigger28_" + merchantServiceTemplate.getId(), "payment_triggers")
                    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 28 2 ? *"))
                    .endAt(merchantServiceTemplate.getEndScheduleDate())
                    .build();
            triggers.add(trigger28);

            if (dayNumber == 31) {
                CronTrigger trigger30 = TriggerBuilder.newTrigger()
                        .withIdentity("payment_trigger30_" + merchantServiceTemplate.getId(), "payment_triggers")
                        .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 30 4,6,9,11 ? *"))
                        .endAt(merchantServiceTemplate.getEndScheduleDate())
                        .build();
                triggers.add(trigger30);
            }
        }

        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("payment_triggerAll_" + merchantServiceTemplate.getId(), "payment_triggers")
                .withSchedule(interval)
                .endAt(merchantServiceTemplate.getEndScheduleDate())
                .build();
        triggers.add(trigger);


        scheduler.scheduleJob(job, triggers, false);
Nikita Koksharov
  • 10,283
  • 1
  • 62
  • 71
0

As said before, you have to create multiple CronExpressions for each case of days in month, and a trigger for each one, and then add all the triggers to your required Job.

This is my version:

CronExpressions creation:

public static List<CronExpression> getCronExpressionList(int seconds, int minutes,
            int hours, int dayInMonth, Month month,
            DayOfWeek dayOfWeek) {
    final String monthsWith30Days = Month.APR + "," + Month.JUN + ","
                    + Month.SEP + "," + Month.NOV;
    List<CronExpression> crons = new LinkedList<CronExpression>();

    String timeString = String.format(("%s %s %s "), seconds, minutes,
                    hours, 0, 0, 0);
    String dateString = "%s %s %s";
    String cron = null;

    cron = timeString + String.format(dateString, dayInMonth, "*", "?");
    crons.add(new CronExpression(cron));
    if (dayInMonth > 28) {
        String febCron = timeString + getFebruarLastDayDateString(dateString);
        crons.add(new CronExpression(febCron));
        if (dayInMonth == 31) {
            String monthsWithThirtyDaysCron = timeString + String.format(dateString,
                    "L", monthsWith30Days, "?");
            crons.add(new CronExpression(monthsWithThirtyDaysCron));
        }
    }
    return crons;
}

private static String getFebruarLastDayDateString(String initialCron) 
               throws ParseException {
    return String.format(initialCron, "L", Month.FEB, "?");
}

Pay attention that I used "L" in the February cron, because otherwise you would have a bug in a leap year.

Trigger creation:

        Set<CronTrigger> triggers = new HashSet<>();

        int i = 1;
        for (CronExpression cronEx : cronsList) {
            CronTrigger trigger = newTrigger()
                    .withIdentity("trigger" + i, groupName)
                    .withSchedule(cronSchedule(cronEx))
                    .build();
                triggers.add(trigger);
                i++;
        }
yishaiz
  • 2,433
  • 4
  • 28
  • 49