Twidere-App-Android-Twitter.../twidere/src/main/java/org/mariotaku/twidere/model/CronExpression.java

454 lines
15 KiB
Java

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
* Single-file Cron expression parser
* <p>
* Supports POSIX standard syntax only
*/
public class CronExpression {
/**
* Equivalent to {@code 0 0 1 1 *}
*/
public static final CronExpression YEARLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
BasicField.one(FieldType.DAY_OF_MONTH),
BasicField.one(FieldType.MONTH),
AnyField.INSTANCE,
null,
});
public static final CronExpression ANNUALLY = YEARLY;
/**
* Equivalent to {@code 0 0 1 * *}
*/
public static final CronExpression MONTHLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
BasicField.one(FieldType.DAY_OF_MONTH),
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
/**
* Equivalent to {@code 0 0 * * 0}
*/
public static final CronExpression WEEKLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
AnyField.INSTANCE,
AnyField.INSTANCE,
BasicField.zero(FieldType.DAY_OF_WEEK),
null,
});
/**
* Equivalent to {@code 0 0 * * *}
*/
public static final CronExpression DAILY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
/**
* Equivalent to {@code 0 * * * *}
*/
public static final CronExpression HOURLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
private Field[] fields;
private CronExpression(@NonNull Field[] fields) {
if (fields.length < 5) throw new IllegalArgumentException("Fields count must >= 5");
this.fields = fields;
}
@NonNull
public static CronExpression valueOf(@NonNull String string) throws ParseException {
if (string.length() == 0) {
throw new ParseException("Cron expression is empty", -1);
}
if (string.charAt(0) == '@') {
// Parse predefined
final String substr = string.substring(1);
switch (substr) {
case "yearly":
return YEARLY;
case "annually":
return ANNUALLY;
case "monthly":
return MONTHLY;
case "weekly":
return WEEKLY;
case "daily":
return DAILY;
case "hourly":
return HOURLY;
}
throw new ParseException("Unknown pre-defined value " + substr, 1);
}
final String[] segments = split(string, ' ');
if (segments.length > 6) {
throw new ParseException("Unrecognized segments " + string, -1);
}
// Parse minute field
Field[] fields = new Field[6];
fields[0] = FieldType.MINUTE.parseField(segments[0]);
// Parse hour field
fields[1] = FieldType.HOUR_OF_DAY.parseField(segments[1]);
// Parse day-of-month field
fields[2] = FieldType.DAY_OF_MONTH.parseField(segments[2]);
// Parse month field
fields[3] = FieldType.MONTH.parseField(segments[3]);
// Parse day-of-week field
fields[4] = FieldType.DAY_OF_WEEK.parseField(segments[4]);
return new CronExpression(fields);
}
public boolean matches(Calendar cal) {
for (Field field : fields) {
if (field == null) continue;
if (!field.contains(cal)) return false;
}
return true;
}
public String toExpression() {
StringBuilder sb = new StringBuilder();
for (int i = 0, j = fields.length; i < j; i++) {
Field field = fields[i];
if (field == null) continue;
if (i != 0) {
sb.append(' ');
}
sb.append(field.toExpression());
}
return sb.toString();
}
interface Field {
boolean contains(Calendar cal);
String toExpression();
}
public enum FieldType {
MINUTE(Calendar.MINUTE, 0, new Range(0, 59), null),
HOUR_OF_DAY(Calendar.HOUR_OF_DAY, 0, new Range(0, 23), null),
DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 0, new Range(1, 31), null),
MONTH(Calendar.MONTH, 1, new Range(1, 12), new String[]{"JAN", "FEB", "MAR", "APR", "JUN", "JUL",
"AUG", "SEP", "OCT", "NOV", "DEC"}),
DAY_OF_WEEK(Calendar.DAY_OF_WEEK, -1, new Range(0, 6), new String[]{"SUN", "MON", "TUE", "WED", "THU",
"FRI", "SAT"}),
/* Used in nncron, not decided whether to implement */
YEAR(Calendar.YEAR, 0, new Range(1970, 2099), null);
final int calendarField;
final int calendarOffset;
final Range allowedRange;
final String[] textRepresentations;
FieldType(int calendarField, int calendarOffset, Range allowedRange,
@Nullable String[] textRepresentations) {
this.calendarField = calendarField;
this.calendarOffset = calendarOffset;
this.allowedRange = allowedRange;
this.textRepresentations = textRepresentations;
}
Field parseField(@NonNull String text) throws ParseException {
if ("*".equals(text)) return AnyField.INSTANCE;
return BasicField.parse(text, this);
}
}
enum AnyField implements Field {
INSTANCE;
@Override
public boolean contains(Calendar cal) {
return true;
}
@Override
public String toExpression() {
return "*";
}
}
/**
* POSIX-compliant CRON field
*/
final static class BasicField implements Field {
@NonNull
final Range[] ranges;
final int calendarField;
final int calendarOffset;
BasicField(@NonNull final Range[] ranges, int calendarField, int calendarOffset) {
this.ranges = ranges;
this.calendarField = calendarField;
this.calendarOffset = calendarOffset;
}
public static Field parse(String text, FieldType fieldType) throws ParseException {
final Range allowedRange = fieldType.allowedRange;
final String[] rangeStrings = split(text, ',');
final Range[] ranges = new Range[rangeStrings.length];
final String[] textRepresentations = fieldType.textRepresentations;
for (int i = 0, l = rangeStrings.length; i < l; i++) {
String rangeString = rangeStrings[i];
ranges[i] = Range.parse(rangeString, allowedRange, textRepresentations);
if (!allowedRange.contains(ranges[i])) {
throw new ParseException(ranges[i] + " out of range " + allowedRange, -1);
}
}
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
public static Field zero(FieldType fieldType) {
final Range[] ranges = {Range.single(0)};
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
public static Field one(FieldType fieldType) {
final Range[] ranges = {Range.single(1)};
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
@Override
public boolean contains(Calendar cal) {
final int cmp = cal.get(calendarField) + calendarOffset;
for (Range range : ranges) {
if (range.contains(cmp)) return true;
}
return false;
}
@Override
public String toExpression() {
StringBuilder sb = new StringBuilder();
for (int i = 0, j = ranges.length; i < j; i++) {
Range range = ranges[i];
if (i != 0) {
sb.append(',');
}
sb.append(range.toExpression());
}
return sb.toString();
}
}
final static class DayOfMonthField implements Field {
static Field parse(String text, @NonNull Range allowedRange,
@Nullable String[] textRepresentations) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(Calendar cal) {
throw new UnsupportedOperationException();
}
@Override
public String toExpression() {
throw new UnsupportedOperationException();
}
}
final static class DayOfWeekField implements Field {
public static Field parse(String text, @NonNull Range allowedRange,
@Nullable String[] textRepresentations) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(Calendar cal) {
throw new UnsupportedOperationException();
}
@Override
public String toExpression() {
throw new UnsupportedOperationException();
}
}
final static class Range {
final int start, endInclusive;
Range(int start, int endInclusive) {
if (endInclusive < start) {
throw new IllegalArgumentException("endInclusive < start");
}
this.start = start;
this.endInclusive = endInclusive;
}
public boolean contains(int num) {
return num >= start && num <= endInclusive;
}
public boolean contains(Range that) {
return this.start <= that.start && this.endInclusive >= that.endInclusive;
}
public int size() {
return endInclusive - start + 1;
}
public int valueAt(int index) {
if (index >= size()) {
throw new IndexOutOfBoundsException("Range index out of bounds");
}
return start + index;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Range range = (Range) o;
if (start != range.start) return false;
return endInclusive == range.endInclusive;
}
@Override
public int hashCode() {
int result = start;
result = 31 * result + endInclusive;
return result;
}
@Override
public String toString() {
return "Range{" +
"start=" + start +
", endInclusive=" + endInclusive +
'}';
}
public static Range single(int num) {
return new Range(num, num);
}
public static Range parse(String string, Range allowedRange,
@Nullable String[] textRepresentations) throws ParseException {
int dashIdx = string.indexOf('-');
if (dashIdx == -1) {
return single(parseNumber(string, allowedRange, textRepresentations));
}
final int start = parseNumber(string.substring(0, dashIdx), allowedRange,
textRepresentations);
final int endInclusive = parseNumber(string.substring(dashIdx + 1),
allowedRange, textRepresentations);
return new Range(start, endInclusive);
}
private static int parseNumber(String input, Range allowedRange,
@Nullable String[] textRepresentations) throws ParseException {
if (textRepresentations != null) {
int textRepresentationIndex = indexOf(textRepresentations, input);
if (textRepresentationIndex != -1) {
return allowedRange.valueAt(textRepresentationIndex);
}
}
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new ParseException(e.getMessage(), -1);
}
}
public String toExpression() {
if (endInclusive == start) return Integer.toString(start);
return start + "-" + endInclusive;
}
}
private static <T> int indexOf(@NonNull final T[] array, @NonNull final T objectToFind) {
int length = array.length;
for (int i = 0; i < length; i++) {
if (objectToFind.equals(array[i])) {
return i;
}
}
return -1;
}
private static String[] split(@NonNull final String str, final char separatorChar) {
// Performance tuned for 2.0 (JDK1.4)
final int len = str.length();
if (len == 0) {
return new String[0];
}
final List<String> list = new ArrayList<>();
int i = 0, start = 0;
boolean match = false;
while (i < len) {
if (str.charAt(i) == separatorChar) {
if (match) {
list.add(str.substring(start, i));
match = false;
}
start = ++i;
continue;
}
match = true;
i++;
}
if (match) {
list.add(str.substring(start, i));
}
return list.toArray(new String[list.size()]);
}
}