|
| | | |
• II.4. VBA - Konwersja ciągów [3] •
- 4.20 Jak przyspieszyć operację wielokrotnej konkatencji (np. w pętli For i = 0 To 5000 ?
- 4.21 W jaki sposób ciąg znaków typu: 1h15m25s, 15m25s, 22h5s, 15h itd. przekonwertować na czas ?
- 4.22 W jaki sposób podzielić ciąg znaków na elementy stałej szerokości i rozdzielić je separatorem dowolnej długości ?
- 4.23 Jak przekonwertować tekst Unicode (UCS-2) w zapisie szesnastkowym (każdy bajt rozdzielony jest znakiem spacji) na tekst ANSI ?
- 4.24 Jak utworzyć hasło będące kombinacją dużych i małych liter, cyfr oraz znaków specjalnych ?
- 4.25 Jak przekonwertować tekst pisany systemem znaków UTF8 na WIN-1250 i odwrotnie ?
- 4.26 Jak przekonwertować tekst na zapis heksadecymalny (każda litera reprezentowana jest przez dwa znaki) ?
- 4.27 Jak pobrać dane z pliku, który nie zawiera separatorów, ale dane są o określonej długości ?
- 4.28 Jak pobrać z ciągu znaków liczby rozdzielone jednoznakowym separatorem (np. numer telefoniczny) ?
- <• idź do str. 1 •>
<• idź do str. 2 •>
| | | | |
|
| | |
|
4.20 Jak przyspieszyć operację wielokrotnej konkatencji (np. w pętli For i = 0 To 5000 ?
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
' Poniższy tekst będziemy w pętli For i = 0 To 5 000 Next łączyć ze sobą w jeden długi tekst o sumarycznej długości 1 320 000 znaków
Private Const MY_TEXT As String = _
"Rzekł pajączek do pajączki:" & vbNewLine & _
"Popatrz, ja mam złote rączki!" & vbNewLine & _
"Już uprzędłem dla was nową" & vbNewLine & _
"Pajęczynę własnościową." & vbNewLine & _
"Będziesz pewnie bardzo rada," & vbNewLine & _
"Bo to cud, mucha nie siada!" & vbNewLine & _
"Na to ona z gniewem wrzaśnie:" & vbNewLine & _
"A ma, durniu, siadać właśnie!" & vbNewLine & _
" (Bogdan Brzeziński)"
Private Const MY_FOR As Long = 5000

' • Metoda I - klasyczne łączenie ciągów znaków (konkatenacja)
Private Sub btnStrCat_Click()
Dim lStart As Long
Dim sText As String
Dim i As Long
lStart = timeGetTime
For i = 1 To MY_FOR
sText = sText & MY_TEXT
Next
Debug.Print "Konkatenacja: "; timeGetTime - lStart
End Sub
• czas wykonywania wynosi ok. 45 sekund

' • Metoda II - kolejne dopisywanie do otwartego pliku tymczasowego poszczególnych ciągów znaków i odczyt do zmiennej sText całej zawartości pliku
Private Sub btnTextToFileFromFile_Click()
Dim sTmpFile As String
Dim sText As String
Dim lStart As Long
Dim ff As Integer
Dim i As Long
lStart = timeGetTime
' wygeneruj ścieżkę pliku tymczasowego
sTmpFile = Environ$("TEMP") & "\~" & _
CStr(CLng(Now * 10000) + 1) & ".tmp"
' jeżeli plik tymczasowy istnieje, to go usuń
If Len(Dir(sTmpFile)) > 0 Then Kill sTmpFile
ff = FreeFile
' otwórz plik do zapisu
Open sTmpFile For Binary Access Write As #ff
For i = 1 To MY_FOR
sText = MY_TEXT
' sukcesywnie dopisuj ciąg znaków na końcu pliku
Put #ff, , sText
Next
Close #ff
ff = FreeFile
' otwórz plik do odczytu
Open sTmpFile For Binary Access Read As #ff
sText = String(LOF(ff), vbNullChar)
Get #ff, , sText
Close #ff
' usuń plik tymczasowy
If Len(Dir(sTmpFile)) > 0 Then Kill sTmpFile
Debug.Print "Zapis do pliku: "; timeGetTime - lStart
End Sub
• czas wykonywania wynosi ok. 0,086 sekundy. Szybkość tej metody jest rewelacyjna, ale niestety musimy pisać po dysku, co jest czasami niezbyt dobrym pomysłem.
A dlaczego szybkości obu metod tak znacznie się różnią od siebie (ok. 500 razy) wyjaśnia Robert Winkler:


<cyt>
Zamiast bawić się w zapisywanie danych na dysku
zastosuj lepiej coś działajacego jak StringBuilder
Dzięki temu że aplikacja nie będzie miała kontaktu z dyskiem
uzyskasz jeszcze większą prędkość działania.
Przykład takiej klasy znajdziesz na: StringBuilder Class for VB
(kod ten przeznaczony jest dla VB6, może więc nie działać z VBA)
Jaki jest powód takiego zachowania. Łączenie stringów w Visual Basicu
polega na tym iż runtime rezerwuje nowy, pusty obszar pamięci
o wielkości równej sumarycznej wielkości dwóch łączonych stringów
i do tego nowego obszaru kopiowana jest ich zawartość.
Dla każdej kolejnej operacji konkatenacji niezależnie czy zapisana jest z wykorzystaniem operatora + czy &
wykonywana jest ponownie powyższa operacja.
</cyt>
Przekładając to na język VB otrzymamy przykładową funkcję StrCat(...)
przedstawiającą mechanizm łączenia ciągów znaków w VB

Private Declare Function lstrcat Lib "kernel32" _
Alias "lstrcatA" _
(ByVal lpString1 As String, _
ByVal lpString2 As String) As Long
Private Declare Function lstrlen Lib "kernel32" _
Alias "lstrlenA" _
(ByVal lpString As String) As Long

Public Function StrCat(sString1 As String, sString2 As String) As String
Dim sBuffer As String
' przygotuj bufor o sumarycznej wielkości obu ciągów wejściowych
StrCat = String$(lstrlen(sString1) + lstrlen(sString2), vbNullChar)
' kopiuj pierwszy ciąg znaków
lstrcat StrCat, sString1
' kopiuj (dołącz) drugi ciąg znaków
lstrcat StrCat, sString2
End Function
Jako ciekawostkę dodam, że funkcja lstrlen(String) jest ok. 30% wolniejsza od wbudowanej funkcji Len(String)

' • Metoda III - w oparciu o wyjaśnienia Roberta Winklera :
<cyt>
... StringBuilder jest dodatkową klasą, zakładającą własny bufor wewnątrz którego łączone są stringi.
Za każdym razem kiedy wielkość wynikowego łańcuch znaków ma przekroczyc stworzony wcześniej bufor jego wielkość jest podwajana (jego wielkość wzrasta więc w postępie geometrycznym)
Dzięki wypracowanemu sporemu zapasowi miejsca w buforze StringBuilder może rzadziej alokować nową pamięć, nie musi też tak często kopiować dużych porcji danych, która to operacja występuje przy każdej normalnej konkatenacji.
</cyt>
Aby sprawdzić jak działa poniższy przykład utwórz nowy moduł klasy, wklej do niego kod i zapisz klasę jako clsStrCat
Private m_sBuffer As String
Private m_lPos As Long
Private m_lSizeBff As Long

Private Sub Class_Initialize()
m_lPos = 1
End Sub

Public Property Let StringAdd(sStrAdd As String)
Dim lNewLen As Long
lNewLen = m_lPos + Len(sStrAdd)
If lNewLen > m_lSizeBff Then
m_sBuffer = m_sBuffer & String$(lNewLen, vbNullChar)
m_lSizeBff = m_lSizeBff + lNewLen
End If
Mid$(m_sBuffer, m_lPos) = sStrAdd
m_lPos = lNewLen
End Property

Public Property Get StringGet() As String
StringGet = Left$(m_sBuffer, m_lPos - 1)
End Property

' przykładowe wywołanie:
Private Sub btnClsStrCat_Click()
Dim sText As String
Dim lStart As Long
Dim i As Long
Dim cls As clsStrCat
lStart = timeGetTime
Set cls = New clsStrCat
For i = 1 To MY_FOR
cls.StringAdd = MY_TEXT
Next
sText = cls.StringGet
Set cls = Nothing
Debug.Print "clsStrCat: "; timeGetTime - lStart
End Sub
• czas wykonywania wynosi ok. 0,116 sekundy.

' • Metoda IV - przy zastosowaniu funkcji API CopyMemory(...) i tablicy bajtów jako odpowiednika naszego ciągu znaków, a zwiększanie buforu wykonywać będziemy za pomocą ReDim Preserve m_bBuffer(0 To m_lSizeBff) eliminując tym samym konkatenację podczas powiększania buforu.
• Utwórz nowy moduł klasy, wklej do niego kod i zapisz klasę jako clsCopyMem
Private Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
Private m_bBuffer() As Byte
Private m_lPos As Long
Private m_lSizeBff As Long

Private Sub Class_Initialize()
' m_lSizeBff = 0
' ReDim m_bBuffer(0 To m_lSizeBff)
End Sub

Public Property Let StringAdd(sStrAdd As String)
Dim lLenStrAdd As Long
Dim lNewLen As Long
lLenStrAdd = Len(sStrAdd)
If lLenStrAdd > 0 Then
lNewLen = m_lPos + lLenStrAdd
If lNewLen > m_lSizeBff Then
m_lSizeBff = m_lSizeBff + lNewLen
ReDim Preserve m_bBuffer(0 To m_lSizeBff)
End If
CopyMemory m_bBuffer(m_lPos), ByVal sStrAdd, lLenStrAdd
m_lPos = lNewLen
End If
End Property

Public Property Get StringGet() As String
If m_lPos > 0 Then
ReDim Preserve m_bBuffer(0 To m_lPos - 1)
StringGet = StrConv(m_bBuffer(), vbUnicode)
End If
End Property

' przykładowe wywołanie:
Private Sub btnClsCopyMem_Click()
Dim sText As String
Dim lStart As Long
Dim i As Long
Dim cls As clsCopyMem
lStart = timeGetTime
Set cls = New clsCopyMem
For i = 1 To MY_FOR
cls.StringAdd = MY_TEXT
Next
sText = cls.StringGet
Set cls = Nothing
Debug.Print "clsStrCat: "; timeGetTime - lStart
End Sub
Zestawienie szybkości poszczególnych metod |
For i = ? | Znaków | TxtToFile | clsStrCat | CopyMem | classRW |
5 000 | 1 320 000 | 86 ms | 106 ms | 85 ms | 103 ms |
50 000 | 13 200 000 | 812 ms | 952 ms | 802 ms | 930 ms |
100 000 | 26 400 000 | 1 835 ms | 1 901 ms | 1 826 ms | 2 134 ms |
200 000 | 52 800 000 | 3 650 ms | 4 082 ms | 3 344 ms | 3 970 ms |
* zmodyfikowana przez Roberta Winklera klasa: Fast string concatenation
ΔΔΔ | | | | |
|
| | |
|
4.21 W jaki sposób ciąg znaków typu: 1h15m25s, 15m25s, 22h5s, 15h itd. przekonwertować na czas ?


<cyt>
Najlepsza konwersja to oczywiście do datetime (który zresztą jest podtypem Double). Służy do tego funkcja TimeSerial (+ewentualnie DateSerial).
Oczywiście trzeba sobie napisać funkcję, która wyłuska odpowiednie "składowe".
Mogłaby wyglądać następująco:
Option Compare Database
Option Explicit
Private Type args
d As Long
h As Long
m As Long
s As Long
End Type

Function GetTime(czas As String)
Dim r As args
Dim i As Integer
Dim retVal As String
Dim litera As String
retVal = "0"
For i = 1 To Len(czas)
litera = Mid(czas, i, 1)
If IsNumeric(litera) Then
retVal = retVal & litera
Else
Select Case litera
Case "d"
r.d = r.d + Val(retVal)
Case "h"
r.h = r.h + Val(retVal)
Case "m"
r.m = r.m + Val(retVal)
Case "s"
r.s = r.s + Val(retVal)
Case Else
End Select
retVal = "0"
End If
Next
GetTime = r.d + TimeSerial(r.h, r.m, r.s)
End Function

Obłuży różne dziwolągi:
1h23m4s | 01:23:04 |
12h13s | 12:00:13 |
15m25s | 00:15:25 |
22h5s | 22:00:05 |
15h | 15:00:00 |
34m23s | 00:34:23 |
3d23h345s | 02-01-1900 23:05:45 |
12hh34s12s34mm123ggabcdefg | 12:34:46 |
75m23s | 01:15:23 |
literały spoza "dhms" zignoruje, kilkakrotne wystąpienie tego samego literału zsumuje.
Czy to potrzebne, pewnie nie. Ale warto się zabezpieczać na wszelki wypadek.
Jeśli interwał przekroczy 1 (1 doba), to wyświetli jako datę, np. 02-01-1900 23:05:45, ale nie trzeba się tym przejmować, bo jest to tak naprawdę liczba 3,96232638888889, która bardzo ładnie zostanie zsumowana z innymi, w przypadku agregacji.
</cyt>
Tytułem testu spróbowałem Twoją funkcję przerobić tak, by nie działała na ciągach znaków (retVal , litera) i nie porównywać ciągów znaków
Case "d"
Case "h"
oraz wyeliminować Val(retVal) na rzecz porównywania liczb typu Byte, będących odpowiednikiem poszczególnych znaków ciągu:

Function GetTimeBis(czas As String)
Dim r As args
Dim aBytes() As Byte
Dim lRetVal As Long
Dim bAscPrev As Byte
Dim i As Long
Dim k As Long
' dodaj znak na początku stringu, przekształć ciąg na duże litery
' i przekonwertuj na tablicę bajtów
aBytes = StrConv(UCase(vbNullChar & czas), vbFromUnicode)
' i sprawdzaj od tyłu (po to ten znak na początku by się wykonało
' ostatnie dodawanie w Case bAscPrev dla k > 0
For i = UBound(aBytes) To LBound(aBytes) Step -1
Select Case aBytes(i)
Case 48 To 57
lRetVal = lRetVal + (aBytes(i) - 48) * 10 ^ k
k = k + 1
Case Else
If k > 0 Then
Select Case bAscPrev
Case vbKeyD
r.d = r.d + lRetVal
Case vbKeyH
r.h = r.h + lRetVal
Case vbKeyM
r.m = r.m + lRetVal
Case vbKeyS
r.s = r.s + lRetVal
Case Else
End Select
lRetVal = 0: k = 0
bAscPrev = aBytes(i)
Else
bAscPrev = aBytes(i)
End If
End Select
Next
GetTimeBis = r.d + TimeSerial(r.h, r.m, r.s)
End Function
Porównanie szybkości obu metod dla 50 000 wywołań |
Ciąg wejściowy | GetTime | GetTimeBis |
"15h" | 754 ms | 740 ms |
"12h13s" | 1 340 ms | 963 ms |
"1h23m4s" | 1 662 ms | 1 033 ms |
"12hh34s12s34mm123ggabcdefg" | 5 464 ms | 2 041 ms |
• Jak widać im dłuższy ciąg znaków, tym korzyść z takiego podejścia jest większa,
ΔΔΔ | | | | |
|
| | |
|
4.22 W jaki sposób podzielić ciąg znaków na elementy stałej szerokości i rozdzielić je separatorem dowolnej długości ?


' funkcja autorstwa Pawel81 po moich drobnych przeróbkach dostosowawczych:
Public Function strComposeBis(valStr As String, _
splitCount As Byte, _
Spliter As String) As String
' valStr - ciąg znaków do przedzielenia
' splitCount - ilość znaków w podziale
' Splitter - znak rozdzielający
Dim tmpStr As String
tmpStr = valStr
While Len(tmpStr) > 0
strComposeBis = strComposeBis & _
Left$(tmpStr, splitCount) & Spliter
tmpStr = Mid $(tmpStr, splitCount + 1)
Wend
End Function

' spróbujmy użyć instrukcję Mid$ by wyeliminować operacje konkatencji, co powinno przyspieszyć działanie funkcji, zwłaszcza dla długich ciągów znaków
Public Function zbDelimitedString(sStrIn As String, _
lLenField As Long, _
Optional sDelim As String = ";") As String
Dim lCountDelim As Long
Dim lLenDelim As Long
Dim lLenStr As Long
Dim lOffset As Long
Dim lStep As Long
Dim i As Long
lLenDelim = Len(sDelim)
lLenStr = Len(sStrIn)
If (lLenStr Mod lLenField) > 0 Then
lCountDelim = lLenStr \ lLenField
Else
lCountDelim = lLenStr \ lLenField - 1
End If
lLenStr = lLenStr + lLenDelim * (lCountDelim + 1)
' zapełnij bufor wyjściowy
zbDelimitedString = Space(lLenStr)
lStep = lLenField + lLenDelim
For i = 0 To lCountDelim
lOffset = i * lStep + 1
' wstawiaj z odpowiednim przesunięciem elementy ciągu wejściowego
Mid$(zbDelimitedString, lOffset, lLenField) = _
Mid$(sStrIn, i * lLenField + 1, lLenField)
Next
lLenStr = lLenStr - lLenDelim
For i = lLenField + 1 To lLenStr Step lStep
Mid$(zbDelimitedString, i, lLenDelim) = sDelim
Next
' dopisz separator na końcu
Mid$(zbDelimitedString, lLenStr + 1, lLenDelim) = sDelim
End Function
Porównanie szybkości obu metod dla 1000 wywołań |
Długość ciągu | zbDelimitedString | strComposeBis | Wsp. [DS/CB] |
100 znaków | 30 ms | 20 ms | 1,5 |
400 znaków | 130 ms | 60 ms | 2,2 |
1 600 znaków | 970 ms | 210 ms | 4,6 |
6 400 znaków | 15 040 ms | 840 ms | 17,9 |
12 800 znaków | 55 961 ms | 1 660 ms | 33,7 |
• Jak widać im dłuższy ciąg znaków tym korzyść z takiego podejścia jest większa,
ΔΔΔ | | | | |
|
| | |
|
4.23 Jak przekonwertować tekst Unicode (UCS-2) w zapisie szesnastkowym (każdy bajt rozdzielony jest znakiem spacji) na tekst ANSI ?
Ciąg wejściowy ma postać,
Const MY_UCS As String = _
"00 57 00 65 00 73 00 6f 01 42 00 79 00 63 00 68 " & _
"00 20 01 5a 00 77 00 69 01 05 00 74 00 20 00 64 " & _
"00 6c 00 61 00 20 00 43 00 69 00 65 00 62 00 69 " & _
"00 65 00 20 00 69 00 20 00 54 00 77 00 6f 00 69 " & _
"00 63 00 68 00 20 00 62 00 6c 00 69 00 73 00 6b " & _
"00 69 00 63 00 68 00 2e "
aby go przekonwertować, musimy odpowiedzieć sobie na kilka pytań:
1. Jak rozdzielić ciąg wejściowy na zbiory odpowiadające wartościom liczbowym poszczególnych znaków ?
- • Mamy do dyspozycji kilka podstawowych funkcji operujących na ciągach znaków:
- Mid(string, start[, length])
- zwraca wartość typu Variant (String) zawierającą podaną liczbę znaków z ciągu znaków.
- Len(ciąg | nazwa_zmiennej)
- zwraca wartość typu Long zawierającą liczbę znaków w ciągu znaków lub liczbę bajtów niezbędnych do przechowania zmiennej.
- Split(expression[, delimiter[, limit[, compare]]])
- zwraca jednowymiarową tablicą o dolnym indeksie równym zero, zawierającą wyszczególnioną liczbę podciągów.
- Replace(expression, find, replace[, start[, count[, compare]]])
- zwraca ciag znaków w którym określone podciągi zostały zastąpione, wyszczególnioną ilość razy, innym podciągiem .
Zadeklarujmy potrzebne zmienne:
Dim sHexLong As String
Dim sHexByte1 As String
Dim sHexByte2 As String
Dim sReplaced As String
Dim sHexByte() As String
Dim i As Long
Metoda 1 - Mid & Mid
' pobiera poszczególne bajty (po 2 znaki) i łączy je w jeden ciąg, tworząc szesnastkowy kod znaku
For i = 1 To Len(MY_UCS) Step 6
sHexLong = "&H" & Mid(MY_UCS, i, 2) & _
Mid(MY_UCS, i + 3, 2)
Next
Metoda 2 - Mid, Mid
' pobiera poszczególne bajty (po 2 znaki) zapisane szesnastkowo do dwóch różnych zmiennych, ale ich nie łączy ze sobą
For i = 1 To Len(MY_UCS) Step 6
sHexByte1 = "&H" & Mid(MY_UCS, i, 2)
sHexByte2 = "&H" & Mid(MY_UCS, i + 3, 2)
Next
Metoda 3 - Replace
' usuwa wszystkie spacje w ciągu wejściowym, a następnie pobiera po 2 bajty (4 znaki) tworząc szesnastkowy kod znaku
sReplaced = Replace(MY_UCS, " ", "", , , vbBinaryCompare)
For i = 1 To Len(sReplaced) Step 4
sHexLong = "&H" & Mid(sReplaced, i, 4)
Next
Metoda 4 - Split
' tworzy tablicę typu string, traktując spację jako separator, łączy ze sobą po dwa kolejne elementy tworząc szesnastkowy kod znaku, w tym przypadku nie jest stosowana funkcja Mid
sHexByte = Split(MY_UCS, " ", , vbBinaryCompare)
For i = 0 To UBound(sHexByte) - 1 Step 2
sHexLong = "&H" & sHexByte(i) & sHexByte(i + 1)
Next
Porównanie szybkości czterech metod dla 10 000 wywołań |
Metoda | Mid |
1. Mid & Mid | 1 993 ms |
2. Mid, Mid | 1 953 ms |
3. Replace | 1 802 ms |
4. Split | 2 334 ms |
2. W jakiej postaci ma być zbiór wartości liczbowych poszczególnych znaków i jak przekonwertować ten zbiór na tekst ?
- • Postać zbioru zależy od funkcji jakiej użyjemy do konwersji wartości numerycznych
na znakowe:
- Chr(charcode)
- zwraca wartość typu String zawierającą znak odpowiadający podanemu kodowi.
• charcode
Obowiązkowy argument identyfikujący znak powinien być w/g opisu typu Long, ale przykładowo dla litery "ł" funkcji ChrW można przekazać argument następująco:
• liczbowy, dziesiętnie ChrW(322),
• liczbowy, szesnastkowo ChrW(&H0142)
• liczbowy, przekonwertowany ciąg znaków ChrW(CLng("&H0142"))
• ciag znaków, dziesiętnie ChrW("322"),
• ciag znaków, szesnastkowo ChrW("&H0142")
Funkcja ChrW zawsze zwraca wartość typu String zawierającą znak w formacie Unicode z wyjątkiem platform gdzie format Unicode nie jest obsługiwany
- StrConv(string, conversion)
- zwraca wartość typu Variant (String) poddaną żądanej konwersji.
• string
w/g opisu argument jest zmienną znakową, ale można również w tym argumencie przekazać odpowiednio przygotowana tablicę typu Byte
np. dla litery "ł" tablica aBytes(0 To 3) będzie zawierała cztery elementy aBytes(0) = &H42: aBytes(1) = &H0: aBytes(2) = &H1: aBytes(3) = &H0
Możemy to sprawdzić w poniższy sposób:
Dim aBytes() As Byte
aBytes = StrConv("ł", vbUnicode)
For i = 0 To UBound(aBytes)
Debug.Print "&H" & Hex$(aBytes(i)); " ";
Next
• conversion
vbUnicode - zamienia ciąg na postać Unicode, stosując domyślną stronę kodową systemu.
vbFromUnicode - zamienia ciąg z postaci Unicode na znaki z domyślnej strony kodowej systemu.

Ze względu na szybkość działania odrzucamy najwolniejszy sposób oparty na funkcji Split, a tym samym zostają nam do przetestowania trzy metody:
Metoda 1. (Mid & Mid) + ChrW
• praktycznie dla metody tej dopisujemy jedną linijkę kodu konwertującą ciąg sHexLong na liczbę typu Long, którą funkcja ChrW$ konwertuje na znak i dołącza do zwracanego tekstu sStrOut :
For i = 1 To Len(MY_UCS) Step 6
sHexLong = "&H" & Mid(MY_UCS, i, 2) & Mid(MY_UCS, i + 3, 2)
sStrOut = sStrOut & ChrW(CLng(sHexLong))
Next
Metoda 2. (Mid, Mid)+ CopyMemory
Private Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
• w metodzie tej każdy bajt w zapisie szesnastkowym konwertujemy na liczbę, którą za pomocą funkcji CopyMemory wstawiamy w podpowiednie miejsce w przygotowanej uprzednio tablicy aBytes(). Tablica ta jest następnie konwertowana przez funkcję StrConv na ciąg znaków sStrOut :
ReDim aBytes(0 To (2 * Len(MY_UCS) / 3 - 1))
For i = 1 To Len(MY_UCS) Step 6
sHexByte1 = "&H" & Mid(MY_UCS, i, 2)
sHexByte2 = "&H" & Mid(MY_UCS, i + 3, 2)
CopyMemory aBytes(k + 2), CLng(sHexByte1), 2
CopyMemory aBytes(k), CLng(sHexByte2), 2
k = k + 4
Next
sStrOut = StrConv(aBytes(), vbFromUnicode)
Metody 3. (Replace) + ChrW
• podobnie jak w Metodzie 1. dopisujemy jedną linijkę kodu konwertującą ciąg sHexLong na liczbę typu Long, którą funkcja ChrW$ konwertuje na znak i dołącza do zwracanego tekstu sStrOut :
sReplaced = Replace(MY_UCS, " ", "", , , vbBinaryCompare)
For i = 1 To Len(sReplaced) Step 4
sHexLong = "&H" & Mid(sReplaced, i, 4)
sStrOut = sStrOut & ChrW(CLng(sHexLong))
Next
3. Przetestujmy szybkość powyżej opisanych funkcji z dodatkowymi zmianami:
- zamiast funkcji Mid i ChrW użyjemy funkcji tekstowych Mid$ i ChrW$
-
zamiast konkatenacji ciągów znaków zastosujemy instrukcję Mid$ zgodnie z poniższym przykładem:
Dim sStrOut As String
Dim sHexLong As String * 6
Dim i As Long
' zainicjuj zwracany ciąg znaków (przykładowo 100 znaków)
sStrOut = Space(100)
' wpisz jednorazowo dwa pierwsze znaki,
' aby nie robić tego za każdym razem w pętli
Mid$(sHexLong, 1, 2) = "&H"
For i = 1 To 100
' na trzecim i następnych miejscach wpisz cztery znaki
' liczby w zapisie szesnastkowym,
Mid$(sHexLong, 3, 4) = "0142"
' dopisuj kolejno przekonwertowane znaki w zwracanym ciągu
Mid$(sStrOut, i, 1) = ChrW$(CLng(sHexLong))
Next
Ostatecznie nasze funkcje będą miały postać:
' Metoda 1. Mid$ & Mid$ + inst. Mid$
Public Function zbUCS2Win_Mid(sUcsIn As String) As String
Dim sHexLong As String * 6
Dim lLenStr As Long
Dim i As Long, j As Long, k As Long
lLenStr = Len(sUcsIn)
zbUCS2Win_Mid = Space(lLenStr \ 6)
Mid$(sHexLong, 1, 2) = "&H"
j = 1
For i = 1 To lLenStr Step 6
Mid$(sHexLong, 3, 2) = Mid$(sUcsIn, j, 2)
Mid$(sHexLong, 5, 2) = Mid$(sUcsIn, j + 3, 2)
j = i + 6: k = k + 1
Mid$(zbUCS2Win_Mid, k, 1) = ChrW$(CLng(sHexLong))
Next
End Function
' Metoda 2. Mid$, Mid$ + CopyMempry + inst. Mid$
Private Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
Public Function zbUCS2Win_Mem(sUcsIn As String) As String
Dim sHexByte As String * 4
Dim aBytes() As Byte
Dim lLenStr As Long
Dim i As Long, j As Long, k As Long
lLenStr = Len(sUcsIn)
ReDim aBytes(0 To (2 * lLenStr \ 3 - 1))
j = 1: k = 0
sHexByte = "&H"
For i = 1 To lLenStr Step 6
Mid$(sHexByte, 3, 2) = Mid$(sUcsIn, j + 3, 2)
CopyMemory aBytes(k), CLng(sHexByte), 2
Mid$(sHexByte, 3, 2) = Mid$(sUcsIn, j, 2)
CopyMemory aBytes(k + 2), CLng(sHexByte), 2
k = k + 4: j = i + 6
Next
zbUCS2Win_Mem = StrConv(aBytes(), vbFromUnicode)
End Function
' Metoda 3. Replace + inst. Mid$
Public Function rkUCS2Win_Replace(sUcsIn As String) As String
Dim sHexLong As String * 6
Dim sReplaced As String
Dim lLenStr As Long
Dim i As Long, j As Long
sReplaced = Replace(sUcsIn, " ", "", , , vbBinaryCompare)
lLenStr = Len(sReplaced)
rkUCS2Win_Replace = Space(lLenStr \ 4)
Mid$(sHexLong, 1, 2) = "&H"
For i = 1 To lLenStr Step 4
Mid$(sHexLong, 3, 4) = Mid$(sReplaced, i, 4)
j = j + 1
Mid$(rkUCS2Win_Replace, j, 1) = ChrW$(CLng(sHexLong))
Next
End Function
Porównanie szybkości trzech metod dla 10 000 wywołań |
Metoda | Mid | Mid$ + ChrW$ | instr. Mid$ |
1. Mid & Mid + ChrW | 3 025 ms | 1 793 ms | 1 552 ms |
2. Mid, Mid + CopyMemory | 3 285 ms | 2 153 ms | 2 344 ms |
3. Replace + ChrW | 2 814 ms | 2 514 ms | 2 423 ms |
4. I ostatnie pytanie: Czy warto wkładać tyle trudu w optymalizację kodu i tracić czas, by na 10 000 operacjach zyskać ok. 1,5 sekundy ?
Oczywiście można sobie odpuścić taką optymalizację, jeżeli jesteśmy na 100% pewni, że nie spotka nas niespodzianka. Wynikowy tekst, po przetworzeniu testowego ciągu MY_UCS, zawiera 44 znaki i brzmi: "Wesołych Świąt dla Ciebie i Twoich bliskich."
Sprawdźmy, jak będzie się zmieniała szybkość przetwarzania coraz dłuższych ciągów znaków. Porównamy szybkość funkcji zbUCS2Win_Mid z poniższą funkcją dla jednorazowego wywołania dla ciągu sUcsIn dającego wynikowy tekst o coraz większej długości:
Public Function zbMidAndMid(sUcsIn As String) As String
Dim i As Long
For i = 1 To Len(sUcsIn) Step 6
zbMidAndMid = zbMidAndMid & _
ChrW$(CLng("&H" & _
Mid$(sUcsIn, i, 2) & _
Mid$(sUcsIn, i + 3, 2)))
Next
End Function
Porównanie szybkości dla jednorazowego wywołania |
Długość ciągu wynikowego | zbMidAndMid | zbUCS2Win_Mid |
5 632 znaków | 50 ms | 21 ms |
11 264 znaków | 181 ms | 40 ms |
22 528 znaków | 1 241 ms | 81 ms |
45 056 znaków | 3 755 ms | 160 ms |
90 112 znaków | 28 251 ms | 310 ms |
180 224 znaków | 193 448 ms | 621 ms |
A co będzie jeżeli zajdzie potrzeba odkodowywania kilkudziesięciu długich tekstów dziennie. Jeżeli nie będzie takiej potrzeby, to nic się nie stanie. Nikt nie zwróci uwagi na te 50 ms przyspieszenia ;-)
ΔΔΔ | | | | |
|
| | |
|
4.24 Jak utworzyć hasło będące kombinacją dużych i małych liter, cyfr oraz znaków specjalnych ?


Private m_sBigLetters As String
Private m_sSmallLetters As String
Private m_sSpecChars As String
Private m_lLenLett As Long
Private m_lLenSpec As Long

' ustaw jednorazowo (przy starcie) wszystkie potrzebne zmienne
Private Sub Form_Load()
Dim i As Long
For i = 33 To 126
Select Case i
Case 33 To 47, 58 To 64, 91 To 96, 123 To 126
m_sSpecChars = m_sSpecChars & Chr$(i)
Case 65 To 90
m_sBigLetters = m_sBigLetters & Chr$(i)
Case Else
End Select
Next
' dodaj ogonki
m_sBigLetters = m_sBigLetters & "ĄĆĘŁŃÓŚŻŹ"
m_sSmallLetters = LCase(m_sBigLetters)
m_lLenSpec = Len(m_sSpecChars)
m_lLenLett = Len(m_sBigLetters)
End Sub

' zwraca ciąg znaków zawierający kombinacją dużych i małych liter,
' cyfr oraz znaków specjalnych.
Private Function zbMakePassword(lLenPsw As Long) As String
Dim sPsw As String
Dim i As Long
Dim j As Long
Dim k As Long
k = lLenPsw \ 4
If k - lLenPsw / 4 = 0 Then k = k - 1
For i = 0 To k
Randomize
' wylosuj dużą literę
j = Int(m_lLenLett * Rnd) + 1
sPsw = sPsw & Mid$(m_sBigLetters, j, 1)
' wylosuj małą literę
j = Int(m_lLenLett * Rnd) + 1
sPsw = sPsw & Mid$(m_sSmallLetters, j, 1)
' wylosuj znak specjalny
j = Int(m_lLenSpec * Rnd) + 1
sPsw = sPsw & Mid$(m_sSpecChars, j, 1)
' wylosuj liczbę
j = Int(10 * Rnd)
sPsw = sPsw & CStr(j)
Next
' wymieszaj poszczególne znaki i obetnij do żądanej długości
zbMakePassword = Left$( zbMixString(sPsw), lLenPsw)
End Function

' Zwraca przypadkową permutację wejściowego ciągu znaków
Private Function zbMixString(ByVal sStrIn As String) As String
Dim i As Long
Dim j As Long
For i = Len(sStrIn) To 1 Step -1
j = Int(i * Rnd) + 1
zbMixString = zbMixString & Mid$(sStrIn, j, 1)
sStrIn = Left$(sStrIn, j - 1) & Mid$(sStrIn, j + 1)
Next
End Function
ΔΔΔ | | | | |
|
| | |
|
4.25 Jak przekonwertować tekst pisany systemem znaków UTF8 na WIN-1250 i odwrotnie ?
Private Declare Function WideCharToMultiByte Lib "kernel32.dll" ( _
ByVal CodePage As Long, _
ByVal dwFlags As Long, _
ByVal lpWideCharStr As Long, _
ByVal cchWideChar As Long, _
ByVal lpMultiByteStr As String, _
ByVal cbMultiByte As Long, _
ByVal lpDefaultChar As Long, _
ByVal lpUsedDefaultChar As Long) As Long
Private Declare Function MultiByteToWideChar Lib "kernel32.dll" ( _
ByVal CodePage As Long, _
ByVal dwFlags As Long, _
ByVal lpMultiByteStr As String, _
ByVal cbMultiByte As Long, _
ByVal lpWideCharStr As Long, _
ByVal cchWideChar As Long) As Long
Private Const CP_UTF8 As Long = 65001

Public Function UTF8toWin(sStrngUTF8 As String) As String
Dim lLenIn As Long
Dim lLenOut As Long
Dim lRet As Long
lLenIn = Len(sStrngUTF8)
' pobierz wielkość potrzebnego buforu na wyjściowy ciąg znaków WIN-1250
lLenOut = MultiByteToWideChar( _
CP_UTF8, 0&, sStrngUTF8, _
lLenIn, 0&, 0&)
' przygotuj bufor
UTF8toWin = String$(lLenOut, vbNullChar)
' konwertuj ciąg wejściowy
lRet = MultiByteToWideChar( _
CP_UTF8, 0&, sStrngUTF8, _
lLenIn, StrPtr(UTF8toWin), _
lLenOut)
UTF8toWin = Left$(UTF8toWin, lRet)
End Function

Public Function WinToUTF8(sStringIn As String) As String
Dim lLenIn As Long
Dim lLenOut As Long
Dim sBuffer As String
Dim lRet As Long
lLenIn = Len(sStringIn)
' pobierz wielkość potrzebnego buforu na wyjściowy ciąg znaków UTF8
lLenOut = WideCharToMultiByte( _
CP_UTF8, 0&, _
StrPtr(sStringIn), _
lLenIn, 0&, 0&, 0&, 0&)
' przygotuj bufor
WinToUTF8 = String(lLenOut, vbNullChar)
' konwertuj ciąg wejściowy
lRet = WideCharToMultiByte( _
CP_UTF8, 0&, _
StrPtr(sStringIn), _
lLenIn, WinToUTF8, _
lLenOut, 0&, 0&)
WinToUTF8 = Left$(WinToUTF8, lRet)
End Function
ΔΔΔ | | | | |
|
| | |
|
4.26 Jak przekonwertować tekst na zapis heksadecymalny (każda litera reprezentowana jest przez dwa znaki) ?

grupa: pl.comp.lang.vbasic
wątek: MD5 i Excel
przedstawił: Piotr Lipski

' Nieco zmodyfikowana (dostosowana do ogólniejszych potrzeb) funkcja Piotra.
Public Function plGetMD5HashHex(ByVal strData As String) As String
Dim strRet As String
Dim chrChar As String
Dim i As Long
For i = 1 To Len(strData)
chrChar = Hex$(Asc(Mid$(strData, i, 1)))
If Len(chrChar) = 1 Then chrChar = "0" & chrChar
strRet = strRet & chrChar
Next
plGetMD5HashHex = strRet
End Function
' Jeżeli zamiast operacji na ciągach znaków, zastosujemy działanie na tablicy bajtów zawierającej kody ASCII poszczególnych znaków, to możemy przyspieszyć wykonywanie funkcji w porównaniu do plGetMD5HashHex (...) od ok. 2x dla krótkich ciągów znaków, do ok. 100x dla długich ciągów znaków (ok. 30 000 znaków)

Public Function zbTextToHex(ByVal sText As String) As String
Dim aBytes() As Byte
Dim bAsc As Byte
Dim lLen As Long
Dim i As Long
lLen = Len(sText)
' konwertuj tekst na tablicę kodów ASCII
aBytes = StrConv(sText, vbFromUnicode)
' przygotuj bufor wyjściowy (2x dłuższy)
zbTextToHex = String(2 * lLen, vbNullChar)
' konwertuj poszczególne bajty do postaci heksadecymalnej
For i = 0 To lLen - 1
bAsc = aBytes(i)
If bAsc < &H10 Then
Mid$(zbTextToHex, 2 * i + 1, 2) = "0" & Hex$(bAsc)
Else
Mid$(zbTextToHex, 2 * i + 1, 2) = Hex$(bAsc)
End If
Next
End Function
ΔΔΔ | | | | |
|
| | |
|
4.27 Jak pobrać dane z pliku, który nie zawiera separatorów, ale dane są o określonej długości ?


' utwórzmy strukturę zawierającą elementy o długościach
' odpowiadającym długościom poszczególnych pól w umownym
' rekordzie pliku tekstowego
Private Type MY_RECORD
fld1 As String * 8
fld2 As String * 12
fld3 As String * 14
End Type
Private Const REC_LEN As Long = 8 + 12 + 14

Private Sub btnTest_Click()
Dim dbs As DAO.Database
Dim rst As DAO.Recordset
Dim sFilePath As String
Dim snRecCount As Single
Dim ff As Integer
Dim mrec As MY_RECORD
sFilePath = "c:\Test.txt"
snRecCount = FileLen(sFilePath) / REC_LEN
' sprawdź, czy długość pliku jest wielokrotnoscią długości rekordu
If snRecCount - Int(snRecCount) <> 0 Then
MsgBox "Błąd długości pliku"
Exit Sub
End If
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset("tTestTable", dbOpenDynaset)
ff = FreeFile
Open sFilePath For Binary Access Read As #ff
Do While Loc(ff) < LOF(ff)
With rst
.AddNew
' pobieraj pojedyncze (umowne) rekordy do struktury mrec
Get #ff, Loc(ff) + 1, mrec
' zapisz pola rekordu w tabeli
!NameField1 = mrec.fld1
!NameField2 = mrec.fld2
!NameField3 = mrec.fld3
.Update
End With
Loop
Close #ff
rst.Close
Set rst = Nothing
Set dbs = Nothing
End Sub
ΔΔΔ | | | | |
|
| | |
|
4.27 Jak pobrać dane z pliku, który nie zawiera separatorów, ale dane są o określonej długości ?


' utwórzmy strukturę zawierającą elementy o długościach
' odpowiadającym długościom poszczególnych pól w umownym
' rekordzie pliku tekstowego

Public Function NumberFromString(sFrom As String, _
Optional sDelim As String = "-") As String
Dim sBytes() As Byte
Dim i As Long
Dim sRet As String
Dim bDelim As Byte
Dim fIsNumber As Boolean
sFilePath = "c:\Test.txt"
snRecCount = FileLen(sFilePath) / REC_LEN
' sprawdź, czy długość pliku jest wielokrotnoscią długości rekordu
If snRecCount - Int(snRecCount) <> 0 Then
MsgBox "Błąd długości pliku"
Exit Sub
End If
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset("tTestTable", dbOpenDynaset)
ff = FreeFile
Open sFilePath For Binary Access Read As #ff
Do While Loc(ff) < LOF(ff)
With rst
.AddNew
' pobieraj pojedyncze (umowne) rekordy do struktury mrec
Get #ff, Loc(ff) + 1, mrec
' zapisz pola rekordu w tabeli
!NameField1 = mrec.fld1
!NameField2 = mrec.fld2
!NameField3 = mrec.fld3
.Update
End With
Loop
Close #ff
rst.Close
Set rst = Nothing
Set dbs = Nothing
End Sub
ΔΔΔ | | | | |
|
| |