I want to find a smarter way to test if two range objects, in fact, refer to the same range:
Set A = Range("B1:B3,A2:C2")
Set B = Range("B1,A2:C2,B3")
Set C = Range("A2,B1:B3,C2")
Set D = Range("B1,A2,B2,C2,B3")
The function I'm trying to write must return True when comparing any pair of ranges described above, and False when comparing any of those ranges to a range containing cells that are not part of the first range or not containing some cells from the first range.
What algorithm other than going cell by cell and checking that Intersect() is not Nothing is there for this problem?
I wrote this code on another forum some years back as a quick method to add a Subtract Range option, the same approach I used in Fast method for determining unlocked cell range
background
This function accepts two ranges, removes the cells where the two ranges intersect, and then produces a string output containing the address of the reduced range. This is done by:
creating a new one-sheet WorkBook
entering the N/A formula into all the cells on this sheet contained in rng1,
clearing the contents of all cells on this sheet that are contained by rng2,
using SpecialCells to return the remaining N/A formulae which represents the cells in rng1 that are not found in rng2,
If the Boolean variable, bBothRanges, is set to True, then the process is repeated with the cells with the opposite range order,
the code then returns the "reduced" range as a string, then closes the WorkBook.
As an example:
'Return the hidden cell range on the ActiveSheet
Set rngTest1 = ActiveSheet.UsedRange.Cells
Set rngTest2 = ActiveSheet.UsedRange.SpecialCells(xlVisible)
If rngTest1.Cells.Count > rngTest2.Cells.Count Then
strTemp = RemoveIntersect(rngTest1, rngTest2)
MsgBox "Hidden cell range is " & strTemp, vbInformation
Else
MsgBox "No hidden cells", vbInformation
End If
In your case the code runs the bBothRanges option and then checks if the RemoveIntersect returns vbNullStringto see if the ranges are the same.
For very short ranges as you have provided, a simple cell by cell loop would suffice, for larger ranges this shortcut may be useful.
Sub Test()
Dim A As Range, B As Range, C As Range, D As Range
Set A = Range("B1:B3,A2:C2")
Set B = Range("B1,A2:C2,B3")
Set C = Range("A2,B1:B3,C2")
Set D = Range("B1,A2,B2,C2,B3")
MsgBox RemoveIntersect(A, B, True) = vbNullString
End Sub
main
Function RemoveIntersect(ByRef rng1 As Range, ByRef rng2 As Range, Optional bBothRanges As Boolean) As String
Dim wb As Workbook
Dim ws1 As Worksheet
Dim rng3 As Range
Dim lCalc As Long
'disable screenupdating, event code and warning messages.
'set calculation to Manual
With Application
.ScreenUpdating = False
.EnableEvents = False
.DisplayAlerts = False
lCalc = .Calculation
.Calculation = xlCalculationManual
End With
'add a working WorkBook
Set wb = Workbooks.Add(1)
Set ws1 = wb.Sheets(1)
On Error Resume Next
ws1.Range(rng1.Address).Formula = "=NA()"
ws1.Range(rng2.Address).Formula = vbNullString
Set rng3 = ws1.Cells.SpecialCells(xlCellTypeFormulas, 16)
If bBothRanges Then
ws1.UsedRange.Cells.ClearContents
ws1.Range(rng2.Address).Formula = "=NA()"
ws1.Range(rng1.Address).Formula = vbNullString
Set rng3 = Union(rng3, ws1.Cells.SpecialCells(xlCellTypeFormulas, 16))
End If
On Error GoTo 0
If Not rng3 Is Nothing Then RemoveIntersect = rng3.Address(0, 0)
'Close the working file
wb.Close False
'cleanup user interface and settings
'reset calculation
With Application
.ScreenUpdating = True
.EnableEvents = True
.DisplayAlerts = True
lCalc = .Calculation
End With
End Function
You could always do it manually, like this:
Private Function isRangeEquivalent(ByRef range1 As Range, ByRef range2 As Range) As Boolean
isRangeEquivelent = (range1.Cells.Count = range2.Cells.Count)
If isRangeEquivelent Then
Dim addresses As collection
Set addresses = New collection
Dim cell As Range
For Each cell In range1.Cells
Call addresses.Add(cell.Address, cell.Address)
Next cell
For Each cell In range2.Cells
If Not isInCollection(addresses, cell.Address) Then
isRangeEquivelent = False
Exit For
End If
Next cell
End If
End Function
Private Function isInCollection(ByRef collection As collection, ByVal sKey As String)
On Error GoTo Catch
collection.Item sKey
isInCollection = True
Exit Function
Catch:
isInCollection = False
End Function
Related
Below is my code. All of the code works, but I get error 438 object doesn't support this property or method vba in this line. i.offset(-7,-8).paste
Sub insert_6_rows()
Dim rActive As Range
Dim wb As Workbook
Set rActive = ActiveCell
Application.ScreenUpdating = False
Dim f As Range
Set f = Sheets("Format").Range("A1:J6")
Dim FindST As Range
Set FindST = Sheets("Driver").Range("I:I").Find(What:="Subtotal", LookIn:=xlValues)
FindST.Offset(-1, 0).EntireRow.Resize(6).Insert
f.Copy
Dim i As Range
Set i = Sheets("Driver").Range("I:I").Find(What:="Subtotal", LookIn:=xlValues)
i.Offset(-7, -8).Paste
rActive.Select
Application.CutCopyMode = False
Application.ScreenUpdating = True
End Sub
Use the Destination argument of a Range.Copy method.
Sub insert_6_rows()
Dim rActive As Range
Dim wb As Workbook
Set rActive = ActiveCell
Application.ScreenUpdating = False
Dim f As Range, FindST As Range, i As Range
Set f = workSheets("Format").Range("A1:J6")
Set FindST = workSheets("Driver").Range("I:I").Find(What:="Subtotal", LookIn:=xlValues)
FindST.Offset(-1, 0).EntireRow.Resize(6).Insert
f.Copy Destination:=workSheets("Driver").cells(FindST.row-1, "A")
rActive.select
Application.ScreenUpdating = True
End Sub
Use meaningful variable names! Everyone would think of i is a simple counter. Eg. better name it FoundCell.
If nothing is found then you cannot .Offset from "nothing", that's why it fails. So you will need to test if something was found.
I suggest:
Dim FoundCell As Range
Set FoundCell = Sheets("Driver").Range("I:I").Find(What:="Subtotal", LookIn:=xlValues)
'check if something was found
If FoundCell is Nothing Then
MsgBox "Subtotal not found in column I"
Exit Sub
End If
'check if found cell.row is at least 7 rows so we can offset -7
If FoundCell.Row <= 7 Then
MsgBox "Cannot offset -7 rows because found cell is less then 7 rows from top"
Exit Sub
End If
SourceRange.Copy
FoundCell.Offset(-7, -8).Paste
Change Paste to PasteSpecial
i.Offset(-7, -8).PasteSpecial
I have a very strange problem with some Excel VBA code I created.
I won't get into the specifics (unless needed), but I have code which filters and copies data from one sheet to another.
On this second sheet it checks for empty columns and deletes them.
I have created this small macro to do the delete-part:
Public Sub deleteemptyrows()
Dim C As Integer
Range("A1").Select
Application.CutCopyMode = False
C = ActiveSheet.Cells.SpecialCells(xlLastCell).Column
Do Until C = 0
If WorksheetFunction.CountA(Columns(C)) = 1 Then
Columns(C).Delete
End If
Debug.Print C
C = C - 1
Loop
End Sub
Now this macro works perfectly and superfast (for the approx. 500 columns I'm checking every time) but the problem occurs when I call this macro in my VBA code (after the code copies the filtered data).
When it reaches the line Columns(C).Delete the memory of EXCEL.exe is going up to 6 GB in task manager, and it's running very, very slow column by column.
I have added the Application.CutCopyMode = False line because I thought it might have the copied data in it's memory, but that didn't help.
Any idea on how to fix this? Thanks!
Whilst you need to loop over the columns in the used range of the sheet - you don't need to delete them one-by-one. You can build a range - using Union - to create a non-contiguous range of columns with only headers and then delete them all in a single go. Using this technique in with disabling various properties of Application should give you an efficient method:
Option Explicit
Sub DeleteColumnsEfficiently()
Dim ws As Worksheet
Dim rngEmptyColumns As Range
Dim rngColumn As Range
Dim wsf As WorksheetFunction
Dim lngSetting As Long
' set a reference to worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
' set reference to WorksheetFunction
Set wsf = Application.WorksheetFunction
' initialise range of empty columns
Set rngEmptyColumns = Nothing
' set application settings to optimise ui change
With Application
.ScreenUpdating = False
.EnableEvents = False
lngSetting = .Calculation
.Calculation = xlCalculationManual
End With
' loop columns in usedrange
For Each rngColumn In ws.UsedRange.Columns
' check if only header populated
If wsf.CountA(rngColumn) = 1 Then
' if just header - then add to range of columns
If rngEmptyColumns Is Nothing Then
Set rngEmptyColumns = rngColumn.Offset
Else
Set rngEmptyColumns = Application.Union(rngEmptyColumns, rngColumn)
End If
End If
Next rngColumn
' delete columns with only header
rngEmptyColumns.Delete
' reset application settings
With Application
.ScreenUpdating = True
.EnableEvents = True
.Calculation = lngSetting
End With
End Sub
If you are trying to run the sub routine by passing the sheet string, you may try the Robin's code as below...
Remember you are supposed to place this code on a Standard Module like Module1, Module2 etc. after inserting a New Module not on ThisWorkbook Module as you did in the sample workbook.
Option Explicit
Sub DeleteColumnsEfficiently(ByVal strSheetName As String)
Dim ws As Worksheet
Dim rngEmptyColumns As Range
Dim rngColumn As Range
Dim wsf As WorksheetFunction
Dim lngSetting As Long
' set a reference to worksheet
Set ws = ThisWorkbook.Worksheets(strSheetName)
' set reference to WorksheetFunction
Set wsf = Application.WorksheetFunction
' initialise range of empty columns
Set rngEmptyColumns = Nothing
' set application settings to optimise ui change
With Application
.ScreenUpdating = False
.EnableEvents = False
lngSetting = .Calculation
.Calculation = xlCalculationManual
End With
' loop columns in usedrange
For Each rngColumn In ws.UsedRange.Columns
rngColumn.Select
rngColumn.Offset.Select
' check if only header populated
If wsf.CountA(rngColumn) = 1 Then
' if just header - then add to range of columns
If rngEmptyColumns Is Nothing Then
Set rngEmptyColumns = rngColumn
Else
Set rngEmptyColumns = Application.Union(rngEmptyColumns, rngColumn)
End If
End If
Next rngColumn
' delete columns with only header
If Not rngEmptyColumns Is Nothing Then
rngEmptyColumns.Delete
End If
' reset application settings
With Application
.ScreenUpdating = True
.EnableEvents = True
.Calculation = lngSetting
End With
End Sub
Sub Test()
DeleteColumnsEfficiently "Sheet1"
End Sub
I have several VBA routines in an Excel 2007. There is a template worksheet which gets copied (and accordingly altered) up to 50 times. Now, this template contains a range called "HideRows", so this range gets copied several times in all those new worksheets. I want to hide all rows that contain the value 0 in the range "HideRows". Not all rows shall be hidden, only those rows that contain the value 0. This is what I've got so far:
Option Explicit
Sub HideEmptyRows()
Dim rngName As Range
Dim cell As Range
Application.ScreenUpdating = False
For Each rngName In ActiveWorkbook.Names
If rngName.Name = "HideRows" Then
With cell
For Each cell In rngName
If .Value = 0 Then
.EntireRow.Hidden = True
End If
Next cell
End With
End If
Next rngName
What's wrong here and what do I need to do to get it to work?
You can address the named range directly without looping. There is no test that this named range exists, as per your description it is safe to assume so.
Secondly, do not use the "with" statement outside of the loop that sets the referenced variable. Try this instead:
Option Explicit
Sub HideEmptyRows()
Dim rngName As Range
Dim cell As Range
Application.ScreenUpdating = False
For Each cell In range("HideRows")
If cell.Value = 0 Then
cell.EntireRow.Hidden = True
End If
Next cell
Application.ScreenUpdating = True
edit:
If the workbook contains multiple identical sheets where each sheet may contain this named range you will have to loop. This code will not loop over all names but over all sheets, and test for existance of the named range in each sheet:
Sub HideEmptyRows()
Dim sh As Sheets
Dim rng As Range, cell As Range
For Each sh In ActiveWorkbook.Sheets
Set rng = Nothing ' this is crucial!
On Error Resume Next
Set rng = sh.Names("HideRows")
On Error GoTo 0
If Not rng Is Nothing Then
For Each cell In rng
cell.EntireRow.Hidden = (cell.Value = 0)
Next cell
End If
Next sh
End Sub
The range variable has to be reset explicitly before the assignment as this step is skipped if the range does not exist. The following If would use the value last assigned then, which would be wrong.
I wrote a macro and while it works, functionally its not what is needed. It's an interactive checklist that breaks down multiple areas of machines and if they are working checks them off and then this updates a master list with multiple sections. However, it only works with one cell at a time and it needs to be able to work with multiple cells at a time (both with rows and columns). Here's my current code:
'Updates needed:
' Make so more than one cell works at a time
' in both x and y directions
Private Sub Worksheet_Change(ByVal Target As Excel.range)
Dim wb As Workbook
Dim mWS As Worksheet
Dim conName As String
Dim mCol As range
Dim mCon As Integer
Dim count As Long
Dim cell As range
Dim y As String
count = 1
y = ""
Set wb = ActiveWorkbook
Set mWS = wb.Sheets("Master")
Set mCol = mWS.range("B:B")
mCon = 0
'Selects the name of the string value in which we need to search for in master list
If Target.Column < 100 Then
ThisRow = Target.Row
conName = ActiveSheet.Cells(ThisRow, "B")
y = Target.Value
End If
'search for matching string value in master list
For Each cell In mCol
If cell.Value = conName Then
mCon = count
Exit For
End If
count = count + 1
Next
'mark as "x" in Master list
Dim cVal As Variant
Set cVal = mWS.Cells(count, Target.Column)
cVal.Value = y
End Sub
What is happening - If I drag down "x" for multiple rows or columns my codes breaks at y = Target.Value and will only update the cell I first selected and its counterpart on the master list. What it should do is if I drag and drop the "x" onto multiple rows of columns it should update all of them in the sheet I'm working on and the master list. I only set up the macro for one cell at a time and I have no idea how to set it up for dragging and dropping the "x" value for multiple rows
I think you need a For ... Each iteration over the Target in order to work with multiple cells. As Michael noted in the comments, the _Change event fires only once, but the Target reflects all cell(s) that changed, so you should be able to iterate over the Target range. I tested using this simple event handler:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim myRange As Range
Dim myCell As Range
Set myRange = Target
For Each myCell In myRange.Cells
Debug.Print myCell.Address
Next
End Sub
I am not able to test obviously on your data/worksheet, but I think it should put you on the right track.
Private Sub Worksheet_Change(ByVal Target As Excel.range)
Dim wb As Workbook
Dim mWS As Worksheet
Dim conName As String
Dim mCol As range
Dim mCon As Integer
Dim count As Long
Dim cell As range
Dim y As String
count = 1
y = ""
Set wb = ActiveWorkbook
Set mWS = wb.Sheets("Master")
Set mCol = mWS.range("B:B")
mCon = 0
'Add some new variables:
Dim myRange as Range
Dim myCell as Range
Set myRange = Target
Application.EnableEvents = False '## prevents infinite loop
For each myCell in myRange.Cells
If myCell.Column < 100 Then
ThisRow = myCell.Row
conName = ActiveSheet.Cells(ThisRow, "B")
y = myCell.Value
End If
'search for matching string value in master list
For Each cell In mCol
If cell.Value = conName Then
mCon = count
Exit For
End If
count = count + 1
Next
'mark as "x" in Master list
Dim cVal As Variant
Set cVal = mWS.Cells(count, Target.Column)
cVal.Value = y
Next
Application.EnableEvents = True '## restores event handling to True
End Sub
You need to iterate through the cells using a ForEach loop.
Also, you may be better using the Selection object rather than Target
Private Sub Worksheet_Change(ByVal Target As Range)
Application.EnableEvents = False
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
For Each cell In Selection
Debug.Print cell.Address
Next cell
Application.EnableEvents = True
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Exit Sub
I am trying to loop through some columns in a row and create new worksheets with the name of the value of the current column/row that I am in.
Sub test()
Range("R5").Select
Do Until IsEmpty(ActiveCell)
Sheets.Add.Name = ActiveCell.Value
ActiveCell.Offset(0, 1).Select
Loop
End Sub
This code creates the first one correctly starting at R5 but then it appears that the macro switches to that worksheet and doesn't complete the task.
The Sheets.Add automatically moves your selection to the newly created sheet (just like if you insert a new sheet by hand). In consequence the Offset is based on cell A1 of the new sheet which now has become your selection - you select an empty cell (as the sheet is empty) and the loop terminates.
Sub test()
Dim MyNames As Range, MyNewSheet As Range
Set MyNames = Range("R5").CurrentRegion ' load contigeous range into variable
For Each MyNewSheet In MyNames.Cells ' loop through cell children of range variable
Sheets.Add.Name = MyNewSheet.Value
Next MyNewSheet
MyNames.Worksheet.Select ' move selection to original sheet
End Sub
This will work better .... you assign the list of names to an object variable of type Range and work this off in a For Each loop. After you finish you put your Selection back to where you came from.
Sheets.Add will automatically make your new sheet the active sheet. Your best bet is to declare variables to your objects (this is always best practice) and reference them. See like I've done below:
Sub test()
Dim wks As Worksheet
Set wks = Sheets("sheet1")
With wks
Dim rng As Range
Set rng = .Range("R5")
Do Until IsEmpty(rng)
Sheets.Add.Name = rng.Value
Set rng = rng.Offset(0, 1)
Loop
End With
End Sub
Error handling should always be used when naming sheets from a list to handle
invalid characters in sheet names
sheet names that are too long
duplicate sheet names
Pls change Sheets("Title") to match the sheet name (or position) of your title sheet
The code below uses a variant array rather than a range for the sheet name for performance reasons, although turning off ScreenUpdating is likely to make the biggest difference to the user
Sub SheetAdd()
Dim ws1 As Worksheet
Dim ws2 As Worksheet
Dim strError As String
Dim vArr()
Dim lngCnt As Long
Dim lngCalc As Long
Set ws1 = Sheets("Title")
vArr = ws1.Range(ws1.[r5], ws1.[r5].End(xltoRight))
If UBound(vArr) = Rows.Count - 5 Then
MsgBox "sheet range for titles appears to be empty"
Exit Sub
End If
With Application
.ScreenUpdating = False
.EnableEvents = False
lngCalc = .Calculation
End With
For lngCnt = 1 To UBound(vArr)
Set ws2 = Sheets.Add
On Error Resume Next
ws2.Name = vArr(lngCnt, 1)
If Err.Number <> 0 Then strError = strError & vArr(lngCnt, 1) & vbNewLine
On Error GoTo 0
Next lngCnt
With Application
.ScreenUpdating = True
.EnableEvents = True
.Calculation = lngCalc
End With
If Len(strError) > 0 Then MsgBox strError, vbCritical, "These potential sheet names were invalid"
End Sub
This is probably the simplest. No error-handling, just a one-time code to create sheets
Sub test()
Workbooks("Book1").Sheets("Sheet1").Range("A1").Activate
Do Until IsEmpty(ActiveCell)
Sheets.Add.Name = ActiveCell.Value
Workbooks("Book1").Sheets("Sheet1").Select
ActiveCell.Offset(0, 1).Select
Loop
End Sub