poniedziałek, 16 maja 2016

• Konwersja: String na Hex

Czasami potrzebujemy przekonwertować ciąg znaków na zapis heksadecymalny. Zrobić taką konwersję możemy na dziesiątki sposobów. Podstawowe czynności to pobranie kolejnych (pojedynczych) znaków konwertowanego ciągu, odczytanie dla każdego znaku kodu ANSI w reprezentacji dziesiętnej, a następnie przekonwertowanie wartości dziesiętnej kodu ANSI na postać heksadecymalną. Podczas konwersji na postać heksadecymalną należy zadbać, by wartości liczbowe mniejsze od 16 dla których zapis heksadecymalny ma postać (&H0 = 0 do &HF = F) zapisać w postaci dwuznakowej (&H0=00 do &HF=0F), by można było jednoznacznie dokonać konwersji w kierunku przeciwnym tj. z zapisu heksadecymalnego na zapis dziesiętny.

Aby przekonwertować ciąg znaków (jego poszczególne znaki ) na zapis heksadecymalny posłużymy się funkcjami:
• Asc(ciąg)
zwracającą wartość typu Integer odpowiadającą kodowi znaku pierwszego elementu w ciągu znaków ciąg
• Hex(liczba)
zwracającą wartość typu String reprezentującą heksadecymalną (szesnastkową) wartość argumentu liczba
• StrConv(ciąg, konwersja)
zwracającą wartość typu Variant podtyp String w postaci ciągu znaków poddanych konwersji określonej przez argument konwersja
• Mid(ciąg, start[, długość])
zwracającą wartość typu Variant podtyp String zawierającą podaną liczbę znaków długość z ciągu znaków, począwszy od pozycji start.
• Right(ciąg, długość)
zwracającą wartość typu Variant podtyp String zawierającą podaną liczbę znaków długość począwszy od prawej strony ciągu znaków.
• Format(wyrażenie[, format[, pierwszy_dzień_tygodnia [, pierwszy_tydzień_roku]]])
zwracającą wartość typu Variant podtyp String zawierającą wyrażenie sformatowane zgodnie z instrukcjami zawartymi w wyrażeniu formatującym.
• String(liczba, znak)
zwracającą wartość typu Variant podtyp String zawierającą ciąg składający się ze znaku powtórzonego podaną liczbę razy
oraz instrukcją Mid(...)
• Mid(zmienna_znakowa, początek[, długość]) = ciąg
zastępującą podaną liczbę znaków w argumencie zmienna_znakowa typu Variant podtyp String znakami z innego ciągu znaków. Liczba zastępowanych znaków jest zawsze mniejsza lub równa liczbie znaków w zmiennej_znakowej

Wykorzystując wyżej wymienione funkcję przedstawię trzy wersje funkcji konwertujących ciąg znaków na zapis heksadecymalny.

1. Funkcja TextToHexMid(ByRef sText As String) As String
Dla każdego kolejnego znaku ciągu wejściowego, zwróconego przez funkcję Mid$, pobierany jest jego kod ANSI za pomocą funkcji Asc, który konwertowany jest do postaci heksadecymalnej. Następnie z przodu dopisywany jest znak "0" i z tak powstałego ciągu za pomocą funkcji Right$ pobierane są dwa ostatnie znaki.

Public Function TextToHexMid(ByRef sText As String) As String
Dim i As Long

  For i = 1 To Len(sText)
    TextToHexMid = TextToHexMid & Right$("0" & Hex$(Asc(Mid$(sText, i, 1))), 2)
  Next

End Function

2. Funkcja TextToHexFormat(ByRef sText As String) As String
Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów. Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej i formatowany za pomocą funkcji Format$ do postaci dwuznakowej.

Public Function TextToHexFormat(ByRef sText As String) As String
Dim aBytes() As Byte
Dim i As Long

  aBytes = StrConv(sText, vbFromUnicode)
  
  For i = LBound(aBytes) To UBound(aBytes)
    TextToHexFormat = TextToHexFormat & Format$(Hex$(aBytes(i)), "00")
  Next i

End Function

3. Funkcja TextToHex(ByRef sText As String) As String
Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów. Przygotowywany jest bufor wyjściowy dwukrotnie dłuższy od ciągu wejściowego. Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej i dopisywany za pomocą instrukcji Mid$ na kolejnych miejscach w buforze wyjściowym.
Option Compare Database
Option Explicit
' • Function TextToHex(ByRef sText As String) As String
'  Funkcja konwertująca ciąg znaków na na postać heksadecymalną
' --------------------------------------------------------------------------------------
' autor: Zbigniew Bratko - 04.2016
'
' Pobiera:
'  • sText - ciąg wejściowy, który ma zostać poddany konwersji.
' Zwraca:
'  Przy powodzeniu zwraca ciąg znaków przekonwertowany do postaci heksadecymalnej.
'  Każdy znak ciągu wejściowego [sText] zapisany jest za pomocą dwóch znaków. Dla znaków,
'  których kod ANSI jest mniejszy od 16, heksadecymalny zapis poprzedzony jest cyfrą "0" (ZERO)
'  Przy niepowodzeniu zwraca ciąg zerowej długości.
' --------------------------------------------------------------------------------------
' Ciąg wejściowy konwertowany jest za pomocą funkcji StrConv do tablicy bajtów.
' Przygotowywany jest bufor wyjściowy dwukrotnie dłuższy od ciągu wejściowego.
' Każdy element tablicy (kod ANSI) konwertowany jest do postaci heksadecymalnej
' i dopisywany za pomocą instrukcji Mid$ na kolejnych miejscach w buforze wyjściowym.
Public Function TextToHex(ByRef sText As String) As String
Dim aBytes() As Byte
Dim bAsc As Byte
Dim lLen As Long
Dim i As Long

  lLen = Len(sText)
  ' zamienia ciąg z postaci Unicode na znaki z domyślnej strony kodowej systemu (ANSI)
  aBytes = StrConv(sText, vbFromUnicode)
  ' przygotuj bufor wyjściowy (2x dłuższy)
  TextToHex = String(2 * lLen, vbNullChar)
  ' konwertuj poszczególne bajty do postaci heksadecymalnej
  For i = 0 To lLen - 1
    bAsc = aBytes(i)
    If bAsc > &HF Then
      Mid$(TextToHex, 2 * i + 1, 2) = Hex$(bAsc)
    Else
      Mid$(TextToHex, 2 * i + 1, 2) = "0" & Hex$(bAsc)
    End If
  Next

End Function

Prawie wszystko już wiemy na temat konwersji ciągu znaków na postać heksadecymalną. Piszę prawie ponieważ pozostało tylko przetestować szybkość działania wyżej przedstawionych funkcji.

Test będzie się składał z dwóch zadań:
• ciąg znaków "Ala ma Asa a Ola" (o długości 16 znaków) będzie 1000 razy konwertowany na postać heksadecymalną,
• ciąg znaków o długości 131 072 znaków będzie jednokrotnie poddany konwersji na postać heksadecymalną
Option Compare Database
Option Explicit

#If VBA7 Then
  Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
#Else
  Private Declare Function timeGetTime Lib "winmm.dll" () As Long
#End If


Public Function SpeedTextToHex()
Dim sText As String
Dim sRet As String
Dim lTime As Long
Dim i As Long
Const cMyText As String = "Ala ma Asa a Ola"
Const cForTo As Long = 1000

  ' rozruch
  sText = cMyText
  lTime = timeGetTime
    For i = 1 To cForTo
      DoEvents
    Next
  lTime = timeGetTime - lTime
  
  Debug.Print String(45, "-")
  Debug.Print "Ilość wywołań = " & cForTo, "Len(sText) = " & Len(sText)
  Debug.Print String(45, "-")
  
  ' wywołuj poszczególne funkcje 1000 razy dla ciągu o długości Len(sText) = 16 znaków
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHexFormat(sText)
    Next
  Debug.Print "TextToHexFormat", (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHexMid(sText)
    Next
  Debug.Print "TextToHexMid", , (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    For i = 1 To cForTo
      sRet = TextToHex(sText)
    Next
  Debug.Print "TextToHex", , (timeGetTime - lTime); "milisekund"

  '=================================================================
  ' utwórz ciąg znaków o długości 131 072 znaków
  sText = cMyText
  For i = 1 To 13
    sText = sText & sText
  Next
  
  Debug.Print String(45, "-")
  Debug.Print "Ilość wywołań = 1", "Len(sText) = " & Len(sText)
  Debug.Print String(45, "-")
  
  ' wywołaj poszczególne funkcje dla ciągu o długości Len(sText) = 131 072 znaków
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHexFormat(sText)
  Debug.Print "TextToHexFormat", (timeGetTime - lTime); "milisekund"
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHexMid(sText)
  Debug.Print "TextToHexMid", , (timeGetTime - lTime); "milisekund"
  
  sRet = ""
  lTime = timeGetTime
    sRet = TextToHex(sText)
  Debug.Print "TextToHex", , (timeGetTime - lTime); "milisekund"
 
End Function
  

Szybkość funkcji konwertującej krótki tekst jest bez znaczenia. Czy to będą 3/100 sekundy, czy 8/1000 lub 3/1000 sekundy dla pojedynczej operacji konwersji, praktycznie nie jesteśmy w stanie stwierdzić, która funkcja wykonuje się najdłużej. Różnice szybkości będą zauważalne dla bardzo dużej ilości operacji (rzędu 100 000 lub więcej). Dla krótkich tekstów funkcja TextToHex(...) jest ok. 3 razy szybsza od funkcji TextToHexMid(...) oraz 10 razy szybsza od funkcji TextToHexFormat(...)
Inaczej sprawa wygląda dla bardzo długich ciągów znaków. Przykładowo dla ciągu o długości 130 000 znaków obie funkcje: TextToHexMid(...) oraz TextToHexFormat(...) są przeraźliwie wolne w porównaniu z funkcją TextToHex(...), która jest ok. 400 x szybsza od obu funkcji.

środa, 11 maja 2016

• Test funkcji HexToDec

W poprzednim artykule HexToDec pisałem o problemach konwersji z zapisu postaci heksadecymalnej na postać dziesiętną przy użyciu funkcji Val(ciąg). Aby ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca, poprzez stosowanie znaków deklaracji typu liczby (sufiksów %&^. Zaproponowałem, by zamiast używać funkcji Val(ciąg) i sufiksów typu liczby (%&^) w funkcjach konwertujących, używać funkcji konwersji typu według poniższego schematu:


Debug.Print "CByte", CByte("&H" & sHex)
Debug.Print "CInt", CInt("&H" & sHex)
Debug.Print "CLng", CLng("&H" & sHex)
Debug.Print "CSng", CSng("&H" & sHex)
Debug.Print "CDbl", CDbl("&H" & sHex)
Debug.Print "CDec", CDec("&H" & sHex)
Debug.Print "CCur", CCur("&H" & sHex)
#If VBA7 Then
   Debug.Print "CLngLng", CLngLng("&H" & sHex)
   Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
#End If
Public Function HexToDec(ByVal sHex As String)

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

End Function

Na podstawie powyższego schematu, napisałem funkcją testującą konwersję z zapisu postaci heksadecymalnej na postać dziesiętną:


Public Function testHexToDec(ByVal sHex As String)
On Error GoTo ErrHandler
Const errOVERFLOW = 6
Const errMISMATCH = 13

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

ExiHere:
  Exit Function
ErrHandler:
  If Err.Number = errOVERFLOW Or Err.Number = errMISMATCH Then
    Debug.Print "Błąd nr " & Err.Number & "  " & Err.Description
    Resume Next
  Else
    MsgBox "Błąd nr " & Err.Number & vbNewLine & Err.Description
    Resume ExiHere
  End If
End Function 
 

Poniżej wyniki przykładowych wywołań funkcji konwersji typu:
&HFFFF,
&HFFFF FFFF,
&HFFFF FFFF FFFF,
&HFFFF FFFF FFFF FFFF,
&H8000,
&H8000 0000,
&H8000 0000 0000,
&H8000 0000 0000 0000 0000,
oraz test konwersji dolnej i górnej granicy zakresu liczbu typu Long
Hex$(-2 147 483 648) i Hex$(2 147 483 647)

Test w 64-bitowym środowisku VBA7 na konwersję do postaci heksadecymalnej i potem powrotną konwersję na postać dziesiętną, liczby 2 147 483 647 będącej górnym zakresem liczby typu Long

' górna granica zakresu Long = 2 147 483 647
   Hex$(2147483647) = 7FFFFFFF
   CLng("&H" & "7FFFFFFF") = 2147483647
' w wersji skróconej test ma postać:
   CLng("&H" & Hex$(2147483647)) = 2147483647
' i równość ta jest prawdziwa

oraz liczby -2 147 483 648 będącej dolną granicą zakresu liczby typu Long

' dolna granica zakresu Long = -2 147 483 648
   Hex$(-2147483648) = FFFFFFFF80000000
   CLng("&H" & "FFFFFFFF80000000") = Błąd nr 13. Type mismatch
' wynik nieznany, gdyż MS Access wyświetla komunikat:
' MS Access podczas konwersji na postać heksadecymalną potraktował liczbę -2147483648 jako typ LongLong i aby otrzymać prawidłową wartość, musimy przekonwertowanej liczbę z postaci heksadecymalnej na typ LonLong
   CLngLng("&H" & "FFFFFFFF80000000") = -2147483648

Wcześniej pisałem, by ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca. Jak widać, powinniśmy pilnować także typu liczby poddawanej konwersji na postać heksadecymalną.

' dolna granica zakresu Long = -2 147 483 646
   Hex$(CLng(-2147483648)) = 80000000
   CLng("&H" & "80000000") = -2147483648
' w wersji skróconej test ma postać:
   CLng("&H" & Hex$(CLng(-2147483648))) = -2147483648
' i równość ta jest prawdziwa

niedziela, 1 maja 2016

• Konwersja - HexToDec

Czasami potrzebujemy przekonwertować liczbę całkowitą na postać heksadecymalną. Żaden problem. Od zawsze w VBA istniała funkcja do tego celu przeznaczona. Jest nią funkcja Hex(liczba) zwracająca wartość typu String reprezentującą heksadecymalną (szesnastkową) wartość liczby (do ośmiu znaków szesnastkowych w środowisku 32-bitowym i szesnastu znaków w środowisku 64-bitowym. Obowiązkowy argument liczba jest dowolnym poprawnym wyrażeniem numerycznym lub wyrażeniem znakowym. Jeżeli argument liczba nie jest liczbą całkowitą, przed obliczeniem wartości funkcji Hex(liczba) jest ona zaokrąglana do najbliższej parzystej liczby całkowitej.
No to zróbmy mały test funkcji Hex(liczba):

Public Function TestHex()
Dim iInt As Integer
Dim lLng As Long
Dim llLngLng As LongLong

  Debug.Print "--------------------------------------------"
  Debug.Print "Hex$(65535)   = "; Hex$(65535)
  Debug.Print "Hex$(-1)      = "; Hex$(-1)
  Debug.Print "--------------------------------------------"
  iInt = -1: lLng = -1: llLngLng = -1
  Debug.Print "iInt=-1; lLng=-1; llLng=-1"
  Debug.Print "--------------------------------------------"
  Debug.Print "Hex$(iInt)    = "; Hex$(iInt)
  Debug.Print "Hex$(lLng)    = "; Hex$(lLng)
  Debug.Print "Hex$(llLngLng)= "; Hex$(llLngLng)
 
 DoCmd.RunCommand acCmdDebugWindow

End Function

Na pierwszy rzut oka wyniki nie są zbyt optymistyczne.
Liczba 65535 w zapisie heksadecymalnym ma postać &HFFFF.
Liczba -1 w zapisie heksadecymalnym ma również postać &HFFFF.
Liczba -1 przypisana do zmiennej typu Integer ma postać &HFFFF.
Liczba -1 przypisana do zmiennej typu Long ma postać &HFFFFFFFF.
Liczba -1 przypisana do zmiennej typu LongLong ma postać &HFFFFFFFFFFFFFFFF.

Jeżeli chodzi o konwersję z zapisu postaci heksadecymalnej na postać dziesiętną MS Access i jego VBA nie posiada wbudowanej funkcji dokonującej takiej konwersji. Ale w  artukule Q161304 How To Convert Hexadecimal Numbers to Long Integer możemy znaleźć rozwiązanie problemu konwersji liczby z reprezentacji heksadecymalnej, na postać dziesiętną za pomocą funkcji  Val(ciąg).

Funkcja Val(ciąg) zwraca wartość liczb tworzących ciąg w postaci wartości numerycznej odpowiedniego typu. Funkcja Val(...) przerywa odczyt ciągu przy pierwszym znaku, który nie jest fragmentem liczby. Symbole i znaki, które często stanowią element wartości numerycznych, na przykład znak dolara ($), czy przecinek (,) nie są rozpoznawane przez funkcję Val(...). Funkcja ta rozpoznaje jednak symbol podstawy &O (dla systemu ósemkowego) i &H (dla systemu szesnastkowego). Spacje, tabulatory i znaki wysuwu wiersza są pomijane. Liczby z zakresu &H80000000 (-2147483648) do &HFFFFFFFF (-1) oraz liczby z zakresu &H8000 (-32768) do &HFFFF (-1) są traktowane przez funkcję Val(ciąg) jako liczby ujemne.

Jak widzimy poniżej wyniki są trochę dziwne:

Val("&HFFFF") = -1
Val("&HFFFFFFFF") = -1
Val("&H" & Hex$(65535)) = -1

Przedstawiona w tym artykule funkcja HexToLong(...)

Function HexToLong(ByVal sHex As String) As Long
   HexToLong = Val("&H" & sHex & "&")
End Function

zwraca następujące wartości:

HexToLong("FFFF") = 65535
HexToLong("FFFFFFFF") = -1
HexToLong(Hex$(65535)) = 65535
HexToLong(Hex$(-1)) = 65535

Jak widać, zawsze jest coś nie tak. Aby ustrzec się błędów podczas konwersji z postaci heksadecymalnej na postać dziesiętną powinniśmy pilnować typów liczb, jakie powinna zwrócić funkcja konwertująca. W tym celu możemy użyć jednego ze znaków deklaracji (sufiksu).

Znak deklaracji (sufiks)Typ danychPrzykład
%IntegerDim intLiczba%
&LongDim lngLiczba&
^LongLongDim lnglngLiczba&
@CurrencyDim curLiczba@
!SingleDim sngLiczba!
#DoubleDim dblLiczba#

W wyniku konwersji z postaci heksadecymalnej na postać dziesiętną zawsze otrzymamy liczbę całkowitą, gdyż funkcja Hex(liczba) zaokrągla do najbliższej parzystej liczby całkowitej, więc przydatne mogą być jedynie funkcje konwertująca na liczbę typu Integer, Long lub LongLong (w środowisku VBA7).

Function HexToInteger(ByVal sHex As String) As Integer
   HexToInteger = Val("&H" & sHex & "%")
End Function
___________________________________________________________________________________

Function HexToLongLong(ByVal sHex As String) As LongLong
   HexToLongLong = Val("&H" & sHex & "^")
End Function

Moim zdaniem, zamiast używać funkcji Val(ciąg) i sufiksów typu (%,&, ^) w funkcjach konwertujących, możemy użyć funkcje konwersji typu według poniższego schematu:

Public Function HexToDec(ByVal sHex As String)

 Debug.Print "CByte", CByte("&H" & sHex)
  Debug.Print "CInt", CInt("&H" & sHex)
  Debug.Print "CLng", CLng("&H" & sHex)
  Debug.Print "CSng", CSng("&H" & sHex)
  Debug.Print "CDbl", CDbl("&H" & sHex)
  Debug.Print "CDec", CDec("&H" & sHex)
  Debug.Print "CCur", CCur("&H" & sHex)
  #If VBA7 Then
    Debug.Print "CLngLng", CLngLng("&H" & sHex)
    Debug.Print "CLngPtr", CLngPtr("&H" & sHex)
  #End If

End Function