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

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