2

I need to extract the data from an excel worksheet to an array that will be used in an application that uses VBScript as scripting language (Quick Test Professional). We can use the following code for that:

' ws must be an object of type Worksheet
Public Function GetArrayFromWorksheet(byref ws)
    GetArrayFromWorksheet = ws.UsedRange.Value
End Function

myArray = GetArrayFromWorksheet(myWorksheet)
MsgBox "The value of cell C2 = " & myArray(2, 3)

All nice and well, but unfortunately the array that gets returned does not only contain the literal text strings, but also primitives of type date, integer, double etc. It happened multiple times that that data got transformed.

[edit] Example: when entering =NOW() in a cell and set the cell formatting to hh:mm makes the displayed value 17:45, the above method retuns a variable of type double and a value like 41194.7400990741

The following solution worked better: I can get the literal text from a cell by using the .Text property, but they only work on one cell and not on a range of cells. I cannot do this at once for an array as I could with the .Value property, so I have to fill the array one cell at a time:

Public Function GetArrayFromWorksheet_2(byref ws)
    Dim range, myArr(), row, col
    Set range = ws.UsedRange

    ' build a new array with the row / column count as upperbound
    ReDim myArr(range.rows.count, range.columns.count)

    For row = 1 to range.rows.count
        For col = 1 to range.columns.count
            myArr(row, col) = range.cells(row, col).text
        Next
    Next

    GetArrayFromWorksheet_2 = myArr
End Function

But ouch... a nested for loop. And yes, on big worksheets there is a significant performance drop noticable.
Does somebody know a better way to do this?

Community
  • 1
  • 1
AutomatedChaos
  • 7,267
  • 2
  • 27
  • 47
  • Why not loop the array rather than the range and use CStr? –  Oct 12 '12 at 14:29
  • Can you give any specific examples of values entered that would not work using ooo's method? – Daniel Oct 12 '12 at 15:06
  • @DanielCook I'll have to search for that: I got these problems longer than six months ago and found the second solution. It was sufficient at that time. – AutomatedChaos Oct 12 '12 at 15:33
  • Very well. I only asked because I have a tendency to try to replicate the issue and then find a solution. Without some examples, I'm not sure why the values aren't good enough other than taking your word for it. (which I do, but it's not very useful if I'm going to help) – Daniel Oct 12 '12 at 15:40
  • @DanielCook I added a reproducable example. – AutomatedChaos Oct 12 '12 at 15:51
  • @ooo, that will still give me the performance penalty; `UsedRange.Value` returns me the array instantly, while looping will take time. Furthermore; In the added example there is a case where converting to string will not work. – AutomatedChaos Oct 12 '12 at 15:54
  • 1
    @AutomatedChaos - There's no built-in one-liner to do what you want. You might be able to improve your performance slightly by not continually recalculating `Columns.Count` and `Rows.Count`: put the counts in variables and use those for the loop limits. – Tim Williams Oct 12 '12 at 16:07
  • 1
    I think what you are doing is as fast as it gets. The only thing to add is that if you have different row heights, your loops will be dramatically slower. See this Charles Williams post: http://fastexcel.wordpress.com/2011/11/30/text-vs-value-vs-value2-slow-text-and-how-to-avoid-it/ – Doug Glancy Oct 12 '12 at 16:16
  • FYI: Here's a related previous question, the accepted answer indicates to basically do what you're doing. http://stackoverflow.com/questions/5590963/get-text-from-a-cell-range-in-excel-vba – Daniel Oct 12 '12 at 16:38
  • I'm going to give up now, but if your worksheet has a long of blank cells in the UsedRange, you could have speed improvement by generating the array based upon values and then looping through your array and replacing any non-empty slots with the text. – Daniel Oct 12 '12 at 17:34
  • There IS a built in one liner to do this, 1-6 lines depending on your useage. I personally use this to collect a worksheet into an array and process that. I added my answer to the mix. – danielpiestrak Oct 12 '12 at 20:56
  • @Tim Thanks, that is actually what I did but for the sake of the simplicity of the example I let it out because this wasn't a performance bottleneck. – AutomatedChaos Oct 15 '12 at 08:13
  • @DougGlancy, The link explains it nicely and this way seems to be the fastest way without transforming numbers or dates indeed. – AutomatedChaos Oct 15 '12 at 08:23

3 Answers3

2

As we covered in the comments, in order to avoid the issue you will need to loop through the array at some point. However, I am posting this because it may give you a significant speed boost depending on the type of data on your worksheet. With 200 cells half being numeric, this was about 38% faster. With 600 cells with the same ratio the improvement was 41%.

By looping through the array itself, and only retrieving the .Text for values interpreted as doubles (numeric), you can see speed improvement if there is a significant amount of non-double data. This will not check .Text for cells with Text, dates formatted as dates, or blank cells.

Public Function GetArrayFromWorksheet_3(ByRef ws)

    Dim range, myArr, row, col
    Set range = ws.UsedRange

    'Copy the values of the range to temporary array
    myArr = range
    'Confirm that an array was returned.
    'Value will not be an array if the used range is only 1 cells
    If IsArray(myArr) Then
        For row = 1 To range.Rows.Count
            For col = 1 To range.Columns.Count
                'Make sure array value is not empty and is numeric
                If Not IsEmpty(myArr(row, col)) And _
                            IsNumeric(myArr(row, col)) Then
                    'Replace numeric value with a string of the text.
                    myArr(row, col) = range.Cells(row, col).Text
                End If
            Next
        Next
    Else
        'Change myArr into an array so you still return an array.
        Dim tempArr(1 To 1, 1 To 1)
        tempArr(1, 1) = myArr
        myArr = tempArr
    End If

    GetArrayFromWorksheet_3 = myArr
End Function
Daniel
  • 12,982
  • 3
  • 36
  • 60
  • Marked it as best solution so far, because it will fit nicely in my situation: There are a lot of empty cells and non-numeric cells in my worksheets so this will give a huge speedgain. Protip: VBScript does not have [short circuit evaluation](http://en.wikipedia.org/wiki/Short-circuit_evaluation), so I will split the inner `IsEmpty` and `IsNumeric` in two nested `If` statements. – AutomatedChaos Oct 15 '12 at 09:32
0
  • Copy your worksheet into a new worksheet.
  • Copy Paste values to remove formulas
  • Do a text to columns for each column, turning each column into Text
  • Load your array as you were initially doing
  • Delete the new worksheet
nutsch
  • 5,922
  • 2
  • 20
  • 35
  • 1
    If I do that, using the example of =NOW(), I get the text "=NOW" as a result. – Doug Glancy Oct 12 '12 at 16:33
  • OK. Then copy first, then do a paste values before you run the text to columns. (edit coming) – nutsch Oct 12 '12 at 16:42
  • 1
    For the OPs example, this will still end up seeing the 11:40 derived from NOW() for example as something like 41194.4861681713 – Daniel Oct 12 '12 at 16:49
  • @Daniel, not my experience when I test a Copy Paste Formats, Copy Paste Values, then a text to columns. The =Now() retains the date / time format. – nutsch Oct 12 '12 at 17:01
  • Well, from the comments we can conclude that it is not a stable solution unfortunately. It also adds another layer of complexity and possibility to fail: using a work around can give problems in the future. Thanks for thinking along! – AutomatedChaos Oct 15 '12 at 09:23
0

You cant do this quickly and easily without looping through the worksheet. If you use the technique above with 2 lines of code it must a variant type array.

I've included a real example from my code that does it in 6 lines because I like to A) work with the worksheet object and B) keep a variable handy with the original last row.

Dim wsKeyword As Worksheet
Set wsKeyword = Sheets("Keywords")

Dim iLastKeywordRow As Long
iLastKeywordRow = wsKeyword.Range("A" & wsKeyword.Rows.Count).End(xlUp).Row

Dim strKeywordsArray As Variant
strKeywordsArray = wsKeyword.Range("A1:N" & iLastKeywordRow).Value

Note your array MUST be a variant to be used this way.

The reason that Variants work like this is that when you create an array of variants, each 'cell' in the array is set to a variant type. Each cell then get's it's variant type set to whatever kind of value is assigned to it. So a variant being assigned a string gets set to variant.string and can now only be used as a string. In your original example it looks like you had time values which were kind of stored as variant.time instead of variant.string.

There are two ways you can approach your original problem 1) loop through and do the process with more control, like the double nested for loop. explained in another answer which gives you complete control 2) store all the data in the array as is and then either re-format it into a second array, or format it as desired text as you use it (both should be faster)

danielpiestrak
  • 5,279
  • 3
  • 30
  • 29
  • Nice solution, but the transformation from `range().text` to `array` does not work (that was part of the problem`. I was thinking about using the worksheet object too, because you do the data transformation only when needed and only with the cells you want the data from, but I didn't want to have an excel instance open (I use VBScript to create a COM instance to Excel). So putting it into an array was a concession I had to make. – AutomatedChaos Oct 15 '12 at 09:37
  • Ah, well i'll try and update my answer to when i find time today about how the 2 liner only works with .value + variants. I'v been a bit too busy at work lately, may just end up deleting it! Thanks for the feedback. – danielpiestrak Oct 15 '12 at 13:50
  • Updated my answer to explain why OP's original didnt work only. – danielpiestrak Oct 18 '12 at 15:29