2011年9月29日 星期四

練習題:取得指定月份第幾週星期幾的日期(日期的操作)


問題:
開始日期:2011/09/28
結束日期:2011/12/31
請取出 "每一個月第4週的星期二"  的日期。


其實這一題是延續「練習題:於指定的日期區間中取出符合指定DayOfWeek的日期」,

看起來好像蠻簡單的,但是這裡頭也碰到了幾個基本的日期操作,例如:

一年有多少週數?

如何自定每週的第一天是星期幾?

指定月份有幾週?

指定月份的第一天是一年之中的第幾週?

指定月份的最後一天是一年之中的第幾週?

指定週數的第一天,其日期為何?

指定週數的最後一天,其日期為何?


一年有多少週數?

如何因應地區文化的不同而設定?

如何設定每週的第一天是星期幾?

最快取得一年有多少週數的方法就是直接取得最後一天的所在週數,

網路上有一堆人都是用計算的方式做加減乘除的操作,而且日期的操作還有個「ISO 8601」是要特別小心謹慎,

假如地區文化的差異,或是一週開始的是星期日還是星期一,用計算的方式來取得一年有多少週數就不是那麼絕對了。

DateTime lastDateOfYear = new DateTime(DateTime.Now.Year, 12, 31);
 
CultureInfo info = CultureInfo.CurrentCulture;
 
int totalWeekOfYear = info.Calendar.GetWeekOfYear
(
    lastDateOfYear, 
    CalendarWeekRule.FirstDay, 
    DayOfWeek.Sunday
);

CultureInfo,這邊直接指定目前執行序所使用的CultureInfo,

使用 CultureInfo.Calendar.GetWeekOfYear() 方法,以取得指定日期的所在週數。

第一個參數,使用指定的該年度的最後一天,

第二個參數,定義日曆週的列舉值,

FirstDay    指示一年的第一週開始於該年的第一天,並結束於被指定為該週第一天的前一天。
FirstFullWeek    指示一年的第一週開始於,一年的第一天當天或之後被指定為一週第一天的那天。
FirstFourDayWeek    指示一年的第一週有四天以上在被指定為該週的第一天之前。

第三個參數,一週第一天的列舉值。

執行結果:

下面是指定「CalendarWeekRule.FirstDay」所得到的結果

image

下面是指定「CalendarWeekRule.FirstFourDayWeek」的結果

image

下面是指定「CalendarWeekRule.FirstFullWeek」的結果

image

MSDN 參考:

CultureInfo 類別
提供有關特定文化特性 (Culture) 的資訊 (文化特性在 Unmanaged 程式碼開發中稱為「地區設定」(Locale))。 提供的資訊包括文化特性的名稱、書寫系統、使用的曆法,以及日期和排序字串的格式。

CalendarWeekRule 列舉型別
定義決定年份的第一週的各種規則 (Rule)。


指定月份有幾週?

指定月份的第一天是一年之中的第幾週?

指定月份的最後一天是一年之中的第幾週?

根據上面取得該年度有多少週的程式內容,我們先找出指定月份的第一天以及最後一天,

然後分別取得指定月份第一天所在週數以及最後一天所在週數的資料,如此就可以取得指定月份有多少週了。

DateTime dt = DateTime.Now;    //2011-09-29
 
int year = dt.Year;
int month = dt.Month;
 
DateTime firstDateOfMonth = new DateTime(year, month, 1);
DateTime lastDateOfMonth = firstDateOfMonth.AddMonths(1).AddDays(-1);
 
CultureInfo info = CultureInfo.CurrentCulture;
 
int firstDateWeekNumber = info.Calendar.GetWeekOfYear(firstDateOfMonth, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
 
int lastDateWeekNumber = info.Calendar.GetWeekOfYear(lastDateOfMonth,CalendarWeekRule.FirstDay, DayOfWeek.Sunday);

執行結果:

這是指定「CalendarWeekRule.FirstDay」所得到的結果,至於其他兩個列舉就不再顯示,結果當然是不同的。

image


指定週數的第一天,其日期為何?

指定週數的最後一天,其日期為何?

在說明這個部分之前,先說明一下有關「ISO week date」,

這是因為每個地區甚至每個人對於一年當中的第一週定義都有所不同,

有的人會認為既使1/1出現在週六,那一週就算是該年的第一週。

有的人認為一週都是該年一月的日期時,那一週才算是該年的第一週。

而有的國家、地區會認為1/7才開始算第一週…等等。

於是乎ISO就制定了一些標準:

There are mutually equivalent descriptions of week 01:

  • the week with the year's first Thursday in it (the formal ISO definition),
  • the week with 4 January in it,
  • the first week with the majority (four or more) of its days in the starting year, and
  • the week starting with the Monday in the period 29 December – 4 January.

1.如果該週的星期四也是一月份,則該週就是第一週。
2.如果該週有包含1/4,則該週就是第一週。
3.如果該週一月份的天數超過四天,則該週就是第一週。
4.如果該週的週一為12/29,則該週就是第一週。

而且ISO 8601制定一週開始的第一天為星期一,而非星期日(beginning with Monday and ending with Sunday)。

還有就是weekday number的設定為 1 ~ 7 (對應週一到週日),而非程式常用的0 ~ 6(對應週日到週六)。

 

參考資料:

WIKIPEDIA:ISO 8601

WIKIPEDIA:ISO week date

Rex’s blah blah blah : 現在是哪一週?

 

所以這部份的命題就會有兩種的解題方法了。

 

符合ISO 8601的方法:

以下是符合ISO 8601的取得指定日期所在的週數(參考來源:ISO weeknumbers of a date, a C# implementation

我有做了一點小修改

public int WeekNumber(DateTime fromDate)
{
    // Get jan 1st of the year
    DateTime startOfYear = new DateTime(fromDate.Year, 1, 1);
    
    // Get dec 31st of the year
    DateTime endOfYear = startOfYear.AddYears(1).AddDays(-1);
    
    // ISO 8601 weeks start with Monday 
    // The first week of a year includes the first Thursday 
    // DayOfWeek returns 0 for sunday up to 6 for saterday
    int[] iso8601Correction = {6,7,8,9,10,4,5};
    int nds = fromDate.Subtract(startOfYear).Days  + iso8601Correction[(int)startOfYear.DayOfWeek];
    int wk = nds / 7;
    switch(wk)
    {
        case 0 : 
            // Return weeknumber of dec 31st of the previous year
            return WeekNumber(startOfYear.AddDays(-1));
        case 53 : 
            // If dec 31st falls before thursday it is week 01 of next year
            if (endOfYear.DayOfWeek < DayOfWeek.Thursday)
            {
                return 1;
            }
            else
            {
                return wk;
            }
        default : 
            return wk;
    }
}

以下的程式來做個驗證:

int weekNumber = 20;
//先取得該年第一天的日期
DateTime firstDateOfYear = new DateTime(DateTime.Now.Year, 1, 1);
//該年第一天再加上周數乘以七
DateTime dayInWeek = firstDateOfYear.AddDays(weekNumber * 7);
 
DateTime firstDayInWeek = dayInWeek.Date;
//ISO 8601所制定的標準中,一週的第一天為週一
while (firstDayInWeek.DayOfWeek != DayOfWeek.Monday)
{
    firstDayInWeek = firstDayInWeek.AddDays(-1);
}

執行結果:

可以看到一開始指定要取得第20週的第一天日期,並且也取得2011/5/16的結果,符合ISO 8601,

而且用「取得指定日期所在的週數」的方法去反求2011/5/16的週數是否等於一開始所指定的週數,其結果也是正確。

我想該週最後一天的日期的取得方式就不用顯示吧!(就是取得的第一天再用AddDays(6)就可以取得)

image


 

不是符合ISO 8601的作法(年度第一週開始於第一天,且一週的起始為週日):

這邊我們也一樣指定要取得第20週的開始日期與結束日期,而且指定一週啟始為週日,

第三行的地方要說明為何要將weekNumber減一,這是因為不用再去多加第一週的天數,

int weekNumber = 20;
DateTime firstDateOfYear = new DateTime(2011, 1, 1);
DateTime dayInWeek = firstDateOfYear.AddDays((weekNumber - 1) * 7);
 
DateTime firstDayInWeek = dayInWeek.Date;
while (firstDayInWeek.DayOfWeek != DayOfWeek.Sunday)
{
    firstDayInWeek = firstDayInWeek.AddDays(-1);
}
 
DateTime lastDayinWeek = firstDayInWeek.AddDays(6);

執行結果:

image


回到文章一開始的問題上:

開始日期:2011/09/28
結束日期:2011/12/31
請取出 "每一個月第4週的星期二"  的日期。

程式部分:

DateTime startDate = new DateTime(2011, 9, 28);
DateTime endDate = new DateTime(2011, 12, 31);
 
DateTime firstDate = DateTime.MinValue;
DateTime firstDateInMonth = DateTime.MinValue;
DateTime targetDate = DateTime.MinValue;
 
int firstWeekNumber = 0;
int fourthWeekNumber = 0;
 
CultureInfo info = CultureInfo.CurrentCulture;
 
List<DateTime> result = new List<DateTime>();
 
for(int i=9; i<=12; i++)
{
    firstDateInMonth = new DateTime(2011, i, 1);
    firstDate = new DateTime(firstDateInMonth.Year, 1, 1);
    
    firstWeekNumber = info.Calendar.GetWeekOfYear(firstDateInMonth, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
    fourthWeekNumber = firstWeekNumber + 3;
    
    targetDate = firstDate.AddDays((fourthWeekNumber - 1) * 7);
    
    while(targetDate.DayOfWeek != DayOfWeek.Tuesday)
    {
        targetDate = targetDate.AddDays(-1);
    }
    if(targetDate >= startDate)
    {
        result.Add(targetDate);
    }
}

執行結果:

image

其實上面的程式還有很大的修改空間,例如:

日期區間有遇到跨年度的時候該如何解決?

是否可以定義要取值的週數?

是否可以定義取得星期幾?

是否可以自行定義ClutureInfo?

是否可以符合ISO 8601?

 

2011-09-29 補充:將運算程式包裝為方法,可以接受跨年度,可以指定要取那一週,可以指定要取星期幾。

public List<DateTime> GetTargetDate(DateTime startDate, DateTime endDate, int weekInterval, DayOfWeek dayOfWeek)
{
    List<DateTime> result = new List<DateTime>();
    
    CultureInfo info = CultureInfo.CurrentCulture;
        
    DateTime firstDate = DateTime.MinValue;
    DateTime firstDateInMonth = DateTime.MinValue;
    DateTime targetDate = DateTime.MinValue;
    
    int startMonth = 0;
    int endMonth = 0;    
 
    int firstWeekNumber = 0;
    int targetWeekNumber = 0;
                    
    for(int x = startDate.Year; x <= endDate.Year; x++)
    {        
        startMonth = (x > startDate.Year) ? 1 : startDate.Month;
        endMonth = (x < endDate.Year) ? 12 : endDate.Month;
        
        for(int i = startMonth; i<= endMonth; i++)
        {
            firstDateInMonth = new DateTime(x, i, 1);
            firstDate = new DateTime(firstDateInMonth.Year, 1, 1);
            
            firstWeekNumber = info.Calendar.GetWeekOfYear(firstDateInMonth, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
            targetWeekNumber = firstWeekNumber + (weekInterval -1);
            
            targetDate = firstDate.AddDays((targetWeekNumber - 1) * 7);
            
            while(targetDate.DayOfWeek != dayOfWeek)
            {
                targetDate = targetDate.AddDays(-1);
            }
            if(targetDate >= startDate)
            {
                result.Add(targetDate);
            }
        }
    }
    
    return result;
}

執行結果:

image

 

看似簡單的問題,其實真正下去Coding的時候才發現到還有蠻多學問的,

讓我認識到:

何謂「ISO 8601

如何使用「CultureInfo.Calendar.GetWeekOfYear() 方法」去取得週數

CalendarWeekRule列舉屬性」有哪些列舉值,各自代表什麼意義

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。