tags:

views:

77

answers:

3

I need to create a function to return the next processing date for a given item. An item has a number that represents the month within a quarter that it is processed, as well as a number that represents the week within that month when it is processed. So, given a particular item's create date, I need to get the next processing date for that item, which will be the first day of it's assigned week and month within a quarter.

Note that weeks are broken out by 7 days from the start of the month, regardless of what day of the week. So the first day of the first week could start on Tuesday or any other day for the purposes of this calculation.

Example:
Let's say I have an item with a completed date of 1/8/2010. That item has a monthWithinQuarter value of 2. It has a weekWithinMonth value of 3. So for this item that resolves to the third week of February, so I would want the function to return a date of 2/15/2010.

The function should look something like this:

var nextProcessingDate = GetNextProcessingDate(
                             itemCompletedDate,
                             monthWithinQuarter,
                             weekWithinMonth);

This calculation has to be pretty fast as this calculation is going to be happening a lot, both in real time to display on a web site as well as in batch mode when processing items.

Thanks,

~ Justin

A: 

From what I understand, this should do the job:

public static DateTime GetNextProcessingDate(DateTime itemCreationDate, int monthWithinQuarter,
    int weekWithinMonth)
{
    var quarter = (itemCreationDate.Month - 1) / 4; // Assumes quarters are divided by calendar year.
    var month = quarter * 4 + monthWithinQuarter; // First quarter of month plus month within quarter
    var dayInMonth = (weekWithinMonth - 1) * 7 + 1; // Weeks are counted from first day, regardless of day of week (as you mention).
    return new DateTime(itemCreationDate.Year, month, dayInMonth);
}

Let me know if any of it isn't clear.

Noldorin
This does not work for `itemCreationDate = new DateTime(2010, 3, 31), monthWithinQuarter = 3` and `weekWithinMonth = 1`.
Jason
This also does not work for `itemCreationDate = new DateTime(2010, 1, 1), monthWithinQuarter = 1 and weekWithinMonth = 1`.
Jason
@Jason. Sure it does. I don't see why not... Did this really deserve a down-vote?
Noldorin
Oh hrmm... small bug, just fixed.
Noldorin
+1  A: 

Okay, this should do it for you:

static DateTime GetNextProcessingDate(
    DateTime itemCompletedDate,
    int monthWithinQuarter,
    int weekWithinMonth
) {
        if (monthWithinQuarter < 1 || monthWithinQuarter > 3) {
            throw new ArgumentOutOfRangeException("monthWithinQuarter");
        }
        if (weekWithinMonth < 1 || weekWithinMonth > 5) {
            throw new ArgumentOutOfRangeException("weekWithinMonth");
        }
        int year = itemCompletedDate.Year;
        DateTime[] startOfQuarters = new[] {
            new DateTime(year, 1, 1),
            new DateTime(year, 4, 1),
            new DateTime(year, 7, 1),
            new DateTime(year, 10, 1)
        };
        DateTime startOfQuarter = startOfQuarters.Where(d => d <= itemCompletedDate)
                                                 .OrderBy(d => d)
                                                 .Last();
        int month = startOfQuarter.Month + monthWithinQuarter - 1;
        int day = (weekWithinMonth - 1) * 7 + 1;
        if (day > DateTime.DaysInMonth(year, month)) {
            throw new ArgumentOutOfRangeException("weekWithinMonth");
        }
        DateTime candidate = new DateTime(year, month, day);
        if (candidate < itemCompletedDate) {
            month += 3;
            if(month > 12) {
                year++;
                month -= 12;
            }
        }
        return new DateTime(year, month, day);
    }

As far as efficiency, the place where I see the most room for improvement is repeatedly creating the array

DateTime[] startOfQuarters = new[] {
    new DateTime(year, 1, 1),
    new DateTime(year, 4, 1),
    new DateTime(year, 7, 1),
    new DateTime(year, 10, 1)
};

So let's offload that to a method and memoize it:

static Dictionary<int, DateTime[]> cache = new Dictionary<int, DateTime[]>();
public static DateTime[] StartOfQuarters(DateTime date) {
    int year = date.Year;
    DateTime[] startOfQuarters;
    if(!cache.TryGetValue(year, out startOfQuarters)) {
        startOfQuarters = new[] {
            new DateTime(year, 1, 1),
            new DateTime(year, 4, 1),
            new DateTime(year, 7, 1),
            new DateTime(year, 10, 1)
        };
        cache.Add(year, startOfQuarters);
    }
    return startOfQuarters;
}

If you don't need the flexibility of quarters possibly starting on unusual days, you could replace

DateTime[] startOfQuarters = new[] {
    new DateTime(year, 1, 1),
    new DateTime(year, 4, 1),
    new DateTime(year, 7, 1),
    new DateTime(year, 10, 1)
};
DateTime startOfQuarter = startOfQuarters.Where(d => d <= itemCompletedDate).OrderBy(d => d).Last();
int month = startOfQuarter.Month + monthWithinQuarter - 1;

with

int month = 3 * ((itemCompletedDate.Month - 1) / 3) + monthWithinQuarter;
Jason
Uncannily similar to my solution, in most parts... The method of detecting the quarter may either be overkill or what the OP wants. Can't be sure with the current info.
Noldorin
Yeah, it depends. I come from a world where quarter starts are often not `1/1`, `4/1`, `7/1`, `10/1` so I always prepare for the possibility that they are not.
Jason
@Downvoter: Please explain.
Jason
@Jason: This is a crazy man that experiments with downvotes :))
serhio
With some minor adaptations to the actual problem, this worked perfectly. Thanks!
jkohlhepp
A: 

Your calculation, I suppose should be reduced at

    DateTime dt;
    dt.AddDays(daysToAdd);
    dt.AddMonths(monthsToAdd);
    dt.AddHours(hoursToAdd);
    dt.AddYears(yearsToAdd);
serhio