PageRenderTime 60ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/Tools/Quartz/CronExpression.cs

#
C# | 2172 lines | 1463 code | 166 blank | 543 comment | 492 complexity | 112f5758925b44a129428db490f78aa2 MD5 | raw file
Possible License(s): GPL-2.0

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

  1. #region License
  2. /*
  3. * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  6. * use this file except in compliance with the License. You may obtain a copy
  7. * of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. * License for the specific language governing permissions and limitations
  15. * under the License.
  16. *
  17. */
  18. #endregion
  19. using System;
  20. using System.Collections.Generic;
  21. using System.Globalization;
  22. using System.Runtime.Serialization;
  23. using System.Text;
  24. using System.Text.RegularExpressions;
  25. using Quartz.Collection;
  26. namespace Quartz
  27. {
  28. /// <summary>
  29. /// Provides a parser and evaluator for unix-like cron expressions. Cron
  30. /// expressions provide the ability to specify complex time combinations such as
  31. /// &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every
  32. /// last Friday of the month&quot;.
  33. /// </summary>
  34. /// <remarks>
  35. /// <para>
  36. /// Cron expressions are comprised of 6 required fields and one optional field
  37. /// separated by white space. The fields respectively are described as follows:
  38. /// </para>
  39. /// <table cellspacing="8">
  40. /// <tr>
  41. /// <th align="left">Field Name</th>
  42. /// <th align="left"> </th>
  43. /// <th align="left">Allowed Values</th>
  44. /// <th align="left"> </th>
  45. /// <th align="left">Allowed Special Characters</th>
  46. /// </tr>
  47. /// <tr>
  48. /// <td align="left">Seconds</td>
  49. /// <td align="left"> </td>
  50. /// <td align="left">0-59</td>
  51. /// <td align="left"> </td>
  52. /// <td align="left">, - /// /</td>
  53. /// </tr>
  54. /// <tr>
  55. /// <td align="left">Minutes</td>
  56. /// <td align="left"> </td>
  57. /// <td align="left">0-59</td>
  58. /// <td align="left"> </td>
  59. /// <td align="left">, - /// /</td>
  60. /// </tr>
  61. /// <tr>
  62. /// <td align="left">Hours</td>
  63. /// <td align="left"> </td>
  64. /// <td align="left">0-23</td>
  65. /// <td align="left"> </td>
  66. /// <td align="left">, - /// /</td>
  67. /// </tr>
  68. /// <tr>
  69. /// <td align="left">Day-of-month</td>
  70. /// <td align="left"> </td>
  71. /// <td align="left">1-31</td>
  72. /// <td align="left"> </td>
  73. /// <td align="left">, - /// ? / L W C</td>
  74. /// </tr>
  75. /// <tr>
  76. /// <td align="left">Month</td>
  77. /// <td align="left"> </td>
  78. /// <td align="left">1-12 or JAN-DEC</td>
  79. /// <td align="left"> </td>
  80. /// <td align="left">, - /// /</td>
  81. /// </tr>
  82. /// <tr>
  83. /// <td align="left">Day-of-Week</td>
  84. /// <td align="left"> </td>
  85. /// <td align="left">1-7 or SUN-SAT</td>
  86. /// <td align="left"> </td>
  87. /// <td align="left">, - /// ? / L #</td>
  88. /// </tr>
  89. /// <tr>
  90. /// <td align="left">Year (Optional)</td>
  91. /// <td align="left"> </td>
  92. /// <td align="left">empty, 1970-2199</td>
  93. /// <td align="left"> </td>
  94. /// <td align="left">, - /// /</td>
  95. /// </tr>
  96. /// </table>
  97. /// <para>
  98. /// The '*' character is used to specify all values. For example, &quot;*&quot;
  99. /// in the minute field means &quot;every minute&quot;.
  100. /// </para>
  101. /// <para>
  102. /// The '?' character is allowed for the day-of-month and day-of-week fields. It
  103. /// is used to specify 'no specific value'. This is useful when you need to
  104. /// specify something in one of the two fields, but not the other.
  105. /// </para>
  106. /// <para>
  107. /// The '-' character is used to specify ranges For example &quot;10-12&quot; in
  108. /// the hour field means &quot;the hours 10, 11 and 12&quot;.
  109. /// </para>
  110. /// <para>
  111. /// The ',' character is used to specify additional values. For example
  112. /// &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
  113. /// Wednesday, and Friday&quot;.
  114. /// </para>
  115. /// <para>
  116. /// The '/' character is used to specify increments. For example &quot;0/15&quot;
  117. /// in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And
  118. /// &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
  119. /// 50&quot;. Specifying '*' before the '/' is equivalent to specifying 0 is
  120. /// the value to start with. Essentially, for each field in the expression, there
  121. /// is a set of numbers that can be turned on or off. For seconds and minutes,
  122. /// the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
  123. /// 31, and for months 1 to 12. The &quot;/&quot; character simply helps you turn
  124. /// on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
  125. /// month field only turns on month &quot;7&quot;, it does NOT mean every 6th
  126. /// month, please note that subtlety.
  127. /// </para>
  128. /// <para>
  129. /// The 'L' character is allowed for the day-of-month and day-of-week fields.
  130. /// This character is short-hand for &quot;last&quot;, but it has different
  131. /// meaning in each of the two fields. For example, the value &quot;L&quot; in
  132. /// the day-of-month field means &quot;the last day of the month&quot; - day 31
  133. /// for January, day 28 for February on non-leap years. If used in the
  134. /// day-of-week field by itself, it simply means &quot;7&quot; or
  135. /// &quot;SAT&quot;. But if used in the day-of-week field after another value, it
  136. /// means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
  137. /// means &quot;the last friday of the month&quot;. You can also specify an offset
  138. /// from the last day of the month, such as "L-3" which would mean the third-to-last
  139. /// day of the calendar month. <i>When using the 'L' option, it is important not to
  140. /// specify lists, or ranges of values, as you'll get confusing/unexpected results.</i>
  141. /// </para>
  142. /// <para>
  143. /// The 'W' character is allowed for the day-of-month field. This character
  144. /// is used to specify the weekday (Monday-Friday) nearest the given day. As an
  145. /// example, if you were to specify &quot;15W&quot; as the value for the
  146. /// day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
  147. /// the month&quot;. So if the 15th is a Saturday, the trigger will fire on
  148. /// Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
  149. /// 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
  150. /// However if you specify &quot;1W&quot; as the value for day-of-month, and the
  151. /// 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
  152. /// 'jump' over the boundary of a month's days. The 'W' character can only be
  153. /// specified when the day-of-month is a single day, not a range or list of days.
  154. /// </para>
  155. /// <para>
  156. /// The 'L' and 'W' characters can also be combined for the day-of-month
  157. /// expression to yield 'LW', which translates to &quot;last weekday of the
  158. /// month&quot;.
  159. /// </para>
  160. /// <para>
  161. /// The '#' character is allowed for the day-of-week field. This character is
  162. /// used to specify &quot;the nth&quot; XXX day of the month. For example, the
  163. /// value of &quot;6#3&quot; in the day-of-week field means the third Friday of
  164. /// the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month).
  165. /// Other examples: &quot;2#1&quot; = the first Monday of the month and
  166. /// &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
  167. /// &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
  168. /// no firing will occur that month. If the '#' character is used, there can
  169. /// only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is
  170. /// not valid, since there are two expressions).
  171. /// </para>
  172. /// <para>
  173. /// <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
  174. /// This character is short-hand for "calendar". This means values are
  175. /// calculated against the associated calendar, if any. If no calendar is
  176. /// associated, then it is equivalent to having an all-inclusive calendar. A
  177. /// value of "5C" in the day-of-month field means "the first day included by the
  178. /// calendar on or after the 5th". A value of "1C" in the day-of-week field
  179. /// means "the first day included by the calendar on or after Sunday". -->
  180. /// </para>
  181. /// <para>
  182. /// The legal characters and the names of months and days of the week are not
  183. /// case sensitive.
  184. /// </para>
  185. /// <para>
  186. /// <b>NOTES:</b>
  187. /// <ul>
  188. /// <li>Support for specifying both a day-of-week and a day-of-month value is
  189. /// not complete (you'll need to use the '?' character in one of these fields).
  190. /// </li>
  191. /// <li>Overflowing ranges is supported - that is, having a larger number on
  192. /// the left hand side than the right. You might do 22-2 to catch 10 o'clock
  193. /// at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
  194. /// very important to note that overuse of overflowing ranges creates ranges
  195. /// that don't make sense and no effort has been made to determine which
  196. /// interpretation CronExpression chooses. An example would be
  197. /// "0 0 14-6 ? * FRI-MON". </li>
  198. /// </ul>
  199. /// </para>
  200. /// </remarks>
  201. /// <author>Sharada Jambula</author>
  202. /// <author>James House</author>
  203. /// <author>Contributions from Mads Henderson</author>
  204. /// <author>Refactoring from CronTrigger to CronExpression by Aaron Craven</author>
  205. /// <author>Marko Lahma (.NET)</author>
  206. [Serializable]
  207. public class CronExpression : ICloneable, IDeserializationCallback
  208. {
  209. /// <summary>
  210. /// Field specification for second.
  211. /// </summary>
  212. protected const int Second = 0;
  213. /// <summary>
  214. /// Field specification for minute.
  215. /// </summary>
  216. protected const int Minute = 1;
  217. /// <summary>
  218. /// Field specification for hour.
  219. /// </summary>
  220. protected const int Hour = 2;
  221. /// <summary>
  222. /// Field specification for day of month.
  223. /// </summary>
  224. protected const int DayOfMonth = 3;
  225. /// <summary>
  226. /// Field specification for month.
  227. /// </summary>
  228. protected const int Month = 4;
  229. /// <summary>
  230. /// Field specification for day of week.
  231. /// </summary>
  232. protected const int DayOfWeek = 5;
  233. /// <summary>
  234. /// Field specification for year.
  235. /// </summary>
  236. protected const int Year = 6;
  237. /// <summary>
  238. /// Field specification for all wildcard value '*'.
  239. /// </summary>
  240. protected const int AllSpecInt = 99; // '*'
  241. /// <summary>
  242. /// Field specification for not specified value '?'.
  243. /// </summary>
  244. protected const int NoSpecInt = 98; // '?'
  245. /// <summary>
  246. /// Field specification for wildcard '*'.
  247. /// </summary>
  248. protected const int AllSpec = AllSpecInt;
  249. /// <summary>
  250. /// Field specification for no specification at all '?'.
  251. /// </summary>
  252. protected const int NoSpec = NoSpecInt;
  253. private static readonly Dictionary<string, int> monthMap = new Dictionary<string, int>(20);
  254. private static readonly Dictionary<string, int> dayMap = new Dictionary<string, int>(60);
  255. private readonly string cronExpressionString;
  256. private TimeZoneInfo timeZone;
  257. /// <summary>
  258. /// Seconds.
  259. /// </summary>
  260. [NonSerialized]
  261. protected TreeSet<int> seconds;
  262. /// <summary>
  263. /// minutes.
  264. /// </summary>
  265. [NonSerialized]
  266. protected TreeSet<int> minutes;
  267. /// <summary>
  268. /// Hours.
  269. /// </summary>
  270. [NonSerialized]
  271. protected TreeSet<int> hours;
  272. /// <summary>
  273. /// Days of month.
  274. /// </summary>
  275. [NonSerialized]
  276. protected TreeSet<int> daysOfMonth;
  277. /// <summary>
  278. /// Months.
  279. /// </summary>
  280. [NonSerialized]
  281. protected TreeSet<int> months;
  282. /// <summary>
  283. /// Days of week.
  284. /// </summary>
  285. [NonSerialized]
  286. protected TreeSet<int> daysOfWeek;
  287. /// <summary>
  288. /// Years.
  289. /// </summary>
  290. [NonSerialized]
  291. protected TreeSet<int> years;
  292. /// <summary>
  293. /// Last day of week.
  294. /// </summary>
  295. [NonSerialized]
  296. protected bool lastdayOfWeek;
  297. /// <summary>
  298. /// Nth day of week.
  299. /// </summary>
  300. [NonSerialized]
  301. protected int nthdayOfWeek;
  302. /// <summary>
  303. /// Last day of month.
  304. /// </summary>
  305. [NonSerialized]
  306. protected bool lastdayOfMonth;
  307. /// <summary>
  308. /// Nearest weekday.
  309. /// </summary>
  310. [NonSerialized]
  311. protected bool nearestWeekday;
  312. [NonSerialized]
  313. protected int lastdayOffset = 0;
  314. /// <summary>
  315. /// Calendar day of week.
  316. /// </summary>
  317. [NonSerialized]
  318. protected bool calendardayOfWeek;
  319. /// <summary>
  320. /// Calendar day of month.
  321. /// </summary>
  322. [NonSerialized]
  323. protected bool calendardayOfMonth;
  324. /// <summary>
  325. /// Expression parsed.
  326. /// </summary>
  327. [NonSerialized]
  328. protected bool expressionParsed;
  329. public static readonly int MaxYear = DateTime.Now.Year + 100;
  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(DateTimeOffset dateUtc)
  379. {
  380. DateTimeOffset test = new DateTimeOffset(dateUtc.Year, dateUtc.Month, dateUtc.Day, dateUtc.Hour, dateUtc.Minute, dateUtc.Second, dateUtc.Offset).AddSeconds(-1);
  381. DateTimeOffset? timeAfter = GetTimeAfter(test);
  382. if (timeAfter.HasValue && timeAfter.Value.Equals(dateUtc))
  383. {
  384. return true;
  385. }
  386. return false;
  387. }
  388. /// <summary>
  389. /// Returns the next date/time <i>after</i> the given date/time which
  390. /// satisfies the cron expression.
  391. /// </summary>
  392. /// <param name="date">the date/time at which to begin the search for the next valid date/time</param>
  393. /// <returns>the next valid date/time</returns>
  394. public virtual DateTimeOffset? GetNextValidTimeAfter(DateTimeOffset date)
  395. {
  396. return GetTimeAfter(date);
  397. }
  398. /// <summary>
  399. /// Returns the next date/time <i>after</i> the given date/time which does
  400. /// <i>not</i> satisfy the expression.
  401. /// </summary>
  402. /// <param name="date">the date/time at which to begin the search for the next invalid date/time</param>
  403. /// <returns>the next valid date/time</returns>
  404. public virtual DateTimeOffset? GetNextInvalidTimeAfter(DateTimeOffset date)
  405. {
  406. long difference = 1000;
  407. //move back to the nearest second so differences will be accurate
  408. DateTimeOffset lastDate =
  409. new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Offset).AddSeconds(-1);
  410. //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.
  411. //keep getting the next included time until it's farther than one second
  412. // apart. At that point, lastDate is the last valid fire time. We return
  413. // the second immediately following it.
  414. while (difference == 1000)
  415. {
  416. DateTimeOffset? newDate = GetTimeAfter(lastDate);
  417. if (newDate == null)
  418. {
  419. break;
  420. }
  421. difference = (long) (newDate.Value - lastDate).TotalMilliseconds;
  422. if (difference == 1000)
  423. {
  424. lastDate = newDate.Value;
  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="ICronTrigger" /> will be resolved.
  432. /// </summary>
  433. public virtual TimeZoneInfo TimeZone
  434. {
  435. set { timeZone = value; }
  436. get
  437. {
  438. if (timeZone == null)
  439. {
  440. timeZone = TimeZoneInfo.Local;
  441. }
  442. return timeZone;
  443. }
  444. }
  445. /// <summary>
  446. /// Returns the string representation of the <see cref="CronExpression" />
  447. /// </summary>
  448. /// <returns>The string representation of the <see cref="CronExpression" /></returns>
  449. public override string ToString()
  450. {
  451. return cronExpressionString;
  452. }
  453. /// <summary>
  454. /// Indicates whether the specified cron expression can be parsed into a
  455. /// valid cron expression
  456. /// </summary>
  457. /// <param name="cronExpression">the expression to evaluate</param>
  458. /// <returns>a boolean indicating whether the given expression is a valid cron
  459. /// expression</returns>
  460. public static bool IsValidExpression(string cronExpression)
  461. {
  462. try
  463. {
  464. new CronExpression(cronExpression);
  465. }
  466. catch (FormatException)
  467. {
  468. return false;
  469. }
  470. return true;
  471. }
  472. public static void ValidateExpression(string cronExpression)
  473. {
  474. new CronExpression(cronExpression);
  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<int>();
  493. }
  494. if (minutes == null)
  495. {
  496. minutes = new TreeSet<int>();
  497. }
  498. if (hours == null)
  499. {
  500. hours = new TreeSet<int>();
  501. }
  502. if (daysOfMonth == null)
  503. {
  504. daysOfMonth = new TreeSet<int>();
  505. }
  506. if (months == null)
  507. {
  508. months = new TreeSet<int>();
  509. }
  510. if (daysOfWeek == null)
  511. {
  512. daysOfWeek = new TreeSet<int>();
  513. }
  514. if (years == null)
  515. {
  516. years = new TreeSet<int>();
  517. }
  518. int exprOn = Second;
  519. string[] exprsTok = expression.Trim().Split(new char[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
  520. foreach (string exprTok in exprsTok)
  521. {
  522. string expr = exprTok.Trim();
  523. if (expr.Length == 0)
  524. {
  525. continue;
  526. }
  527. if (exprOn > Year)
  528. {
  529. break;
  530. }
  531. // throw an exception if L is used with other days of the month
  532. if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
  533. {
  534. throw new FormatException("Support for specifying 'L' and 'LW' with other days of the month is not implemented");
  535. }
  536. // throw an exception if L is used with other days of the week
  537. if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
  538. {
  539. throw new FormatException("Support for specifying 'L' with other days of the week is not implemented");
  540. }
  541. if (exprOn == DayOfWeek && expr.IndexOf('#') != -1 && expr.IndexOf('#', expr.IndexOf('#') + 1) != -1)
  542. {
  543. throw new FormatException("Support for specifying multiple \"nth\" days is not imlemented.");
  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. ISortedSet<int> dow = GetSet(DayOfWeek);
  561. ISortedSet<int> 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")) && (!Regex.IsMatch(s, "^L-[0-9]*[W]?")))
  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 specified for Day-of-Month or Day-of-Week.");
  701. }
  702. if (type == DayOfWeek && !lastdayOfMonth)
  703. {
  704. int val = daysOfMonth[daysOfMonth.Count - 1];
  705. if (val == NoSpecInt)
  706. {
  707. throw new FormatException(
  708. "'?' can only be specified 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 == '-')
  792. {
  793. ValueSet vs = GetValue(0, s, i + 1);
  794. lastdayOffset = vs.theValue;
  795. if (lastdayOffset > 30)
  796. {
  797. throw new FormatException("Offset from last day must be <= 30");
  798. }
  799. i = vs.pos;
  800. }
  801. if (s.Length > i)
  802. {
  803. c = s[i];
  804. if (c == 'W')
  805. {
  806. nearestWeekday = true;
  807. i++;
  808. }
  809. }
  810. }
  811. return i;
  812. }
  813. else if (c >= '0' && c <= '9')
  814. {
  815. int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  816. i++;
  817. if (i >= s.Length)
  818. {
  819. AddToSet(val, -1, -1, type);
  820. }
  821. else
  822. {
  823. c = s[i];
  824. if (c >= '0' && c <= '9')
  825. {
  826. ValueSet vs = GetValue(val, s, i);
  827. val = vs.theValue;
  828. i = vs.pos;
  829. }
  830. i = CheckNext(i, s, val, type);
  831. return i;
  832. }
  833. }
  834. else
  835. {
  836. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character: {0}", c));
  837. }
  838. return i;
  839. }
  840. /// <summary>
  841. /// Checks the next value.
  842. /// </summary>
  843. /// <param name="pos">The position.</param>
  844. /// <param name="s">The string to check.</param>
  845. /// <param name="val">The value.</param>
  846. /// <param name="type">The type to search.</param>
  847. /// <returns></returns>
  848. protected virtual int CheckNext(int pos, string s, int val, int type)
  849. {
  850. int end = -1;
  851. int i = pos;
  852. if (i >= s.Length)
  853. {
  854. AddToSet(val, end, -1, type);
  855. return i;
  856. }
  857. char c = s[pos];
  858. if (c == 'L')
  859. {
  860. if (type == DayOfWeek)
  861. {
  862. if (val < 1 || val > 7)
  863. {
  864. throw new FormatException("Day-of-Week values must be between 1 and 7");
  865. }
  866. lastdayOfWeek = true;
  867. }
  868. else
  869. {
  870. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'L' option is not valid here. (pos={0})", i));
  871. }
  872. ISortedSet<int> data = GetSet(type);
  873. data.Add(val);
  874. i++;
  875. return i;
  876. }
  877. if (c == 'W')
  878. {
  879. if (type == DayOfMonth)
  880. {
  881. nearestWeekday = true;
  882. }
  883. else
  884. {
  885. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'W' option is not valid here. (pos={0})", i));
  886. }
  887. if (val > 31)
  888. {
  889. throw new FormatException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)");
  890. }
  891. ISortedSet<int> data = GetSet(type);
  892. data.Add(val);
  893. i++;
  894. return i;
  895. }
  896. if (c == '#')
  897. {
  898. if (type != DayOfWeek)
  899. {
  900. throw new FormatException(
  901. string.Format(CultureInfo.InvariantCulture, "'#' option is not valid here. (pos={0})", i));
  902. }
  903. i++;
  904. try
  905. {
  906. nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  907. if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
  908. {
  909. throw new Exception();
  910. }
  911. }
  912. catch (Exception)
  913. {
  914. throw new FormatException(
  915. "A numeric value between 1 and 5 must follow the '#' option");
  916. }
  917. ISortedSet<int> data = GetSet(type);
  918. data.Add(val);
  919. i++;
  920. return i;
  921. }
  922. if (c == 'C')
  923. {
  924. if (type == DayOfWeek)
  925. {
  926. calendardayOfWeek = true;
  927. }
  928. else if (type == DayOfMonth)
  929. {
  930. calendardayOfMonth = true;
  931. }
  932. else
  933. {
  934. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'C' option is not valid here. (pos={0})", i));
  935. }
  936. ISortedSet<int> data = GetSet(type);
  937. data.Add(val);
  938. i++;
  939. return i;
  940. }
  941. if (c == '-')
  942. {
  943. i++;
  944. c = s[i];
  945. int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  946. end = v;
  947. i++;
  948. if (i >= s.Length)
  949. {
  950. AddToSet(val, end, 1, type);
  951. return i;
  952. }
  953. c = s[i];
  954. if (c >= '0' && c <= '9')
  955. {
  956. ValueSet vs = GetValue(v, s, i);
  957. int v1 = vs.theValue;
  958. end = v1;
  959. i = vs.pos;
  960. }
  961. if (i < s.Length && ((c = s[i]) == '/'))
  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. AddToSet(val, end, v2, type);
  984. return i;
  985. }
  986. }
  987. else
  988. {
  989. AddToSet(val, end, 1, type);
  990. return i;
  991. }
  992. }
  993. if (c == '/')
  994. {
  995. i++;
  996. c = s[i];
  997. int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  998. i++;
  999. if (i >= s.Length)
  1000. {
  1001. AddToSet(val, end, v2, type);
  1002. return i;
  1003. }
  1004. c = s[i];
  1005. if (c >= '0' && c <= '9')
  1006. {
  1007. ValueSet vs = GetValue(v2, s, i);
  1008. int v3 = vs.theValue;
  1009. AddToSet(val, end, v3, type);
  1010. i = vs.pos;
  1011. return i;
  1012. }
  1013. else
  1014. {
  1015. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character '{0}' after '/'", c));
  1016. }
  1017. }
  1018. AddToSet(val, end, 0, type);
  1019. i++;
  1020. return i;
  1021. }
  1022. /// <summary>
  1023. /// Gets the cron expression string.
  1024. /// </summary>
  1025. /// <value>The cron expression string.</value>
  1026. public string CronExpressionString
  1027. {
  1028. get { return cronExpressionString; }
  1029. }
  1030. /// <summary>
  1031. /// Gets the expression summary.
  1032. /// </summary>
  1033. /// <returns></returns>
  1034. public virtual string GetExpressionSummary()
  1035. {
  1036. StringBuilder buf = new StringBuilder();
  1037. buf.Append("seconds: ");
  1038. buf.Append(GetExpressionSetSummary(seconds));
  1039. buf.Append("\n");
  1040. buf.Append("minutes: ");
  1041. buf.Append(GetExpressionSetSummary(minutes));
  1042. buf.Append("\n");
  1043. buf.Append("hours: ");
  1044. buf.Append(GetExpressionSetSummary(hours));
  1045. buf.Append("\n");
  1046. buf.Append("daysOfMonth: ");
  1047. buf.Append(GetExpressionSetSummary(daysOfMonth));
  1048. buf.Append("\n");
  1049. buf.Append("months: ");
  1050. buf.Append(GetExpressionSetSummary(months));
  1051. buf.Append("\n");
  1052. buf.Append("daysOfWeek: ");
  1053. buf.Append(GetExpressionSetSummary(daysOfWeek));
  1054. buf.Append("\n");
  1055. buf.Append("lastdayOfWeek: ");
  1056. buf.Append(lastdayOfWeek);
  1057. buf.Append("\n");
  1058. buf.Append("nearestWeekday: ");
  1059. buf.Append(nearestWeekday);
  1060. buf.Append("\n");
  1061. buf.Append("NthDayOfWeek: ");
  1062. buf.Append(nthdayOfWeek);
  1063. buf.Append("\n");
  1064. buf.Append("lastdayOfMonth: ");
  1065. buf.Append(lastdayOfMonth);
  1066. buf.Append("\n");
  1067. buf.Append("calendardayOfWeek: ");
  1068. buf.Append(calendardayOfWeek);
  1069. buf.Append("\n");
  1070. buf.Append("calendardayOfMonth: ");
  1071. buf.Append(calendardayOfMonth);
  1072. buf.Append("\n");
  1073. buf.Append("years: ");
  1074. buf.Append(GetExpressionSetSummary(years));
  1075. buf.Append("\n");
  1076. return buf.ToString();
  1077. }
  1078. /// <summary>
  1079. /// Gets the expression set summary.
  1080. /// </summary>
  1081. /// <param name="data">The data.</param>
  1082. /// <returns></returns>
  1083. protected virtual string GetExpressionSetSummary(Collection.ISet<int> data)
  1084. {
  1085. if (data.Contains(NoSpec))
  1086. {
  1087. return "?";
  1088. }
  1089. if (data.Contains(AllSpec))
  1090. {
  1091. return "*";
  1092. }
  1093. StringBuilder buf = new StringBuilder();
  1094. bool first = true;
  1095. foreach (int iVal in data)
  1096. {
  1097. string val = iVal.ToString(CultureInfo.InvariantCulture);
  1098. if (!first)
  1099. {
  1100. buf.Append(",");
  1101. }
  1102. buf.Append(val);
  1103. first = false;
  1104. }
  1105. return buf.ToString();
  1106. }
  1107. /// <summary>
  1108. /// Skips the white space.
  1109. /// </summary>
  1110. /// <param name="i">The i.</param>
  1111. /// <param name="s">The s.</param>
  1112. /// <returns></returns>
  1113. protected virtual int SkipWhiteSpace(int i, string s)
  1114. {
  1115. for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)
  1116. {
  1117. }
  1118. return i;
  1119. }
  1120. /// <summary>
  1121. /// Finds the next white space.
  1122. /// </summary>
  1123. /// <param name="i">The i.</param>
  1124. /// <param name="s">The s.</param>
  1125. /// <returns></returns>
  1126. protected virtual int FindNextWhiteSpace(int i, string s)
  1127. {
  1128. for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)
  1129. {
  1130. }
  1131. return i;
  1132. }
  1133. /// <summary>
  1134. /// Adds to set.
  1135. /// </summary>
  1136. /// <param name="val">The val.</param>
  1137. /// <param name="end">The end.</param>
  1138. /// <param name="incr">The incr.</param>
  1139. /// <param name="type">The type.</param>
  1140. protected virtual void AddToSet(int val, int end, int incr, int type)
  1141. {
  1142. ISortedSet<int> data = GetSet(type);
  1143. if (type == Second || type == Minute)
  1144. {
  1145. if ((val < 0 || val > 59 || end > 59) && (val != AllSpecInt))
  1146. {
  1147. throw new FormatException(
  1148. "Minute and Second values must be between 0 and 59");
  1149. }
  1150. }
  1151. else if (type == Hour)
  1152. {
  1153. if ((val < 0 || val > 23 || end > 23) && (val != AllSpecInt))
  1154. {
  1155. throw new FormatException(
  1156. "Hour values must be between 0 and 23");
  1157. }
  1158. }
  1159. else if (type == DayOfMonth)
  1160. {
  1161. if ((val < 1 || val > 31 || end > 31) && (val != AllSpecInt)
  1162. && (val != NoSpecInt))
  1163. {
  1164. throw new FormatException(
  1165. "Day of month values must be between 1 and 31");
  1166. }
  1167. }
  1168. else if (type == Month)
  1169. {
  1170. if ((val < 1 || val > 12 || end > 12) && (val != AllSpecInt))
  1171. {
  1172. throw new FormatException(
  1173. "Month values must be between 1 and 12");
  1174. }
  1175. }
  1176. else if (type == DayOfWeek)
  1177. {
  1178. if ((val == 0 || val > 7 || end > 7) && (val != AllSpecInt)
  1179. && (val != NoSpecInt))
  1180. {
  1181. throw new FormatException(
  1182. "Day-of-Week values must be between 1 and 7");
  1183. }
  1184. }
  1185. if ((incr == 0 || incr == -1) && val != AllSpecInt)
  1186. {
  1187. if (val != -1)
  1188. {
  1189. data.Add(val);
  1190. }
  1191. else
  1192. {
  1193. data.Add(NoSpec);
  1194. }
  1195. return;
  1196. }
  1197. int startAt = val;
  1198. int stopAt = end;
  1199. if (val == AllSpecInt && incr <= 0)
  1200. {
  1201. incr = 1;
  1202. data.Add(AllSpec); // put in a marker, but also fill values
  1203. }
  1204. if (type == Second || type == Minute)
  1205. {
  1206. if (stopAt == -1)
  1207. {
  1208. stopAt = 59;
  1209. }
  1210. if (startAt == -1 || startAt == AllSpecInt)
  1211. {
  1212. startAt = 0;
  1213. }
  1214. }
  1215. else if (type == Hour)
  1216. {
  1217. if (stopAt == -1)
  1218. {
  1219. stopAt = 23;
  1220. }
  1221. if (startAt == -1 || startAt == AllSpecInt)
  1222. {
  1223. startAt = 0;
  1224. }
  1225. }
  1226. else if (type == DayOfMonth)
  1227. {
  1228. if (stopAt == -1)
  1229. {
  1230. stopAt = 31;
  1231. }
  1232. if (startAt == -1 || startAt == AllSpecInt)
  1233. {
  1234. startAt = 1;
  1235. }
  1236. }
  1237. else if (type == Month)
  1238. {
  1239. if (stopAt == -1)
  1240. {
  1241. stopAt = 12;
  1242. }
  1243. if (startAt == -1 || startAt == AllSpecInt)
  1244. {
  1245. startAt = 1;
  1246. }
  1247. }
  1248. else if (type == DayOfWeek)
  1249. {
  1250. if (stopAt == -1)
  1251. {
  1252. stopAt = 7;
  1253. }
  1254. if (startAt == -1 || startAt == AllSpecInt)
  1255. {
  1256. startAt = 1;
  1257. }
  1258. }
  1259. else if (type == Year)
  1260. {
  1261. if (stopAt == -1)
  1262. {
  1263. stopAt = MaxYear;
  1264. }
  1265. if (startAt == -1 || startAt == AllSpecInt)
  1266. {
  1267. startAt = 1970;
  1268. }
  1269. }
  1270. // if the end of the range is before the start, then we need to overflow into
  1271. // the next day, month etc. This is done by adding the maximum amount for that
  1272. // type, and using modulus max to determine the value being added.
  1273. int max = -1;
  1274. if (stopAt < startAt)
  1275. {
  1276. switch (type)
  1277. {
  1278. case Second: max = 60; break;
  1279. case Minute: max = 60; break;
  1280. case Hour: max = 24; break;
  1281. case Month: max = 12; break;
  1282. case DayOfWeek: max = 7; break;
  1283. case DayOfMonth: max = 31; break;
  1284. case Year: throw new ArgumentException("Start year must be less than stop year");
  1285. default: throw new ArgumentException("Unexpected type encountered");
  1286. }
  1287. stopAt += max;
  1288. }
  1289. for (int i = startAt; i <= stopAt; i += incr)
  1290. {
  1291. if (max == -1)
  1292. {
  1293. // ie: there's no max to overflow over
  1294. data.Add(i);
  1295. }
  1296. else
  1297. {
  1298. // take the modulus to get the real value
  1299. int i2 = i % max;
  1300. // 1-indexed ranges should not include 0, and should include their max
  1301. if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))
  1302. {
  1303. i2 = max;
  1304. }
  1305. data.Add(i2);
  1306. }
  1307. }
  1308. }
  1309. /// <summary>
  1310. /// Gets the set of given…

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