MS Access - VBA - String manipulation - ms-access

I developed some code for an Access Database that manipulates a string with a statement like:
myString = Left(myString, somePosition) & Right(myString, someOtherPosition)
the above is part of a loop that has thousands of iterations and the variable myString is thousand of characters long.
I know the above code is bad practice in Java and a StringBuffer should be used instead of a string.
My code is taking a lot of time to run (about 7 minutes) and I suspect the problem might be related to the heavy string manipulation that is going on. Can you please confirm if there is anything similar to StringBuffer in VBA that could improve the efficiency of my code?
Update: full code with StringBuilder
Function SelectColumns2(str As String, columns As String, separator As String) As String
'column_number is the number of the column we are reading when we loop through a line
'z is the counter of the field (a portion of str between two separators)
'i is the counter of the str (the position of the modified string)
Dim column_number As Integer, i As Double, z As Integer, leftPosition As Double
'stringbuilder that stores the string that will represent the final file
Dim sb As StringBuilder, leftStr As StringBuilder, rightStr As StringBuilder
Set sb = New StringBuilder
Set leftStr = New StringBuilder
Set rightStr = New StringBuilder
sb.Append str
column_number = 1
i = 1 ' full str
z = 0 ' full field
While sb.Length >= i
z = z + 1
If Mid(sb.Text, i, 1) = separator Then
If InStr(1, columns, "/" & column_number & "/") = 0 Then
leftStr.Append left(sb.Text, i - z)
rightStr.Append right(sb.Text, sb.Length - i)
sb.Clear
sb.Append leftStr.Text
sb.Append rightStr.Text
leftStr.Clear
rightStr.Clear
i = i - z
End If
column_number = column_number + 1
z = 0
ElseIf Mid(sb.Text, i, 1) = Chr(10) Then
If InStr(1, columns, "/" & column_number & "/") = 0 Then
leftPosition = max((i - z - 1), 0)
If leftPosition = 0 Then
leftStr.Append left(sb.Text, leftPosition)
rightStr.Append right(sb.Text, sb.Length - i)
sb.Clear
sb.Append leftStr.Text
sb.Append rightStr.Text
Else
leftStr.Append left(sb.Text, leftPosition)
rightStr.Append right(sb.Text, sb.Length - i + 1)
sb.Clear
sb.Append leftStr.Text
sb.Append rightStr.Text
End If
leftStr.Clear
rightStr.Clear
i = i - z
End If
column_number = 1
z = 0
End If
i = i + 1
Wend
SelectColumns2 = left(sb.Text, sb.Length - 1)
End Function

You can use CreateObject to create the .Net stringbuilder class. Note that you will have to have the relevant .Net library installed, and VBA does not support overloading, so it will handle a little differently than in VB.Net.
Sample code:
Public Sub TestSB()
Dim sb As Object
Set sb = CreateObject("System.Text.StringBuilder")
sb.Append_3 "Hello"
sb.Append_3 " "
sb.Append_3 "World"
sb.Append_3 "!"
Debug.Print sb.ToString
End Sub
Alternatively, you can build your own stringbuilder. This answer provides a stringbuilder class, and this question also shows some sample code.

You can - for an extremely simple implementation - use Mid.
For example, this code runs in about 0.1 ms for the quite large strings entered:
Public Function ChopString() As String
Dim Source As String
Dim LeftPart As Long
Dim RightPart As Long
Dim Result As String
Source = String(100000, "x")
LeftPart = 30000
RightPart = 40000
Result = Space(LeftPart + RightPart)
Mid(Result, 1) = Left(Source, LeftPart)
Mid(Result, 1 + LeftPart) = Right(Source, RightPart)
ChopString = Result
End Function
For smaller strings of a few K, it runs way faster.

Related

Search string in between HTML tags and replace

Newbie here. I have an HTML source code and would like to look for string in between header tags <h1></h1>, <h2></h2>till <h5></h5> and then convert the text to lower case except acronyms or abbreviations (these are all capitals in 2 or more characters). And make sure that all country names in between use proper case.
As an example: It will find <h1>HR Policies and Procedures for Hiring - argentina LTD</h1>
It will convert it to:<H1>HR policies and procedures for hiring - Argentina LTD</H1>
I've tried a user defined function for Excel VBA found online: CapIt(A2). It uses Search, Split and Join. I'm not able to put them together to come up with the result. Would appreciate very much your help. Thank you.
Code I saw online as initial reference:
Function Capit(s As String)
Dim v As Variant, j As Long
v = Split(s, " ") ' separates the words
For j = LBound(v) To UBound(v)
If StrComp(v(j), UCase(v(j)), vbBinaryCompare) <> 0 Then v(j) = StrConv(v(j), vbProperCase)
Next j
Capit = Join(v, " ") ' joins the words
End Function
'Added this code below, can we use the results to lowercase the string and exclude the output in this function
Function FindAcronyms(yourWord As String)
Dim I As Integer
Dim ctr As Integer
FindAcronyms = Null
For I = 1 To Len(yourWord)
If Asc(Mid(yourWord, I, 1)) <= 90 And _
Asc(Mid(yourWord, I, 1)) >= 65 Then
If ctr > 0 Then
FindAcronyms = FindAcronyms & Mid(yourWord, I - 1, 1)
End If
ctr = ctr + 1
Else
If ctr > 1 Then
FindAcronyms = FindAcronyms & Mid(yourWord, I - 1, 1) & ", "
End If
ctr = 0
End If
Next
If ctr > 1 Then
FindAcronyms = FindAcronyms & Mid(yourWord, I - 1, 1)
End If
If Right(FindAcronyms, 2) = ", " Then
FindAcronyms = Left(FindAcronyms, Len(FindAcronyms) - 2)
End If
End Function
'the final look would be something like this
Sub TitleChange()
'define array
myarray = Range("A1:A100")
' Define the pattern
Dim pattern As String: pattern = "<h*>*</h*>" 'looks for the header tags
Dim f As Variant
For Each f In myarray
If f Like pattern = True Then Capital (f) 'changes all string to lower case except countries (to retain proper case) and acronyms (to retain uppercase)
Next f
End Sub
You can include the countries in an array
Sub Test()
Debug.Print Capital("HR Policies and Procedures for Hiring - argentina LTD")
End Sub
Function Capital(ByVal s As String)
Dim a, v As Variant, j As Long
a = Array("Argentina", "Egypt", "Enland")
v = Split(s, " ")
For j = LBound(v) To UBound(v)
If StrComp(v(j), UCase(v(j)), vbBinaryCompare) <> 0 Then v(j) = StrConv(v(j), vbLowerCase)
If Not IsError(Application.Match(v(j), a, 0)) Then v(j) = StrConv(v(j), vbProperCase)
Next j
Capital = Join(v, " ")
End Function
Added UDF that parses HTML code, used the Sub Test above as UDF Capital and UDF to bring together. Welcome suggestions to make it cleaner or more efficient
Dim rng As Range, cell As Range
Set rng = Range("A1:A5")
' Define the pattern
Dim pattern As String: pattern = "*<h?>*</h?>*"
' Check each item against the pattern
For Each cell In rng
If (cell Like pattern = True) Then
cell.Offset(0, 16).Value = cell.Value
cell.Offset(0, 16).Value = joinCell(Capital(StripHTML(cell)), cell.Offset(0, 0).Value) 'used UDF for striping innertext, applying rules and joining back string
End If
Next cell
End Sub

Speed up MS ACCESS VBA Script iterating over bytes in String

I need a very simple hash-function and based on some experiments with excel just a sum of byte values should do it:
Function HashPart(strVal As String) As Long
' work with byte representation for speed
Dim b() As Byte
b = strVal
Dim result As Long
result = 0
For i = 0 To UBound(b)
result = result + b(i)
Next
Quersumme = result
End Function
This is done many time over all records (about 100) resulting from a query:
Set rs = db.OpenRecordset(strSQL)
' Loop through records
Do While Not rs.EOF
resultHash = resultHash + HashPart(rs(0))
resultLen = resultLen + Len(rs(0))
rs.MoveNext
Loop
rs.Close
MyHash = Str(resultLen) & "-" & Str(resultHash)
This works well enough, but is very slow. My previous version iterating over the String using Mid was even slower, but now I am out of ideas how to improve this.
Is there a way to speed this up?
Edit: the problem wasn't in the hash function but in the query.
Test code with constant strings showed that the function itself is very fast. 10,000 calls with strings of ca. 110 characters take only 0.04 seconds.
Conclusion: the performance problem was in the query, not the hash function.
Function HashPart(strVal As String) As Long
' work with byte representation for speed
Dim b() As Byte
Dim result As Long
Dim i As Long
b = strVal
result = 0
For i = 0 To UBound(b)
result = result + b(i)
Next
HashPart = result
End Function
Sub TestHashPart()
Const NumRounds = 10000
Dim i As Long
Dim res As Long
Dim SumRes As Double ' avoid limitation of Long (2^31)
Dim S As String
Dim t1 As Single
t1 = Timer
For i = 1 To NumRounds
' constant string with tiny variations
S = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ " & CStr(i ^ 2)
res = HashPart(S)
' This would slow down the process dramatically. DO NOT activate for NumRounds > 1000 !
' Debug.Print i, res, Len(S), S
SumRes = SumRes + res
Next i
Debug.Print SumRes, Timer - t1 & " seconds"
End Sub
Function HashPart(strVal As String) As Long
' work with byte representation for speed
Dim b() As Byte
b = strVal
For i = 0 To UBound(b)
HashPart = HashPart + b(i)
Next
End Function
There's not much to improve, I think if you don't put the additional variable in there and don't set a number to 0 that defaults to 0 you're very slightly better off.

Moving through the Recordset in Access VBA

I have a simple function using Excel VBA for calculating volatility. It takes as inputs a column of numbers (Zeros) and two dates. The code is:
Function EWMA(Zeros As Range, Lambda As Double, MarkDate As Date, MaturityDate As Date) As Double
Dim vZeros() As Variant
Dim Price1 As Double, Price2 As Double
Dim SumWtdRtn As Double
Dim I As Long
Dim m As Double
Dim LogRtn As Double, RtnSQ As Double, WT As Double, WtdRtn As Double
vZeros = Zeros
m = Month(MaturityDate) - Month(MarkDate)
For I = 2 To UBound(vZeros, 1)
Price1 = Exp(-vZeros(I - 1, 1) * (m / 12))
Price2 = Exp(-vZeros(I, 1) * (m / 12))
LogRtn = Log(Price1 / Price2)
RtnSQ = LogRtn ^ 2
WT = (1 - Lambda) * Lambda ^ (I - 2)
WtdRtn = WT * RtnSQ
SumWtdRtn = SumWtdRtn + WtdRtn
Next I
EWMA = SumWtdRtn ^ (1 / 2)
End Function
The main feature enabling the function to work is the For loop. I want to re-create this in Access VBA using recordset objects. The recordset has the same fields as the Excel spreadsheet. I'm not exactly sure how to convert the code over, though. Here is what I have so far:
Function EWMA(rsCurve As Recordset, InterpRate As Double, Lambda As Double) As Double
Dim vZeros() As Variant
Dim Price1 As Double, Price2 As Double
Dim SumWtdRtn As Double
Dim I As Long
Dim mat As Date
Dim mark As Date
Dim LogRtn As Double, RtnSQ As Double, WT As Double, WtdRtn As Double
CurveInterpolateRecordset = Rnd()
If rsCurve.RecordCount <> 0 Then
vZeros = CVar(rsCurve.Fields("CurveInterpolateRecordset"))
mat = CDate(rsCurve.Fields("MaturityDate"))
mark = CDate(rsCurve.Fields("MarkDate"))
m = Month(mat) - Month(mark)
For I = 2 To UBound(vZeros, 1)
Price1 = Exp(-vZeros(I - 1, 1) * (m / 12))
Price2 = Exp(-vZeros(I, 1) * (m / 12))
LogRtn = Log(Price1 / Price2)
RtnSQ = LogRtn ^ 2
WT = (1 - Lambda) * Lambda ^ (I - 2)
WtdRtn = WT * RtnSQ
SumWtdRtn = SumWtdRtn + WtdRtn
Next I
EWMA = SumWtdRtn ^ (1 / 2)
End If
Debug.Print EWMA
End Function
The function is called in an earlier subroutine in Access. What am I missing in order to move through the recordset in Access, similar to looping through the spreadsheet in Excel VBA?
The easiest method would be to use GetRows to pull an array from your recordset:
Recordset.GetRows Method
Then the new code would be nearly a copy-n-paste of your proven code starting with basically this:
vZeros = rsCurve.GetRows(rsCurve.RecordCount)
As a side note you wouldn't need CDate here:
mat = rsCurve.Fields("MaturityDate").Value
Here are some basics about using a recordset.
Dim rs As New ADODB.Recordset
'Add fields to your recordset for storing data.
With rs
.Fields.Append "Row", adInteger
.Fields.Append "ColumnName2", adChar, 30
.Fields.Append "ColumnName3", adInteger
.Open
End With
Add records to it manually
rs.AddNew
rs.Fields("Row").Value = 1
rs.Fields("ColumnName2").Value = "Put some value in"
rs.Update
rs.AddNew
rs.Fields("Row").Value = 2
rs.Fields("ColumnName2").Value = "Put another value in"
rs.Update
You can also populate it with a query of a table.
Move to the begining of the recordset
If rs.EOF = False Then
rs.MoveFirst
End If
Loop through the recordset
Do While rs.EOF = False
msgbox(rs.Fields("ColumnName2").Value)
rs.MoveNext
Loop

Autoincrement a string's last integer value

I am using vb.net and reading a string from mysql database. Now the string contains "test 1". I want to increment the last digit (1) by the value 1. I somehow used string length and solved it but it works only for string up to "test 10" and then it keeps adding spaces for example: "test 13" and so on. I want to just have strings auto incremented like "test 20" or "test 100". How can I do so?
This is the code I am using:
If reader2.Read Then
var = reader2.GetInt64("version")
a = reader2.GetString("project_id")
location = reader2.GetString("location")
governorate = reader2.GetString("governorate")
memo = reader2.GetString("memo")
title = reader2.GetString("project_title")
var = var + 1
Dim st As String
st = a.Substring(0, a.Length - 2)
st = st & " " & var
b = st
b should have the value of "test 2" or the incremented final value
If in your case projected is “description value” then it is easily done with split function like below:
var = var + 1
Dim st As String
st = a.Split(" ")(0)
st = st & " " & var
b = st
The core issue is that you are using Substring(0, a.Length - 2) where 2 is a constant value which never increases even if the number of digits increases.
If there is a space between the project-name and the number you can use String.Split instead:
Dim version = reader2.GetInt64("version")
Dim projectID = reader2.GetString("project_id")
Dim token As String() = projectID.Split() ' splits by white-space characters '
Dim projectName As String = token(0).Trim()
Dim result As String = String.Format("{0} {1}", projectName, version + 1)
If you want to use Substring instead of Split:
' ... '
Dim projectName = projectID
Dim spaceIndex As Int32 = projectName.IndexOf(" ")
If spaceIndex >= 0 Then projectName = projectName.Substring(0, spaceIndex)
' ... '

Query not returning all records, need all records

I have written some code for retrieving 3 seperate columns from my database, yet for some reason it isn't loading all of the records which result from my query.
Or well, at least it doesn't seem to do so.
Also, for some reason it won't show me a messagebox which should tell me howmany records have been read after the reader is closed.
Here's my code:
Public Class frmPlayerLocations
Dim str(2), loc As String
Dim xx, yy As Integer
Dim itm As ListViewItem
Private Sub frmPlayerLocations_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ListView1.Columns.Add("ID", 60, HorizontalAlignment.Left)
ListView1.Columns.Add("Name", 115, HorizontalAlignment.Left)
ListView1.Columns.Add("Approximate Location", 115, HorizontalAlignment.Left)
Dim qry = "SELECT profile.unique_id, profile.name, survivor.worldspace FROM profile, survivor WHERE survivor.unique_id = profile.unique_id AND survivor.is_dead = '0' ORDER BY profile.name"
Dim connection As MySqlConnection
connection = New MySqlConnection()
connection.ConnectionString = "Host=" & adminPanel.IP & ";port=" & adminPanel.port & ";user=" & adminPanel.username & ";password=" & adminPanel.password & ";database=" & adminPanel.DBname & ";"
connection.Open()
Dim cmd As New MySqlCommand(qry, connection)
Dim reader As MySqlDataReader = cmd.ExecuteReader()
Dim count As Integer = 0
While reader.Read()
count += 1
str(0) = reader.GetString(0)
str(1) = reader.GetString(1)
loc = reader.GetString(2)
loc = Mid(loc, loc.IndexOf(",") + 3)
xx = CInt(Replace(Mid(loc, 1, loc.IndexOf(",")), ".", ",", 1, -1, CompareMethod.Text))
xx = (xx / 10000)
loc = Mid(loc, loc.IndexOf(",") + 2)
yy = CInt(Replace(Mid(loc, 1, loc.IndexOf(",")), ".", ",", 1, -1, CompareMethod.Text))
yy = 152 - (yy / 10000)
If xx < 100 Then
If xx < 10 Then
loc = "00" & xx.ToString & " | "
Else
loc = "0" & xx.ToString & " | "
End If
Else : loc = xx.ToString & " | "
End If
If yy < 100 Then
If yy < 10 Then
loc &= "00" & yy.ToString
Else
loc &= "0" & yy.ToString
End If
Else : loc &= yy.ToString
End If
str(2) = loc
itm = New ListViewItem(str)
ListView1.Items.Add(itm)
End While
reader.Close()
connection.Close()
MessageBox.Show(count)
End Sub
End Class
Edit: I noticed that when calling the form twice in a row, the second time I do get this error:
An unhandled exception of type 'System.ArgumentException' occurred in Microsoft.VisualBasic.dll
Additional information: Argument 'Length' must be greater or equal to zero.
And it refers to this line of code:
yy = CInt(Replace(Mid(loc, 1, loc.IndexOf(",")), ".", ",", 1, -1, CompareMethod.Text))
Last but not least, the values which are used in that line of code:
loc "7.305e-04]]" String
yy 131 Integer
PS: This may be helpful: the values which are in survivor.worldspace are in this format initially:
[168,[1291.16,5343.54,0.27]]
If the message box is not being displayed then the most likely situation is that an exception is being thrown inside the while loop which is probably silently caught somewhere else.
Unfortunately there are just too many places where an exception might occur withing that while loop so it's hard to say from just looking at that code. It could be trying to cast a DBNull to string, or an index out of bounds, etc.
My suggestion is to either step through with the debugger and identify the offending line, or put a try catch inside the while loop and put a break-point inside the catch. That should give you information about what (and where) the exception is occurring is..
Based on your update it looks like the problem is the arguments passed to the Mid() functions. Based on your data it looks like you are attempting to get a sub-string of loc using the start index of 1 and the end index of -1 which is what loc.IndexOf(",") returns in that case because there is no , (comma) in loc.
You probably want to re-factor that code a bit.. In particular it looks like you are actually trying to replace . with , but doing it after your attempt to call Mid(). That seems to be your problem!