Skip to content

Commit

Permalink
Solution to solve feature request for new GetPreviousOccurence() meth…
Browse files Browse the repository at this point in the history
…ods. atifaziz#64
  • Loading branch information
DaveRMaltby committed Jan 29, 2023
1 parent 9b68c8d commit f47eb25
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 1 deletion.
26 changes: 26 additions & 0 deletions NCrontab.Tests/CrontabScheduleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,14 @@ public void GetNextOccurrences_NextOccurrenceInvalidTime_ShouldStopAtLastValidTi
Assert.AreEqual(new DateTime(9988, 2, 29), occurrences.Last());
}

[Test]
public void GetPrevOccurrences_PrevOccurrenceInvalidTime_ShouldStopAtLastValidTime()
{
var schedule = CrontabSchedule.Parse("0 0 29 Feb Mon");
var occurrences = schedule.GetPrevOccurrences(new DateTime(20, 1, 1), DateTime.MinValue);
Assert.AreEqual(new DateTime(16, 2, 29), occurrences.Last());
}

// Instead of using strings and parsing as date,
// consider NUnit's TestCaseData:
// https://github.com/nunit/docs/wiki/TestCaseData
Expand All @@ -426,6 +434,24 @@ public void GetNextOccurrence(string expression, string startDate, string endDat
Assert.AreEqual(expected, occurrence);
}

// Instead of using strings and parsing as date,
// consider NUnit's TestCaseData:
// https://github.com/nunit/docs/wiki/TestCaseData

[TestCase("0 0 29 Feb Mon", "2017-12-31", "2017-01-01", "2017-01-01")]
[TestCase("0 0 29 Feb Mon", "9009-12-31", "9000-01-01", "9008-02-29")]
public void GetPrevOccurrence(string expression, string startDate, string endDate, string expectedValue)
{
var schedule = CrontabSchedule.Parse(expression);
var start = Time(startDate);
var end = Time(endDate);
var expected = Time(expectedValue);

var occurrence = schedule.GetPrevOccurrence(start, end);

Assert.AreEqual(expected, occurrence);
}


static void CronCall(string startTimeString, string cronExpression, string nextTimeString, ParseOptions options)
{
Expand Down
22 changes: 22 additions & 0 deletions NCrontab/CrontabField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ public static CrontabField DaysOfWeek(string expression) =>

public int GetFirst() => _minValueSet < int.MaxValue ? _minValueSet : -1;

/// <summary>
/// Gets the last value of the field or -1.
/// </summary>
public int GetLast() => _maxValueSet >= 0 ? _maxValueSet : -1;

/// <summary>
/// Gets the next value of the field that occurs after the given
/// start value or -1 if there is no next value available.
Expand All @@ -139,6 +144,23 @@ public int Next(int start)
return -1;
}

public int Prev(int start)
{
if (start > _maxValueSet)
return _maxValueSet;

var startIndex = ValueToIndex(start);
var lastIndex = ValueToIndex(_minValueSet);

for (var i = startIndex; i >= lastIndex; i--)
{
if (_bits[i])
return IndexToValue(i);
}

return -1;
}

int IndexToValue(int index) => index + _impl.MinValue;
int ValueToIndex(int value) => value - _impl.MinValue;

Expand Down
194 changes: 194 additions & 0 deletions NCrontab/CrontabSchedule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,27 @@ public IEnumerable<DateTime> GetNextOccurrences(DateTime baseTime, DateTime endT
}
}

public IEnumerable<DateTime> GetPrevOccurrences(DateTime baseTime, DateTime startTime)
{
for (var occurrence = TryGetPrevOccurrence(baseTime, startTime);
occurrence != null && occurrence > startTime;
occurrence = TryGetPrevOccurrence(occurrence.Value, startTime))
{
yield return occurrence.Value;
}
}

/// <summary>
/// Gets the next occurrence of this schedule starting with a base time.
/// </summary>

public DateTime GetNextOccurrence(DateTime baseTime) =>
GetNextOccurrence(baseTime, DateTime.MaxValue);


public DateTime GetPrevOccurrence(DateTime baseTime) =>
GetPrevOccurrence(baseTime, DateTime.MinValue);

/// <summary>
/// Gets the next occurrence of this schedule starting with a base
/// time and up to an end time limit.
Expand All @@ -210,6 +224,9 @@ public DateTime GetNextOccurrence(DateTime baseTime) =>
public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) =>
TryGetNextOccurrence(baseTime, endTime) ?? endTime;

public DateTime GetPrevOccurrence(DateTime baseTime, DateTime startTime) =>
TryGetPrevOccurrence(baseTime, startTime) ?? startTime;

DateTime? TryGetNextOccurrence(DateTime baseTime, DateTime endTime)
{
const int nil = -1;
Expand Down Expand Up @@ -377,6 +394,183 @@ public DateTime GetNextOccurrence(DateTime baseTime, DateTime endTime) =>
return TryGetNextOccurrence(new DateTime(year, month, day, 23, 59, 59, 0, baseTime.Kind), endTime);
}


DateTime? TryGetPrevOccurrence(DateTime baseTime, DateTime startTime)
{
const int nil = -1;

var baseYear = baseTime.Year;
var baseMonth = baseTime.Month;
var baseDay = baseTime.Day;
var baseHour = baseTime.Hour;
var baseMinute = baseTime.Minute;
var baseSecond = baseTime.Second;

var startYear = startTime.Year;
var startMonth = startTime.Month;
var startDay = startTime.Day;

var year = baseYear;
var month = baseMonth;
var day = baseDay;
var hour = baseHour;
var minute = baseMinute;
var second = baseSecond - 1;

//
// Second
//

var seconds = _seconds ?? SecondZero;
second = seconds.Prev(second);

if (second == nil)
{
second = seconds.GetLast();
minute--;
}

//
// Minute
//

minute = _minutes.Prev(minute);

if (minute == nil)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
hour--;
}
else if (minute < baseMinute)
{
second = seconds.GetLast();
}

//
// Hour
//

hour = _hours.Prev(hour);

// this variable for trick
// to keep day when it adjusted day 31 for
// "short" months
bool adjusting = false;

RetryDayMonth:

if (hour == nil)
{
minute = _minutes.GetLast();
hour = _hours.GetLast();
day--;
}
else if (hour > baseHour)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
}

//
// Day
//



day = _days.Prev(day);


if (day == nil)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
hour = _hours.GetLast();
day = _days.GetLast();
month--;
}
else if (day < baseDay)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
hour = _hours.GetLast();
}

//
// Month
//

month = _months.Prev(month);

if (month == nil)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
hour = _hours.GetLast();
day = _days.GetLast();
month = _months.GetLast();
year--;
}
else if (month < baseMonth)
{
second = seconds.GetLast();
minute = _minutes.GetLast();
hour = _hours.GetLast();
if (!adjusting)
day = _days.GetLast();
}

//
// Stop processing when year is too large for the datetime or calendar
// object. Otherwise we would get an exception.
//

if (year < Calendar.MinSupportedDateTime.Year)
return null;

//
// The day field in a cron expression spans the entire range of days
// in a month, which is from 1 to 31. However, the number of days in
// a month tend to be variable depending on the month (and the year
// in case of February). So a check is needed here to see if the
// date is a border case. If the day happens to be beyond 28
// (meaning that we're dealing with the suspicious range of 29-31)
// and the date part has changed then we need to determine whether
// the day still makes sense for the given year and month. If the
// day is beyond the last possible value, then the day/month part
// for the schedule is re-evaluated. So an expression like "0 0
// 15,31 * *" will yield the following sequence starting on midnight
// of Jan 1, 2000:
//
// Jan 15, Jan 31, Feb 15, Mar 15, Apr 15, Apr 31, ...
//

var dateChanged = day != baseDay || month != baseMonth || year != baseYear;

if (day > 28 && dateChanged && day > Calendar.GetDaysInMonth(year, month))
{
if (year <= startYear && month <= startMonth && day <= startDay)
return startTime;

hour = nil;
adjusting = true;
goto RetryDayMonth;
}

var prevTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind);

if (prevTime <= startTime)
return startTime;

//
// Day of week
//

if (_daysOfWeek.Contains((int)prevTime.DayOfWeek))
return prevTime;

return TryGetPrevOccurrence(new DateTime(year, month, day, 0, 0, 0, 0, baseTime.Kind), startTime);
}
/// <summary>
/// Returns a string in crontab expression (expanded) that represents
/// this schedule.
Expand Down
2 changes: 2 additions & 0 deletions NCrontab/ICrontabField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public interface ICrontabField
{
int GetFirst();
int Next(int start);
int GetLast();
int Prev(int start);
bool Contains(int value);
}
}
7 changes: 7 additions & 0 deletions NCrontab/PublicAPI/net35/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ NCrontab.CrontabField.Contains(int value) -> bool
NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void
NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void
NCrontab.CrontabField.GetFirst() -> int
NCrontab.CrontabField.GetLast() -> int
NCrontab.CrontabField.Next(int start) -> int
NCrontab.CrontabField.Prev(int start) -> int
NCrontab.CrontabField.ToString(string? format) -> string!
NCrontab.CrontabFieldKind
NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind
Expand All @@ -22,6 +24,9 @@ NCrontab.CrontabSchedule
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.ParseOptions
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void
Expand All @@ -30,7 +35,9 @@ NCrontab.ExceptionProvider
NCrontab.ICrontabField
NCrontab.ICrontabField.Contains(int value) -> bool
NCrontab.ICrontabField.GetFirst() -> int
NCrontab.ICrontabField.GetLast() -> int
NCrontab.ICrontabField.Next(int start) -> int
NCrontab.ICrontabField.Prev(int start) -> int
override NCrontab.CrontabField.ToString() -> string!
override NCrontab.CrontabSchedule.ToString() -> string!
static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField!
Expand Down
7 changes: 7 additions & 0 deletions NCrontab/PublicAPI/netstandard1.0/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ NCrontab.CrontabField.Contains(int value) -> bool
NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void
NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void
NCrontab.CrontabField.GetFirst() -> int
NCrontab.CrontabField.GetLast() -> int
NCrontab.CrontabField.Next(int start) -> int
NCrontab.CrontabField.Prev(int start) -> int
NCrontab.CrontabField.ToString(string? format) -> string!
NCrontab.CrontabFieldKind
NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind
Expand All @@ -21,6 +23,9 @@ NCrontab.CrontabSchedule
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.ParseOptions
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void
Expand All @@ -29,7 +34,9 @@ NCrontab.ExceptionProvider
NCrontab.ICrontabField
NCrontab.ICrontabField.Contains(int value) -> bool
NCrontab.ICrontabField.GetFirst() -> int
NCrontab.ICrontabField.GetLast() -> int
NCrontab.ICrontabField.Next(int start) -> int
NCrontab.ICrontabField.Prev(int start) -> int
override NCrontab.CrontabField.ToString() -> string!
override NCrontab.CrontabSchedule.ToString() -> string!
static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField!
Expand Down
7 changes: 7 additions & 0 deletions NCrontab/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ NCrontab.CrontabField.Contains(int value) -> bool
NCrontab.CrontabField.Format(System.IO.TextWriter! writer) -> void
NCrontab.CrontabField.Format(System.IO.TextWriter! writer, bool noNames) -> void
NCrontab.CrontabField.GetFirst() -> int
NCrontab.CrontabField.GetLast() -> int
NCrontab.CrontabField.Next(int start) -> int
NCrontab.CrontabField.Prev(int start) -> int
NCrontab.CrontabField.ToString(string? format) -> string!
NCrontab.CrontabFieldKind
NCrontab.CrontabFieldKind.Day = 3 -> NCrontab.CrontabFieldKind
Expand All @@ -21,6 +23,9 @@ NCrontab.CrontabSchedule
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrence(System.DateTime baseTime, System.DateTime endTime) -> System.DateTime
NCrontab.CrontabSchedule.GetNextOccurrences(System.DateTime baseTime, System.DateTime endTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrence(System.DateTime baseTime, System.DateTime startTime) -> System.DateTime
NCrontab.CrontabSchedule.GetPrevOccurrences(System.DateTime baseTime, System.DateTime startTime) -> System.Collections.Generic.IEnumerable<System.DateTime>!
NCrontab.CrontabSchedule.ParseOptions
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.get -> bool
NCrontab.CrontabSchedule.ParseOptions.IncludingSeconds.set -> void
Expand All @@ -29,7 +34,9 @@ NCrontab.ExceptionProvider
NCrontab.ICrontabField
NCrontab.ICrontabField.Contains(int value) -> bool
NCrontab.ICrontabField.GetFirst() -> int
NCrontab.ICrontabField.GetLast() -> int
NCrontab.ICrontabField.Next(int start) -> int
NCrontab.ICrontabField.Prev(int start) -> int
override NCrontab.CrontabField.ToString() -> string!
override NCrontab.CrontabSchedule.ToString() -> string!
static NCrontab.CrontabField.Days(string! expression) -> NCrontab.CrontabField!
Expand Down
Loading

0 comments on commit f47eb25

Please sign in to comment.