Array keeping values after the end of the code - arrays

I was wondering if there is a way to keep some values stored in an array after the code ends and the excel file is closed.
The idea would be to have an array containing some values. When running the code, I might or might not modify some of its values and when I close the file I'd like this array to subsist and keep its values.
What I mean by that is that when I would re-open the file and run the code, all the other variables would be re-initialised (and obviously would get default values again), but this special array would still have all its values stored (so I would of course not re-declare it in that code).
One way to avoid this trouble would be to store all the values contained in the array in a worksheet and whenever the code is run again, put them back into a new array.
But I am wondering if there is a way to do this without using a worksheet.

One tends to think of a worksheet as the heart and soul of Excel and is, therefore, reluctant to make "such a big effort" to store just one array. From Excel's point of view a sheet is nothing more than an array, taking up just a few bytes of memory while it is empty. I would argue that a VeryHidden worksheet is precisely what you are asking for, in fact very similar to Word's Document Variables.
As an alternative I could suggest using one of the built-in properties, such as Keywords or Comments, to store a string which can be Split into an array. This would probably be more to your liking but I think it is inferior because the user can edit it, unless that is something you can use to your advantage.
Technically, and for all practical purposes, a VeryHidden worksheet is precisely what you are asking for. Your resistance is based in psychology, not reason - at least, that is how I convinced myself, and I can say being more liberal with the number of worksheets I create has given me more liberties at no perceptible cost.

Either store information, as you mentioned, within a (hidden) sheet, or use Document Variables.
From the documentation:
Sub AddDocumentVariable()
ActiveDocument.Variables.Add Name:="Age", Value:=12
End Sub
These variables will be available even if your code has finished or the workbook was closed and opened again.
Edit: (for excel, code found here)
Sub test()
Dim wb As Workbook
Dim docProps As DocumentProperties
Dim docProp As DocumentProperty
Set wb = ActiveWorkbook
Set docProps = wb.CustomDocumentProperties
With docProps
.Add Name:="CustomNumber", _
LinkToContent:=False, _
Type:=msoPropertyTypeNumber, _
Value:=1000
.Add Name:="CustomString", _
LinkToContent:=False, _
Type:=msoPropertyTypeString, _
Value:="This is a custom property."
.Add Name:="CustomDate", _
LinkToContent:=False, _
Type:=msoPropertyTypeDate, _
Value:=Date
End With
For Each docProp In docProps
Debug.Print docProp.Name, docProp.Value
Next
End Sub

Related

Using array instead of ranges in VBA

I've only been using VBA for a couple of weeks now so bear with me.
I'm trying to change a macro so that it reads from an array instead of a range. One of the sections I need to change uses .formulaR1C1 to run a vlookup, but im having a hard time trying to work out how this can be done with the array.
An example of this is the below line:
.Range("M2:L" & lastrow).FormulaR1C1 = "=VLOOKUP(RC[-1], Sheet2!R1C1:R4C10, 3, 0)"
I'm not too sure on whether I can set the value of an array to a formula, as I've done above, or if I have to maybe store the value as a String and then edit the value of the cell later when printing the column back on to the worksheet.
What I have so far is below:
For i = 2 To lastrow
arr(i, 13).FormulaR1C1 = "=VLOOKUP(RC[-1],Sheet2!R1C1:R4C10,3,0)"
Next
Thanks in advance!
Storing the value as a string would certainly be a valid option. Simply write the formula in your array, presuming it is of Variant / String type, and when you put the data back into a worksheet you can use .FormulaR1C1 on a Cell (Range) object to apply it as a formula.
arr(i, 13) = "=VLOOKUP(RC[-1],WMSPAM!R1C1:R4C10,3,0)"
...
Range("M2").FormulaR1C1 = Arr(1,13)
I believe this approach is likely the most effective and most easily maintained for you. As you are learning and appear to be curious of what is possible, here are a couple more examples of how you could approach this, with some further explanation.
.FormulaR1C1 is a Range object method and so the only way it could be called on an Array item would be if that item was a Range object.
Dim arr(0 To 10) As Range
Set arr(0) = Range("A1")
arr(0).FormulaR1C1 = "=2+2"
Note that as Ranges are an Object (of reference type), this operation will directly effect the Range specified in the array. In the above example, the formula "=2+2" will be placed into cell A1. You can learn more about the difference between Reference and Value types here.
If your array contains only values, the other way to achieve what you need is to use the WorksheetFunction object. With this object you can access the formula functions, that you would use in worksheet, within VBA.
WorksheetFunction.VLookup(Arg1, Arg2, Arg3, Arg4)
As with anything in writing code the WorksheetFunction methods take some trial and error to get them to work how you would expect, but in my experience these specifically can be a tricky to implement, there are though some cases where they can be very useful!
You can read more about the VLookup method here.
You may try to use .Offset():
Sub Test()
lastrow = 11
With Sheets(1)
Set Rng = .Range("M2:L" & lastrow)
End With
For i = 0 To Rng.Rows.Count - 1
Rng.Offset(i, 12).FormulaR1C1 = "=VLOOKUP(RC[-1],Sheet2!R1C1:R4C10,3,0)"
Next
End Sub
I'm not sure what you are asking. Your first code line writes a formula to a two-column range in the most efficient way there is already, your second snippet shows the less efficient way of doing it one cell at the time. If the goal is to use the vlookup to fill cells and then dispose of the formula, one efficient way is this:
.Range("M2:L" & lastrow).FormulaR1C1 = "=VLOOKUP(RC[-1], Sheet2!R1C1:R4C10, 3, 0)"
.Range("M2:L" & lastrow).Value2 = .Range("M2:L" & lastrow).Value2

Storing rows, columns and sheets in dynamic arrays

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

Overwriting & Resizing Arrays in Excel VBA

See end for an edit in response to PatricK's comment.
Apologies if this is a basic question, but I have been looking around here and other sites and cannot find the answer.
This is my first foray into VBA arrays (I'm new to VBA in general) and I'm struggling with some syntax. I am trying to read some data (part of a single column) from a worksheet into an array, do some processing on it, write it to a different sheet and then overwrite the array with some more data read from the first sheet. One key point is that the number of data points read per iteration will vary (e.g. 4 data points on the first loop, 3 on the second perhaps...) and so the array size should change too.
My attempt at the code works for the first iteration, but it does not seem to pick up the data on the second loop, instead leaving the array blank. Here's my code (sorry if this doesn't format properly, I only currently have internet access via my phone!):
Looping...
Dim DataSubSet
With ActiveWorkbook.Worksheets("Sheet1").Range("A" & CStr(I) & ":A" & CStr(io))
ReDim DataSubSet(1 To .Rows.Count, 1 To .Columns.Count)
DataSubSet = .Range ("A" & CStr(I) & ":A" & CStr(io))
End With
**Do processing**
End loop
Here i and io are variables that determine the cells that I'm interested in; they each change with the overarching loop.
As I mentioned, this works for the first iteration only. Any pointers on where I've gone wrong would be appreciated!
Thanks in advance,
Sam
-----------------------------------------------------------------------------------
PatricK, for further clarification, here's a quick routine that I knocked up to illustrate your point.
** formatting check failed (still writing on phone) so wouldn't let me post. Here's a dropbox link to the code in a text file:
https://db.tt/E0btoI5f
One possible reason it worked once may be because the "Do processing" codes Activated another workbook. Also not sure why you use 2D array to store 1D array values.
But please try:
Dim oWB as Workbook
' Loop Start
Set oWB = ActiveWorkbook
'Or Set oWB = Workbooks("<workbook name you are trying to get the data from>")
With oWB.Worksheets("Sheet1").Range("A" & i & ":A" & io)
ReDim DataSubSet(1 To .Rows.Count, 1 To .Columns.Count)
DataSubSet = .Value
End With
' Do Processing and finish loop above this line
Set oWB = Nothing
Test data in Sheet1:
Watching the variable DataSubSet (I have set i = 3 and io = 10):

Populating VBA dynamic arrays

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.

Excel VBA: Variants in Array Variables

A question on variants. Im aware that variants in Excel vba are both the default data type and also inefficient (from the viewpoint of overuse in large apps). However, I regularly use them for storing data in arrays that have multiple data types. A current project I am working on is essentially a task that requires massive optimistaion of very poor code (c.7000 lines)- and it got me thinking; is there a way around this?
To explain; the code frequently stores data in array variables. So consider a dataset of 10 columns by 10000. The columns are multiple different data types (string, double, integers, dates,etc). Assuming I want to store these in an array, I would usually;
dim myDataSet(10,10000) as variant
But, my knowledge says that this will be really inefficient with the code evaluating each item to determine what data type it is (when in practise I know what Im expecting). Plus, I lose the control that dimensioning individual data types gives me. So, (assuming the first 6 are strings, the next 4 doubles for ease of explaining the point), I could;
dim myDSstrings(6,10000) as string
dim myDSdoubles(4,10000) as double
This gives me back the control and efficiency- but is also a bit clunky (in practise the types are mixed and different- and I end up having an odd number of elements in each one, and end up having to assign them individually in the code- rather than on mass). So, its a case of;
myDSstrings(1,r) = cells(r,1)
myDSdoubles(2,r) = cells(r,2)
myDSstrings(2,r) = cells(r,3)
myDSstrings(3,r) = cells(r,4)
myDSdoubles(3,r) = cells(r,5)
..etc...
Which is a lot more ugly than;
myDataSet(c,r) = cells(r,c)
So- it got me thinking- I must be missing something here. What is the optimal way for storing an array of different data types? Or, assuming there is no way of doing it- what would be best coding-practise for storing an array of mixed data-types?
Never optimize your code without measuring first. You'll might be surprised where the code is the slowest. I use the PerfMon utility from Professional Excel Development, but you can roll your own also.
Reading and writing to and from Excel Ranges is a big time sink. Even though Variants can waste a lot of memory, this
Dim vaRange as Variant
vaRange = Sheet1.Range("A1:E10000").Value
'do something to the array
Sheet1.Range("A1:E10000").Value = vaRange
is generally faster than looping through rows and cells.
My preferred method for using arrays with multiple data types is to not use arrays at all. Rather, I'll use a custom class module and create properties for the elements. That's not necessarily a performance boost, but it makes the code much easier to write and read.
I'm not sure your bottleneck comes from the Variant typing of your array.
By the way, to set values from an array to an Excel range, you should use (in Excel 8 or higher):
Range("A1:B2") = myArray
On previous versions, you should use the following code:
Sub SuperBlastArrayToSheet(TheArray As Variant, TheRange As Range)
With TheRange.Parent.Parent 'the workbook the range is in
.Names.Add Name:="wstempdata", RefersToR1C1:=TheArray
With TheRange
.FormulaArray = "=wstempdata"
.Copy
.PasteSpecial Paste:=xlValues
End With
.Names("wstempdata").Delete
End With
End Sub
from this source that you should read for VBA optimization.
Yet, you should profile your app to see where your bottlenecks are. See this question from Issun to help you benchmark your code.

Resources