7

I would like to turn values in given range into VBA string where original cell values are separated by any chosen column delimiter and row delimiter. Delimiters could be one character or longer strings. The row delimiter is the string at the end of the line. The string should be done just as we read text from left top corner, from left to right, to bottom right corner.

Here is an example of the VALUES in range A1:C5:

+----+----+----+
| A1 | B1 | C1 |
+----+----+----+
| A2 | B2 | C2 |
+----+----+----+
| A3 | B3 | C3 |
+----+----+----+
| A4 | B4 | C4 |
+----+----+----+
| A5 | B5 | C5 |
+----+----+----+

Desired results is a VBA string:

A1,B1,C1@$A$2,$B$2,$C$2@A3,B3,C3@A4,B4,C4@A5,B5,C5@

For the sake of readability I will show it like this:

A1,B1,C1@
A2,B2,C2@
A3,B3,C3@
A4,B4,C4@
A5,B5,C5@

As a column delimiter I have chosen , (comma), and as a row delimiter @ sign. Of course these could be any characters like \r\n.

The reason why I want fast cooking of the string from range is because I want to to send it to SQL Server through ADO connection. As I have tested so far it is the fastest way to transfer lots of data on the fly. The twin question how to split this string on SQL Server is here: Split string into table given row delimiter and column delimiter in SQL server

Solution 1. Loop through all rows and columns. Question is if there be any more elegant way then just looping through all rows and columns? I would prefer VBA solution, not formula one.

Solution 2. Suggested by Mat's Mug in comment. CSV file is desired results. I would like to do it on the fly without saving. But good point - imitate CSV is what I want but I want it without saving.

Edit after bounty

Answer of Thomas Inzina works crazy fast and his solution is portable. Ordinary VBA loop turned out to be way faster then worksheet functions like JOIN on large data sets. I do not recommend using worksheet functions in VBA for that purpose. I have voted up everybody. Thank you all.

Community
  • 1
  • 1
Przemyslaw Remin
  • 6,276
  • 25
  • 113
  • 191
  • 1
    The first thing that pops to mind is *Save As -> CSV* - and then read the file into a string if that's what you need. The question is *why* would you need a comma-separated list of cell values in a string? – Mathieu Guindon Sep 20 '16 at 15:30
  • Re your update - you aren't planning to use this in an INSERT statement are you? If that's the case you might want to rethink this and use a parameterized query instead. – Comintern Sep 20 '16 at 15:51
  • 1
    Thank you for that hint. @Comintern yes, unfortunately I want to do it that way. I know other solutions like these here: http://www.excel-sql-server.com/ However stored procedure with one big parameter - the string - which later on the SQL server is cut to pieces to make inserts is the fastest solution. Much more faster than all solutions from Excel side I know. – Przemyslaw Remin Sep 20 '16 at 18:50
  • @PrzemyslawRemin the last character should not be a delimiter. It will create an empty element. Your example creates an empty record. –  Oct 04 '16 at 12:10
  • 1
    I see the bounty is still open - what haven't any of the answers done correctly? What are we missing? – BruceWayne Oct 04 '16 at 13:57
  • @ThomasInzina My idea was to imitate CSV file which has the return carriage at the end of every line, including the last line. But I accept solutions omitting the EOL delimiter at very end. – Przemyslaw Remin Oct 04 '16 at 17:47
  • Gotcha. Just be aware that when you split the values in your query, that you will have to ignore the last row –  Oct 04 '16 at 17:50
  • 1
    I believe that my function will turn out to be the fastest way to build the string, however, I would use ADO Recordset.getString method to build it because of it's versatility. –  Oct 04 '16 at 17:54
  • @ThomasInzina if you mean my query here http://stackoverflow.com/a/39758478/1903793 I have just corrected presented there stored procedure so it splits correctly both strings (with, and without row delimiter at the very end of string). Thank you for idea of `Recordset.getString`. – Przemyslaw Remin Oct 04 '16 at 18:50
  • @ThomasInzina RE: GetString -- If the macro is running within the workbook, I'm not sure it's possible to open an ADO connection to the worksheet. If possible, I would suggest opening an external connection to the worksheet from a different environment (another workbook, another VBA environment, WSH) and then calling `GetString`. – Zev Spitz Oct 04 '16 at 21:25
  • You can query the Activeworkbook. I've experimented before where I joined an Access table, a Worksheet on the ActiveWorkbook and a worksheet on an external Workbook in a single query. –  Oct 04 '16 at 21:35
  • @ZevSpitz [Download Sample Workbook](https://www.dropbox.com/s/8s5l9wi274yw30a/Turn%20Excel%20range%20into%20VBA%20string.xlsm?dl=0) The ADODB.Recordset.GetString took 15 seconds to execute compared to my String Builder function that took 0.98 Seconds. The Recordset performed the same using early or late binding. The query itself only took 0.53125 Second(s). So it took 14.5 seconds for it to build the string. In my sample file I had the row and column delimiters reversed. –  Oct 04 '16 at 22:10
  • Join() is NOT a worksheet function! – Excel Hero Feb 16 '19 at 01:12

7 Answers7

7

To optimize performance my function emulates a String Builder.

Variables

  • Text: A very large string to hold the data
  • CELLLENGTH: A contant that determines the size of the BufferSize
  • BufferSize: The initial size of Text string
  • Data(): An Array derived from the source range

As the rows and columns of the Data() array are iterated over the current element (Data(x, y)) value replaces a portion of the Text string. The text string is resized as needed. This reduces the number of concatenations immensely. The initial BufferSize is set pretty high. I got my best results, 0.8632813 Second(s), by reducing CELLLENGTH to 25.

Download Sample Data from Sample-Videos.com

Results

enter image description here

Code

Function getRangeText(Source As Range, Optional rowDelimiter As String = "@", Optional ColumnDelimiter As String = ",")
    Const CELLLENGTH = 255
    Dim Data()
    Dim text As String
    Dim BufferSize As Double, length As Double, x As Long, y As Long
    BufferSize = CELLLENGTH * Source.Cells.Count
    text = Space(BufferSize)

    Data = Source.Value

    For x = 1 To UBound(Data, 1)
        If x > 1 Then
            Mid(text, length + 1, Len(rowDelimiter)) = rowDelimiter
            length = length + Len(rowDelimiter)
        End If

        For y = 1 To UBound(Data, 2)
            If length + Len(Data(x, y)) + 2 > Len(text) Then text = text & Space(CDbl(BufferSize / 4))
            If y > 1 Then
                Mid(text, length + 1, Len(ColumnDelimiter)) = ColumnDelimiter
                length = length + Len(ColumnDelimiter))
            End If

            Mid(text, length + 1, Len(Data(x, y))) = Data(x, y)
            length = length + Len(Data(x, y))
        Next
    Next

    getRangeText = Left(text, length) & rowDelimiter
End Function

Test

Sub TestGetRangeText()
    Dim s As String
    Dim Start: Start = Timer

    s = getRangeText(ActiveSheet.UsedRange)

    Debug.Print "Execution Time: "; Timer - Start; "Second(s)"
    Debug.Print "Rows: "; ActiveSheet.UsedRange.Rows.Count; "Columns: "; ActiveSheet.UsedRange.Columns.Count
    Debug.Print "Result Length: "; Format(Len(s), "#,###")
End Sub
  • Can you please explain in short the logic of your UDF? What buffering for? How the string is constructed? – Przemyslaw Remin Oct 04 '16 at 11:15
  • This article: [MSDN: How To Improve String Concatenation Performance](https://support.microsoft.com/en-us/kb/170964) Every time you concatenate a string a temp memory location is created the first string is copied to being of the temp memory, the second string is copied to the end of the temp memory, the destination string is then resized, the memory is copied from the temp memory to the newly resized memory and the old variables are cleaned up. –  Oct 04 '16 at 11:25
  • In my test I am combining 535,563 cells together with another 535,563 delimiters. Instead of performing 1,071,126 concatenations I create a buffered string large enough to hold all the data. If my buffer size is too small it will concatenate another very large string. Reducing the number of concatenations to 2 or 3 at the most. I'm never took computer science but at 6 operations per concatenation this reduces the total number of operations from over 6 million to 535,563 + (6 + 6 + 1) ish. –  Oct 04 '16 at 11:35
  • According to that article it is 100 times faster than concatenating the strings. –  Oct 04 '16 at 11:56
  • I have just noticed two issues with your code. (1) It cannot handle delimiters longer then one character. (2) It skips empty cells. If the cell is empty it should add to string at least column delimiter. Otherwise it leads to catastrophic results. Would you be kind to correct your answer? – Przemyslaw Remin Oct 24 '16 at 13:45
  • @PrzemyslawRemin I just modified my answer to accept multi-character delimiters. Empty cells were never skipped. You can test this out by selecting an empty worksheet and running this `?getRangeText([A1:F2])` in the Immediate Window. –  Oct 24 '16 at 16:50
  • Your code needs further modification to deal with delimiters longer then one character. Instead of 'length = length + 1' (first occurrence) should be `length = length + Len(rowDelimiter)`. In the second occurrence should be `length = length + Len(ColumnDelimiter)`. – Przemyslaw Remin Oct 12 '17 at 13:55
  • @Thomas Inzina : Great code Thomas, I knew Mid could be used as an left hand operator but your example shows just how mega-powerful this technique can be. Superb! – S Meaden Oct 18 '17 at 13:29
  • Thanks @PrzemyslawRemin. I made those modifications. –  Oct 18 '17 at 18:47
  • 1
    Thanks @SMeaden. At the time I didn't know that we could use CreateObject("System.Text.StringBuilder"). [Access: using .Net strings in VBA for fun and profit | Cypris' lookout](http://blog.nkadesign.com/2013/access-using-net-strings-in-vba-for-fun-and-profit/) does an excellent job of Wrapping a StringBuilder. It would be interesting to test my code against an actual StringBuilder. –  Oct 18 '17 at 18:56
5

Here's a quick way to test (Note: this will only work with Excel 2016 (or if you have the TextJoin() function).

First, in the empty column D, do =C1&"@", so you get your last column filled with the cell+@

Then, say in cell E1, =TEXTJOIN(",",TRUE,A1:C5) (Note: TRUE there means to skip blanks. If you have blanks, and want to keep them, change that to FALSE).

THen, on that cell, run

=Substitute(E1,"@,","@")

enter image description here

Or combine the formulas into one: =SUBSTITUTE(TEXTJOIN(",",TRUE,A1:C4),"@,","@").

If you need vba, just throw the formula into a VBA macro and run like that.

BruceWayne
  • 22,923
  • 15
  • 65
  • 110
  • 1
    Just an FYI this UDF mimics TEXTJOIN() somewhat: http://stackoverflow.com/questions/39532189/vlookup-with-multiple-criteria-returning-values-in-one-cell – Scott Craner Sep 20 '16 at 15:58
  • @ScottCraner - Yeah, I think `TextJoin` was probably a request that Excel folks got a lot, and they implemented it. OP - If you don't have Excel 2016, check the link Scott provided. – BruceWayne Sep 20 '16 at 16:02
4

Here is a UDF that returns the desired output:

EDIT Changed to add EOL at the end.

Option Explicit
Function MultiJoin(Rng As Range, Delimiter As String, EOL As String) As String
    Dim V As Variant, W As Variant
    Dim COL As Collection
    Dim I As Long, J As Long

V = Rng
Set COL = New Collection
ReDim W(1 To UBound(V, 2))
For I = 1 To UBound(V, 1)
    For J = 1 To UBound(V, 2)
        W(J) = V(I, J)
    Next J
    COL.Add W
Next I

ReDim V(1 To COL.Count)
For I = 1 To COL.Count
    V(I) = Join(COL(I), Delimiter)
Next I

W = Join(V, EOL)
MultiJoin = W & EOL

End Function

One could shorten the code by using WorksheetFunctions, but I would guess execution time would be slower.

Shortened Code

Option Explicit
Function MultiJoin(Rng As Range, Delimiter As String, EOL As String) As String
    Dim V As Variant, W As Variant
    Dim I As Long, J As Long

V = Rng
With WorksheetFunction

For I = 1 To UBound(V, 1)
    V(I, 1) = Join(.Index(V, I, 0), Delimiter)
Next I
MultiJoin = Join(.Transpose(.Index(V, 0, 1)), EOL) & EOL

End With

End Function
Ron Rosenfeld
  • 53,870
  • 7
  • 28
  • 60
3

This solution will require either a reference to the Microsoft Forms 2.0 Object Library in your project or some other way of fetching the contents of the clipboard (like through an API call).

Function TurnExcelRangeIntoVBAString(Optional cellDelimiter As String = ",", _
                                     Optional rowDelimiter As String = "@") _
         As String

    Dim rng As Range
    Set rng = ActiveSheet.UsedRange
    rng.Copy

    Dim clip As New MSForms.DataObject
    Dim txt As String
    clip.GetFromClipboard
    txt = clip.GetText()
    txt = Replace(Replace(txt, vbTab, cellDelimiter), vbCrLf, rowDelimiter)

    TurnExcelRangeIntoVBAString = txt
End Function
Patrick Wynne
  • 1,864
  • 15
  • 20
  • This is very elegant short code, The external reference is a big drawback as solution will be used by different users. – Przemyslaw Remin Sep 29 '16 at 10:08
  • 3
    You can eliminate the external reference by using late binding: `Dim clip as Object` and use `Set clip =CreateObject("New:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}")` –  Sep 29 '16 at 19:14
  • 1
    @PrzemyslawRemin - Microsoft Forms 2.0 shouldn't be a problem at all - it automatically gets added any time you insert a UserForm anyway. – Comintern Oct 03 '16 at 23:09
  • @Comintern or an activeX control for that matter –  Oct 04 '16 at 11:08
2

you could try this

Option Explicit

Sub main()
    Dim strng As String
    Dim cell As Range

    With Worksheets("TurnRangeIntoString") '<--| change "TurnRangeIntoString" to your actual worksheet name
        For Each cell In Intersect(.UsedRange, .Columns(1)) '<--| loop through its column 1 cells
            strng = strng & Join(Application.Transpose(Application.Transpose(.Range(cell, cell.End(xlToRight)).value)), ",") & "@" '<--| build string
        Next cell
    End With
    MsgBox strng
End Sub
user3598756
  • 28,893
  • 4
  • 18
  • 28
  • @PrzemyslawRemin, did you get through it? – user3598756 Sep 24 '16 at 06:54
  • Yes, thank you. Works good. I also like your idea. Voted up. Your solution would be better if it was a function like the one proposed by Ron Rosenfeld, because it allows to choose exact range. – Przemyslaw Remin Sep 28 '16 at 10:54
  • You are welcome. Thanks for upvote. Turning my Sub into a Function is quite straightforward like you can see with Ron solution. Fine that you found the most suited one! – user3598756 Sep 28 '16 at 14:11
  • Look, it's not a race between Ron and me. It's a chance to help you. And the greatest help I myself would appreciate is help me learning. That said, you try switching my Sub into a Function and if you meet problems just make a new post showing your efforts and telling what's wrong with them. – user3598756 Sep 28 '16 at 18:59
  • I am grateful for your contribution. I would accept both answers if it was possible. Your solution seems very fast in my tests. – Przemyslaw Remin Sep 28 '16 at 19:48
1
Sub aquatique()
dim a(),s$,i&,j&:a=selection.value
for i=1 to ubound(a)
for j=1 to ubound(a,2)
    if j=1 then
        if i=1 then
            s=  a(i,j)
        else
            s=s &"@" & vbnewline & a(i,j)
        end if
    else
        s=s &";" & a(i,j)
    end if
next
next
end sub

simple but does the job. Slow on huge ranges, you'd need to use "join"

Pierre
  • 1,046
  • 7
  • 21
1

How about this?:

Sub Concatenate()
Dim Cel As Range, Rng As Range
Dim sString As String, r As Long, c As Long, r2 As Long

Set Rng = Selection
r = Selection.Row
c = Selection.Column
r2 = Selection.Row
For Each Cel In Rng
    r = Cel.Row
    If sString = "" Then
        sString = Cel.Value
        Else
            If r <> r2 Then sString = sString & "@" & Cel.Value
            If r = r2 Then sString = sString & "," & Cel.Value
    End If
    r2 = Cel.Row
Next

sString = sString & "@"
Debug.Print sString

End Sub
Jeremy
  • 1,337
  • 3
  • 12
  • 26