317 lines
12 KiB
Java
Executable file
317 lines
12 KiB
Java
Executable file
/*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2020 Ziver Koc
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
package zutil;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* This is a utility class that will generate timestamps from a Cron formatted String.
|
|
* <br>
|
|
* A cron string consists of 5 to 6 sections separated by a space:
|
|
* <ol>
|
|
* <li>Minute in Hour</li>
|
|
* <li>Hour in a Day</li>
|
|
* <li>Day of Month</li>
|
|
* <li>Month</li>
|
|
* <li>Day of Week</li>
|
|
* <li>Year</li>
|
|
* </ol>
|
|
* Each section is defined by a number or by special characters:
|
|
* <ul>
|
|
* <li>*: any value, wildcard</li>
|
|
* <li>,: separator for multiple expressions, e.g. 3,4,5</li>
|
|
* <li>-: defines a range, e.g. 3-6</li>
|
|
* <li>/: step values, e.g. */10 (every tenth)</li>
|
|
* <li>?: sets value as start time, when the cron was initialized. NOT SUPPORTED</li>
|
|
* </ul>
|
|
* <br>
|
|
* Examples (from Cron Format Specification):
|
|
* <ul>
|
|
* <li>"* * * * * *": Each minute</li>
|
|
* <li>"0 0 * * * *": Daily at midnight</li>
|
|
* <li>"* * * 1,2,3 * *": Each minute in January, February or March</li>
|
|
* </ul>
|
|
* <br>
|
|
* Note that this class will only calculate the next cron up to 50 years in the future ant not more.
|
|
*
|
|
* @see <a href="http://www.nncron.ru/help/EN/working/cron-format.htm">Cron Format Specification</a>
|
|
* @see <a href="https://crontab.guru/">Cron calculator</a>
|
|
* @see <a href="http://stackoverflow.com/a/322058/833746">Stackoverflow implementation reference</a>
|
|
*/
|
|
public class CronTimer implements Iterator<Long>, Iterable<Long>{
|
|
|
|
private TimeZone timeZone;
|
|
|
|
private int[] minutes;
|
|
private int[] hours;
|
|
private int[] dayOfMonths;
|
|
private int[] months;
|
|
private int[] dayOfWeeks;
|
|
private int[] years;
|
|
|
|
|
|
/**
|
|
* A Constructor that takes a String containing 5 (or 6 for extended) individual fields in Cron format
|
|
*/
|
|
public CronTimer(String cron) {
|
|
String[] arr = cron.split("\\s");
|
|
if (arr.length < 5 || arr.length > 6)
|
|
throw new IllegalArgumentException(
|
|
"String must contain between 5-6 fields, but got(" + arr.length + " fields): " + cron);
|
|
init(arr[0], arr[1], arr[2], arr[3], arr[4], (arr.length>5 ? arr[5]: "*"));
|
|
}
|
|
/**
|
|
* A Constructor that takes separate Strings for each field
|
|
*/
|
|
public CronTimer(String minute, String hour, String dayOfMonth, String monthOfYear, String dayOfWeek) {
|
|
this(minute, hour, dayOfMonth, monthOfYear, dayOfWeek, "*");
|
|
}
|
|
/**
|
|
* A Constructor that takes separate Strings for each field with an extended year field
|
|
*/
|
|
public CronTimer(String minute, String hour, String dayOfMonth, String monthOfYear, String dayOfWeek, String year) {
|
|
init(minute, hour, dayOfMonth, monthOfYear, dayOfWeek, year);
|
|
}
|
|
|
|
private void init(String minute, String hour, String dayOfMonth, String monthOfYear, String dayOfWeek, String year) {
|
|
minutes = ArrayUtil.toIntArray(getRange(minute, 0, 59));
|
|
hours = ArrayUtil.toIntArray(getRange(hour, 0, 23));
|
|
dayOfMonths = ArrayUtil.toIntArray(getRange(dayOfMonth, 1, 31));
|
|
months = ArrayUtil.toIntArray(getRange(monthOfYear, 1, 12));
|
|
dayOfWeeks = ArrayUtil.toIntArray(getRange(dayOfWeek, 1, 7));
|
|
years = ArrayUtil.toIntArray(getRange(year,
|
|
1970,
|
|
Calendar.getInstance().get(Calendar.YEAR)+30));
|
|
}
|
|
protected static List<Integer> getRange(String str, int from, int to) {
|
|
if (str == null || str.isEmpty())
|
|
return Collections.emptyList();
|
|
|
|
List<Integer> list = new LinkedList<>();
|
|
|
|
String[] commaArr = str.split(",");
|
|
if (commaArr.length > 1) {
|
|
for (String section : commaArr)
|
|
list.addAll(getRange(section, from, to));
|
|
}
|
|
else {
|
|
String[] divisionArr = str.split("/", 2);
|
|
if (divisionArr.length == 2) {
|
|
float divider = Integer.parseInt(divisionArr[1]);
|
|
for (Integer i : getRange(divisionArr[0], from, to)) {
|
|
if (i % divider == 0)
|
|
list.add(i);
|
|
}
|
|
}
|
|
else {
|
|
String[] rangeArr;
|
|
if (str.equals("*"))
|
|
rangeArr = new String[]{"" +from, "" +to};
|
|
else
|
|
rangeArr = str.split("-", 2);
|
|
if (rangeArr.length == 2) {
|
|
int rangeFrom = Integer.parseInt(rangeArr[0]);
|
|
int rangeTo = Integer.parseInt(rangeArr[1]);
|
|
if (from > rangeFrom || rangeTo > to)
|
|
throw new IllegalArgumentException("Invalid range " + rangeFrom + "-" + rangeTo + " must be between: " + from + "-" + to);
|
|
for (int i = rangeFrom; i <= rangeTo; ++i)
|
|
list.add(i);
|
|
} else {
|
|
int value = Integer.parseInt(str);
|
|
if (from > value || value > to)
|
|
throw new IllegalArgumentException("Valid values are between " + from + "-" + to + " but got: " + value);
|
|
list.add(value);
|
|
}
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the TimeZone that should be used by the cron algorithm
|
|
*/
|
|
public void setTimeZone(TimeZone timeZone) {
|
|
this.timeZone = timeZone;
|
|
}
|
|
|
|
|
|
@Override
|
|
public boolean hasNext() {
|
|
return true;
|
|
}
|
|
@Override
|
|
public Iterator<Long> iterator() {
|
|
return this;
|
|
}
|
|
@Override
|
|
public void remove() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
/**
|
|
* @return the next timestamp that triggers this cron timer from now,
|
|
* -1 if there is no more future trigger points.
|
|
*/
|
|
@Override
|
|
public Long next() {
|
|
return next(System.currentTimeMillis());
|
|
}
|
|
/**
|
|
* @param fromTimestamp the timestamp offset to check the trigger from. Should be in MS
|
|
* @return the next timestamp that triggers this cron timer from the given timestamp,
|
|
* -1 if there is no more future trigger points.
|
|
*/
|
|
public Long next(long fromTimestamp) {
|
|
Calendar cal = getCalendar(fromTimestamp);
|
|
cal.set(Calendar.MILLISECOND, 0);
|
|
cal.set(Calendar.SECOND, 0);
|
|
cal.add(Calendar.MINUTE, 1); // skipp current time
|
|
|
|
while (true) {
|
|
int index;
|
|
|
|
int year = cal.get(Calendar.YEAR);
|
|
index = Arrays.binarySearch(years, year);
|
|
if (index < 0) { // index not found in array
|
|
if (Math.abs(index) > years.length)
|
|
return -1L; // We have reach the limit no more years left
|
|
else
|
|
cal.set(Calendar.YEAR, years[Math.abs(index + 1)]);
|
|
cal.set(Calendar.MONTH, months[0] - 1);
|
|
cal.set(Calendar.DAY_OF_MONTH, dayOfMonths[0]);
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[0]);
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
continue;
|
|
}
|
|
|
|
int month = cal.get(Calendar.MONTH); // month ids are between 0-11 :(
|
|
index = Arrays.binarySearch(months, month + 1);
|
|
if (index < 0) { // index not found in array
|
|
if (Math.abs(index) > months.length) {
|
|
cal.set(Calendar.MONTH, months[0] - 1);
|
|
cal.add(Calendar.YEAR, 1);
|
|
} else
|
|
cal.set(Calendar.MONTH, months[Math.abs(index + 1)] - 1);
|
|
cal.set(Calendar.DAY_OF_MONTH, dayOfMonths[0]);
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[0]);
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
continue;
|
|
}
|
|
|
|
int day = cal.get(Calendar.DAY_OF_MONTH);
|
|
index = Arrays.binarySearch(dayOfMonths, day);
|
|
if (index < 0) { // index not found in array
|
|
if (Math.abs(index) > dayOfMonths.length ||
|
|
// check if month have that many days in it
|
|
dayOfMonths[Math.abs(index + 1)] > cal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
|
|
cal.set(Calendar.DAY_OF_MONTH, dayOfMonths[0]);
|
|
cal.add(Calendar.MONTH, 1);
|
|
} else
|
|
cal.set(Calendar.DAY_OF_MONTH, dayOfMonths[Math.abs(index + 1)]);
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[0]);
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
continue;
|
|
}
|
|
|
|
// Calendar DAY_OF_WEEK is weird so we need to convert it to a logical number
|
|
int dayOfWeek = getDayOfWeekID(cal.get(Calendar.DAY_OF_WEEK));
|
|
index = Arrays.binarySearch(dayOfWeeks, dayOfWeek);
|
|
if (index < 0) { // index not found in array
|
|
if (Math.abs(index) > dayOfWeeks.length) {
|
|
cal.set(Calendar.DAY_OF_WEEK, getDayOfWeekEnum(dayOfWeeks[0]));
|
|
cal.add(Calendar.WEEK_OF_YEAR, 1);
|
|
} else
|
|
cal.set(Calendar.DAY_OF_WEEK, getDayOfWeekEnum(dayOfWeeks[Math.abs(index + 1)]));
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[0]);
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
continue;
|
|
}
|
|
|
|
int hour = cal.get(Calendar.HOUR_OF_DAY);
|
|
index = Arrays.binarySearch(hours, hour);
|
|
if (index < 0) { // not found in array
|
|
if (Math.abs(index) > hours.length) {
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[0]);
|
|
cal.add(Calendar.DAY_OF_MONTH, 1);
|
|
} else
|
|
cal.set(Calendar.HOUR_OF_DAY, hours[Math.abs(index + 1)]);
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
continue;
|
|
}
|
|
|
|
int minute = cal.get(Calendar.MINUTE);
|
|
index = Arrays.binarySearch(minutes, minute);
|
|
if (index < 0) { // not found in array
|
|
if (Math.abs(index) > minutes.length) {
|
|
cal.set(Calendar.MINUTE, minutes[0]);
|
|
cal.add(Calendar.HOUR_OF_DAY, 1);
|
|
} else
|
|
cal.set(Calendar.MINUTE, minutes[Math.abs(index + 1)]);
|
|
continue;
|
|
}
|
|
|
|
// If we reach the end that means that we got match for all parameters
|
|
break;
|
|
}
|
|
return cal.getTimeInMillis();
|
|
}
|
|
|
|
protected Calendar getCalendar(long timestamp) {
|
|
Calendar cal = Calendar.getInstance();
|
|
if (timeZone != null)
|
|
cal.setTimeZone(timeZone);
|
|
cal.setTimeInMillis(timestamp);
|
|
return cal;
|
|
}
|
|
|
|
/**
|
|
* Converts Calendar DAY_OF_WEEK enum to id starting from 1 (Monday) to 7 (Sunday)
|
|
*/
|
|
private int getDayOfWeekID(int calDayOfWeek) {
|
|
switch (calDayOfWeek) {
|
|
case Calendar.MONDAY: return 1;
|
|
case Calendar.TUESDAY: return 2;
|
|
case Calendar.WEDNESDAY: return 3;
|
|
case Calendar.THURSDAY: return 4;
|
|
case Calendar.FRIDAY: return 5;
|
|
case Calendar.SATURDAY: return 6;
|
|
case Calendar.SUNDAY: return 7;
|
|
}
|
|
return -1;
|
|
}
|
|
private int getDayOfWeekEnum(int dayId) {
|
|
switch (dayId) {
|
|
case 1: return Calendar.MONDAY;
|
|
case 2: return Calendar.TUESDAY;
|
|
case 3: return Calendar.WEDNESDAY;
|
|
case 4: return Calendar.THURSDAY;
|
|
case 5: return Calendar.FRIDAY;
|
|
case 6: return Calendar.SATURDAY;
|
|
case 7: return Calendar.SUNDAY;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|