com.espertech.esper.schedule.ScheduleComputeHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of esper Show documentation
Show all versions of esper Show documentation
Complex event processing and event series analysis component
/**************************************************************************************
* Copyright (C) 2006-2015 EsperTech Inc. All rights reserved. *
* http://www.espertech.com/esper *
* http://www.espertech.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the GPL license *
* a copy of which has been included with this distribution in the license.txt file. *
**************************************************************************************/
package com.espertech.esper.schedule;
import com.espertech.esper.type.CronOperatorEnum;
import com.espertech.esper.type.CronParameter;
import com.espertech.esper.type.ScheduleUnit;
import com.espertech.esper.util.ExecutionPathDebugLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.SortedSet;
import java.util.TimeZone;
/**
* For a crontab-like schedule, this class computes the next occurance given a start time and a specification of
* what the schedule looks like.
* The resolution at which this works is at the second level. The next occurance
* is always at least 1 second ahead.
* The class implements an algorithm that starts at the highest precision (seconds) and
* continues to the lowest precicion (month). For each precision level the
* algorithm looks at the list of valid values and finds a value for each that is equal to or greater then
* the valid values supplied. If no equal or
* greater value was supplied, it will reset all higher precision elements to its minimum value.
*/
public final class ScheduleComputeHelper
{
private static final Log log = LogFactory.getLog(ScheduleComputeHelper.class);
private static int[] DAY_OF_WEEK_ARRAY = new int[] {Calendar.SUNDAY,
Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
Calendar.FRIDAY, Calendar.SATURDAY};
/**
* Minimum time to next occurance.
*/
private static final int MIN_OFFSET_MSEC = 1000;
/**
* Computes the next lowest date in milliseconds based on a specification and the
* from-time passed in.
* @param spec defines the schedule
* @param afterTimeInMillis defines the start time
* @return a long date millisecond value for the next schedule occurance matching the spec
*/
public static long computeNextOccurance(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone)
{
if ((ExecutionPathDebugLog.isDebugEnabled) && (log.isDebugEnabled()))
{
log.debug(".computeNextOccurance Computing next occurance, afterTimeInMillis=" + (new Date(afterTimeInMillis)) +
" as long=" + afterTimeInMillis +
" spec=" + spec);
}
// Add the minimum resolution to the start time to ensure we don't get the same exact time
if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS))
{
afterTimeInMillis += MIN_OFFSET_MSEC;
}
else
{
afterTimeInMillis += 60 * MIN_OFFSET_MSEC;
}
return compute(spec, afterTimeInMillis, timeZone);
}
/**
* Computes the next lowest date in milliseconds based on a specification and the
* from-time passed in and returns the delta from the current time.
* @param spec defines the schedule
* @param afterTimeInMillis defines the start time
* @return a long millisecond value representing the delta between current time and the next schedule occurance matching the spec
*/
public static long computeDeltaNextOccurance(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone)
{
return computeNextOccurance(spec, afterTimeInMillis, timeZone) - afterTimeInMillis;
}
private static long compute(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone)
{
while (true)
{
Calendar after;
if (spec.getOptionalTimeZone() != null) {
after = Calendar.getInstance(TimeZone.getTimeZone(spec.getOptionalTimeZone()));
}
else {
after = Calendar.getInstance(timeZone);
}
after.setTimeInMillis(afterTimeInMillis);
ScheduleCalendar result = new ScheduleCalendar();
result.setMilliseconds(after.get(Calendar.MILLISECOND));
SortedSet minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
SortedSet hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);
SortedSet monthsSet = spec.getUnitValues().get(ScheduleUnit.MONTHS);
SortedSet secondsSet = null;
boolean isSecondsSpecified = false;
if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS))
{
isSecondsSpecified = true;
secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
}
if (isSecondsSpecified)
{
result.setSecond(nextValue(secondsSet, after.get(Calendar.SECOND)));
if (result.getSecond() == -1)
{
result.setSecond(nextValue(secondsSet, 0));
after.add(Calendar.MINUTE, 1);
}
}
result.setMinute(nextValue(minutesSet, after.get(Calendar.MINUTE)));
if (result.getMinute() != after.get(Calendar.MINUTE))
{
result.setSecond(nextValue(secondsSet, 0));
}
if (result.getMinute() == -1)
{
result.setMinute(nextValue(minutesSet, 0));
after.add(Calendar.HOUR_OF_DAY, 1);
}
result.setHour(nextValue(hoursSet, after.get(Calendar.HOUR_OF_DAY)));
if (result.getHour() != after.get(Calendar.HOUR_OF_DAY))
{
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
}
if (result.getHour() == -1)
{
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
}
// This call may change second, minute and/or hour parameters
// They may be reset to minimum values if the day rolled
result.setDayOfMonth(determineDayOfMonth(spec, after, result));
boolean dayMatchRealDate = false;
while (!dayMatchRealDate)
{
if (checkDayValidInMonth(timeZone, result.getDayOfMonth(), after.get(Calendar.MONTH), after.get(Calendar.YEAR)))
{
dayMatchRealDate = true;
}
else
{
after.add(Calendar.MONTH, 1);
}
}
int currentMonth = after.get(Calendar.MONTH) + 1;
result.setMonth(nextValue(monthsSet, currentMonth));
if (result.getMonth() != currentMonth)
{
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
result.setDayOfMonth(determineDayOfMonth(spec, after, result));
}
if (result.getMonth() == -1)
{
result.setMonth(nextValue(monthsSet, 0));
after.add(Calendar.YEAR, 1);
}
// Perform a last valid date check, if failing, try to compute a new date based on this altered after date
int year = after.get(Calendar.YEAR);
if (!(checkDayValidInMonth(timeZone, result.getDayOfMonth(), result.getMonth() - 1, year)))
{
afterTimeInMillis = after.getTimeInMillis();
continue;
}
return getTime(result, after.get(Calendar.YEAR), spec.getOptionalTimeZone(), timeZone);
}
}
/*
* Determine the next valid day of month based on the given specification of valid days in month and
* valid days in week. If both days in week and days in month are supplied, the days are OR-ed.
*/
private static int determineDayOfMonth(ScheduleSpec spec,
Calendar after,
ScheduleCalendar result)
{
SortedSet daysOfMonthSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_MONTH);
SortedSet daysOfWeekSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_WEEK);
SortedSet secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
SortedSet minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
SortedSet hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);
int dayOfMonth;
// If days of week is a wildcard, just go by days of month
if (spec.getOptionalDayOfMonthOperator() != null || spec.getOptionalDayOfWeekOperator() != null) {
boolean isWeek = false;
CronParameter op = spec.getOptionalDayOfMonthOperator();
if (spec.getOptionalDayOfMonthOperator() == null) {
op = spec.getOptionalDayOfWeekOperator();
isWeek = true;
}
// may return the current day or a future day in the same month,
// and may advance the "after" date to the next month
int currentYYMMDD = getTimeYYYYMMDD(after);
increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
int rolledYYMMDD = getTimeYYYYMMDD(after);
// if rolled then reset time portion
if (rolledYYMMDD > currentYYMMDD) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
return after.get(Calendar.DAY_OF_MONTH);
}
// rolling backwards is not allowed
else if (rolledYYMMDD < currentYYMMDD) {
throw new IllegalStateException("Failed to evaluate special date op, rolled date less then current date");
}
else {
Calendar work = (Calendar) after.clone();
work.set(Calendar.SECOND, result.getSecond());
work.set(Calendar.MINUTE, result.getMinute());
work.set(Calendar.HOUR_OF_DAY, result.getHour());
if (!work.after(after)) { // new date is not after current date, so bump
after.add(Calendar.DAY_OF_MONTH, 1);
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
}
return after.get(Calendar.DAY_OF_MONTH);
}
}
else if (daysOfWeekSet == null)
{
dayOfMonth = nextValue(daysOfMonthSet, after.get(Calendar.DAY_OF_MONTH));
if (dayOfMonth != after.get(Calendar.DAY_OF_MONTH))
{
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
}
if (dayOfMonth == -1)
{
dayOfMonth = nextValue(daysOfMonthSet, 0);
after.add(Calendar.MONTH, 1);
}
}
// If days of weeks is not a wildcard and days of month is a wildcard, go by days of week only
else if (daysOfMonthSet == null)
{
// Loop to find the next day of month that works for the specified day of week values
while(true)
{
dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;
// If the day matches neither the day of month nor the day of week
if (!daysOfWeekSet.contains(dayOfWeek))
{
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
}
else
{
break;
}
}
}
// Both days of weeks and days of month are not a wildcard
else
{
// Loop to find the next day of month that works for either day of month OR day of week
while(true)
{
dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;
// If the day matches neither the day of month nor the day of week
if ((!daysOfWeekSet.contains(dayOfWeek)) &&
(!daysOfMonthSet.contains(dayOfMonth)))
{
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
}
else
{
break;
}
}
}
return dayOfMonth;
}
private static long getTime(ScheduleCalendar result, int year, String optionalTimeZone, TimeZone timeZone)
{
Calendar calendar;
if (optionalTimeZone != null) {
calendar = Calendar.getInstance(TimeZone.getTimeZone(optionalTimeZone));
}
else {
calendar = Calendar.getInstance(timeZone);
}
calendar.set(year, result.getMonth() - 1, result.getDayOfMonth(), result.getHour(), result.getMinute(), result.getSecond());
calendar.set(Calendar.MILLISECOND, result.getMilliseconds());
return calendar.getTimeInMillis();
}
/*
* Check if this is a valid date.
*/
private static boolean checkDayValidInMonth(TimeZone timeZone, int day, int month, int year)
{
try
{
Calendar calendar = Calendar.getInstance(timeZone);
calendar.setLenient(false);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.getTime();
}
catch (IllegalArgumentException e)
{
return false;
}
return true;
}
/*
* Determine if in the supplied valueSet there is a value after the given start value.
* Return -1 to indicate that there is no value after the given startValue.
* If the valueSet passed is null it is treated as a wildcard and the same startValue is returned
*/
private static int nextValue(SortedSet valueSet, int startValue)
{
if (valueSet == null)
{
return startValue;
}
if (valueSet.contains(startValue))
{
return startValue;
}
SortedSet tailSet = valueSet.tailSet(startValue + 1);
if (tailSet.isEmpty())
{
return -1;
}
else
{
return tailSet.first();
}
}
private static int getTimeYYYYMMDD(Calendar calendar) {
return 10000*calendar.get(Calendar.YEAR) + (calendar.get(Calendar.MONTH) + 1) * 100 + calendar.get(Calendar.DAY_OF_MONTH);
}
private static void increaseAfterDayOfMonthSpecialOp(CronOperatorEnum operator, Integer day, Integer month, boolean week, Calendar after) {
DateChecker checker;
if (operator == CronOperatorEnum.LASTDAY) {
if (!week) {
checker = new DateCheckerLastDayOfMonth(day, month);
}
else {
if (day == null) {
checker = new DateCheckerLastDayOfWeek(month);
}
else {
checker = new DateCheckerLastSpecificDayWeek(day, month);
}
}
}
else if (operator == CronOperatorEnum.LASTWEEKDAY) {
checker = new DateCheckerLastWeekday(day, month);
}
else {
checker = new DateCheckerMonthWeekday(day, month);
}
int dayCount = 0;
while(!checker.fits(after)) {
after.add(Calendar.DAY_OF_MONTH, 1);
dayCount++;
if (dayCount > 10000) {
throw new IllegalArgumentException("Invalid crontab expression: failed to find match day");
}
}
}
private interface DateChecker {
public boolean fits(Calendar cal);
}
private static class DateCheckerLastSpecificDayWeek implements DateChecker {
private final int dayCode;
private final Integer month;
private DateCheckerLastSpecificDayWeek(int day, Integer month) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
// e.g. 31=Sun,30=Sat,29=Fri,28=Thu,27=Wed,26=Tue,25=Mon
// e.g. 31-7 = 24
return cal.get(Calendar.DAY_OF_MONTH) > cal.getActualMaximum(Calendar.DAY_OF_MONTH) - 7;
}
}
private static class DateCheckerLastDayOfMonth implements DateChecker {
private final Integer dayCode;
private final Integer month;
private DateCheckerLastDayOfMonth(Integer day, Integer month) {
if (day != null) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
}
else {
dayCode = null;
}
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
return (cal.get(Calendar.DAY_OF_MONTH) == cal.getActualMaximum(Calendar.DAY_OF_MONTH));
}
}
private static class DateCheckerLastDayOfWeek implements DateChecker {
private final Integer month;
private DateCheckerLastDayOfWeek(Integer month) {
this.month = month;
}
public boolean fits(Calendar cal) {
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
return (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY);
}
}
private static class DateCheckerLastWeekday implements DateChecker {
private final Integer dayCode;
private final Integer month;
private DateCheckerLastWeekday(Integer day, Integer month) {
if (day != null) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
}
else {
dayCode = null;
}
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
if (!isWeekday(cal)) {
return false;
}
int day = cal.get(Calendar.DAY_OF_MONTH);
int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day == max) {
return true;
}
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
return day >= max-2 && dayOfWeek == Calendar.FRIDAY;
}
}
public static class DateCheckerMonthWeekday implements DateChecker {
private final Integer day;
private final Integer month;
private DateCheckerMonthWeekday(Integer day, Integer month) {
if (day != null) {
if (day < 1 || day > 31) {
throw new IllegalArgumentException("xx day of the month has to be a in range (1-31)");
}
}
this.day = day;
this.month = month;
}
public boolean fits(Calendar cal) {
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
if (!isWeekday(cal)) {
return false;
}
if (day == null) {
return true;
}
Calendar work = (Calendar) cal.clone();
int target = computeNearestWeekdayDay(day, work);
return cal.get(Calendar.DAY_OF_MONTH) == target;
}
private static int computeNearestWeekdayDay(int day, Calendar work) {
int max = work.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day <= max) {
work.set(Calendar.DAY_OF_MONTH, day);
}
else {
work.set(Calendar.DAY_OF_MONTH, max);
}
if (isWeekday(work)) {
return work.get(Calendar.DAY_OF_MONTH);
}
if (work.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
if (work.get(Calendar.DAY_OF_MONTH) > 1) {
work.add(Calendar.DAY_OF_MONTH, -1);
return work.get(Calendar.DAY_OF_MONTH);
}
else {
work.add(Calendar.DAY_OF_MONTH, 2);
return work.get(Calendar.DAY_OF_MONTH);
}
}
else {
// handle Sunday
if (max == work.get(Calendar.DAY_OF_MONTH)) {
work.add(Calendar.DAY_OF_MONTH, -2);
return work.get(Calendar.DAY_OF_MONTH);
}
else {
work.add(Calendar.DAY_OF_MONTH, 1);
return work.get(Calendar.DAY_OF_MONTH);
}
}
}
}
private static boolean isWeekday(Calendar cal) {
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
return !(dayOfWeek < Calendar.MONDAY || dayOfWeek > Calendar.FRIDAY);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy