wtorek, 29 grudnia 2015

• Bug funkcji WOY (MyDate As Date) As Integer ' Week Of Year

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
 

sobota, 26 grudnia 2015

• Data tygodniowa ISO 8601

Konwersja daty do formatu Daty tygodniowej, zgodnie z normą ISO 8601.

ISO 8601 to międzynarodowa norma ISO określająca sposób zapisu daty i czasu. W standardzie tym używa się sformatowanych ciągów znaków ustawionych w porządku od najbardziej znaczących (rok) do najmniej znaczących (dni, sekundy lub ich części). Norma określa numeryczny format daty i czasu (bez używania nazw miesięcy, dni, tygodni i literowych oznaczeń czasu przed lub po południu).
Na stronie Głównego Urzędu Miar Numerowanie tygodni w roku czytamy, że:

  1. Tydzień jest to okres 7 dni,
  2. Poniedziałek jest pierwszym dniem tygodnia kalendarzowego (ze względu na łatwiejsze rozliczenia handlowe i finansowe)
  3. Pierwszym tygodniem kalendarzowym w obrębie danego roku jest tydzień zawierający pierwszy czwartek tego roku

piątek, 25 grudnia 2015

• VBA 7.0 i MS Access 2010+

Na stronie Alexa Gorbatcheva pod adresem http://alexgorbatchev.com/SyntaxHighlighter/ znalazłem skrypt służący do podświetlania (kolorowania) elementów składniowych kodu źródłowego programu. Na swoim blogu Kevin Junghans w artykule Adding SyntaxHighlighter to Blogger Dynamic Views przedstawił, jak zastosować SyntaxHighlighter na swoim blogu. Początkowo skorzystałem z rad Kevina, ale po pewnym czasie opracowałem swój sposób na zastosowanie skryptu służącego do kolorowania składni. Ale o tym będzie później. Na razie przejdę do właściwego tematu, jakim jest 64-bitowy Access, a raczej 64-bitowe VBA 7.0.

Microsoft Office 2010+ jest obecnie dostępny w wersji 32 i 64-bitowej. Wersja 64 bitowa umożliwia pracę ze znacznie większymi zbiorami danych. Możliwość ta jest szczególnie ważna podczas pracy z dużą ilością danych w programie Microsoft Excel 2010+. Wraz z wprowadzeniem nowej, 64 bitowej, wersji pakietu Microsoft Office 2010+, wprowadzono nową wersję Visual Basic for Applications (VBA) pod nazwą VBA 7.0, która przeznaczona jest do pracy zarówno w 32-bitowych i 64-bitowych aplikacjach.
Niestety, jedynie wersja 32 bitowa pakietu Office 2010+ pozwala nadal używać 32 bitowe dodatki dla pakietu Office oraz korzystanie ze „starego” kodu bez konieczności jakichkolwiek modyfikacji w kodzie VBA. Wszystkie dodatkowe wtyczki dla Office będą działały jedynie w 32 bitowej wersji, gdyż nie są one zgodne z 64 bitowym Officem. Próba instalacji jakiegokolwiek dodatku (pluginu) zakończy się niepowodzeniem. Jeśli więc zdecydujmy się na zainstalowanie 64 bitowego Office, to musimy się liczyć z pewnymi ograniczeniami funkcjonalności 64 bitowego Office w porównaniu do jego 32 bitowej wersji.

Nowości i zmiany w VBA 7.0

• VBA7 - stała kompilacji warunkowej
Umożliwia sprawdzenie, czy uruchomiona aplikacja używa VBA 7.0 (64 bitowego lub 32 bitowego), czy poprzedniej 32 bitowej wersji VBA (6.0). Zwracana wartość VBA7 = TRUE informuje, że uruchomiona została aplikacja pakietu Office w wersji 2010 lub nowszej.
• Win64 - stała kompilacji warunkowej
Pozwala stwierdzić, czy uruchomiona została 32 bitowa, czy 64 bitowa wersja pakietu Microsoft Office.
 
Public Sub testVBA()
 ' Sprawdź wersję VBA
 #If VBA7 Then
   ' środowisko VBA 7 (wersja 32 lub 64 bitowa MS Office +2010+)
   #If Win64 Then
      ' 64-bitowe środowisko VBA 7.
      MsgBox "Kod VBA 7 uruchomiony w 64-bitowej wersji MS Office 2010+"
   #Else
      ' 32-bitowe środowisko VBA 7.
      MsgBox "Kod VBA 7 uruchomiony w 32-bitowej wersji MS Office 2010+"
   #End If
 #Else
   ' Środowisko wersji VBA 6, lub wersji niższej
   MsgBox "Kod VBA uruchomiony w wersji VBA 6 lub niższej."
 #End If

End Function
 
• PtrSafe - kwalifikator
Używany jest do deklarowania procedur i funkcji z zewnętrznych bibliotek DLL w 64 bitowym VBA 7. Określa, że deklaracja funkcji jest zgodna z 64 bitowymi środowiskami programistycznymi. Atrybut ten jest obowiązkowy w systemach 64 bitowych. Deklarując funkcję z zewnętrznej biblioteki należy za każdą instrukcją Declare umieścić atrybut (kwalifikator, słowo kluczowe) PtrSafe. Należy także zmienić 32 bitowe odwołania do uchwytów i wskaźników na zgodne z typem LongPtr, a 64 bitowym argumentom przypisać nowy typ danych LongLong. Aby zapewnić wsteczną kompatybilność z VBA w wersji 6.0 atrybut PtrSafe nie jest wymagany w 32 bitowym środowisku VBA 7, ale nic nie stoi na przeszkodzie, by kwalifikator ten stosować w 32 bitowym środowisku VBA 7.
 
#If VBA7 Then
   ' Aplikacja używa 32 lub 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
   ' Zastosuj atrybut PtrSafe po instrukcji Declare
   Public Declare PtrSafe Function...
#Else
   ' Aplikacja używa 32 bitowej wersji VBA 6 (Office 2007 i niższe wersje)
   ' Nie używaj atrybutu PtrSafe po instrukcji Declare
   Public Declare Function...
#End If
 
• LongLong - typ danych
8 bajtowy typ danych. Przechowuje liczby w przedziale wartości od -9.223.372.036.854.775.808 do 9.223.372.036.854.775.807. Typ ten jest dostępny tylko w 64 bitowym środowisku (w 64 bitowej wersji pakietu Office).
• LongPtr - typ danych
W 64 bitowym środowisku VBA 7 jest to 8 bajtowa liczba typu LongLong. W 32 bitowym środowisku VBA 7 jest to liczba 4 bajtowa typu Long. Typ ten (LongPtr) jest zalecany przy deklarowaniu uchwytów i wskaźników w obu środowiskach VBA 7 (64 i 32 bitowym). Korzystanie z typu LongPtr umożliwia pisanie kodu, który będzie działał zarówno w 32 jak i 64 bitowym środowisku VBA 7.
Typ LongPtr obsługiwany jest tylko w VBA 7 (zarówno w 32 bitowym jak i 64 bitowym środowisku).
Ponieważ 32 bitowe VBA nie miało takiego typu, więc w przypadku zadeklarowania wskaźnika (uchwytu) jako typ Long, wszystkie zmienne 64 bitowe zostały by „ucięte” do wartości 32 bitowej i mogłyby wskazywać na nieprawidłowy uchwyt (wskaźnik).
 
 ' Sprawdź wersję VBA
 #If VBA7 Then
    ' Aplikacja używa 32 lub 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
    ' Zastosuj atrybut PtrSafe po instrukcji Declare
    ' i zmień deklaracje uchwytów i wskaźników na typ LongPtr
     Public Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As LongPtr, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As LongPtr, _
                                                  lParam As Any) As LongPtr
 #Else
    ' Aplikacja używa 32 bitowej wersji VBA 6 (Office 2007 i niższe wersje)
    ' Nie używaj atrybutu PtrSafe po instrukcji Declare
    ' deklaracje uchwytów i wskaźników są typu Long
     Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As Long, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As Long, _
                                                  lParam As Any) As Long
 #End If
 

Ponieważ atrybut PtrSafe nie jest wymagany w 32 bitowym środowisku VBA 7, możemy skorzystać ze stałej kompilacji Win64 i typu LongLong, bez używania typu LongPtr.

 
 ' Sprawdź, czy środowisko VBA jest 64 bitowe
 #If Win64 Then
    ' Aplikacja używa 64-bitowej wersji VBA 7 (Office 2010 i wyższe wersje).
    ' Zastosuj atrybut PtrSafe po instrukcji Declare
    ' i zmień deklaracje uchwytów i wskaźników na typ LongLong
     Public Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As LongLong, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As LongLong, _
                                                  lParam As Any) As LongLong
 #Else
    ' Aplikacja używa 32 bitowej wersji VBA 7 (Office 2010+) lub VBA 6 (Office 2007-)
    ' Nie używaj atrybutu PtrSafe po instrukcji Declare
    ' deklaracje uchwytów i wskaźników powinny być typu Long
     Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
                                                  ByVal hwnd As Long, _
                                                  ByVal wMsg As Long, _
                                                  ByVal wParam As Long, _
                                                  lParam As Any) As Long
 #End If
 

Powiem szczerze. Nie wiem która metoda deklaracji jest optymalna. Czy korzystanie ze stałej kompilacji VBA7 i typu LongPtr do deklaracji uchwytów i wskaźników, czy korzystanie ze stałej kompilacji Win64 i typu LongLong do deklaracji uchwytów i wskaźników w środowisku 64 bitowym, a typu Long w 32 bitowych VBA 7 i VBA 6.