using NCC.Dependency; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; namespace NCC.TaskScheduler { /// /// Cron 表达式的解析器和调度程序 /// 代码参考自:https://github.com/HangfireIO/Cronos /// [SuppressSniffer] public sealed class CronExpression : IEquatable { private const long NotFound = 0; private const int MinNthDayOfWeek = 1; private const int MaxNthDayOfWeek = 5; private const int SundayBits = 0b1000_0001; private const int MaxYear = 2099; private static readonly TimeZoneInfo UtcTimeZone = TimeZoneInfo.Utc; private static readonly CronExpression Yearly = Parse("0 0 1 1 *"); private static readonly CronExpression Weekly = Parse("0 0 * * 0"); private static readonly CronExpression Monthly = Parse("0 0 1 * *"); private static readonly CronExpression Daily = Parse("0 0 * * *"); private static readonly CronExpression Hourly = Parse("0 * * * *"); private static readonly CronExpression Minutely = Parse("* * * * *"); private static readonly CronExpression Secondly = Parse("* * * * * *", CronFormat.IncludeSeconds); private static readonly int[] DeBruijnPositions = { 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, 62, 5, 39, 46, 44, 42, 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, 63, 52, 6, 26, 37, 40, 33, 47, 61, 45, 43, 21, 23, 58, 17, 10, 51, 25, 36, 32, 60, 20, 57, 16, 50, 31, 19, 15, 30, 14, 13, 12 }; private long _second; // 60 bits -> from 0 bit to 59 bit private long _minute; // 60 bits -> from 0 bit to 59 bit private int _hour; // 24 bits -> from 0 bit to 23 bit private int _dayOfMonth; // 31 bits -> from 1 bit to 31 bit private short _month; // 12 bits -> from 1 bit to 12 bit private byte _dayOfWeek; // 8 bits -> from 0 bit to 7 bit private byte _nthDayOfWeek; private byte _lastMonthOffset; private CronExpressionFlag _flags; private CronExpression() { } /// /// Constructs a new based on the specified /// cron expression. It's supported expressions consisting of 5 fields: /// minute, hour, day of month, month, day of week. /// If you want to parse non-standard cron expressions use with specified CronFields argument. /// See more: https://github.com/HangfireIO/Cronos /// public static CronExpression Parse(string expression) { return Parse(expression, CronFormat.Standard); } /// /// Constructs a new based on the specified /// cron expression. It's supported expressions consisting of 5 or 6 fields: /// second (optional), minute, hour, day of month, month, day of week. /// See more: https://github.com/HangfireIO/Cronos /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe CronExpression Parse(string expression, CronFormat format) { if (string.IsNullOrEmpty(expression)) throw new ArgumentNullException(nameof(expression)); fixed (char* value = expression) { var pointer = value; SkipWhiteSpaces(ref pointer); CronExpression cronExpression; if (Accept(ref pointer, '@')) { cronExpression = ParseMacro(ref pointer); SkipWhiteSpaces(ref pointer); if (cronExpression == null || !IsEndOfString(*pointer)) ThrowFormatException("Macro: Unexpected character '{0}' on position {1}.", *pointer, pointer - value); return cronExpression; } cronExpression = new CronExpression(); if (format == CronFormat.IncludeSeconds) { cronExpression._second = ParseField(CronField.Seconds, ref pointer, ref cronExpression._flags); ParseWhiteSpace(CronField.Seconds, ref pointer); } else { SetBit(ref cronExpression._second, CronField.Seconds.First); } cronExpression._minute = ParseField(CronField.Minutes, ref pointer, ref cronExpression._flags); ParseWhiteSpace(CronField.Minutes, ref pointer); cronExpression._hour = (int)ParseField(CronField.Hours, ref pointer, ref cronExpression._flags); ParseWhiteSpace(CronField.Hours, ref pointer); cronExpression._dayOfMonth = (int)ParseDayOfMonth(ref pointer, ref cronExpression._flags, ref cronExpression._lastMonthOffset); ParseWhiteSpace(CronField.DaysOfMonth, ref pointer); cronExpression._month = (short)ParseField(CronField.Months, ref pointer, ref cronExpression._flags); ParseWhiteSpace(CronField.Months, ref pointer); cronExpression._dayOfWeek = (byte)ParseDayOfWeek(ref pointer, ref cronExpression._flags, ref cronExpression._nthDayOfWeek); ParseEndOfString(ref pointer); // Make sundays equivalent. if ((cronExpression._dayOfWeek & SundayBits) != 0) { cronExpression._dayOfWeek |= SundayBits; } return cronExpression; } } /// /// Calculates next occurrence starting with (optionally ) in UTC time zone. /// public DateTime? GetNextOccurrence(DateTime fromUtc, bool inclusive = false) { if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); var found = FindOccurrence(fromUtc.Ticks, inclusive); if (found == NotFound) return null; return new DateTime(found, DateTimeKind.Utc); } /// /// Returns the list of next occurrences within the given date/time range, /// including and excluding /// by default, and UTC time zone. When none of the occurrences found, an /// empty list is returned. /// public IEnumerable GetOccurrences( DateTime fromUtc, DateTime toUtc, bool fromInclusive = true, bool toInclusive = false) { if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); for (var occurrence = GetNextOccurrence(fromUtc, fromInclusive); occurrence < toUtc || occurrence == toUtc && toInclusive; // ReSharper disable once RedundantArgumentDefaultValue // ReSharper disable once ArgumentsStyleLiteral occurrence = GetNextOccurrence(occurrence.Value, inclusive: false)) { yield return occurrence.Value; } } /// /// Calculates next occurrence starting with (optionally ) in given /// public DateTime? GetNextOccurrence(DateTime fromUtc, TimeZoneInfo zone, bool inclusive = false) { if (fromUtc.Kind != DateTimeKind.Utc) ThrowWrongDateTimeKindException(nameof(fromUtc)); if (ReferenceEquals(zone, UtcTimeZone)) { var found = FindOccurrence(fromUtc.Ticks, inclusive); if (found == NotFound) return null; return new DateTime(found, DateTimeKind.Utc); } var fromOffset = new DateTimeOffset(fromUtc); var occurrence = GetOccurrenceConsideringTimeZone(fromOffset, zone, inclusive); return occurrence?.UtcDateTime; } /// /// Returns the list of next occurrences within the given date/time range, including /// and excluding by default, and /// specified time zone. When none of the occurrences found, an empty list is returned. /// public IEnumerable GetOccurrences( DateTime fromUtc, DateTime toUtc, TimeZoneInfo zone, bool fromInclusive = true, bool toInclusive = false) { if (fromUtc > toUtc) ThrowFromShouldBeLessThanToException(nameof(fromUtc), nameof(toUtc)); for (var occurrence = GetNextOccurrence(fromUtc, zone, fromInclusive); occurrence < toUtc || occurrence == toUtc && toInclusive; // ReSharper disable once RedundantArgumentDefaultValue // ReSharper disable once ArgumentsStyleLiteral occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) { yield return occurrence.Value; } } /// /// Calculates next occurrence starting with (optionally ) in given /// public DateTimeOffset? GetNextOccurrence(DateTimeOffset from, TimeZoneInfo zone, bool inclusive = false) { if (ReferenceEquals(zone, UtcTimeZone)) { var found = FindOccurrence(from.UtcTicks, inclusive); if (found == NotFound) return null; return new DateTimeOffset(found, TimeSpan.Zero); } return GetOccurrenceConsideringTimeZone(from, zone, inclusive); } /// /// Returns the list of occurrences within the given date/time offset range, /// including and excluding by /// default. When none of the occurrences found, an empty list is returned. /// public IEnumerable GetOccurrences( DateTimeOffset from, DateTimeOffset to, TimeZoneInfo zone, bool fromInclusive = true, bool toInclusive = false) { if (from > to) ThrowFromShouldBeLessThanToException(nameof(from), nameof(to)); for (var occurrence = GetNextOccurrence(from, zone, fromInclusive); occurrence < to || occurrence == to && toInclusive; // ReSharper disable once RedundantArgumentDefaultValue // ReSharper disable once ArgumentsStyleLiteral occurrence = GetNextOccurrence(occurrence.Value, zone, inclusive: false)) { yield return occurrence.Value; } } /// public override string ToString() { var expressionBuilder = new StringBuilder(); AppendFieldValue(expressionBuilder, CronField.Seconds, _second).Append(' '); AppendFieldValue(expressionBuilder, CronField.Minutes, _minute).Append(' '); AppendFieldValue(expressionBuilder, CronField.Hours, _hour).Append(' '); AppendDayOfMonth(expressionBuilder, _dayOfMonth).Append(' '); AppendFieldValue(expressionBuilder, CronField.Months, _month).Append(' '); AppendDayOfWeek(expressionBuilder, _dayOfWeek); return expressionBuilder.ToString(); } /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// /// true if the specified is equal to the current ; otherwise, false. /// public bool Equals(CronExpression other) { if (other == null) return false; return _second == other._second && _minute == other._minute && _hour == other._hour && _dayOfMonth == other._dayOfMonth && _month == other._month && _dayOfWeek == other._dayOfWeek && _nthDayOfWeek == other._nthDayOfWeek && _lastMonthOffset == other._lastMonthOffset && _flags == other._flags; } /// /// Determines whether the specified is equal to this instance. /// /// The to compare with this instance. /// /// true if the specified is equal to this instance; /// otherwise, false. /// public override bool Equals(object obj) { return Equals(obj as CronExpression); } /// /// Returns a hash code for this instance. /// /// /// A hash code for this instance, suitable for use in hashing algorithms and data /// structures like a hash table. /// public override int GetHashCode() { var hash = new HashCode(); hash.Add(_second); hash.Add(_minute); hash.Add(_hour); hash.Add(_dayOfMonth); hash.Add(_month); hash.Add(_dayOfWeek); hash.Add(_nthDayOfWeek); hash.Add(_lastMonthOffset); hash.Add(_flags); return hash.ToHashCode(); } /// /// Implements the operator ==. /// public static bool operator ==(CronExpression left, CronExpression right) => Equals(left, right); /// /// Implements the operator !=. /// public static bool operator !=(CronExpression left, CronExpression right) => !Equals(left, right); private DateTimeOffset? GetOccurrenceConsideringTimeZone(DateTimeOffset fromUtc, TimeZoneInfo zone, bool inclusive) { if (!DateTimeHelper.IsRound(fromUtc)) { // Rarely, if fromUtc is very close to DST transition, `TimeZoneInfo.ConvertTime` may not convert it correctly on Windows. // E.g., In Jordan Time DST started 2017-03-31 00:00 local time. Clocks jump forward from `2017-03-31 00:00 +02:00` to `2017-03-31 01:00 +3:00`. // But `2017-03-30 23:59:59.9999000 +02:00` will be converted to `2017-03-31 00:59:59.9999000 +03:00` instead of `2017-03-30 23:59:59.9999000 +02:00` on Windows. // It can lead to skipped occurrences. To avoid such errors we floor fromUtc to seconds: // `2017-03-30 23:59:59.9999000 +02:00` will be floored to `2017-03-30 23:59:59.0000000 +02:00` and will be converted to `2017-03-30 23:59:59.0000000 +02:00`. fromUtc = DateTimeHelper.FloorToSeconds(fromUtc); inclusive = false; } var from = TimeZoneInfo.ConvertTime(fromUtc, zone); var fromLocal = from.DateTime; if (TimeZoneHelper.IsAmbiguousTime(zone, fromLocal)) { var currentOffset = from.Offset; var standardOffset = zone.BaseUtcOffset; if (standardOffset != currentOffset) { var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, fromLocal); var daylightTimeLocalEnd = TimeZoneHelper.GetDaylightTimeEnd(zone, fromLocal, daylightOffset).DateTime; // Early period, try to find anything here. var foundInDaylightOffset = FindOccurrence(fromLocal.Ticks, daylightTimeLocalEnd.Ticks, inclusive); if (foundInDaylightOffset != NotFound) return new DateTimeOffset(foundInDaylightOffset, daylightOffset); fromLocal = TimeZoneHelper.GetStandardTimeStart(zone, fromLocal, daylightOffset).DateTime; inclusive = true; } // Skip late ambiguous interval. var ambiguousIntervalLocalEnd = TimeZoneHelper.GetAmbiguousIntervalEnd(zone, fromLocal).DateTime; if (HasFlag(CronExpressionFlag.Interval)) { var foundInStandardOffset = FindOccurrence(fromLocal.Ticks, ambiguousIntervalLocalEnd.Ticks - 1, inclusive); if (foundInStandardOffset != NotFound) return new DateTimeOffset(foundInStandardOffset, standardOffset); } fromLocal = ambiguousIntervalLocalEnd; inclusive = true; } var occurrenceTicks = FindOccurrence(fromLocal.Ticks, inclusive); if (occurrenceTicks == NotFound) return null; var occurrence = new DateTime(occurrenceTicks); if (zone.IsInvalidTime(occurrence)) { var nextValidTime = TimeZoneHelper.GetDaylightTimeStart(zone, occurrence); return nextValidTime; } if (TimeZoneHelper.IsAmbiguousTime(zone, occurrence)) { var daylightOffset = TimeZoneHelper.GetDaylightOffset(zone, occurrence); return new DateTimeOffset(occurrence, daylightOffset); } return new DateTimeOffset(occurrence, zone.GetUtcOffset(occurrence)); } private long FindOccurrence(long startTimeTicks, long endTimeTicks, bool startInclusive) { var found = FindOccurrence(startTimeTicks, startInclusive); if (found == NotFound || found > endTimeTicks) return NotFound; return found; } private long FindOccurrence(long ticks, bool startInclusive) { if (!startInclusive) ticks++; CalendarHelper.FillDateTimeParts( ticks, out var startSecond, out var startMinute, out var startHour, out var startDay, out var startMonth, out var startYear); var minMatchedDay = GetFirstSet(_dayOfMonth); var second = startSecond; var minute = startMinute; var hour = startHour; var day = startDay; var month = startMonth; var year = startYear; if (!GetBit(_second, second) && !Move(_second, ref second)) minute++; if (!GetBit(_minute, minute) && !Move(_minute, ref minute)) hour++; if (!GetBit(_hour, hour) && !Move(_hour, ref hour)) day++; // If NearestWeekday flag is set it's possible forward shift. if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CronField.DaysOfMonth.First; if (!GetBit(_dayOfMonth, day) && !Move(_dayOfMonth, ref day)) goto RetryMonth; if (!GetBit(_month, month)) goto RetryMonth; Retry: if (day > GetLastDayOfMonth(year, month)) goto RetryMonth; if (HasFlag(CronExpressionFlag.DayOfMonthLast)) day = GetLastDayOfMonth(year, month); var lastCheckedDay = day; if (HasFlag(CronExpressionFlag.NearestWeekday)) day = CalendarHelper.MoveToNearestWeekDay(year, month, day); if (IsDayOfWeekMatch(year, month, day)) { if (CalendarHelper.IsGreaterThan(year, month, day, startYear, startMonth, startDay)) goto RolloverDay; if (hour > startHour) goto RolloverHour; if (minute > startMinute) goto RolloverMinute; goto ReturnResult; RolloverDay: hour = GetFirstSet(_hour); RolloverHour: minute = GetFirstSet(_minute); RolloverMinute: second = GetFirstSet(_second); ReturnResult: var found = CalendarHelper.DateTimeToTicks(year, month, day, hour, minute, second); if (found >= ticks) return found; } day = lastCheckedDay; if (Move(_dayOfMonth, ref day)) goto Retry; RetryMonth: if (!Move(_month, ref month) && ++year >= MaxYear) return NotFound; day = minMatchedDay; goto Retry; } private static bool Move(long fieldBits, ref int fieldValue) { if (fieldBits >> ++fieldValue == 0) { fieldValue = GetFirstSet(fieldBits); return false; } fieldValue += GetFirstSet(fieldBits >> fieldValue); return true; } private int GetLastDayOfMonth(int year, int month) { return CalendarHelper.GetDaysInMonth(year, month) - _lastMonthOffset; } private bool IsDayOfWeekMatch(int year, int month, int day) { if (HasFlag(CronExpressionFlag.DayOfWeekLast) && !CalendarHelper.IsLastDayOfWeek(year, month, day) || HasFlag(CronExpressionFlag.NthDayOfWeek) && !CalendarHelper.IsNthDayOfWeek(day, _nthDayOfWeek)) { return false; } if (_dayOfWeek == CronField.DaysOfWeek.AllBits) return true; var dayOfWeek = CalendarHelper.GetDayOfWeek(year, month, day); return ((_dayOfWeek >> (int)dayOfWeek) & 1) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetFirstSet(long value) { // TODO: Add description and source var res = unchecked((ulong)(value & -value) * 0x022fdd63cc95386d) >> 58; return DeBruijnPositions[res]; } private bool HasFlag(CronExpressionFlag value) { return (_flags & value) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void SkipWhiteSpaces(ref char* pointer) { while (IsWhiteSpace(*pointer)) { pointer++; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ParseWhiteSpace(CronField prevField, ref char* pointer) { if (!IsWhiteSpace(*pointer)) ThrowFormatException(prevField, "Unexpected character '{0}'.", *pointer); SkipWhiteSpaces(ref pointer); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe void ParseEndOfString(ref char* pointer) { if (!IsWhiteSpace(*pointer) && !IsEndOfString(*pointer)) ThrowFormatException(CronField.DaysOfWeek, "Unexpected character '{0}'.", *pointer); SkipWhiteSpaces(ref pointer); if (!IsEndOfString(*pointer)) ThrowFormatException("Unexpected character '{0}'.", *pointer); } private static unsafe CronExpression ParseMacro(ref char* pointer) { switch (ToUpper(*pointer++)) { case 'A': if (AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'U') && AcceptCharacter(ref pointer, 'A') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Yearly; return null; case 'D': if (AcceptCharacter(ref pointer, 'A') && AcceptCharacter(ref pointer, 'I') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Daily; return null; case 'E': if (AcceptCharacter(ref pointer, 'V') && AcceptCharacter(ref pointer, 'E') && AcceptCharacter(ref pointer, 'R') && AcceptCharacter(ref pointer, 'Y') && Accept(ref pointer, '_')) { if (AcceptCharacter(ref pointer, 'M') && AcceptCharacter(ref pointer, 'I') && AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'U') && AcceptCharacter(ref pointer, 'T') && AcceptCharacter(ref pointer, 'E')) return Minutely; if (*(pointer - 1) != '_') return null; if (AcceptCharacter(ref pointer, 'S') && AcceptCharacter(ref pointer, 'E') && AcceptCharacter(ref pointer, 'C') && AcceptCharacter(ref pointer, 'O') && AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'D')) return Secondly; } return null; case 'H': if (AcceptCharacter(ref pointer, 'O') && AcceptCharacter(ref pointer, 'U') && AcceptCharacter(ref pointer, 'R') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Hourly; return null; case 'M': if (AcceptCharacter(ref pointer, 'O') && AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'T') && AcceptCharacter(ref pointer, 'H') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Monthly; if (ToUpper(*(pointer - 1)) == 'M' && AcceptCharacter(ref pointer, 'I') && AcceptCharacter(ref pointer, 'D') && AcceptCharacter(ref pointer, 'N') && AcceptCharacter(ref pointer, 'I') && AcceptCharacter(ref pointer, 'G') && AcceptCharacter(ref pointer, 'H') && AcceptCharacter(ref pointer, 'T')) return Daily; return null; case 'W': if (AcceptCharacter(ref pointer, 'E') && AcceptCharacter(ref pointer, 'E') && AcceptCharacter(ref pointer, 'K') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Weekly; return null; case 'Y': if (AcceptCharacter(ref pointer, 'E') && AcceptCharacter(ref pointer, 'A') && AcceptCharacter(ref pointer, 'R') && AcceptCharacter(ref pointer, 'L') && AcceptCharacter(ref pointer, 'Y')) return Yearly; return null; default: pointer--; return null; } } private static unsafe long ParseField(CronField field, ref char* pointer, ref CronExpressionFlag flags) { if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) { if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; return ParseStar(field, ref pointer); } var num = ParseValue(field, ref pointer); var bits = ParseRange(field, ref pointer, num, ref flags); if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); return bits; } private static unsafe long ParseDayOfMonth(ref char* pointer, ref CronExpressionFlag flags, ref byte lastDayOffset) { var field = CronField.DaysOfMonth; if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); if (AcceptCharacter(ref pointer, 'L')) return ParseLastDayOfMonth(field, ref pointer, ref flags, ref lastDayOffset); var dayOfMonth = ParseValue(field, ref pointer); if (AcceptCharacter(ref pointer, 'W')) { flags |= CronExpressionFlag.NearestWeekday; return GetBit(dayOfMonth); } var bits = ParseRange(field, ref pointer, dayOfMonth, ref flags); if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); return bits; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe long ParseDayOfWeek(ref char* pointer, ref CronExpressionFlag flags, ref byte nthWeekDay) { var field = CronField.DaysOfWeek; if (Accept(ref pointer, '*') || Accept(ref pointer, '?')) return ParseStar(field, ref pointer); var dayOfWeek = ParseValue(field, ref pointer); if (AcceptCharacter(ref pointer, 'L')) return ParseLastWeekDay(dayOfWeek, ref flags); if (Accept(ref pointer, '#')) return ParseNthWeekDay(field, ref pointer, dayOfWeek, ref flags, out nthWeekDay); var bits = ParseRange(field, ref pointer, dayOfWeek, ref flags); if (Accept(ref pointer, ',')) bits |= ParseList(field, ref pointer, ref flags); return bits; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe long ParseStar(CronField field, ref char* pointer) { return Accept(ref pointer, '/') ? ParseStep(field, ref pointer, field.First, field.Last) : field.AllBits; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe long ParseList(CronField field, ref char* pointer, ref CronExpressionFlag flags) { var num = ParseValue(field, ref pointer); var bits = ParseRange(field, ref pointer, num, ref flags); do { if (!Accept(ref pointer, ',')) return bits; bits |= ParseList(field, ref pointer, ref flags); } while (true); } private static unsafe long ParseRange(CronField field, ref char* pointer, int low, ref CronExpressionFlag flags) { if (!Accept(ref pointer, '-')) { if (!Accept(ref pointer, '/')) return GetBit(low); if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; return ParseStep(field, ref pointer, low, field.Last); } if (field.CanDefineInterval) flags |= CronExpressionFlag.Interval; var high = ParseValue(field, ref pointer); if (Accept(ref pointer, '/')) return ParseStep(field, ref pointer, low, high); return GetBits(field, low, high, 1); } private static unsafe long ParseStep(CronField field, ref char* pointer, int low, int high) { // Get the step size -- note: we don't pass the // names here, because the number is not an // element id, it's a step size. 'low' is // sent as a 0 since there is no offset either. var step = ParseNumber(field, ref pointer, 1, field.Last); return GetBits(field, low, high, step); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe long ParseLastDayOfMonth(CronField field, ref char* pointer, ref CronExpressionFlag flags, ref byte lastMonthOffset) { flags |= CronExpressionFlag.DayOfMonthLast; if (Accept(ref pointer, '-')) lastMonthOffset = (byte)ParseNumber(field, ref pointer, 0, field.Last - 1); if (AcceptCharacter(ref pointer, 'W')) flags |= CronExpressionFlag.NearestWeekday; return field.AllBits; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe long ParseNthWeekDay(CronField field, ref char* pointer, int dayOfWeek, ref CronExpressionFlag flags, out byte nthDayOfWeek) { nthDayOfWeek = (byte)ParseNumber(field, ref pointer, MinNthDayOfWeek, MaxNthDayOfWeek); flags |= CronExpressionFlag.NthDayOfWeek; return GetBit(dayOfWeek); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ParseLastWeekDay(int dayOfWeek, ref CronExpressionFlag flags) { flags |= CronExpressionFlag.DayOfWeekLast; return GetBit(dayOfWeek); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe bool Accept(ref char* pointer, char character) { if (*pointer == character) { pointer++; return true; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe bool AcceptCharacter(ref char* pointer, char character) { if (ToUpper(*pointer) == character) { pointer++; return true; } return false; } private static unsafe int ParseNumber(CronField field, ref char* pointer, int low, int high) { var num = GetNumber(ref pointer, null); if (num == -1 || num < low || num > high) { ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", low, high); } return num; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int ParseValue(CronField field, ref char* pointer) { var num = GetNumber(ref pointer, field.Names); if (num == -1 || num < field.First || num > field.Last) { ThrowFormatException(field, "Value must be a number between {0} and {1} (all inclusive).", field.First, field.Last); } return num; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static StringBuilder AppendFieldValue(StringBuilder expressionBuilder, CronField field, long fieldValue) { if (field.AllBits == fieldValue) return expressionBuilder.Append('*'); // Unset 7 bit for Day of week field because both 0 and 7 stand for Sunday. if (field == CronField.DaysOfWeek) fieldValue &= ~(1 << field.Last); for (var i = GetFirstSet(fieldValue); ; i = GetFirstSet(fieldValue >> i << i)) { expressionBuilder.Append(i); if (fieldValue >> ++i == 0) break; expressionBuilder.Append(','); } return expressionBuilder; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private StringBuilder AppendDayOfMonth(StringBuilder expressionBuilder, int domValue) { if (HasFlag(CronExpressionFlag.DayOfMonthLast)) { expressionBuilder.Append('L'); if (_lastMonthOffset != 0) expressionBuilder.Append($"-{_lastMonthOffset}"); } else { AppendFieldValue(expressionBuilder, CronField.DaysOfMonth, (uint)domValue); } if (HasFlag(CronExpressionFlag.NearestWeekday)) expressionBuilder.Append('W'); return expressionBuilder; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AppendDayOfWeek(StringBuilder expressionBuilder, int dowValue) { AppendFieldValue(expressionBuilder, CronField.DaysOfWeek, dowValue); if (HasFlag(CronExpressionFlag.DayOfWeekLast)) expressionBuilder.Append('L'); else if (HasFlag(CronExpressionFlag.NthDayOfWeek)) expressionBuilder.Append($"#{_nthDayOfWeek}"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long GetBits(CronField field, int num1, int num2, int step) { if (num2 < num1) return GetReversedRangeBits(field, num1, num2, step); if (step == 1) return (1L << (num2 + 1)) - (1L << num1); return GetRangeBits(num1, num2, step); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long GetRangeBits(int low, int high, int step) { var bits = 0L; for (var i = low; i <= high; i += step) { SetBit(ref bits, i); } return bits; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long GetReversedRangeBits(CronField field, int num1, int num2, int step) { var high = field.Last; // Skip one of sundays. if (field == CronField.DaysOfWeek) high--; var bits = GetRangeBits(num1, high, step); num1 = field.First + step - (high - num1) % step - 1; return bits | GetRangeBits(num1, num2, step); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long GetBit(int num1) { return 1L << num1; } private static unsafe int GetNumber(ref char* pointer, int[] names) { if (IsDigit(*pointer)) { var num = GetNumeric(*pointer++); if (!IsDigit(*pointer)) return num; num = num * 10 + GetNumeric(*pointer++); if (!IsDigit(*pointer)) return num; return -1; } if (names == null) return -1; if (!IsLetter(*pointer)) return -1; var buffer = ToUpper(*pointer++); if (!IsLetter(*pointer)) return -1; buffer |= ToUpper(*pointer++) << 8; if (!IsLetter(*pointer)) return -1; buffer |= ToUpper(*pointer++) << 16; var length = names.Length; for (var i = 0; i < length; i++) { if (buffer == names[i]) { return i; } } return -1; } [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowFormatException(CronField field, string format, params object[] args) { throw new CronFormatException(field, string.Format(format, args)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowFormatException(string format, params object[] args) { throw new CronFormatException(string.Format(format, args)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowFromShouldBeLessThanToException(string fromName, string toName) { throw new ArgumentException($"The value of the {fromName} argument should be less than the value of the {toName} argument.", fromName); } [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowWrongDateTimeKindException(string paramName) { throw new ArgumentException("The supplied DateTime must have the Kind property set to Utc", paramName); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool GetBit(long value, int index) { return (value & (1L << index)) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetBit(ref long value, int index) { value |= 1L << index; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsEndOfString(int code) { return code == '\0'; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhiteSpace(int code) { return code == '\t' || code == ' '; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsDigit(int code) { return code >= 48 && code <= 57; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsLetter(int code) { return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetNumeric(int code) { return code - 48; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ToUpper(int code) { if (code >= 97 && code <= 122) { return code - 32; } return code; } } }