PageRenderTime 57ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/trunk/CS.Scheduling.Framework/TaskScheduling/CronExpression.cs

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

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