PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/Libs/Quartz/src/Quartz/CronExpression.cs

#
C# | 2100 lines | 1430 code | 153 blank | 517 comment | 466 complexity | 182f4ec60313ac31562fb43c76d1b98a MD5 | raw file

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

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

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