Strange cell addresses behaviour for non-contiguous ranges: VBA - vba

I was trying to answer this question when I came across some bizarre VBA behaviour in Excel. I have written a very simple sub to demonstrate the issue:
Sub debugAddresses(rng As Range)
Debug.Print "Whole range: " & rng.Address
Dim i As Long
For i = 1 To rng.Cells.Count
Debug.Print rng.Cells(i).Address
Next i
End Sub
I loop over each cell in a range object and print its address, simple right?
debugAddresses Range("B2:B3")
' Result as expected:
' >> Whole range: $B$2:$B$3
' >> $B$2
' >> $B$3
However, for non-contiguous ranges I get some strange behaviour:
debugAddresses Range("A1,B2")
' Strange behaviour when getting addresses of individual cells:
' >> Whole range: $A$1,$B$2
' >> $A$1
' >> $A$2
Can anyone shed any light on this please? Specifically why the Cells objects, which can be used for indexing of a contiguous range, seem to just extend the first selected Area.
Edit: It might be worth noting that using a For Each loop through the actual cell range objects gives the expected result*
Sub debugAddresses2(rng As Range)
Debug.Print "Whole range: " & rng.Address
Dim c As Range
For Each c In rng
Debug.Print c.Address
Next c
End Sub
*See my answer for a comment on a more robust solution, as this (apparently) may not always give the expected result

Try using the modified Sub debugAddresses code below:
Sub debugAddresses(rng As Range)
Dim RngA As Range
Dim C As Range
For Each RngA In rng.Areas
For Each C In RngA.Cells
Debug.Print C.Address
Next C
Next RngA
End Sub

Here is your code "fixed". by just adding one more For Loop
Sub debugAddresses(rng As Range)
Debug.Print "Whole range: " & rng.Address
For Each r In rng ' this loops through the range even if separated cells
Dim i As Long
For i = 1 To r.Cells.Count 'changed to r instead of rng
Debug.Print r.Cells(i).Address 'changed to r instead of rng
Next i
Next r
End Sub
So .Range works by entering the cell address ex. "B1", or by using R1C1 meaning row column ex. 1,2.
But you cant use just one R1C1 inside of .Range, since the range here is a span of cells. So to properly use R1C1 in .Range, you have to specify 2 of them.
So .Range("B5:B10") is equal to Range(Cells(5,2),Cells(10,2))
What you did was Specify a Range, then from that created another range using Cells. Very much like offset.
So Range("A1,B2") then adding Cells(1) then Cells(2) adds rows to the first range that is "A1" or offsets.
Sub selector()
Set Rng = Range("A1")
Rng.Select
Rng.Cells(4, 4).Select
End Sub
This offsets 4 colums and 4 rows from A1

It appears that Florent's comment was in the correct direction, and that this method is extending the first Area within the range object.
In a contiguous range (e.g. "A1:B5", "C10:C100") the following method loops over each cell in the given range, rng.
Dim j As Long
For j = 1 To rng.Cells.Count
Debug.Print rng.Cells(j).Address
Next j
However, in non-contiguous ranges it appears that this is equivalent (or shorthand for)
For j = 1 To rng.Cells.Count
Debug.Print rng.Areas(1).Cells(j).Address
Next j
There doesn't appear to be any direct mention of this in the documentation but it is a sensible conclusion to draw by looking in the Locals browser of the VBA editor.
In the range object rng, there is a Cells property which only contains one "Item", which is the first Area. So it's reasonable to assume this one item is what .Cells(j) has access to.
In rng we can also see the Areas property, which contains 2 items (in this example) equal to the number of Areas in my non-contiguous range.
So rng.Cells(j) is accessing the jth element within the first area of rng. Because .Cells() can extend past the original size of rng, we see the addresses listed of cells outside rng.
The solution(s):
Either ensure you directly loop through the range objects within rng using a For Each loop as shown in the question.
Or loop over each area, and then each cell within that area.
The first option is more concise, but Shai points out that to be completely sure, the most robust method is to do the two For Each loops as there may be more complicated edge cases which aren't captured with the single loop.

Related

How can I test if a selection is completely within a range?

So i have found this which is similar:
VBA test if cell is in a range
but this seems to be testing (as I understand it) if the cells selected intersect the range at all. However I need to find a way to confirm if the selected range is COMPLETELY within the range so that I can restrict the macro to only work inside a specified range of cells.
here is what I've got so far....I name the selected cells as a range (sel_rng) and set them as a variable....then I name the acceptable range as a named range (okay_rng)....then (hopefully....but this is the part I'm still unclear how to pull off) if "sel_rng" lies completely within "okay_rng" I want to grab "sel_rng" and merge it, otherwise throw up an error"
Sub Merge_Cells()
'
' Merge_Cells Macro
Dim selcells As Range
Selection.Name = "sel_rng"
selcells = Range("sel_rng")
Dim okayrng As Integer
okayrng = Range("itemrows").Value + 28
ActiveSheet.Range("C29:C" & okayrng).Select
Selection.Name = "okay_rng"
Range("sel_rng").Select
Selection.Merge
Thoughts anyone?
The intersection of the two ranges will determine if one range is completely within another range.
dim rng1 as range, rng2 as range
set rng1 = range("b2:c3")
set rng2 = range("a1:d4")
'if rng1 is completely within rng2, the intersection's address will be the same as rng1's address
if application.intersect(rng1, rng2).address = rng1.address then
debug.print rng1.address(0, 0) & " is within " & rng2.address(0, 0)
end if
btw, there is the possibility that the intersect could be nothing. You should add error handling for that.

Select cells between bold cells using a loop

I am working with data where the only consistency is the layout and the bold headings to distinguish between a new date.
I am trying to find the cells in between these cells in bold, find the value "Individual" (in column A) in the selected rows, then sum the values of the given rows in column D (as there can be more then 1 row with "Individual"), and copy this new value to a different cell.
Since the cells between the bold is one date, if the value is not there, the output cell needs to shift down one without filling in anything.
Here is what I have so far:
Sub SelectBetween()
Dim findrow As Long, findrow2 As Long
findrow = range("A:A").Find("test1", range("A1")).Row
findrow2 = range("A:A").Find("test2", range("A" & findrow)).Row
range("A" & findrow + 1 & ":A" & findrow2 - 1).Select
Selection.Find("Individual").Activate
range("D" & (ActiveCell.Row)).Select
Selection.copy
sheets("Mix of Business").Select
range("C4").Select
ActiveSheet.Paste
Exit Sub
errhandler:
MsgBox "No Cells containing specified text found"
End Sub
How can I loop through the data and each time it loops through a range, no matter if it finds the value (e.g. individual) or not, shifts down one row on the output cell? Also, how can I change the findrow to be a format (Bold) rather then a value?
Here is some data for reference:
This is what I am trying to get it to look like:
So you have a good start to trying to work through your data. I have a few tips to share that can hopefully help get you closer. (And please come back and ask more questions as you work through it!)
First and foremost, try to avoid using Select or Activate in your code. When you look at a recorded macro, I know that's all you see. BUT that is a recording of your keystrokes and mouseclicks (selecting and activating). You can access the data in a cell or a range without it (see my example below).
In order to approach your data, your first issue is to figure out where your data set starts (which row) and where it ends. Generally, your data is between cells with BOLD data. The exception is the last data set, which just has a many blank rows (until the end of the column). So I've created a function that starts at a given row and checks each row below it to find either a BOLD cell or the end of the data.
Private Function EndRowOfDataSet(ByRef ws As Worksheet, _
ByVal startRow As Long, _
Optional maxRowsInDataSet As Long = 50) As Long
'--- checks each row below the starting row for either a BOLD cell
' or, if no BOLD cells are detected, returns the last row of data
Dim checkCell As Range
Set checkCell = ws.Cells(startRow, 1) 'assumes column "A"
Dim i As Long
For i = startRow To maxRowsInDataSet
If ws.Cells(startRow, 1).Font.Bold Then
EndRowOfDataSet = i - 1
Exit Function
End If
Next i
'--- if we make it here, we haven't found a BOLD cell, so
' find the last row of data
EndRowOfDataSet = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
End Function
To show you how to use that with your specific data, I've created a test subroutine indicating how to loop through all the different data sets:
Option Explicit
Public Sub DataBetween()
Dim thisWB As Workbook
Dim dataWS As Worksheet
Set thisWB = ThisWorkbook
Set dataWS = thisWB.Sheets("YourNameOfSheetWithData")
'--- find the first bold cell...
'Dim nextBoldCell As Range
'Set nextBoldCell = FindNextBoldInColumn(dataWS.Range("A1"))
'--- now note the start of the data and find the next bold cell
Dim startOfDataRow As Long
Dim endOfDataRow As Long
Dim lastRowOfAllData As Long
startOfDataRow = 3
lastRowOfAllData = dataWS.Cells(ws.Rows.Count, "A").End(xlUp).Row
'--- this loop is for all the data sets...
Loop
endOfDataRow = EndRowOfDataSet(dataWS, startOfDataRow)
'--- this loop is to work through one data set
For i = startOfDataRow To endOfDataRow
'--- work through each of the data rows and copy your
' data over to the other sheet here
Next i
startOfDataRow = endOfDataRow + 1
Do While endOfDataRow < lastRowOfAllData
End Sub
Use both of those together and see if that can get you closer to a full solution.
EDIT: I should have deleted that section of code. It was from an earlier concept I had that didn't completely work. I commented out those lines (for the sake of later clarity in reading the comments). Below, I'll include the function and why it didn't completely work for this situation.
So here's the function in question:
Public Function FindNextBoldInColumn(ByRef startCell As Range, _
Optional columnNumber As Long = 1) As Range
'--- beginning at the startCell row, this function check each
' lower row in the same column and stops when it encounters
' a BOLD font setting
Dim checkCell As Range
Set checkCell = startCell
Do While Not checkCell.Font.Bold
Set checkCell = checkCell.Offset(1, 0)
If checkCell.Row = checkCell.Parent.Rows.Count Then
'--- we've reached the end of the column, so
' return nothing
Set FindNextBoldInColumn = Nothing
Exit Function
End If
Loop
Set FindNextBoldInColumn = checkCell
End Function
Now, while this function works perfectly well, the situation is DOES NOT account for is the end of the last data set. In other words, a situation like this:
The function FindNextBoldInColumn will return nothing in this case and not the end of the data. So I (should have completely) deleted that function and replaced it with EndRowOfDataSet which does exactly what you need. Sorry about that.

Setting variables VBA

complete novice here
I started some VBA a few days ago, I have simple question but cant seem to find what I am doing wrong.
I am trying to make a button which will take the coordinates of the active cell and compare them to another worksheet to retrieve a specific value from another table.
I set variables to the active cell column and row, I want to do this so I can later compare these locations to another worksheet and get the value at a specified position on another worksheet.
So far I have written simply what I could find on the internet as I have no formal training.
The msgbox at the end is just to test whether or not it actually picks up the reference.
Sub CommandButton1_Click()
Dim Arow As Range
Dim Acol As Range
Set Arow = Worksheets("Sheet1").Range(ActiveCell.Row)
Set Acol = Worksheets("Sheet1").Range(ActiveCell.Column)
MsgBox (Arow)
End Sub
So far I have error run-time error '1004' Application defined or object defined error highlighting the 4th Row. If anyone could help me solve this or redirect me to some help it would be much appreciated.
I think this won't work, you should put there
Set arow = Worksheets("Sheet1").Range(ActiveCell.Row & ":" & ActiveCell.Row)
Putting there simply number won't work. For the column, you should put there somethong like C:C. For getting letter of column, see this qestion: Function to convert column number to letter?
For more information about Range property, please see official documentation https://msdn.microsoft.com/en-us/library/office/ff836512.aspx.
The thing is, that you have to supply either the address in so called A1 reference, which is "A1", or "$A$1" or name of cell, etc, or you have to supply two Range objects, such as two cells Worksheets("Sheet1").Range(Worksheets("Sheet1").Cells(1,1), Worksheets("Sheet1").Cells(2,2)), which defines area starting with upper-left corner in first parameter and lower right in second parameter.
ActiveCell.Row and ActiveCell.Column returns you some Integer value representing number of row and column, i.e. if you point cell B4, ActiveCell.Row would return 4, and ActiveCell.Column gonna return 2. An Range() property need as an argument whole adress for some range, i.e. Range("C6") or Range("G3:J8").
When you have your column as a number, you can use Cells() property for pointing first and last cell in your range, i.e. Range(Cells(2, 4), Cells(6, 8) would be the same range as Range("D2:H6").
Following this, one of the ways that you can do what you have described is:
Sub CommandButton1_Click()
Dim Rng As Range
Set Rng = Worksheets("Sheet1").Cells(ActiveCell.Row, ActiveCell.Column)
End Sub
Now you have under variable Rng an Range of the same coordinates as ActiveCell, but in Sheet1. You can pass some value into i.e Rng.Value = "Hello World", paste something with Rng.PasteSpecial xlPasteAll etc.
if you want the value from other sheet at the same location as activeCell, use this code,
Private Sub CommandButton1_Click()
valueFromOtherSheet = Sheets("Sheet2").Range(ActiveCell.Address)
MsgBox (valueFromOtherSheet)
End Sub
Like the others have said, it's just about knowing your variable types. This is another way you could achieve what you want
Sub CommandButton1_Click()
Dim Acell As Range
Set Acell = Worksheets("Sheet2").Range(ActiveCell.Address)
MsgBox "Value on ActiveSheet: " & ActiveCell.Value & vbNewLine & _
"Value on Sheet2: " & Acell.Value
End Sub
Thank you everyone for the help and clarification, In the end I was able to come up with some code that seems to do what I need it to.
Private Sub CommandButton1_Click()
Dim cabDate As Range
Dim searchCol As Integer
Dim newindex As Range
Set cabDate = WorksheetFunction.Index(Range("A1:O9999"), ActiveCell.Row, 2)
searchCol = ActiveCell.Column
Set newindex = WorksheetFunction.Index(Worksheets("Deadlines").Range("A1:O9999"), cabDate.Row, searchCol)
MsgBox (newindex)
End Sub
I wasn't aware about conflicting data types so thank you all for the assistance.

Find and FindNext for Excel VBA

I've been stuck trying to figure out what to do with this, but basically I want a way to print out the value in column B given a specific value that matches column A. So for example:
Column A Column B
1 ABC
2 DEF
3 GHI
1 JKL
I want to, after using find/findnext or whatever it is, to print out this string:
ABC JKL
I tried using
Set cellFound = ActiveWorkbook.Worksheets("sheet1").Range("F1:F1000000").Find("1")
string = cellFound.Offset(0, 1).value
And I have a loop to loop through as many time as it needs to get all the rows taken care of. But with find it keeps returning me the same first string ("ABC") and the string ends up being ABC ABC instead of ABC JKL
I tried using FindNext instead of find, but what I got is a 1004 Error. So I'm not really sure where I'm doing this wrong. Anyone has any idea?
You don't need FindNext if you start each Find after the previous one:
Sub qwerty()
Dim rFirst As Range, r As Range
Dim A As Range
Set A = Range("A:A")
Do
If rFirst Is Nothing Then
Set rFirst = A.Find(What:=1, After:=A(1))
Set r = rFirst
Else
Set r = A.Find(What:=1, After:=r)
If r.Address = rFirst.Address Then Exit Do
End If
MyString = MyString & " " & r.Offset(0, 1)
Loop
MsgBox MyString
End Sub
You need to call Find once, and then successively FindNext. But there are a couple of non-obvious things:
Each time you call FindNext, the search will start again from the upper-left corner of the range; unless you pass in the current found cell.
The search will wrap around (up or down, depending on your search direction. You need to write code that handles this possibility.
The minimal working code would look something like this:
Dim rng As Excel.Range
Set rng = ActiveWorkbook.Worksheets("sheet1").Range("F1:F1000000")
Dim lastRow as Integer
Set cellFound = rng.Find("1")
Do While Not cellFound Is Nothing
' handles wraparound
If cellFound.Row < lastRow Then Exit Do
string = cellFound.Offset(0, 1).Value
' do something here with string
Set cellFound = rng.FindNext(cellFound)
Loop
Reference:
Find method
FindNext method
When using the Range.FindNext method, one need just include some reference to the initial find position. For example, I recorded this macro using excel; while I'm not a fan of using selection and activate, I think it helps to understand how the method functions:
Sub Using_Find()
Selection.Find(What:="my search string here", After:=ActiveCell _
, LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, _
SearchDirection:=xlNext, MatchCase:=False, SearchFormat:=False).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
Selection.FindNext(After:=ActiveCell).Activate
End Sub
To generate this subroutine, I used the record > macro in excel, then selected Home > Find & Select > Find.
The way I see this subroutine working is:
Step #1: Find the first location of the string, activate it;
Step #2: FindNext looks after the active cell that we just activated, finds the next location of the string, then activates it;
Etc. etc. So, the observation here is that the .FindNext method needs some reference to the prior find cell (which the first answer accomplishes by manually identifying it as a unique reference). I'm not saying anything to that answer, it works just as well. My goal was to help provide some insight into the Range.FindNext method.
Some other points worth mentioning:
Range.FindNext will return a Range object. (Microsoft)
The After parameter is described as:
"The cell after which you want to search. This corresponds to the position of the active cell when a search is done from the user interface. Be aware that After must be a single cell in the range. Remember that the search begins after this cell; the specified cell is not searched until the method wraps back around to this cell. If this argument is not specified, the search starts after the cell in the upper-left corner of the range." (Microsoft)
...and
Under the Remarks section, Microsoft notes that, "The search will wrap around to the beginning of the range." They suggest to save the first address and do a check against it for each subsequent .FindNext. This way, once the method does wrap around, it will check the address against the first and end the check.
So, modeling the Range.FindNext Method provided by Microsoft, I wrote this introductory subroutine for review:
Sub USING_FIND()
'this line sets the range to our used range on the active sheet
With ActiveSheet.UsedRange
'setting c variable to .Find method, where the first value is what we're looking for,
'i.e. "1"; LookIn:= can be changed to our needs but set currently to xlValues
Set c = .Find(1, LookIn:=xlValues)
'begin first conditional; this conditional checks c (our .Find method) to see if it has
'some reference, then sets the address to a constant 'firstAddress' so we can check it
'against the .FindNext returns later to prevent endless loop
If Not c Is Nothing Then
firstAddress = c.Address
'Do...is where we place our "work"; this can be a redirect to another function/sub, etc
'for now I've just tossed a msgbox as a placeholder that returns the offset 1 column over
Do
MsgBox c.Offset(0, 1)
'Now we set c to the .FindNext method, using the original .Find method as the 'after'
Set c = .FindNext(c)
'Another empty reference check/exit as a conditional
If c Is Nothing Then
GoTo DoneFinding
'ends the empty reference conditional
End If
'using our .FindNext method that we replaced 'c' with earlier, we can now loop through
'the remainder of the value returns. The Loop While 'c.Address <> firstAddress' sentence
'is checking that each subsequent .FindNext address IS NOT the first address;
'-our loop will return to the 'Do' sentence to repeat the loop, starting on the
'MsgBox c.Offset(0,1) sentence with the next string occurence
'-the characters '<>' means 'does not equal'; i.e. the opposite of '='
Loop While c.Address <> firstAddress
'this ends the address check loop
End If
DoneFinding:
End With
End Sub
To adjust this code to your specific needs, we can change the sentence after the Do line: 'MsgBox c.Offset(0,1)' to our specific needs.
Depending on how complex your output needs are, you can add all occurrences to an array, then have the array output the values in order of how you want to see them. This can be done by redim array and preserve each return. Once the .Find loop completes, open a new workbook with the Workbooks.Open method, and run a quick loop that takes each array value and places it in the order that you prefer.
Another option is to 'print' to .txt. Open a new .txt as #1, then 'print' accordingly. This can also be done as a second subroutine via the array option suggested previously.
Hope this helps add some context to your initial question with respect to the .FindNext method, as well as provides some ideas for future direction/implementation. Good luck!
Microsoft page on Range.FindNext Method:
https://msdn.microsoft.com/en-us/VBA/Excel-VBA/articles/range-findnext-method-excel
Function FindMultiResut(ByRef What As String, _
ByRef FindRng As Range, _
ByRef OutputRng As Range, _
ByRef Delimite As String)
Dim fRng As Range
Dim Rng1 As Range
Dim Rng2 As Range
Dim temp As String
Set fRng = FindRng
Do
If Rng1 Is Nothing Then
Set Rng1 = fRng.Find(What:=What, After:=fRng(1))
Set Rng2 = Rng1
Else
Set Rng2 = fRng.Find(What:=What, After:=Rng2)
If Rng2.Address = Rng1.Address Then Exit Do
End If
If OutputRng.Worksheet.Cells(Rng2.Row, OutputRng.Column) <> Empty Then
temp = temp & OutputRng.Worksheet.Cells(Rng2.Row, OutputRng.Column) & Delimite
End If
Loop
FindMultiResut = Left(temp, Len(temp) - 1)
End Function
Here is an implementation of the suggestion I made in my comment under your question.
Function RowBeforeLast(ByVal What As Variant) As Long
Dim Fnd As Range
Set Fnd = Range("E:E").Find(What:=What, After:=Range("E1"), _
LookAt:=xlWhole, _
Searchdirection:=xlPrevious)
If Not Fnd Is Nothing Then
Set Fnd = Range("E:E").Find(What:=What, After:=Fnd, _
LookAt:=xlWhole, _
Searchdirection:=xlPrevious)
If Not Fnd Is Nothing Then RowBeforeLast = Fnd.Row
End If
End Function
It's designed as a UDF so that you can call it from the worksheet with a worksheet function like =RowBeforeLast(E5). You can also call it with code like
Private Sub TestGet()
RowBeforeLast "GR 3"
End Sub
Either way it will return the row number in which the search criterium was found for the second time from the bottom of the column. If there is only one or no occurrance the function will return zero.

Type Mismatch Error after MsgBox

my data is as below .
Updated Question
Sub Solution()
Dim shData As Worksheet
Set shData = Sheets("Sheet1") 'or other reference to data sheet
Dim coll As Collection, r As Range, j As Long
Dim myArr As Variant
Dim shNew As Worksheet
shData.Activate
'get unique values based on Excel features
Range("a1").AutoFilter
Set coll = New Collection
On Error Resume Next
For Each r In Range("A1:A10")
coll.Add r.Value, r.Value
Next r
On Error GoTo 0
'Debug.Print coll.Count
For j = 1 To coll.Count
MsgBox coll(j)
myArr = coll(j)
Next j
Range("a1").AutoFilter
Dim i As Long
For i = 0 To UBound(myArr)
shData.Range("$A$1").AutoFilter Field:=1, Criteria1:=myArr(i), _
Operator:=xlAnd
On Error Resume Next
Sheets(myArr(i)).Range("A1").CurrentRegion.ClearContents
If Err.Number = 0 Then
Range("A1").CurrentRegion.Copy Sheets(myArr(i)).Range("A1")
Else
Set shNew = Sheets.Add(After:=Sheets(Sheets.Count))
shData.Range("A1").CurrentRegion.Copy shNew.Range("A1")
shNew.Name = myArr(i)
Err.Clear
End If
Next i
'removing filter in master sheet
shData.Range("a1").AutoFilter
End Sub
When I run above macro I don't know why it is giving Type Mismatch Error after MsgBox coll(j) , simply I want to store data in Array and I'm passing that data , Here I am using For Each r In Range("A1:A10") Where A10 length is static how can I find last written column?
When you add something to collection the key needs to be a string so use:
coll.Add r.Value, CStr(r.Value)
instead of:
coll.Add r.Value, r.Value
You are still assigning coll(j) to a Variant which is not an array.
You need to:
ReDim myArr(1 to coll.Count)
Before your for loop and then in the loop:
myArr(j) = coll(j)
Before attempting to respond to this question, I would like to write what I believe you are trying to accomplish; when you confirm this is what you are trying to do, I will try to help you get working code to achieve it. This would normally be done with comments, but the threads of comments so far are a bit disjointed, and the code is quite complex...
You have data in a sheet (called "sheet1" - it might be something else though)
The first column contains certain values that might be repeated
You don't know how many columns there might be... you would like to know that though
You attempt to find each unique value in column A (call it the "key value"), and display it (one at a time) in a message box. This looks more like a debug step than actual functionality for the final program.
You then turn on the autofilter on column A; selecting only rows that match a certain value
Using that same value as the name of a sheet, you see if such a sheet exists: if it does, you clear its contents; if it does not, then you create it at the end of the workbook (and give it the name of the key)
You select all rows with the same (key) value in column A on sheet1, and copy them to the sheet whose name is equal to the value in column A that you filtered on
You want to repeat step 5-8 for each of the unique (key) values in column A
When all is done, I believe you have (at least) one more sheet than you had key values in column A (you also have the initial data sheet); however you do not delete any "superfluous" sheets (with other names). Each sheet will have only rows of data corresponding to the current contents of sheet1 (any earlier data was deleted).
During the operation you turn autofiltering on and off; you want to end up with auto filter disabled.
Please confirm that this is indeed what you are attempting to do. If you could give an idea of the format of the values in column A, that would be helpful. I suspect that some things could be done rather more efficiently than you are currently doing them. Finally I do wonder whether the whole purpose of organizing your data in this way might be to organize the data in a specific way, and maybe do further calculations / graphs etc. There are all kinds of functions built in to excel (VBA) to make the job of data extraction easier - it's rare that this kind of data rearranging is necessary to get a particular job done. If you would care to comment on that...
The following code does all the above. Note the use for For Each, and functions / subroutines to take care of certain tasks (unique, createOrClear, and worksheetExists). This makes the top level code much easier to read and understand. Also note that the error trapping is confined to just a small section where we check if a worksheet exists - for me it ran without problems; if any errors occur, just let me know what was in the worksheet since that might affect what happens (for example, if a cell in column A contains a character not allowed in a sheet name, like /\! etc. Also note that your code was deleting "CurrentRegion". Depending on what you are trying to achieve, "UsedRange" might be better...
Option Explicit
Sub Solution()
Dim shData As Worksheet
Dim nameRange As Range
Dim r As Range, c As Range, A1c As Range, s As String
Dim uniqueNames As Variant, v As Variant
Set shData = Sheets("Sheet1") ' sheet with source data
Set A1c = shData.[A1] ' first cell of data range - referred to a lot...
Set nameRange = Range(A1c, A1c.End(xlDown)) ' find all the contiguous cells in the range
' find the unique values: using custom function
' omit second parameter to suppress dialog
uniqueNames = unique(nameRange, True)
Application.ScreenUpdating = False ' no need for flashing screen...
' check if sheet with each name exists, or create it:
createOrClear uniqueNames
' filter on each value in turn, and copy to corresponding sheet:
For Each v In uniqueNames
A1c.AutoFilter Field:=1, Criteria1:=v, _
Operator:=xlAnd
A1c.CurrentRegion.Copy Sheets(v).[A1]
Next v
' turn auto filter off
A1c.AutoFilter
' and screen updating on
Application.ScreenUpdating = True
End Sub
Function unique(r As Range, Optional show)
' return a variant array containing unique values in range
' optionally present dialog with values found
' inspired by http://stackoverflow.com/questions/3017852/vba-get-unique-values-from-array
Dim d As Object
Dim c As Range
Dim s As String
Dim v As Variant
If IsMissing(show) Then show = False
Set d = CreateObject("Scripting.Dictionary")
' dictionary object will create unique keys
' have to make it case-insensitive
' as sheet names and autofilter are case insensitive
For Each c In r
d(LCase("" & c.Value)) = c.Value
Next c
' the Keys() contain unique values:
unique = d.Keys()
' optionally, show results:
If show Then
' for debug, show the list of unique elements:
s = ""
For Each v In d.Keys
s = s & vbNewLine & v
Next v
MsgBox "unique elements: " & s
End If
End Function
Sub createOrClear(names)
Dim n As Variant
Dim s As String
Dim NewSheet As Worksheet
' loop through list: add new sheets, or delete content
For Each n In names
s = "" & n ' convert to string
If worksheetExists(s) Then
Sheets(s).[A1].CurrentRegion.Clear ' UsedRange might be better...?
Else
With ActiveWorkbook.Sheets
Set NewSheet = .Add(after:=Sheets(.Count))
NewSheet.Name = s
End With
End If
Next n
End Sub
Function worksheetExists(wsName)
' adapted from http://www.mrexcel.com/forum/excel-questions/3228-visual-basic-applications-check-if-worksheet-exists.html
worksheetExists = False
On Error Resume Next
worksheetExists = (Sheets(wsName).Name <> "")
On Error GoTo 0
End Function