I have a macro I'm trying to create to map a bunch of old values to new values. At one point I need to do a match on 2 columns and get the value from a third column. In normal excel I would run the formula below, but as it is a Formula Array (have to CTRL+SHIFT+ENTER to run) I'm not quite sure how to get this to work in VBA
=INDEX($D:$D,MATCH(1,(E2=$A:$A)*(F2=$B:$B),0))
Any help appreciated
Have you tried?
Cell.FormulaArray()
It should work with normal notation, although it is intended for R1C1
There are essentially three ways to evaluate an array formula to a result within VBA.
The first is 'square bracket' evaluation. This processes a formula just as if it is on the worksheet.
Dim result As Variant
result = [INDEX('Sheet1'!D:D,MATCH(1,(E2='Sheet1'!A:A)*(F2='Sheet1'!B:B),0))]
Debug.Print result
Care must be given to explicitly show worksheet parentage. In the above, the full columns have been given a parent worksheet but the individual cells have not. The individual cells will default to the ActiveSheet property which may or may not even be in the same workbook. Define all cell references explicitly!
The second method is with Application.Evaluate method. This allows more freedom with the formulas using string construction.
Dim result As Variant, wsn As String
wsn = Worksheets("Sheet1").Name
result = Application.Evaluate("INDEX('" & wsn & "'!D:D, " & _
"MATCH(1, (E2='" & wsn & "'!A:A)*(F2='" & wsn & "'!B:B),0))")
Debug.Print result
Again, only the full column references have been given parent worksheet references. This should be fixed before run-time use. This method is better for array formulas because you can use string construction to cut the full column references down to the rows in the Worksheet.UsedRange property.
As mentioned, the third method involves writing the formula to a worksheet cell using the Range.FormulaArray property and retrieving the answer from the cell.
Related
I am trying to figure out how to work on a specific row among a big range. However it appears that a range created with the rows property does not behave the same as a simple range. Check the following code, the first time the variable SpecificRow is defined, it is not possible to select a specific cell. However with a weird workaround that redefines the range, it works fine. Do you have an idea why and how could I define the range with a more elegant way?
'The following shows the weird behavior of Rows property
Dim SourceRng As Range
Dim SpecificRow As Range
Dim i As Long
i = 3
Set SourceRng = Range("A1:D20")
Set SpecificRow = SourceRng.Rows(i)
'This will show the address of the selected row ("A3:D3")
MsgBox SpecificRow.Address
'Unexplicable behavior of the range when trying to select a specific cell
'where it will instead consider the whole row (within the limits of SourceRng)
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
'This would send an error
'MsgBox SpecificRow(1, 1).Address
'Workaround
Set SpecificRow = Intersect(SpecificRow, SpecificRow)
'The following will select the same address than before
MsgBox SpecificRow.Address
'However, now it has a correct behavior when selecting a specific cell
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
You should expect weird behavior if you're passing indexed properties the incorrect parameters. As demonstrated by your code, the Range returned by SourceRng.Rows(i) is actually correct. It just isn't doing what you think it's doing. The Rows property of a Range just returns a pointer to the exact same Range object that it was called on. You can see that in its typelib definition:
HRESULT _stdcall Rows([out, retval] Range** RHS);
Note that it doesn't take any parameters. The returned Range object is what you're providing the indexing for, and you're indexing it based on it's default property of Item (technically it's _Default, but the 2 are interchangeable). The first parameter (which is the only one you're passing with Rows(i), is RowIndex. So Rows(i) is exactly the same thing as Rows.Item(RowIndex:=i). You can actually see this in the IntelliSense tooltip that pops up when you provide a Row index:
Excel handles the indexing differently on this call though, because providing any value parameter for the second parameter is a Run-time error '1004'. Note that a similar property call is going on when you call SpecificRow(1).Address. Again, the default property of Range is Range.Item(), so you're specifying a row again - not a column. SpecificRow(1).Address is exactly the same thing as SpecificRow.Item(RowIndex:=1).Address.
The oddity in Excel appears to be that the Range returned by Range.Rows "forgets" the fact that it was called within the context of a Rows call and doesn't suppress the column indexer anymore. Remember from the typelib definition above that the object returned is just a pointer back to the original Range object. That means SpecificRow(2) "leaks" out of the narrowed context.
All things considered, I'd say the Excel Rows implementation is somewhat of a hack. Application.Intersect(SpecificRow, SpecificRow) is apparently giving you back a new "hard" Range object, but the last 2 lines of code are not what you should consider "correct" behavior. Again, when you provide only the first parameter to Range.Items, it is declared as the RowIndex:
What appears to happen is that Excel determines that there is only one row in the Range at this point and just assumes that the single parameter passed is a ColumnIndex.
As pointed out by #CallumDA, you can avoid all of this squirrelly behavior by not relying on default properties at all and explicitly providing all of the indexes that you need, i.e.:
Debug.Print SpecificRow.Item(1, 1).Address
'...or...
Debug.Print SpecificRow.Cells(1, 1).Address
This is how I would work with rows and specific cells within those rows. The only real difference is the use of .Cells():
Sub WorkingWithRows()
Dim rng As Range, rngRow As Range
Set rng = Sheet1.Range("A1:C3")
For Each rngRow In rng.Rows
Debug.Print rngRow.Cells(1, 1).Address
Debug.Print rngRow.Cells(1, 2).Address
Debug.Print rngRow.Cells(1, 3).Address
Next rngRow
End Sub
which returns:
$A$1
$B$1
$C$1
$A$2
$B$2
$C$2
$A$3
$B$3
$C$3
As you would expect
I cannot find any proper documentation on this, but this observed behaviour actually appears to be very logical.
The Range class in Excel has two important properties:
A single instance of Range is enough to represent any possible range on a sheet
It is iterable (can be used in a For Each loop)
I believe that in order to achieve logically looking iterability and yet avoid creating unnecessary entities (i.e. separate classes like CellsCollection, RowsCollection and ColumnsCollection), the Excel developers came up with a design where each instance of Range holds a private property that tells it in which units it is going to count itself (so that one range could be "a collection of rows" and another range could be "a collection of cells").
This property is set to (say) "rows" when you create a range via the Rows property, to (say) "columns" when you create a range via the Columns property, and to (say) "cells" when you create a range in any other way.
This allows you to do this and not become unnecessarily surprised:
For Each r In SomeRange.Rows
' will iterate through rows
Next
For Each c In SomeRange.Columns
' will iterate through columns
Next
Both Rows and Columns here return the same type, Range, that refers to the exactly same sheet area, and yet the For Each loop iterates via rows in the first case and via columns in the second, as if Rows and Columns returned two different types (RowsCollection and ColumnsCollection).
It makes sense that it was designed this way, because the important property of a For Each loop is that it cannot provide multiple parameters to a Range object in order to fetch the next item (cell, row, or column). In fact, For Each cannot provide any parameters at all, it can only ask "Next one please."
To support that, the Range class had to be able to give the next "something" without parameters, even though a range is two-dimensional and needs two coordinates to fetch the "something." Which is why each instance of Range has to remember in what units it will be counting itself.
A side effect of that design is that it is perfectly fine to look up "somethings" in a Range providing only one coordinate. This is exactly what the For Each mechanism would do, we are just directly jumping to the ith item.
When iterating over (or indexing into) a range returned by Rows, we're going to get the ith row, from top to bottom; for a range returned by Columns we're getting the ith column, from left to right; and for a range returned by Cells or by any other method we're going to get the ith cell, counting from top left corner to the right and then to the bottom.
Another side effect of this design is that can "step out" of a range in a meaningful way. That is, if you have a range of three cells, and you ask for the 4th cell, you still get it, and it will be the cell dictated by the shape of the range and the units it's counting itself in:
Dim r As Range
Set r = Range("A1:C3") ' Contains 9 cells
Debug.Print r.Cells(12).Address ' $C$4 - goes outside of the range but maintains its shape
So your workaround of Set SpecificRow = Intersect(SpecificRow, SpecificRow) resets the internal counting mode of that specific Range instance from (say) "rows" to (say) "cells".
You could have achieved the same with
Set SpecificRow = SpecificRow.Cells
MsgBox SpecificRow(1).Address
But it's better to keep the Cells close to the point of usage rather than the point of range creation:
MsgBox SpecificRow.Cells(1).Address
I have an excel file. There is a changable row quantity for column A for every time and that's why I need to make dynamic formula. For example;
I want to write "=LEN(B1)" formula on B1. And when make double click on right down corner of the cell, it's going to the end of until when column A values ends. That's why, before all this process, I wrote "=COUNT(A:A)" on cell C1.
In my VBA, I want to write;
Dim TEMP As String
TEMP = Range("C1").Value
Range("B1").Select
ActiveCell.FormulaR1C1="=+LEN(RC[-1])"
Selection.AutoFill Destination:=Range("B1:TEMP")
But there is something wrong in this process. As a result, I just want to use a cell value in my VBA as a range or string or number.
Thanks for your support.
Two notes:
It's best practice to always qualify your objects (workbooks, worksheets, ranges, etc.) beforehand
When you use R1C1 notation, you can just write the formula directly to the range (with no need for AutoFill or FillDown)
Try this:
Dim ws as Worksheet
Set ws = ThisWorkbook.Sheets("Sheet1") 'edit for your sheet name
Dim lRow as Long
lRow = ws.Range("A" & ws.Rows.count).End(xlup).Row
ws.Range("B1:B" & lRow).FormulaR1C1 = "=Len(RC[-1])"
And just as a side note worth mentioning the way you wrote Range("B1:TEMP") is not proper syntax. Correct syntax would be Range("B1:B" & TEMP), which, of course, would only work if TEMP was indeed a numerical value :)
I'm new to VBA. I'm attempting to create over 500 xlClusteredColumn charts using two columns of information and I'd like to expedite the work. The first column contains names I'd like to use for named ranges (i.e.: Line1, Line2, etc.) and the second column contains the indirect references of the data ranges (i.e., Sheet1!C4:D28, Sheet1!C28:D90). I noticed that if I use a named range for the "Chart Data Series" field, the data shows up nicely (but I have to first create that named range being sure to include the INDIRECT formula in the reference, (e.g.: Named Range Line1 is equal to =INDIRECT(Sheet1!C4:C28)). The ranges will be static.
In reviewing prior questions I couldn't seem to find a solution that would select the first cell in this set and name it, then uses the second cell to define that range. I think I might need the ActiveWorkbook.Names.Add Name:= formula and combine it with a loop (but I couldn't get it to use a selection or cell to define the Add Name aspect, only a hard coded name).
If the solution requires it, I can go back and extract the individual ranges (i.e.: C4:D28) from the cell and have the chart reference only that if it makes the code simpler. I know my first outlined attempt isn't the only solution and there's probably one much more elegant. I figured using named ranges would speed up the chart work, but perhaps there's a way to cut that step out?
Populating a new sheet with all the charts for each of these ranges would be icing on the cake, but I'll be happy enough receiving help to get the data set up to chart.
Example information:
NameRange1 (let's say in cells A1:A4)
WKD_1_NB
WKD_2_EB
WKD_3_EB
SerRange1 (in cells B1:B4)
WKDpivot!C4:D43
WKDpivot!C84:D140
WKDpivot!C197:D233
(Chart data range requires the reference of named range "WKD_1_NB" to be '=INDIRECT(WKDpivot!C4:D43)' in order for the chart to work.
OK so why don't you try the two-step process. I am going to do this without Indirect because I don't see that it is necessary.
Sub CreateNames()
Dim rng As Range
Dim r As Range
Dim myName As String
Dim addr As String
Set rng = Range("A1:A2") '## Modify as needed
For Each r In rng.Cells
myName = r.Value
addr = "=" & r.Offset(0, 1).Value
ThisWorkbook.Names.Add myName, addr
Next
End Sub
This creates your names (screenshot). Note there are some rules about naming conventions and allowable names, etc., the code above does not take any of these in to account.
From there it should be fairly simple to create a loop that adds your charts one by one, and assigns each named range to each chart.
So I have a cell with the formula =A+B, A and B are external values for which the external spreadsheet is no longer open. I was wondering if excel remembers the formula elements and if it was possible to get those element values into other cells. i.e.
Row Formula Value
1 =A+B 45
2 =ELEMENT(A1, 1) 10
3 =ELEMENT(A1, 2) 35
I can't imagine it being that simple though? I could seperate out the formula in vba using the + as a pivot, but this is not ideal as it would require the external spreadsheet to be reopened. If the external spreadsheet has to be reopened then I needn't bother trying to seperate the formula in the first place. I hope this makes sense and has an answer.
Your formula is already accessing those values in order to sum them in the open workbook. You do NOT need to re-open the workbook in order to evaluate the reference values, nor to parse the formula in the active workbook.
Assuming your formula references a closed workbook, with a simple sum function like:
='C:\Users\david_zemens\Desktop\[test.xlsx]Sheet1'!$A$1 + ='C:\Users\david_zemens\Desktop\[test.xlsx]Sheet1'!$B$1
You can parse the formula either in VBA or using string functions on the worksheet. As you note, for this example, parsing by the + operator will suffice. Since I think you understand how to do this, my example does not demonstrate how to parse the formula.
As long as you know or can obtain the references from the formula, you should be able to access those cells' values via formula, or programmatically in VBA using ExecuteExcel4Macro.
Sub GetValsFromClosedWorkbook()
Dim fileName As String
Dim filePath As String
Dim sheetName As String
Dim cellref As String
Dim myFormula As String
'This example might be one of the two arguments in your "A+B" formula:
' 'C:\Users\david_zemens\Desktop\[test.xlsx]Sheet1'!A1
'## Assume you can properly parse the formula to arrive at these:
fileName = "test.xlsx"
filePath = "C:\Users\david_zemens\Desktop\"
sheetName = "Sheet1"
cellref = "A1"
'Concatenate in to R1C1 notation
myFormula = "='" & filePath & "[" & fileName & "]" & sheetName & "'!" & _
Range(cellref).Address(, , xlR1C1)
'## First, demonstrate that we can evaluate external references:
With Range("B1")
.Value = myFormula
MsgBox .Value
.Clear
End With
'## Evaluate the formula using ExecuteExcel4Macro:
MsgBox ExecuteExcel4Macro(Replace(myFormula, "=", vbNullString))
End Sub
Additional info
http://spreadsheetpage.com/index.php/tip/a_vba_function_to_get_a_value_from_a_closed_file/
Update
Based on OP's question of how this works (comments, below), I am not certain of how it works behind the scenes but here is my guess/explanation:
Firstly, keep in mind that a cell may have various properties like .Text, .Value, .Value2, .Formula etc. which in one way or another represent its contents.
The values aren't in the active file until the formula is evaluated, at which point Excel queries the external file and returns the current Value at that reference. Now the active file contains that Value as well as a Formula reference the external file.
As you continue working the active file preserves the Value and the reference, but may only update the value when prompted. For example, when you re-open the linked workbook, you will be prompted whether to "update external links".
if you say no, it will use the previously stored values (still preserves the Formula, it just doesn't evaluate it.
if you say yes, it will re-evaluate the formula (in case the value in the external file has changed) and return the new .Value
So, that's what I suspect is happening. If you're asking "How does Excel access the data inside an un-opened workbook" I don't really know how I just know that it does.
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.