5

I have a list of dates that are apart by a month in the sense that all dates are the "First Monday of the month". In some cases months are missing so I need to write a function to determine if all dates are consecutive

So for example if this was the list of dates, the function would return true as all items are the "First Friday of the month" and there are no gaps. This example below would return true.

 var date = new DateTime(2013, 1, 4);
 var date1 = new DateTime(2013, 2, 1);
 var date2 = new DateTime(2013, 3, 1);
 var date3 = new DateTime(2013, 4, 5);

 var dateArray = new DateTime[]{date, date1, date2, date3};
 bool isConsecutive = IsThisListConsecutive(dateArray);

where this example below would return false because, even though they are also all "First Friday of the month", its missing the March 2013 item.

 var date = new DateTime(2013, 1, 4);
 var date1 = new DateTime(2013, 2, 1);
 var date3 = new DateTime(2013, 4, 5);

 var dateArray = new DateTime[]{date, date1, date3};
 bool isConsecutive = IsThisListConsecutive(dateArray);

so i am trying to figure out the right logic for the IsThisListConsecutive() method:

Here was my first try: (Note I already know upfront that all dates are same day of week and same week of month so the only thing i am looking for is a missing slot)

  private bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
    {
        DateTime firstDate = orderedSlots.First();
        int count = 0;
        foreach (var slot in orderedSlots)
        {
            if (slot.Month != firstDate.AddMonths(count).Month)
            {
                return false;
            }
            count++;
        }
        return true;
    }

This code above works exept if the list crosses over from one year to another. I wanted to get any advice on a better way to create this function and how that line could be rewritten to deal with dates that cross over years.

leora
  • 188,729
  • 360
  • 878
  • 1,366
  • 1
    Where does the `orderedSlots` in your code come from? Also I think you are using the word “consecutive” in an odd way. – poke Mar 11 '13 at 01:30
  • @poke - I fixed the type in the code around orderedSlots. can you think of a better word to use compared to "consecutive" to get across what i am looking at – leora Mar 11 '13 at 01:34
  • Btw. is it intended that `date` and `date1` are a Wednesday, but `date2` and `date3` are a Thursday? – poke Mar 11 '13 at 01:34
  • @poke - I put 2012 instead of 2013 as the year . . i just fixed this (thanks for pointing it out) . . all dates are the same day of week and week of month – leora Mar 11 '13 at 01:35

6 Answers6

3

So to implement this we'll start with a simple helper method that takes a sequence and returns a sequence of pairs that make up each item with it's previous item.

public static IEnumerable<Tuple<T, T>> Pair<T>(this IEnumerable<T> source)
{
    T previous;
    using (var iterator = source.GetEnumerator())
    {
        if (iterator.MoveNext())
            previous = iterator.Current;
        else
            yield break;

        while(iterator.MoveNext())
        {
            yield return Tuple.Create(previous, iterator.Current);
            previous = iterator.Current;
        }
    }
}

We'll also use this simple method to determine if two dates are in the same month:

public static bool AreSameMonth(DateTime first, DateTime second)
{
    return first.Year == second.Year 
        && first.Month == second.Month;
}

Using that, we can easily grab the month of each date and see if it's the month after the previous month. If it's true for all of the pairs, then we have consecutive months.

private static bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
{
    return orderedSlots.Pair()
        .All(pair => AreSameMonth(pair.Item1.AddMonths(1), pair.Item2));
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • @pst Right, fixed. Oh, and using `Zip` would result in the sequence being iterated twice if you did something like `source.Zip(source.Skip(1)`, so to avoid the double iterating (and maintain laziness) you can't use `Zip`. – Servy Mar 11 '13 at 17:10
  • Hmm. I didn't think about that case with Zip .. I guess pass in a List and usually don't think about it. (I'm generally not an efficient programmer in that aspect, but I listen to ReSharpers "double evaluation" warnings and emit ToList in many cases where I don't *need* it lazy.) –  Mar 11 '13 at 17:11
  • @pst When dealing with a specific case in which you know the general size of what you're passing in that may be fine, but when writing more general utility methods in which you don't know the size of the data set (or when writing code for strangers on the internet in which you don't know the size of the data set) it's best to make as few assumptions as possible. – Servy Mar 11 '13 at 17:15
2

I would recommend looking at the TimeSpan structure. Thanks to operator overload you can get a TimeSpan by substracting two dates and then receive a TimeSpan that expresses the difference between the two dates.

http://msdn.microsoft.com/en-us/library/system.timespan.aspx

Nathan Anderson
  • 6,768
  • 26
  • 29
  • the issue with using Timespan is that subtracting the dates doesn't help with the comparison logic (as its not a direct month apart ) – leora Mar 11 '13 at 01:40
  • If you made use of the modulus operator and a little bit of logic you could adequately accommodate for dates that are 30, 60, or whatever days apart (but still even number of months apart). – Nathan Anderson Mar 11 '13 at 01:43
  • @NathanAnderson Not all months are the same length and weekdays (especially first/lasts) do not uniformly align with month days. Dates are darn tricky. –  Mar 11 '13 at 01:43
  • @pst that is correct, but weeks always are - which is why the difference between the dates is important. 4 mondays later or 8 mondays later is always exactly the same, regardless of the calendar date. – Nathan Anderson Mar 11 '13 at 01:45
  • @NathanAnderson But it's not always the next month. –  Mar 11 '13 at 01:45
  • 1
    @pst Very valid point. Recurrence is a tricky subject with a lot of edge cases, as we can see. In the past I've leveraged an assembly called `Aspose.iCalendar` to help me with this, but it seems this component is no longer offered. This seems to be an comparable solution: http://www.daypilot.org/calendar-tutorial-recurring-events.html – Nathan Anderson Mar 11 '13 at 01:53
  • 1
    I have used an open source project called recurrence.net before, which among other things can generate list of dates for recurrence for a calendar/scheduling app. You can pull the "engine" into some classes and forego the GUI, but the GUI helps with learning usage. Then use it to specify recurrence from first month to last month, and compare the dates in the returned list with the original list of dates. They should be the same (in the same order), or else they are not consecutive. http://sourceforge.net/projects/recurrence-net/ – Dmitriy Khaykin Mar 11 '13 at 01:59
2

Note: This is completely untested, and the date checks are probably pretty bad or somewhat redundant, but that’s the best I could come up with right now ^^

public bool AreSameWeekdayEveryMonth(IEnumerable<DateTime> dates)
{
    var en = dates.GetEnumerator();
    if (en.MoveNext())
    {
        DayOfWeek weekday = en.Current.DayOfWeek;
        DateTime previous = en.Current;
        while (en.MoveNext())
        {
            DateTime d = en.Current;
            if (d.DayOfWeek != weekday || d.Day > 7)
                return false;
            if (d.Month != previous.Month && ((d - previous).Days == 28 || (d - previous).Days == 35))
                return false;
            previous = d;
        }
    }
    return true;
}
poke
  • 369,085
  • 72
  • 557
  • 602
  • Why did you choose to use the `IEnumerable` interface directly instead of a foreach loop? – Nathan Anderson Mar 11 '13 at 01:56
  • 1
    @NathanAnderson Good question. I wanted to pick the first element separately but didn’t feel like doing a `bool first = true` thing, or making `weekday` and `previous` nullable… – poke Mar 11 '13 at 01:57
2

okay, your code doesnt work when the years cross over becuase jan 1st may be a monday on one year and a tuesday on the next. If I was doing this, I would first check that

a) they are the same day of the week in each month (use DateTime.DayOfWeek)

b) they are the same week of the month in each month* use extension method DayOfMonth (see link) * Calculate week of month in .NET *

(you said you already know a & b to be true so lets go on to the third condition)

c) we have to determine if they are in consecutive months

//order the list of dates & place it into an array for ease of looping
DateTime[] orderedSlots = slots.OrderBy( t => t).ToArray<DateTime>();


//create a variable to hold the date from the previous month
DateTime temp = orderedSlots[0];


for(i= 1; index < orderedSlots.Length; index++)
{
    if((orderedSlots[index].Month != temp.AddMonths(1).Month |
        orderedSlots[index].Year  != temp.AddMonths(1).Year)){
        return false;
    }

    previousDate =  orderedSlots[index];
}

return true;

if you need to check conditions a & b as well add change the if statement as follows

    if( orderedSlots[index].Month != temp.AddMonths(1).Month |
        orderedSlots[index].Year  != temp.AddMonths(1).Year) |
        orderedSlots[index].DayOfWeek != temp.DayOfWeek      |
        orderedSlots[index].GetWeekOfMonth != temp.AddMonths(1).GetWeekOfMonth){
        return false;
    }

remember that to use the get week of month extension method you have to include the code in Calculate week of month in .NET I'm sure there are typos as I did this in a text editor.

Community
  • 1
  • 1
monkeyhouse
  • 2,875
  • 3
  • 27
  • 42
1

Well, here is my initial thought on how I would approach this problem.

First, is to define a function that will turn the dates into the ordinal values corresponding to the order in which they should appear.

int ToOrdinal(DateTime d, DateTime baseline) {
   if (d.Day <= 7
       && d.DayInWeek == baseline.DayInWeek) {
      // Since there is only one "First Friday" a month, and there are
      // 12 months in year we can easily compose the ordinal.
      // (As per default.kramer's comment, months normalized to [0,11].)
      return d.Year * 12 + (d.Month - 1);
   } else {
      // Was not correct "kind" of day -
      // Maybe baseline is Tuesday, but d represents Wednesday or
      // maybe d wasn't in the first week ..
      return 0;
   }
}

var dates = ..;
var baseline = dates.FirstOrDefault();
var ordinals = dates.Select(d => ToOrdinal(d, baseline));

Then, for the dates provided, we end up with ordinal sequences like:

[24156 + 0, 24156 + 1, 24156 + 2, 24156 + 3]

And

[24156 + 0, 24156 + 1, /* !!!! */ 24156 + 3]

From here it is just a trivial matter of iterating the list and ensuring that the integers occur in sequence without gaps or stalls - that is, each item/integer is exactly one more than the previous.

0

I could be misinterpreting what you are trying to do, but I think this will work, assuming you don't have to handle ancient dates. See if there are any gaps in the dates converted to "total months"

int totalMonths = date.Year * 12 + (date.Month - 1);
default.kramer
  • 5,943
  • 2
  • 32
  • 50