4

I am trying to learn how to use stringbuilder functions in VBA, but am having trouble finding resources in VBA on how to use them. I could use some help to use a pre-made stringbuilder class.

I do know that each of these subs play some role in writing a final code. For example, I have seen other examples that use "string.append" but I am not sure if that is how it works in this case. I could use a little bit of insight into how to utilize this code. Please help!

The best way for me to understand is if someone can throw together a few lines of sample code using this example stringbuilder class. Thank you!!

Private m_arrBuffer
Private m_strDelimiter
Private Sub Class_Initialize()
    m_arrBuffer = Array()
    m_strDelimiter = “”
End Sub

Private Sub Class_Terminate()
    m_arrBuffer = Empty
End Sub

Public Property Get Delimiter()
    Delimiter = m_strDelimiter
End Property

Public Property Let Delimiter(strDelimiter)
    m_strDelimiter = strDelimiter
End Property

Public Sub Append(strValue)
    ReDim Preserve m_arrBuffer(UBound(m_arrBuffer) + 1)
    m_arrBuffer(UBound(m_arrBuffer)) = strValue
End Sub

Public Sub AppendLine(strValue)
    Me.Append strValue & vbCrLf
End Sub

Public Sub Compact()
    If Not Me.Delimiter = “” Then
        strOriginalDelimiter = Me.Delimiter
        Me.Delimiter = “”
    End If
    strTemp = Me.ToString
    m_arrBuffer = Array()
    Me.Append strTemp
    Me.Delimiter = strOriginalDelimiter
End Sub

Public Function ToArray()
    ToArray = m_arrBuffer
End Function

Public Function ToString()
    ToString = Join(m_arrBuffer, m_strDelimiter)
End Function
Bond
  • 16,071
  • 6
  • 30
  • 53
Coding Novice
  • 437
  • 2
  • 8
  • 22
  • This code needs to be put in a class module. Your calling code then creates a new instance of the class and calls its methods as needed (eg the `Append` method) – Rory Aug 03 '15 at 13:02
  • Thank you very much. I understand now and I have created a class! – Coding Novice Aug 03 '15 at 13:41

4 Answers4

5

String concatenation (&) in VBA is notoriously slow, so often "stringbuilder" classes like this one are used to speed up the process if you have a large number of strings you need to combine.

The general idea is to use an Array() to store individual string components and then combine all strings once, when you need them, using the Join() function. The array is automatically resized as strings are added. Many use a "GrowBy" feature (although this one doesn't) to grow the array by a static size or factor as the array limit is reached. That can improve performance as well, since calling ReDim Preserve for every string insertion can take its toll.

To answer your question, pretend you needed to build a portion of an HTML file. You could use the shown string class like so:

Dim sb
Set sb = New StringBuilder      ' Guessing here. You haven't shown the class name.
sb.Append "some string"
sb.Append "another string"
sb.Append "a third string"
....
sb.Delimiter = "<br>"
myHtmlFile.Write sb.ToString()

Would print the following:

some string<br>another string<br>a third string

That's the general idea. Avoid the use of & as much as possible by using an array and you should see some significant performance improvements.

Bond
  • 16,071
  • 6
  • 30
  • 53
  • This was certainly the most complete explenation. Thank you :) – Coding Novice Aug 03 '15 at 13:19
  • Just one question. In the portion that you say 'guessing here, what would I need to do to show the class name or at least define it. As it stands i am getting an error at this portion, but I dont know how to solve that. – Coding Novice Aug 03 '15 at 13:26
  • 1
    The error is that the user-defined type is not defined – Coding Novice Aug 03 '15 at 13:27
  • In VBA, you would need to add a `Class Module` to your project (as you would add a `Module` or `UserForm`). Paste the code above into this `Class`. By default, the class will have a name of `Class1` but you can change that in the `Properties` window. In my example above, I assume the class was named `StringBuilder`. – Bond Aug 03 '15 at 13:28
  • I created a class. I am actually very excited because I learned something very new :) I now will be able to play with classes, Yay! – Coding Novice Aug 03 '15 at 13:42
  • @Bond Good information. Question: Is it more efficient to keep resizing the array; or would it be more efficient to "collect" all the substrings into a Collection object using the Add method; and then create an array of the desired size and populate and join only after all of the substrings have been collected? – Ron Rosenfeld Aug 03 '15 at 17:01
  • @RonRosenfeld It's certainly a question worth asking. And it may depend on the intended application. Arrays have less overhead than Collections and can be joined natively into a string. Collections, as you said, can continue to grow without the need for `ReDim Preserve`. But, at a low level, you'd have to imagine that the Collection is doing something similar anyway (growing dynamically) since it has no clue how much memory to allocate, either. For critical apps, I like to do my own testing. If I were using the class above, however, I'd use a grow factor to minimize `ReDim Preserve` calls. – Bond Aug 03 '15 at 17:08
  • @Bond I guess the question has to do with the overhead incurred by `Redim Preserve` vs. `Collection.Add`. And it may vary in unexpected ways, also. I guess testing in a particular application is the way to go, though. Thanks. – Ron Rosenfeld Aug 03 '15 at 17:20
  • @RonRosenfeld - That certainly plays a role. With an array at least, you can control the number of `ReDim Preserve` calls. If you (unknowingly) need to store 100 strings and you use a class like above with an initial size of 10 and a grow factor of 2, you'll have to call `ReDim Preserve` 5 times (`10 -> 20`, `20 -> 40`, `40 -> 80`, `80 -> 160`, `160 -> 100`). Much better than the 100 times needed above. How many times does `Collection.Add()` need to grow/resize the collection? You really have no control over it. But, to your point, it could be efficient enough to not matter. – Bond Aug 03 '15 at 17:30
  • @Bond Well, there's a bit more overhead if you are using a string builder with a delimiter, and your array is oversized, you need to deal with possible extra delimiters at the end. Again, testing. – Ron Rosenfeld Aug 03 '15 at 17:39
  • @RonRosenfeld - Well, if you're growing by a factor, you'll always do one final ReDim Preserve (to the exact size filled) before joining (that's the 160 -> 100 step in my previous comment). So there should should never be any extra array elements/delimiters. – Bond Aug 03 '15 at 17:49
  • @Bond Yes, a final `Redim Preserve` is how I would deal with that issue. But there is that extra overhead of keeping track of the number of elements, as opposed to just looking at the `Count` property of the `Collection` object (which is one of the things that adds to the Collection method overhead). – Ron Rosenfeld Aug 03 '15 at 18:34
1

I'm not sure I follow your question since VBA has no string builder. Strings can simply be built.

Dim strTemp as string
Dim strCountry as string

strCountry = "Nowhere"
strTemp = "I am from " & strCountry
MatthewD
  • 6,719
  • 5
  • 22
  • 41
  • Yes I believe (from my limited knowledge) That string builders in VBA actually need to be made to be used. Hence the "sample code" i included. Someone else coded this for use as a string builder – Coding Novice Aug 03 '15 at 13:20
  • It is an old thread but not to mention that String Builders are faster, the real need for it is the [Out of String Space](https://stackoverflow.com/questions/93932/out-of-string-space-in-visual-basic-6)... if you play a lot with concatenating &, you will find it on the next corner – Marcelo Scofano Diniz Mar 29 '20 at 02:16
1

I agree with @MatthewD but in case you are doing this for practice I will add some more info.

Here are some links on declaring variables and using classes in VBA from Chip Pearson's great site.

You need to add a new Class Module to your VBA project and paste in your code. Be careful - you speech marks " in your example might need to be replaced, they've been mangled into a similar looking but different character somewhere. Change the name of the class module to "StringBuilder" and then you can use the code in an ordinary module like this:

Sub test1()

Dim strBld As StringBuillder

    Set strBld = New StringBuillder

    strBld.Append "Hello"
    strBld.Append " world"
    strBld.Append "!"

    Debug.Print strBld.ToArray(0)
    Debug.Print strBld.ToString

End Sub
Community
  • 1
  • 1
ChipsLetten
  • 2,923
  • 1
  • 11
  • 28
1

This is an old question and the OP has probably moved on. However, when I looked for something on this problem, this was the page I found. In case others find it, here is my attempt at a better answer and a link to an even better answer.

I cannot find an adequate explanation of the problem in either the question or any of the previous answers. In my experiments, the accepted answer performs worse than the inbuilt method of building a long string.

The problem

Suppose I write:

StrVar = "abcde"
StrVar = StrVar & "fghij"
StrVar = StrVar & "klmno"

For the first statement, the VBA interpreter finds memory sufficient to hold 5 characters, writes "abcde" to that memory and points StrVar at that memory.

For the second statement, the VBA interpreter finds memory sufficient to hold 10 characters, copies the existing contents of StrVar to that memory, appends "fghij", points StrVar at that memory and releases the old memory for garbage collection.

For the third statement, the VBA interpreter find memory sufficient to hold 15 characters, copies the existing contents of StrVar to that memory, appends "klmno", points StrVar at that memory and releases the old memory for garbage collection.

The point is VBA strings are immutable; they are never changed. Instead, every time your macro amends a string, new memory is found for the amended string and the memory for the original string is released. For a few string amendments, nothing is done about the released memory so there is no obvious time wasted building a long string. But, if you do this often enough, the interpreter runs out of new memory and it has to call the garbage collector. The garbage collector works through all the memory identifying what bits are still being used and which bits aren’t. It moves all the bits that are still being used together leaving the unused bits as new, available memory. Control is returned to the interpreter which continues handling new string assignments until it runs out of new memory again.

If you are building a longer and longer string, the interpreter runs out of memory faster and faster until your macro slows to a crawl.

What I wanted to achieve, my solution and a faster solution

I wanted to convert an Excel range to Html. Ignoring formatting, I wanted to create a string like this:

<table>
  <tr>
    <td> … </td>
    <td> … </td>
  </tr>
  <tr>
    <td> … </td>
    <td> … </td>
  </tr>
  <tr>
    <td> … </td>
    <td> … </td>
  </tr>
</table>

On my laptop, the duration of macro building this string increased steadily until about 10,000 cells. It then started slowing down significantly:

Duration
 in secs    Cells
  0.000       200
  0.016       400
  0.031     1,000
  0.062     2,000
  0.14      4,000
  0.44      8,000
  0.67     10,000
  0.97     12,000
  2.27     14,000
  6.79     16,000
 10.9      18,000
 63        32,000

On my laptop, 8,000 cells took 3 times as long as 4,000 cells; 16,000 cells took 15 times as long as 8,000 cells and 32,000 cells took 9 times as long as 16,000 cells. If you repeat my experiment, the increase in duration on your computer will depend on how much memory you have and on whatever else you are doing.

I have no expectation of needing to convert 32,000 cells. However, for this experiment, I was only converting the cell value. Once I started converting the formatting, string lengths would increase dramatically and the maximum number of cells with acceptable performance would drop,

My next idea was to create a string array with one element per row. Output each table row to a different element of the string array and then Join the elements to create the full string. This is the same idea as in the accepted answer. When I tried this idea, 32,000 cells took 91 seconds. Perhaps I could have reduced the duration by optimising my code but this did not appear to be a promising approach.

My next idea was to write each bit of the string to a disc file and then read the file to get the entire. I have not timed them properly but disc reads and write have always seems fast enough. With this approach, durations were:

Duration
 in secs    Cells
   .375     10,000
   .609     18,000
  3.45     100,000
 34.8    1,000,000

These durations are much more acceptable. The duration will increase as I start converting the formatting but since 1,000,000 cells is way more that I expect to convert, I should get acceptable performance.

To show my technique, this is the important part of my code:

  ' Needs reference to Microsoft Scripting RunTime

  Dim Body As String
  Dim ColCrnt As Long, ColLeft As Long, ColRight As Long
  Dim FileOut As TextStream
  Dim Fso As FileSystemObject
  Dim PathFileName As String
  Dim RowBot As Long, RowCrnt As Long, RowTop As Long
  Dim Wsht As Excel.Worksheet

  Set Fso = CreateObject("Scripting.FileSystemObject")
  PathFileName = ThisWorkbook.Path & "\RtH.txt"
  ' Create text file. First True = Overwrite existing file. Second True = Unicode output
  Set FileOut = Fso.CreateTextFile(PathFileName, True, True)

    FileOut.WriteLine "<table>"

    For RowCrnt = RowTop To RowBot
      FileOut.WriteLine "  <tr>"
      For ColCrnt = ColLeft To ColRight
        FileOut.WriteLine "    <td>" & Wsht.Cells(RowCrnt, ColCrnt).Value & "</td>"
      Next
      FileOut.WriteLine "  </tr>"
    Next
    FileOut.WriteLine "</table>"

  FileOut.Close
  ' Open text file. 1 = Open for reading.  -1 = Unicode
  Set FileOut = Fso.OpenTextFile(PathFileName, 1, -1)
  Body = FileOut.ReadAll
  FileOut.Close

Having got this far in this answer, I had another look for a solution to the string building problem and discovered: A lightning-fast StringBuilder

This page contains several version of the solution as different people suggested improvements. I tried the version by Duran. The duration of my code to convert 1,000,000 cells reduced from 34.8 seconds with my disc solution to 3.92 seconds.

My solution has the advantage that it only uses simple, easy VBA and is way faster than any other simple solution. I have tested it with a variety of characters across the 16-bit Unicode range without problems.

The linked solution used more advanced VBA. I would not let that stop you from using it; you use VBA standard routines without knowing how they work. I suggest reading every version of the solution and every comment since there is much useful information. I chose the Duran version but you might prefer another. The only important point that I could not find in any of the answers or comments is that the code must be placed in a Class module named “StringBuilder”.

Tony Dallimore
  • 12,335
  • 7
  • 32
  • 61