How can I optimise Range Set and .Value - vba

Is there a more efficient way of writing the following:
Private Sub ConvertDatesToValue_Click()
Dim Rng1 As Range, Rng2 As Range, Rng3 As Range, Rng4 As Range
Set Rng1 = Range("Q8:Q12")
Set Rng2 = Range("Q16:Q20")
Set Rng3 = Range("T8:T12")
Set Rng4 = Range("T16:T20")
Rng1.Value = Rng1.Value
Rng2.Value = Rng2.Value
Rng3.Value = Rng3.Value
Rng4.Value = Rng4.Value
End Sub
It looks and feels a bit janky.

Private Sub ConvertDatesToValue_Click()
So we're looking at a control's Click handler, presumably an ActiveX button on a worksheet, in which case we're in that worksheet's code-behind module.
The button means to invoke a command that will convert the formulas with their value in a specific range of cells that contain dates.
I would start with a procedure that's responsible for assigning Range.Value onto itself, given a Range:
Public Sub FreezeFormulaResult(ByVal target As Range)
target.Value = target.Value
End Sub
Next we need to identify the range we'll pass into that procedure.
Dim Rng1 As Range, Rng2 As Range, Rng3 As Range, Rng4 As Range
Set Rng1 = Range("Q8:Q12")
Set Rng2 = Range("Q16:Q20")
Set Rng3 = Range("T8:T12")
Set Rng4 = Range("T16:T20")
Kudos for avoiding the implicit Variant trap, and declaring an explicit type for every one of these variables.
it looks and feels a bit janky
That's because the variables have that numeric suffix. Rng1...RngN is a code smell indeed: it's a dirty solution to the problem of needing a bunch of something.
Often, a more elegant solution would be to use an array:
Dim ranges As Variant
ranges = Array(Range("Q8:Q12"), Range("Q16:Q20"), Range("T8:T12"), Range("T16:T20"))
There are different many ways to skin a cat, but a union'ed disjointed Range1 will not produce the expected results. Because you need 4 distinct areas, you'll need 4 distinct operations.
How the click handler needs to fetch the ranges, depends on where that handler is.
If we're looking at an MSForms.CommandButton (ActiveX) button on a Worksheet, then the click handler is in the very same sheet we want to get the cells from.
In that case we can work off the current object, Me - and in fact by not qualifying Range calls we're doing exactly that... implicitly.
In other words this:
Set Rng1 = Range("Q8:Q12")
Means this:
Set Rng1 = Me.Range("Q8:Q12")
You can't have a button Click handler in a standard module, but if you were to write this in one:
Set Rng1 = Range("Q8:Q12")
Then that would be implicity this:
Set Rng1 = ActiveSheet.Range("Q8:Q12")
Note the difference: that's why implicit code is evil, and why context is everything - by writing explicit code, you reduce the cognitive load by making the context local rather than ambient.
We need something that gives us an array of Range objects to work with. Let's try abstraction - it could be a public property in the sheet's code-behind:
Public Property Get ImportantDateRanges() As Variant
ImportantDateRanges = Array( _
Me.Range("Q8:Q12"), _
Me.Range("Q16:Q20"), _
Me.Range("T8:T12"), _
Me.Range("T16:T20"))
End Property
And now the button's Click handler no longer needs to care what the cells are, and the abstraction level is just perfect:
Private Sub ConvertDatesToValues_Click()
FreezeDateFormulas
End sub
Private Sub FreezeDateFormulas()
Dim dateRanges As Variant
dateRanges = Me.ImportantDateRanges
Dim i As Long
For i = LBound(dateRanges) To UBound(dateRanges)
FreezeFormulaResult dateRanges(i)
Next
End Sub
If we're looking at an MSForms.CommandButton on a UserForm, it's the exact same ActiveX button, ...but it's an entirely different story, because while you don't own a Worksheet instance (Excel does), you do own a UserForm instance - and that comes with implications too numerous to explain here (that link goes to an article I wrote about how forms don't need to run the show).
1How can I optimise Range Set and .Value

Try a direct value reversion.
Private Sub ConvertDatesToValue_Click()
With Worksheets("sheet9")
.Range("Q8:Q12") = .Range("Q8:Q12").Value
.Range("Q16:Q20") = .Range("Q16:Q20").Value
.Range("T8:T12") = .Range("T8:T12").Value
.Range("T16:T20") = .Range("T16:T20").Value
End With
End Sub
You really should be aware of what worksheet you're on.

You can loop through the 4 areas :
Dim r As Range
For Each r In Range("Q8:Q12,Q16:Q20,T8:T12,T16:T20").Areas
r = r.Value
Next

Related

VBA Dim and Set Range global

I have a probably easy problem using variables globally:
I want to Dim and Set various ranges in order to use them in different subs.
Simple Example given:
Sub Variables()
Dim rng1 as Range
Set rng1 = Worksheets("Sheet1").Cells
Dim rng2 as Range
Set rng2 = Worksheets("Sheet2").Cells
End Sub
Then in order to use my variables in other subs I tried to call my Variable sub, like:
Sub calc()
Call Variables
Dim i as Integer
i = rng1.Find("Hello").Column
With Sheet1.Cells(1, i)
.Value = "World"
End With
I get the Error "Object required". I also tried to define my Variables as Public Sub but it still doesn't work.
I hope you know how to solve this problem or how to give a different approach!
Thanks!
Option Explicit
Dim rng1 As Range
Dim rng2 As Range
Sub Variables()
Set rng1 = Worksheets("Sheet1").Cells
Set rng2 = Worksheets("Sheet2").Cells
End Sub
Sub calc()
Variables
Dim i As Integer
i = rng1.Find("Hello").Column
Sheet1.Cells(1, i).Value = "World"
End Sub
You declared inside the Sub, you can't able to use them in other Sub, so that's why it gives you an error.
The solution here is put them at the top of your Procedure.

Application.Caller / Storing worksheet name as string

Im trying to store the CurrentWorksheet name in order to reference it in a different Sub routine.
The code I currently have is as follows:
Private Sub InsertNewBill_Click()
Dim rng As Range
Set rng = Worksheets(CurrentWorksheet).Range("A30:L30")
rng.Insert Shift:=xlDown
End Sub
Current Worksheet Function:
Function CurrentWorksheet()
CurrentSheet = Application.Caller.Worksheet.Name
End Function
I need to try to reference the "CurrentSheet" variable in the "InsertNewBill" Sub routine.
The function of this is to insert a new line of cells between "A30:L30" on the currently selected worksheet.
Thanks in advance
I didn't plan to write this as answer, but to explain to Batteredburrito how to deal with objects rather than names:
Option Explicit
Private Sub InsertNewBill_Click()
Dim rng As Range
Set rng = currentWorksheet.Range("A31:AC31")
rng.Insert Shift:=xlDown
End Sub
Current Worksheet Function
Function currentWorksheet() As Worksheet
set currentWorksheet = ActiveSheet
End Function
This is doing exactly the same as your version (so you can stick with that), it's just to show how to deal with objects. But there are situations where it is much better to deal with objects that with name - especially if you are dealing with multiple Workbooks (that might have the same sheet names).
And, of course, in the end you could get rid of the function completely by simply writing
...
Set rng = ActiveSheet.Range("A31:AC31")
...
Thank you all for your help and direction.
I have resolved the issue with the following
Insert new cells between a range of cells on button press:
Private Sub InsertNewBill_Click()
Dim rng As Range
Set rng = Worksheets(CurrentWorksheet).Range("A31:AC31")
rng.Insert Shift:=xlDown
End Sub
Current Worksheet Function:
Function CurrentWorksheet()
CurrentWorksheet = ActiveSheet.Name
End Function

Am I using the Columns() property improperly, and if so, is there a good workaround?

I'm currently in the process of solving a
type mismatch error
in a macro I'm writing, and I've written a short subroutine to drill down on the specific issue. This subroutine should loop through all of Column A, entering the numbers 1-10 in rows 1-10.
Sub looptest()
Dim rRange As Range
Dim rCell As Range
Dim i As Integer
Set rRange = ThisWorkbook.Worksheets(1).Columns(1)
i = 0
For Each rCell In rRange
If i < 10 Then
i = i + 1
rCell.Value2 = i
End If
Next rCell
End Sub
Instead this fills every cell in Column A with 1. Stepping through it in debug mode shows that instead of referencing a single cell, rCell references the entire column.
I have found that if I replace
Set rRange = ThisWorkbook.Worksheets(1).Columns(1)
with
Set rRange = ThisWorkbook.Worksheets(1).Range("A1:A100")
the macro works as intended, however I'd prefer to be able to use Columns() or something similar in my production code.
Am I using the Columns() property improperly, and if so, is there a good workaround?
Begin with these changes:
Set rRange = ThisWorkbook.Worksheets(1).Columns(1).Cells
Dim i As Long
Using the .Cells lets us loop over the cells in a column rather than columns in a worksheet.

VBA Excel select a range of cells within a named range

I have a Named Range col_9395 it is an entire column. I want to set a range within this Named range. I want the range to start at row 3 to row 200 of same column. Whats the best way to do this?
Original working line without Named Range:
Set rngBlnk = Sheet108.Range("T3:T200").SpecialCells(xlCellTypeBlanks)
This is the code I tried with no luck:
Set rngBlnk = Range("col_9395)(3,1):Range("col_9395)(200,1).SpecialCells (xlCellTypeBlanks)
Might be wrong, but I find this the easiest one:
Private Sub Test()
Dim rngBlnk As Range
Set rngBlnk = Range("col_9395").Rows("3:200").SpecialCells(xlCellTypeBlanks)
End Sub
You can see the sort of logic with
Option Explicit
Sub test()
Dim colToUse As Long
colToUse = ThisWorkbook.Worksheets("Sheet1").Range("ol_9395").Column
With ThisWorkbook.Worksheets("Sheet1")
Debug.Print .Range(.Cells(3, colToUse), .Cells(200, colToUse)).Address
End With
End Sub
Sub t()
Dim rng As Range
Set rng = Range("col_9395") ' for easier use
Dim blnkRng As Range
Set blnkRng = Range(Cells(rng.Rows(3).Row, rng.Column), Cells(rng.Rows(200).Row, rng.Column)).SpecialCells(xlCellTypeBlanks)
blnkRng.Select
End Sub
What I did was assign your named range to a variable (just for easier referencing). Then using the Range() property, I used the 3rd and 200th row of your named range to set the range to look for blank cells.
The idea is that this will help you in the event your named range isn't simply an entire column. It will get the relative 3rd and 200th row from your named range.
Option Explicit
Public Sub TestMe()
Dim rngBlnk As Range
Dim firstCell As Range
Dim lastCell As Range
Set firstCell = [col_9395].Cells(3, 1)
Set lastCell = [col_9395].Cells(200, 1)
If WorksheetFunction.CountBlank(Range(firstCell, lastCell)) > 0 Then
Set rngBlnk = Range(firstCell, lastCell).SpecialCells(xlCellTypeBlanks)
End If
End Sub
A kind of a problem with SpecialCells and assigning to them is that if there are no cells from the specific type, it throws an error.
Thus, there is a check with WorksheetFunction.CountBlank()>0, before the setting of rngBlnk to the special cells.
I'm late to the party, I know, but maybe this will help someone. I've stumbled on a technique that I don't see presented as an option in any of the handful of "range-in-a-range" questions here.
I discovered you can ask VBA for a range of a range directly. I have formatted some data as a Table, but it could be simply a Named Range, or even an unnamed range I suppose. My code that is working looks like this:
With Workbooks(Filename)
.Worksheets(tabName).Activate
.Worksheets(tabName).Range("SummaryBand").Range("B2:R2").Copy
End With
My Table is named SummaryBand, which due to a previous step was not necessarily always in the same absolute position on the spreadsheet, but I wanted an absolute Range within SummaryBand. In this example, "B2:R2" is the absolute position within the Table, with the top left cell of the Table being A1.

Match Index in VBA error

I am trying to use the Index and Match Worksheet Functions to match an item number from a combo box in a user form (ItemNum.value) to a product list table ("Product Pricing") on a worksheet. Then pull cells that match the same row from that worksheet ("Product Pricing") and copy them to cell ("i4") on an input data worksheet ("Review Lighting Data"). The problem is every time I run the macro it gives me the error "1004 Unable to get the Match Property of the WorksheetFunction class error"
My code is:
Private Sub InputLight_Click()
With Sheets("Review Lighting Data")
.Range("i4").Value = Application.WorksheetFunction.Index(Sheets("Product Pricing").Range("c7:c102"), Application.WorksheetFunction.Match(ItemNum.Value, Sheets("Product Pricing").Range("b7:b102"), 0))
End With
End Sub
I've since tried starting a new workbook to try to simplify things. Here is my complete code so far:
Private Sub inputbutton_Click()
Dim MatchRow As Long
Dim WS0 As Worksheet, WS1 As Worksheet
Dim R0 As Range, R1 As Range
With ThisWorkbook
Set WS0 = .Sheets("Review Lighting Data")
Set WS1 = .Sheets("Product Pricing")
End With
With WS1
Set R0 = WS1.Range("B7:B11")
Set R1 = WS1.Range("C7:C11")
End With
MatchRow = Application.Match(itemnum.Value, R0, 0)
MsgBox MatchRow
End Sub
Private Sub UserForm_Initialize()
With Me.itemnum
.AddItem "1001"
.AddItem "1002"
.AddItem "1003"
.AddItem "1004"
.AddItem "1005"
End With
End Sub
I've entered the item numbers into the combobox (itemnum) EXACTLY with no spaces or anything. I've even tried deleting the " marks around each number but that doesn't work either. I've tried outputting (MatchRow) into a MsgBox to try to catch the error but it breaks before it does.
The problem is that when Match doesn't found value in range, it returns error. You can handle this situation using IsError:
Private Sub InputLight_Click()
Dim matchRes
With Sheets("Review Lighting Data")
matchRes = Application.Match(ItemNum.Value, Sheets("Product Pricing").Range("b7:b102"), 0)
If Not IsError(matchRes) Then
.Range("i4").Value = Application.Index(Sheets("Product Pricing").Range("c7:c102"), matchRes)
End If
End With
End Sub
One good practice is to qualify everything properly. This makes the code easier to read and to debug. Also, you have to use Application.Match and not Application.WorksheetFunction....
Private Sub InputLight_Click()
Dim MatchRow
Dim WS0 As Worksheet, WS1 As Worksheet
Dim R0 As Range, R1 As Range
With ThisWorkbook
Set WS0 = .Sheets("Review Lighting Data")
Set WS1 = .Sheets("Product Pricing")
End With
With WS1
Set R0 = .Range("B7:B102")
Set R1 = .Range("C7:C102")
End With
MatchRow = Application.Match(ItemNum.Value, R0, 0)
If Not IsError(MatchRow) Then
WS0.Range("I4").Value = Application.Index(R1, MatchRow)
End If
End Sub
A very similar issue is found here: Match Not working Excel: Error 1004 Unable to get the Match Property, wherein #simoco and I also dealt with it similarly.
Okay after trying a bunch of different ways to get match index working within the userform I decided to 'cheat' a little and just used the regular match index function in the cells. Thank you all so much for your help!