/*
 * Copyright 2002-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.scheduling.support;

import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Arrays;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

Representation of a crontab expression that can calculate the next time it matches.

CronExpression instances are created through parse(String); the next match is determined with next(T).

Author:Arjen Poutsma
See Also:
Since:5.3
/** * Representation of a * <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a> * that can calculate the next time it matches. * * <p>{@code CronExpression} instances are created through * {@link #parse(String)}; the next match is determined with * {@link #next(Temporal)}. * * @author Arjen Poutsma * @since 5.3 * @see CronTrigger */
public final class CronExpression { static final int MAX_ATTEMPTS = 366; private static final String[] MACROS = new String[] { "@yearly", "0 0 0 1 1 *", "@annually", "0 0 0 1 1 *", "@monthly", "0 0 0 1 * *", "@weekly", "0 0 0 * * 0", "@daily", "0 0 0 * * *", "@midnight", "0 0 0 * * *", "@hourly", "0 0 * * * *" }; private final CronField[] fields; private final String expression; private CronExpression( CronField seconds, CronField minutes, CronField hours, CronField daysOfMonth, CronField months, CronField daysOfWeek, String expression) { // to make sure we end up at 0 nanos, we add an extra field this.fields = new CronField[]{CronField.zeroNanos(), seconds, minutes, hours, daysOfMonth, months, daysOfWeek}; this.expression = expression; }
Parse the given crontab expression string into a CronExpression. The string has six single space-separated time and date fields:
┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *

The following rules apply:

  • A field may be an asterisk (*), which always stands for "first-last". For the "day of the month" or "day of the week" fields, a question mark (?) may be used instead of an asterisk.
  • Ranges of numbers are expressed by two numbers separated with a hyphen (-). The specified range is inclusive.
  • Following a range (or *) with /n specifies the interval of the number's value through the range.
  • English names can also be used for the "month" and "day of week" fields. Use the first three letters of the particular day or month (case does not matter).
  • The "day of month" and "day of week" fields can contain a L-character, which stands for "last", and has a different meaning in each field:
    • In the "day of month" field, L stands for "the last day of the month". If followed by an negative offset (i.e. L-n), it means "nth-to-last day of the month". If followed by W (i.e. LW), it means "the last weekday of the month".
    • In the "day of week" field, L stands for "the last day of the week". If prefixed by a number or three-letter name (i.e. dL or DDDL), it means "the last day of week d (or DDD) in the month".
  • The "day of month" field can be nW, which stands for "the nearest weekday to day of the month n". If n falls on Saturday, this yields the Friday before it. If n falls on Sunday, this yields the Monday after, which also happens if n is 1 and falls on a Saturday (i.e. 1W stands for "the first weekday of the month").
  • The "day of week" field can be d#n (or DDD#n), which stands for "the n-th day of week d (or DDD) in the month".

Example expressions:

  • "0 0 * * * *" = the top of every hour of every day.
  • "*/10 * * * * *" = every ten seconds.
  • "0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
  • "0 0 6,19 * * *" = 6:00 AM and 7:00 PM every day.
  • "0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
  • "0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
  • "0 0 0 25 12 ?" = every Christmas Day at midnight
  • "0 0 0 L * *" = last day of the month at midnight
  • "0 0 0 L-3 * *" = third-to-last day of the month at midnight
  • "0 0 0 1W * *" = first weekday of the month at midnight
  • "0 0 0 LW * *" = last weekday of the month at midnight
  • "0 0 0 * * 5L" = last Friday of the month at midnight
  • "0 0 0 * * THUL" = last Thursday of the month at midnight
  • "0 0 0 ? * 5#2" = the second Friday in the month at midnight
  • "0 0 0 ? * MON#1" = the first Monday in the month at midnight

The following macros are also supported:

  • "@yearly" (or "@annually") to run un once a year, i.e. "0 0 0 1 1 *",
  • "@monthly" to run once a month, i.e. "0 0 0 1 * *",
  • "@weekly" to run once a week, i.e. "0 0 0 * * 0",
  • "@daily" (or "@midnight") to run once a day, i.e. "0 0 0 * * *",
  • "@hourly" to run once an hour, i.e. "0 0 * * * *".
Params:
  • expression – the expression string to parse
Throws:
Returns:the parsed CronExpression object
/** * Parse the given * <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a> * string into a {@code CronExpression}. * The string has six single space-separated time and date fields: * <pre> * &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; second (0-59) * &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; minute (0 - 59) * &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; hour (0 - 23) * &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the month (1 - 31) * &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; month (1 - 12) (or JAN-DEC) * &#9474; &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the week (0 - 7) * &#9474; &#9474; &#9474; &#9474; &#9474; &#9474; (0 or 7 is Sunday, or MON-SUN) * &#9474; &#9474; &#9474; &#9474; &#9474; &#9474; * &#42; &#42; &#42; &#42; &#42; &#42; * </pre> * * <p>The following rules apply: * <ul> * <li> * A field may be an asterisk ({@code *}), which always stands for * "first-last". For the "day of the month" or "day of the week" fields, a * question mark ({@code ?}) may be used instead of an asterisk. * </li> * <li> * Ranges of numbers are expressed by two numbers separated with a hyphen * ({@code -}). The specified range is inclusive. * </li> * <li>Following a range (or {@code *}) with {@code /n} specifies * the interval of the number's value through the range. * </li> * <li> * English names can also be used for the "month" and "day of week" fields. * Use the first three letters of the particular day or month (case does not * matter). * </li> * <li> * The "day of month" and "day of week" fields can contain a * {@code L}-character, which stands for "last", and has a different meaning * in each field: * <ul> * <li> * In the "day of month" field, {@code L} stands for "the last day of the * month". If followed by an negative offset (i.e. {@code L-n}), it means * "{@code n}th-to-last day of the month". If followed by {@code W} (i.e. * {@code LW}), it means "the last weekday of the month". * </li> * <li> * In the "day of week" field, {@code L} stands for "the last day of the * week". * If prefixed by a number or three-letter name (i.e. {@code dL} or * {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD}) * in the month". * </li> * </ul> * </li> * <li> * The "day of month" field can be {@code nW}, which stands for "the nearest * weekday to day of the month {@code n}". * If {@code n} falls on Saturday, this yields the Friday before it. * If {@code n} falls on Sunday, this yields the Monday after, * which also happens if {@code n} is {@code 1} and falls on a Saturday * (i.e. {@code 1W} stands for "the first weekday of the month"). * </li> * <li> * The "day of week" field can be {@code d#n} (or {@code DDD#n}), which * stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in * the month". * </li> * </ul> * * <p>Example expressions: * <ul> * <li>{@code "0 0 * * * *"} = the top of every hour of every day.</li> * <li><code>"*&#47;10 * * * * *"</code> = every ten seconds.</li> * <li>{@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.</li> * <li>{@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.</li> * <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li> * <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li> * <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li> * <li>{@code "0 0 0 L * *"} = last day of the month at midnight</li> * <li>{@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight</li> * <li>{@code "0 0 0 1W * *"} = first weekday of the month at midnight</li> * <li>{@code "0 0 0 LW * *"} = last weekday of the month at midnight</li> * <li>{@code "0 0 0 * * 5L"} = last Friday of the month at midnight</li> * <li>{@code "0 0 0 * * THUL"} = last Thursday of the month at midnight</li> * <li>{@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight</li> * <li>{@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight</li> * </ul> * * <p>The following macros are also supported: * <ul> * <li>{@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},</li> * <li>{@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},</li> * <li>{@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},</li> * <li>{@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},</li> * <li>{@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.</li> * </ul> * * @param expression the expression string to parse * @return the parsed {@code CronExpression} object * @throws IllegalArgumentException in the expression does not conform to * the cron format */
public static CronExpression parse(String expression) { Assert.hasLength(expression, "Expression string must not be empty"); expression = resolveMacros(expression); String[] fields = StringUtils.tokenizeToStringArray(expression, " "); if (fields.length != 6) { throw new IllegalArgumentException(String.format( "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); } try { CronField seconds = CronField.parseSeconds(fields[0]); CronField minutes = CronField.parseMinutes(fields[1]); CronField hours = CronField.parseHours(fields[2]); CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]); CronField months = CronField.parseMonth(fields[4]); CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]); return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression); } catch (IllegalArgumentException ex) { String msg = ex.getMessage() + " in cron expression \"" + expression + "\""; throw new IllegalArgumentException(msg, ex); } } private static String resolveMacros(String expression) { expression = expression.trim(); for (int i = 0; i < MACROS.length; i = i + 2) { if (MACROS[i].equalsIgnoreCase(expression)) { return MACROS[i + 1]; } } return expression; }
Calculate the next Temporal that matches this expression.
Params:
  • temporal – the seed value
Type parameters:
  • <T> – the type of temporal
Returns:the next temporal that matches this expression, or null if no such temporal can be found
/** * Calculate the next {@link Temporal} that matches this expression. * @param temporal the seed value * @param <T> the type of temporal * @return the next temporal that matches this expression, or {@code null} * if no such temporal can be found */
@Nullable public <T extends Temporal & Comparable<? super T>> T next(T temporal) { return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1)); } @Nullable private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) { for (int i = 0; i < MAX_ATTEMPTS; i++) { T result = nextOrSameInternal(temporal); if (result == null || result.equals(temporal)) { return result; } temporal = result; } return null; } @Nullable private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) { for (CronField field : this.fields) { temporal = field.nextOrSame(temporal); if (temporal == null) { return null; } } return temporal; } @Override public int hashCode() { return Arrays.hashCode(this.fields); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof CronExpression) { CronExpression other = (CronExpression) o; return Arrays.equals(this.fields, other.fields); } else { return false; } }
Return the expression string used to create this CronExpression.
Returns:the expression string
/** * Return the expression string used to create this {@code CronExpression}. * @return the expression string */
@Override public String toString() { return this.expression; } }