如何获取给定月份的每个星期一?

5
如何获得给定月份中的每个“星期一”?
一个例子: 输入:2017年7月11日(11.07.2017) 输出:(3,10,17,24,31) 2017年7月3日 星期一 2017年7月10日 星期一 2017年7月17日 星期一 2017年7月24日 星期一 2017年7月31日
我可以得到给定月份的天数(对于2017年7月,它是31天)。 然后编写迭代(例如for循环),如果dayOfWeek等于Monday,则添加到列表中。 但这不是好代码,因为for循环将执行31次。 应该有更好的算法来实现目标。
我正在使用C#.net框架4.6
更新 感谢所有人的帮助,在收到一些答案后; 我使用简单而肮脏的基准代码测试了所有代码,以找到更快的算法。
这是我的基准代码;
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;
        }

    }
}

这是结果表格; Benchmarkdotnet Results

如果您愿意,我可以删除任何代码和图片名称
我标记了Jonathan的答案。简单、干净、速度更快(有趣)。


3
前往目标月份的第一个星期一,然后连续加上7天,直到该月结束。 - litelite
2
@litelite的答案是正确的,找到第一个星期一,然后每次加7天直到月份不同。这非常简单明了。 - maccettura
1
即使有一个名为GetEveryMondayInMonth()的函数,在底层它仍然会执行某种循环。你的“应该有更好的算法”方法使得这个问题无法回答。什么因素决定了一个算法是否更好? - LarsTech
2
@Lost_In_Library 只需在循环中使用 Date 结构和 AddDays 方法。而且你还可以使用属性检查 当前月份。所有边缘情况都可以通过 Date 处理。 - litelite
1
向您致敬,因为您浏览了所有的解决方案并进行了基准测试。这将一个相对简单的练习/问题解决尝试转化为了一个学习时刻... - Jonathan
显示剩余4条评论
6个回答

16

其他答案都行,但我更喜欢使用Jon Skeet在foreach day in month中提供的AllDaysInMonth函数。

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);
        }
    }

然后你可以像这样使用LINQ进行调用:

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

不过,我想这取决于您将要使用它的次数,是否值得将其拆分为单独的函数。


你已经将它变成了一个单独的函数,创建了许多不必要的DateTime对象,只是为了再次迭代。我觉得这非常低效。 - maccettura
我同意,这可能不是最高效的方法。但在我看来,它非常易读和易于维护。我想这取决于具体情况,你是否想要使用它。 - Jonathan

3

尝试类似于这样的内容:

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


1
非科学性地说,在两年时间内随机检查几个月的给定工作日,这会快一些,大约快几千次迭代。
差别微不足道。是毫秒级别的。所以我会选择更易读的方式。我发现这种方式更容易阅读,尽管在另一个答案中函数名称已经足够清晰了。如果函数名称清晰并且已进行单元测试,则我不会为其余事项纠结。
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);
        }
    }
}

接着,Jon Skeet在另一条评论中提到的函数几乎肯定比我获取月份天数的函数更高效。我还测试了一种计算出每个月份天数后就将其存储的版本。虽然速度稍微快了一点点,但只有几毫秒的差距。 - Scott Hannen

0

这是它:

    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
我的版本可以达到相同的结果,但是避免了从每个月的第一天循环直到第一个星期一(或者你想要的任何星期几)的过程,而是通过计算从每个月的第一天到所需日期的第一次出现之间的偏移量来实现。
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;
}

要解决OP的问题,您需要这样调用该方法:
var mondays = GetDaysOfWeek(DateTime.Today, DayOfWeek.Monday);

为什么不在循环之前放置 workingDate.AddDays(offset); 呢? - litelite
@litelite 很好的发现。已更改代码(并校正了一个错误!) - swatsonpicken

0
你可以在自己的代码中仅使用两个除输入外的信息来解决整个问题,即月份的第一天和该月的天数,而无需进行任何迭代。尽管如此,在我的答案中我选择了一个简单的循环。
    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;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接