W poprzednim poście Data tygodniowa ISO 8601
poruszyłem temat „Funkcja konwertująca datę do formatu daty tygodniowej, zgodnie z normą ISO 8601”. Napisana tam funkcja funDataTygodniowaISO8601 (...)
nie działała prawidłowo. Użyte funkcje Format (...) i DatePart (...) nie zawsze zwracały prawidłowy wynik.
Na stronie http://support.microsoft.com/pl-pl/kb/200299
znalazłem artykuł, który opisuje błąd biblioteki Oleaut32.dll:
Przy ustalaniu numeru tygodnia zgodnie z normą ISO 8601, za pomocą funkcji
Format (...) lub DatePart (...) z biblioteki Oleaut32.dll w postaci:
'• AnyDate - argument typu Variant (Date), która ma zostać wyliczony '• "WW" - (tydzień) przedział czasu, który ma zostać zwrócony '• vbMonday - tydzień zaczyna się od Poniedziałku, '• vbFirstFourDays - pierwszy tydzień roku musi zawierać Czwartek Format (AnyDate "WW", vbMonday, vbFirstFourDays) DatePart ("WW", AnyDate, vbMonday, vbFirstFourDays)
funkcja Format (...) oraz funkcja DatePart (...), dla ostatniego poniedziałku w niektórych latach, zwracają nieprawidłowy numer tygodnia (53 tydzień), zamiast 1-szy tydzień.
Przykładowo:
29-12-2003; poniedziałek; tydzień: 53; zamiast; 1
31-12-2007; poniedziałek; tydzień: 53; zamiast; 1
30-12-2019; poniedziałek; tydzień: 53; zamiast; 1
29-12-2031; poniedziałek; tydzień: 53; zamiast; 1
31-12-2035; poniedziałek; tydzień: 53; zamiast; 1
30-12-2047; poniedziałek; tydzień: 53; zamiast; 1
W Visual Basic for Applications, wszystkie funkcje daty, z wyjątkiem funkcji DateSerial (...), pochodzą z biblioteki Oleaut32.dll. Ponieważ zarówno funkcja Format (...) jak i funkcja DatePart (...) mogą zwrócić liczbę tygodni kalendarzowych dla danej daty, obie są dotknięte tym błędem. Aby uniknąć tego problemu:
Microsoft proponuje następujące obejście problemu:
Jeżeli funkcja Format (...) lub DatePart (...) zwróci numer tygodnia równy 53, należy uruchomić instrukcję sprawdzającą numer następnego tygodnia dla argumentu MyDate + 7. Jeżeli zwracany następny tydzień będzie 2-gim tygodniem roku, to zwracana wartość przez funkcję WOY musi być równa 1.
'• MyDate - argument typu Variant (Date), która ma zostać wyliczony '• "ww" - (tydzień) przedział czasu, który ma zostać zwrócony '• vbMonday - tydzień zaczyna się od Poniedziałku, '• vbFirstFourDays - pierwszy tydzień roku musi zawierać Czwartek Function WOY(MyDate As Date) As Integer ' Week Of Year WOY = Format(MyDate, "ww", vbMonday, vbFirstFourDays) If WOY > 52 Then If Format(MyDate + 7, "ww", vbMonday, vbFirstFourDays) = 2 Then WOY = 1 End If End Function
I wszystko by było dobrze, ale funkcja WOY (MyDate As Date) dla poniższych dat zwraca numer tygodnia 53, a powinna zwrócić numer 52.
WOY(CDate("02-01-2101")) = 53
WOY(CDate("02-01-2501")) = 53
WOY(CDate("02-01-2901")) = 53
WOY(CDate("02-01-3301")) = 53
WOY(CDate("02-01-3701")) = 53
WOY(CDate("02-01-4101")) = 53
WOY(CDate("02-01-4501")) = 53
Innymi słowy, Microsoft powinien zrobić poprawkę do poprawki. Mniej więcej, coś w tym stylu:
'• MyDate - argument typu Variant (Date), która ma zostać wyliczony '• "ww" - (tydzień) przedział czasu, który ma zostać zwrócony '• vbMonday - tydzień zaczyna się od Poniedziałku, '• vbFirstFourDays - pierwszy tydzień roku musi zawierać Czwartek Function WOYC(MyDate As Date) As Integer ' Week Of Year Corrected WOYC = Format(MyDate, "ww", vbMonday, vbFirstFourDays) If WOYC > 52 Then If Format(MyDate + 7, "ww", vbMonday, vbFirstFourDays) = 2 Then WOYC = 1 ElseIf Format(MyDate + 7, "ww", vbMonday, vbFirstFourDays) = 1 And _ WeekDay(MyDate, vbMonday) = 7 And _ Day(MyDate) = 2 And _ Format(MyDate - 7, "ww", vbMonday, vbFirstFourDays) = 51 Then WOYC = 52 End If End If End Function
Nie ma tego złego, co by na dobre nie wyszło. Pod sam koniec artykułu Microsoft zamieścił funkcję WeekNumber (InDate As Date), która prawidłowo wylicza numery dni tygodni i nie
korzysta z niedopracowanej (zwracającej błędne dane) funkcji WOY(MyDate As Date).
Poniżej przedstawiam listing funkcji WeekNumber (...) ze strony http://support.microsoft.com/pl-pl/kb/200299
Function WeekNumber(InDate As Date) As Integer Dim DayNo As Integer Dim StartDays As Integer Dim StopDays As Integer Dim StartDay As Integer Dim StopDay As Integer Dim VNumber As Integer Dim ThurFlag As Boolean DayNo = Days(InDate) StartDay = WeekDay(DateSerial(Year(InDate), 1, 1)) - 1 StopDay = WeekDay(DateSerial(Year(InDate), 12, 31)) - 1 ' Number of days belonging to first calendar week StartDays = 7 - (StartDay - 1) ' Number of days belonging to last calendar week StopDays = 7 - (StopDay - 1) ' Test to see if the year will have 53 weeks or not If StartDay = 4 Or StopDay = 4 Then ThurFlag = True Else ThurFlag = False VNumber = (DayNo - StartDays - 4) / 7 ' If first week has 4 or more days, it will be calendar week 1 ' If first week has less than 4 days, it will belong to last year's ' last calendar week If StartDays >= 4 Then WeekNumber = Fix(VNumber) + 2 Else WeekNumber = Fix(VNumber) + 1 End If ' Handle years whose last days will belong to coming year's first ' calendar week If WeekNumber > 52 And ThurFlag = False Then WeekNumber = 1 ' Handle years whose first days will belong to the last year's ' last calendar week If WeekNumber = 0 Then WeekNumber = WeekNumber(DateSerial(Year(InDate) - 1, 12, 31)) End If End Function Function Days(DayNo As Date) As Integer Days = DayNo - DateSerial(Year(DayNo), 1, 0) End Function
Przy wykorzystaniu, do wyliczenia numeru tygodnia funkcji WeekNumber (InDate As Date) ze strony Microsoftu, funkcja WeekDateISO (...) konwertująca datę do formatu daty tygodniowej, zgodnie z normą ISO 8601, będzie miała postać:
' Pobiera: ' • datDate - data do przekonwertowania na format ISO ' • strDelimDate - separator daty, domyślnie [-] myślnik ' • strPrefixWeek - prefix przed dwucyfrowym numerem tygodnia, domyślnie [W] ' Zwraca: ' Przy powodzeniu zwraca sformatowaną datę tygodniową (ISO-8601 ) ' Poszczególne elementy daty rozdzielone opcjonalnym separatorem daty ' przekazanym w argumencie strDelimDate, dwucyfrowy numer tygodnia poprzedzony ' jest opcjonalnym prefiksem przekazanym w argumentcie strPrefixWeek. ' Przy niepowodzeniu zwraca ciąg zerowej długości. Public Function WeekDateISO(datDate As Date, _ Optional strDelimDate As String = "-", _ Optional strPrefixWeek As String = "W") As String Dim intYear As Integer Dim intWeek As Integer Dim intDay As Integer ' pobierz rok intYear = Year(datDate) ' pobierz numer tygodnia, korzystając z funkcji WeekNumber Microsoftu: intWeek = WeekNumber(datDate) If Month(datDate) = 12 And intWeek = 1 Then intYear = intYear + 1 ElseIf (Month(datDate) = 1 And (intWeek = 52 Or intWeek = 53)) Then intYear = intYear - 1 End If ' pobierz numer dnia w tygodniu (licząc od poniedziałku) intDay = WeekDay(datDate, vbMonday) WeekDateISO = CStr(intYear) & strDelimDate & _ strPrefixWeek & Format$(intWeek, "00") & _ strDelimDate & CStr(intDay) End Function ' poniżej przykład jak wywołać funkcję: WeekDateISO(...) Private Sub cmdTest_Click() Dim strDataISO As String Dim dtData As Date dtData = CDate("3 stycznia 2010") 'prawidłowo powinniśmy przekazać datę po "amerykańsku" #1/3/2010# strDataISO = WeekDateISO(dtData, "-") MsgBox "3 stycznia 2010" & " => " & strDataISO dtData = CDate("31 grudnia 2007") strDataISO = WeekDateISO(dtData, "-") MsgBox "31 grudnia 2007" & " => " & strDataISO End Sub