From 2e874b085a207d3977b7abdaea88020bf3932f47 Mon Sep 17 00:00:00 2001
From: Nikita Koksharov <nkoksharov@redisson.pro>
Date: Fri, 14 Oct 2022 08:46:35 +0300
Subject: [PATCH] Fixed - RScheduledExecutorService cron triggers fire
 continuously for hours for some time zones (regression since 3.16.5)

---
 .../org/redisson/RedissonExecutorService.java |   11 +-
 .../java/org/redisson/api/CronSchedule.java   |    8 +-
 .../org/redisson/executor/CronExpression.java | 1877 ++++++++++++-----
 .../redisson/executor/CronExpressionEx.java   |   71 -
 .../redisson/executor/TasksRunnerService.java |   13 +-
 5 files changed, 1379 insertions(+), 601 deletions(-)
 delete mode 100644 redisson/src/main/java/org/redisson/executor/CronExpressionEx.java

diff --git a/redisson/src/main/java/org/redisson/RedissonExecutorService.java b/redisson/src/main/java/org/redisson/RedissonExecutorService.java
index b3f440e22..cc9a05433 100644
--- a/redisson/src/main/java/org/redisson/RedissonExecutorService.java
+++ b/redisson/src/main/java/org/redisson/RedissonExecutorService.java
@@ -40,8 +40,6 @@ import java.lang.ref.ReferenceQueue;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.time.Duration;
-import java.time.LocalDateTime;
-import java.time.ZonedDateTime;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -1218,12 +1216,11 @@ public class RedissonExecutorService implements RScheduledExecutorService {
         check(task);
         ClassBody classBody = getClassBody(task);
         byte[] state = encode(task);
-        ZonedDateTime currentDate = ZonedDateTime.of(LocalDateTime.now(), cronSchedule.getZoneId());
-        ZonedDateTime startDate = cronSchedule.getExpression().nextTimeAfter(currentDate);
+        Date startDate = cronSchedule.getExpression().getNextValidTimeAfter(new Date());
         if (startDate == null) {
             throw new IllegalArgumentException("Wrong cron expression! Unable to calculate start date");
         }
-        long startTime = startDate.toInstant().toEpochMilli();
+        long startTime = startDate.getTime();
 
         String taskId = id;
         ScheduledCronExpressionParameters params = new ScheduledCronExpressionParameters(taskId);
@@ -1232,14 +1229,14 @@ public class RedissonExecutorService implements RScheduledExecutorService {
         params.setLambdaBody(classBody.getLambda());
         params.setState(state);
         params.setStartTime(startTime);
-        params.setCronExpression(cronSchedule.getExpression().getExpr());
+        params.setCronExpression(cronSchedule.getExpression().getCronExpression());
         params.setTimezone(cronSchedule.getZoneId().toString());
         params.setExecutorId(executorId);
         RemotePromise<Void> result = (RemotePromise<Void>) asyncScheduledServiceAtFixed.schedule(params).toCompletableFuture();
         addListener(result);
         RedissonScheduledFuture<Void> f = new RedissonScheduledFuture<Void>(result, startTime) {
             public long getDelay(TimeUnit unit) {
-                return unit.convert(startDate.toInstant().toEpochMilli() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+                return unit.convert(startDate.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
             };
         };
         storeReference(f, result.getRequestId());
diff --git a/redisson/src/main/java/org/redisson/api/CronSchedule.java b/redisson/src/main/java/org/redisson/api/CronSchedule.java
index 4c05b0b48..f117ae7d4 100644
--- a/redisson/src/main/java/org/redisson/api/CronSchedule.java
+++ b/redisson/src/main/java/org/redisson/api/CronSchedule.java
@@ -16,9 +16,9 @@
 package org.redisson.api;
 
 import org.redisson.executor.CronExpression;
-import org.redisson.executor.CronExpressionEx;
 
 import java.time.ZoneId;
+import java.util.TimeZone;
 
 /**
  * Cron expression object used in {@link RScheduledExecutorService}.
@@ -49,7 +49,7 @@ public final class CronSchedule {
      *             wrapping a ParseException if the expression is invalid
      */
     public static CronSchedule of(String expression) {
-        return new CronSchedule(new CronExpressionEx(expression), ZoneId.systemDefault());
+        return of(expression, ZoneId.systemDefault());
     }
 
     /**
@@ -62,7 +62,9 @@ public final class CronSchedule {
      *             wrapping a ParseException if the expression is invalid
      */
     public static CronSchedule of(String expression, ZoneId zoneId) {
-        return new CronSchedule(new CronExpressionEx(expression), zoneId);
+        CronExpression ce = new CronExpression(expression);
+        ce.setTimeZone(TimeZone.getTimeZone(zoneId));
+        return new CronSchedule(ce, zoneId);
     }
 
     /**
diff --git a/redisson/src/main/java/org/redisson/executor/CronExpression.java b/redisson/src/main/java/org/redisson/executor/CronExpression.java
index 14ccfd37f..ef2de39cc 100644
--- a/redisson/src/main/java/org/redisson/executor/CronExpression.java
+++ b/redisson/src/main/java/org/redisson/executor/CronExpression.java
@@ -13,648 +13,1499 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/**
- * Copyright (C) 2012- Frode Carlsen.
+/*
+ * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
  *
- * 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
+ * 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
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *   http://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.
+ * 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.
  *
- * Note: rewritten to standard Java 8 DateTime by zemiak (c) 2016
  */
+
 package org.redisson.executor;
 
-import java.time.*;
-import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.TreeSet;
 
 /**
- * This provides cron support for java8 using java-time.
- * <P>
- *
- * Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time
- * such as: &quot;Each Monday-Friday at 08:00&quot; or &quot;Every last friday of the month at 01:30&quot;
- * <p>
- * A cron expressions consists of 5 or 6 mandatory fields (seconds may be omitted) separated by space. <br>
- * These are:
- *
- * <table cellspacing="8">
- * <tr>
- * <th align="left">Field</th>
- * <th align="left">&nbsp;</th>
- * <th align="left">Allowable values</th>
- * <th align="left">&nbsp;</th>
- * <th align="left">Special Characters</th>
- * </tr>
- * <tr>
- * <td align="left"><code>Seconds (may be omitted)</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>0-59</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * /</code></td>
- * </tr>
- * <tr>
- * <td align="left"><code>Minutes</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>0-59</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * /</code></td>
- * </tr>
- * <tr>
- * <td align="left"><code>Hours</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>0-23</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * /</code></td>
- * </tr>
- * <tr>
- * <td align="left"><code>Day of month</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>1-31</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * ? / L W</code></td>
- * </tr>
- * <tr>
- * <td align="left"><code>Month</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>1-12 or JAN-DEC (note: english abbreviations)</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * /</code></td>
- * </tr>
- * <tr>
- * <td align="left"><code>Day of week</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>1-7 or MON-SUN (note: english abbreviations)</code></td>
- * <td align="left">&nbsp;</th>
- * <td align="left"><code>, - * ? / L #</code></td>
- * </tr>
- * </table>
- *
- * <P>
- * '*' Can be used in all fields and means 'for all values'. E.g. &quot;*&quot; in minutes, means 'for all minutes'
- * <P>
- * '?' Can be used in Day-of-month and Day-of-week fields. Used to signify 'no special value'. It is used when one want
- * to specify something for one of those two fields, but not the other.
- * <P>
- * '-' Used to specify a time interval. E.g. &quot;10-12&quot; in Hours field means 'for hours 10, 11 and 12'
- * <P>
- * ',' Used to specify multiple values for a field. E.g. &quot;MON,WED,FRI&quot; in Day-of-week field means &quot;for
- * monday, wednesday and friday&quot;
- * <P>
- * '/' Used to specify increments. E.g. &quot;0/15&quot; in Seconds field means &quot;for seconds 0, 15, 30, ad
- * 45&quot;. And &quot;5/15&quot; in seconds field means &quot;for seconds 5, 20, 35, and 50&quot;. If '*' s specified
- * before '/' it is the same as saying it starts at 0. For every field there's a list of values that can be turned on or
- * off. For Seconds and Minutes these range from 0-59. For Hours from 0 to 23, For Day-of-month it's 1 to 31, For Months
- * 1 to 12. &quot;/&quot; character helsp turn some of these values back on. Thus &quot;7/6&quot; in Months field
- * specify just Month 7. It doesn't turn on every 6 month following, since cron fields never roll over
- * <P>
- * 'L' Can be used on Day-of-month and Day-of-week fields. It signifies last day of the set of allowed values. In
- * Day-of-month field it's the last day of the month (e.g.. 31 jan, 28 feb (29 in leap years), 31 march, etc.). In
- * Day-of-week field it's Sunday. If there's a prefix, this will be subtracted (5L in Day-of-month means 5 days before
- * last day of Month: 26 jan, 23 feb, etc.)
- * <P>
- * 'W' Can be specified in Day-of-Month field. It specifies closest weekday (monday-friday). Holidays are not accounted
- * for. &quot;15W&quot; in Day-of-Month field means 'closest weekday to 15 i in given month'. If the 15th is a Saturday,
- * it gives Friday. If 15th is a Sunday, the it gives following Monday.
- * <P>
- * '#' Can be used in Day-of-Week field. For example: &quot;5#3&quot; means 'third friday in month' (day 5 = friday, #3
- * - the third). If the day does not exist (e.g. &quot;5#5&quot; - 5th friday of month) and there aren't 5 fridays in
- * the month, then it won't match until the next month with 5 fridays.
- * <P>
- * <b>Case-sensitive</b> No fields are case-sensitive
- * <P>
- * <b>Dependencies between fields</b> Fields are always evaluated independently, but the expression doesn't match until
- * the constraints of each field are met. Overlap of intervals are not allowed. That is: for
- * Day-of-week field &quot;FRI-MON&quot; is invalid,but &quot;FRI-SUN,MON&quot; is valid
- *
+ * @author Sharada Jambula, James House
+ * @author Contributions from Mads Henderson
+ * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
  */
-@SuppressWarnings({"AvoidInlineConditionals", "MultipleVariableDeclarations", "InnerAssignment", "UnnecessaryParentheses"})
-public class CronExpression {
+@SuppressWarnings({"EmptyStatement", "InnerAssignment", "ConstantName",
+        "BooleanExpressionComplexity", "NestedIfDepth", "ParenPad",
+        "MethodLength", "WhitespaceAfter", "NoClone", "UnnecessaryParentheses", "AvoidInlineConditionals",
+        "EqualsAvoidNull", "OneStatementPerLine"})
+public final class CronExpression implements Serializable, Cloneable {
+
+    private static final long serialVersionUID = 12423409423L;
+
+    protected static final int SECOND = 0;
+    protected static final int MINUTE = 1;
+    protected static final int HOUR = 2;
+    protected static final int DAY_OF_MONTH = 3;
+    protected static final int MONTH = 4;
+    protected static final int DAY_OF_WEEK = 5;
+    protected static final int YEAR = 6;
+    protected static final int ALL_SPEC_INT = 99; // '*'
+    protected static final int NO_SPEC_INT = 98; // '?'
+    protected static final Integer ALL_SPEC = ALL_SPEC_INT;
+    protected static final Integer NO_SPEC = NO_SPEC_INT;
+
+    protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20);
+    protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60);
+    static {
+        monthMap.put("JAN", 0);
+        monthMap.put("FEB", 1);
+        monthMap.put("MAR", 2);
+        monthMap.put("APR", 3);
+        monthMap.put("MAY", 4);
+        monthMap.put("JUN", 5);
+        monthMap.put("JUL", 6);
+        monthMap.put("AUG", 7);
+        monthMap.put("SEP", 8);
+        monthMap.put("OCT", 9);
+        monthMap.put("NOV", 10);
+        monthMap.put("DEC", 11);
+
+        dayMap.put("SUN", 1);
+        dayMap.put("MON", 2);
+        dayMap.put("TUE", 3);
+        dayMap.put("WED", 4);
+        dayMap.put("THU", 5);
+        dayMap.put("FRI", 6);
+        dayMap.put("SAT", 7);
+    }
 
-    enum CronFieldType {
-        SECOND(0, 59, null) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getSecond();
-            }
+    private final String cronExpression;
+    private TimeZone timeZone = null;
+    protected transient TreeSet<Integer> seconds;
+    protected transient TreeSet<Integer> minutes;
+    protected transient TreeSet<Integer> hours;
+    protected transient TreeSet<Integer> daysOfMonth;
+    protected transient TreeSet<Integer> months;
+    protected transient TreeSet<Integer> daysOfWeek;
+    protected transient TreeSet<Integer> years;
+
+    protected transient boolean lastdayOfWeek = false;
+    protected transient int nthdayOfWeek = 0;
+    protected transient boolean lastdayOfMonth = false;
+    protected transient boolean nearestWeekday = false;
+    protected transient int lastdayOffset = 0;
+    protected transient boolean expressionParsed = false;
+
+    public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                return dateTime.withSecond(value).withNano(0);
-            }
+    /**
+     * Constructs a new <CODE>CronExpression</CODE> based on the specified
+     * parameter.
+     *
+     * @param cronExpression String representation of the cron expression the
+     *                       new object should represent
+     */
+    public CronExpression(String cronExpression) {
+        if (cronExpression == null) {
+            throw new IllegalArgumentException("cronExpression cannot be null");
+        }
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                return dateTime.plusMinutes(1).withSecond(0).withNano(0);
-            }
-        },
-        MINUTE(0, 59, null) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getMinute();
-            }
+        this.cronExpression = cronExpression.toUpperCase(Locale.US);
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                return dateTime.withMinute(value).withSecond(0).withNano(0);
-            }
+        try {
+            buildExpression(this.cronExpression);
+        } catch (ParseException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                return dateTime.plusHours(1).withMinute(0).withSecond(0).withNano(0);
-            }
-        },
-        HOUR(0, 23, null) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getHour();
-            }
+    /**
+     * Constructs a new {@code CronExpression} as a copy of an existing
+     * instance.
+     *
+     * @param expression
+     *            The existing cron expression to be copied
+     */
+    public CronExpression(CronExpression expression) {
+        /*
+         * We don't call the other constructor here since we need to swallow the
+         * ParseException. We also elide some of the sanity checking as it is
+         * not logically trippable.
+         */
+        this.cronExpression = expression.getCronExpression();
+        try {
+            buildExpression(cronExpression);
+        } catch (ParseException ex) {
+            throw new AssertionError();
+        }
+        if (expression.getTimeZone() != null) {
+            setTimeZone((TimeZone) expression.getTimeZone().clone());
+        }
+    }
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                return dateTime.withHour(value).withMinute(0).withSecond(0).withNano(0);
-            }
+    /**
+     * Indicates whether the given date satisfies the cron expression. Note that
+     * milliseconds are ignored, so two Dates falling on different milliseconds
+     * of the same second will always have the same result here.
+     *
+     * @param date the date to evaluate
+     * @return a boolean indicating whether the given date satisfies the cron
+     *         expression
+     */
+    public boolean isSatisfiedBy(Date date) {
+        Calendar testDateCal = Calendar.getInstance(getTimeZone());
+        testDateCal.setTime(date);
+        testDateCal.set(Calendar.MILLISECOND, 0);
+        Date originalDate = testDateCal.getTime();
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                return dateTime.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
-            }
-        },
-        DAY_OF_MONTH(1, 31, null) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getDayOfMonth();
-            }
+        testDateCal.add(Calendar.SECOND, -1);
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                return dateTime.withDayOfMonth(value).withHour(0).withMinute(0).withSecond(0).withNano(0);
-            }
+        Date timeAfter = getTimeAfter(testDateCal.getTime());
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                return dateTime.plusMonths(1).withDayOfMonth(0).withHour(0).withMinute(0).withSecond(0).withNano(0);
-            }
-        },
-        MONTH(1, 12,
-                Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getMonthValue();
-            }
+        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
+    }
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                return dateTime.withMonth(value).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
-            }
+    /**
+     * Returns the next date/time <I>after</I> the given date/time which
+     * satisfies the cron expression.
+     *
+     * @param date the date/time at which to begin the search for the next valid
+     *             date/time
+     * @return the next valid date/time
+     */
+    public Date getNextValidTimeAfter(Date date) {
+        return getTimeAfter(date);
+    }
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                return dateTime.plusYears(1).withMonth(1).withHour(0).withDayOfMonth(1).withMinute(0).withSecond(0).withNano(0);
-            }
-        },
-        DAY_OF_WEEK(1, 7, Arrays.asList("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")) {
-            @Override
-            int getValue(ZonedDateTime dateTime) {
-                return dateTime.getDayOfWeek().getValue();
-            }
+    /**
+     * Returns the next date/time <I>after</I> the given date/time which does
+     * <I>not</I> satisfy the expression
+     *
+     * @param date the date/time at which to begin the search for the next
+     *             invalid date/time
+     * @return the next valid date/time
+     */
+    public Date getNextInvalidTimeAfter(Date date) {
+        long difference = 1000;
 
-            @Override
-            ZonedDateTime setValue(ZonedDateTime dateTime, int value) {
-                throw new UnsupportedOperationException();
-            }
+        //move back to the nearest second so differences will be accurate
+        Calendar adjustCal = Calendar.getInstance(getTimeZone());
+        adjustCal.setTime(date);
+        adjustCal.set(Calendar.MILLISECOND, 0);
+        Date lastDate = adjustCal.getTime();
 
-            @Override
-            ZonedDateTime overflow(ZonedDateTime dateTime) {
-                throw new UnsupportedOperationException();
-            }
-        };
+        Date newDate;
 
-        final int from, to;
-        final List<String> names;
+        //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
 
-        CronFieldType(int from, int to, List<String> names) {
-            this.from = from;
-            this.to = to;
-            this.names = names;
-        }
+        //keep getting the next included time until it's farther than one second
+        // apart. At that point, lastDate is the last valid fire time. We return
+        // the second immediately following it.
+        while (difference == 1000) {
+            newDate = getTimeAfter(lastDate);
+            if(newDate == null)
+                break;
 
-        /**
-         * @param dateTime {@link ZonedDateTime} instance
-         * @return The field time or date value from {@code dateTime}
-         */
-        abstract int getValue(ZonedDateTime dateTime);
+            difference = newDate.getTime() - lastDate.getTime();
 
-        /**
-         * @param dateTime Initial {@link ZonedDateTime} instance to use
-         * @param value    to set for this field in {@code dateTime}
-         * @return {@link ZonedDateTime} with {@code value} set for this field and all smaller fields cleared
-         */
-        abstract ZonedDateTime setValue(ZonedDateTime dateTime, int value);
+            if (difference == 1000) {
+                lastDate = newDate;
+            }
+        }
 
-        /**
-         * Handle when this field overflows and the next higher field should be incremented
-         *
-         * @param dateTime Initial {@link ZonedDateTime} instance to use
-         * @return {@link ZonedDateTime} with the next greater field incremented and all smaller fields cleared
-         */
-        abstract ZonedDateTime overflow(ZonedDateTime dateTime);
+        return new Date(lastDate.getTime() + 1000);
     }
 
-    private final String expr;
-    private final SimpleField secondField;
-    private final SimpleField minuteField;
-    private final SimpleField hourField;
-    private final DayOfWeekField dayOfWeekField;
-    private final SimpleField monthField;
-    private final DayOfMonthField dayOfMonthField;
+    /**
+     * Returns the time zone for which this <code>CronExpression</code>
+     * will be resolved.
+     *
+     * @return time zone
+     */
+    public TimeZone getTimeZone() {
+        if (timeZone == null) {
+            timeZone = TimeZone.getDefault();
+        }
 
-    public CronExpression(final String expr) {
-        this(expr, true);
+        return timeZone;
     }
 
-    public CronExpression(final String expr, final boolean withSeconds) {
-        if (expr == null) {
-            throw new IllegalArgumentException("expr is null"); //$NON-NLS-1$
-        }
+    /**
+     * Sets the time zone for which  this <code>CronExpression</code>
+     * will be resolved.
+     *
+     * @param timeZone object
+     */
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    /**
+     * Returns the string representation of the <CODE>CronExpression</CODE>
+     *
+     * @return a string representation of the <CODE>CronExpression</CODE>
+     */
+    @Override
+    public String toString() {
+        return cronExpression;
+    }
 
-        this.expr = expr;
+    /**
+     * Indicates whether the specified cron expression can be parsed into a
+     * valid cron expression
+     *
+     * @param cronExpression the expression to evaluate
+     * @return a boolean indicating whether the given expression is a valid cron
+     *         expression
+     */
+    public static boolean isValidExpression(String cronExpression) {
 
-        final int expectedParts = withSeconds ? 6 : 5;
-        final String[] parts = expr.split("\\s+"); //$NON-NLS-1$
-        if (parts.length != expectedParts) {
-            throw new IllegalArgumentException(String.format("Invalid cron expression [%s], expected %s field, got %s", expr, expectedParts, parts.length));
+        try {
+            new CronExpression(cronExpression);
+        } catch (IllegalArgumentException pe) {
+            return false;
         }
 
-        int ix = withSeconds ? 1 : 0;
-        this.secondField = new SimpleField(CronFieldType.SECOND, withSeconds ? parts[0] : "0");
-        this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[ix++]);
-        this.hourField = new SimpleField(CronFieldType.HOUR, parts[ix++]);
-        this.dayOfMonthField = new DayOfMonthField(parts[ix++]);
-        this.monthField = new SimpleField(CronFieldType.MONTH, parts[ix++]);
-        this.dayOfWeekField = new DayOfWeekField(parts[ix++]);
+        return true;
     }
 
-    public static CronExpression create(final String expr) {
-        return new CronExpression(expr, true);
-    }
+    public static void validateExpression(String cronExpression) throws ParseException {
 
-    public static CronExpression createWithoutSeconds(final String expr) {
-        return new CronExpression(expr, false);
+        new CronExpression(cronExpression);
     }
 
-    public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime) {
-        // will search for the next time within the next 4 years. If there is no
-        // time matching, an InvalidArgumentException will be thrown (it is very
-        // likely that the cron expression is invalid, like the February 30th).
-        return nextTimeAfter(afterTime, afterTime.plusYears(4));
-    }
 
-    public LocalDateTime nextLocalDateTimeAfter(LocalDateTime dateTime) {
-        return nextTimeAfter(ZonedDateTime.of(dateTime, ZoneId.systemDefault())).toLocalDateTime();
-    }
+    ////////////////////////////////////////////////////////////////////////////
+    //
+    // Expression Parsing Functions
+    //
+    ////////////////////////////////////////////////////////////////////////////
 
-    public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, long durationInMillis) {
-        // will search for the next time within the next durationInMillis
-        // millisecond. Be aware that the duration is specified in millis,
-        // but in fact the limit is checked on a day-to-day basis.
-        return nextTimeAfter(afterTime, afterTime.plus(Duration.ofMillis(durationInMillis)));
-    }
+    protected void buildExpression(String expression) throws ParseException {
+        expressionParsed = true;
 
-    public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, ZonedDateTime dateTimeBarrier) {
-        ZonedDateTime[] nextDateTime = { afterTime.plusSeconds(1).withNano(0) };
+        try {
 
-        while (true) {
-            checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier);
-            if (!monthField.nextMatch(nextDateTime)) {
-                continue;
+            if (seconds == null) {
+                seconds = new TreeSet<Integer>();
             }
-            if (!findDay(nextDateTime, dateTimeBarrier)) {
-                continue;
+            if (minutes == null) {
+                minutes = new TreeSet<Integer>();
             }
-            if (!hourField.nextMatch(nextDateTime)) {
-                continue;
+            if (hours == null) {
+                hours = new TreeSet<Integer>();
             }
-            if (!minuteField.nextMatch(nextDateTime)) {
-                continue;
+            if (daysOfMonth == null) {
+                daysOfMonth = new TreeSet<Integer>();
             }
-            if (!secondField.nextMatch(nextDateTime)) {
-                continue;
+            if (months == null) {
+                months = new TreeSet<Integer>();
+            }
+            if (daysOfWeek == null) {
+                daysOfWeek = new TreeSet<Integer>();
+            }
+            if (years == null) {
+                years = new TreeSet<Integer>();
             }
 
-            checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier);
-            return nextDateTime[0];
-        }
-    }
+            int exprOn = SECOND;
 
-    /**
-     * Find the next match for the day field.
-     * <p>
-     * This is handled different than all other fields because there are two ways to describe the day and it is easier
-     * to handle them together in the same method.
-     *
-     * @param dateTime        Initial {@link ZonedDateTime} instance to start from
-     * @param dateTimeBarrier At which point stop searching for next execution time
-     * @return {@code true} if a match was found for this field or {@code false} if the field overflowed
-     * @see {@link SimpleField#nextMatch(ZonedDateTime[])}
-     */
-    private boolean findDay(ZonedDateTime[] dateTime, ZonedDateTime dateTimeBarrier) {
-        int month = dateTime[0].getMonthValue();
+            StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
+                    false);
+
+            while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
+                String expr = exprsTok.nextToken().trim();
 
-        while (!(dayOfMonthField.matches(dateTime[0].toLocalDate())
-                && dayOfWeekField.matches(dateTime[0].toLocalDate()))) {
-            dateTime[0] = dateTime[0].plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
-            if (dateTime[0].getMonthValue() != month) {
-                return false;
+                // throw an exception if L is used with other days of the month
+                if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
+                    throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
+                }
+                // throw an exception if L is used with other days of the week
+                if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1  && expr.contains(",")) {
+                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
+                }
+                if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) {
+                    throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
+                }
+
+                StringTokenizer vTok = new StringTokenizer(expr, ",");
+                while (vTok.hasMoreTokens()) {
+                    String v = vTok.nextToken();
+                    storeExpressionVals(0, v, exprOn);
+                }
+
+                exprOn++;
             }
-        }
-        return true;
-    }
 
-    private static void checkIfDateTimeBarrierIsReached(ZonedDateTime nextTime, ZonedDateTime dateTimeBarrier) {
-        if (nextTime.isAfter(dateTimeBarrier)) {
-            throw new IllegalArgumentException("No next execution time could be determined that is before the limit of " + dateTimeBarrier);
-        }
-    }
+            if (exprOn <= DAY_OF_WEEK) {
+                throw new ParseException("Unexpected end of expression.",
+                        expression.length());
+            }
 
-    public String getExpr() {
-        return expr;
-    }
+            if (exprOn <= YEAR) {
+                storeExpressionVals(0, "*", YEAR);
+            }
 
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "<" + expr + ">";
-    }
+            TreeSet<Integer> dow = getSet(DAY_OF_WEEK);
+            TreeSet<Integer> dom = getSet(DAY_OF_MONTH);
 
-    static class FieldPart implements Comparable<FieldPart> {
-        private int from = -1, to = -1, increment = -1;
-        private String modifier, incrementModifier;
+            // Copying the logic from the UnsupportedOperationException below
+            boolean dayOfMSpec = !dom.contains(NO_SPEC);
+            boolean dayOfWSpec = !dow.contains(NO_SPEC);
 
-        @Override
-        public int compareTo(FieldPart o) {
-            return Integer.compare(from, o.from);
+            if (!dayOfMSpec || dayOfWSpec) {
+                if (!dayOfWSpec || dayOfMSpec) {
+                    throw new ParseException(
+                            "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
+                }
+            }
+        } catch (ParseException pe) {
+            throw pe;
+        } catch (Exception e) {
+            throw new ParseException("Illegal cron expression format ("
+                    + e.toString() + ")", 0);
         }
     }
 
-    abstract static class BasicField {
-        private static final Pattern CRON_FIELD_REGEXP = Pattern
-                .compile("(?:                                             # start of group 1\n"
-                                + "   (?:(?<all>\\*)|(?<ignore>\\?)|(?<last>L))  # global flag (L, ?, *)\n"
-                                + " | (?<start>[0-9]{1,2}|[a-z]{3,3})              # or start number or symbol\n"
-                                + "      (?:                                        # start of group 2\n"
-                                + "         (?<mod>L|W)                             # modifier (L,W)\n"
-                                + "       | -(?<end>[0-9]{1,2}|[a-z]{3,3})        # or end nummer or symbol (in range)\n"
-                                + "      )?                                         # end of group 2\n"
-                                + ")                                              # end of group 1\n"
-                                + "(?:(?<incmod>/|\\#)(?<inc>[0-9]{1,7}))?        # increment and increment modifier (/ or \\#)\n",
-                        Pattern.CASE_INSENSITIVE | Pattern.COMMENTS);
+    protected int storeExpressionVals(int pos, String s, int type)
+            throws ParseException {
 
-        final CronFieldType fieldType;
-        final List<FieldPart> parts = new ArrayList<>();
+        int incr = 0;
+        int i = skipWhiteSpace(pos, s);
+        if (i >= s.length()) {
+            return i;
+        }
+        char c = s.charAt(i);
+        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) {
+            String sub = s.substring(i, i + 3);
+            int sval = -1;
+            int eval = -1;
+            if (type == MONTH) {
+                sval = getMonthNumber(sub) + 1;
+                if (sval <= 0) {
+                    throw new ParseException("Invalid Month value: '" + sub + "'", i);
+                }
+                if (s.length() > i + 3) {
+                    c = s.charAt(i + 3);
+                    if (c == '-') {
+                        i += 4;
+                        sub = s.substring(i, i + 3);
+                        eval = getMonthNumber(sub) + 1;
+                        if (eval <= 0) {
+                            throw new ParseException("Invalid Month value: '" + sub + "'", i);
+                        }
+                    }
+                }
+            } else if (type == DAY_OF_WEEK) {
+                sval = getDayOfWeekNumber(sub);
+                if (sval < 0) {
+                    throw new ParseException("Invalid Day-of-Week value: '"
+                            + sub + "'", i);
+                }
+                if (s.length() > i + 3) {
+                    c = s.charAt(i + 3);
+                    if (c == '-') {
+                        i += 4;
+                        sub = s.substring(i, i + 3);
+                        eval = getDayOfWeekNumber(sub);
+                        if (eval < 0) {
+                            throw new ParseException(
+                                    "Invalid Day-of-Week value: '" + sub
+                                            + "'", i);
+                        }
+                    } else if (c == '#') {
+                        try {
+                            i += 4;
+                            nthdayOfWeek = Integer.parseInt(s.substring(i));
+                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+                                throw new Exception();
+                            }
+                        } catch (Exception e) {
+                            throw new ParseException(
+                                    "A numeric value between 1 and 5 must follow the '#' option",
+                                    i);
+                        }
+                    } else if (c == 'L') {
+                        lastdayOfWeek = true;
+                        i++;
+                    }
+                }
 
-        private BasicField(CronFieldType fieldType, String fieldExpr) {
-            this.fieldType = fieldType;
-            parse(fieldExpr);
+            } else {
+                throw new ParseException(
+                        "Illegal characters for this position: '" + sub + "'",
+                        i);
+            }
+            if (eval != -1) {
+                incr = 1;
+            }
+            addToSet(sval, eval, incr, type);
+            return (i + 3);
         }
 
-        private void parse(String fieldExpr) { // NOSONAR
-            String[] rangeParts = fieldExpr.split(",");
-            for (String rangePart : rangeParts) {
-                Matcher m = CRON_FIELD_REGEXP.matcher(rangePart);
-                if (!m.matches()) {
-                    throw new IllegalArgumentException("Invalid cron field '" + rangePart + "' for field [" + fieldType + "]");
+        if (c == '?') {
+            i++;
+            if ((i + 1) < s.length()
+                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
+                throw new ParseException("Illegal character after '?': "
+                        + s.charAt(i), i);
+            }
+            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
+                throw new ParseException(
+                        "'?' can only be specfied for Day-of-Month or Day-of-Week.",
+                        i);
+            }
+            if (type == DAY_OF_WEEK && !lastdayOfMonth) {
+                int val = daysOfMonth.last();
+                if (val == NO_SPEC_INT) {
+                    throw new ParseException(
+                            "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.",
+                            i);
                 }
-                String startNummer = m.group("start");
-                String modifier = m.group("mod");
-                String sluttNummer = m.group("end");
-                String incrementModifier = m.group("incmod");
-                String increment = m.group("inc");
-
-                FieldPart part = new FieldPart();
-                part.increment = 999;
-                if (startNummer != null) {
-                    part.from = mapValue(startNummer);
-                    part.modifier = modifier;
-                    if (sluttNummer != null) {
-                        part.to = mapValue(sluttNummer);
-                        part.increment = 1;
-                    } else if (increment != null) {
-                        part.to = fieldType.to;
-                    } else {
-                        part.to = part.from;
-                    }
-                } else if (m.group("all") != null) {
-                    part.from = fieldType.from;
-                    part.to = fieldType.to;
-                    part.increment = 1;
-                } else if (m.group("ignore") != null) {
-                    part.modifier = m.group("ignore");
-                } else if (m.group("last") != null) {
-                    part.modifier = m.group("last");
-                } else {
-                    throw new IllegalArgumentException("Invalid cron part: " + rangePart);
+            }
+
+            addToSet(NO_SPEC_INT, -1, 0, type);
+            return i;
+        }
+
+        if (c == '*' || c == '/') {
+            if (c == '*' && (i + 1) >= s.length()) {
+                addToSet(ALL_SPEC_INT, -1, incr, type);
+                return i + 1;
+            } else if (c == '/'
+                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
+                    .charAt(i + 1) == '\t')) {
+                throw new ParseException("'/' must be followed by an integer.", i);
+            } else if (c == '*') {
+                i++;
+            }
+            c = s.charAt(i);
+            if (c == '/') { // is an increment specified?
+                i++;
+                if (i >= s.length()) {
+                    throw new ParseException("Unexpected end of string.", i);
                 }
 
-                if (increment != null) {
-                    part.incrementModifier = incrementModifier;
-                    part.increment = Integer.parseInt(increment);
+                incr = getNumericValue(s, i);
+
+                i++;
+                if (incr > 10) {
+                    i++;
+                }
+                if (incr > 59 && (type == SECOND || type == MINUTE)) {
+                    throw new ParseException("Increment > 60 : " + incr, i);
+                } else if (incr > 23 && (type == HOUR)) {
+                    throw new ParseException("Increment > 24 : " + incr, i);
+                } else if (incr > 31 && (type == DAY_OF_MONTH)) {
+                    throw new ParseException("Increment > 31 : " + incr, i);
+                } else if (incr > 7 && (type == DAY_OF_WEEK)) {
+                    throw new ParseException("Increment > 7 : " + incr, i);
+                } else if (incr > 12 && (type == MONTH)) {
+                    throw new ParseException("Increment > 12 : " + incr, i);
                 }
+            } else {
+                incr = 1;
+            }
 
-                validateRange(part);
-                validatePart(part);
-                parts.add(part);
+            addToSet(ALL_SPEC_INT, -1, incr, type);
+            return i;
+        } else if (c == 'L') {
+            i++;
+            if (type == DAY_OF_MONTH) {
+                lastdayOfMonth = true;
+            }
+            if (type == DAY_OF_WEEK) {
+                addToSet(7, 7, 0, type);
+            }
+            if(type == DAY_OF_MONTH && s.length() > i) {
+                c = s.charAt(i);
+                if(c == '-') {
+                    ValueSet vs = getValue(0, s, i+1);
+                    lastdayOffset = vs.value;
+                    if(lastdayOffset > 30)
+                        throw new ParseException("Offset from last day must be <= 30", i+1);
+                    i = vs.pos;
+                }
+                if(s.length() > i) {
+                    c = s.charAt(i);
+                    if(c == 'W') {
+                        nearestWeekday = true;
+                        i++;
+                    }
+                }
+            }
+            return i;
+        } else if (c >= '0' && c <= '9') {
+            int val = Integer.parseInt(String.valueOf(c));
+            i++;
+            if (i >= s.length()) {
+                addToSet(val, -1, -1, type);
+            } else {
+                c = s.charAt(i);
+                if (c >= '0' && c <= '9') {
+                    ValueSet vs = getValue(val, s, i);
+                    val = vs.value;
+                    i = vs.pos;
+                }
+                i = checkNext(i, s, val, type);
+                return i;
             }
+        } else {
+            throw new ParseException("Unexpected character: " + c, i);
+        }
+
+        return i;
+    }
+
+    protected int checkNext(int pos, String s, int val, int type)
+            throws ParseException {
+
+        int end = -1;
+        int i = pos;
 
-            Collections.sort(parts);
+        if (i >= s.length()) {
+            addToSet(val, end, -1, type);
+            return i;
         }
 
-        protected void validatePart(FieldPart part) {
-            if (part.modifier != null) {
-                throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
-            } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) {
-                throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
+        char c = s.charAt(pos);
+
+        if (c == 'L') {
+            if (type == DAY_OF_WEEK) {
+                if(val < 1 || val > 7)
+                    throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
+                lastdayOfWeek = true;
+            } else {
+                throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
             }
+            TreeSet<Integer> set = getSet(type);
+            set.add(val);
+            i++;
+            return i;
         }
 
-        private void validateRange(FieldPart part) {
-            if ((part.from != -1 && part.from < fieldType.from) || (part.to != -1 && part.to > fieldType.to)) {
-                throw new IllegalArgumentException(String.format("Invalid interval [%s-%s], must be %s<=_<=%s", part.from, part.to, fieldType.from,
-                        fieldType.to));
-            } else if (part.from != -1 && part.to != -1 && part.from > part.to) {
-                throw new IllegalArgumentException(
-                        String.format(
-                                "Invalid interval [%s-%s].  Rolling periods are not supported (ex. 5-1, only 1-5) since this won't give a deterministic result. Must be %s<=_<=%s",
-                                part.from, part.to, fieldType.from, fieldType.to));
+        if (c == 'W') {
+            if (type == DAY_OF_MONTH) {
+                nearestWeekday = true;
+            } else {
+                throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
             }
+            if(val > 31)
+                throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
+            TreeSet<Integer> set = getSet(type);
+            set.add(val);
+            i++;
+            return i;
         }
 
-        protected int mapValue(String value) {
-            int idx;
-            if (fieldType.names != null && (idx = fieldType.names.indexOf(value.toUpperCase(Locale.getDefault()))) >= 0) {
-                return idx + fieldType.from;
+        if (c == '#') {
+            if (type != DAY_OF_WEEK) {
+                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
+            }
+            i++;
+            try {
+                nthdayOfWeek = Integer.parseInt(s.substring(i));
+                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+                    throw new Exception();
+                }
+            } catch (Exception e) {
+                throw new ParseException(
+                        "A numeric value between 1 and 5 must follow the '#' option",
+                        i);
             }
-            return Integer.parseInt(value);
+
+            TreeSet<Integer> set = getSet(type);
+            set.add(val);
+            i++;
+            return i;
         }
 
-        protected boolean matches(int val, FieldPart part) {
-            if (val >= part.from && val <= part.to && (val - part.from) % part.increment == 0) {
-                return true;
+        if (c == '-') {
+            i++;
+            c = s.charAt(i);
+            int v = Integer.parseInt(String.valueOf(c));
+            end = v;
+            i++;
+            if (i >= s.length()) {
+                addToSet(val, end, 1, type);
+                return i;
+            }
+            c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                ValueSet vs = getValue(v, s, i);
+                end = vs.value;
+                i = vs.pos;
+            }
+            if (i < s.length() && ((c = s.charAt(i)) == '/')) {
+                i++;
+                c = s.charAt(i);
+                int v2 = Integer.parseInt(String.valueOf(c));
+                i++;
+                if (i >= s.length()) {
+                    addToSet(val, end, v2, type);
+                    return i;
+                }
+                c = s.charAt(i);
+                if (c >= '0' && c <= '9') {
+                    ValueSet vs = getValue(v2, s, i);
+                    int v3 = vs.value;
+                    addToSet(val, end, v3, type);
+                    i = vs.pos;
+                    return i;
+                } else {
+                    addToSet(val, end, v2, type);
+                    return i;
+                }
+            } else {
+                addToSet(val, end, 1, type);
+                return i;
             }
-            return false;
         }
 
-        protected int nextMatch(int val, FieldPart part) {
-            if (val > part.to) {
-                return -1;
+        if (c == '/') {
+            i++;
+            c = s.charAt(i);
+            int v2 = Integer.parseInt(String.valueOf(c));
+            i++;
+            if (i >= s.length()) {
+                addToSet(val, end, v2, type);
+                return i;
             }
-            int nextPotential = Math.max(val, part.from);
-            if (part.increment == 1 || nextPotential == part.from) {
-                return nextPotential;
+            c = s.charAt(i);
+            if (c >= '0' && c <= '9') {
+                ValueSet vs = getValue(v2, s, i);
+                int v3 = vs.value;
+                addToSet(val, end, v3, type);
+                i = vs.pos;
+                return i;
+            } else {
+                throw new ParseException("Unexpected character '" + c + "' after '/'", i);
             }
+        }
+
+        addToSet(val, end, 0, type);
+        i++;
+        return i;
+    }
+
+    public String getCronExpression() {
+        return cronExpression;
+    }
+
+    public String getExpressionSummary() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("seconds: ");
+        buf.append(getExpressionSetSummary(seconds));
+        buf.append("\n");
+        buf.append("minutes: ");
+        buf.append(getExpressionSetSummary(minutes));
+        buf.append("\n");
+        buf.append("hours: ");
+        buf.append(getExpressionSetSummary(hours));
+        buf.append("\n");
+        buf.append("daysOfMonth: ");
+        buf.append(getExpressionSetSummary(daysOfMonth));
+        buf.append("\n");
+        buf.append("months: ");
+        buf.append(getExpressionSetSummary(months));
+        buf.append("\n");
+        buf.append("daysOfWeek: ");
+        buf.append(getExpressionSetSummary(daysOfWeek));
+        buf.append("\n");
+        buf.append("lastdayOfWeek: ");
+        buf.append(lastdayOfWeek);
+        buf.append("\n");
+        buf.append("nearestWeekday: ");
+        buf.append(nearestWeekday);
+        buf.append("\n");
+        buf.append("NthDayOfWeek: ");
+        buf.append(nthdayOfWeek);
+        buf.append("\n");
+        buf.append("lastdayOfMonth: ");
+        buf.append(lastdayOfMonth);
+        buf.append("\n");
+        buf.append("years: ");
+        buf.append(getExpressionSetSummary(years));
+        buf.append("\n");
+
+        return buf.toString();
+    }
+
+    protected String getExpressionSetSummary(java.util.Set<Integer> set) {
 
-            int remainder = ((nextPotential - part.from) % part.increment);
-            if (remainder != 0) {
-                nextPotential += part.increment - remainder;
+        if (set.contains(NO_SPEC)) {
+            return "?";
+        }
+        if (set.contains(ALL_SPEC)) {
+            return "*";
+        }
+
+        StringBuilder buf = new StringBuilder();
+
+        Iterator<Integer> itr = set.iterator();
+        boolean first = true;
+        while (itr.hasNext()) {
+            Integer iVal = itr.next();
+            String val = iVal.toString();
+            if (!first) {
+                buf.append(",");
             }
+            buf.append(val);
+            first = false;
+        }
 
-            return nextPotential <= part.to ? nextPotential : -1;
+        return buf.toString();
+    }
+
+    protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) {
+
+        if (list.contains(NO_SPEC)) {
+            return "?";
         }
+        if (list.contains(ALL_SPEC)) {
+            return "*";
+        }
+
+        StringBuilder buf = new StringBuilder();
+
+        Iterator<Integer> itr = list.iterator();
+        boolean first = true;
+        while (itr.hasNext()) {
+            Integer iVal = itr.next();
+            String val = iVal.toString();
+            if (!first) {
+                buf.append(",");
+            }
+            buf.append(val);
+            first = false;
+        }
+
+        return buf.toString();
     }
 
-    static class SimpleField extends BasicField {
-        SimpleField(CronFieldType fieldType, String fieldExpr) {
-            super(fieldType, fieldExpr);
+    protected int skipWhiteSpace(int i, String s) {
+        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
+            ;
         }
 
-        public boolean matches(int val) {
-            if (val >= fieldType.from && val <= fieldType.to) {
-                for (FieldPart part : parts) {
-                    if (matches(val, part)) {
-                        return true;
-                    }
-                }
+        return i;
+    }
+
+    protected int findNextWhiteSpace(int i, String s) {
+        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
+            ;
+        }
+
+        return i;
+    }
+
+    protected void addToSet(int val, int end, int incr, int type)
+            throws ParseException {
+
+        TreeSet<Integer> set = getSet(type);
+
+        if (type == SECOND || type == MINUTE) {
+            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Minute and Second values must be between 0 and 59",
+                        -1);
+            }
+        } else if (type == HOUR) {
+            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Hour values must be between 0 and 23", -1);
+            }
+        } else if (type == DAY_OF_MONTH) {
+            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
+                    && (val != NO_SPEC_INT)) {
+                throw new ParseException(
+                        "Day of month values must be between 1 and 31", -1);
+            }
+        } else if (type == MONTH) {
+            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
+                throw new ParseException(
+                        "Month values must be between 1 and 12", -1);
+            }
+        } else if (type == DAY_OF_WEEK) {
+            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
+                    && (val != NO_SPEC_INT)) {
+                throw new ParseException(
+                        "Day-of-Week values must be between 1 and 7", -1);
             }
-            return false;
         }
 
-        /**
-         * Find the next match for this field. If a match cannot be found force an overflow and increase the next
-         * greatest field.
-         *
-         * @param dateTime {@link ZonedDateTime} array so the reference can be modified
-         * @return {@code true} if a match was found for this field or {@code false} if the field overflowed
-         */
-        protected boolean nextMatch(ZonedDateTime[] dateTime) {
-            int value = fieldType.getValue(dateTime[0]);
-
-            for (FieldPart part : parts) {
-                int nextMatch = nextMatch(value, part);
-                if (nextMatch > -1) {
-                    if (nextMatch != value) {
-                        dateTime[0] = fieldType.setValue(dateTime[0], nextMatch);
-                    }
-                    return true;
-                }
+        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
+            if (val != -1) {
+                set.add(val);
+            } else {
+                set.add(NO_SPEC);
             }
 
-            dateTime[0] = fieldType.overflow(dateTime[0]);
-            return false;
+            return;
         }
-    }
 
-    static class DayOfWeekField extends BasicField {
+        int startAt = val;
+        int stopAt = end;
 
-        DayOfWeekField(String fieldExpr) {
-            super(CronFieldType.DAY_OF_WEEK, fieldExpr);
+        if (val == ALL_SPEC_INT && incr <= 0) {
+            incr = 1;
+            set.add(ALL_SPEC); // put in a marker, but also fill values
         }
 
-        boolean matches(LocalDate dato) {
-            for (FieldPart part : parts) {
-                if ("L".equals(part.modifier)) {
-                    YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue());
-                    return dato.getDayOfWeek() == DayOfWeek.of(part.from) && dato.getDayOfMonth() > (ym.lengthOfMonth() - 7);
-                } else if ("#".equals(part.incrementModifier)) {
-                    if (dato.getDayOfWeek() == DayOfWeek.of(part.from)) {
-                        int num = dato.getDayOfMonth() / 7;
-                        return part.increment == (dato.getDayOfMonth() % 7 == 0 ? num : num + 1);
-                    }
-                    return false;
-                } else if (matches(dato.getDayOfWeek().getValue(), part)) {
-                    return true;
-                }
+        if (type == SECOND || type == MINUTE) {
+            if (stopAt == -1) {
+                stopAt = 59;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 0;
+            }
+        } else if (type == HOUR) {
+            if (stopAt == -1) {
+                stopAt = 23;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 0;
+            }
+        } else if (type == DAY_OF_MONTH) {
+            if (stopAt == -1) {
+                stopAt = 31;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == MONTH) {
+            if (stopAt == -1) {
+                stopAt = 12;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == DAY_OF_WEEK) {
+            if (stopAt == -1) {
+                stopAt = 7;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1;
+            }
+        } else if (type == YEAR) {
+            if (stopAt == -1) {
+                stopAt = MAX_YEAR;
+            }
+            if (startAt == -1 || startAt == ALL_SPEC_INT) {
+                startAt = 1970;
             }
-            return false;
         }
 
-        @Override
-        protected int mapValue(String value) {
-            // Use 1-7 for weedays, but 0 will also represent sunday (linux practice)
-            return "0".equals(value) ? 7 : super.mapValue(value);
+        // if the end of the range is before the start, then we need to overflow into
+        // the next day, month etc. This is done by adding the maximum amount for that
+        // type, and using modulus max to determine the value being added.
+        int max = -1;
+        if (stopAt < startAt) {
+            switch (type) {
+                case       SECOND : max = 60; break;
+                case       MINUTE : max = 60; break;
+                case         HOUR : max = 24; break;
+                case        MONTH : max = 12; break;
+                case  DAY_OF_WEEK : max = 7;  break;
+                case DAY_OF_MONTH : max = 31; break;
+                case         YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
+                default           : throw new IllegalArgumentException("Unexpected type encountered");
+            }
+            stopAt += max;
         }
 
-        @Override
-        protected boolean matches(int val, FieldPart part) {
-            return "?".equals(part.modifier) || super.matches(val, part);
+        for (int i = startAt; i <= stopAt; i += incr) {
+            if (max == -1) {
+                // ie: there's no max to overflow over
+                set.add(i);
+            } else {
+                // take the modulus to get the real value
+                int i2 = i % max;
+
+                // 1-indexed ranges should not include 0, and should include their max
+                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
+                    i2 = max;
+                }
+
+                set.add(i2);
+            }
         }
+    }
+
+    TreeSet<Integer> getSet(int type) {
+        switch (type) {
+            case SECOND:
+                return seconds;
+            case MINUTE:
+                return minutes;
+            case HOUR:
+                return hours;
+            case DAY_OF_MONTH:
+                return daysOfMonth;
+            case MONTH:
+                return months;
+            case DAY_OF_WEEK:
+                return daysOfWeek;
+            case YEAR:
+                return years;
+            default:
+                return null;
+        }
+    }
 
-        @Override
-        protected void validatePart(FieldPart part) {
-            if (part.modifier != null && Arrays.asList("L", "?").indexOf(part.modifier) == -1) {
-                throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
-            } else if (part.incrementModifier != null && Arrays.asList("/", "#").indexOf(part.incrementModifier) == -1) {
-                throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
+    protected ValueSet getValue(int v, String s, int i) {
+        char c = s.charAt(i);
+        StringBuilder s1 = new StringBuilder(String.valueOf(v));
+        while (c >= '0' && c <= '9') {
+            s1.append(c);
+            i++;
+            if (i >= s.length()) {
+                break;
             }
+            c = s.charAt(i);
+        }
+        ValueSet val = new ValueSet();
+
+        val.pos = (i < s.length()) ? i : i + 1;
+        val.value = Integer.parseInt(s1.toString());
+        return val;
+    }
+
+    protected int getNumericValue(String s, int i) {
+        int endOfVal = findNextWhiteSpace(i, s);
+        String val = s.substring(i, endOfVal);
+        return Integer.parseInt(val);
+    }
+
+    protected int getMonthNumber(String s) {
+        Integer integer = monthMap.get(s);
+
+        if (integer == null) {
+            return -1;
         }
+
+        return integer;
     }
 
-    static class DayOfMonthField extends BasicField {
-        DayOfMonthField(String fieldExpr) {
-            super(CronFieldType.DAY_OF_MONTH, fieldExpr);
+    protected int getDayOfWeekNumber(String s) {
+        Integer integer = dayMap.get(s);
+
+        if (integer == null) {
+            return -1;
         }
 
-        boolean matches(LocalDate dato) {
-            for (FieldPart part : parts) {
-                if ("L".equals(part.modifier)) {
-                    YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue());
-                    return dato.getDayOfMonth() == (ym.lengthOfMonth() - (part.from == -1 ? 0 : part.from));
-                } else if ("W".equals(part.modifier)) {
-                    if (dato.getDayOfWeek().getValue() <= 5) {
-                        if (dato.getDayOfMonth() == part.from) {
-                            return true;
-                        } else if (dato.getDayOfWeek().getValue() == 5) {
-                            return dato.plusDays(1).getDayOfMonth() == part.from;
-                        } else if (dato.getDayOfWeek().getValue() == 1) {
-                            return dato.minusDays(1).getDayOfMonth() == part.from;
+        return integer;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //
+    // Computation Functions
+    //
+    ////////////////////////////////////////////////////////////////////////////
+
+    public Date getTimeAfter(Date afterTime) {
+
+        // Computation is based on Gregorian year only.
+        Calendar cl = new java.util.GregorianCalendar(getTimeZone());
+
+        // move ahead one second, since we're computing the time *after* the
+        // given time
+        afterTime = new Date(afterTime.getTime() + 1000);
+        // CronTrigger does not deal with milliseconds
+        cl.setTime(afterTime);
+        cl.set(Calendar.MILLISECOND, 0);
+
+        boolean gotOne = false;
+        // loop until we've computed the next time, or we've past the endTime
+        while (!gotOne) {
+
+            //if (endTime != null && cl.getTime().after(endTime)) return null;
+            if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop...
+                return null;
+            }
+
+            SortedSet<Integer> st = null;
+            int t = 0;
+
+            int sec = cl.get(Calendar.SECOND);
+            int min = cl.get(Calendar.MINUTE);
+
+            // get second.................................................
+            st = seconds.tailSet(sec);
+            if (st != null && st.size() != 0) {
+                sec = st.first();
+            } else {
+                sec = seconds.first();
+                min++;
+                cl.set(Calendar.MINUTE, min);
+            }
+            cl.set(Calendar.SECOND, sec);
+
+            min = cl.get(Calendar.MINUTE);
+            int hr = cl.get(Calendar.HOUR_OF_DAY);
+            t = -1;
+
+            // get minute.................................................
+            st = minutes.tailSet(min);
+            if (st != null && st.size() != 0) {
+                t = min;
+                min = st.first();
+            } else {
+                min = minutes.first();
+                hr++;
+            }
+            if (min != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, min);
+                setCalendarHour(cl, hr);
+                continue;
+            }
+            cl.set(Calendar.MINUTE, min);
+
+            hr = cl.get(Calendar.HOUR_OF_DAY);
+            int day = cl.get(Calendar.DAY_OF_MONTH);
+            t = -1;
+
+            // get hour...................................................
+            st = hours.tailSet(hr);
+            if (st != null && st.size() != 0) {
+                t = hr;
+                hr = st.first();
+            } else {
+                hr = hours.first();
+                day++;
+            }
+            if (hr != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.DAY_OF_MONTH, day);
+                setCalendarHour(cl, hr);
+                continue;
+            }
+            cl.set(Calendar.HOUR_OF_DAY, hr);
+
+            day = cl.get(Calendar.DAY_OF_MONTH);
+            int mon = cl.get(Calendar.MONTH) + 1;
+            // '+ 1' because calendar is 0-based for this field, and we are
+            // 1-based
+            t = -1;
+            int tmon = mon;
+
+            // get day...................................................
+            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
+            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
+            if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
+                st = daysOfMonth.tailSet(day);
+                if (lastdayOfMonth) {
+                    if(!nearestWeekday) {
+                        t = day;
+                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                        day -= lastdayOffset;
+                        if(t > day) {
+                            mon++;
+                            if(mon > 12) {
+                                mon = 1;
+                                tmon = 3333; // ensure test of mon != tmon further below fails
+                                cl.add(Calendar.YEAR, 1);
+                            }
+                            day = 1;
+                        }
+                    } else {
+                        t = day;
+                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                        day -= lastdayOffset;
+
+                        java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
+                        tcal.set(Calendar.SECOND, 0);
+                        tcal.set(Calendar.MINUTE, 0);
+                        tcal.set(Calendar.HOUR_OF_DAY, 0);
+                        tcal.set(Calendar.DAY_OF_MONTH, day);
+                        tcal.set(Calendar.MONTH, mon - 1);
+                        tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
+
+                        int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                        int dow = tcal.get(Calendar.DAY_OF_WEEK);
+
+                        if(dow == Calendar.SATURDAY && day == 1) {
+                            day += 2;
+                        } else if(dow == Calendar.SATURDAY) {
+                            day -= 1;
+                        } else if(dow == Calendar.SUNDAY && day == ldom) {
+                            day -= 2;
+                        } else if(dow == Calendar.SUNDAY) {
+                            day += 1;
+                        }
+
+                        tcal.set(Calendar.SECOND, sec);
+                        tcal.set(Calendar.MINUTE, min);
+                        tcal.set(Calendar.HOUR_OF_DAY, hr);
+                        tcal.set(Calendar.DAY_OF_MONTH, day);
+                        tcal.set(Calendar.MONTH, mon - 1);
+                        Date nTime = tcal.getTime();
+                        if(nTime.before(afterTime)) {
+                            day = 1;
+                            mon++;
                         }
                     }
-                } else if (matches(dato.getDayOfMonth(), part)) {
-                    return true;
+                } else if(nearestWeekday) {
+                    t = day;
+                    day = daysOfMonth.first();
+
+                    java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
+                    tcal.set(Calendar.SECOND, 0);
+                    tcal.set(Calendar.MINUTE, 0);
+                    tcal.set(Calendar.HOUR_OF_DAY, 0);
+                    tcal.set(Calendar.DAY_OF_MONTH, day);
+                    tcal.set(Calendar.MONTH, mon - 1);
+                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
+
+                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
+
+                    if(dow == Calendar.SATURDAY && day == 1) {
+                        day += 2;
+                    } else if(dow == Calendar.SATURDAY) {
+                        day -= 1;
+                    } else if(dow == Calendar.SUNDAY && day == ldom) {
+                        day -= 2;
+                    } else if(dow == Calendar.SUNDAY) {
+                        day += 1;
+                    }
+
+
+                    tcal.set(Calendar.SECOND, sec);
+                    tcal.set(Calendar.MINUTE, min);
+                    tcal.set(Calendar.HOUR_OF_DAY, hr);
+                    tcal.set(Calendar.DAY_OF_MONTH, day);
+                    tcal.set(Calendar.MONTH, mon - 1);
+                    Date nTime = tcal.getTime();
+                    if(nTime.before(afterTime)) {
+                        day = daysOfMonth.first();
+                        mon++;
+                    }
+                } else if (st != null && st.size() != 0) {
+                    t = day;
+                    day = st.first();
+                    // make sure we don't over-run a short month, such as february
+                    int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+                    if (day > lastDay) {
+                        day = daysOfMonth.first();
+                        mon++;
+                    }
+                } else {
+                    day = daysOfMonth.first();
+                    mon++;
+                }
+
+                if (day != t || mon != tmon) {
+                    cl.set(Calendar.SECOND, 0);
+                    cl.set(Calendar.MINUTE, 0);
+                    cl.set(Calendar.HOUR_OF_DAY, 0);
+                    cl.set(Calendar.DAY_OF_MONTH, day);
+                    cl.set(Calendar.MONTH, mon - 1);
+                    // '- 1' because calendar is 0-based for this field, and we
+                    // are 1-based
+                    continue;
                 }
+            } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
+                if (lastdayOfWeek) { // are we looking for the last XXX day of
+                    // the month?
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    }
+                    if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+
+                    if (day + daysToAdd > lDay) { // did we already miss the
+                        // last one?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    }
+
+                    // find date of last occurrence of this day in this month...
+                    while ((day + daysToAdd + 7) <= lDay) {
+                        daysToAdd += 7;
+                    }
+
+                    day += daysToAdd;
+
+                    if (daysToAdd > 0) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' here because we are not promoting the month
+                        continue;
+                    }
+
+                } else if (nthdayOfWeek != 0) {
+                    // are we looking for the Nth XXX day in the month?
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    } else if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    boolean dayShifted = false;
+                    if (daysToAdd > 0) {
+                        dayShifted = true;
+                    }
+
+                    day += daysToAdd;
+                    int weekOfMonth = day / 7;
+                    if (day % 7 > 0) {
+                        weekOfMonth++;
+                    }
+
+                    daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
+                    day += daysToAdd;
+                    if (daysToAdd < 0
+                            || day > getLastDayOfMonth(mon, cl
+                            .get(Calendar.YEAR))) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    } else if (daysToAdd > 0 || dayShifted) {
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' here because we are NOT promoting the month
+                        continue;
+                    }
+                } else {
+                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
+                    int dow = daysOfWeek.first(); // desired
+                    // d-o-w
+                    st = daysOfWeek.tailSet(cDow);
+                    if (st != null && st.size() > 0) {
+                        dow = st.first();
+                    }
+
+                    int daysToAdd = 0;
+                    if (cDow < dow) {
+                        daysToAdd = dow - cDow;
+                    }
+                    if (cDow > dow) {
+                        daysToAdd = dow + (7 - cDow);
+                    }
+
+                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
+
+                    if (day + daysToAdd > lDay) { // will we pass the end of
+                        // the month?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, 1);
+                        cl.set(Calendar.MONTH, mon);
+                        // no '- 1' here because we are promoting the month
+                        continue;
+                    } else if (daysToAdd > 0) { // are we swithing days?
+                        cl.set(Calendar.SECOND, 0);
+                        cl.set(Calendar.MINUTE, 0);
+                        cl.set(Calendar.HOUR_OF_DAY, 0);
+                        cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
+                        cl.set(Calendar.MONTH, mon - 1);
+                        // '- 1' because calendar is 0-based for this field,
+                        // and we are 1-based
+                        continue;
+                    }
+                }
+            } else { // dayOfWSpec && !dayOfMSpec
+                throw new UnsupportedOperationException(
+                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
+            }
+            cl.set(Calendar.DAY_OF_MONTH, day);
+
+            mon = cl.get(Calendar.MONTH) + 1;
+            // '+ 1' because calendar is 0-based for this field, and we are
+            // 1-based
+            int year = cl.get(Calendar.YEAR);
+            t = -1;
+
+            // test for expressions that never generate a valid fire date,
+            // but keep looping...
+            if (year > MAX_YEAR) {
+                return null;
+            }
+
+            // get month...................................................
+            st = months.tailSet(mon);
+            if (st != null && st.size() != 0) {
+                t = mon;
+                mon = st.first();
+            } else {
+                mon = months.first();
+                year++;
+            }
+            if (mon != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.HOUR_OF_DAY, 0);
+                cl.set(Calendar.DAY_OF_MONTH, 1);
+                cl.set(Calendar.MONTH, mon - 1);
+                // '- 1' because calendar is 0-based for this field, and we are
+                // 1-based
+                cl.set(Calendar.YEAR, year);
+                continue;
+            }
+            cl.set(Calendar.MONTH, mon - 1);
+            // '- 1' because calendar is 0-based for this field, and we are
+            // 1-based
+
+            year = cl.get(Calendar.YEAR);
+            t = -1;
+
+            // get year...................................................
+            st = years.tailSet(year);
+            if (st != null && st.size() != 0) {
+                t = year;
+                year = st.first();
+            } else {
+                return null; // ran out of years...
             }
-            return false;
-        }
 
-        @Override
-        protected void validatePart(FieldPart part) {
-            if (part.modifier != null && Arrays.asList("L", "W", "?").indexOf(part.modifier) == -1) {
-                throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier));
-            } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) {
-                throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier));
+            if (year != t) {
+                cl.set(Calendar.SECOND, 0);
+                cl.set(Calendar.MINUTE, 0);
+                cl.set(Calendar.HOUR_OF_DAY, 0);
+                cl.set(Calendar.DAY_OF_MONTH, 1);
+                cl.set(Calendar.MONTH, 0);
+                // '- 1' because calendar is 0-based for this field, and we are
+                // 1-based
+                cl.set(Calendar.YEAR, year);
+                continue;
             }
+            cl.set(Calendar.YEAR, year);
+
+            gotOne = true;
+        } // while( !done )
+
+        return cl.getTime();
+    }
+
+    /**
+     * Advance the calendar to the particular hour paying particular attention
+     * to daylight saving problems.
+     *
+     * @param cal the calendar to operate on
+     * @param hour the hour to set
+     */
+    protected void setCalendarHour(Calendar cal, int hour) {
+        cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
+        if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) {
+            cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1);
         }
+    }
+
+    public Date getTimeBefore(Date endTime) {
+        // FUTURE_TODO: implement QUARTZ-423
+        return null;
+    }
+
+    public Date getFinalFireTime() {
+        // FUTURE_TODO: implement QUARTZ-423
+        return null;
+    }
+
+    protected boolean isLeapYear(int year) {
+        return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
+    }
 
-        @Override
-        protected boolean matches(int val, FieldPart part) {
-            return "?".equals(part.modifier) || super.matches(val, part);
+    protected int getLastDayOfMonth(int monthNum, int year) {
+
+        switch (monthNum) {
+            case 1:
+                return 31;
+            case 2:
+                return (isLeapYear(year)) ? 29 : 28;
+            case 3:
+                return 31;
+            case 4:
+                return 30;
+            case 5:
+                return 31;
+            case 6:
+                return 30;
+            case 7:
+                return 31;
+            case 8:
+                return 31;
+            case 9:
+                return 30;
+            case 10:
+                return 31;
+            case 11:
+                return 30;
+            case 12:
+                return 31;
+            default:
+                throw new IllegalArgumentException("Illegal month number: "
+                        + monthNum);
         }
     }
-}
\ No newline at end of file
+
+
+    private void readObject(java.io.ObjectInputStream stream)
+            throws java.io.IOException, ClassNotFoundException {
+
+        stream.defaultReadObject();
+        try {
+            buildExpression(cronExpression);
+        } catch (Exception ignore) {
+        } // never happens
+    }
+
+    @Override
+    @Deprecated
+    public Object clone() {
+        return new CronExpression(this);
+    }
+}
+
+@SuppressWarnings("VisibilityModifier")
+class ValueSet {
+    public int value;
+
+    public int pos;
+}
diff --git a/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java b/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java
deleted file mode 100644
index b04db7bf5..000000000
--- a/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2013-2022 Nikita Koksharov
- *
- * 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
- *
- *    http://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.redisson.executor;
-
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.NavigableSet;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-public class CronExpressionEx extends CronExpression {
-
-    private static final ThreadLocal<NavigableSet<Integer>> YEARS_FIELD = new ThreadLocal<>();
-
-    private final NavigableSet<Integer> years;
-
-    public CronExpressionEx(String expr) {
-        super(parseYear(expr));
-        years = YEARS_FIELD.get();
-        YEARS_FIELD.remove();
-    }
-
-    public CronExpressionEx(String expr, boolean withSeconds) {
-        super(parseYear(expr), withSeconds);
-        years = YEARS_FIELD.get();
-        YEARS_FIELD.remove();
-    }
-
-    @Override
-    public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime) {
-        if (years != null) {
-            afterTime = afterTime.withYear(years.ceiling(afterTime.getYear()));
-        }
-        return super.nextTimeAfter(afterTime);
-    }
-
-    private static String parseYear(String expr) {
-        String[] parts = expr.split("\\s+");
-        if (parts.length == 7) {
-            String year = parts[6];
-            String[] years = year.split(",");
-            if (years.length > 1) {
-                NavigableSet<Integer> yy = new TreeSet<>(Arrays.stream(years).map(y -> Integer.valueOf(y))
-                                                    .collect(Collectors.toList()));
-                YEARS_FIELD.set(yy);
-            }
-            String[] yearsRange = year.split("-");
-            if (yearsRange.length > 1) {
-                NavigableSet<Integer> yy = new TreeSet<>(IntStream.rangeClosed(Integer.valueOf(yearsRange[0]), Integer.valueOf(yearsRange[1]))
-                                                    .boxed().collect(Collectors.toList()));
-                YEARS_FIELD.set(yy);
-            }
-            return expr.replace(year, "").trim();
-        }
-        return expr;
-    }
-}
diff --git a/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java b/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java
index 5d241b348..d454ebbad 100644
--- a/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java
+++ b/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java
@@ -38,11 +38,10 @@ import org.redisson.remote.ResponseEntry;
 
 import java.io.ByteArrayInputStream;
 import java.io.ObjectInput;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.Map;
+import java.util.TimeZone;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
@@ -137,13 +136,13 @@ public class TasksRunnerService implements RemoteExecutorService {
     
     @Override
     public void schedule(ScheduledCronExpressionParameters params) {
-        CronExpression expression = new CronExpressionEx(params.getCronExpression());
-        ZonedDateTime currentDate = ZonedDateTime.of(LocalDateTime.now(), ZoneId.of(params.getTimezone()));
-        ZonedDateTime nextStartDate = expression.nextTimeAfter(currentDate);
+        CronExpression expression = new CronExpression(params.getCronExpression());
+        expression.setTimeZone(TimeZone.getTimeZone(params.getTimezone()));
+        Date nextStartDate = expression.getNextValidTimeAfter(new Date());
         RFuture<Void> future = null;
         if (nextStartDate != null) {
             RemoteExecutorServiceAsync service = asyncScheduledServiceAtFixed(params.getExecutorId(), params.getRequestId());
-            params.setStartTime(nextStartDate.toInstant().toEpochMilli());
+            params.setStartTime(nextStartDate.getTime());
             future = service.schedule(params);
         }
         try {