PageRenderTime 61ms 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
  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 type.
  1311. /// </summary>
  1312. /// <param name="type">The type of set to get.</param>
  1313. /// <returns></returns>
  1314. protected virtual ISortedSet<int> GetSet(int type)
  1315. {
  1316. switch (type)
  1317. {
  1318. case Second:
  1319. return seconds;
  1320. case Minute:
  1321. return minutes;
  1322. case Hour:
  1323. return hours;
  1324. case DayOfMonth:
  1325. return daysOfMonth;
  1326. case Month:
  1327. return months;
  1328. case DayOfWeek:
  1329. return daysOfWeek;
  1330. case Year:
  1331. return years;
  1332. default:
  1333. return null;
  1334. }
  1335. }
  1336. /// <summary>
  1337. /// Gets the value.
  1338. /// </summary>
  1339. /// <param name="v">The v.</param>
  1340. /// <param name="s">The s.</param>
  1341. /// <param name="i">The i.</param>
  1342. /// <returns></returns>
  1343. protected virtual ValueSet GetValue(int v, string s, int i)
  1344. {
  1345. char c = s[i];
  1346. StringBuilder s1 = new StringBuilder(v.ToString(CultureInfo.InvariantCulture));
  1347. while (c >= '0' && c <= '9')
  1348. {
  1349. s1.Append(c);
  1350. i++;
  1351. if (i >= s.Length)
  1352. {
  1353. break;
  1354. }
  1355. c = s[i];
  1356. }
  1357. ValueSet val = new ValueSet();
  1358. if (i < s.Length)
  1359. {
  1360. val.pos = i;
  1361. }
  1362. else
  1363. {
  1364. val.pos = i + 1;
  1365. }
  1366. val.theValue = Convert.ToInt32(s1.ToString(), CultureInfo.InvariantCulture);
  1367. return val;
  1368. }
  1369. /// <summary>
  1370. /// Gets the numeric value from string.
  1371. /// </summary>
  1372. /// <param name="s">The string to parse from.</param>
  1373. /// <param name="i">The i.</param>
  1374. /// <returns></returns>
  1375. protected virtual int GetNumericValue(string s, int i)
  1376. {
  1377. int endOfVal = FindNextWhiteSpace(i, s);
  1378. string val = s.Substring(i, endOfVal - i);
  1379. return Convert.ToInt32(val, CultureInfo.InvariantCulture);
  1380. }
  1381. /// <summary>
  1382. /// Gets the month number.
  1383. /// </summary>
  1384. /// <param name="s">The string to map with.</param>
  1385. /// <returns></returns>
  1386. protected virtual int GetMonthNumber(string s)
  1387. {
  1388. if (monthMap.ContainsKey(s))
  1389. {
  1390. return monthMap[s];
  1391. }
  1392. return -1;
  1393. }
  1394. /// <summary>
  1395. /// Gets the day of week number.
  1396. /// </summary>
  1397. /// <param name="s">The s.</param>
  1398. /// <returns></returns>
  1399. protected virtual int GetDayOfWeekNumber(string s)
  1400. {
  1401. if (dayMap.ContainsKey(s))
  1402. {
  1403. return dayMap[s];
  1404. }
  1405. return -1;
  1406. }
  1407. /// <summary>
  1408. /// Gets the time from given time parts.
  1409. /// </summary>
  1410. /// <param name="sc">The seconds.</param>
  1411. /// <param name="mn">The minutes.</param>
  1412. /// <param name="hr">The hours.</param>
  1413. /// <param name="dayofmn">The day of month.</param>
  1414. /// <param name="mon">The month.</param>
  1415. /// <returns></returns>
  1416. protected virtual DateTimeOffset? GetTime(int sc, int mn, int hr, int dayofmn, int mon)
  1417. {
  1418. try
  1419. {
  1420. if (sc == -1)
  1421. {
  1422. sc = 0;
  1423. }
  1424. if (mn == -1)
  1425. {
  1426. mn = 0;
  1427. }
  1428. if (hr == -1)
  1429. {
  1430. hr = 0;
  1431. }
  1432. if (dayofmn == -1)
  1433. {
  1434. dayofmn = 0;
  1435. }
  1436. if (mon == -1)
  1437. {
  1438. mon = 0;
  1439. }
  1440. return new DateTimeOffset(SystemTime.UtcNow().Year, mon, dayofmn, hr, mn, sc, TimeSpan.Zero);
  1441. }
  1442. catch (Exception)
  1443. {
  1444. return null;
  1445. }
  1446. }
  1447. /// <summary>
  1448. /// Gets the next fire time after the given time.
  1449. /// </summary>
  1450. /// <param name="afterTimeUtc">The UTC time to start searching from.</param>
  1451. /// <returns></returns>
  1452. public virtual DateTimeOffset? GetTimeAfter(DateTimeOffset afterTimeUtc)
  1453. {
  1454. // move ahead one second, since we're computing the time *after* the
  1455. // given time
  1456. afterTimeUtc = afterTimeUtc.AddSeconds(1);
  1457. // CronTrigger does not deal with milliseconds
  1458. DateTimeOffset d = CreateDateTimeWithoutMillis(afterTimeUtc);
  1459. // change to specified time zone
  1460. d = TimeZoneInfo.ConvertTime(d, TimeZone);
  1461. bool gotOne = false;
  1462. // loop until we've computed the next time, or we've past the endTime
  1463. while (!gotOne)
  1464. {
  1465. ISortedSet<int> st;
  1466. int t;
  1467. int sec = d.Second;
  1468. // get second.................................................
  1469. st = seconds.TailSet(sec);
  1470. if (st != null && st.Count != 0)
  1471. {
  1472. sec = st.First();
  1473. }
  1474. else
  1475. {
  1476. sec = seconds.First();
  1477. d = d.AddMinutes(1);
  1478. }
  1479. d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond, d.Offset);
  1480. int min = d.Minute;
  1481. int hr = d.Hour;
  1482. t = -1;
  1483. // get minute.................................................
  1484. st = minutes.TailSet(min);
  1485. if (st != null && st.Count != 0)
  1486. {
  1487. t = min;
  1488. min = st.First();
  1489. }
  1490. else
  1491. {
  1492. min = minutes.First();
  1493. hr++;
  1494. }
  1495. if (min != t)
  1496. {
  1497. d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond, d.Offset);
  1498. d = SetCalendarHour(d, hr);
  1499. continue;
  1500. }
  1501. d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond, d.Offset);
  1502. hr = d.Hour;
  1503. int day = d.Day;
  1504. t = -1;
  1505. // get hour...................................................
  1506. st = hours.TailSet(hr);
  1507. if (st != null && st.Count != 0)
  1508. {
  1509. t = hr;
  1510. hr = st.First();
  1511. }
  1512. else
  1513. {
  1514. hr = hours.First();
  1515. day++;
  1516. }
  1517. if (hr != t)
  1518. {
  1519. int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);
  1520. if (day > daysInMonth)
  1521. {
  1522. d = new DateTimeOffset(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond, d.Offset).AddDays(day - daysInMonth);
  1523. }
  1524. else
  1525. {
  1526. d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond, d.Offset);
  1527. }
  1528. d = SetCalendarHour(d, hr);
  1529. continue;
  1530. }
  1531. d = new DateTimeOffset(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond, d.Offset);
  1532. day = d.Day;
  1533. int mon = d.Month;
  1534. t = -1;
  1535. int tmon = mon;
  1536. // get day...................................................
  1537. bool dayOfMSpec = !daysOfMonth.Contains(NoSpec);
  1538. bool dayOfWSpec = !daysOfWeek.Contains(NoSpec);
  1539. if (dayOfMSpec && !dayOfWSpec)
  1540. {
  1541. // get day by day of month rule
  1542. st = daysOfMonth.TailSet(day);
  1543. if (lastdayOfMonth)
  1544. {
  1545. if (!nearestWeekday)
  1546. {
  1547. t = day;
  1548. day = GetLastDayOfMonth(mon, d.Year);
  1549. day -= lastdayOffset;
  1550. }
  1551. else
  1552. {
  1553. t = day;
  1554. day = GetLastDayOfMonth(mon, d.Year);
  1555. day -= lastdayOffset;
  1556. DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1557. int ldom = GetLastDayOfMonth(mon, d.Year);
  1558. DayOfWeek dow = tcal.DayOfWeek;
  1559. if (dow == System.DayOfWeek.Saturday && day == 1)
  1560. {
  1561. day += 2;
  1562. }
  1563. else if (dow == System.DayOfWeek.Saturday)
  1564. {
  1565. day -= 1;
  1566. }
  1567. else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1568. {
  1569. day -= 2;
  1570. }
  1571. else if (dow == System.DayOfWeek.Sunday)
  1572. {
  1573. day += 1;
  1574. }
  1575. DateTimeOffset nTime = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Millisecond, d.Offset);
  1576. if (nTime.ToUniversalTime() < afterTimeUtc)
  1577. {
  1578. day = 1;
  1579. mon++;
  1580. }
  1581. }
  1582. }
  1583. else if (nearestWeekday)
  1584. {
  1585. t = day;
  1586. day = daysOfMonth.First();
  1587. DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1588. int ldom = GetLastDayOfMonth(mon, d.Year);
  1589. DayOfWeek dow = tcal.DayOfWeek;
  1590. if (dow == System.DayOfWeek.Saturday && day == 1)
  1591. {
  1592. day += 2;
  1593. }
  1594. else if (dow == System.DayOfWeek.Saturday)
  1595. {
  1596. day -= 1;
  1597. }
  1598. else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1599. {
  1600. day -= 2;
  1601. }
  1602. else if (dow == System.DayOfWeek.Sunday)
  1603. {
  1604. day += 1;
  1605. }
  1606. tcal = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Offset);
  1607. if (tcal.ToUniversalTime() < afterTimeUtc)
  1608. {
  1609. day = daysOfMonth.First();
  1610. mon++;
  1611. }
  1612. }
  1613. else if (st != null && st.Count != 0)
  1614. {
  1615. t = day;
  1616. day = st.First();
  1617. // make sure we don't over-run a short month, such as february
  1618. int lastDay = GetLastDayOfMonth(mon, d.Year);
  1619. if (day > lastDay)
  1620. {
  1621. day = daysOfMonth.First();
  1622. mon++;
  1623. }
  1624. }
  1625. else
  1626. {
  1627. day = daysOfMonth.First();
  1628. mon++;
  1629. }
  1630. if (day != t || mon != tmon)
  1631. {
  1632. if (mon > 12)
  1633. {
  1634. d = new DateTimeOffset(d.Year, 12, day, 0, 0, 0, d.Offset).AddMonths(mon - 12);
  1635. }
  1636. else
  1637. {
  1638. // This is to avoid a bug when moving from a month
  1639. //with 30 or 31 days to a month with less. Causes an invalid datetime to be instantiated.
  1640. // ex. 0 29 0 30 1 ? 2009 with clock set to 1/30/2009
  1641. int lDay = DateTime.DaysInMonth(d.Year, mon);
  1642. if (day <= lDay)
  1643. {
  1644. d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1645. }
  1646. else
  1647. {
  1648. d = new DateTimeOffset(d.Year, mon, lDay, 0, 0, 0, d.Offset).AddDays(day - lDay);
  1649. }
  1650. }
  1651. continue;
  1652. }
  1653. }
  1654. else if (dayOfWSpec && !dayOfMSpec)
  1655. {
  1656. // get day by day of week rule
  1657. if (lastdayOfWeek)
  1658. {
  1659. // are we looking for the last XXX day of
  1660. // the month?
  1661. int dow = daysOfWeek.First(); // desired
  1662. // d-o-w
  1663. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1664. int daysToAdd = 0;
  1665. if (cDow < dow)
  1666. {
  1667. daysToAdd = dow - cDow;
  1668. }
  1669. if (cDow > dow)
  1670. {
  1671. daysToAdd = dow + (7 - cDow);
  1672. }
  1673. int lDay = GetLastDayOfMonth(mon, d.Year);
  1674. if (day + daysToAdd > lDay)
  1675. {
  1676. // did we already miss the
  1677. // last one?
  1678. if (mon == 12)
  1679. {
  1680. //will we pass the end of the year?
  1681. d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1682. }
  1683. else
  1684. {
  1685. d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1686. }
  1687. // we are promoting the month
  1688. continue;
  1689. }
  1690. // find date of last occurrence of this day in this month...
  1691. while ((day + daysToAdd + 7) <= lDay)
  1692. {
  1693. daysToAdd += 7;
  1694. }
  1695. day += daysToAdd;
  1696. if (daysToAdd > 0)
  1697. {
  1698. d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1699. // we are not promoting the month
  1700. continue;
  1701. }
  1702. }
  1703. else if (nthdayOfWeek != 0)
  1704. {
  1705. // are we looking for the Nth XXX day in the month?
  1706. int dow = daysOfWeek.First(); // desired
  1707. // d-o-w
  1708. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1709. int daysToAdd = 0;
  1710. if (cDow < dow)
  1711. {
  1712. daysToAdd = dow - cDow;
  1713. }
  1714. else if (cDow > dow)
  1715. {
  1716. daysToAdd = dow + (7 - cDow);
  1717. }
  1718. bool dayShifted = false;
  1719. if (daysToAdd > 0)
  1720. {
  1721. dayShifted = true;
  1722. }
  1723. day += daysToAdd;
  1724. int weekOfMonth = day/7;
  1725. if (day%7 > 0)
  1726. {
  1727. weekOfMonth++;
  1728. }
  1729. daysToAdd = (nthdayOfWeek - weekOfMonth)*7;
  1730. day += daysToAdd;
  1731. if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year))
  1732. {
  1733. if (mon == 12)
  1734. {
  1735. d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1736. }
  1737. else
  1738. {
  1739. d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1740. }
  1741. // we are promoting the month
  1742. continue;
  1743. }
  1744. else if (daysToAdd > 0 || dayShifted)
  1745. {
  1746. d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1747. // we are NOT promoting the month
  1748. continue;
  1749. }
  1750. }
  1751. else
  1752. {
  1753. int cDow = ((int) d.DayOfWeek) + 1; // current d-o-w
  1754. int dow = daysOfWeek.First(); // desired
  1755. // d-o-w
  1756. st = daysOfWeek.TailSet(cDow);
  1757. if (st != null && st.Count > 0)
  1758. {
  1759. dow = st.First();
  1760. }
  1761. int daysToAdd = 0;
  1762. if (cDow < dow)
  1763. {
  1764. daysToAdd = dow - cDow;
  1765. }
  1766. if (cDow > dow)
  1767. {
  1768. daysToAdd = dow + (7 - cDow);
  1769. }
  1770. int lDay = GetLastDayOfMonth(mon, d.Year);
  1771. if (day + daysToAdd > lDay)
  1772. {
  1773. // will we pass the end of the month?
  1774. if (mon == 12)
  1775. {
  1776. //will we pass the end of the year?
  1777. d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1778. }
  1779. else
  1780. {
  1781. d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1782. }
  1783. // we are promoting the month
  1784. continue;
  1785. }
  1786. else if (daysToAdd > 0)
  1787. {
  1788. // are we swithing days?
  1789. d = new DateTimeOffset(d.Year, mon, day + daysToAdd, 0, 0, 0, d.Offset);
  1790. continue;
  1791. }
  1792. }
  1793. }
  1794. else
  1795. {
  1796. // dayOfWSpec && !dayOfMSpec
  1797. throw new Exception(
  1798. "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
  1799. }
  1800. d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, d.Minute, d.Second, d.Offset);
  1801. mon = d.Month;
  1802. int year = d.Year;
  1803. t = -1;
  1804. // test for expressions that never generate a valid fire date,
  1805. // but keep looping...
  1806. if (year > MaxYear)
  1807. {
  1808. return null;
  1809. }
  1810. // get month...................................................
  1811. st = months.TailSet((mon));
  1812. if (st != null && st.Count != 0)
  1813. {
  1814. t = mon;
  1815. mon = st.First();
  1816. }
  1817. else
  1818. {
  1819. mon = months.First();
  1820. year++;
  1821. }
  1822. if (mon != t)
  1823. {
  1824. d = new DateTimeOffset(year, mon, 1, 0, 0, 0, d.Offset);
  1825. continue;
  1826. }
  1827. d = new DateTimeOffset(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second, d.Offset);
  1828. year = d.Year;
  1829. t = -1;
  1830. // get year...................................................
  1831. st = years.TailSet((year));
  1832. if (st != null && st.Count != 0)
  1833. {
  1834. t = year;
  1835. year = st.First();
  1836. }
  1837. else
  1838. {
  1839. return null;
  1840. } // ran out of years...
  1841. if (year != t)
  1842. {
  1843. d = new DateTimeOffset(year, 1, 1, 0, 0, 0, d.Offset);
  1844. continue;
  1845. }
  1846. d = new DateTimeOffset(year, d.Month, d.Day, d.Hour, d.Minute, d.Second, d.Offset);
  1847. gotOne = true;
  1848. } // while( !done )
  1849. return d.ToUniversalTime();
  1850. }
  1851. /// <summary>
  1852. /// Creates the date time without milliseconds.
  1853. /// </summary>
  1854. /// <param name="time">The time.</param>
  1855. /// <returns></returns>
  1856. protected static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time)
  1857. {
  1858. return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset);
  1859. }
  1860. /// <summary>
  1861. /// Advance the calendar to the particular hour paying particular attention
  1862. /// to daylight saving problems.
  1863. /// </summary>
  1864. /// <param name="date">The date.</param>
  1865. /// <param name="hour">The hour.</param>
  1866. /// <returns></returns>
  1867. protected static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour)
  1868. {
  1869. // Java version of Quartz uses lenient calendar
  1870. // so hour 24 creates day increment and zeroes hour
  1871. int hourToSet = hour;
  1872. if (hourToSet == 24)
  1873. {
  1874. hourToSet = 0;
  1875. }
  1876. DateTimeOffset d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset);
  1877. if (hour == 24)
  1878. {
  1879. // inrement day
  1880. d = d.AddDays(1);
  1881. }
  1882. return d;
  1883. }
  1884. /// <summary>
  1885. /// Gets the time before.
  1886. /// </summary>
  1887. /// <param name="endTime">The end time.</param>
  1888. /// <returns></returns>
  1889. public virtual DateTimeOffset? GetTimeBefore(DateTimeOffset? endTime)
  1890. {
  1891. // TODO: implement
  1892. return null;
  1893. }
  1894. /// <summary>
  1895. /// NOT YET IMPLEMENTED: Returns the final time that the
  1896. /// <see cref="CronExpression" /> will match.
  1897. /// </summary>
  1898. /// <returns></returns>
  1899. public virtual DateTimeOffset? GetFinalFireTime()
  1900. {
  1901. // TODO: implement QUARTZ-423
  1902. return null;
  1903. }
  1904. /// <summary>
  1905. /// Determines whether given year is a leap year.
  1906. /// </summary>
  1907. /// <param name="year">The year.</param>
  1908. /// <returns>
  1909. /// <c>true</c> if the specified year is a leap year; otherwise, <c>false</c>.
  1910. /// </returns>
  1911. protected virtual bool IsLeapYear(int year)
  1912. {
  1913. return DateTime.IsLeapYear(year);
  1914. }
  1915. /// <summary>
  1916. /// Gets the last day of month.
  1917. /// </summary>
  1918. /// <param name="monthNum">The month num.</param>
  1919. /// <param name="year">The year.</param>
  1920. /// <returns></returns>
  1921. protected virtual int GetLastDayOfMonth(int monthNum, int year)
  1922. {
  1923. return DateTime.DaysInMonth(year, monthNum);
  1924. }
  1925. /// <summary>
  1926. /// Creates a new object that is a copy of the current instance.
  1927. /// </summary>
  1928. /// <returns>
  1929. /// A new object that is a copy of this instance.
  1930. /// </returns>
  1931. public object Clone()
  1932. {
  1933. CronExpression copy;
  1934. try
  1935. {
  1936. copy = new CronExpression(CronExpressionString);
  1937. copy.TimeZone = TimeZone;
  1938. }
  1939. catch (FormatException)
  1940. {
  1941. // never happens since the source is valid...
  1942. throw new Exception("Not Cloneable.");
  1943. }
  1944. return copy;
  1945. }
  1946. public void OnDeserialization(object sender)
  1947. {
  1948. BuildExpression(cronExpressionString);
  1949. }
  1950. /// <summary>
  1951. /// Determines whether the specified <see cref="CronExpression"/> is equal to the current <see cref="CronExpression"/>.
  1952. /// </summary>
  1953. /// <returns>
  1954. /// true if the specified <see cref="CronExpression"/> is equal to the current <see cref="CronExpression"/>; otherwise, false.
  1955. /// </returns>
  1956. /// <param name="other">The <see cref="CronExpression"/> to compare with the current <see cref="CronExpression"/>. </param>
  1957. public bool Equals(CronExpression other)
  1958. {
  1959. if (ReferenceEquals(null, other)) return false;
  1960. if (ReferenceEquals(this, other)) return true;
  1961. return Equals(other.cronExpressionString, cronExpressionString) && Equals(other.timeZone, timeZone);
  1962. }
  1963. /// <summary>
  1964. /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
  1965. /// </summary>
  1966. /// <returns>
  1967. /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
  1968. /// </returns>
  1969. /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>. </param>
  1970. public override bool Equals(object obj)
  1971. {
  1972. if (ReferenceEquals(null, obj)) return false;
  1973. if (ReferenceEquals(this, obj)) return true;
  1974. if (obj.GetType() != typeof (CronExpression)) return false;
  1975. return Equals((CronExpression) obj);
  1976. }
  1977. /// <summary>
  1978. /// Serves as a hash function for a particular type.
  1979. /// </summary>
  1980. /// <returns>
  1981. /// A hash code for the current <see cref="T:System.Object"/>.
  1982. /// </returns>
  1983. /// <filterpriority>2</filterpriority>
  1984. public override int GetHashCode()
  1985. {
  1986. unchecked
  1987. {
  1988. return ((cronExpressionString != null ? cronExpressionString.GetHashCode() : 0)*397) ^ (timeZone != null ? timeZone.GetHashCode() : 0);
  1989. }
  1990. }
  1991. }
  1992. /// <summary>
  1993. /// Helper class for cron expression handling.
  1994. /// </summary>
  1995. public class ValueSet
  1996. {
  1997. /// <summary>
  1998. /// The value.
  1999. /// </summary>
  2000. public int theValue;
  2001. /// <summary>
  2002. /// The position.
  2003. /// </summary>
  2004. public int pos;
  2005. }
  2006. }