PageRenderTime 120ms CodeModel.GetById 29ms app.highlight 59ms RepoModel.GetById 16ms app.codeStats 1ms

/Cron.NET/CronExpression.cs

http://github.com/FelicePollano/Cron.NET
C# | 2097 lines | 1419 code | 157 blank | 521 comment | 467 complexity | 414a5f470e280a57b999d5ec816048fb MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* 
   2* Copyright 2004-2009 James House 
   3* 
   4* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
   5* use this file except in compliance with the License. You may obtain a copy 
   6* of the License at 
   7* 
   8*   http://www.apache.org/licenses/LICENSE-2.0 
   9*   
  10* Unless required by applicable law or agreed to in writing, software 
  11* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
  12* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
  13* License for the specific language governing permissions and limitations 
  14* under the License.
  15* 
  16*/
  17using System;
  18using System.Collections;
  19using System.Globalization;
  20using System.Runtime.Serialization;
  21using System.Text;
  22
  23
  24using NullableDateTime = System.Nullable<System.DateTime>;
  25
  26
  27
  28//using TimeZone = System.TimeZoneInfo;
  29
  30
  31using Quartz.Collection;
  32
  33namespace Quartz
  34{
  35    /// <summary>
  36    /// Provides a parser and evaluator for unix-like cron expressions. Cron 
  37    /// expressions provide the ability to specify complex time combinations such as
  38    /// &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every 
  39    /// last Friday of the month&quot;. 
  40    /// </summary>
  41    /// <remarks>
  42    /// <p>
  43    /// Cron expressions are comprised of 6 required fields and one optional field
  44    /// separated by white space. The fields respectively are described as follows:
  45    /// </p>
  46    /// <table cellspacing="8">
  47    /// <tr>
  48    /// <th align="left">Field Name</th>
  49    /// <th align="left"> </th>
  50    /// <th align="left">Allowed Values</th>
  51    /// <th align="left"> </th>
  52    /// <th align="left">Allowed Special Characters</th>
  53    /// </tr>
  54    /// <tr>
  55    /// <td align="left">Seconds</td>
  56    /// <td align="left"> </td>
  57    /// <td align="left">0-59</td>
  58    /// <td align="left"> </td>
  59    /// <td align="left">, - /// /</td>
  60    /// </tr>
  61    /// <tr>
  62    /// <td align="left">Minutes</td>
  63    /// <td align="left"> </td>
  64    /// <td align="left">0-59</td>
  65    /// <td align="left"> </td>
  66    /// <td align="left">, - /// /</td>
  67    /// </tr>
  68    /// <tr>
  69    /// <td align="left">Hours</td>
  70    /// <td align="left"> </td>
  71    /// <td align="left">0-23</td>
  72    /// <td align="left"> </td>
  73    /// <td align="left">, - /// /</td>
  74    /// </tr>
  75    /// <tr>
  76    /// <td align="left">Day-of-month</td>
  77    /// <td align="left"> </td>
  78    /// <td align="left">1-31</td>
  79    /// <td align="left"> </td>
  80    /// <td align="left">, - /// ? / L W C</td>
  81    /// </tr>
  82    /// <tr>
  83    /// <td align="left">Month</td>
  84    /// <td align="left"> </td>
  85    /// <td align="left">1-12 or JAN-DEC</td>
  86    /// <td align="left"> </td>
  87    /// <td align="left">, - /// /</td>
  88    /// </tr>
  89    /// <tr>
  90    /// <td align="left">Day-of-Week</td>
  91    /// <td align="left"> </td>
  92    /// <td align="left">1-7 or SUN-SAT</td>
  93    /// <td align="left"> </td>
  94    /// <td align="left">, - /// ? / L #</td>
  95    /// </tr>
  96    /// <tr>
  97    /// <td align="left">Year (Optional)</td>
  98    /// <td align="left"> </td>
  99    /// <td align="left">empty, 1970-2099</td>
 100    /// <td align="left"> </td>
 101    /// <td align="left">, - /// /</td>
 102    /// </tr>
 103    /// </table>
 104    /// <p>
 105    /// The '*' character is used to specify all values. For example, &quot;*&quot; 
 106    /// in the minute field means &quot;every minute&quot;.
 107    /// </p>
 108    /// <p>
 109    /// The '?' character is allowed for the day-of-month and day-of-week fields. It
 110    /// is used to specify 'no specific value'. This is useful when you need to
 111    /// specify something in one of the two fields, but not the other.
 112    /// </p>
 113    /// <p>
 114    /// The '-' character is used to specify ranges For example &quot;10-12&quot; in
 115    /// the hour field means &quot;the hours 10, 11 and 12&quot;.
 116    /// </p>
 117    /// <p>
 118    /// The ',' character is used to specify additional values. For example
 119    /// &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
 120    /// Wednesday, and Friday&quot;.
 121    /// </p>
 122    /// <p>
 123    /// The '/' character is used to specify increments. For example &quot;0/15&quot;
 124    /// in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And 
 125    /// &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
 126    /// 50&quot;.  Specifying '*' before the  '/' is equivalent to specifying 0 is
 127    /// the value to start with. Essentially, for each field in the expression, there
 128    /// is a set of numbers that can be turned on or off. For seconds and minutes, 
 129    /// the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
 130    /// 31, and for months 1 to 12. The &quot;/&quot; character simply helps you turn
 131    /// on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
 132    /// month field only turns on month &quot;7&quot;, it does NOT mean every 6th 
 133    /// month, please note that subtlety.  
 134    /// </p>
 135    /// <p>
 136    /// The 'L' character is allowed for the day-of-month and day-of-week fields.
 137    /// This character is short-hand for &quot;last&quot;, but it has different 
 138    /// meaning in each of the two fields. For example, the value &quot;L&quot; in 
 139    /// the day-of-month field means &quot;the last day of the month&quot; - day 31 
 140    /// for January, day 28 for February on non-leap years. If used in the 
 141    /// day-of-week field by itself, it simply means &quot;7&quot; or 
 142    /// &quot;SAT&quot;. But if used in the day-of-week field after another value, it
 143    /// means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
 144    /// means &quot;the last friday of the month&quot;. When using the 'L' option, it
 145    /// is important not to specify lists, or ranges of values, as you'll get 
 146    /// confusing results.
 147    /// </p>
 148    /// <p>
 149    /// The 'W' character is allowed for the day-of-month field.  This character 
 150    /// is used to specify the weekday (Monday-Friday) nearest the given day.  As an 
 151    /// example, if you were to specify &quot;15W&quot; as the value for the 
 152    /// day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
 153    /// the month&quot;. So if the 15th is a Saturday, the trigger will fire on 
 154    /// Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
 155    /// 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. 
 156    /// However if you specify &quot;1W&quot; as the value for day-of-month, and the
 157    /// 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not 
 158    /// 'jump' over the boundary of a month's days.  The 'W' character can only be 
 159    /// specified when the day-of-month is a single day, not a range or list of days.
 160    /// </p>
 161    /// <p>
 162    /// The 'L' and 'W' characters can also be combined for the day-of-month 
 163    /// expression to yield 'LW', which translates to &quot;last weekday of the 
 164    /// month&quot;.
 165    /// </p>
 166    /// <p>
 167    /// The '#' character is allowed for the day-of-week field. This character is
 168    /// used to specify &quot;the nth&quot; XXX day of the month. For example, the 
 169    /// value of &quot;6#3&quot; in the day-of-week field means the third Friday of 
 170    /// the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month). 
 171    /// Other examples: &quot;2#1&quot; = the first Monday of the month and 
 172    /// &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
 173    /// &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
 174    /// no firing will occur that month. If the '#' character is used, there can
 175    /// only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is 
 176    /// not valid, since there are two expressions).
 177    /// </p>
 178    /// <p>
 179    /// <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
 180    /// This character is short-hand for "calendar". This means values are
 181    /// calculated against the associated calendar, if any. If no calendar is
 182    /// associated, then it is equivalent to having an all-inclusive calendar. A
 183    /// value of "5C" in the day-of-month field means "the first day included by the
 184    /// calendar on or after the 5th". A value of "1C" in the day-of-week field
 185    /// means "the first day included by the calendar on or after sunday". -->
 186    /// </p>
 187    /// <p>
 188    /// The legal characters and the names of months and days of the week are not
 189    /// case sensitive.
 190    /// </p>
 191    /// <p>
 192    /// <b>NOTES:</b>
 193    /// <ul>
 194    /// <li>Support for specifying both a day-of-week and a day-of-month value is
 195    /// not complete (you'll need to use the '?' character in one of these fields).
 196    /// </li>
 197    /// <li>Overflowing ranges is supported - that is, having a larger number on 
 198    /// the left hand side than the right. You might do 22-2 to catch 10 o'clock 
 199    /// at night until 2 o'clock in the morning, or you might have NOV-FEB. It is 
 200    /// very important to note that overuse of overflowing ranges creates ranges 
 201    /// that don't make sense and no effort has been made to determine which 
 202    /// interpretation CronExpression chooses. An example would be 
 203    /// "0 0 14-6 ? * FRI-MON". </li>
 204    /// </ul>
 205    /// </p>
 206    /// </remarks>
 207    /// <author>Sharada Jambula</author>
 208    /// <author>James House</author>
 209    /// <author>Contributions from Mads Henderson</author>
 210    /// <author>Refactoring from CronTrigger to CronExpression by Aaron Craven</author>
 211    [Serializable]
 212    public class CronExpression : ICloneable, IDeserializationCallback
 213    {
 214		/// <summary>
 215		/// Field specification for second.
 216		/// </summary>
 217        protected const int Second = 0;
 218
 219		/// <summary>
 220		/// Field specification for minute.
 221		/// </summary>
 222		protected const int Minute = 1;
 223        
 224		/// <summary>
 225		/// Field specification for hour.
 226		/// </summary>
 227		protected const int Hour = 2;
 228        
 229		/// <summary>
 230		/// Field specification for day of month.
 231		/// </summary>
 232		protected const int DayOfMonth = 3;
 233        
 234		/// <summary>
 235		/// Field specification for month.
 236		/// </summary>
 237		protected const int Month = 4;
 238        
 239		/// <summary>
 240		/// Field specification for day of week.
 241		/// </summary>
 242		protected const int DayOfWeek = 5;
 243        
 244		/// <summary>
 245		/// Field specification for year.
 246		/// </summary>
 247		protected const int Year = 6;
 248        
 249		/// <summary>
 250		/// Field specification for all wildcard value '*'.
 251		/// </summary>
 252		protected const int AllSpecInt = 99; // '*'
 253        
 254		/// <summary>
 255		/// Field specification for not specified value '?'.
 256		/// </summary>
 257		protected const int NoSpecInt = 98; // '?'
 258        
 259		/// <summary>
 260		/// Field specification for wildcard '*'.
 261		/// </summary>
 262		protected const int AllSpec = AllSpecInt;
 263		
 264		/// <summary>
 265		/// Field specification for no specification at all '?'.
 266		/// </summary>
 267		protected const int NoSpec = NoSpecInt;
 268
 269        private static readonly Hashtable monthMap = new Hashtable(20);
 270        private static readonly Hashtable dayMap = new Hashtable(60);
 271
 272        private readonly string cronExpressionString = null;
 273        
 274        private TimeZone timeZone = null;
 275
 276        /// <summary>
 277        /// Seconds.
 278        /// </summary>
 279        [NonSerialized]
 280        protected TreeSet seconds;
 281        /// <summary>
 282        /// minutes.
 283        /// </summary>
 284        [NonSerialized]
 285        protected TreeSet minutes;
 286        /// <summary>
 287        /// Hours.
 288        /// </summary>
 289        [NonSerialized]
 290        protected TreeSet hours;
 291        /// <summary>
 292        /// Days of month.
 293        /// </summary>
 294        [NonSerialized]
 295        protected TreeSet daysOfMonth;
 296        /// <summary>
 297        /// Months.
 298        /// </summary>
 299        [NonSerialized]
 300        protected TreeSet months;
 301        /// <summary>
 302        /// Days of week.
 303        /// </summary>
 304        [NonSerialized]
 305        protected TreeSet daysOfWeek;
 306        /// <summary>
 307        /// Years.
 308        /// </summary>
 309        [NonSerialized]
 310        protected TreeSet years;
 311
 312        /// <summary>
 313        /// Last day of week.
 314        /// </summary>
 315        [NonSerialized]
 316        protected bool lastdayOfWeek = false;
 317        /// <summary>
 318        /// Nth day of week.
 319        /// </summary>
 320        [NonSerialized]
 321        protected int nthdayOfWeek = 0;
 322        /// <summary>
 323        /// Last day of month.
 324        /// </summary>
 325        [NonSerialized]
 326        protected bool lastdayOfMonth = false;
 327        /// <summary>
 328        /// Nearest weekday.
 329        /// </summary>
 330        [NonSerialized]
 331        protected bool nearestWeekday = false;
 332        /// <summary>
 333        /// Calendar day of week.
 334        /// </summary>
 335        [NonSerialized]
 336        protected bool calendardayOfWeek = false;
 337        /// <summary>
 338        /// Calendar day of month.
 339        /// </summary>
 340        [NonSerialized]
 341        protected bool calendardayOfMonth = false;
 342        /// <summary>
 343        /// Expression parsed.
 344        /// </summary>
 345        [NonSerialized]
 346        protected bool expressionParsed = false;
 347
 348        static CronExpression()
 349        {
 350            monthMap.Add("JAN", 0);
 351            monthMap.Add("FEB", 1);
 352            monthMap.Add("MAR", 2);
 353            monthMap.Add("APR", 3);
 354            monthMap.Add("MAY", 4);
 355            monthMap.Add("JUN", 5);
 356            monthMap.Add("JUL", 6);
 357            monthMap.Add("AUG", 7);
 358            monthMap.Add("SEP", 8);
 359            monthMap.Add("OCT", 9);
 360            monthMap.Add("NOV", 10);
 361            monthMap.Add("DEC", 11);
 362
 363            dayMap.Add("SUN", 1);
 364            dayMap.Add("MON", 2);
 365            dayMap.Add("TUE", 3);
 366            dayMap.Add("WED", 4);
 367            dayMap.Add("THU", 5);
 368            dayMap.Add("FRI", 6);
 369            dayMap.Add("SAT", 7);
 370        }
 371
 372        ///<summary>
 373        /// Constructs a new <see cref="CronExpressionString" /> based on the specified 
 374        /// parameter.
 375        /// </summary>
 376        /// <param name="cronExpression">
 377        /// String representation of the cron expression the new object should represent
 378        /// </param>
 379        /// <see cref="CronExpressionString" />
 380        public CronExpression(string cronExpression)
 381        {
 382            if (cronExpression == null)
 383            {
 384                throw new ArgumentException("cronExpression cannot be null");
 385            }
 386
 387            cronExpressionString = cronExpression.ToUpper(CultureInfo.InvariantCulture);
 388            BuildExpression(cronExpression);
 389        }
 390
 391        /// <summary>
 392        /// Indicates whether the given date satisfies the cron expression. 
 393        /// </summary>
 394        /// <remarks>
 395        /// Note that  milliseconds are ignored, so two Dates falling on different milliseconds
 396        /// of the same second will always have the same result here.
 397        /// </remarks>
 398        /// <param name="dateUtc">The date to evaluate.</param>
 399        /// <returns>a boolean indicating whether the given date satisfies the cron expression</returns>
 400        public virtual bool IsSatisfiedBy(DateTime dateUtc)
 401        {
 402            DateTime test =
 403                new DateTime(dateUtc.Year, dateUtc.Month, dateUtc.Day, dateUtc.Hour, dateUtc.Minute, dateUtc.Second).AddSeconds(-1);
 404
 405            NullableDateTime timeAfter = GetTimeAfter(test);
 406
 407            if (timeAfter.HasValue && timeAfter.Value.Equals(dateUtc))
 408            {
 409                return true;
 410            }
 411            else
 412            {
 413                return false;
 414            }
 415        }
 416
 417        /// <summary>
 418        /// Returns the next date/time <i>after</i> the given date/time which
 419        /// satisfies the cron expression.
 420        /// </summary>
 421        /// <param name="date">the date/time at which to begin the search for the next valid date/time</param>
 422        /// <returns>the next valid date/time</returns>
 423        public virtual NullableDateTime GetNextValidTimeAfter(DateTime date)
 424        {
 425            return GetTimeAfter(date);
 426        }
 427
 428        /// <summary>
 429        /// Returns the next date/time <i>after</i> the given date/time which does
 430        /// <i>not</i> satisfy the expression.
 431        /// </summary>
 432        /// <param name="date">the date/time at which to begin the search for the next invalid date/time</param>
 433        /// <returns>the next valid date/time</returns>
 434        public virtual NullableDateTime GetNextInvalidTimeAfter(DateTime date)
 435        {
 436            long difference = 1000;
 437
 438            //move back to the nearest second so differences will be accurate
 439            DateTime lastDate =
 440                new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second).AddSeconds(-1);
 441
 442            //TODO: 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.
 443
 444            //keep getting the next included time until it's farther than one second
 445            // apart. At that point, lastDate is the last valid fire time. We return
 446            // the second immediately following it.
 447            while (difference == 1000)
 448            {
 449                DateTime newDate = GetTimeAfter(lastDate).Value;
 450
 451                difference = (long) (newDate - lastDate).TotalMilliseconds;
 452
 453                if (difference == 1000)
 454                {
 455                    lastDate = newDate;
 456                }
 457            }
 458
 459            return lastDate.AddSeconds(1);
 460        }
 461
 462        /// <summary>
 463        /// Sets or gets the time zone for which the <see cref="CronExpression" /> of this
 464        /// <see cref="CronTrigger" /> will be resolved.
 465        /// </summary>
 466        public virtual TimeZone TimeZone
 467        {
 468            set { timeZone = value; }
 469            get
 470            {
 471                if (timeZone == null)
 472                {
 473                    timeZone = TimeZone.CurrentTimeZone;
 474                }
 475
 476                return timeZone;
 477            }
 478        }
 479
 480        /// <summary>
 481        /// Returns the string representation of the <see cref="CronExpression" />
 482        /// </summary>
 483        /// <returns>The string representation of the <see cref="CronExpression" /></returns>
 484        public override string ToString()
 485        {
 486            return cronExpressionString;
 487        }
 488
 489        /// <summary>
 490        /// Indicates whether the specified cron expression can be parsed into a 
 491        /// valid cron expression
 492        /// </summary>
 493        /// <param name="cronExpression">the expression to evaluate</param>
 494        /// <returns>a boolean indicating whether the given expression is a valid cron
 495        ///         expression</returns>
 496        public static bool IsValidExpression(string cronExpression)
 497        {
 498            try
 499            {
 500                new CronExpression(cronExpression);
 501            }
 502            catch (FormatException)
 503            {
 504                return false;
 505            }
 506
 507            return true;
 508        }
 509
 510        ////////////////////////////////////////////////////////////////////////////
 511        //
 512        // Expression Parsing Functions
 513        //
 514        ////////////////////////////////////////////////////////////////////////////
 515
 516		/// <summary>
 517		/// Builds the expression.
 518		/// </summary>
 519		/// <param name="expression">The expression.</param>
 520        protected void BuildExpression(string expression)
 521        {
 522            expressionParsed = true;
 523
 524            try
 525            {
 526                if (seconds == null)
 527                {
 528                    seconds = new TreeSet();
 529                }
 530                if (minutes == null)
 531                {
 532                    minutes = new TreeSet();
 533                }
 534                if (hours == null)
 535                {
 536                    hours = new TreeSet();
 537                }
 538                if (daysOfMonth == null)
 539                {
 540                    daysOfMonth = new TreeSet();
 541                }
 542                if (months == null)
 543                {
 544                    months = new TreeSet();
 545                }
 546                if (daysOfWeek == null)
 547                {
 548                    daysOfWeek = new TreeSet();
 549                }
 550                if (years == null)
 551                {
 552                    years = new TreeSet();
 553                }
 554
 555                int exprOn = Second;
 556
 557#if NET_20
 558                string[] exprsTok = expression.Trim().Split(new char[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
 559#else
 560				string[] exprsTok = expression.Trim().Split(new char[] { ' ', '\t', '\r', '\n' });
 561#endif
 562                foreach (string exprTok in exprsTok)
 563                {
 564                    string expr = exprTok.Trim();
 565
 566					if (expr.Length == 0)
 567					{
 568						continue;
 569					}
 570                    if (exprOn > Year)
 571                    {
 572                        break;
 573                    }
 574
 575                    // throw an exception if L is used with other days of the month
 576                    if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
 577                    {
 578                        throw new FormatException("Support for specifying 'L' and 'LW' with other days of the month is not implemented");
 579                    }
 580                    // throw an exception if L is used with other days of the week
 581                    if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
 582                    {
 583                        throw new FormatException("Support for specifying 'L' with other days of the week is not implemented");
 584                    }
 585
 586                    string[] vTok = expr.Split(',');
 587                    foreach (string v in vTok)
 588                    {
 589                        StoreExpressionVals(0, v, exprOn);
 590                    }
 591
 592                    exprOn++;
 593                }
 594
 595                if (exprOn <= DayOfWeek)
 596                {
 597                    throw new FormatException("Unexpected end of expression.");
 598                }
 599
 600                if (exprOn <= Year)
 601                {
 602                    StoreExpressionVals(0, "*", Year);
 603                }
 604
 605                TreeSet dow = GetSet(DayOfWeek);
 606                TreeSet dom = GetSet(DayOfMonth);
 607
 608                // Copying the logic from the UnsupportedOperationException below
 609                bool dayOfMSpec = !dom.Contains(NoSpec);
 610                bool dayOfWSpec = !dow.Contains(NoSpec);
 611
 612                if (dayOfMSpec && !dayOfWSpec)
 613                {
 614                    // skip
 615                }
 616                else if (dayOfWSpec && !dayOfMSpec)
 617                {
 618                    // skip
 619                }
 620                else
 621                {
 622                    throw new FormatException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
 623                }
 624            }
 625            catch (FormatException)
 626            {
 627                throw;
 628            }
 629            catch (Exception e)
 630            {
 631                throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Illegal cron expression format ({0})", e));
 632            }
 633        }
 634
 635		/// <summary>
 636		/// Stores the expression values.
 637		/// </summary>
 638		/// <param name="pos">The position.</param>
 639		/// <param name="s">The string to traverse.</param>
 640		/// <param name="type">The type of value.</param>
 641		/// <returns></returns>
 642        protected virtual int StoreExpressionVals(int pos, string s, int type)
 643        {
 644            int incr = 0;
 645            int i = SkipWhiteSpace(pos, s);
 646            if (i >= s.Length)
 647            {
 648                return i;
 649            }
 650            char c = s[i];
 651            if ((c >= 'A') && (c <= 'Z') && (!s.Equals("L")) && (!s.Equals("LW")))
 652            {
 653                String sub = s.Substring(i, 3);
 654                int sval;
 655                int eval = -1;
 656                if (type == Month)
 657                {
 658                    sval = GetMonthNumber(sub) + 1;
 659                    if (sval <= 0)
 660                    {
 661                        throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid Month value: '{0}'", sub));
 662                    }
 663                    if (s.Length > i + 3)
 664                    {
 665                        c = s[i + 3];
 666                        if (c == '-')
 667                        {
 668                            i += 4;
 669                            sub = s.Substring(i, 3);
 670                            eval = GetMonthNumber(sub) + 1;
 671                            if (eval <= 0)
 672                            {
 673                                throw new FormatException(
 674                                    string.Format(CultureInfo.InvariantCulture, "Invalid Month value: '{0}'", sub));
 675                            }
 676                        }
 677                    }
 678                }
 679                else if (type == DayOfWeek)
 680                {
 681                    sval = GetDayOfWeekNumber(sub);
 682                    if (sval < 0)
 683                    {
 684                        throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid Day-of-Week value: '{0}'", sub));
 685                    }
 686                    if (s.Length > i + 3)
 687                    {
 688                        c = s[i + 3];
 689                        if (c == '-')
 690                        {
 691                            i += 4;
 692                            sub = s.Substring(i, 3);
 693                            eval = GetDayOfWeekNumber(sub);
 694                            if (eval < 0)
 695                            {
 696                                throw new FormatException(
 697                                    string.Format(CultureInfo.InvariantCulture, "Invalid Day-of-Week value: '{0}'", sub));
 698                            }
 699                        }
 700                        else if (c == '#')
 701                        {
 702                            try
 703                            {
 704                                i += 4;
 705                                nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
 706                                if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
 707                                {
 708                                    throw new Exception();
 709                                }
 710                            }
 711                            catch (Exception)
 712                            {
 713                                throw new FormatException(
 714                                    "A numeric value between 1 and 5 must follow the '#' option");
 715                            }
 716                        }
 717                        else if (c == 'L')
 718                        {
 719                            lastdayOfWeek = true;
 720                            i++;
 721                        }
 722                    }
 723                }
 724                else
 725                {
 726                    throw new FormatException(
 727                        string.Format(CultureInfo.InvariantCulture, "Illegal characters for this position: '{0}'", sub));
 728                }
 729                if (eval != -1)
 730                {
 731                    incr = 1;
 732                }
 733                AddToSet(sval, eval, incr, type);
 734                return (i + 3);
 735            }
 736
 737            if (c == '?')
 738            {
 739                i++;
 740                if ((i + 1) < s.Length
 741                    && (s[i] != ' ' && s[i + 1] != '\t'))
 742                {
 743                    throw new FormatException("Illegal character after '?': "
 744                                              + s[i]);
 745                }
 746                if (type != DayOfWeek && type != DayOfMonth)
 747                {
 748                    throw new FormatException(
 749                        "'?' can only be specified for Day-of-Month or Day-of-Week.");
 750                }
 751                if (type == DayOfWeek && !lastdayOfMonth)
 752                {
 753                    int val = (int) daysOfMonth[daysOfMonth.Count - 1];
 754                    if (val == NoSpecInt)
 755                    {
 756                        throw new FormatException(
 757                            "'?' can only be specified for Day-of-Month -OR- Day-of-Week.");
 758                    }
 759                }
 760
 761                AddToSet(NoSpecInt, -1, 0, type);
 762                return i;
 763            }
 764
 765            if (c == '*' || c == '/')
 766            {
 767                if (c == '*' && (i + 1) >= s.Length)
 768                {
 769                    AddToSet(AllSpecInt, -1, incr, type);
 770                    return i + 1;
 771                }
 772                else if (c == '/'
 773                         && ((i + 1) >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t'))
 774                {
 775                    throw new FormatException("'/' must be followed by an integer.");
 776                }
 777                else if (c == '*')
 778                {
 779                    i++;
 780                }
 781                c = s[i];
 782                if (c == '/')
 783                {
 784                    // is an increment specified?
 785                    i++;
 786                    if (i >= s.Length)
 787                    {
 788                        throw new FormatException("Unexpected end of string.");
 789                    }
 790
 791                    incr = GetNumericValue(s, i);
 792
 793                    i++;
 794                    if (incr > 10)
 795                    {
 796                        i++;
 797                    }
 798                    if (incr > 59 && (type == Second || type == Minute))
 799                    {
 800                        throw new FormatException(
 801                            string.Format(CultureInfo.InvariantCulture, "Increment > 60 : {0}", incr));
 802                    }
 803                    else if (incr > 23 && (type == Hour))
 804                    {
 805                        throw new FormatException(
 806                            string.Format(CultureInfo.InvariantCulture, "Increment > 24 : {0}", incr));
 807                    }
 808                    else if (incr > 31 && (type == DayOfMonth))
 809                    {
 810                        throw new FormatException(
 811                            string.Format(CultureInfo.InvariantCulture, "Increment > 31 : {0}", incr));
 812                    }
 813                    else if (incr > 7 && (type == DayOfWeek))
 814                    {
 815                        throw new FormatException(
 816                            string.Format(CultureInfo.InvariantCulture, "Increment > 7 : {0}", incr));
 817                    }
 818                    else if (incr > 12 && (type == Month))
 819                    {
 820                        throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Increment > 12 : {0}", incr));
 821                    }
 822                }
 823                else
 824                {
 825                    incr = 1;
 826                }
 827
 828                AddToSet(AllSpecInt, -1, incr, type);
 829                return i;
 830            }
 831            else if (c == 'L')
 832            {
 833                i++;
 834                if (type == DayOfMonth)
 835                {
 836                    lastdayOfMonth = true;
 837                }
 838                if (type == DayOfWeek)
 839                {
 840                    AddToSet(7, 7, 0, type);
 841                }
 842                if (type == DayOfMonth && s.Length > i)
 843                {
 844                    c = s[i];
 845                    if (c == 'W')
 846                    {
 847                        nearestWeekday = true;
 848                        i++;
 849                    }
 850                }
 851                return i;
 852            }
 853            else if (c >= '0' && c <= '9')
 854            {
 855                int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
 856                i++;
 857                if (i >= s.Length)
 858                {
 859                    AddToSet(val, -1, -1, type);
 860                }
 861                else
 862                {
 863                    c = s[i];
 864                    if (c >= '0' && c <= '9')
 865                    {
 866                        ValueSet vs = GetValue(val, s, i);
 867                        val = vs.theValue;
 868                        i = vs.pos;
 869                    }
 870                    i = CheckNext(i, s, val, type);
 871                    return i;
 872                }
 873            }
 874            else
 875            {
 876                throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character: {0}", c));
 877            }
 878
 879            return i;
 880        }
 881
 882		/// <summary>
 883		/// Checks the next value.
 884		/// </summary>
 885		/// <param name="pos">The position.</param>
 886		/// <param name="s">The string to check.</param>
 887		/// <param name="val">The value.</param>
 888		/// <param name="type">The type to search.</param>
 889		/// <returns></returns>
 890        protected virtual int CheckNext(int pos, string s, int val, int type)
 891        {
 892            int end = -1;
 893            int i = pos;
 894
 895            if (i >= s.Length)
 896            {
 897                AddToSet(val, end, -1, type);
 898                return i;
 899            }
 900
 901            char c = s[pos];
 902
 903            if (c == 'L')
 904            {
 905                if (type == DayOfWeek)
 906                {
 907                    lastdayOfWeek = true;
 908                }
 909                else
 910                {
 911                    throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'L' option is not valid here. (pos={0})", i));
 912                }
 913                TreeSet data = GetSet(type);
 914                data.Add(val);
 915                i++;
 916                return i;
 917            }
 918
 919            if (c == 'W')
 920            {
 921                if (type == DayOfMonth)
 922                {
 923                    nearestWeekday = true;
 924                }
 925                else
 926                {
 927                    throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'W' option is not valid here. (pos={0})", i));
 928                }
 929                TreeSet data = GetSet(type);
 930                data.Add(val);
 931                i++;
 932                return i;
 933            }
 934
 935            if (c == '#')
 936            {
 937                if (type != DayOfWeek)
 938                {
 939                    throw new FormatException(
 940                        string.Format(CultureInfo.InvariantCulture, "'#' option is not valid here. (pos={0})", i));
 941                }
 942                i++;
 943                try
 944                {
 945                    nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
 946                    if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
 947                    {
 948                        throw new Exception();
 949                    }
 950                }
 951                catch (Exception)
 952                {
 953                    throw new FormatException(
 954                        "A numeric value between 1 and 5 must follow the '#' option");
 955                }
 956
 957                TreeSet data = GetSet(type);
 958                data.Add(val);
 959                i++;
 960                return i;
 961            }
 962
 963            if (c == 'C')
 964            {
 965                if (type == DayOfWeek)
 966                {
 967                    calendardayOfWeek = true;
 968                }
 969                else if (type == DayOfMonth)
 970                {
 971                    calendardayOfMonth = true;
 972                }
 973                else
 974                {
 975                    throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'C' option is not valid here. (pos={0})", i));
 976                }
 977                TreeSet data = GetSet(type);
 978                data.Add(val);
 979                i++;
 980                return i;
 981            }
 982
 983            if (c == '-')
 984            {
 985                i++;
 986                c = s[i];
 987                int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
 988                end = v;
 989                i++;
 990                if (i >= s.Length)
 991                {
 992                    AddToSet(val, end, 1, type);
 993                    return i;
 994                }
 995                c = s[i];
 996                if (c >= '0' && c <= '9')
 997                {
 998                    ValueSet vs = GetValue(v, s, i);
 999                    int v1 = vs.theValue;
1000                    end = v1;
1001                    i = vs.pos;
1002                }
1003                if (i < s.Length && ((c = s[i]) == '/'))
1004                {
1005                    i++;
1006                    c = s[i];
1007                    int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
1008                    i++;
1009                    if (i >= s.Length)
1010                    {
1011                        AddToSet(val, end, v2, type);
1012                        return i;
1013                    }
1014                    c = s[i];
1015                    if (c >= '0' && c <= '9')
1016                    {
1017                        ValueSet vs = GetValue(v2, s, i);
1018                        int v3 = vs.theValue;
1019                        AddToSet(val, end, v3, type);
1020                        i = vs.pos;
1021                        return i;
1022                    }
1023                    else
1024                    {
1025                        AddToSet(val, end, v2, type);
1026                        return i;
1027                    }
1028                }
1029                else
1030                {
1031                    AddToSet(val, end, 1, type);
1032                    return i;
1033                }
1034            }
1035
1036            if (c == '/')
1037            {
1038                i++;
1039                c = s[i];
1040                int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
1041                i++;
1042                if (i >= s.Length)
1043                {
1044                    AddToSet(val, end, v2, type);
1045                    return i;
1046                }
1047                c = s[i];
1048                if (c >= '0' && c <= '9')
1049                {
1050                    ValueSet vs = GetValue(v2, s, i);
1051                    int v3 = vs.theValue;
1052                    AddToSet(val, end, v3, type);
1053                    i = vs.pos;
1054                    return i;
1055                }
1056                else
1057                {
1058                    throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character '{0}' after '/'", c));
1059                }
1060            }
1061
1062            AddToSet(val, end, 0, type);
1063            i++;
1064            return i;
1065        }
1066
1067		/// <summary>
1068		/// Gets the cron expression string.
1069		/// </summary>
1070		/// <value>The cron expression string.</value>
1071        public string CronExpressionString
1072        {
1073            get { return cronExpressionString; }
1074        }
1075
1076		/// <summary>
1077		/// Gets the expression summary.
1078		/// </summary>
1079		/// <returns></returns>
1080        public virtual string GetExpressionSummary()
1081        {
1082            StringBuilder buf = new StringBuilder();
1083
1084            buf.Append("seconds: ");
1085            buf.Append(GetExpressionSetSummary(seconds));
1086            buf.Append("\n");
1087            buf.Append("minutes: ");
1088            buf.Append(GetExpressionSetSummary(minutes));
1089            buf.Append("\n");
1090            buf.Append("hours: ");
1091            buf.Append(GetExpressionSetSummary(hours));
1092            buf.Append("\n");
1093            buf.Append("daysOfMonth: ");
1094            buf.Append(GetExpressionSetSummary(daysOfMonth));
1095            buf.Append("\n");
1096            buf.Append("months: ");
1097            buf.Append(GetExpressionSetSummary(months));
1098            buf.Append("\n");
1099            buf.Append("daysOfWeek: ");
1100            buf.Append(GetExpressionSetSummary(daysOfWeek));
1101            buf.Append("\n");
1102            buf.Append("lastdayOfWeek: ");
1103            buf.Append(lastdayOfWeek);
1104            buf.Append("\n");
1105            buf.Append("nearestWeekday: ");
1106            buf.Append(nearestWeekday);
1107            buf.Append("\n");
1108            buf.Append("NthDayOfWeek: ");
1109            buf.Append(nthdayOfWeek);
1110            buf.Append("\n");
1111            buf.Append("lastdayOfMonth: ");
1112            buf.Append(lastdayOfMonth);
1113            buf.Append("\n");
1114            buf.Append("calendardayOfWeek: ");
1115            buf.Append(calendardayOfWeek);
1116            buf.Append("\n");
1117            buf.Append("calendardayOfMonth: ");
1118            buf.Append(calendardayOfMonth);
1119            buf.Append("\n");
1120            buf.Append("years: ");
1121            buf.Append(GetExpressionSetSummary(years));
1122            buf.Append("\n");
1123
1124            return buf.ToString();
1125        }
1126
1127		/// <summary>
1128		/// Gets the expression set summary.
1129		/// </summary>
1130		/// <param name="data">The data.</param>
1131		/// <returns></returns>
1132        protected virtual string GetExpressionSetSummary(ISet data)
1133        {
1134            if (data.Contains(NoSpec))
1135            {
1136                return "?";
1137            }
1138            if (data.Contains(AllSpec))
1139            {
1140                return "*";
1141            }
1142
1143            StringBuilder buf = new StringBuilder();
1144
1145            bool first = true;
1146            foreach (int iVal in data)
1147            {
1148                string val = iVal.ToString(CultureInfo.InvariantCulture);
1149                if (!first)
1150                {
1151                    buf.Append(",");
1152                }
1153                buf.Append(val);
1154                first = false;
1155            }
1156
1157            return buf.ToString();
1158        }
1159
1160		/// <summary>
1161		/// Skips the white space.
1162		/// </summary>
1163		/// <param name="i">The i.</param>
1164		/// <param name="s">The s.</param>
1165		/// <returns></returns>
1166        protected virtual int SkipWhiteSpace(int i, string s)
1167        {
1168            for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)
1169            {
1170                ;
1171            }
1172
1173            return i;
1174        }
1175
1176        /// <summary>
1177        /// Finds the next white space.
1178        /// </summary>
1179        /// <param name="i">The i.</param>
1180        /// <param name="s">The s.</param>
1181        /// <returns></returns>
1182        protected virtual int FindNextWhiteSpace(int i, string s)
1183        {
1184            for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)
1185            {
1186                ;
1187            }
1188
1189            return i;
1190        }
1191
1192        /// <summary>
1193        /// Adds to set.
1194        /// </summary>
1195        /// <param name="val">The val.</param>
1196        /// <param name="end">The end.</param>
1197        /// <param name="incr">The incr.</param>
1198        /// <param name="type">The type.</param>
1199        protected virtual void AddToSet(int val, int end, int incr, int type)
1200        {
1201            TreeSet data = GetSet(type);
1202
1203            if (type == Second || type == Minute)
1204            {
1205                if ((val < 0 || val > 59 || end > 59) && (val != AllSpecInt))
1206                {
1207                    throw new FormatException(
1208                        "Minute and Second values must be between 0 and 59");
1209                }
1210            }
1211            else if (type == Hour)
1212            {
1213                if ((val < 0 || val > 23 || end > 23) && (val != AllSpecInt))
1214                {
1215                    throw new FormatException(
1216                        "Hour values must be between 0 and 23");
1217                }
1218            }
1219            else if (type == DayOfMonth)
1220            {
1221                if ((val < 1 || val > 31 || end > 31) && (val != AllSpecInt)
1222                    && (val != NoSpecInt))
1223                {
1224                    throw new FormatException(
1225                        "Day of month values must be between 1 and 31");
1226                }
1227            }
1228            else if (type == Month)
1229            {
1230                if ((val < 1 || val > 12 || end > 12) && (val != AllSpecInt))
1231                {
1232                    throw new FormatException(
1233                        "Month values must be between 1 and 12");
1234                }
1235            }
1236            else if (type == DayOfWeek)
1237            {
1238                if ((val == 0 || val > 7 || end > 7) && (val != AllSpecInt)
1239                    && (val != NoSpecInt))
1240                {
1241                    throw new FormatException(
1242                        "Day-of-Week values must be between 1 and 7");
1243                }
1244            }
1245
1246            if ((incr == 0 || incr == -1) && val != AllSpecInt)
1247            {
1248                if (val != -1)
1249                {
1250                    data.Add(val);
1251                }
1252                else
1253                {
1254                    data.Add(NoSpec);
1255                }
1256                return;
1257            }
1258
1259            int startAt = val;
1260            int stopAt = end;
1261
1262            if (val == AllSpecInt && incr <= 0)
1263            {
1264                incr = 1;
1265                data.Add(AllSpec); // put in a marker, but also fill values
1266            }
1267
1268            if (type == Second || type == Minute)
1269            {
1270                if (stopAt == -1)
1271                {
1272                    stopAt = 59;
1273                }
1274                if (startAt == -1 || startAt == AllSpecInt)
1275                {
1276                    startAt = 0;
1277                }
1278            }
1279            else if (type == Hour)
1280            {
1281                if (stopAt == -1)
1282                {
1283                    stopAt = 23;
1284                }
1285                if (startAt == -1 || startAt == AllSpecInt)
1286                {
1287                    startAt = 0;
1288                }
1289            }
1290            else if (type == DayOfMonth)
1291            {
1292                if (stopAt == -1)
1293                {
1294                    stopAt = 31;
1295                }
1296                if (startAt == -1 || startAt == AllSpecInt)
1297                {
1298                    startAt = 1;
1299                }
1300            }
1301            else if (type == Month)
1302            {
1303                if (stopAt == -1)
1304                {
1305                    stopAt = 12;
1306                }
1307                if (startAt == -1 || startAt == AllSpecInt)
1308                {
1309                    startAt = 1;
1310                }
1311            }
1312            else if (type == DayOfWeek)
1313            {
1314                if (stopAt == -1)
1315                {
1316                    stopAt = 7;
1317                }
1318                if (startAt == -1 || startAt == AllSpecInt)
1319                {
1320                    startAt = 1;
1321                }
1322            }
1323            else if (type == Year)
1324            {
1325                if (stopAt == -1)
1326                {
1327                    stopAt = 2099;
1328                }
1329                if (startAt == -1 || startAt == AllSpecInt)
1330                {
1331                    startAt = 1970;
1332                }
1333            }
1334
1335            // if the end of the range is before the start, then we need to overflow into 
1336            // the next day, month etc. This is done by adding the maximum amount for that 
1337            // type, and using modulus max to determine the value being added.
1338            int max = -1;
1339            if (stopAt < startAt)
1340            {
1341                switch (type)
1342                {
1343                    case Second: max = 60; break;
1344                    case Minute: max = 60; break;
1345                    case Hour: max = 24; break;
1346                    case Month: max = 12; break;
1347                    case DayOfWeek: max = 7; break;
1348                    case DayOfMonth: max = 31; break;
1349                    case Year: throw new ArgumentException("Start year must be less than stop year");
1350                    default: throw new ArgumentException("Unexpected type encountered");
1351                }
1352                stopAt += max;
1353            }
1354
1355            for (int i = startAt; i <= stopAt; i += incr)
1356            {
1357                if (max == -1)
1358                {
1359                    // ie: there's no max to overflow over
1360                    data.Add(i);
1361                }
1362                else
1363                {
1364                    // take the modulus to get the real value
1365                    int i2 = i % max;
1366
1367                    // 1-indexed ranges should not include 0, and should include their max
1368                    if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))
1369                    {
1370                        i2 = max;
1371                    }
1372
1373                    data.Add(i2);
1374                }
1375            }
1376        }
1377
1378		/// <summary>
1379		/// Gets the set of given type.
1380		/// </summary>
1381		/// <param name="type">The type of set to get.</param>
1382		/// <returns></returns>
1383        protected virtual TreeSet GetSet(int type)
1384        {
1385            switch (type)
1386            {
1387                case Second:
1388                    return seconds;
1389                case Minute:
1390                    return minutes;
1391                case Hour:
1392                    return hours;
1393                case DayOfMonth:
1394                    return daysOfMonth;
1395                case Month:
1396                    return months;
1397                case DayOfWeek:
1398                    return daysOfWeek;

Large files files are truncated, but you can click here to view the full file