The following code gives me error 9 "subscript out of range". I meant to declare a dynamic array so that the dimension changes as I add elements to it. Do I have to create a "spot" on the array before I store something in it like in JS?
Sub test_array()
Dim test() As Integer
Dim i As Integer
For i = 0 To 3
test(i) = 3 + i
Next i
End Sub
in your for loop use a Redim on the array like here:
For i = 0 to 3
ReDim Preserve test(i)
test(i) = 3 + i
Next i
As Cody and Brett mentioned, you could reduce VBA slowdown with sensible use of Redim Preserve. Brett suggested Mod to do this.
You can also use a user defined Type and Sub to do this. Consider my code below:
Public Type dsIntArrayType
eElems() As Integer
eSize As Integer
End Type
Public Sub PushBackIntArray( _
ByRef dsIntArray As dsIntArrayType, _
ByVal intValue As Integer)
With dsIntArray
If UBound(.eElems) < (.eSize + 1) Then
ReDim Preserve .eElems(.eSize * 2 + 1)
End If
.eSize = .eSize + 1
.eElems(.eSize) = intValue
End With
End Sub
This calls ReDim Preserve only when the size has doubled. The member variable eSize keeps track of the actual data size of eElems. This approach has helped me improve performance when final array length is not known until run time.
Hope this helps others too.
Yes, you're looking for the ReDim statement, which dynamically allocates the required amount of space in the array.
The following statement
Dim MyArray()
declares an array without dimensions, so the compiler doesn't know how big it is and can't store anything inside of it.
But you can use the ReDim statement to resize the array:
ReDim MyArray(0 To 3)
And if you need to resize the array while preserving its contents, you can use the Preserve keyword along with the ReDim statement:
ReDim Preserve MyArray(0 To 3)
But do note that both ReDim and particularly ReDim Preserve have a heavy performance cost. Try to avoid doing this over and over in a loop if at all possible; your users will thank you.
However, in the simple example shown in your question (if it's not just a throwaway sample), you don't need ReDim at all. Just declare the array with explicit dimensions:
Dim MyArray(0 To 3)
In addition to Cody's useful comments it is worth noting that at times you won't know how big your array should be. The two options in this situation are
Creating an array big enough to handle anything you think will be thrown at it
Sensible use of Redim Preserve
The code below provides an example of a routine that will dimension myArray in line with the lngSize variable, then add additional elements (equal to the initial array size) by use of a Mod test whenever the upper bound is about to be exceeded
Option Base 1
Sub ArraySample()
Dim myArray() As String
Dim lngCnt As Long
Dim lngSize As Long
lngSize = 10
ReDim myArray(1 To lngSize)
For lngCnt = 1 To lngSize*5
If lngCnt Mod lngSize = 0 Then ReDim Preserve myArray(1 To UBound(myArray) + lngSize)
myArray(lngCnt) = "I am record number " & lngCnt
Next
End Sub
I see many (all) posts above relying on LBound/UBound calls upon yet potentially uninitialized VBA dynamic array, what causes application's inevitable death ...
Erratic code:
Dim x As Long
Dim arr1() As SomeType
...
x = UBound(arr1) 'crashes
Correct code:
Dim x As Long
Dim arr1() As SomeType
...
ReDim Preserve arr1(0 To 0)
...
x = UBound(arr1)
... i.e. any code where Dim arr1() is followed immediatelly by LBound(arr1)/UBound(arr1) calls without ReDim arr1(...) in between, crashes. The roundabout is to employ an On Error Resume Next and check the Err.Number right after the LBound(arr1)/UBound(arr1) call - it should be 0 if the array is initialized, otherwise non-zero. As there is some VBA built-in misbehavior, the further check of array's limits is needed. Detailed explanation may everybody read at Chip Pearson's website (which should be celebrated as a Mankind Treasure Of VBA Wisdom ...)
Heh, that's my first post, believe it is legible.
A class-based approach with BetterArray
In 2013, Ioannis commented that you could create a class to manage array resizing in chunks. An implementation with many other features—sorting, slicing, filtering, and conversion, to name a few—can now be found here: https://senipah.github.io/VBA-Better-Array/ (I am not connected to the project). A page on its capacity property explains the internal doubling process. More generally:
Stored in a single .cls file, the BetterArray class can easily be
imported into any new or existing VBA project. It’s written in pure
VBA and doesn’t use any external dependencies. As such, it should work
in any application that supports VBA scripting across both Windows and
Mac operating systems.
In other words, you simply download the class module (a single .cls file, linked here), drag it into your project*, and from there it’s available like a collection or any other object to create. Here I use it to get the contents of the current directory:
Sub DemoBetterArray()
Dim ba As BetterArray, tempDir As String, basicArray As Variant
Set ba = New BetterArray
tempDir = Dir("")
Do While tempDir <> ""
ba.Push tempDir 'I set no bounds, but I'm adding an element
tempDir = Dir()
Loop
basicArray = ba.Items 'See results in a traditional array
End Sub
For this example, you could do similar with an ArrayList, generally available on Windows via .NET (but apparently obsolete in .NET). See a summary. In any case, there are major differences between these objects you could explore. For adding 1,000,000 integers, I found BetterArray to be several times faster than an ArrayList.
A tip for using the BetterArray documentation: While the page on examples is currently blank, the pages on methods (listed here) give many helpful examples of what the class can do in addition to expanding efficiently.
Expansion via Error Handling
Another possibility not yet discussed is to use error handling. The approach is demonstrated by Bruce McKinney in Hardcore Visual Basic, 2nd Edition (1997). The function below uses this idea.
Sub VectorFill(sourceVector As Variant, Index As Long, Value As Variant)
'Fills a 1d array at a specified index and increases the UBound if needed (by doubling it).
'Trim unneeded space separately with ReDim Preserve.
Const resizeMultiplier As Integer = 2
'With this statement, an out of bounds error will trigger a resize
On Error GoTo ErrorHandling
sourceVector(Index) = Value
Exit Sub
'ErrorHandling used to resize array in chunks
ErrorHandling:
newBound = (UBound(sourceVector) + 1) * resizeMultiplier '+1 helps with initial 0
ReDim Preserve sourceVector(newBound)
Resume 'New space is available, so go back and try again
End Sub
The above function can be used as follows:
Sub DemoVectorFill()
Dim dirContents() As Variant, i As Long
ReDim dirContents(0)
dirContent = Dir("")
Do While dirContent <> ""
VectorFill dirContents, i, dirContent
dirContent = Dir
i = i + 1
Loop
ReDim Preserve dirContents(i - 1)
End Sub
With this approach, you don't have to check capacity at each iteration, and leverage the error when it occurs. On my tests it is not faster than making that check (it’s a hair slower), but as sizes increase either way is much faster than ReDim Preserve at each iteration.
*If you try to copy and paste the BetterArray code into a class module, this will not entirely work. Class modules have some code that is hidden in the VBA editor, and won't be copied via copy paste. The two options are to drag the .cls file into the Project pane or use File --> Import File.
Related
I am aware that this;
Arr() =Range("E2:X2500")
...then doing stuff with the Arr, and then dumping back using:
Range("E2:X2500")=Arr()
is many folds more efficient (faster) that looping through and referencing cells directly.
It's light speed!
But, this range-to-array assignment only grabs cells' value.
Is there a way to assign an actual range (continuous or not) into an array (with the same light speed) in such a way that you could then treat array items the way you would refer to cells, like:
arr(23).row 'getting a row number
Or;
If Arr(23).Value ="Pending" then arr(23).font.bold=1 else arr(23).font.bold=0
I know i can dim a range-type array where each item can store an actual single cell range. But this array cannot be handled the same way - with one liner assignment:
Dim Arr () as Range
Set Arr = Range("E2:X2500") 'error
Instead, I would need to iterate each cell and assign it to the next item in the range-type array, which would allow me to treat items the way I'd refer to cells, but take substantially longer to load as I'm dealing with a Loop.
Also how would I dump a range-type array back into the sheet with the same ease and effectiveness of the one liner assignment? I think the only way would be to use a loop yet again, correct?
Side question :
Speedwise, is it any better to refer to cells via a range-type array over referring to cells directly via the sheet, or are both basically the same?
Thanks!
Well, the array use will save a lot of code running time. But, there are some issues which must be understood:
First thing when work in VBA and your project increases, is to properly declare your variables. Try making a reflex in putting Option Explicit on top of all your modules. In the array case, the thing, from this point of view stays like that:
Dim Arr() As variant, arr1 As Variant
Both declarations work in excel. But the second one is recommended (on mai taste), when you need an array from a range. When you want building a, let us say, result array, it will be zero based and you must take care of the range size where the values will be returned.
The array content cannot be retrieved exactly like you tried in your question in case of not fix/known number of elements. Look at the next test code:
Sub testArrays()
Dim sh As Worksheet, rng As Range, arrTest As Variant
Set sh = ActiveSheet
Set rng = sh.Range("A1:F4")
arrTest = rng.value
sh.Range("J1").Resize(UBound(arrTest, 1), UBound(arrTest, 2)).value = arrTest
End Sub
It is recommended to use arrTest = sh.Range("A1:F4").value. Using range Value. Excel is able to understand what you need according to your declaration, but it is good for you to differentiate somehow, from the way of the range definition.
Sometimes, you need to build an array during analyzing of the dynamic range. If you cannot know the new array dimensions and need to Redim (Preserve), only the second dimension of the array can be re-dimensioned and Transpose function must be use, in such a case. And finally the resulted array can be properly loaded in a range, only if you know the array number of rows and columns.
You can deduce the range row, from the array row, in the next way:
If we are referring to the above arrTest we know that its first row is first row of the sheet and it has 5 columns.
So, arrTest(3, 1) will be sh.Range("A3").Value and its row would be 3.
Then, arrTest(3, 4) will be sh.Range("D3").Value and its row would be also 3.
If your array comes from a range starting with the fifth row, you must add four in order to obtain the sheet row extracted from the array row...
So, your example can be transformed in:
If arrTest(3, 4) ="Pending" then sh.Cells(3, 4).Font.Bold=1 Else sh.Cells(3, 4).Font.Bold=0
Now if you need a ranges array, you cannot do it in the way you tried. You must use the ranges address and build the range at the end:
Sub testArraysBis()
Dim sh As Worksheet, rng As Range, rng1 As Range, lastCol As Long
Dim rng2, arrTest As Variant, arrT As Variant, arrF As Variant
Set sh = ActiveSheet
lastCol = sh.Cells(1, Cells.Columns.Count).End(xlToLeft).column
Set rng = sh.Range(sh.Cells(1, 1), sh.Cells(4, lastCol))
Set rng1 = sh.Range("A5:F6")
arrT = Array(rng.Address, rng1.Address)
arrTest = rng.value
Debug.Print UBound(arrTest), LBound(arrTest)
sh.Range("J1").Resize(UBound(arrTest, 1), UBound(arrTest, 2)).value = arrTest
Set rng2 = sh.Range(arrT(0))
Debug.Print rng2.Address
arrF = sh.Range(arrT(0)).value
Debug.Print UBound(arrF, 2)
End Sub
rng2 range will be built using the address string, extracted from arrT array. An array (arrF) can also be extracted from the arrT first element...
Epilog:
The best way, in terms of speed, is to load the range in arrays, make all processing using them (in memory and very fast due to this aspect), but the most important issue is to build another array (or even a range, using Union) and retrieve the data AT ONCE. Sending of each partial processing result to a cell/range consumes a lot of time and other resources, for a big range size...
I like to use an array as if it was an "in RAM memory" spreadsheet, so the code is more clean and it runs faster.
So I would like to ask: generally, is it better start the code with a huge array, say Dim arr(10000), so I can work with it as if it was a blank sheet I fill when necessary, or is that a bad practice, and, instead, I should use Redim Preserve all the time I need to insert data into the array?
Thanks in advance for any help.
The answer to your question depends on how you intend to fill the array (noting that this is in the context of Excel). Here are four simple scenarios:
Filling from a Range. Simply Dim a Variant and let VBA do all the work.
Dim myArray as Variant
Dim myRange as Range
'Set myRange = something
myArray = myRange.Value '<-- Fills a 2-D array
For rowIterator = LBound(myArray,1) to UBound(myArray,1) '<- How identify the size of the first dimension
For colIterator = LBound(myArray,2) to UBound(myArray,2) '<- How identify the size of the second dimension
'Do something to myArray(rowIterator, colIterator)
Next colIterator
Next rowIterator
Filling when you define your bounds. In some code, you can learn early in the code running what your bounds are going to be. In this case, you can ReDim before you get into a loop.
Dim myArray() As String
Dim someCounter As Long
'Some early code
someCounter = CLng(myRange.Value)
ReDim myArray(someCounter) '<-- Do this only once.
For iterator = LBound(myArray) to UBound(myArray)
' Do Something to myArray(iterator)
Next iterator
Filling within a loop. Avoid this when ever you can as ReDimming an array, especially with ReDim Preserve is computationally expensive. However, if you know it is a small loop, you might want to accept the cost.
An alternative to resizing in a loop is to use a Collection.
Oversizing to start with. Rather than continually re-dimensioning the array, you could oversize the array but you then have the additional management overhead of trying to manage the unused array slots. How do you know if the value in the array of "" or 0 is really "" or 0 instead of a default null value?
I have been told there is a way of storing data in dynamic arrays which can then be referred to in a function.
I come from my recent answered question:Showing hidden column in another sheet
I am trying to find out how to store in a dynamic array which row (first array), column (second array) and sheet (third array) my code has to make action on.
I still haven't done arrays in my class, I'm just guessing it is a dynamic one for what I have researched. Do you think this could be done in the same array using different dimensions?
(Edited ->) -- Being more specific: I am looking for a way to store a number of specific rows(or columns) of a specific sheet to then (in a loop I guess) run my function for each element.
I have read the basic documentation (https://msdn.microsoft.com/en-us/library/aa716275(v=vs.60).aspx) and looked for similar posts here but I can't find it. If someone can point out what I should do or read to achieve this I can try and do it myself if you believe it's better.
Edited:
I think I am doing some progress, but I still don't know how to refer to a the sheet within the range.
Would this do the job (if I didn't need sheet referring)?
Public Sub Test()
Dim Rng As Range
Dim Area As Range
Set Rng = Range("A1:A10,B1:B20,G1:G3")
For Each Area In Rng.Areas
Area.Hidden = True
Next Area
End Sub
You can manage that with a single array of Range because the range refer to:
The sheet
The row
The Column
Dim array() as Range
...
' Store with
set array(i) = worksheet.Range(a, b)
...
' Read with
set range = array(i)
The link to msdn in your question explain how to manage Dynamic Arrays
update
The problem in your code is you not refer the worksheet you want.
If no worksheet is indicate, in the best case an error is thrown, in the worst case it takes the "Activesheet" (yes, an error is a better case then work on you don't know what).
Consider you know the name of the sheet (or the position of it), you can pass it in parameters
Public Sub Test(byval sheetname as string)
' First solution: declare a worksheet variable referencing to your worksheet
dim ws as worksheet, rng as range, area as range
set ws = Worksheets(sheetname)
Set rng = ws.Range("A1:A10,B1:B20,G1:G3")
For Each area In rng.Areas
area.Hidden = True
Next Area
' You could replace the dim of ws by a With Worksheets(sheetname)
' and work with .Range() instead
End Sub
I am redesigning a part of a webpage to make it easier to update in the future. Currently, it is a series of tables, that are hard-coded. To redesign the table (for example, to alphabetize it like I want to), requires manually swapping around a lot of values in the html.
This is what I'd like to do:
Create a url_Link object with a title and link variable, to hold the display name and the url respectively.
Create an array of url_Link objects, and populate it at the top of the .asp file for the page.
Perform a for each loop on those arrays to build and populate the table
This itself isn't so bad, but I run into two problems.
First, I'd like to not have to define the array size, as this makes a second place that has to be changed when changes are made to the number of links.
There will be some logic to prevent certain url_Link objects from being displayed (for example, some users can't access certain pages, so they will not see links), and this would cause issues when sizing the arrays.
I know that I could just make the arrays of a large size, but this seems wasteful to me (and I don't know how for each functions and do not want a bunch of empty rows to show up).
What can I do to resolve these problems? I'm not very knowledgeable in vbscript, and most of the code that I have been working with does not take advantage of arrays or objects.
UPDATE:
I've tried using a redim PRESERVE to trim the excess fat of an oversized array. The problem is that in some cases, my array is populated by smaller amounts of objects than its max size because of if conditions. This is causing problems later when I use a for loop (tried to get a for each to work and that is not happening at the moment). I get the error "This array is fixed or temporarily locked" on the redim line
Code:
dim systemSettingsArray(1)
arrayCounter = 0
if ADMIN = "Y" then
set systemSettingsArray(arrayCounter) = (new url_Link).Init("Account Administration","Maintenance/Account_Admin.asp")
arrayCounter = arrayCounter + 1
end if
set systemSettingsArray(arrayCounter) = (new url_Link).Init("Time Approval","Maintenance/system_Time_Approval.asp")
redim Preserve systemSettingsArray(arrayCounter)
To show the correct way to use dynamic arrays in VBScript and to prove Matt's comment wrong:
Option Explicit
ReDim a(-1)
Dim b : b = Array()
Dim c()
Dim i
For i = 0 To 1
ReDim Preserve a(UBound(a) + 1) : a(UBound(a)) = i
ReDim Preserve b(UBound(b) + 1) : b(UBound(b)) = i
On Error Resume Next
ReDim Preserve c(UBound(c) + 1) : c(UBound(c)) = i
WScript.Echo Err.Description, "- caused by Dim c()"
On Error GoTo 0
Next
WScript.Echo "a:", Join(a)
WScript.Echo "b:", Join(b)
output:
Subscript out of range - caused by Dim c()
Subscript out of range - caused by Dim c()
a: 0 1
b: 0 1
Update wrt comment:
Both the a and the b way are correct - you get an one dimensional dynamic array to which UBound() can be applied from the start. Some people may prefer b, because they don't like ReDim v without a previous Dim v; other may feel that b is clumsy or errorprone.
If you look at this problem about a two-dimensional array, you may come to the conclusion, that the a way scales better.
Use redim preserve on the array. You can use UBound to find the current number of elements and do something like
ReDim Preserve myArrayName (UBound(myArrayName) + 1)
http://msdn.microsoft.com/en-us/library/c850dt17%28v=vs.84%29.aspx
Does VBA support using an array of range variables?
dim rangeArray() as range
dim count as integer
dim i as integer
count = 3
redim rangeArray(1 to count)
for i = 1 to count
msgbox rangeArray(i).cells(1,1).value
next
I can't get it to work in this type of application. I want to store a series of ranges in a certain order as a "master copy". I can then add, delete, sort or do whatever to this array and then just print it out to a series of ranges in excel. It doesn't seem like excel supports this - it just forces you to store your data in the spreadsheet and you have to reread it in order to use it.
No, arrays can't hold objects. But oObjects can hold objects. I think what you may want is a Range object that consists of various specific other Range object. In this example, rMaster is my "array" that holds three cells.
Sub StoreRanges()
Dim rMaster As Range
Dim rCell As Range
Set rMaster = Sheet1.Range("A1")
Set rMaster = Union(rMaster, Sheet1.Range("A10"))
Set rMaster = Union(rMaster, Sheet1.Range("A20"))
For Each rCell In rMaster
MsgBox rCell.Address
Next rCell
End Sub
With my new found knowledge that arrays can hold ranges (thnx jtolle), here's an example of how you would store ranges in an array and sort them
Sub UseArray()
Dim aRng(1 To 3) As Range
Dim i As Long
Set aRng(1) = Range("a1")
Set aRng(2) = Range("a10")
Set aRng(3) = Range("a20")
BubbleSortRangeArray aRng
For i = LBound(aRng) To UBound(aRng)
Debug.Print aRng(i).Address, aRng(i).Value
Next i
End Sub
Sub BubbleSortRangeArray(ByRef vArr As Variant)
Dim i As Long, j As Long
Dim vTemp As Variant
For i = LBound(vArr) To UBound(vArr) - 1
For j = i To UBound(vArr)
If vArr(i).Value > vArr(j).Value Then
Set vTemp = vArr(i)
Set vArr(i) = vArr(j)
Set vArr(j) = vTemp
End If
Next j
Next i
End Sub
It's not entirely clear what you want to do, but...
If you want a collection, why not use a VBA Collection Object?
Dim myRanges as New Collection
A Collection.Item can be any object, including a Range.
A Range object doesn't hold data; it holds a reference to worksheet cells. If you want the Range contents in your collection, you'll have to copy them to and from the worksheet.
As with Java, your VBA variables are ephemeral, whether in an Array or Collection. If you want to close the file and have the data there when you open it again, you have to have it in worksheet cells. The worksheets are your persistence mechanism.
I'm going to take a big leap here so if I'm way off, ignore me. What I think you're looking for suggests setting up a separate worksheet as your "database", populated with List/Table objects holding your raw data. In front of that, is your "user sheet" where you do the interesting stuff, referring to the data in the database sheet. Name everything.
It's not completely clear for me what you're talking about.
If you're asking about an ability to create Ranges that map to nothing and exist on their own, then no, there's no way. A Range object is just something that refers to a certain area of a worksheet. It doesn't have any storage of its own or something. Several different instances of Range class can refer to the same worksheet area, too.
And if you just want to store some references in an array, then that's fine, go for it. The only problem with your code is that you don't initialize the array elements before using them: as the Range is a reference type, all elements get initialized with Nothings by default.