5

How to get every "monday" day of given month ?

An example;
Input: 11.July.2017 (11.07.2017)
Output: ( 3,10,17,24,31 )
3.7.2017 Monday
10.7.2017 Monday
17.7.2017 Monday
24.7.2017 Monday
31.7.2017

I could get number of days of given month(For july 2017, it's 31 days). Then write an iteration(for loop a.e.) if dayOfWeek equals Monday, add to a list. But this is not good code because for loop will work 31 times. There should be a better algorithm to archive the goal.

I'm using C# .net framework 4.6

UPDATE
Thanks for all for your helps, after some answers I've got so far; I tested all codes with a simple & dirty benchmark codes to find faster algorithm.

Here is my benchmark code;

using System;
using System.Collections.Generic;
using System.Linq;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Columns;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Engines;

using X.Core.Helpers;

namespace X.ConsoleBenchmark
{
    [SimpleJob(RunStrategy.ColdStart, targetCount: 5)]
    [MinColumn, MaxColumn, MeanColumn, MedianColumn]
    public class LoopTest
    {
        [Benchmark]
        public void CalculateNextSalaryDateWithLoopAllDays()
        {
            DateTime date = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                List<DateTime> allXDaysInMonth = date.GetAllXDaysInMonthWithLoopAllDays(DayOfWeek.Tuesday);
                if (allXDaysInMonth != null && allXDaysInMonth.FirstOrDefault().Day != 4)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        [Benchmark]
        public void CalculateNextSalaryDate()
        {
            DateTime date = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                List<DateTime> allXDaysInMonth = date.GetAllXDaysInMonth(DayOfWeek.Tuesday);
                if (allXDaysInMonth != null && allXDaysInMonth.FirstOrDefault().Day != 4)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        [Benchmark]
        public void Maccettura_GetAllDayOfWeekPerMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                var date = new DateTime(exampleDate.Year, exampleDate.Month, 1);

                if (date.DayOfWeek != DayOfWeek.Thursday)
                {
                    int daysUntilDayOfWeek = ((int)DayOfWeek.Thursday - (int)date.DayOfWeek + 7) % 7;
                    date = date.AddDays(daysUntilDayOfWeek);
                }

                List<DateTime> days = new List<DateTime>();

                while (date.Month == exampleDate.Month)
                {
                    days.Add(date);
                    date = date.AddDays(7);
                }

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        [Benchmark]
        public void ScottHannen_GetWeekdaysForMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                IEnumerable<DateTime> days = ScottHannen_GetDaysInMonth(exampleDate).Where(day => day.DayOfWeek == DayOfWeek.Thursday);

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        private IEnumerable<DateTime> ScottHannen_GetDaysInMonth(DateTime date)
        {
            var dateLoop = new DateTime(date.Year, date.Month, 1);

            while (dateLoop.Month == date.Month)
            {
                yield return dateLoop;
                dateLoop = dateLoop.AddDays(1);
            }
        }

        [Benchmark]
        public void Trioj_GetWeekdaysForMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                IEnumerable<DateTime> days = Trioj_GetDatesInMonthByWeekday(exampleDate, DayOfWeek.Thursday);

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        private List<DateTime> Trioj_GetDatesInMonthByWeekday(DateTime date, DayOfWeek dayOfWeek)
        {
            // We know the first of the month falls on, well, the first.
            var first = new DateTime(date.Year, date.Month, 1);
            int daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);

            // Find the first day of the week that matches the requested day of week.
            if (first.DayOfWeek != dayOfWeek)
            {
                first = first.AddDays(((((int)dayOfWeek - (int)first.DayOfWeek) + 7) % 7));
            }

            // A weekday in a 31 day month will only occur five times if it is one of the first three weekdays.
            // A weekday in a 30 day month will only occur five times if it is one of the first two weekdays.
            // A weekday in February will only occur five times if it is the first weekday and it is a leap year.
            // Incidentally, this means that if we subtract the day of the first occurrence of our weekday from the 
            // days in month, then if that results in an integer greater than 27, there will be 5 occurrences.
            int maxOccurrences = (daysInMonth - first.Day) > 27 ? 5 : 4;
            var list = new List<DateTime>(maxOccurrences);

            for (int i = 0; i < maxOccurrences; i++)
            {
                list.Add(new DateTime(first.Year, first.Month, (first.Day + (7 * i))));
            }

            return list;
        }

        [Benchmark]
        public void Jonathan_GetWeekdaysForMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                IEnumerable<DateTime> days = Jonathan_AllDatesInMonth(exampleDate.Year, exampleDate.Month).Where(x => x.DayOfWeek == DayOfWeek.Thursday);

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        private static IEnumerable<DateTime> Jonathan_AllDatesInMonth(int year, int month)
        {
            int days = DateTime.DaysInMonth(year, month);
            for (int day = 1; day <= days; day++)
            {
                yield return new DateTime(year, month, day);
            }
        }

        [Benchmark]
        public void Swatsonpicken_GetWeekdaysForMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                IEnumerable<DateTime> days = Swatsonpicken_GetDaysOfWeek(exampleDate, DayOfWeek.Thursday);

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        private static IEnumerable<DateTime> Swatsonpicken_GetDaysOfWeek(DateTime startDate, DayOfWeek desiredDayOfWeek)
        {
            var daysOfWeek = new List<DateTime>();
            var workingDate = new DateTime(startDate.Year, startDate.Month, 1);
            var offset = ((int)desiredDayOfWeek - (int)workingDate.DayOfWeek + 7) % 7;

            // Jump to the first desired day of week.
            workingDate = workingDate.AddDays(offset);

            do
            {
                daysOfWeek.Add(workingDate);

                // Jump forward seven days to get the next desired day of week.
                workingDate = workingDate.AddDays(7);
            } while (workingDate.Month == startDate.Month);

            return daysOfWeek;
        }

        [Benchmark]
        public void AliaksandrHmyrak_GetWeekdaysForMonth()
        {
            DateTime exampleDate = new DateTime(2017, 7, 3);
            const int oneMillion = 1000000;
            for (int i = 0; i < oneMillion; i++)
            {
                IEnumerable<DateTime> days = AliaksandrHmyrak_GetDaysOfWeek(exampleDate, DayOfWeek.Thursday);

                if (days.FirstOrDefault().Day != 6)
                {
                    throw new ApplicationException("Calculate method has errors.");
                }
            }
        }

        private static List<DateTime> AliaksandrHmyrak_GetDaysOfWeek(DateTime date, DayOfWeek dayOfWeek)
        {
            var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);
            var i = 1;

            List<DateTime> result = new List<DateTime>(5);

            do
            {
                var testDate = new DateTime(date.Year, date.Month, i);

                if (testDate.DayOfWeek == dayOfWeek)
                {
                    result.Add(testDate);
                    i += 7;
                }
                else
                {
                    i++;
                }

            } while (i <= daysInMonth);

            return result;
        }

    }
}

And this is the results table; Benchmarkdotnet Results

I can remove any code and picture-name if you want
I marked Jonathan's answer. Simple, clean and faster (interestingly).

Lost_In_Library
  • 3,265
  • 6
  • 38
  • 70
  • 3
    Go to the first moday of the month. Then add 7 days for as long as you are in the target month. – litelite Jul 11 '17 at 16:45
  • 1
    Well, a Monday is every seven days, no need to go through all of the 31 days in the month. See which day is the first of the month, based on that determine which is the first Monday, the loop every Monday afterwards. – Sami Kuhmonen Jul 11 '17 at 16:45
  • @litelite (and @Sami) Thanks for your quick response. But I don't know how many mondays in month. An example August 2017 has 4 mondays, but July 2017 has 5. I could check last date is actualy exist like 32.07.2017 is not a date. Or I could count how many mondays of given date then add +7. But I'm searching a better approach. – Lost_In_Library Jul 11 '17 at 16:50
  • 2
    @litelite has the right answer, find the first monday and add 7 until the month is different. Its really straight forward. – maccettura Jul 11 '17 at 16:51
  • 1
    Even if there was a function called `GetEveryMondayInMonth()`, under the hood, it would still do a loop of some kind. Your "should be a better algorithm" approach makes this question un-answerable. What makes something a better algorithm? – LarsTech Jul 11 '17 at 16:53
  • 2
    @Lost_In_Library Just use the [Date](https://msdn.microsoft.com/en-us/library/system.datetime.adddays(v=vs.110).aspx) structure and [AddDays](https://msdn.microsoft.com/en-us/library/system.datetime.adddays(v=vs.110).aspx) in a loop. And you have a property to check the [current month](https://msdn.microsoft.com/en-us/library/system.datetime.month(v=vs.110).aspx). All your edge case are handled with `Date` – litelite Jul 11 '17 at 16:57
  • @litelite ok acceptable answer. If you write your comment as answer I will check it.( Because you were the first ). I wrote "I'm searching a better approach" because it's some kind of math problem. I tried to find the best algorithm maybe with some math magic trick. Check this out; http://blog.artofmemory.com/how-to-calculate-the-day-of-the-week-4203.html – Lost_In_Library Jul 11 '17 at 17:15
  • 1
    @Lost_In_Library maccettura's answer is exactly what i would have writen – litelite Jul 11 '17 at 17:24
  • 1
    Kudos to you for going through all the solutions and benchmarking. That has turned a relatively simple exercise / problem solving attempt into a learning moment... – Jonathan Jul 17 '17 at 01:26

6 Answers6

15

Other answers work, but I would prefer to make use of Jon Skeet's AllDaysInMonth function from foreach day in month

public static IEnumerable<DateTime> AllDatesInMonth(int year, int month)
    {
        int days = DateTime.DaysInMonth(year, month);
        for (int day = 1; day <= days; day++)
        {
            yield return new DateTime(year, month, day);
        }
    }

And then you can call with LINQ like so:

var mondays = AllDatesInMonth(2017, 7).Where(i => i.DayOfWeek == DayOfWeek.Monday);

But I guess it depends on how many times you're going to use it as to wherther or not it's worth breaking out into a separate function.

Jonathan
  • 4,916
  • 2
  • 20
  • 37
  • You already made it a separate function that creates many unnecessary DateTime objects, just to iterate over again. I feel like this is very inefficient – maccettura Jul 11 '17 at 17:29
  • I agree, not the most efficient it can be. But in my opinion very readable and maintainable. I guess it depends on the situation as to whether or not you'd want to use it. – Jonathan Jul 11 '17 at 17:31
3

Try something like this:

public static IEnumerable<DateTime> GetAllDayOfWeekPerMonth(int month, int year, DayOfWeek dayOfWeek)
{
    var date = new DateTime(year, month, 1);

    if(date.DayOfWeek != dayOfWeek)
    {
        int daysUntilDayOfWeek = ((int) dayOfWeek - (int) date.DayOfWeek + 7) % 7;
        date = date.AddDays(daysUntilDayOfWeek);
    }

    List<DateTime> days = new List<DateTime>();

    while(date.Month == month)
    {
        days.Add(date);
        date = date.AddDays(7);         
    }

    return days;
}

Demo fiddle here

maccettura
  • 10,514
  • 3
  • 28
  • 35
1

Non-scientifically, this runs a little bit faster over a few thousand iterations of checking getting a given weekday for random months in a two-year period.

The difference is trivial. It's milliseconds. So I'd do whatever is easier to read. I find this a little easier to read, although in the other answer the function name makes it clear enough. If the function name is clear and it's unit tested then I wouldn't split hairs over the rest.

public class WeekdaysByMonth
{
    public IEnumerable<DateTime> GetWeekdaysForMonth(DateTime month, DayOfWeek weekDay)
    {
        return GetDaysInMonth(month).Where(day => day.DayOfWeek == weekDay);
    }

    private IEnumerable<DateTime> GetDaysInMonth(DateTime date)
    {
        var dateLoop = new DateTime(date.Year,date.Month,1);
        while (dateLoop.Month == date.Month)
        {
            yield return dateLoop;
            dateLoop = dateLoop.AddDays(1);
        }
    }
}
Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • And then Jon Skeet's function referenced in another comment is almost certainly more efficient than mine for getting the days in a month. I also tested a version that stores the days in the month once they're calculated. That was a tiny, tiny bit faster but by a few milliseconds. – Scott Hannen Jul 11 '17 at 17:46
0

Here is it:

    private static List<DateTime> GetDaysOfWeek(DateTime date, DayOfWeek dayOfWeek)
    {            
        var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);
        var i = 1;

        List<DateTime> result = new List<DateTime>(5);

        do
        {
            var testDate = new DateTime(date.Year, date.Month, i);

            if (testDate.DayOfWeek == dayOfWeek)
            {
                result.Add(testDate);
                i += 7;
            }
            else
            {
                i++;
            }

        } while (i <= daysInMonth);

        return result;
    }
0

My version achieves the same result but avoids looping from the first of the month until the first Monday (or whatever day of the week you desire) by calculating the offset from the first day of the month to the first occurrence of the desired day.

public static IEnumerable<DateTime> GetDaysOfWeek(DateTime startDate, DayOfWeek desiredDayOfWeek)
{
    var daysOfWeek = new List<DateTime>();
    var workingDate = new DateTime(startDate.Year, startDate.Month, 1);
    var offset = ((int)desiredDayOfWeek - (int)workingDate.DayOfWeek + 7) % 7;

    // Jump to the first desired day of week.
    workingDate = workingDate.AddDays(offset);

    do
    {
        daysOfWeek.Add(workingDate);

        // Jump forward seven days to get the next desired day of week.
        workingDate = workingDate.AddDays(7);
    } while (workingDate.Month == startDate.Month);

    return daysOfWeek;
}

To solve the OPs question you would call this method like so:

var mondays = GetDaysOfWeek(DateTime.Today, DayOfWeek.Monday);
swatsonpicken
  • 873
  • 1
  • 7
  • 21
0

You can technically solve the whole problem without iterating at all in your own code with only two pieces of information other than the input: first day in month, and number of days in month. Nevertheless, I have opted for a single minor loop in my answer.

    public List<DateTime> GetDatesInMonthByWeekday(DateTime date, DayOfWeek dayOfWeek) {
        // We know the first of the month falls on, well, the first.
        var first = new DateTime(date.Year, date.Month, 1);
        int daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);

        // Find the first day of the week that matches the requested day of week.
        if (first.DayOfWeek != dayOfWeek) {
            first = first.AddDays(((((int)dayOfWeek - (int)first.DayOfWeek) + 7) % 7));
        }

        // A weekday in a 31 day month will only occur five times if it is one of the first three weekdays.
        // A weekday in a 30 day month will only occur five times if it is one of the first two weekdays.
        // A weekday in February will only occur five times if it is the first weekday and it is a leap year.
        // Incidentally, this means that if we subtract the day of the first occurrence of our weekday from the 
        // days in month, then if that results in an integer greater than 27, there will be 5 occurrences.
        int maxOccurrences = (daysInMonth - first.Day) > 27 ? 5 : 4;
        var list = new List<DateTime>(maxOccurrences);

        for (int i = 0; i < maxOccurrences; i++) {
            list.Add(new DateTime(first.Year, first.Month, (first.Day + (7 * i))));
        }

        return list;
    }
Trioj
  • 303
  • 2
  • 8