Monday, April 24, 2006

VB: Word Scramble using StrConv and StringBuilder

I had answered a fellow who wanted to know how you can write a word scramble in Visual Basic. For this problem, I will provide two examples, one in VB6, and the other in VB .Net. The VB6 version will simple take the string in a textbox and scramble the letters. The VB.Net version will take a string, separate each word, and scramble the words themselves. I thought this would be an interesting demonstration since working with character elements in VB strings has the tendency to be a bit of a pain. This will demonstrate using a character array to work with individual elements, using the StrConv function in VB6 to convert to a byte array, and using the StringBuilder class in VB.Net for more efficient string appends.

Below is the VB6 version 1:
Option Explicit
Private Sub CommandButton1_Click()
    Dim x, pos As Integer
    Dim char As String
    Dim s() As String
    
    'Re-dimension the array to the size of the string in the textbox
    ReDim s(Len(TextBox1.Text)) As String
    
    'Assign each character to an element in the array
    For x = 1 To Len(TextBox1.Text)
        s(x) = Mid(TextBox1.Text, x, 1)
    Next
    
    'For 1 to the size of the array, mix up the letters
    For x = 1 To UBound(s)
         'Pos is a random place in the array
               pos = Int((Len(TextBox1.Text) - 1) * Rnd) + 1
        
     'temporarily assign the temp character to our random letter
               char = s(pos)
        
     'Swap the characters, using the temp character to assign from the overwriten
     'element
        s(pos) = s(x)
        s(x) = char
    Next
    
    'Clear the textbox and concat the value with each element in the array
    TextBox1.Text = ""
    For x = 1 To UBound(s)
        TextBox1.Text = TextBox1.Text & s(x)
    Next
End Sub

Private Sub UserForm_Click()
    Randomize
End Sub

The above is a fairly simple. Take two elements in an array, and swaps them. This is pretty much the same as the VB.Net version below with a few exceptions. Alternatively, we can use the StrConv function to eliminate having to go through each element in the string and assign it to the array. This method is demonstrated below:

VB6 Version 2 Code:
Private Sub Command1_Click()
    Dim b() As Byte
    Dim temp As Byte
    Dim s As String
    Dim x, pos As Integer
    
    s = Text1.Text
    
    ReDim b(Len(s)) As Byte
    b = StrConv(s, vbFromUnicode)
    
    For x = LBound(b) To UBound(b)
        pos = Int((Len(Text1.Text) - 1) * Rnd) + 1
    
        temp = b(pos)
        
        b(pos) = b(x)
        b(x) = temp
    Next
    
    s = StrConv(b, vbUnicode)
End Sub


The above code is much cleaner in my opinion. In the StrConv() function, notice the use of the parameter vbFromUnicode and vbUnicode. The reason for this is that VB uses Unicode strings. Had we just assigned b the value of s, we would have an array twice as large, and the scramble code would need to take into account the additional byte that Unicode characters contain, otherwise we would have some undefined results.

Below is the VB .Net version 1. I used the first algorithm from above as the model:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Initialize the random number generator
     Randomize()
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim s_array() As String = Split(TextBox1.Text, " ")
        Dim s, final As String
        Dim c As Char
        Dim x, i, pos As Integer

        'We need to cycle through the array of strings set in the dim statement above
        For x = LBound(s_array) To (UBound(s_array))

            'If the current place in the array is greater than the lower bound and less than or
            'equal to the last string, then we need to add a space in between words
            If (x > LBound(s_array)) And (x <= UBound(s_array)) Then
                TextBox1.Text = TextBox1.Text & " "
            End If

            'For each letter in the current word, cycle through and use a
            'simple character swap with a random position in the string
            For i = 0 To (s_array(x).Length - 1)
                'Get random position in the string
                pos = CInt(Int(s_array(x).Length * Rnd()))

                'Store the character so swap temporarily so it does not get lost
                c = s_array(x).Chars(pos)

                'Set current letter with the letter to get swapped
                s_array(x) = s_array(x).Insert(pos, s_array(x).Chars(i))
                s_array(x) = s_array(x).Remove(pos + 1, 1)

                'Set current letter to the stored letter, completing the swap
                s_array(x) = s_array(x).Insert(i, c)
                s_array(x) = s_array(x).Remove(i + 1, 1)
            Next

            'If we are on the first word, clear the textbox and set to current element
            'in the array, otherwise concat the textbox with current value
            If (x = LBound(s_array)) Then
                TextBox1.Text = s_array(x)
            Else
                TextBox1.Text = TextBox1.Text & s_array(x)
            End If
        Next
    End Sub
End Class

While the above code works, there are a few issues with it. First, just like in the VB6 version, the handling of individual characters is not clean. We can assign the string to an array of characters directly and be better off. Also, the use of inserts and removes for individual characters are ugly and inefficient. .Net strings are immutable, so for each insert and remove, a copy of the string is created, making things very slow. Fortunately .Net offers the StringBuilder class for doing just this sort of operation. Below is the modified .Net version taking advantage of the direct assignment to a character array and using the StringBuilder class.

Below is the Vb.Net code modified (Version 2):
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim s_array() As String = Split(TextBox1.Text, " ")
        Dim b() As Char
        Dim c As Char
        Dim x, i, pos As Integer
        Dim tmpStrngBldr As System.Text.StringBuilder = New System.Text.StringBuilder()

        'We need to cycle through the array of strings set in the dim statement above
        For x = LBound(s_array) To (UBound(s_array))

            'If the current place in the array is greater than the lower bound and less than or
            'equal to the last string, then we need to add a space in between words
            If (x > LBound(s_array)) And (x <= UBound(s_array)) Then
                tmpStrngBldr.Append(" ")
            End If

            b = s_array(x)

            'For each letter in the current word, cycle through and use a
            'simple character swap with a random position in the string
            For i = LBound(b) To UBound(b)
                'Get random position in the string
                pos = CInt(Int(UBound(b) * Rnd()))

                'Store the character so swap temporarily so it does not get lost
                c = b(pos)

                'Set current letter with the letter to get swapped
                b(pos) = b(i)

                'Set current letter to the stored letter, completing the swap
                b(i) = c
            Next

            ‘Store the swaped word in our stringbuilder
            tmpStrngBldr.Append(b)
        Next

        'assign the textbox the value in our string builder
        TextBox1.Text = tmpStrngBldr.ToString
        tmpStrngBldr = Nothing
    End Sub

1 comment:

Anonymous said...

nice work! For the VB.net version, can you make it so that the array scrambles the words givin in the textbox as the whole words, not scrambled versions of the word. For example "Dog Cat Tree Climb" are in the textbox. How would i change the code so that the array keeps the words intact, but just changes the order of the words for example like this.... "Cat Climb Dog Tree" ?