Related
I have a very large embedded IF formula that appears to occasionally break for no reason. Opening and closing the page a few times eventually gets it working again. I am wondering if there is a VBA alternative for it. Here is the IF formula I am running.
=IF(ISNUMBER(SEARCH("76210",E125)),"_012_00762_10",IF(ISNUMBER(SEARCH("76220",E125)),"_012_00762_20",IF(ISNUMBER(SEARCH("76900",E125)),"_012_00769_00",IF(ISNUMBER(SEARCH("76901",E125)),"_012_00769_01",IF(ISNUMBER(SEARCH("85702",E125)),"_012_00857_02",IF(ISNUMBER(SEARCH("85710",E125)),"_012_00857_10",IF(ISNUMBER(SEARCH("100800",E125)),"_012_01008_00",IF(ISNUMBER(SEARCH("100900",E125)),"_012_01009_00",IF(ISNUMBER(SEARCH("123100",E125)),"_012_01231_00",IF(ISNUMBER(SEARCH("124600",E125)),"_012_01246_00",IF(ISNUMBER(SEARCH("124601",E125)),"_012_01246_01",IF(ISNUMBER(SEARCH("124640",E125)),"_012_01246_40",IF(ISNUMBER(SEARCH("124641",E125)),"_012_01246_41",IF(ISNUMBER(SEARCH("142301",E125)),"_012_01423_01",IF(ISNUMBER(SEARCH("158801",E125)),"_012_01588_01",IF(ISNUMBER(SEARCH("158900",E125)),"_012_01589_00",IF(ISNUMBER(SEARCH("159203",E125)),"_012_01592_03",IF(ISNUMBER(SEARCH("159303",E125)),"_012_01593_03",IF(ISNUMBER(SEARCH("159401",E125)),"_012_01594_01",IF(ISNUMBER(SEARCH("159410",E125)),"_012_01594_10",IF(ISNUMBER(SEARCH("159420",E125)),"_012_01594_20",IF(ISNUMBER(SEARCH("159501",E125)),"_012_01595_01",IF(ISNUMBER(SEARCH("169000",E125)),"_012_01690_00",IF(ISNUMBER(SEARCH("186900",E125)),"_012_01869_00",IF(ISNUMBER(SEARCH("213200",E125)),"_012_02132_00",IF(ISNUMBER(SEARCH("213300",E125)),"_012_02133_00",IF(ISNUMBER(SEARCH("215400",E125)),"_012_02154_00",IF(ISNUMBER(SEARCH("220100",E125)),"_012_02201_00",IF(ISNUMBER(SEARCH("223800",E125)),"_012_02238_00",IF(ISNUMBER(SEARCH("225600",E125)),"_012_02256_00",IF(ISNUMBER(SEARCH("230700",E125)),"_012_02307_00",IF(ISNUMBER(SEARCH("230701",E125)),"_012_02307_01",IF(ISNUMBER(SEARCH("231800",E125)),"_012_02318_00",IF(ISNUMBER(SEARCH("235000",E125)),"_012_02350_00",IF(ISNUMBER(SEARCH("235020",E125)),"_012_02350_20",IF(ISNUMBER(SEARCH("242000",E125)),"_012_02420_00",IF(ISNUMBER(SEARCH("246400",E125)),"_012_02464_00",IF(ISNUMBER(SEARCH("292900",E125)),"_012_02929_00",""))))))))))))))))))))))))))))))))))))))
Basically it is built so a serial number is scanned and it populates a cell for the users who use this sheet with its results from the search. I am already running one macro in this sheet as well. Here is that...
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range
Set rng = Intersect(Range("A2:A500, J2:J500"), Target) ' define range of interest
If Not rng Is Nothing Then ' check it's not "nothing"
If WorksheetFunction.CountA(rng) = rng.Count Then 'check for all of its cells being not empty
On Error GoTo safe_exit 'add error control
Application.EnableEvents = False 'don't do anything until you know something has to be done
rng.Offset(, 1).Value = Date 'write Date next to all relevant changed cells
End If
End If
safe_exit:
Application.EnableEvents = True
End Sub
Maybe there is a better way to build this search using a formula that isn't using embedded IF statements, but i couldn't think of another way to do it. Thanks in advance.
This may be what you're looking for:
=IF(ISNA(MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)),"",INDEX($B$5:$B$42,MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)))
entered as an array formula (CTRL-SHIFT-ENTER).
Here $A$5:$A$42 contains 76210, 76220, ... , 292900 (entered as text, not numbers); and $B$5:$B$42 contains _012_00762_10, _012_00762_20, ... , _012_02929_00.
Hope that helps.
Any time you have to go more than 2 deep on an IF you may want to rethink the usage.
What you can do is build a table from your values. Then reference that table as part of your lookup. Assuming your list of value is in range D8:E45 you could use the formula =VLOOKUP(E125,$D$8:$E$45,2).
The beginning of your table would look like what's seen below. The input result cell is referencing your input value and pulling the match of the second column.
To get your table you can take your source formula and replace (Find and Replace - Ctrl+H) some characters with unique delimiting characters. Then use Text To Columns Alt+D+E and delimit and Copy>Paste special>Transpose to quickly have it close to the format you need.
Alright I'm a beginner with VBA so I need some help. Assuming this is very basic, but here are the steps I am looking at for the code:
-Use Vlookup to find the value "Rec" in column C of Sheet1, and select that row's corresponding value in column D
-Then copy that value from column D in Sheet1 and paste it into the first blank cell in column B of another worksheet titled Sheet2
I've got a basic code that uses Vlookup to find Rec as well as it's corresponding value in column D, then display a msg. The code works fine, and is the following:
Sub BasicFindGSV()
Dim movement_type_code As Variant
Dim total_gsv As Variant
movement_type_code = "Rec"
total_gsv = Application.WorksheetFunction.VLookup(movement_type_code,Sheet1.Range("C2:H25"), 2, False)
MsgBox "GSV is :$" & total_gsv
End Sub
I also have another one that will find the next blank cell in column B Sheet2, it works as well:
Sub SelectFirstBlankCell()
Dim Sheet2 As Worksheet
Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) = True Then cell.Select: Exit For
Next cell
End Sub
Not sure how to integrate the two, and I'm not sure how to make the code paste the Vlookup result in Sheet2. Any help would be greatly appreciated, thanks!
So for being a beginner you're off to a good start by designing two separate subroutines that you can confirm work and then integrating. That's the basic approach that will save you headache after headache when things get more complicated. So to answer your direct question on how to integrate the two, I'd recommend doing something like this
Sub BasicFindGSV()
Dim movement_type_code As Variant
Dim total_gsv As Variant
movement_type_code = "Rec"
total_gsv = Application.WorksheetFunction.VLookup(movement_type_code, Sheet1.Range("C2:H25"), 2, False)
AssignValueToBlankCell (total_gsv)
End Sub
Sub AssignValueToBlankCell(ByVal v As Variant)
Dim Sheet2 As Worksheet
Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) = True Then cell.Value2 = v
Next cell
End Sub
That being said, as Macro Man points out, you can knock out the exact same functionality your asking for with a one liner. Keeping the operational steps separate (so actually a two liner now) would look like this.
Sub FindGSV()
AssignValueToBlankCell WorksheetFunction.VLookup("Rec", Sheet1.Range("C2:H25"), 2, False)
End Sub
Sub AssignValueToBlankCell(ByVal v As Variant)
Sheet3.Range("B" & Rows.Count).End(xlUp).Offset(1, 0).Value2 = v
End Sub
Like I said, if you plan to continue development with this, it's usually a good idea to design your code with independent operations the way you already have begun to. You can build off of this by passing worksheets, ranges, columns, or other useful parameters as arguments to a predefined task or subroutine.
Also, notice that I use Value2 instead of Value. I notice you're retrieving a currency value, so there's actually a small difference between the two. Value2 gives you the more accurate number behind a currency formatted value (although probably unnecessary) and is also faster (although probably negligible in this case). Just something to be aware of though.
Also, I noticed your use of worksheet objects kind of strange, so I thought it'd help to mentioned that you can select a worksheet object by it's object name, it's name property (with sheets() or worksheets()), index number (with sheets() or worksheets()), or the "Active" prefix. It's important to note that what you're doing in your one subroutine is reassigning the reference of the Sheet2 object to your active sheet, which means it may end up being any sheet. This demonstrates the issue:
Sub SheetSelectDemo()
Dim Sheet2 As Worksheet
Set Sheet2 = Sheets(1)
MsgBox "The sheet object named Sheet2 has a name property equal to " & Worksheets(Sheet2.Name).Name & " and has an index of " & Worksheets(Sheet2.Index).Index & "."
End Sub
You can view and change the name of a sheet object, as well as it's name property (which is different) here...
The name property is what you see and change in the worksheet tab in Excel, but once again this is not the same as the object name. You can also change these things programmatically.
Try this:
Sub MacroMan()
Range("B" & Rows.Count).End(xlUp).Offset(1, 0).Value = _
WorksheetFunction.VLookup("Rec", Sheet1.Range("C2:H25"), 2, False)
End Sub
The Range("B" & Rows.Count).End(xlUp) command is the equivalent of going to the last cell in column B and pressing Ctrl + ↑
We then use .Offset(1, 0) to get the cell after this (the next blank one) and write the value of your vlookup directly into this cell.
If Both work, then good, you have two working subs and you want to integrate them. You probably want to keep them so they might be useful for some other work later. Integrating them means invoking them in some third routine.
For many reasons, it is surely better and advised to avoid as much as possible to use (select, copy, paste) in VBA, and to use rather a direct copying method (range1.copy range2).
You need to make your routines as functions that return ranges objects, then in some third routine, invoke them
Function total_gsv() as range
Dim movement_type_code As Variant: movement_type_code = "Rec"
Set total_gsv = Application.WorksheetFunction.VLookup(movement_type_code,Sheet1.Range("C2:H25"), 2, False)
End Sub
Function FindFirstBlankCell() as Range
Dim Sheet2 As Worksheet: Set Sheet2 = ActiveSheet
For Each cell In Sheet2.Columns(2).Cells
If IsEmpty(cell) Then Set FindFirstBlankCell= cell: exit For
Next cell
End Sub
Sub FindAndMoveGsv()
total_gsv.copy FindFirstBlankCell
... 'some other work
End Sub
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.
This code
Sheets(1).Activate
Sheets(2).Range("A1").Select
will fail in VBA because you can only use Select on an object which is Active. I understand this is the case.
What element of the Excel datamodel causes this to be the case? I would think there is an implicit intent from a user/coder to Activate any object immediately prior to using Select - I do not understand why VBA would not make this assumption, and, I am assuming there is a reason this distinction exists.
What part of Excel's datamodel prevents selection without activation?
As brettdj pointed out, you do not have to activate the sheet in order to select a range. Here's a reference with a surprisingly large amount of examples for selecting cells/ranges.
Now as for the why do I have to active the sheet first? I do not believe it is a fault of the datamodel, but simply a limitation of the select method for Ranges.
From experimentation, it looks like there are two requirements to select a range in Excel.
Excel must be able to update the UI to indicate what is selected.
The ranges parent (I.E. the sheet) must be active.
To support this claim, you also cannot select a cell from a hidden sheet.
Sheets(1).Visible = False
Sheets(1).Activate
'The next line fails because the Range cannot be selected.
Sheets(1).Range("A1").Select
Simply put, when it comes to Ranges, you cannot select one you cannot see.
I would have claimed this is a limitation of select all together, except that you can actually select an object in a hidden sheet. Silly Excel.
I know that this is a bit late to the party, but I discovered a hack to do this...
Try this code:
Sheets(1).Activate
Sheets(2).Range("A1").Copy
Sheets(2).Range("A1").PasteSpecial xlPasteFormulas
Application.CutCopyMode = False
Note that it is a hack, but it does the trick!!
#Daniel Cook: thanks for your response, but unfortunately Excel itself doesn't play by the same rules imposed on Excel Macros...
To illustrate, I'll briefly present my current problem...
I'm attempting to re-set a table's contents to a common state. This method will be applied to multiple tables across various sheets:
Public Sub restoreTable()
Dim myTableSheet As Worksheet: Set myTableSheet = Range("Table1").Parent
Dim myTable As ListObject: Set myTable = myTableSheet.ListObjects("Table1")
' --- Clear Table's Filter(s)
If myTable.ShowAutoFilter Then ' table has auto-filters enabled
Call myTable.Range.AutoFilter ' disables autofilter
End If
myTable.Range.AutoFilter ' re-apply autofilter
' --- Sort by Sequence number
Call myTable.Sort.SortFields.Clear ' if not cleared, sorting will not take effect
myTable.Sort. _
SortFields.Add Key:=Range("Table1[[#Headers],[#Data],[Column1]]"), _
SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
myTable.Sort.Header = xlYes
myTable.Sort.Orientation = xlTopToBottom
myTable.Sort.SortMethod = xlPinYin
Call myTable.Sort.Apply
myTable.Sort.SortFields.Clear
End Sub
For each use-case below, Table1 is found in Sheet1
Use-Case 1:
Activate Sheet1, select range A1
Run restoreTable
observe: range Sheet1 A1 remains selected
Use-Case 2:
Activate Sheet1, select range A1
Activate Sheet2
Run restoreTable
observe: range Sheet1 A1 is not selected, instead the range Table1[#Data] is selected
Solution
It's absolutely terrible, but this is the best solution I could find
Public Sub resotreTable_preserveSelection()
Dim curSheet As Worksheet: Set curSheet = ActiveSheet
Dim tableSheet As Worksheet: Set tableSheet = Range("Table1").Parent
' Change Sheet
tableSheet.Activate
' Remember Selection / Active Ranges
Dim originalSelection As Range: Set originalSelection = Selection
Dim originalActiveCell As Range: Set originalActiveCell = ActiveCell
' Restore Table
Call restoreTable
' Restore Old Selection
originalSelection.Select
originalActiveCell.Activate
' Change Back to old sheet
curSheet.Activate
End Sub
Note: in this case, the original* ranges are not necessary, but you get the point: you can buffer the original selection and restore it when you're finished
I really don't like excel
Of course you don't have to select or activate the sheet to select/activate the cell. My way is to use "On Error Resume Next" and "On Error GoTo 0". Code below selects first cell in every worksheet of a workbook without selecting it. The worksheets are even very hidden on this stage.
On Error Resume Next
For i_wks = 1 To wb_macro.Worksheets.Count
wb_macro.Worksheets(i_wks).Cells(1).Select
Next i_wks
On Error GoTo 0
I'm having troubling referring to a Dynamic Name Range in VBA.
My ranges are defined as
=OFFSET(Sheet!$B$2,0,0,COUNTA(Sheet!$B:$B)-1,1)
My code should search one range for all entries in another range, the intention being that any missing entries will be added. So far I have
Sub UpdateSummary()
Dim Cell As Range
Dim rngF As Range
Set rngF = Nothing
' Step through each cell in data range
For Each Cell In Worksheets("Aspect").Range("A_Date")
' search Summary range for current cell value
Set rngF = Worksheets("Summary").Range("Sum_Date").Find(Cell.Value) // Does not work
If rngF Is Nothing Then
' Add date to Summary
End If
Set rngF = Nothing
Next Cell
End Sub
The For loop seems to work ok. However, using the .Find method is giving me an error message.
Application-defined or object-defined error
It does work if I replace the named range with a specific range ($B$2:$B$5000), so it seems to be down to how the named range is being passed.
Any ideas would be appreciated.
Thanks.
The error is almost definitely because Excel can't find a named range Sum_Date that refers to a range on a worksheet named Summary. The most common causes are
Sum_Date refers to a sheet other than Summary. Check the RefersTo property of Sum_Date and make sure nothing is misspelled.
There is not a named range Sum_Date, that is, it's misspelled in the VBA code. Check the spelling of the named range in the Name Manager.
There is an error in the RefersTo formula of Sum_Date. It sounds like you already verified that this isn't the case.
I've had the a similar if not the same problem & here's how I solved it:
I first realized that the method I used to create my named range, using the Name Manager, my named range had a scope of Workbook. This is important because, it doesn't belong to the worksheet, & therefore will not be found there.
So, Worksheets("Summary").Range("Sum_Date") would not work for me.
Since my range belonged to the workbook, the way I was able to find is to use ActiveWorkbook.Names("Sum_Date")
For me I used it to remove the formula from named range that I am using in many places. The huge advantage is that named range is updated only once instead of the formula being called for every cell location that ranged is called. Huge time delay difference!
Public last_Selection As String
Private Sub Worksheet_Change(ByVal Target As Range)
'excel data change detection
If Range(last_Selection).Column = 2 Then
'Disable events, so this only executes once
Application.EnableEvents = False
'This can be done with a complex formula in a cell,
'but this is easily understood
Range("B1").End(xlDown).Select
ActiveWorkbook.Names("last_Entry").Value = ActiveCell.Row
'Re-enable so this routine will execute on the next change
Application.EnableEvents = True
End If
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
'constantly store the last cell to know which one was previously edited
last_Selection = Target.Address
End Sub
I know this is a very old thread, but I had the same issue today and I was looking for solution for quite a long time. So maybe this will help someone.
The named "range" defined by the =OFFSET(...) formula is actually a named FORMULA, so in VBA you have to evaluate it first to get the range. E.g.:
Set rgNamedRange = Worksheets("Summary").Evaluate("Sum_Date")
Credits to a guy named "shg" from mrexcel.com, who got me on right track. :)
I have been experimenting with this for a few days and eventually I came up with the following. It may not be the most efficient but it did work for me!
The named range of "OhDear" was set up in the normal way
Dim vItem As Variant
Set vItem = Names("OhDear")
Debug.Print vItem.Name
Worth a try don't you think!
This does not work if instead of using a variant you use something like: Dim Nm as Name: Set Nm = Names("OhDear"). Any variations using 'Nm' failed!!!