2

Let's say I need to find out when the next scheduled date is when I know that the schedule was based off a start date of 8/1/2014, it's supposed to run every 7 days and the current date is 8/10/2014. I should get back a date of 8/14/2014. I eventually want to make this code work for every X hours, days, and weeks, bur right now I'm just testing with days. I have the following code I'm using to calculate the next run time, but I get it to work for one date and then it fails for another. FYI, I'm using the option to specify the current date for testing purposes. What am I doing wrong?

public class ScheduleComputer
{
    public DateTime GetNextRunTime(ScheduleRequest request)
    {
        var daysSinceBase = ((int)((request.CurrentDate - request.BaseDate).TotalDays)) + 1;
        var partialIntervalsSinceBaseDate = daysSinceBase % request.Interval;
        var fullIntervalsSinceBaseDate = daysSinceBase / request.Interval;
        var daysToNextRun = 0;
        if (partialIntervalsSinceBaseDate > 0)
        {
            daysToNextRun = (request.Interval - partialIntervalsSinceBaseDate) + 1;
        }
        var nextRunDate = request.BaseDate.AddDays((fullIntervalsSinceBaseDate * request.Interval) + daysToNextRun - 1);
        return nextRunDate;
    }
}

public class ScheduleRequest
{
    private readonly DateTime _currentDate;

    public ScheduleRequest()
    {
        _currentDate = DateTime.Now;
    }

    public ScheduleRequest(DateTime currentDate)
    {
        _currentDate = currentDate;
    }

    public DateTime CurrentDate
    {
        get { return _currentDate; }
    }

    public DateTime BaseDate { get; set; }
    public Schedule Schedule { get; set; }
    public int Interval { get; set; }
}

public enum Schedule
{
    Hourly,
    Daily,
    Weekly
}

And here are my unit tests

[TestFixture]
public class ScheduleComputerTests
{
    private ScheduleComputer _scheduleComputer;

    [SetUp]
    public void SetUp()
    {
        _scheduleComputer = new ScheduleComputer();
    }

    [Test]
    public void ThisTestPassesAndItShould()
    {
        var scheduleRequest = new ScheduleRequest(currentDate: DateTime.Parse("8/14/2014"))
        {
            BaseDate = DateTime.Parse("8/1/2014"),
            Schedule = Schedule.Daily,
            Interval = 7
        };
        var result = _scheduleComputer.GetNextRunTime(scheduleRequest);
        Assert.AreEqual(DateTime.Parse("8/14/2014"), result);
    }

    [Test]
    public void ThisTestFailsAndItShouldNot()
    {
        var scheduleRequest = new ScheduleRequest(currentDate: DateTime.Parse("8/2/2014"))
        {
            BaseDate = DateTime.Parse("8/1/2014"),
            Schedule = Schedule.Daily,
            Interval = 7
        };
        var result = _scheduleComputer.GetNextRunTime(scheduleRequest);
        Assert.AreEqual(DateTime.Parse("8/7/2014"), result);
    }

FYI, I saw the post here, but I can't seem to tailor it to my needs.

--- UPDATE 1 ---

Here is my updated code. I know I've made it verbose with variables so I can understand the logic better (hopefully that doesn't impact performance much). I also added logic to deal with different periods (hours, days, weeks) and added extension methods to make the code somewhat cleaner. However, this code seems to be working perfectly for hours and days, but is failing on weeks. Somewhere I'm not multiplying or dividing by 7 properly.

public class ScheduleComputer
{
    public DateTime GetNextRunTime(ScheduleRequest request)
    {
        var timeBetwenCurrentAndBase = request.CurrentDate - request.BaseDate;
        var totalPeriodsBetwenCurrentAndBase = timeBetwenCurrentAndBase.TotalPeriods(request.Schedule);
        var fractionalIntervals = totalPeriodsBetwenCurrentAndBase % request.Interval;
        var partialIntervalsLeft = request.Interval - fractionalIntervals;
        if (request.Schedule != Schedule.Hourly) partialIntervalsLeft = partialIntervalsLeft - 1;
        var nextRunTime = request.CurrentDate.AddPeriods(partialIntervalsLeft, request.Schedule);
        return nextRunTime;
    }
}

public static class ScheduleComputerExtensions
{
    public static double TotalPeriods(this TimeSpan timeBetwenCurrentAndBase, Schedule schedule)
    {
        switch (schedule)
        {
            case Schedule.Hourly: return timeBetwenCurrentAndBase.TotalHours;
            case Schedule.Daily: return timeBetwenCurrentAndBase.TotalDays;
            case Schedule.Weekly: return timeBetwenCurrentAndBase.TotalDays * 7;
            default: throw new ApplicationException("Invalid Schedule Provided");
        }
    }

    public static DateTime AddPeriods(this DateTime dateTime, double partialIntervalsLeft, Schedule schedule)
    {
        switch (schedule)
        {
            case Schedule.Hourly: return dateTime.AddHours(partialIntervalsLeft);
            case Schedule.Daily: return dateTime.AddDays(partialIntervalsLeft);
            case Schedule.Weekly: return dateTime.AddDays(partialIntervalsLeft * 7);
            default: throw new ApplicationException("Invalid Schedule Provided");
        }
    }
}
Community
  • 1
  • 1
bigmac
  • 2,553
  • 6
  • 38
  • 61
  • What problem(s) did you encounter when you borrowed the arithmetic from the other post you cited? – HABO Aug 03 '14 at 00:07
  • Have you looked at [Quartz.NET](http://www.quartz-scheduler.net/)? It can make all your problems go away. – Marcel N. Aug 03 '14 at 00:11

1 Answers1

4

Try replacing your GetNextRunTime with this

public DateTime GetNextRunTime(ScheduleRequest request)
{
    double days = (request.Interval - ((request.CurrentDate - request.BaseDate).TotalDays % request.Interval));
    return request.CurrentDate.AddDays(days-1);
}

That should give you the correct dates.

EDIT: Let's break it down in hopes of helping you figure out the logic.

diff = (request.CurrentDate - request.BaseDate).TotalDays this gives you the number of days between the BaseDate and CurrentDate. Note that the number of days DOES NOT INCLUDE the day for BaseDate. So the different between 8/7/14 and 8/1/14 is 6 days.

daysSinceLast = diff % request.Interval this gives you the number of days that have past since the last interval hit, so if the last interval hit on 8/1/14 and it is now 8/7/14 then the result would be 6 % 7 = 6; 6 days have past since the last scheduled interval (not including the last interval date). This is the most important part of the calculation; It keeps the number of days, no matter how many have passed within the interval, so for example, if 100 days have passed since the BaseDate and the interval is 7: 100 % 7 = 2 which means that 2 days have passed since the last interval triggered, there is no need to actually know the last date it was triggered. All you need is the BaseDate and CurrentDate. You could use this logic to find the date of the last triggered interval, just subtract the number of days from the CurrentDate.

daysUntil = request.Interval - daysSinceLast This gives you the number of days until the next scheduled interval. 7 - 6 = 1 day until the next scheduled interval

1 day in this scenario is not correct and the result will never be correct because the calculation of TimeSpan differences does not include the day for BaseDate, so you need to subtract 1 from the daysUntil nextDate = request.CurrentDate.AddDays(daysUntil - 1)

Adding the number of remaining days (minus 1 for the base date) to the current date gives you the required value. Does this help at all?

UPDATE In light of your testing, I see that the problem is on both of our ends. My calculation was incorrect and you were multiplying by 7 when you needed to divide by 7. Either way, the result was still wrong. Try this instead.

  • Remove your extension class completely
  • Modify your GetNextRunTime with the below code
  • Modify your ScheduleRequest and Schedule class/enum with the below code

GetNextRunTime

public DateTime GetNextRunTime(ScheduleRequest request)
{
    double diffMillis = (request.CurrentDate - request.BaseDate).TotalMilliseconds;
    double modMillis = (diffMillis % request.IntervalMillis);
    double timeLeft = (request.IntervalMillis - modMillis);

    ulong adjust = (request.Schedule == Schedule.Daily) ? (ulong)Schedule.Daily : 0;

    return request.CurrentDate.AddMilliseconds(timeLeft - adjust);
}

ScheduleRequest

public class ScheduleRequest
{
    private readonly DateTime _currentDate;

    public ScheduleRequest()
    {
        _currentDate = DateTime.Now;
    }

    public ScheduleRequest(DateTime currentDate)
    {
        _currentDate = currentDate;
    }

    public DateTime CurrentDate
    {
        get { return _currentDate; }
    }

    public DateTime BaseDate { get; set; }

    public Schedule Schedule { get; set; }

    public double IntervalMillis { get { return (double)this.Schedule * this.Interval; } }

    public int Interval { get; set; }
}

Schedule

public enum Schedule : ulong
{
    Hourly = 3600000,
    Daily = 86400000,
    Weekly = 604800000
}

This should work correctly for all dates, intervals and schedules. EDIT: corrected adjust value

vane
  • 2,125
  • 1
  • 21
  • 40
  • Thanks vane. This seems to work (though I'm still trying to figure out the logic). I'm testing with a bunch of dates and I'll let you know if I run into any issues. – bigmac Aug 03 '14 at 16:38
  • 1
    @bigmac I edited the answer to include an explanation, I hope it makes sense. If not, let me know and I'll try to explain it better. – vane Aug 03 '14 at 17:08
  • thank you very much for such a detailed explanation!!! I "think" I've dissected your logic into my updated code in the post. I have added in logic to deal with hours, days and weeks. However, my weeks calculation doesn't seem to be working. Any chance you can take a gander at it and see where I'm missing a multiplication or division? – bigmac Aug 03 '14 at 17:54
  • @bigmac Can you provide me with dates that don't work for the weeks calculation? I tested a date range and your code seemed to work fine. – vane Aug 03 '14 at 18:02
  • This request should return back 1/14/2000 01:00:00 but returns back 1/9/2000 06:00:00 - `var scheduleRequest = new ScheduleRequest(currentDate: DateTime.Parse("1/10/2000 01:23:45")) {BaseDate = DateTime.Parse("1/1/2000 01:00:00"),Schedule = Schedule.Weekly,Interval = 1};` – bigmac Aug 03 '14 at 18:21
  • @bigmac I updated the answer with a new way of doing it, it brings everything down to the millisecond to make all calculations the same and adjusts for your odd exclusion of the last day when calculating days/weeks and the last hour when doing hours; for the most part, the original logic still applies – vane Aug 03 '14 at 19:14
  • I love the logic! However, it's working for everything but Hourly. For some reason, each of my hourly tests are coming back with a return value one hour less than expected. `var scheduleRequest = new ScheduleRequest(currentDate: DateTime.Parse("2/3/2000 01:23:45")) {BaseDate = DateTime.Parse("1/1/2000 00:00:00"),Schedule = Schedule.Hourly,Interval = 12};` – bigmac Aug 03 '14 at 19:38
  • Found the issue... if I simply change one line in your code for the adjustment to be the following, all tests pass. Thanks for all your help! I owe you one! `var adjust = (request.Schedule == Schedule.Hourly) ? 0 : (ulong)Schedule.Daily;` – bigmac Aug 03 '14 at 19:58
  • Correction... here's the proper adjust line `var adjust = (request.Schedule == Schedule.Daily) ? (ulong)Schedule.Daily : 0;` – bigmac Aug 03 '14 at 20:20
  • @bigmac I corrected the adjust value per your findings. If this answer works for you, please accept it. Also, if there are any further comments on this, please take it to a private discussion with me so we can keep the comments list short. – vane Aug 03 '14 at 20:51