PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Quartz/CronExpression.cs

http://github.com/quartznet/quartznet
C# | 2316 lines | 1557 code | 188 blank | 571 comment | 481 complexity | 9e7abdb33adf035953fc6cc8b5e1202c MD5 | raw file
Possible License(s): GPL-2.0

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

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

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