Identifying and using Relative variable in excel VBA Macro? - vba

Alright, so I have a spreadsheet, and I want to use this code (and more) but I'm getting problems. I want to take row A and Find the [-x] I should be using (see RC[StartColumn]) and autofill the rest. I then want to be able to repeat the steps for columns A-AF.
So essentially I want to perform the first calculation below, and the second one right next to that, and I want to be able to start it anywhere on the document. I then want to be able to copy that down each row for every column leading up to the first cell I chose.
Problem is I'm incredibly new to Macros, I put that together by observing my record macro results and playing around a little, but this is becoming too advanced for my current level and I want to finish this by today if possible. Could someone please show me the way, in a way that could teach me so I could do this on my own in the future?
Sub ConcatenateStep1()
Dim StartColumn As Integer
StartColumn = 1 - ActiveCell.Column
ActiveCell.FormulaR1C1 = _
"=IF(AND(RC14=R[1]C14),IF(OR(ISNUMBER(FIND(LOWER(RC[StartColumn]),LOWER(R[1]C[StartColumn]),1)),ISNUMBER(FIND(LOWER(R[1]C[StartColumn]),LOWER(RC[StartColumn]),1))),IF(LEN(RC[StartColumn])<LEN(R[1]C[StartColumn]),R[1]C[StartColumn],RC[StartColumn]),CONCATENATE(RC[StartColumn],"", "",R[1]C[StartColumn])),IF(AND(RC14<>R[1]C14,RC14<>R[-1]C14),RC[StartColumn],"" ""))"
Range("AK2").Select
Set SourceRange = Range("AK2")
Set fillRange = Range("AK2:AK22000")
SourceRange.AutoFill Destination:=fillRange
Range("AL2").Select
ActiveCell.FormulaR1C1 = _
"=IF(AND(RC14=R[1]C14,RC14<>R[-1]C14),IF(OR(R[1]C[-1]="" "",ISNUMBER(FIND(LOWER(RC[-1]),LOWER(R[1]C[-1]),1)),ISNUMBER(FIND(LOWER(R[1]C[-1]),LOWER(RC[-1]),1))),IF(LEN(RC[-1])<LEN(R[1]C[-1]),R[1]C[-1],RC[-1]),CONCATENATE(RC[-1],"", "",R[1]C[-1])),IF(AND(RC14<>R[1]C14,RC14<>R[-1]C14),RC[-1],"" ""))"
Set SourceRange = Range("AL2")
Set fillRange = Range("AL2:AL22000")
SourceRange.AutoFill Destination:=fillRange
Range("AL2").Select
End Sub

This is only a partial answer.
Are these incredibly long and complicated formulae the best way of achieving whatever you want to achieving?
There are two errors in the first formula that prevent it being accepted by Excel when the macro attempts to place it in a cell.
The formula is a long string containing StartColumn. But StartColumn is a variable. You need to replace every occurence of StartColumn with " & StartColumn & ".
Within a string each double quote required in the result must be replaced by two double quotes. Within CONCATENATE(RC[StartColumn],"", "",R[1]C[StartColumn]) the double quotes have not been doubled although those at the end of the formula have.
Excel will accept the following (which means it is syntactically correct) although I do not understand its purpose so it may be logically incorrect:
ActiveCell.FormulaR1C1 = _
"=IF(AND(RC14=R[1]C14),IF(OR(ISNUMBER(FIND(LOWER(RC[" & StartColumn & _
"]),LOWER(R[1]C[" & StartColumn & "]),1)),ISNUMBER(FIND(LOWER(R[1]C[" & _
StartColumn & "]),LOWER(RC[" & StartColumn & "]),1))),IF(LEN(RC[" & _
StartColumn & "])<LEN(R[1]C[" & StartColumn & "]),R[1]C[" & StartColumn & _
"],RC[" & StartColumn & "]),CONCATENATE(RC[" & StartColumn & _
"],"""", """",R[1]C[" & StartColumn & _
"])),IF(AND(RC14<>R[1]C14,RC14<>R[-1]C14),RC[" & StartColumn & "],"" ""))"
If I have parsed this formula correctly, CONCATENATE(RC[StartColumn],"", "",R[1]C[StartColumn]) has the same output as CONCATENATE(RC[StartColumn],R[1]C[StartColumn])
I agree with MP24, you must clarify your question before I or someone else can help you further. The Macro Recorder records syntactically correct VBA but it is not good VBA. It does not know your objective so records individual keyboard actions when a skilled programmer would combine them. It is almost never a good idea to use Select or to work with the ActiveCell but the Macro Recorder has no choice but to do so.
Part 2
Although Excel allowed the macro to place the first forumula, it later complained the formula contains a circular reference. Its gone midnight here, I will look at it tomorrow.
Part 3
Having looked at your first formula again, I believe I incorrectly parsed it last night. I now believe the concatenate function is correct. Also it appears that the circular reference depends on the location of the ActiveCell when the macro starts. I was able to clear this error by selecting a different start position.
My current attempt to parse your formula gives:
Outer: "=IF(X1,X2,X3)"
X1: AND(RC14=R[1]C14)
X2: IF(X4,X5,X6)
X3: IF(X7,RC[StartColumn],"" "")
X4: OR(X8,X9)
X5: IF(X10,R[1]C[StartColumn],RC[StartColumn])
X6: CONCATENATE(RC[StartColumn],"", "",R[1]C[StartColumn])
X7: AND(RC14<>R[1]C14,RC14<>R[-1]C14)
X8: ISNUMBER(X11)
X9: ISNUMBER(X12)
X10: LEN(RC[StartColumn])<LEN(R[1]C[StartColumn])
X11: FIND(LOWER(RC[StartColumn]),LOWER(R[1]C[StartColumn]),1)
X12: FIND(LOWER(R[1]C[StartColumn]),LOWER(RC[StartColumn]),1)
I have used macros to place formulae so they can later give changing results as the user enters values. Do you want changing results? If not, replacing this formula with VBA would almost certainly give more understandable and more maintainable code.
I do not approve of formulae like this. I am aware that many people use complex formulae successfully and are pleased with the results. However, I have spent too much of my life picking up the pieces left by over clever people who have moved on. How long does it take to get a formula like this correct? How long does it take to amend when six or twelve months later a slightly different result is required? Too often, in my experience, the person who has to amend it six or twelve months later is not the original author. With VBA the author can leave comments saying what the code is doing but with formulae the author cannot. Over clever authors don't always leave the comments they should but at least VBA code is usually easier to decode than a complex formula.

Related

How to create formula to sum with dynamic ranges? I just can't make the right syntax

So what I would like to do is add a sum formula to a cell, so it can be edited later(normally, not through vba). It sums up some cells, but the amount of cells is not always the same. Sometimes it's 4 cells, sometimes it's 10. So I'm trying to have:
lastrow = Sheets(7).Cells(Sheets(7).Rows.Count, "A").End(xlUp).Row
then using it:
Sheets(7).Range("B" & lastrow + 1).Formula = "=SUM(b2:b&"lastrow")"
My problem is the syntax actually, I can't seem to make it right. How to add lastrow to this formula?
Hope it's understandable, English is not my native language.
You're missing to concatenate.
Sheets(7).Range("B" & lastrow + 1).Formula = "=SUM(b2:b" & lastrow & ")"
Instead of using the .End(xlUp) method, I'd suggest that using Named Ranges and in particular the built-in structured referencing of Excel Tables/ListObjects would make for more robust and simpler code.
Using Named Ranges avoids hard-coding references in your code. If you hard code cell address into your code, those references will be pointing at the wrong place if you (or a user) later adds new rows/columns above/to the left of those hard-coded references. Using Names avoids this, and makes for more robust code.
I almost always use Excel Tables aka ListObjects to hold any data that VBA interacts with for the same reason...ListObjects are dynamic named ranges that Excel automatically expands/contracts to suit the data, and unlike .End(xlUp) they won't throw your code off if the column contains blanks.
So I'd do it like this:
Range("SomeNamedRange") = "=SUM(" & Range("SomeTable[SomeColumn]").Address & ")"
...or more likely, I'd use the [] shorthand notation:
[SomeNamedRange] = "=SUM(" & [SomeTable[SomeColumn]].Address & ")"

VBA Error method of '_default' if object 'range' failed when inserting formula into cell

I am trying to use VBA to put a formula into a cell. This is the formula.
Range(Cells(2, 15)).FormulaR1C1 = "=SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7clrow,Classes!R[]C[-13]))"
The error is inside the quotes on the right. I've tried taking off range, using A1 notation, and everything else I can think of. I have several extremely similar lines before and after that all work fine, so I'm not quite sure what I'm missing. Thanks for any help.
EDIT: Here is the line several people have suggested:
Cells(2, 15).FormulaR1C1 = " =SUMPRODUCT(COUNTIF('All Failing Classes'!$G$2:$G$" & lrow & ",Classes!B2))"
or
Cells(2, 15).FormulaR1C1 = "=SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7C" & lrow & ",Classes!R[]C[-13]))"
Which is the same as far as I can tell.
While Mat was helping me I accidentally added a space in front of the = to create the line:
Cells(2, 15).FormulaR1C1 = " =SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7C" & lrow & ",Classes!R[]C[-13]))"
and that line goes in just fine? When I move to XL and take the space out, the formula works as intended. Does this help narrow down what I'm doing wrong?
Edit 2: Here is what I was doing wrong - I had redefined a variable before that line that referenced another sheet. When I did that the program apparently kept referencing that sheet, and gave me my issues. When I changed and added the sheet before the cells (sheets(blah).cellc(blah) then it worked. Thanks to everyone (especially Mat) who helped out.
Does =SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7clrow,Classes!R[]C[-13])) work as a formula?
If it doesn't work (and no, it doesn't), then Excel itself can't assign the broken formula to the cell you're typing it in - for the exact same reason, VBA won't be able to do it either.
So you mean lrow to be evaluated by VBA before it's inserted into the formula, right? For this to happen, VBA needs to be able to see that lrow variable. Being in a string literal, it doesn't, and it can't guess at your intentions either: as far as VBA is concerned, this is what's happening:
SomeObject.SomeProperty = "some string literal"
That's all.
You need to concatenate the variable's value into the string literal, the way you concatenate strings in VBA:
SomeObject.SomeProperty = "some " & someVariable & " string literal"
In other words make the right-hand side of the assignment a string-valued expression, not just a literal:
"=SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7c" & lrow & ",Classes!R[]C[-13]))"
Try this
Cells(2, 15).FormulaR1C1 = "=SUMPRODUCT(COUNTIF('All Failing Classes'R7C2:r7c" & lrow & ",Classes!R[]C[-13]))"
I'm guessing that lrow is a variable...

Declaring last row references in Excel formulas through VBA

In an attempt to take one of my rudimentary programs, and develop it more I am in need of help referencing last row references in Excel formulas as well as dragging formulas down consistently to the last row of a document.
To elaborate I am attempting to reference the last row in an ever expanding mapping table in my vlookup formula. The reason I need this is because as this mapping table expands when I am no longer coding this program, I need my vlookup formula which will be generated every time I run the program to adapt to the ever changing size.
Also my more pressing issue is in regard to taking that vlookup formula and being able to drag it down to the last row of a worksheet. The worksheet will be static and the last row will range anywhere from 70,000 rows to 90,000 rows. I am trying to avoid loops in this scenario as this will already be a very demanding formula and I would hate to loop through each row.
Currently my rudimentary code looks like this (this was built as a proof of concept, I understand the current method isn't the most ingenious but it served its initial purpose).
Ath.Cells(1, x) = "Business"
Ath.Cells(2, x).Select
ActiveCell.FormulaR1C1 = "=IFERROR(VLOOKUP(RC[1],Mapping!R2C4:R264C5,2,0),VLOOKUP(LEFT(RC[1],2),Mapping!R1C3:R264C5,3,0))"
Ath.Range("d2").Copy
Range("d2:d90000").Select
ActiveSheet.Paste
Calculate
I namely want to change this
Ath.Range("d2").Copy
Range("d2:d90000").Select
ActiveSheet.Paste
and
ActiveCell.FormulaR1C1 = "=IFERROR(VLOOKUP(RC[1],Mapping!R2C4:R264C5,2,0),VLOOKUP(LEFT(RC[1],2),Mapping!R1C3:R264C5,3,0))"
Perhaps these?
Ath.Range("d2").Copy
Range("d2:d" & Range("D" & Rows.Count).End(xlUp).Row).Paste
ActiveCell.FormulaR1C1 = "=IFERROR(VLOOKUP(RC[1],Mapping!R2C4:R" & Sheets("Mapping").Range(ActiveCell.Offset(Rows.Count, -1)).End(xlUp).Row & "C5,2,0),VLOOKUP(LEFT(RC[1],2),Mapping!R1C3:R" & Sheets("Mapping").Range(ActiveCell.Offset(Rows.Count, -1)).End(xlUp).Row & "C5,3,0))"
Combining Dans answer with work of my own I have developed a solution.
Sub Formulas()
Dim Map As Worksheet
Dim Ath As Worksheet
Dim last As Long
Set Ath = Sheets("Athena IBT TEST")
Set Map = Sheets("Mapping")
last = Map.Cells(Rows.Count, "D").End(xlUp).Row
ActiveCell.FormulaR1C1 = "=IFERROR(VLOOKUP(RC[1],Mapping!R1C1:R" & last & "C4,4,0),IFERROR(VLOOKUP(LEFT(RC[1],3),Mapping!R1C2:R" & last & "C4,3,0),VLOOKUP(LEFT(RC[1],3),Mapping!R1C3:R" & last & "C4,2,0)))"
End Sub

Why is my conditional format offset when added by VBA?

I was trying to add conditional formats like this:
If expression =($G5<>"") then make set interior green, use this for $A$5:$H$25.
Tried this, worked fine, as expected, then tried to adapt this as VBA-Code with following code, which is working, but not as expected:
With ActiveSheet.UsedRange.Offset(1)
.FormatConditions.Delete
'set used row range to green interior color, if "Erledigt Datum" is not empty
With .FormatConditions.Add(Type:=xlExpression, _
Formula1:="=($" & cstrDefaultProgressColumn & _
.row & "<>"""")")
.Interior.ColorIndex = 4
End With
End With
The Problem is, .row is providing the right row while in debug, however my added conditional-formula seems to be one or more rows off - depending on my solution for setting the row. So I am ending up with a conditional formatting, which has an offset to the row, which should have been formatted.
In the dialog it is then =($G6<>"") or G3 or G100310 or something like this. But not my desired G5.
Setting the row has to be dynamicall, because this is used to setup conditional formats on different worksheets, which can have their data starting at different rows.
I was suspecting my With arrangement, but it did not fix this problem.
edit: To be more specific, this is NOT a UsedRange problem, having the same trouble with this:
Dim rngData As Range
Set rngData = ActiveSheet.Range("A:H") 'ActiveSheet.UsedRange.Offset(1)
rngData.FormatConditions.Delete
With rngData.FormatConditions.Add(Type:=xlExpression, _
Formula1:="=($" & cstrDefaultProgressColumn & _
1 & "<>"""")")
.Interior.ColorIndex = 4
End With
My Data looks like this:
1 -> empty cells
2 -> empty cells
3 -> empty cells
4 -> TitleCols -> A;B;C;...;H
5 -> Data to TitleCols
. .
. .
. .
25
When I execute this edited code on Excel 2007 and lookup the formula in the conditional dialog it is =($G1048571<>"") - it should be =($G1<>""), then everything works fine.
Whats even more strange - this is an edited version of a fine working code, which used to add conditional formats for each row. But then I realized, that it's possible to write an expression, which formats a whole row or parts of it - thought this would be adapted in a minute, and now this ^^
edit: Additional task informations
I use conditional formatting here, because this functions shall setup a table to react on user input. So, if properly setup and a user edits some cell in my conditionalized column of this tabel, the corresponding row will turn green for the used range of rows.
Now, because there might be rows before the main header-row and there might be a various number of data-columns, and also the targeted column may change, I do of course use some specific informations.
To keep them minimal, I do use NamedRanges to determine the correct offset and to determine the correct DefaultProgessColumn.
GetTitleRow is used to determine the header-row by NamedRange or header-contents.
With ActiveSheet.UsedRange.Offset(GetTitleRow(ActiveSheet.UsedRange) - _
ActiveSheet.UsedRange.Rows(1).row + 1)
Corrected my Formula1, because I found the construct before not well formed.
Formula1:="=(" & Cells(.row, _
Range(strMatchCol1).Column).Address(RowAbsolute:=False) & _
"<>"""")"
strMatchCol1 - is the name of a range.
Got it, lol. Set the ActiveCell before doing the grunt work...
ActiveSheet.Range("A1").Activate
Excel is pulling its automagic range adjusting which is throwing off the formula when the FromatCondition is added.
The reason that Conditional Formatting and Data Validation exhibit this strange behavior is because the formulas they use are outside the normal calculation chain. They have to be so that you can refer to the active cell in the formula. If you're in G1, you can't type =G1="" because you'll create a circular reference. But in CF or DV, you can type that formula. Those formulas are disassociated with the current cell unlike real formulas.
When you enter a CF formula, it's always relative to the active cell. If, in CF, you make a formula
=ISBLANK($G2)
and you're in A5, Excel converts it to
=ISBLANK(R[-3]C7)
and when that gets put into the CF, it ends up being relative to the cell it's applied to. So in row 2, the formula comes out to
=ISBLANK($G655536)
(for Excel 2003). It offsets -3 rows and that wraps to the bottom of the spreadsheet.
You can use Application.ConvertFormula to make the formula relative to some other cell. If I'm in row 5 and the start of my range is in row 2, I make the formula relative to row 8. That way the R[-3] will put the formula in A5 as $G5 (three rows up from A8).
Sub test()
Dim cstrDefaultProgressColumn As String
Dim sFormula As String
cstrDefaultProgressColumn = "$G"
With ActiveSheet.UsedRange.Offset(1)
.FormatConditions.Delete
'set used row range to green interior color, if "Erledigt Datum" is not empty
'Build formula
sFormula = "=ISBLANK(" & cstrDefaultProgressColumn & .Row & ")"
'convert to r1c1
sFormula = Application.ConvertFormula(sFormula, xlA1, xlR1C1)
'convert to a1 and make relative
sFormula = Application.ConvertFormula(sFormula, xlR1C1, xlA1, , ActiveCell.Offset(ActiveCell.Row - .Cells(1).Row))
With .FormatConditions.Add(Type:=xlExpression, _
Formula1:=sFormula)
.Interior.ColorIndex = 4
End With
End With
End Sub
I only offset .Cells(1) row-wise because the column is absolute in this example. If both row and column are relative in your CF formula, you need more offsetting. Also, this only works if the active cell is below the first cell in your range. To make it more general purpose, you would have to determine where the activecell is relative to the range and offset appropriately. If the offset put you above row 1, you would need to code it so that it referred to a cell nearer the bottom of the total number of rows for your version of Excel.
If you thought selecting was a bit of a kludge, I'm sure you'll agree that this is worse. Even though I abhor unnecessary Selecting and Activating, Conditional Formatting and Data Validation are two places where it's a necessary evil.
A brief example:
Sub Format_Range()
Dim oRange As Range
Dim iRange_Rows As Integer
Dim iCnt As Integer
'First, create a named range manually in Excel (eg. "FORMAT_RANGE")
'In your case that would be range "$A$5:$H$25".
'You only need to do this once,
'through VBA you can afterwards dynamically adapt size + location at any time.
'If you don't feel comfortable with that, you can create headers
'and look for the headers dynamically in the sheet to retrieve
'their position dynamically too.
'Setting this range makes it independent
'from which sheet in the workbook is active
'No unnecessary .Activate is needed and certainly no hard coded "A1" cell.
'(which makes it more potentially subject to bugs later on)
Set oRange = ThisWorkbook.Names("FORMAT_RANGE").RefersToRange
iRange_Rows = oRange.Rows.Count
For iCnt = 1 To iRange_Rows
If oRange(iCnt, 1) <> oRange(iCnt, 2) Then
oRange(iCnt, 2).Interior.ColorIndex = 4
End If
Next iCnt
End Sub
Regarding my comments given on the other reply:
If you have to do this for many rows, it is definitely faster to load the the entire range into memory (an array) and check the conditions within the array, after which you do the writing on those cells that need to be written (formatted).
I could agree that this technique is not "necessary" in this case - however it is good practise because it is flexible for many (any type of) customizations afterwards and easier to debug (using the immediate / locals / watches window).
I'm not a fan of Offset although I don't state it doesn't work as it should and in some limited scenarios I could say that the chance for problems "could" be small: I experienced that some business users tend to use it constantly (here offset +3, there offset -3, then again -2, etc...); although it is easy to write, I can tell you it is hell to revise. It is also very often subject to bugs when changes are made by end users.
I am very much "for" the use of headers (although I'm also a fan of reducing database capabilities for Excel, because for many it results in avoiding Access), because it will allow you very much flexibility. Even when I used columns 1 and 2; better is it to retrieve the column nr dynamically based on the location of the named range of the header. If then another column is inserted, no bugs will appear.
Last but not least, it may sound exaggerated, but the last time, I used a class module with properties and functions to perform all retrievals of potential data within each sheet dynamically, perform checks on all bugs I could think of and some additional functions to execute specific tasks.
So if you need many types of data from a specific sheet, you can instantiate that class and have all the data at your disposal, accessible through defined functions. I haven't noticed anyone doing it so far, but it gives you few trouble despite a little bit more work (you can use the same principles again over and over).
Now I don't think that this is what you need; but there may come a day that you need to make large tools for end users who don't know how it works but will complain a lot about things because of something they might have done themselves (even when it's not your "fault"); it's good to keep this in mind.

VBA VLOOKUP Convert to Values Gives #N/A

I'm having some trouble with VLOOKUP in my VBA. Here's an example of the code I'm using:
Sub Macro15()
'
' Macro15 Macro
Dim LR As Long
LR = Cells(Rows.Count, "A").End(xlUp).Row
Range("B1:B" & LR).FormulaR1C1 = _
"=VLOOKUP(RC[-1],'https://internal_sharepoint_address
/[Vendor_Information.xlsx]Sheet1'!R3C3:R150C18,4,FALSE)"
Range("C1:C" & LR).FormulaR1C1 = _
"=VLOOKUP(RC[-2],'https://internal_sharepoint_address
/[Vendor_Information.xlsx]Sheet1'!R3C3:R150C18,5,FALSE)"
With Range("B1:C" & LR)
.Value = .Value
End With
End Sub
The problem is that the values in Columns B & C (the VLOOKUP formulas) return a value of #N/A.
However, if I stop the code before converting the formula to values (the "With Range("B1:C" & LR)" line), the VLOOKUP formula returns the correct values.
Also strange - if I clear the contents of Columns B & C and re-run the above code, the values return fine. If I try to add a second cycle to the VBA, however, it does NOT work.
Any wisdom that anyone can provide would be a huge help. I've been stuck on this for a long time, and I'm just at my wit's end.
Thanks all,
David
You'll probably need to add in a step that runs a calculation cycle before you try to replace with the value:
Application.Calculate
Edit from comment: I would imagine that retrieving lookup data from a linked workbook on a Sharepoint site would take awhile. Maybe add some delay loops? Can you make two separate macros (one ending with the formulas, and a second one starting at the Paste Values), and run them separately with a pause in between?