Excel VBA Rows.Count reference for a loop - vba

I need to pull a range of data from a sheet where the top 16 rows will always be the same but the data below will vary. I can find the starting cell with this
Sheets("AA").Next.Select
Range("A17").Select
Selection.End(xlDown).Offset(2, 2).Select
and then I want to count the cells using (starting cell selected above) to end of cells containing data. I have tried this
Range(Selection, Range(Selection.End(xlDown)).Rows.Count
and all kinds of variations on it but cannot seem to make it work. I need to be able refer back to both the start cell and the number of rows to pull data from that cell down to the last cell using a loop (another topic I will probably ask questions on when I get that far)
Can someone help?

Your explanation as to why you are trying to do this makes it appear you are placing more work on yourself than you need to.
Let's create a couple functions.
The first function we will call nextBlankCell, which will automatically grab the last cell before an empty range. In your case, we can even use your selection to determine this - which we will do in our next function.
nextBlankCell Function
Function nextBlankCell(ByVal startRng As Range) As Range
Set nextBlankCell = startRng.End(xlDown)
End Function
Next, let's create a function that will automatically set your entire range for you. In this case, it will be the range from your current selection to the last row containing data that we get from using the above function.
getRngDownwards Function
Function getRngDownwards() As Range
Dim celStart As Range, celEnd As Range, ws As Worksheet
Set celStart = Selection
Set ws = celStart.Parent
Set celEnd = nextBlankCell(celStart)
Set getRngDownwards = ws.Range(celStart, celEnd)
End Function
In the above function, we have two ranges celStart and celEnd. celStart is simply your current selection. I always prefer to immediately set your selection to a static range if you must use Selection (most cases it’s not necessary).
celEnd is the range that will contain the last used cell in your column.
We also determine the worksheet ws by using the selection's parent object. Protip: We avoided ActiveSheet!
Now you can put it to the test:
Sub test()
' This test shows you the address of the range
MsgBox getRngDownwards.Address
' This test visually shows you the range
getRngDownwards.Select
End Sub

Related

How do I get rid of a circular reference in a VBA dynamic range?

I am trying to create a user-defined Excel Function that, in part, counts all non-blank cells above the cell in which the formula is placed (technically from a specific cell that defines the first cell in the range). The trouble I am having is that copying the formula down is causing a circular reference. I don't want other users to encounter this problem. How can I avoid the circular reference?
I have been trying to solve the problem with:
Set CellOne = Range(“A10”)
Set CellTwo = Range(Selection.Address).Offset(-1, 0)
Set MyRange = Application.Range(Cell1:=CellOne.Address, Cell2:=CellTwo.Address)
CountNonBlanks = Application.WorksheetFunction.CountA(MyRange)
This code also causes the circular reference when copying down:
Set CellTwo = Range(ActiveCell.Address).Offset(-1, 0)
The problem appears to be caused by the reference being relative to which cell is selected or active. I just want MyRange to end one cell above where the formula is placed irrespective of which cell is active or selected.
FWIW, the ultimate purpose of the user-defined formula is to return the next letter in the alphabet no matter how many rows below the prior letter the formula is placed. This native function works, but I was hoping for a more elegant appearing solution:
=MID("abcdefghijklmnopqrstuvwxyz",COUNTA(A$10:A10)+1,1)
Thank you.
You shouldn't be using Selection or Activecell in a worksheet's udf since those are constantly changing. Either pass a range reference into the udf or use application.caller to refer to the cell containing the udf as a range object.
I could edit this response to provide more specific help if you posted the whole udf or at least the declaration. Here's an example.
Public Function nextLetter()
'since no reference is passed in, you might want to make this volatile
Application.Volatile
With Application.Caller.Parent
nextLetter = Chr(97 + Application.CountA(.Range(.Cells(10, "A"), _
.Cells(Application.Caller.Row - 1, "A"))))
End With
End Function
Alternative with starting cell passed in.
Public Function nextLetter2(startRng As Range)
'since only a single cell reference is passed in, you might want to make this volatile
Application.Volatile
With Application.Caller.Parent
nextLetter2 = Chr(97 + Application.CountA(.Range(startRng, _
.Cells(Application.Caller.Row-1, startRng.Column))))
End With
End Function
Use like =nextLetter2(A$10)

Excel VBA Dynamic Range / Loop issue

I am developing a financial model for a bank and come across the below issue which I am not able to resolve in Excel VBA, and would appreciate your help.
I have built a simple macro which essentially does two things: (i) it clears contents in a given range, (ii) it populates the same range with a formula. In a very abbreviated way it looks like the following:
Sub AutoCalculateCashFlows()
Range(D208:L208).ClearContents
Range("L208").FormulaR1C1 = "=+R[-34]C-R[-34]C[-1]"
Range("L208").AutoFill Destination:=Range("E208:L208"), Type:=xlFillDefault
End Sub
My problem is that the range that should be auto populated is dependent on how many cells did the user fill in within the range of E10:L10. Users will start populating this range from right to left, but I don't know how far they will go from column L to the left. The formula that my macro auto populates needs at least two data, ie. at least L10 and K10 should be populated and if the latter is the case then the macro only needs to auto populate L208 with formula, in case J10:L10 is filled out then the macro needs to auto populate the range L208:K208 and so on to the point that in case the full D10:L10 range is filled out then E208:L208 should be populated with formula.
I have thought to resolve this issue via two routes: (i) approaching it as a dynamic range problem in which case I need a vba code to determine the previous to the last cell populated by the user in the range D10:L10 and use the column code of that cell in "Destination:=Range("E208:L208")", or (ii) run a loop which will populate range E208:L208 with formula until the cell in the previous column within range D10:L10 is filled in by the user and stop when it is not.
Hope this makes sense and thanks in advance for the help.
When you need a dynamic range in VBA, you should simply build one. This is probably the easiest method:
Sub TestMe()
Dim colRange As Long
Dim rowRange As Long
Dim rngMain As Range
rowRange = 10
With Worksheets(1)
colRange = .Cells(1, .Columns.Count).End(xlToLeft).Column
Set rngMain = .Range(.Cells(rowRange, colRange), .Cells(100, 200))
MsgBox rngMain.Address
End With
End Sub
It is dynamic, based on the last used column in row 1 of the first Worksheet.
Concerning the second used column in Row 1, one of these 3 would probably do it for you, depending on what exactly do you want:
.Cells(1, 1).End(xlToRight).End(xlToRight).Column
.Cells(1, 1).End(xlToRight).Column
.Cells(1, 1).End(xlToRight).Column + 1

Copy a static range on one sheet, and paste in a dynamic range in another sheet based on a single value in a cell

I have three parts to this problem. I have a single cell with a Week number in Sheet1!A1. I have a static range in Sheet1!B1:F1 that needs to be copied. Then I need to paste the value in a dynamic range in Sheet2 offset by the week number for rows. This is part of a larger macro I am writing for a sheet I use regularly, but I seem to have those parts down. I may be either oversimplifying or oversimplifying but this is what I have currently.
Sub CopyPaste()
Sheets(1).Range("B1:F1").Copy
OffsetRange = Sheets(1).Cells(1,1).Value
Sheets(2).Cells(1+OffsetRange,1).Paste
End Sub
When I run this, it either gives me a Runtime Error 9 or Runtime Error 438.
Anyone know whats causing these errors? When I paste the range, does the cells object point towards the first cell of the copied range when I paste in at the location?
Try it as,
Option Explicit
Sub CopyPasteOffset()
Dim OffsetRange As Long
OffsetRange = Worksheets(1).Cells(1, 1).Value
Worksheets(1).Range("B1:F1").Copy _
Destination:=Worksheets(2).Cells(1 + OffsetRange, 1)
End Sub
The .Paste method is a member of Worksheet, not Range or Cells. You may have it confused with .PasteSpecial which is a member of the Range object. In any event, it is unnecessary as a destination can be applied directly to the copy command.

Get the cell reference of the value found by Excel INDEX function

The Problem
Assume that the active cell contains a formula based on the INDEX function:
=INDEX(myrange, x,y)
I would like to build a macro that locates the value found value by INDEX and moves the focus there, that is a macro changing the active cell to:
Range("myrange").Cells(x,y)
Doing the job without macros (slow but it works)
Apart from trivially moving the selection to myrange and manually counting x rows y and columns, one can:
Copy and paste the formula in another cell as follows:
=CELL("address", INDEX(myrange, x,y))
(that shows the address of the cell matched by INDEX).
Copy the result of the formula above.
Hit F5, Ctrl-V, Enter (paste the copied address in the GoTo dialog).
You are now located on the very cell found by the INDEX function.
Now the challenge is to automate these steps (or similar ones) with a macro.
Tentative macros (not working)
Tentative 1
WorksheetFunction.CELL("address", ActiveCell.Formula)
It doesn't work since CELL for some reason is not part of the members of WorksheetFunction.
Tentative 2
This method involves parsing the INDEX-formula.
Sub GoToIndex()
Dim form As String, rng As String, row As String, col As String
form = ActiveCell.Formula
form = Split(form, "(")(1)
rng = Split(form, ",")(0)
row = Split(form, ",")(1)
col = Split(Split(form, ",")(2), ")")(0)
Range(rng).Cells(row, CInt(col)).Select
End Sub
This method actually works, but only for a simple case, where the main INDEX-formula has no nested subformulas.
Note
Obviously in a real case myrange, x and ycan be both simple values, such as =INDEX(A1:D10, 1,1), or values returned from complex expressions. Typically x, y are the results of a MATCH function.
EDIT
It was discovered that some solutions do not work when myrange is located on a sheet different from that hosting =INDEX(myrange ...).
They are common practice in financial reporting, where some sheets have the main statements whose entries are recalled from others via an INDEX+MATCH formula.
Unfortunately it is just when the found value is located on a "far" report out of sight that you need more the jump-to-the-cell function.
The task could be done in one line much simpler than any other method:
Sub GoToIndex()
Application.Evaluate(ActiveCell.Formula).Select
End Sub
Application.Evaluate(ActiveCell.Formula) returns a range object from which the CELL function gets properties when called from sheets.
EDIT
For navigating from another sheet you should first activate the target sheet:
Option Explicit
Sub GoToIndex()
Dim r As Range
Set r = Application.Evaluate(ActiveCell.Formula)
r.Worksheet.Activate
r.Select
End Sub
Add error handling for a general case:
Option Explicit
Sub GoToIndex()
Dim r As Range
On Error Resume Next ' errors off
Set r = Application.Evaluate(ActiveCell.Formula) ' will work only if the result is a range
On Error GoTo 0 ' errors on
If Not (r Is Nothing) Then
r.Worksheet.Activate
r.Select
End If
End Sub
There are several approaches to select the cell that a formula refers to...
Assume the active cell contains: =INDEX(myrange,x,y).
From the Worksheet, you could try any of these:
Copy the formula from the formula bar and paste into the name box (to the left of the formula bar)
Define the formula as a name, say A. Then type A into the Goto box or (name box)
Insert hyperlink > Existing File or Web page > Address: #INDEX(myrange,x,y)
Adapt the formula to make it a hyperlink: =HYPERLINK("#INDEX(myrange,x,y)")
Or from the VBA editor, either of these should do the trick:
Application.Goto Activecell.FormulaR1C1
Range(Activecell.Formula).Select
Additional Note:
If the cell contains a formula that refers to relative references such as =INDEX(A:A,ROW(),1) the last of these would need some tweaking. (Also see: Excel Evaluate formula error). To allow for this you could try:
Range(Evaluate("cell(""address""," & Mid(ActiveCell.Formula, 2) & ")")).Select
This problem doesn't seem to occur with R1C1 references used in Application.Goto or:
ThisWorkbook.FollowHyperlink "#" & mid(ActiveCell.FormulaR1C1,2)
You could use the MATCH() worksheet function or the VBA FIND() method.
EDIT#1
As you correctly pointed out, INDEX will return a value that may appear many times within the range, but INDEX will always return a value from some fixed spot, say
=INDEX(A1:K100,3,7)
will always give the value in cell G3 so the address is "builtin" to the formula
If, however, we have something like:
=INDEX(A1:K100,Z100,Z101)
Then we would require a macro to parse the formula and evaluate the arguments.
Both #lori_m and #V.B. gave brilliant solutions in their own way almost in parallel.
Very difficult for me to choose the closing answer, but V.B. even created Dropbox test file, so...
Here I just steal the best from parts from them.
'Move to cell found by Index()
Sub GoToIndex()
On Error GoTo ErrorHandler
Application.Goto ActiveCell.FormulaR1C1 ' will work only if the result is a range
Exit Sub
ErrorHandler:
MsgBox ("Active cell does not evaluate to a range")
End Sub
I associated this "jump" macro with CTRL-j and it works like a charm.
If you use balance sheet like worksheets (where INDEX-formulas, selecting entries from other sheets, are very common), I really suggest you to try it.

Copy the contents and formatting of a cell if a column within the row = today()

I'm currently building a small project planner in Excel that uses the current date to plot coloured blocks under a date column to depict which stage of the project we are currently at for a particular customer (see image below).
Behind each of the coloured blocks is a drop-down menu populated by a list on another sheet. My aim is to search for the current date in cell A1 ( populated using today() ) within all columns that follow the freezed panes (depicted by the black right hand border). When the current date is found, the value of in each of the coloured blocks should be copied into the corresponding cells so that as the project progresses, a line of coloured blocks are entered for each day (with the relevant text from the drop-down depicting the current stage of that block).
Currently I am using the following formula copied into all cells that follow the freeze:
=IF(F$1 = $A$1,$C2,"")
However, when the current date is changed this merely moves the copied blocks across to the relevant column without maintaining the old values from previous days.
I've also attempted this with a VLOOKUP so that I can enter it into a macro and run if from a button but the layout does not allow for a successful VLOOKUP.
The simplest solution I believe would be to have a button that allows the user to save the current state of the column with a header that matches the current date however it has been some time since I have coded in VBA and do not remember how to do this.
Any ideas? Thanks in advance.
Not sure if this is exactly what you're looking for, but here goes...
Sub ColorCode()
Dim ws As Worksheet
Dim rng As Range
Dim cel As Range
Set ws = ThisWorkbook.Sheets("SheetNameHere")
Set rng = ws.Range("F1:I1")***
For Each cel In rng
If cel.Value = ws.Range("A1").Value Then
ws.Range("C2:C8").Copy
ws.Range(Cells(2, cel.Column), Cells(8, cel.Column)).PasteSpecial Paste:=xlPasteValues
ws.Range(Cells(2, cel.Column), Cells(8, cel.Column)).PasteSpecial Paste:=xlPasteFormats
End If
Next
End Sub
If you add that to a new module, you can assign it to a command button. I haven't had a chance to test it, but it cycles through the dates in the first row to see if they match the date in A1. If they do, it copies over the values and formats from C2:C8(change if you need to) into the rows underneath that date. You may need to change some of the ranges to suit your specific worksheet.
So your requirements seem fairly straightforward to me:
you need the tracker to identify the column with today's date
you need to establish a permanent value for each day as it occurs
you need the color of today's values to be added to the cell, and stay that way even after today's date has passed.
The formula you cite in your question, if copied across all cells, will clearly just provide a value on the column for today's date, and unless you use a circular reference to let it self assess and update its value on today's date, it will not retain information when tomorrow comes.
Your idea for a button would work if you want the user to control the time of update, or you could have code that runs either when the workbook opens or when the worksheet itself is activated (placing it in the appropriate object code under either Private Sub Worksheet_Activate() or Private Sub Workbook_Activate().
I think PermaNoob has a right idea of copying the value of the column and pasting the value (rather than the formlula) into that column, but what is missing is appropriate identification of the column containing today's date and the coloring of those cells (if you don't have some method of coloring them that you did not mention). Something like this might work either attached to a button as you suggest, or to the _Activate event as I suggest. This is untested but should give you an idea of how to approach it:
Sub UpdatePlanner()
'~~>dim variables and set initial values
Dim wb As Workbook
Set wb = Workbooks("NAME or INDEX of YOUR workbook")
Dim ws As Worksheet
Set ws = wb.Worksheets("NAME or INDEX of YOUR sheet")
Dim rngHeader As Range
Set rngHeader = ws.Range("F1", ws.Range("F1").End(xlToRight))
Dim rngDate As Range
Dim rngColumn As Range
Dim rngCell As Range
'~~>loop to find the column with today's date
For Each rngDate In rngHeader
If rngDate.value = ws.Range("A1").value Then
Set rngColumn = ws.Range(rngDate.Address, _
ws.Range(rngDate.Address).Offset(65536, 0).End(xlUp)) 'this assumes
'your column may not have a value in every row
Exit For
End If
Next rngDate
'~~>copy and paste the column values and formats
With rngColumn
.Copy
.PasteSpecial Paste:=xlPasteValues
.PasteSpecial Paste:=xlPasteFormats
End With
'~~>loop to add the color formatting (since I don't see this in your formula)
For Each rngCell In rngColumn
If rngCell.value = ws.Range(Cells(rngCell.Row, 3)).value Then
rngCell.Interior.Color = _
ws.Range(Cells(rngCell.Row, 3)).Interior.Color
End If
Next rngCell
End Sub