PageRenderTime 74ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 1ms

/Cron.NET/CronExpression.cs

http://github.com/FelicePollano/Cron.NET
C# | 2097 lines | 1419 code | 157 blank | 521 comment | 467 complexity | 414a5f470e280a57b999d5ec816048fb MD5 | raw file
  1. /*
  2. * Copyright 2004-2009 James House
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy
  6. * of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations
  14. * under the License.
  15. *
  16. */
  17. using System;
  18. using System.Collections;
  19. using System.Globalization;
  20. using System.Runtime.Serialization;
  21. using System.Text;
  22. using NullableDateTime = System.Nullable<System.DateTime>;
  23. //using TimeZone = System.TimeZoneInfo;
  24. using Quartz.Collection;
  25. namespace Quartz
  26. {
  27. /// <summary>
  28. /// Provides a parser and evaluator for unix-like cron expressions. Cron
  29. /// expressions provide the ability to specify complex time combinations such as
  30. /// &quot;At 8:00am every Monday through Friday&quot; or &quot;At 1:30am every
  31. /// last Friday of the month&quot;.
  32. /// </summary>
  33. /// <remarks>
  34. /// <p>
  35. /// Cron expressions are comprised of 6 required fields and one optional field
  36. /// separated by white space. The fields respectively are described as follows:
  37. /// </p>
  38. /// <table cellspacing="8">
  39. /// <tr>
  40. /// <th align="left">Field Name</th>
  41. /// <th align="left"> </th>
  42. /// <th align="left">Allowed Values</th>
  43. /// <th align="left"> </th>
  44. /// <th align="left">Allowed Special Characters</th>
  45. /// </tr>
  46. /// <tr>
  47. /// <td align="left">Seconds</td>
  48. /// <td align="left"> </td>
  49. /// <td align="left">0-59</td>
  50. /// <td align="left"> </td>
  51. /// <td align="left">, - /// /</td>
  52. /// </tr>
  53. /// <tr>
  54. /// <td align="left">Minutes</td>
  55. /// <td align="left"> </td>
  56. /// <td align="left">0-59</td>
  57. /// <td align="left"> </td>
  58. /// <td align="left">, - /// /</td>
  59. /// </tr>
  60. /// <tr>
  61. /// <td align="left">Hours</td>
  62. /// <td align="left"> </td>
  63. /// <td align="left">0-23</td>
  64. /// <td align="left"> </td>
  65. /// <td align="left">, - /// /</td>
  66. /// </tr>
  67. /// <tr>
  68. /// <td align="left">Day-of-month</td>
  69. /// <td align="left"> </td>
  70. /// <td align="left">1-31</td>
  71. /// <td align="left"> </td>
  72. /// <td align="left">, - /// ? / L W C</td>
  73. /// </tr>
  74. /// <tr>
  75. /// <td align="left">Month</td>
  76. /// <td align="left"> </td>
  77. /// <td align="left">1-12 or JAN-DEC</td>
  78. /// <td align="left"> </td>
  79. /// <td align="left">, - /// /</td>
  80. /// </tr>
  81. /// <tr>
  82. /// <td align="left">Day-of-Week</td>
  83. /// <td align="left"> </td>
  84. /// <td align="left">1-7 or SUN-SAT</td>
  85. /// <td align="left"> </td>
  86. /// <td align="left">, - /// ? / L #</td>
  87. /// </tr>
  88. /// <tr>
  89. /// <td align="left">Year (Optional)</td>
  90. /// <td align="left"> </td>
  91. /// <td align="left">empty, 1970-2099</td>
  92. /// <td align="left"> </td>
  93. /// <td align="left">, - /// /</td>
  94. /// </tr>
  95. /// </table>
  96. /// <p>
  97. /// The '*' character is used to specify all values. For example, &quot;*&quot;
  98. /// in the minute field means &quot;every minute&quot;.
  99. /// </p>
  100. /// <p>
  101. /// The '?' character is allowed for the day-of-month and day-of-week fields. It
  102. /// is used to specify 'no specific value'. This is useful when you need to
  103. /// specify something in one of the two fields, but not the other.
  104. /// </p>
  105. /// <p>
  106. /// The '-' character is used to specify ranges For example &quot;10-12&quot; in
  107. /// the hour field means &quot;the hours 10, 11 and 12&quot;.
  108. /// </p>
  109. /// <p>
  110. /// The ',' character is used to specify additional values. For example
  111. /// &quot;MON,WED,FRI&quot; in the day-of-week field means &quot;the days Monday,
  112. /// Wednesday, and Friday&quot;.
  113. /// </p>
  114. /// <p>
  115. /// The '/' character is used to specify increments. For example &quot;0/15&quot;
  116. /// in the seconds field means &quot;the seconds 0, 15, 30, and 45&quot;. And
  117. /// &quot;5/15&quot; in the seconds field means &quot;the seconds 5, 20, 35, and
  118. /// 50&quot;. Specifying '*' before the '/' is equivalent to specifying 0 is
  119. /// the value to start with. Essentially, for each field in the expression, there
  120. /// is a set of numbers that can be turned on or off. For seconds and minutes,
  121. /// the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
  122. /// 31, and for months 1 to 12. The &quot;/&quot; character simply helps you turn
  123. /// on every &quot;nth&quot; value in the given set. Thus &quot;7/6&quot; in the
  124. /// month field only turns on month &quot;7&quot;, it does NOT mean every 6th
  125. /// month, please note that subtlety.
  126. /// </p>
  127. /// <p>
  128. /// The 'L' character is allowed for the day-of-month and day-of-week fields.
  129. /// This character is short-hand for &quot;last&quot;, but it has different
  130. /// meaning in each of the two fields. For example, the value &quot;L&quot; in
  131. /// the day-of-month field means &quot;the last day of the month&quot; - day 31
  132. /// for January, day 28 for February on non-leap years. If used in the
  133. /// day-of-week field by itself, it simply means &quot;7&quot; or
  134. /// &quot;SAT&quot;. But if used in the day-of-week field after another value, it
  135. /// means &quot;the last xxx day of the month&quot; - for example &quot;6L&quot;
  136. /// means &quot;the last friday of the month&quot;. When using the 'L' option, it
  137. /// is important not to specify lists, or ranges of values, as you'll get
  138. /// confusing results.
  139. /// </p>
  140. /// <p>
  141. /// The 'W' character is allowed for the day-of-month field. This character
  142. /// is used to specify the weekday (Monday-Friday) nearest the given day. As an
  143. /// example, if you were to specify &quot;15W&quot; as the value for the
  144. /// day-of-month field, the meaning is: &quot;the nearest weekday to the 15th of
  145. /// the month&quot;. So if the 15th is a Saturday, the trigger will fire on
  146. /// Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
  147. /// 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
  148. /// However if you specify &quot;1W&quot; as the value for day-of-month, and the
  149. /// 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
  150. /// 'jump' over the boundary of a month's days. The 'W' character can only be
  151. /// specified when the day-of-month is a single day, not a range or list of days.
  152. /// </p>
  153. /// <p>
  154. /// The 'L' and 'W' characters can also be combined for the day-of-month
  155. /// expression to yield 'LW', which translates to &quot;last weekday of the
  156. /// month&quot;.
  157. /// </p>
  158. /// <p>
  159. /// The '#' character is allowed for the day-of-week field. This character is
  160. /// used to specify &quot;the nth&quot; XXX day of the month. For example, the
  161. /// value of &quot;6#3&quot; in the day-of-week field means the third Friday of
  162. /// the month (day 6 = Friday and &quot;#3&quot; = the 3rd one in the month).
  163. /// Other examples: &quot;2#1&quot; = the first Monday of the month and
  164. /// &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify
  165. /// &quot;#5&quot; and there is not 5 of the given day-of-week in the month, then
  166. /// no firing will occur that month. If the '#' character is used, there can
  167. /// only be one expression in the day-of-week field (&quot;3#1,6#3&quot; is
  168. /// not valid, since there are two expressions).
  169. /// </p>
  170. /// <p>
  171. /// <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
  172. /// This character is short-hand for "calendar". This means values are
  173. /// calculated against the associated calendar, if any. If no calendar is
  174. /// associated, then it is equivalent to having an all-inclusive calendar. A
  175. /// value of "5C" in the day-of-month field means "the first day included by the
  176. /// calendar on or after the 5th". A value of "1C" in the day-of-week field
  177. /// means "the first day included by the calendar on or after sunday". -->
  178. /// </p>
  179. /// <p>
  180. /// The legal characters and the names of months and days of the week are not
  181. /// case sensitive.
  182. /// </p>
  183. /// <p>
  184. /// <b>NOTES:</b>
  185. /// <ul>
  186. /// <li>Support for specifying both a day-of-week and a day-of-month value is
  187. /// not complete (you'll need to use the '?' character in one of these fields).
  188. /// </li>
  189. /// <li>Overflowing ranges is supported - that is, having a larger number on
  190. /// the left hand side than the right. You might do 22-2 to catch 10 o'clock
  191. /// at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
  192. /// very important to note that overuse of overflowing ranges creates ranges
  193. /// that don't make sense and no effort has been made to determine which
  194. /// interpretation CronExpression chooses. An example would be
  195. /// "0 0 14-6 ? * FRI-MON". </li>
  196. /// </ul>
  197. /// </p>
  198. /// </remarks>
  199. /// <author>Sharada Jambula</author>
  200. /// <author>James House</author>
  201. /// <author>Contributions from Mads Henderson</author>
  202. /// <author>Refactoring from CronTrigger to CronExpression by Aaron Craven</author>
  203. [Serializable]
  204. public class CronExpression : ICloneable, IDeserializationCallback
  205. {
  206. /// <summary>
  207. /// Field specification for second.
  208. /// </summary>
  209. protected const int Second = 0;
  210. /// <summary>
  211. /// Field specification for minute.
  212. /// </summary>
  213. protected const int Minute = 1;
  214. /// <summary>
  215. /// Field specification for hour.
  216. /// </summary>
  217. protected const int Hour = 2;
  218. /// <summary>
  219. /// Field specification for day of month.
  220. /// </summary>
  221. protected const int DayOfMonth = 3;
  222. /// <summary>
  223. /// Field specification for month.
  224. /// </summary>
  225. protected const int Month = 4;
  226. /// <summary>
  227. /// Field specification for day of week.
  228. /// </summary>
  229. protected const int DayOfWeek = 5;
  230. /// <summary>
  231. /// Field specification for year.
  232. /// </summary>
  233. protected const int Year = 6;
  234. /// <summary>
  235. /// Field specification for all wildcard value '*'.
  236. /// </summary>
  237. protected const int AllSpecInt = 99; // '*'
  238. /// <summary>
  239. /// Field specification for not specified value '?'.
  240. /// </summary>
  241. protected const int NoSpecInt = 98; // '?'
  242. /// <summary>
  243. /// Field specification for wildcard '*'.
  244. /// </summary>
  245. protected const int AllSpec = AllSpecInt;
  246. /// <summary>
  247. /// Field specification for no specification at all '?'.
  248. /// </summary>
  249. protected const int NoSpec = NoSpecInt;
  250. private static readonly Hashtable monthMap = new Hashtable(20);
  251. private static readonly Hashtable dayMap = new Hashtable(60);
  252. private readonly string cronExpressionString = null;
  253. private TimeZone timeZone = null;
  254. /// <summary>
  255. /// Seconds.
  256. /// </summary>
  257. [NonSerialized]
  258. protected TreeSet seconds;
  259. /// <summary>
  260. /// minutes.
  261. /// </summary>
  262. [NonSerialized]
  263. protected TreeSet minutes;
  264. /// <summary>
  265. /// Hours.
  266. /// </summary>
  267. [NonSerialized]
  268. protected TreeSet hours;
  269. /// <summary>
  270. /// Days of month.
  271. /// </summary>
  272. [NonSerialized]
  273. protected TreeSet daysOfMonth;
  274. /// <summary>
  275. /// Months.
  276. /// </summary>
  277. [NonSerialized]
  278. protected TreeSet months;
  279. /// <summary>
  280. /// Days of week.
  281. /// </summary>
  282. [NonSerialized]
  283. protected TreeSet daysOfWeek;
  284. /// <summary>
  285. /// Years.
  286. /// </summary>
  287. [NonSerialized]
  288. protected TreeSet years;
  289. /// <summary>
  290. /// Last day of week.
  291. /// </summary>
  292. [NonSerialized]
  293. protected bool lastdayOfWeek = false;
  294. /// <summary>
  295. /// Nth day of week.
  296. /// </summary>
  297. [NonSerialized]
  298. protected int nthdayOfWeek = 0;
  299. /// <summary>
  300. /// Last day of month.
  301. /// </summary>
  302. [NonSerialized]
  303. protected bool lastdayOfMonth = false;
  304. /// <summary>
  305. /// Nearest weekday.
  306. /// </summary>
  307. [NonSerialized]
  308. protected bool nearestWeekday = false;
  309. /// <summary>
  310. /// Calendar day of week.
  311. /// </summary>
  312. [NonSerialized]
  313. protected bool calendardayOfWeek = false;
  314. /// <summary>
  315. /// Calendar day of month.
  316. /// </summary>
  317. [NonSerialized]
  318. protected bool calendardayOfMonth = false;
  319. /// <summary>
  320. /// Expression parsed.
  321. /// </summary>
  322. [NonSerialized]
  323. protected bool expressionParsed = false;
  324. static CronExpression()
  325. {
  326. monthMap.Add("JAN", 0);
  327. monthMap.Add("FEB", 1);
  328. monthMap.Add("MAR", 2);
  329. monthMap.Add("APR", 3);
  330. monthMap.Add("MAY", 4);
  331. monthMap.Add("JUN", 5);
  332. monthMap.Add("JUL", 6);
  333. monthMap.Add("AUG", 7);
  334. monthMap.Add("SEP", 8);
  335. monthMap.Add("OCT", 9);
  336. monthMap.Add("NOV", 10);
  337. monthMap.Add("DEC", 11);
  338. dayMap.Add("SUN", 1);
  339. dayMap.Add("MON", 2);
  340. dayMap.Add("TUE", 3);
  341. dayMap.Add("WED", 4);
  342. dayMap.Add("THU", 5);
  343. dayMap.Add("FRI", 6);
  344. dayMap.Add("SAT", 7);
  345. }
  346. ///<summary>
  347. /// Constructs a new <see cref="CronExpressionString" /> based on the specified
  348. /// parameter.
  349. /// </summary>
  350. /// <param name="cronExpression">
  351. /// String representation of the cron expression the new object should represent
  352. /// </param>
  353. /// <see cref="CronExpressionString" />
  354. public CronExpression(string cronExpression)
  355. {
  356. if (cronExpression == null)
  357. {
  358. throw new ArgumentException("cronExpression cannot be null");
  359. }
  360. cronExpressionString = cronExpression.ToUpper(CultureInfo.InvariantCulture);
  361. BuildExpression(cronExpression);
  362. }
  363. /// <summary>
  364. /// Indicates whether the given date satisfies the cron expression.
  365. /// </summary>
  366. /// <remarks>
  367. /// Note that milliseconds are ignored, so two Dates falling on different milliseconds
  368. /// of the same second will always have the same result here.
  369. /// </remarks>
  370. /// <param name="dateUtc">The date to evaluate.</param>
  371. /// <returns>a boolean indicating whether the given date satisfies the cron expression</returns>
  372. public virtual bool IsSatisfiedBy(DateTime dateUtc)
  373. {
  374. DateTime test =
  375. new DateTime(dateUtc.Year, dateUtc.Month, dateUtc.Day, dateUtc.Hour, dateUtc.Minute, dateUtc.Second).AddSeconds(-1);
  376. NullableDateTime timeAfter = GetTimeAfter(test);
  377. if (timeAfter.HasValue && timeAfter.Value.Equals(dateUtc))
  378. {
  379. return true;
  380. }
  381. else
  382. {
  383. return false;
  384. }
  385. }
  386. /// <summary>
  387. /// Returns the next date/time <i>after</i> the given date/time which
  388. /// satisfies the cron expression.
  389. /// </summary>
  390. /// <param name="date">the date/time at which to begin the search for the next valid date/time</param>
  391. /// <returns>the next valid date/time</returns>
  392. public virtual NullableDateTime GetNextValidTimeAfter(DateTime date)
  393. {
  394. return GetTimeAfter(date);
  395. }
  396. /// <summary>
  397. /// Returns the next date/time <i>after</i> the given date/time which does
  398. /// <i>not</i> satisfy the expression.
  399. /// </summary>
  400. /// <param name="date">the date/time at which to begin the search for the next invalid date/time</param>
  401. /// <returns>the next valid date/time</returns>
  402. public virtual NullableDateTime GetNextInvalidTimeAfter(DateTime date)
  403. {
  404. long difference = 1000;
  405. //move back to the nearest second so differences will be accurate
  406. DateTime lastDate =
  407. new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second).AddSeconds(-1);
  408. //TODO: IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
  409. //keep getting the next included time until it's farther than one second
  410. // apart. At that point, lastDate is the last valid fire time. We return
  411. // the second immediately following it.
  412. while (difference == 1000)
  413. {
  414. DateTime newDate = GetTimeAfter(lastDate).Value;
  415. difference = (long) (newDate - lastDate).TotalMilliseconds;
  416. if (difference == 1000)
  417. {
  418. lastDate = newDate;
  419. }
  420. }
  421. return lastDate.AddSeconds(1);
  422. }
  423. /// <summary>
  424. /// Sets or gets the time zone for which the <see cref="CronExpression" /> of this
  425. /// <see cref="CronTrigger" /> will be resolved.
  426. /// </summary>
  427. public virtual TimeZone TimeZone
  428. {
  429. set { timeZone = value; }
  430. get
  431. {
  432. if (timeZone == null)
  433. {
  434. timeZone = TimeZone.CurrentTimeZone;
  435. }
  436. return timeZone;
  437. }
  438. }
  439. /// <summary>
  440. /// Returns the string representation of the <see cref="CronExpression" />
  441. /// </summary>
  442. /// <returns>The string representation of the <see cref="CronExpression" /></returns>
  443. public override string ToString()
  444. {
  445. return cronExpressionString;
  446. }
  447. /// <summary>
  448. /// Indicates whether the specified cron expression can be parsed into a
  449. /// valid cron expression
  450. /// </summary>
  451. /// <param name="cronExpression">the expression to evaluate</param>
  452. /// <returns>a boolean indicating whether the given expression is a valid cron
  453. /// expression</returns>
  454. public static bool IsValidExpression(string cronExpression)
  455. {
  456. try
  457. {
  458. new CronExpression(cronExpression);
  459. }
  460. catch (FormatException)
  461. {
  462. return false;
  463. }
  464. return true;
  465. }
  466. ////////////////////////////////////////////////////////////////////////////
  467. //
  468. // Expression Parsing Functions
  469. //
  470. ////////////////////////////////////////////////////////////////////////////
  471. /// <summary>
  472. /// Builds the expression.
  473. /// </summary>
  474. /// <param name="expression">The expression.</param>
  475. protected void BuildExpression(string expression)
  476. {
  477. expressionParsed = true;
  478. try
  479. {
  480. if (seconds == null)
  481. {
  482. seconds = new TreeSet();
  483. }
  484. if (minutes == null)
  485. {
  486. minutes = new TreeSet();
  487. }
  488. if (hours == null)
  489. {
  490. hours = new TreeSet();
  491. }
  492. if (daysOfMonth == null)
  493. {
  494. daysOfMonth = new TreeSet();
  495. }
  496. if (months == null)
  497. {
  498. months = new TreeSet();
  499. }
  500. if (daysOfWeek == null)
  501. {
  502. daysOfWeek = new TreeSet();
  503. }
  504. if (years == null)
  505. {
  506. years = new TreeSet();
  507. }
  508. int exprOn = Second;
  509. #if NET_20
  510. string[] exprsTok = expression.Trim().Split(new char[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
  511. #else
  512. string[] exprsTok = expression.Trim().Split(new char[] { ' ', '\t', '\r', '\n' });
  513. #endif
  514. foreach (string exprTok in exprsTok)
  515. {
  516. string expr = exprTok.Trim();
  517. if (expr.Length == 0)
  518. {
  519. continue;
  520. }
  521. if (exprOn > Year)
  522. {
  523. break;
  524. }
  525. // throw an exception if L is used with other days of the month
  526. if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
  527. {
  528. throw new FormatException("Support for specifying 'L' and 'LW' with other days of the month is not implemented");
  529. }
  530. // throw an exception if L is used with other days of the week
  531. if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",") >= 0)
  532. {
  533. throw new FormatException("Support for specifying 'L' with other days of the week is not implemented");
  534. }
  535. string[] vTok = expr.Split(',');
  536. foreach (string v in vTok)
  537. {
  538. StoreExpressionVals(0, v, exprOn);
  539. }
  540. exprOn++;
  541. }
  542. if (exprOn <= DayOfWeek)
  543. {
  544. throw new FormatException("Unexpected end of expression.");
  545. }
  546. if (exprOn <= Year)
  547. {
  548. StoreExpressionVals(0, "*", Year);
  549. }
  550. TreeSet dow = GetSet(DayOfWeek);
  551. TreeSet dom = GetSet(DayOfMonth);
  552. // Copying the logic from the UnsupportedOperationException below
  553. bool dayOfMSpec = !dom.Contains(NoSpec);
  554. bool dayOfWSpec = !dow.Contains(NoSpec);
  555. if (dayOfMSpec && !dayOfWSpec)
  556. {
  557. // skip
  558. }
  559. else if (dayOfWSpec && !dayOfMSpec)
  560. {
  561. // skip
  562. }
  563. else
  564. {
  565. throw new FormatException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
  566. }
  567. }
  568. catch (FormatException)
  569. {
  570. throw;
  571. }
  572. catch (Exception e)
  573. {
  574. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Illegal cron expression format ({0})", e));
  575. }
  576. }
  577. /// <summary>
  578. /// Stores the expression values.
  579. /// </summary>
  580. /// <param name="pos">The position.</param>
  581. /// <param name="s">The string to traverse.</param>
  582. /// <param name="type">The type of value.</param>
  583. /// <returns></returns>
  584. protected virtual int StoreExpressionVals(int pos, string s, int type)
  585. {
  586. int incr = 0;
  587. int i = SkipWhiteSpace(pos, s);
  588. if (i >= s.Length)
  589. {
  590. return i;
  591. }
  592. char c = s[i];
  593. if ((c >= 'A') && (c <= 'Z') && (!s.Equals("L")) && (!s.Equals("LW")))
  594. {
  595. String sub = s.Substring(i, 3);
  596. int sval;
  597. int eval = -1;
  598. if (type == Month)
  599. {
  600. sval = GetMonthNumber(sub) + 1;
  601. if (sval <= 0)
  602. {
  603. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid Month value: '{0}'", sub));
  604. }
  605. if (s.Length > i + 3)
  606. {
  607. c = s[i + 3];
  608. if (c == '-')
  609. {
  610. i += 4;
  611. sub = s.Substring(i, 3);
  612. eval = GetMonthNumber(sub) + 1;
  613. if (eval <= 0)
  614. {
  615. throw new FormatException(
  616. string.Format(CultureInfo.InvariantCulture, "Invalid Month value: '{0}'", sub));
  617. }
  618. }
  619. }
  620. }
  621. else if (type == DayOfWeek)
  622. {
  623. sval = GetDayOfWeekNumber(sub);
  624. if (sval < 0)
  625. {
  626. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid Day-of-Week value: '{0}'", sub));
  627. }
  628. if (s.Length > i + 3)
  629. {
  630. c = s[i + 3];
  631. if (c == '-')
  632. {
  633. i += 4;
  634. sub = s.Substring(i, 3);
  635. eval = GetDayOfWeekNumber(sub);
  636. if (eval < 0)
  637. {
  638. throw new FormatException(
  639. string.Format(CultureInfo.InvariantCulture, "Invalid Day-of-Week value: '{0}'", sub));
  640. }
  641. }
  642. else if (c == '#')
  643. {
  644. try
  645. {
  646. i += 4;
  647. nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  648. if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
  649. {
  650. throw new Exception();
  651. }
  652. }
  653. catch (Exception)
  654. {
  655. throw new FormatException(
  656. "A numeric value between 1 and 5 must follow the '#' option");
  657. }
  658. }
  659. else if (c == 'L')
  660. {
  661. lastdayOfWeek = true;
  662. i++;
  663. }
  664. }
  665. }
  666. else
  667. {
  668. throw new FormatException(
  669. string.Format(CultureInfo.InvariantCulture, "Illegal characters for this position: '{0}'", sub));
  670. }
  671. if (eval != -1)
  672. {
  673. incr = 1;
  674. }
  675. AddToSet(sval, eval, incr, type);
  676. return (i + 3);
  677. }
  678. if (c == '?')
  679. {
  680. i++;
  681. if ((i + 1) < s.Length
  682. && (s[i] != ' ' && s[i + 1] != '\t'))
  683. {
  684. throw new FormatException("Illegal character after '?': "
  685. + s[i]);
  686. }
  687. if (type != DayOfWeek && type != DayOfMonth)
  688. {
  689. throw new FormatException(
  690. "'?' can only be specified for Day-of-Month or Day-of-Week.");
  691. }
  692. if (type == DayOfWeek && !lastdayOfMonth)
  693. {
  694. int val = (int) daysOfMonth[daysOfMonth.Count - 1];
  695. if (val == NoSpecInt)
  696. {
  697. throw new FormatException(
  698. "'?' can only be specified for Day-of-Month -OR- Day-of-Week.");
  699. }
  700. }
  701. AddToSet(NoSpecInt, -1, 0, type);
  702. return i;
  703. }
  704. if (c == '*' || c == '/')
  705. {
  706. if (c == '*' && (i + 1) >= s.Length)
  707. {
  708. AddToSet(AllSpecInt, -1, incr, type);
  709. return i + 1;
  710. }
  711. else if (c == '/'
  712. && ((i + 1) >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t'))
  713. {
  714. throw new FormatException("'/' must be followed by an integer.");
  715. }
  716. else if (c == '*')
  717. {
  718. i++;
  719. }
  720. c = s[i];
  721. if (c == '/')
  722. {
  723. // is an increment specified?
  724. i++;
  725. if (i >= s.Length)
  726. {
  727. throw new FormatException("Unexpected end of string.");
  728. }
  729. incr = GetNumericValue(s, i);
  730. i++;
  731. if (incr > 10)
  732. {
  733. i++;
  734. }
  735. if (incr > 59 && (type == Second || type == Minute))
  736. {
  737. throw new FormatException(
  738. string.Format(CultureInfo.InvariantCulture, "Increment > 60 : {0}", incr));
  739. }
  740. else if (incr > 23 && (type == Hour))
  741. {
  742. throw new FormatException(
  743. string.Format(CultureInfo.InvariantCulture, "Increment > 24 : {0}", incr));
  744. }
  745. else if (incr > 31 && (type == DayOfMonth))
  746. {
  747. throw new FormatException(
  748. string.Format(CultureInfo.InvariantCulture, "Increment > 31 : {0}", incr));
  749. }
  750. else if (incr > 7 && (type == DayOfWeek))
  751. {
  752. throw new FormatException(
  753. string.Format(CultureInfo.InvariantCulture, "Increment > 7 : {0}", incr));
  754. }
  755. else if (incr > 12 && (type == Month))
  756. {
  757. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Increment > 12 : {0}", incr));
  758. }
  759. }
  760. else
  761. {
  762. incr = 1;
  763. }
  764. AddToSet(AllSpecInt, -1, incr, type);
  765. return i;
  766. }
  767. else if (c == 'L')
  768. {
  769. i++;
  770. if (type == DayOfMonth)
  771. {
  772. lastdayOfMonth = true;
  773. }
  774. if (type == DayOfWeek)
  775. {
  776. AddToSet(7, 7, 0, type);
  777. }
  778. if (type == DayOfMonth && s.Length > i)
  779. {
  780. c = s[i];
  781. if (c == 'W')
  782. {
  783. nearestWeekday = true;
  784. i++;
  785. }
  786. }
  787. return i;
  788. }
  789. else if (c >= '0' && c <= '9')
  790. {
  791. int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  792. i++;
  793. if (i >= s.Length)
  794. {
  795. AddToSet(val, -1, -1, type);
  796. }
  797. else
  798. {
  799. c = s[i];
  800. if (c >= '0' && c <= '9')
  801. {
  802. ValueSet vs = GetValue(val, s, i);
  803. val = vs.theValue;
  804. i = vs.pos;
  805. }
  806. i = CheckNext(i, s, val, type);
  807. return i;
  808. }
  809. }
  810. else
  811. {
  812. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character: {0}", c));
  813. }
  814. return i;
  815. }
  816. /// <summary>
  817. /// Checks the next value.
  818. /// </summary>
  819. /// <param name="pos">The position.</param>
  820. /// <param name="s">The string to check.</param>
  821. /// <param name="val">The value.</param>
  822. /// <param name="type">The type to search.</param>
  823. /// <returns></returns>
  824. protected virtual int CheckNext(int pos, string s, int val, int type)
  825. {
  826. int end = -1;
  827. int i = pos;
  828. if (i >= s.Length)
  829. {
  830. AddToSet(val, end, -1, type);
  831. return i;
  832. }
  833. char c = s[pos];
  834. if (c == 'L')
  835. {
  836. if (type == DayOfWeek)
  837. {
  838. lastdayOfWeek = true;
  839. }
  840. else
  841. {
  842. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'L' option is not valid here. (pos={0})", i));
  843. }
  844. TreeSet data = GetSet(type);
  845. data.Add(val);
  846. i++;
  847. return i;
  848. }
  849. if (c == 'W')
  850. {
  851. if (type == DayOfMonth)
  852. {
  853. nearestWeekday = true;
  854. }
  855. else
  856. {
  857. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'W' option is not valid here. (pos={0})", i));
  858. }
  859. TreeSet data = GetSet(type);
  860. data.Add(val);
  861. i++;
  862. return i;
  863. }
  864. if (c == '#')
  865. {
  866. if (type != DayOfWeek)
  867. {
  868. throw new FormatException(
  869. string.Format(CultureInfo.InvariantCulture, "'#' option is not valid here. (pos={0})", i));
  870. }
  871. i++;
  872. try
  873. {
  874. nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  875. if (nthdayOfWeek < 1 || nthdayOfWeek > 5)
  876. {
  877. throw new Exception();
  878. }
  879. }
  880. catch (Exception)
  881. {
  882. throw new FormatException(
  883. "A numeric value between 1 and 5 must follow the '#' option");
  884. }
  885. TreeSet data = GetSet(type);
  886. data.Add(val);
  887. i++;
  888. return i;
  889. }
  890. if (c == 'C')
  891. {
  892. if (type == DayOfWeek)
  893. {
  894. calendardayOfWeek = true;
  895. }
  896. else if (type == DayOfMonth)
  897. {
  898. calendardayOfMonth = true;
  899. }
  900. else
  901. {
  902. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "'C' option is not valid here. (pos={0})", i));
  903. }
  904. TreeSet data = GetSet(type);
  905. data.Add(val);
  906. i++;
  907. return i;
  908. }
  909. if (c == '-')
  910. {
  911. i++;
  912. c = s[i];
  913. int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  914. end = v;
  915. i++;
  916. if (i >= s.Length)
  917. {
  918. AddToSet(val, end, 1, type);
  919. return i;
  920. }
  921. c = s[i];
  922. if (c >= '0' && c <= '9')
  923. {
  924. ValueSet vs = GetValue(v, s, i);
  925. int v1 = vs.theValue;
  926. end = v1;
  927. i = vs.pos;
  928. }
  929. if (i < s.Length && ((c = s[i]) == '/'))
  930. {
  931. i++;
  932. c = s[i];
  933. int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  934. i++;
  935. if (i >= s.Length)
  936. {
  937. AddToSet(val, end, v2, type);
  938. return i;
  939. }
  940. c = s[i];
  941. if (c >= '0' && c <= '9')
  942. {
  943. ValueSet vs = GetValue(v2, s, i);
  944. int v3 = vs.theValue;
  945. AddToSet(val, end, v3, type);
  946. i = vs.pos;
  947. return i;
  948. }
  949. else
  950. {
  951. AddToSet(val, end, v2, type);
  952. return i;
  953. }
  954. }
  955. else
  956. {
  957. AddToSet(val, end, 1, type);
  958. return i;
  959. }
  960. }
  961. if (c == '/')
  962. {
  963. i++;
  964. c = s[i];
  965. int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  966. i++;
  967. if (i >= s.Length)
  968. {
  969. AddToSet(val, end, v2, type);
  970. return i;
  971. }
  972. c = s[i];
  973. if (c >= '0' && c <= '9')
  974. {
  975. ValueSet vs = GetValue(v2, s, i);
  976. int v3 = vs.theValue;
  977. AddToSet(val, end, v3, type);
  978. i = vs.pos;
  979. return i;
  980. }
  981. else
  982. {
  983. throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Unexpected character '{0}' after '/'", c));
  984. }
  985. }
  986. AddToSet(val, end, 0, type);
  987. i++;
  988. return i;
  989. }
  990. /// <summary>
  991. /// Gets the cron expression string.
  992. /// </summary>
  993. /// <value>The cron expression string.</value>
  994. public string CronExpressionString
  995. {
  996. get { return cronExpressionString; }
  997. }
  998. /// <summary>
  999. /// Gets the expression summary.
  1000. /// </summary>
  1001. /// <returns></returns>
  1002. public virtual string GetExpressionSummary()
  1003. {
  1004. StringBuilder buf = new StringBuilder();
  1005. buf.Append("seconds: ");
  1006. buf.Append(GetExpressionSetSummary(seconds));
  1007. buf.Append("\n");
  1008. buf.Append("minutes: ");
  1009. buf.Append(GetExpressionSetSummary(minutes));
  1010. buf.Append("\n");
  1011. buf.Append("hours: ");
  1012. buf.Append(GetExpressionSetSummary(hours));
  1013. buf.Append("\n");
  1014. buf.Append("daysOfMonth: ");
  1015. buf.Append(GetExpressionSetSummary(daysOfMonth));
  1016. buf.Append("\n");
  1017. buf.Append("months: ");
  1018. buf.Append(GetExpressionSetSummary(months));
  1019. buf.Append("\n");
  1020. buf.Append("daysOfWeek: ");
  1021. buf.Append(GetExpressionSetSummary(daysOfWeek));
  1022. buf.Append("\n");
  1023. buf.Append("lastdayOfWeek: ");
  1024. buf.Append(lastdayOfWeek);
  1025. buf.Append("\n");
  1026. buf.Append("nearestWeekday: ");
  1027. buf.Append(nearestWeekday);
  1028. buf.Append("\n");
  1029. buf.Append("NthDayOfWeek: ");
  1030. buf.Append(nthdayOfWeek);
  1031. buf.Append("\n");
  1032. buf.Append("lastdayOfMonth: ");
  1033. buf.Append(lastdayOfMonth);
  1034. buf.Append("\n");
  1035. buf.Append("calendardayOfWeek: ");
  1036. buf.Append(calendardayOfWeek);
  1037. buf.Append("\n");
  1038. buf.Append("calendardayOfMonth: ");
  1039. buf.Append(calendardayOfMonth);
  1040. buf.Append("\n");
  1041. buf.Append("years: ");
  1042. buf.Append(GetExpressionSetSummary(years));
  1043. buf.Append("\n");
  1044. return buf.ToString();
  1045. }
  1046. /// <summary>
  1047. /// Gets the expression set summary.
  1048. /// </summary>
  1049. /// <param name="data">The data.</param>
  1050. /// <returns></returns>
  1051. protected virtual string GetExpressionSetSummary(ISet data)
  1052. {
  1053. if (data.Contains(NoSpec))
  1054. {
  1055. return "?";
  1056. }
  1057. if (data.Contains(AllSpec))
  1058. {
  1059. return "*";
  1060. }
  1061. StringBuilder buf = new StringBuilder();
  1062. bool first = true;
  1063. foreach (int iVal in data)
  1064. {
  1065. string val = iVal.ToString(CultureInfo.InvariantCulture);
  1066. if (!first)
  1067. {
  1068. buf.Append(",");
  1069. }
  1070. buf.Append(val);
  1071. first = false;
  1072. }
  1073. return buf.ToString();
  1074. }
  1075. /// <summary>
  1076. /// Skips the white space.
  1077. /// </summary>
  1078. /// <param name="i">The i.</param>
  1079. /// <param name="s">The s.</param>
  1080. /// <returns></returns>
  1081. protected virtual int SkipWhiteSpace(int i, string s)
  1082. {
  1083. for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)
  1084. {
  1085. ;
  1086. }
  1087. return i;
  1088. }
  1089. /// <summary>
  1090. /// Finds the next white space.
  1091. /// </summary>
  1092. /// <param name="i">The i.</param>
  1093. /// <param name="s">The s.</param>
  1094. /// <returns></returns>
  1095. protected virtual int FindNextWhiteSpace(int i, string s)
  1096. {
  1097. for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)
  1098. {
  1099. ;
  1100. }
  1101. return i;
  1102. }
  1103. /// <summary>
  1104. /// Adds to set.
  1105. /// </summary>
  1106. /// <param name="val">The val.</param>
  1107. /// <param name="end">The end.</param>
  1108. /// <param name="incr">The incr.</param>
  1109. /// <param name="type">The type.</param>
  1110. protected virtual void AddToSet(int val, int end, int incr, int type)
  1111. {
  1112. TreeSet data = GetSet(type);
  1113. if (type == Second || type == Minute)
  1114. {
  1115. if ((val < 0 || val > 59 || end > 59) && (val != AllSpecInt))
  1116. {
  1117. throw new FormatException(
  1118. "Minute and Second values must be between 0 and 59");
  1119. }
  1120. }
  1121. else if (type == Hour)
  1122. {
  1123. if ((val < 0 || val > 23 || end > 23) && (val != AllSpecInt))
  1124. {
  1125. throw new FormatException(
  1126. "Hour values must be between 0 and 23");
  1127. }
  1128. }
  1129. else if (type == DayOfMonth)
  1130. {
  1131. if ((val < 1 || val > 31 || end > 31) && (val != AllSpecInt)
  1132. && (val != NoSpecInt))
  1133. {
  1134. throw new FormatException(
  1135. "Day of month values must be between 1 and 31");
  1136. }
  1137. }
  1138. else if (type == Month)
  1139. {
  1140. if ((val < 1 || val > 12 || end > 12) && (val != AllSpecInt))
  1141. {
  1142. throw new FormatException(
  1143. "Month values must be between 1 and 12");
  1144. }
  1145. }
  1146. else if (type == DayOfWeek)
  1147. {
  1148. if ((val == 0 || val > 7 || end > 7) && (val != AllSpecInt)
  1149. && (val != NoSpecInt))
  1150. {
  1151. throw new FormatException(
  1152. "Day-of-Week values must be between 1 and 7");
  1153. }
  1154. }
  1155. if ((incr == 0 || incr == -1) && val != AllSpecInt)
  1156. {
  1157. if (val != -1)
  1158. {
  1159. data.Add(val);
  1160. }
  1161. else
  1162. {
  1163. data.Add(NoSpec);
  1164. }
  1165. return;
  1166. }
  1167. int startAt = val;
  1168. int stopAt = end;
  1169. if (val == AllSpecInt && incr <= 0)
  1170. {
  1171. incr = 1;
  1172. data.Add(AllSpec); // put in a marker, but also fill values
  1173. }
  1174. if (type == Second || type == Minute)
  1175. {
  1176. if (stopAt == -1)
  1177. {
  1178. stopAt = 59;
  1179. }
  1180. if (startAt == -1 || startAt == AllSpecInt)
  1181. {
  1182. startAt = 0;
  1183. }
  1184. }
  1185. else if (type == Hour)
  1186. {
  1187. if (stopAt == -1)
  1188. {
  1189. stopAt = 23;
  1190. }
  1191. if (startAt == -1 || startAt == AllSpecInt)
  1192. {
  1193. startAt = 0;
  1194. }
  1195. }
  1196. else if (type == DayOfMonth)
  1197. {
  1198. if (stopAt == -1)
  1199. {
  1200. stopAt = 31;
  1201. }
  1202. if (startAt == -1 || startAt == AllSpecInt)
  1203. {
  1204. startAt = 1;
  1205. }
  1206. }
  1207. else if (type == Month)
  1208. {
  1209. if (stopAt == -1)
  1210. {
  1211. stopAt = 12;
  1212. }
  1213. if (startAt == -1 || startAt == AllSpecInt)
  1214. {
  1215. startAt = 1;
  1216. }
  1217. }
  1218. else if (type == DayOfWeek)
  1219. {
  1220. if (stopAt == -1)
  1221. {
  1222. stopAt = 7;
  1223. }
  1224. if (startAt == -1 || startAt == AllSpecInt)
  1225. {
  1226. startAt = 1;
  1227. }
  1228. }
  1229. else if (type == Year)
  1230. {
  1231. if (stopAt == -1)
  1232. {
  1233. stopAt = 2099;
  1234. }
  1235. if (startAt == -1 || startAt == AllSpecInt)
  1236. {
  1237. startAt = 1970;
  1238. }
  1239. }
  1240. // if the end of the range is before the start, then we need to overflow into
  1241. // the next day, month etc. This is done by adding the maximum amount for that
  1242. // type, and using modulus max to determine the value being added.
  1243. int max = -1;
  1244. if (stopAt < startAt)
  1245. {
  1246. switch (type)
  1247. {
  1248. case Second: max = 60; break;
  1249. case Minute: max = 60; break;
  1250. case Hour: max = 24; break;
  1251. case Month: max = 12; break;
  1252. case DayOfWeek: max = 7; break;
  1253. case DayOfMonth: max = 31; break;
  1254. case Year: throw new ArgumentException("Start year must be less than stop year");
  1255. default: throw new ArgumentException("Unexpected type encountered");
  1256. }
  1257. stopAt += max;
  1258. }
  1259. for (int i = startAt; i <= stopAt; i += incr)
  1260. {
  1261. if (max == -1)
  1262. {
  1263. // ie: there's no max to overflow over
  1264. data.Add(i);
  1265. }
  1266. else
  1267. {
  1268. // take the modulus to get the real value
  1269. int i2 = i % max;
  1270. // 1-indexed ranges should not include 0, and should include their max
  1271. if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))
  1272. {
  1273. i2 = max;
  1274. }
  1275. data.Add(i2);
  1276. }
  1277. }
  1278. }
  1279. /// <summary>
  1280. /// Gets the set of given type.
  1281. /// </summary>
  1282. /// <param name="type">The type of set to get.</param>
  1283. /// <returns></returns>
  1284. protected virtual TreeSet GetSet(int type)
  1285. {
  1286. switch (type)
  1287. {
  1288. case Second:
  1289. return seconds;
  1290. case Minute:
  1291. return minutes;
  1292. case Hour:
  1293. return hours;
  1294. case DayOfMonth:
  1295. return daysOfMonth;
  1296. case Month:
  1297. return months;
  1298. case DayOfWeek:
  1299. return daysOfWeek;
  1300. case Year:
  1301. return years;
  1302. default:
  1303. return null;
  1304. }
  1305. }
  1306. /// <summary>
  1307. /// Gets the value.
  1308. /// </summary>
  1309. /// <param name="v">The v.</param>
  1310. /// <param name="s">The s.</param>
  1311. /// <param name="i">The i.</param>
  1312. /// <returns></returns>
  1313. protected virtual ValueSet GetValue(int v, string s, int i)
  1314. {
  1315. char c = s[i];
  1316. string s1 = v.ToString(CultureInfo.InvariantCulture);
  1317. while (c >= '0' && c <= '9')
  1318. {
  1319. s1 += c;
  1320. i++;
  1321. if (i >= s.Length)
  1322. {
  1323. break;
  1324. }
  1325. c = s[i];
  1326. }
  1327. ValueSet val = new ValueSet();
  1328. if (i < s.Length)
  1329. {
  1330. val.pos = i;
  1331. }
  1332. else
  1333. {
  1334. val.pos = i + 1;
  1335. }
  1336. val.theValue = Convert.ToInt32(s1, CultureInfo.InvariantCulture);
  1337. return val;
  1338. }
  1339. /// <summary>
  1340. /// Gets the numeric value from string.
  1341. /// </summary>
  1342. /// <param name="s">The string to parse from.</param>
  1343. /// <param name="i">The i.</param>
  1344. /// <returns></returns>
  1345. protected virtual int GetNumericValue(string s, int i)
  1346. {
  1347. int endOfVal = FindNextWhiteSpace(i, s);
  1348. string val = s.Substring(i, endOfVal - i);
  1349. return Convert.ToInt32(val, CultureInfo.InvariantCulture);
  1350. }
  1351. /// <summary>
  1352. /// Gets the month number.
  1353. /// </summary>
  1354. /// <param name="s">The string to map with.</param>
  1355. /// <returns></returns>
  1356. protected virtual int GetMonthNumber(string s)
  1357. {
  1358. if (monthMap.ContainsKey(s))
  1359. {
  1360. return (int) monthMap[s];
  1361. }
  1362. else
  1363. {
  1364. return -1;
  1365. }
  1366. }
  1367. /// <summary>
  1368. /// Gets the day of week number.
  1369. /// </summary>
  1370. /// <param name="s">The s.</param>
  1371. /// <returns></returns>
  1372. protected virtual int GetDayOfWeekNumber(string s)
  1373. {
  1374. if (dayMap.ContainsKey(s))
  1375. {
  1376. return (int) dayMap[s];
  1377. }
  1378. else
  1379. {
  1380. return -1;
  1381. }
  1382. }
  1383. /// <summary>
  1384. /// Gets the time from given time parts.
  1385. /// </summary>
  1386. /// <param name="sc">The seconds.</param>
  1387. /// <param name="mn">The minutes.</param>
  1388. /// <param name="hr">The hours.</param>
  1389. /// <param name="dayofmn">The day of month.</param>
  1390. /// <param name="mon">The month.</param>
  1391. /// <returns></returns>
  1392. protected virtual NullableDateTime GetTime(int sc, int mn, int hr, int dayofmn, int mon)
  1393. {
  1394. try
  1395. {
  1396. if (sc == -1)
  1397. {
  1398. sc = 0;
  1399. }
  1400. if (mn == -1)
  1401. {
  1402. mn = 0;
  1403. }
  1404. if (hr == -1)
  1405. {
  1406. hr = 0;
  1407. }
  1408. if (dayofmn == -1)
  1409. {
  1410. dayofmn = 0;
  1411. }
  1412. if (mon == -1)
  1413. {
  1414. mon = 0;
  1415. }
  1416. return new DateTime(DateTime.UtcNow.Year, mon, dayofmn, hr, mn, sc);
  1417. }
  1418. catch (Exception)
  1419. {
  1420. return null;
  1421. }
  1422. }
  1423. /// <summary>
  1424. /// Gets the next fire time after the given time.
  1425. /// </summary>
  1426. /// <param name="afterTimeUtc">The UTC time to start searching from.</param>
  1427. /// <returns></returns>
  1428. public virtual NullableDateTime GetTimeAfter(DateTime afterTimeUtc)
  1429. {
  1430. // move ahead one second, since we're computing the time *after/// the
  1431. // given time
  1432. afterTimeUtc = afterTimeUtc.AddSeconds(1);
  1433. // CronTrigger does not deal with milliseconds
  1434. DateTime d = CreateDateTimeWithoutMillis(afterTimeUtc);
  1435. // change to specified time zone
  1436. d = TimeZone.ToLocalTime(d);
  1437. bool gotOne = false;
  1438. // loop until we've computed the next time, or we've past the endTime
  1439. while (!gotOne)
  1440. {
  1441. ISortedSet st;
  1442. int t;
  1443. int sec = d.Second;
  1444. // get second.................................................
  1445. st = seconds.TailSet(sec);
  1446. if (st != null && st.Count != 0)
  1447. {
  1448. sec = (int) st.First();
  1449. }
  1450. else
  1451. {
  1452. sec = ((int) seconds.First());
  1453. d = d.AddMinutes(1);
  1454. }
  1455. d = new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond);
  1456. int min = d.Minute;
  1457. int hr = d.Hour;
  1458. t = -1;
  1459. // get minute.................................................
  1460. st = minutes.TailSet(min);
  1461. if (st != null && st.Count != 0)
  1462. {
  1463. t = min;
  1464. min = ((int) st.First());
  1465. }
  1466. else
  1467. {
  1468. min = (int) minutes.First();
  1469. hr++;
  1470. }
  1471. if (min != t)
  1472. {
  1473. d = new DateTime(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond);
  1474. d = SetCalendarHour(d, hr);
  1475. continue;
  1476. }
  1477. d = new DateTime(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond);
  1478. hr = d.Hour;
  1479. int day = d.Day;
  1480. t = -1;
  1481. // get hour...................................................
  1482. st = hours.TailSet(hr);
  1483. if (st != null && st.Count != 0)
  1484. {
  1485. t = hr;
  1486. hr = (int) st.First();
  1487. }
  1488. else
  1489. {
  1490. hr = (int) hours.First();
  1491. day++;
  1492. }
  1493. if (hr != t)
  1494. {
  1495. int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);
  1496. if (day > daysInMonth)
  1497. {
  1498. d = new DateTime(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond).AddDays(day - daysInMonth);
  1499. }
  1500. else
  1501. {
  1502. d = new DateTime(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond);
  1503. }
  1504. d = SetCalendarHour(d, hr);
  1505. continue;
  1506. }
  1507. d = new DateTime(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond);
  1508. day = d.Day;
  1509. int mon = d.Month;
  1510. t = -1;
  1511. int tmon = mon;
  1512. // get day...................................................
  1513. bool dayOfMSpec = !daysOfMonth.Contains(NoSpec);
  1514. bool dayOfWSpec = !daysOfWeek.Contains(NoSpec);
  1515. if (dayOfMSpec && !dayOfWSpec)
  1516. {
  1517. // get day by day of month rule
  1518. st = daysOfMonth.TailSet(day);
  1519. if (lastdayOfMonth)
  1520. {
  1521. if (!nearestWeekday)
  1522. {
  1523. t = day;
  1524. day = GetLastDayOfMonth(mon, d.Year);
  1525. }
  1526. else
  1527. {
  1528. t = day;
  1529. day = GetLastDayOfMonth(mon, d.Year);
  1530. DateTime tcal = new DateTime(d.Year, mon, day, 0, 0, 0);
  1531. int ldom = GetLastDayOfMonth(mon, d.Year);
  1532. DayOfWeek dow = tcal.DayOfWeek;
  1533. if (dow == System.DayOfWeek.Saturday && day == 1)
  1534. {
  1535. day += 2;
  1536. }
  1537. else if (dow == System.DayOfWeek.Saturday)
  1538. {
  1539. day -= 1;
  1540. }
  1541. else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1542. {
  1543. day -= 2;
  1544. }
  1545. else if (dow == System.DayOfWeek.Sunday)
  1546. {
  1547. day += 1;
  1548. }
  1549. DateTime nTime = new DateTime(tcal.Year, mon, day, hr, min, sec, d.Millisecond);
  1550. if (nTime.ToUniversalTime() < afterTimeUtc)
  1551. {
  1552. day = 1;
  1553. mon++;
  1554. }
  1555. }
  1556. }
  1557. else if (nearestWeekday)
  1558. {
  1559. t = day;
  1560. day = (int) daysOfMonth.First();
  1561. DateTime tcal = new DateTime(d.Year, mon, day, 0, 0, 0);
  1562. int ldom = GetLastDayOfMonth(mon, d.Year);
  1563. DayOfWeek dow = tcal.DayOfWeek;
  1564. if (dow == System.DayOfWeek.Saturday && day == 1)
  1565. {
  1566. day += 2;
  1567. }
  1568. else if (dow == System.DayOfWeek.Saturday)
  1569. {
  1570. day -= 1;
  1571. }
  1572. else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1573. {
  1574. day -= 2;
  1575. }
  1576. else if (dow == System.DayOfWeek.Sunday)
  1577. {
  1578. day += 1;
  1579. }
  1580. tcal = new DateTime(tcal.Year, mon, day, hr, min, sec);
  1581. if (tcal.ToUniversalTime() < afterTimeUtc)
  1582. {
  1583. day = ((int) daysOfMonth.First());
  1584. mon++;
  1585. }
  1586. }
  1587. else if (st != null && st.Count != 0)
  1588. {
  1589. t = day;
  1590. day = (int) st.First();
  1591. // make sure we don't over-run a short month, such as february
  1592. int lastDay = GetLastDayOfMonth(mon, d.Year);
  1593. if (day > lastDay)
  1594. {
  1595. day = (int) daysOfMonth.First();
  1596. mon++;
  1597. }
  1598. }
  1599. else
  1600. {
  1601. day = ((int) daysOfMonth.First());
  1602. mon++;
  1603. }
  1604. if (day != t || mon != tmon)
  1605. {
  1606. if (mon > 12)
  1607. {
  1608. d = new DateTime(d.Year, 12, day, 0, 0, 0).AddMonths(mon - 12);
  1609. }
  1610. else
  1611. {
  1612. // This is to avoid a bug when moving from a month
  1613. //with 30 or 31 days to a month with less. Causes an invalid datetime to be instantiated.
  1614. // ex. 0 29 0 30 1 ? 2009 with clock set to 1/30/2009
  1615. int lDay = DateTime.DaysInMonth(d.Year, mon);
  1616. if (day <= lDay)
  1617. {
  1618. d = new DateTime(d.Year, mon, day, 0, 0, 0);
  1619. }
  1620. else
  1621. {
  1622. d = new DateTime(d.Year, mon, lDay, 0, 0, 0).AddDays(day - lDay);
  1623. }
  1624. }
  1625. continue;
  1626. }
  1627. }
  1628. else if (dayOfWSpec && !dayOfMSpec)
  1629. {
  1630. // get day by day of week rule
  1631. if (lastdayOfWeek)
  1632. {
  1633. // are we looking for the last XXX day of
  1634. // the month?
  1635. int dow = ((int) daysOfWeek.First()); // desired
  1636. // d-o-w
  1637. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1638. int daysToAdd = 0;
  1639. if (cDow < dow)
  1640. {
  1641. daysToAdd = dow - cDow;
  1642. }
  1643. if (cDow > dow)
  1644. {
  1645. daysToAdd = dow + (7 - cDow);
  1646. }
  1647. int lDay = GetLastDayOfMonth(mon, d.Year);
  1648. if (day + daysToAdd > lDay)
  1649. {
  1650. // did we already miss the
  1651. // last one?
  1652. if (mon == 12)
  1653. {
  1654. //will we pass the end of the year?
  1655. d = new DateTime(d.Year, mon - 11, 1, 0, 0, 0).AddYears(1);
  1656. }
  1657. else
  1658. {
  1659. d = new DateTime(d.Year, mon + 1, 1, 0, 0, 0);
  1660. }
  1661. // we are promoting the month
  1662. continue;
  1663. }
  1664. // find date of last occurance of this day in this month...
  1665. while ((day + daysToAdd + 7) <= lDay)
  1666. {
  1667. daysToAdd += 7;
  1668. }
  1669. day += daysToAdd;
  1670. if (daysToAdd > 0)
  1671. {
  1672. d = new DateTime(d.Year, mon, day, 0, 0, 0);
  1673. // we are not promoting the month
  1674. continue;
  1675. }
  1676. }
  1677. else if (nthdayOfWeek != 0)
  1678. {
  1679. // are we looking for the Nth XXX day in the month?
  1680. int dow = ((int) daysOfWeek.First()); // desired
  1681. // d-o-w
  1682. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1683. int daysToAdd = 0;
  1684. if (cDow < dow)
  1685. {
  1686. daysToAdd = dow - cDow;
  1687. }
  1688. else if (cDow > dow)
  1689. {
  1690. daysToAdd = dow + (7 - cDow);
  1691. }
  1692. bool dayShifted = false;
  1693. if (daysToAdd > 0)
  1694. {
  1695. dayShifted = true;
  1696. }
  1697. day += daysToAdd;
  1698. int weekOfMonth = day/7;
  1699. if (day%7 > 0)
  1700. {
  1701. weekOfMonth++;
  1702. }
  1703. daysToAdd = (nthdayOfWeek - weekOfMonth)*7;
  1704. day += daysToAdd;
  1705. if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year))
  1706. {
  1707. if (mon == 12)
  1708. {
  1709. d = new DateTime(d.Year, mon - 11, 1, 0, 0, 0).AddYears(1);
  1710. }
  1711. else
  1712. {
  1713. d = new DateTime(d.Year, mon + 1, 1, 0, 0, 0);
  1714. }
  1715. // we are promoting the month
  1716. continue;
  1717. }
  1718. else if (daysToAdd > 0 || dayShifted)
  1719. {
  1720. d = new DateTime(d.Year, mon, day, 0, 0, 0);
  1721. // we are NOT promoting the month
  1722. continue;
  1723. }
  1724. }
  1725. else
  1726. {
  1727. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1728. int dow = ((int) daysOfWeek.First()); // desired
  1729. // d-o-w
  1730. st = daysOfWeek.TailSet(cDow);
  1731. if (st != null && st.Count > 0)
  1732. {
  1733. dow = ((int) st.First());
  1734. }
  1735. int daysToAdd = 0;
  1736. if (cDow < dow)
  1737. {
  1738. daysToAdd = dow - cDow;
  1739. }
  1740. if (cDow > dow)
  1741. {
  1742. daysToAdd = dow + (7 - cDow);
  1743. }
  1744. int lDay = GetLastDayOfMonth(mon, d.Year);
  1745. if (day + daysToAdd > lDay)
  1746. {
  1747. // will we pass the end of the month?
  1748. if (mon == 12)
  1749. {
  1750. //will we pass the end of the year?
  1751. d = new DateTime(d.Year, mon - 11, 1, 0, 0, 0).AddYears(1);
  1752. }
  1753. else
  1754. {
  1755. d = new DateTime(d.Year, mon + 1, 1, 0, 0, 0);
  1756. }
  1757. // we are promoting the month
  1758. continue;
  1759. }
  1760. else if (daysToAdd > 0)
  1761. {
  1762. // are we swithing days?
  1763. d = new DateTime(d.Year, mon, day + daysToAdd, 0, 0, 0);
  1764. continue;
  1765. }
  1766. }
  1767. }
  1768. else
  1769. {
  1770. // dayOfWSpec && !dayOfMSpec
  1771. throw new Exception(
  1772. "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
  1773. }
  1774. d = new DateTime(d.Year, d.Month, day, d.Hour, d.Minute, d.Second);
  1775. mon = d.Month;
  1776. int year = d.Year;
  1777. t = -1;
  1778. // test for expressions that never generate a valid fire date,
  1779. // but keep looping...
  1780. if (year > 2099)
  1781. {
  1782. return null;
  1783. }
  1784. // get month...................................................
  1785. st = months.TailSet((mon));
  1786. if (st != null && st.Count != 0)
  1787. {
  1788. t = mon;
  1789. mon = ((int) st.First());
  1790. }
  1791. else
  1792. {
  1793. mon = ((int) months.First());
  1794. year++;
  1795. }
  1796. if (mon != t)
  1797. {
  1798. d = new DateTime(year, mon, 1, 0, 0, 0);
  1799. continue;
  1800. }
  1801. d = new DateTime(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second);
  1802. year = d.Year;
  1803. t = -1;
  1804. // get year...................................................
  1805. st = years.TailSet((year));
  1806. if (st != null && st.Count != 0)
  1807. {
  1808. t = year;
  1809. year = ((int) st.First());
  1810. }
  1811. else
  1812. {
  1813. return null;
  1814. } // ran out of years...
  1815. if (year != t)
  1816. {
  1817. d = new DateTime(year, 1, 1, 0, 0, 0);
  1818. continue;
  1819. }
  1820. d = new DateTime(year, d.Month, d.Day, d.Hour, d.Minute, d.Second);
  1821. gotOne = true;
  1822. } // while( !done )
  1823. return TimeZone.ToUniversalTime(d);
  1824. }
  1825. /// <summary>
  1826. /// Creates the date time without milliseconds.
  1827. /// </summary>
  1828. /// <param name="time">The time.</param>
  1829. /// <returns></returns>
  1830. protected static DateTime CreateDateTimeWithoutMillis(DateTime time)
  1831. {
  1832. return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
  1833. }
  1834. /// <summary>
  1835. /// Advance the calendar to the particular hour paying particular attention
  1836. /// to daylight saving problems.
  1837. /// </summary>
  1838. /// <param name="date">The date.</param>
  1839. /// <param name="hour">The hour.</param>
  1840. /// <returns></returns>
  1841. protected static DateTime SetCalendarHour(DateTime date, int hour)
  1842. {
  1843. // Java version of Quartz uses lenient calendar
  1844. // so hour 24 creates day increment and zeroes hour
  1845. int hourToSet = hour;
  1846. if (hourToSet == 24)
  1847. {
  1848. hourToSet = 0;
  1849. }
  1850. DateTime d =
  1851. new DateTime(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond);
  1852. if (hour == 24)
  1853. {
  1854. // inrement day
  1855. d = d.AddDays(1);
  1856. }
  1857. return d;
  1858. }
  1859. /// <summary>
  1860. /// Gets the time before.
  1861. /// </summary>
  1862. /// <param name="endTime">The end time.</param>
  1863. /// <returns></returns>
  1864. public virtual NullableDateTime GetTimeBefore(NullableDateTime endTime)
  1865. {
  1866. // TODO: implement
  1867. return null;
  1868. }
  1869. /// <summary>
  1870. /// NOT YET IMPLEMENTED: Returns the final time that the
  1871. /// <see cref="CronExpression" /> will match.
  1872. /// </summary>
  1873. /// <returns></returns>
  1874. public virtual NullableDateTime GetFinalFireTime()
  1875. {
  1876. // TODO: implement QUARTZ-423
  1877. return null;
  1878. }
  1879. /// <summary>
  1880. /// Determines whether given year is a leap year.
  1881. /// </summary>
  1882. /// <param name="year">The year.</param>
  1883. /// <returns>
  1884. /// <c>true</c> if the specified year is a leap year; otherwise, <c>false</c>.
  1885. /// </returns>
  1886. protected virtual bool IsLeapYear(int year)
  1887. {
  1888. return DateTime.IsLeapYear(year);
  1889. }
  1890. /// <summary>
  1891. /// Gets the last day of month.
  1892. /// </summary>
  1893. /// <param name="monthNum">The month num.</param>
  1894. /// <param name="year">The year.</param>
  1895. /// <returns></returns>
  1896. protected virtual int GetLastDayOfMonth(int monthNum, int year)
  1897. {
  1898. return DateTime.DaysInMonth(year, monthNum);
  1899. }
  1900. /// <summary>
  1901. /// Creates a new object that is a copy of the current instance.
  1902. /// </summary>
  1903. /// <returns>
  1904. /// A new object that is a copy of this instance.
  1905. /// </returns>
  1906. public object Clone()
  1907. {
  1908. CronExpression copy;
  1909. try
  1910. {
  1911. copy = new CronExpression(CronExpressionString);
  1912. copy.TimeZone = TimeZone;
  1913. }
  1914. catch (FormatException)
  1915. {
  1916. // never happens since the source is valid...
  1917. throw new Exception("Not Cloneable.");
  1918. }
  1919. return copy;
  1920. }
  1921. public void OnDeserialization(object sender)
  1922. {
  1923. BuildExpression(cronExpressionString);
  1924. }
  1925. }
  1926. /// <summary>
  1927. /// Helper class for cron expression handling.
  1928. /// </summary>
  1929. public class ValueSet
  1930. {
  1931. /// <summary>
  1932. /// The value.
  1933. /// </summary>
  1934. public int theValue;
  1935. /// <summary>
  1936. /// The position.
  1937. /// </summary>
  1938. public int pos;
  1939. }
  1940. }