Using a passed string to access different class properties - vba

Preface: I'm very much a dabbler at coding, and I'm not surprised my current code doesn't work, but I can't figure out whether I'm trying an approach that is fundamentally not possible or whether I just don't understand the correct syntax. Unlike the majority of the rest of my current project code, I haven't yet found the solution in the many other posts here.
Context: Playing Elite: Dangerous. I have a list of star systems in an Excel worksheet with each row containing one system (columns: name, x, y, z coordinates, and some properties such as Visited, RareGoodsSource). I've created a StarSystem class and read the worksheet into a Collection of StarSystems (named colSys). This works. For each property of the class I have a separate worksheet (columns: name, property) where I manually adjust property values (e.g. just visited Tau Ceti in game, on worksheet "csvVisited" manually add row "Tau Ceti", "TRUE"). In VBA I then compare those to the values in the Collection elements and update the latter if necessary. (Eventually I pump all this stuff to AutoCAD to visualise and plan travel routes.)
Issue: I currently have a separate Sub for each property, identical except for the name of the worksheet (e.g. "csvVisited" / "csvRareGoodsSource") and the references to access the property (e.g. colSys.Item(r.Value).Visited / colSys.Item(r.Value).RareGoodsSource). This works. But it seems Wrong from the perspectives of aesthetics, efficiency, and maintenance. Surely I should have only one Sub, which I pass Visited or RareGoodsSource as required?
My current code for this generic sub is at the end of the post, first I have an extremely abstracted version for clarity. My first attempt was to simply literally replace Visited with strProperty everywhere in the Sub, and pass Visited or RareGoodsSource to the Sub into that string variable.
This works fine for the worksheet reference, presumably because .Item() requires a string anyway. I am not entirely surprised it does not work for the property reference, because I'm passing a string variable in the hopes VBA understands this as an object property name, but I have been unable to find how one should do this. Hopefully it's just a result of my embarrassing lack of basic programming knowledge, and I just need some brackets or quotes or &'s somewhere.
Simplified example code, which works correctly (...except the bit that doesn't, obviously):
Sub TestVisited()
Call TestGeneric("Visited")
End Sub
Sub TestGeneric(strProperty As String)
Dim wsCSV As Worksheet
Set wsCSV = ActiveWorkbook.Worksheets.Item("csv" & strProperty)
'successfully sets wsCSV to Worksheets.Item("csvVisited"),
'presumably because .Item() expects a string anyway.
Dim r As Range
For Each r In wsCSV.Range(wsCSV.Cells(2, 1), wsCSV.Cells(4, 1))
Debug.Print "Explicitly coded: " & colSys.Item(r.Value).Visited
Debug.Print "Passed as string: " & colSys.Item(r.Value).strProperty
Next r
'The first Debug.Print works, the second does not:
'"Object doesn't support this property or method."
End Sub
The current real code for context:
(Note I've disabled the error trap on the .Contains replacement, because otherwise that would trap this problem instead.)
Sub UpdatePropertyFromWorksheetCSVProperty(strProperty As String)
'set the cell column/row positions in Worksheets.
Let celCSVDataColumn = 2
'prepare reference to Worksheet to read.
Dim wsCSV As Worksheet
Set wsCSV = ActiveWorkbook.Worksheets.Item("csv" & strProperty)
'prepare reference to Range to read.
Dim rngData As Range
Set rngData = wsCSV.Range(wsCSV.Cells(celFirstDataRow, celKeyColumn), wsCSV.Cells( _
wsCSV.Cells(wsCSV.Rows.Count, celKeyColumn).End(xlUp).Row _
, celKeyColumn)) ' middle segment finds the last occupied cell in column A and returns its row index.
'for each Worksheet row, compare the property value in the Worksheet to the value in the Collection Element,
'if different write the Worksheet value to the Collection Element, and flag the Element as ModifiedSinceRead.
Dim r As Range
For Each r In rngData
'check Sytem exists in the Collection.
'except VBA Collections don't have a .Contains method apparently.
'use error trapping instead.
'On Error GoTo ErrorHandler
'compare/copy Worksheet and Collection values.
If Not colSys.Item(r.Value).strProperty = r.Offset(0, celCSVDataColumn - 1).Value Then
On Error GoTo 0 'disables error trap again.
Let colSys.Item(r.Value).strProperty = r.Offset(0, celCSVDataColumn - 1).Value
Let colSys.Item(r.Value).xlsModifiedSinceRead = True
'DEBUG: test to immediate window
Debug.Print "System " & colSys.Item(r.Value).Name & " " & strProperty & " property changed to " & colSys.Item(r.Value).strProperty & "."
'
End If
ResumeNextSystem:
Next r
'DEBUG: test to immediate window
Debug.Print colSys(1).Name & vbTab & colSys(1).x & vbTab & colSys(1).RareGoodsSource & vbTab & colSys(1).RareGoodsChecked & vbTab & colSys(1).Visited & vbTab & colSys(1).xlsModifiedSinceRead
Debug.Print colSys(10160).Name & vbTab & colSys(10160).x & vbTab & colSys(10160).RareGoodsSource & vbTab & colSys(10160).RareGoodsChecked & vbTab & colSys(10160).Visited & vbTab & colSys(10160).xlsModifiedSinceRead
Debug.Print colSys("Lave").Name & vbTab & colSys("Lave").x & vbTab & colSys("Lave").RareGoodsSource & vbTab & colSys("Lave").RareGoodsChecked & vbTab & colSys("Lave").Visited & vbTab & colSys("Lave").xlsModifiedSinceRead
'
Exit Sub
ErrorHandler:
MsgBox ("Processing Worksheet " & wsCSV.Name & " error at system " & r.Value & ", skipping to next.")
'DEBUG: test to immediate window
Debug.Print "Processing Worksheet " & wsCSV.Name & " error at system " & r.Value & ", skipping to next."
'
Resume ResumeNextSystem
End Sub
Solution in real code:
'stays as-is:
Set wsCSV = ActiveWorkbook.Worksheets.Item("csv" & strProperty)
'Get old:
If Not colSys.Item(r.Value).strProperty = r.Offset(0, celCSVDataColumn - 1).Value Then
'new:
If Not CallByName(colSys.Item(r.Value), strProperty, VbGet) = r.Offset(0, celCSVDataColumn - 1).Value Then
'Let old:
Let colSys.Item(r.Value).strProperty = r.Offset(0, celCSVDataColumn - 1).Value
'new:
CallByName colSys.Item(r.Value), strProperty, VbLet, r.Offset(0, celCSVDataColumn - 1).Value

You can use CallByName built-in function to get the property.
v = CallByName(colSys.Item(r.Value), strProperty, vbGet)
This KB article explains it: https://support.microsoft.com/kb/186143

Related

VBA SaveCopy function + lose a worksheet

I created an Excel 2016-based template, which the user can fill and create a work form based on it. User inserts an unique ID and with basic INDEX&MATCH formulas some ID-related parameters are being fetched from separate worksheet a. The work form is created with VBA-macro using SaveCopyAs method.
After the parameters have been fetched and VBA is launched to create the work form the ID will not change anymore. Thus, I don't need the whole worksheet a anymore and would like to drop it to keep the work form more lightweight. I'm capable of retaining the fetched parameters, so this is not a problem.
I would NOT want the user to have to re-open the form every single time a work form is created, so I don't want the VBA to remove worksheet a from the template itself, as even though the user can't save changes to the template, (s)he would have to re-open the template file every time a work form has to be created.
Any idea if something could be done? Might it be possible to somehow run SaveCopyAs or similar method, but drop the worksheet a at the same time from the new target file? Having INDEX&MATCH formula fetch the needed information from another workbook would theoretically work but to my knowledge requires the other workbook to be open at all times which will undoubtedly start to cause unnecessary issues.
My current VBA for work form creating is something like this:
Sub Save_copy()
Dim FileName As String
With ActiveWorkbook
[H3] = Format(Now, "dd.mm.yy_hhmm")
Range("H2").Value = Range("H1").Value
FileName = "SERVICE " & _
Range("H1").Value & _
" - " & Format(Now, "dd.mm.yy") & _
"_" & Format(Now, "hhmm") & _
"." & Right(.Name, Len(.Name) - InStrRev(.Name, "."))
.SaveCopyAs "G:\SERVICE" & "\" & FileName
End With
Call Reset
End Sub
If I understood you properly try something like this ("air-coded" so there may be typos):
Sub Save_copy()
Dim FileName As String
With ActiveWorkbook
[H3] = Format(Now, "dd.mm.yy_hhmm")
Range("H2").Value = Range("H1").Value
FileName = "SERVICE " & _
Range("H1").Value & _
" - " & Format(Now, "dd.mm.yy") & _
"_" & Format(Now, "hhmm") & _
"." & Right(.Name, Len(.Name) - InStrRev(.Name, "."))
.SaveCopyAs "G:\SERVICE\" & FileName
End With
Dim newWorkbook As Excel.Workbook
Set newWorkbook = Workbooks.Open("G:\service\" & FileName)
newWorkbook.Worksheets("A").Delete
newWorkbook.Close True
Reset
End Sub
Additionally, a couple of coding tips:
There's no need for Call - that function is deprecated and only exists to keep ancient code from blowing up
There is an extra concatenation of the "\" in your .SaveCopyAs line - simply put the trailing slash in with the rest of the path (as I did).
The unqualified Range("H2") refers to the ActiveWorksheet and could blow up on you if your user ever happens to click on a different worksheet while your code is running

Linking cells to different worksheet by using vba to insert dynamic formulas

I am creating a new worksheet called varRef and am trying to link cells of an existing worksheet to this new worksheet by using VBA. I have been trying to solve it for a couple of hours but cannot get it right and cannot find a similar case on the web either.
The code is:
Dim varRef as Variant 'name of the new sheet
varRef = Inputbox("xyz") 'this is how I define the name
Dim intRow As Integer 'row that corresponds to the appropriate postion in the existing sheet
intRow = ActiveCell.Row 'see above
Cells(intRow, 22).Formula = "=IF(varRef & ""!I148""="""","""",varRef & _
""!I148"")" 'trying to link the contents within the same workbook
The result in the cell I get is
=IF(varRef & "!I148"="","",varRef & "!I148"
which is obviously not working.
Issue 1) is that VBA does not recognize my variable in the formula. It is working for naming the sheet however.
Issue 2) is the quotation marks, that are not working as intended. One is supposed to use double quotation marks to not end the string. However the second marks will not disappear in the final code of the cell.
Any help is very much appreciated and hopefully valuable for other users as well!
You had some count issues with hoe many " you have before and after the varRef variable.
Also, I prefer to use Chr(34) to have " inside the Formula string, this way I don't get confused with how many " I need to use.
Try the code below:
Cells(intRow, 22).Formula = "=IF(" & varRef & "!I148=" & Chr(34) & Chr(34) & "," _
& Chr(34) & Chr(34) & "," & varRef & "!I148)"

Correct formula giving "Run-time error '1004': Application-defined or object-defined" error when running sub

I want to paste formula's in column B with a delay in between. The formula should only paste the formula if the cell left to it (in case of B1 this is A1) is not empty, like this:
I have the following VBA that pastes the formula with a delay of 1 sec.
Option Explicit
Sub RetrieveDataDelay()
'paste formulas into cells, then calculate, then delay rinse repeat
'=========================
'Declare Variables
'=========================
Dim i As Long 'used to loop
Dim rowCount As Long
Dim LastRow As Long 'used to find the last row
'=========================
'Setup for Speed
'=========================
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
'=========================
'Define Variables
'=========================
LastRow = Worksheets(ActiveSheet.Name).Cells(Rows.Count, 1).End(xlUp).Row 'This will find the last used row in column A, change the number 1 to whatever column number you want
rowCount = 1 ' set to how many rows you want to do at a time
'=========================
'Do Work
'=========================
For i = 1 To LastRow Step rowCount
Range("B" & i, "B" & WorksheetFunction.Min(i + rowCount - 1, LastRow)).Formula = "'=IF(ISBLANK(A" & i & ");" & """" & """" & ";Dump(Volumes(A" & i & ";2528;1010;TRUE;" & "Volume" & ";TRUE)))" 'set the formula to whatever it needs to be
Calculate
Application.Wait (Now + TimeValue("00:00:01")) 'this delays 1 second
Next i
'=========================
'Setup for Speed - Reverse
'=========================
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
End Sub
The error occurs at this part
Range("B" & i, "B" & WorksheetFunction.Min(i + rowCount - 1, LastRow)).Formula = "=IF(ISBLANK(A" & i & ");" & """" & """" & ";Dump(Volumes(A" & i & ";2528;1010;TRUE;" & "Volume" & ";TRUE)))"
The errors has something to due with the formula, which it does not accept. This is a custom formula used with an excel extension. I know that the formula works, as I put a single quotation mark in front as such:
Range("B" & i, "B" & WorksheetFunction.Min(i + rowCount - 1, LastRow)).Formula = "'=IF(ISBLANK(A" & i & ");" & """" & """" & ";Dump(Volumes(A" & i & ";2528;1010;TRUE;" & "Volume" & ";TRUE)))"
so that it pastes the formally literally. If I then remove the quotation mark from the formula the formula works. So the questions remains why it doesn't accept this specific formula in the VBA.
Using Excel 2013.
Range.Formula needs the formula notation always in en_us format independent of the current locale settings. That means functions in English language and also comma as delimiter between formula parameters and not semicolon.
So
.Formula = "=IF(ISBLANK(A" & i & ")," & """" & """" & ",Dump(Volumes(A" & i & ",2528,1010,TRUE," & "Volume" & ",TRUE)))"
After setting Range.Formula, the locale settings will be nevertheless active in the sheet. So in the sheet the formula will be delimited with semicolons if so set.
Btw.: The complicated string notation for double quotes is not neccessary.
.Formula = "=IF(ISBLANK(A" & i & "),"""",Dump(Volumes(A" & i & ",2528,1010,TRUE," & "Volume" & ",TRUE)))"

VBA 1004 error - Unable to set the FormulaArray property of the Range class

I am inserting the following INDEX MATCH formula into a set of cells using VBA.
cell2.FormulaArray = _
"=INDEX('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!AK:AK, _
MATCH(1,('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$A:$A = A " & value & ")* _
('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$B:$B=""Total""),0))*1000"
When this runs, I encounter an error 1004 - "Unable to set the FormulaArray property of the Range class"
I'm fairly certain it has something to do with referencing a workbook outside of the current as it works fine when I INDEX MATCH inside the same workbook.
The condensed version of the formula (for reading clarity) is the following -
=INDEX(Sheet2!AK:AK, MATCH(1,(Sheet2!A:A = A5)*(Sheet2!B:B="Total"),0))
Then in vba -
cell2.FormulaArray = "=INDEX(Sheet2!C:C, MATCH(1,(Sheet2!A:A = A5)*(Sheet2!B:B=""Total""),0))"
How to enter FormulaArray with over 255 characters using VBA
It seems that, in this case, there was an alternate Standard Formula that complied with the requirements of the original FormulaArray. However, there might be cases for which there is not an alternate formula.
For those cases, I have the following method to enter FormulaArray with over 255 characters using VBA.
Most of the time when a FormulaArray is longer than 255 characters is due to the length of the references it contains, as they may relate to long constant arrays, external workbooks with large names (like in this case) or worksheets with large names (also in this case). The method consist in replacing these long strings with shorter ones, however in order for the FormulaArray (after replacement) to be accepted as a FormulaArray those shorter strings need to also represent valid references.
As per the above, there could be, at least, three situations with long references:
Long constant arrays: In these cases use Defined Names as described here
https://support.office.com/en-za/article/Guidelines-and-examples-of-array-formulas-7d94a64e-3ff3-4686-9372-ecfd5caa57c7
Workbooks with large names and
Worksheets with large names
For cases 2 and 3 same method applies: The use of short references pointing to a temporary Worksheet as temporary replacement.
Applying the method to this case:
Original FormulaArray: use variable sFmlArray to hold the formula
Dim sFmlArray As String
sFmlArray = "=INDEX('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!AK:AK," & _
"MATCH(1,('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$A:$A = A" & bVal & ") * " & _
"('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$B:$B=""Total""),0)) * 1000"
I suggest the use of variables to hold the names of the workbook and worksheet in order to avoid having to write them several times.
Dim sFmlRng as string
sFmlRng = "'[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!"
Replace the names of the workbook and worksheet in the FormulaArray with the corresponding variable:
sFmlAry = "=INDEX(" & sFmlRng & "AK:AK," & _
"MATCH(1,(" & sFmlRng & "$A:$A = A" & bVal & ") * " & _
"(" & sFmlRng & "$B:$B=""Total""),0)) * 1000"
Assuming we want to enter this long FormulaArray in the range D7:D10, let’s assign it to a variable
Dim rFmlAry as Range
Set rFmlAry = ActiveSheet.Range("D7:D10")
Use the function below to add the temporary worksheet. This function also provides the temporary reference to be used as replacement in the FormulaArray
Function WshTmp_Add(rFmlAry As Range, sFmlRngTmp As String) As Worksheet
sFmlRngTmp = "#Tmp"
With rFmlAry.Worksheet.Parent
On Error Resume Next
.Worksheets(sFmlRngTmp).Delete
On Error GoTo 0
Set WshTmp_Add = .Worksheets.Add(Before:=.Worksheets(1))
End With
WshTmp_Add.Name = sFmlRngTmp
WshTmp_Add.Tab.Color = 255
sFmlRngTmp = "'" & sFmlRngTmp & "'!"
Application.Goto rFmlAry
End Function
Replace, in the FormulaArray, the long references with the shorter one and enter the temporary FormulaArray in the rFmlAry range
sFmlAryTmp = WorksheetFunction.Substitute(sFmlAry, sFmlRng, sFmlRngTmp)
rFmlAry.FormulaArray = sFmlAryTmp
With the FormulaArray in place, replace the temporary short references with the original long ones
rFmlAry.Replace What:=sFmlRngTmp, Replacement:=sFmlRng, _
LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False, _
SearchFormat:=False, ReplaceFormat:=False
Finally delete the temporary Worksheet
This is the entire procedure (as a test, added few lines at the end to validate the result)
Sub FormulaArray_Over255Chr()
Dim rFmlAry As Range, sFmlAry As String, bVal As Byte
Dim WshTmp As Worksheet, sFmlAryTmp As String
Dim sFmlRng As String, sFmlRngTmp As String
Dim blAppDisplayAlerts As Boolean
blAppDisplayAlerts = Application.DisplayAlerts
Rem Set Ranges & Values
bVal = 5
Set rFmlAry = ActiveSheet.Range("D2:D5")
Rem Define External Reference Variable
sFmlRng = "'[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!"
Rem Define FormulaArray (Original) - with References as Variables
sFmlAry = "=INDEX(" & sFmlRng & "AK:AK," & _
"MATCH(1,(" & sFmlRng & "$A:$A = A" & bVal & ") * " & _
"(" & sFmlRng & "$B:$B=""Total""),0)) * 1000"
Rem Set Range to Enter FormulaArray
Set rFmlAry = ActiveSheet.Range("D7:D10")
Rem Add Temporary Worksheet
Application.DisplayAlerts = False
Set WshTmp = WshTmp_Add(rFmlAry, sFmlRngTmp)
Rem Set Temporary FormulaArray - Replace long references
sFmlAryTmp = WorksheetFunction.Substitute(sFmlAry, sFmlRng, sFmlRngTmp)
Rem Enter Temporary FormulaArray
rFmlAry.FormulaArray = sFmlAryTmp
Rem Set FormulaArray (Original) - Replace short references in situ
rFmlAry.Replace What:=sFmlRngTmp, Replacement:=sFmlRng, _
LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False, _
SearchFormat:=False, ReplaceFormat:=False
Rem Delete Temporary Worksheet
WshTmp.Delete
Application.DisplayAlerts = blAppDisplayAlerts
' ****************************************************************
' Lines for TESTING - Resulting FormulaArray - REMOVED when final
' ****************************************************************
Rem Validate FormulaArray
Debug.Print String(3, vbLf)
Debug.Print "FormulaArray in Range: "
Debug.Print rFmlAry.Cells(1).FormulaArray
Debug.Print "FormulaArray VBA: "
Debug.Print sFmlAry
If rFmlAry.Cells(1).FormulaArray = sFmlAry Then
MsgBox "FormulaArray with +255 entered successfully" & vbLf & _
vbLf & rFmlAry.Cells(1).FormulaArray
Else
MsgBox "Something did not worked!" & vbLf & _
vbLf & "Review formulas printed in the Immediate Window"
SendKeys "^g": Stop
End If
' ****************************************************************
End Sub
As per my comment, this is down to a limitation of the length of formula you can use in VBA with FormulaArray. Here, you can probably simply use a non-array version:
cell2.Formula = _
"=LOOKUP(2,1/('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$A:$A=A" & Value & _
")*('[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!$B:$B=""Total"")," & _
"'[08 Debt Comparison & Provision Report.xlsx]Details by Bus Area & Location'!AK:AK)*1000"
though this will return the last matching item rather than the first, if you have multiple rows matching your criteria.

I want to use a variable which contains a cell reference within a formula

I have been trying this code out and If I use direct cell reference in the formula it works fine but when I substitute the cell reference for a variable it doesn't work.
Can you tell me where im going wrong.
The aim is to add up all the cells which contain a date in november
This is the code
Private Sub Worksheet_Change(ByVal Target As Range)
Dim last As Long
Dim cussat As Variant
Dim Cussatrange As String
With ActiveSheet
last = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
If Target.Address = "$C$" & last + 1 Then
Range("$B$" & last + 1).Value = Date
Range("$A$" & last + 1).Value = "Moss"
Cussatrange = "J1:J" & last
' I would like to substitue the cell reference in the above formula to use Cussatrange or last
cussat = [=SUMPRODUCT(--(TEXT(J1:J43,"mmm yyyy")="Nov 2014"))]
MsgBox "Last used row number in column A is " & last & " " & cussat & " " & Cussatrange
End If
End Sub
If I understand your question correctly, then you can use something like this:
Dim myFormula As String
Dim template As String
template = "=SUMPRODUCT(--(TEXT({0},""mmm yyyy"")=""Nov 2014""))"
myFormula = Replace(template, "{0}", Cussatrange)
cussat = Application.Evaluate(myFormula)
MsgBox "Last used row number in column A is " & last & " " & cussat & " " & Cussatrange
In the code a template for the formula is created and then the substring (in this case it is {0}) is repalaced with the address which is stored in the string Cussatrange.
For more info about Application.Evaluate method have a look here:
http://msdn.microsoft.com/en-us/library/office/ff193019%28v=office.15%29.aspx.
(Using square brackets (for example, "[A1:C5]") is identical to calling
the Evaluate method with a string argument)