I have been working on a macro that summarizes the data from multiple sheets in my workbook. In order to know which columns to use in my summary sheet I need to first extract all the unique values from the first column in my sheets.
The idea is that it will loop through the sheets and define a range, then it will loop through each cell in the range, check if the value of that cell is already in the array and if not copy and paste it and add it to the array.
Unfortunately I get an the error "Index outside of valid Area" for the line that is supposed to add the cell value to the array.
ReDim Preserve uniqueVal(1 To UBound(uniqueVal) + 1) As Variant
I took that specific code from the question https://superuser.com/questions/808798/excel-vba-adding-an-element-to-the-end-of-an-array .
Here is the entire code for reference.
Private Sub CommandButton24_Click()
Dim xSheet As Worksheet
Dim DestSh As Worksheet
Dim Last As Long
Dim copyRng As Range
Dim destRng As Range
Dim cRange As Range
Dim c As Range
Dim uniqueVal() As Variant
With Application
.ScreenUpdating = False
.EnableEvents = False
End With
'Delete the summary worksheet if it exists.
Application.DisplayAlerts = False
On Error Resume Next
ActiveWorkbook.Worksheets("Summary").Delete
On Error GoTo 0
Application.DisplayAlerts = True
' Add a worksheet with the name "Summary"
Set DestSh = ActiveWorkbook.Worksheets.Add
DestSh.Name = "Summary"
Set destRng = DestSh.Range("A1")
'Define inital array values
uniqueVal = Array("Account by Type", "Total")
' Loop through all worksheets and copy the data to the
' summary worksheet.
For Each xSheet In ActiveWorkbook.Worksheets
If InStr(1, xSheet.Name, "ACCOUNT") And xSheet.Range("B1") <> "No Summary Available" Then _
Set copyRng = xSheet.Range("A:A")
For Each c In copyRng.SpecialCells(xlCellTypeVisible)
If Len(c) <> 0 And Not ISIN(c, uniqueVal) Then _
'Copy to destination Range
c.Copy destRng
'move destination Range
Set destRng = destRng.Offset(0, 1)
'change / adjust the size of array
ReDim Preserve uniqueVal(1 To UBound(uniqueVal) + 1) As Variant
'add value on the end of the array
uniqueVal(UBound(uniqueVal)) = c.Value
End If
Next c
End If
Next xSheet
ExitTheSub:
Application.Goto DestSh.Cells(1)
With Application
.ScreenUpdating = True
.EnableEvents = True
End With
End Sub
Per default, arrays in Excel VBA start with the index 0, not the index 1. You can test this by checking your arrays contents: your first string "Account by Type" should be on uniqueval(0) rather than on uniqueval(1).
Two ways to solve this:
add Option Base 1 to the top of your module or
change ReDim Preserve uniqueval(1 To UBound(uniqueval) + 1) to ReDim Preserve uniqueval(0 To UBound(uniqueval) + 1)
It's up to you which one you chose, but imo the latter is cleaner, since you don't have to fiddle with array options on module level.
As I see it, you're not actually using the arrays' contents yet. If you do later on, just loop For i = LBound(uniqueval) To UBound(uniqueval) - in which case it is irrelevant with what option you went.
On the first loop uniqueVal has no Ubound. That's why it fails. So, you should first Redim it as Redim uniqueVal(1 To 1), then write to the Ubound and increase the size thereafter. That would always leave you with a blank element at the top which you can remove at the end.
The better (because it runs faster) is to Dim uniqueVal to a possible max number, then set the current index with a counter, like i = i + 1, and do a Redim Preserve uniqueVal(i) at the end, thereby cutting off all unused elements.
The underscore at the end of a line of code means that the line is continued, logically, in the next line. For example,
If 1 <> 2 Then _
Debug.Print "All is well"
This is the same as If 1 <> 2 Then Debug.Print "All is well"
Observe, however, that there is no End If. If there were more than one command to follow the Then you must use End If, for example,
If 1 <> 2 Then
Debug.Print "All is well"
A = 3
End If
Here, everything between If and End If will only be executed if 1 <> 2. This is the case with If Len(c) <> 0 And Not ISIN(c, uniqueVal) Then _. Once the error of the UBound is cured this one will stop your code from running. Remove the underscore following the Then.
Related
I am trying to write a process that compares strings and deletes the duplicate string within a given column using a selection as the top and bottom constraints.
Most of the process of checking and deleting works however I am having trouble with moving the cell contents up a cell after the duplicate string was deleted.
Image showing how the script should work
Red outline is the loop that selects the String to compare against.
Green outline is the loop that finds, deletes and moves the cells up one.
Blue outline is the Selection.
Stage 1 is to find and compare two strings that are the same.
Stage 2 is to delete the string that is the same as the first string.
Stage 3 is to move everything under the deleted cell with the deleted string up one row so that there is no empty cell.
I'm having problems with stage 3. I don't know how to move all data in those cells up one row without using a loop and I can't use the selection.
Here is the code so far:
Private Sub Tabeller()
Dim vRngMv As Variant
Dim iRowChsr1, iRowChsr2, iRowTtl, iI As Integer
Dim vRowIn, vRowComp As String
Dim oRngSlct, oRngMv As Range: Dim ws As Worksheet: Dim oBS As Object
'Newer Version will get rid of Selection as range determination
'Why does oRngSlct become a Variant/Object/Range here and oRngMv stays a Range object?
'I dont use it, kept it in to ask the question.
Set oRngMv = Selection: Set oRngSlct = Selection
iRowTtl = oRngSlct.Rows.Count
'First Loop For holding target cell data for comparison
For iRowChsr1 = 1 To iRowTtl
'Chooses target cell and string
vRowIn = oRngSlct(iRowChsr1, 1)
'Second loop for Seeking a matching String
For iRowChsr2 = 1 To iRowTtl
'Check to not pick itself
If iRowChsr1 = iRowChsr2 Then
'Offsets Counter by 1 if it enocunters itself
iRowChsr2 = iRowChsr2 + 1
Else
'Sets comparison string
vRowComp = oRngSlct(iRowChsr2, 1)
'String comparison
iI = StrComp(vRowIn, vRowComp, 1)
'If strings are equal
If iI = 0 Then
'Deletes; I know this is redundant but its here for clarity
oRngSlct(iRowChsr2, 1) = ""
'Offsets by iRowChsr by 1
iRowChsr2 = iRowChsr2 + 1
'Create Variant with proper range, it just has to be translated into something that excel can move.
vRngMv = Range((oRngSlct(iRowChsr2, 1)), (oRngSlct(iRowTtl, 1)))
Set oRngMv = Range 'I know this doesnt work
'Offsets back to original Position of Deleted cell
iRowChsr2 = iRowChsr2 - 1
'*******************************
'*Cuts and pastes or moves here*
'*******************************
End If
End If
'Next Comparison String
Next iRowChsr2
'Next target String
Next iRowChsr1
End Sub
Unique (Remove Duplicates)
You could rather use one of the following.
The first solution will leave error values and blanks as part of the resulting data, while the second one will remove them.
The Code
Option Explicit
Sub removeDupesColumnSelection()
' Validate Selection.
If TypeName(Selection) <> "Range" Then Exit Sub
' Remove duplicates.
Selection.Columns(1).RemoveDuplicates Array(1), xlNo
End Sub
Sub uniquifyColumnSelection()
' Validate Selection.
If TypeName(Selection) <> "Range" Then Exit Sub
' Write values from first column of Selection to Data Array.
Dim rg As Range: Set rg = Selection.Columns(1)
Dim rCount As Long: rCount = rg.Rows.Count
Dim Data As Variant
If rCount > 1 Then
Data = rg.Value
Else
ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
End If
' In Unique Dictionary...
With CreateObject("Scripting.Dictionary")
.CompareMode = vbTextCompare
' Write unique values from Data Array to Unique Dictionary.
Dim Key As Variant
Dim i As Long
For i = 1 To rCount
Key = Data(i, 1)
If Not IsError(Key) Then
If Len(Key) > 0 Then
.Item(Key) = Empty
End If
End If
Next i
ReDim Data(1 To rCount, 1 To 1)
If .Count > 1 Then
' Write values from Unique Dictionary to Data Array.
i = 0
For Each Key In .Keys
i = i + 1
Data(i, 1) = Key
Next Key
End If
End With
' Write values from Data Array to Destination Range.
rg.Value = Data
End Sub
I am trying to set up a subroutine that protects and hides worksheets labeled as "restricted" and only protect worksheets labeled as "read only", as specified by the user in certain cells (see the image attached where the user would specify the status for each sheet). The code seems to work without a problem, only for the restricted portion of the sheets. However, as soon as I add the condition to check for the read only sheets, I get the subscript out of range error for the line marked by **, but the weird part is that its for the fourth element of the array always, so not sure why the first 3 work fine, or why adding the second condition stops it from working. If the "Case Read Only" and condition lines are taken out, it works fine. Maybe I am missing something obvious, I am fairly new to VBA as you can see by what is likely my inefficient code. Any help is very appreciated!
User table for selecting restricted and read only sheets
Private Sub Test()
Dim NumberOfSheets As Integer 'Variable for counting the sheets in model
NumberOfSheets = Application.Sheets.Count - 2 'Number of Sheets ignores Master Cmd sheet, and array starts at 0 so 2 is subtracted
Dim iCounter As Integer 'Counter for looping through the array
ReDim CheckWorksheets(NumberOfSheets) As String 'Restricted worksheet name variable declaration
Worksheets("Master Cmd").Activate 'Activate Master Cmd
Range("C7").Activate 'Activate first cell with sheet name
For iCounter = 0 To NumberOfSheets 'Loop to cycle through worksheet names
CheckWorksheets(iCounter) = ActiveCell.Offset(iCounter, 0) 'Setting array variable equal to worksheet name
Next iCounter
For iCounter = 0 To NumberOfSheets 'Loop to change restricted worksheets status to very hidden
Select Case ActiveCell.Offset(iCounter, 2)
Case "Restricted"
Worksheets(CheckWorksheets(iCounter)).Protect password:=AdminPassword 'Protecting sheets
Worksheets(CheckWorksheets(iCounter)).Visible = xlSheetVeryHidden 'Condition to see if status is restricted in Master Cmd sheet
Case "Read Only"
**Worksheets(CheckWorksheets(iCounter)).Protect password:=AdminPassword**
End Select
Next iCounter
End Sub
I would ditch the array. Untested:
Private Sub Test()
Const VAL_RESTRICTED = "Restricted"
Dim wb As Workbook, c As Range
Set wb = ThisWorkbook '? or ActiveWorkbook ?
For Each c In wb.Worksheets("Master Cmd").Range("C7").Resize(wb.Worksheets.Count - 1, 1).Cells
With wb.Worksheets(c.Value)
.Protect Password:=AdminPassword 'Protecting sheets
If c.Offset(0, 2).Value = VAL_RESTRICTED Then .Visible = xlSheetVeryHidden
End With
Next c
End Sub
Edit: to debug the original problem you could modify your code slightly.
Dim shtName
'...
'...
For iCounter = 0 To NumberOfSheets
shtName = CheckWorksheets(iCounter)
Debug.Print iCounter, shtName
Select Case ActiveCell.Offset(iCounter, 2)
Case "Restricted"
Worksheets(shtName).Protect password:=AdminPassword
Worksheets(shtName).Visible = xlSheetVeryHidden
Case "Read Only"
Worksheets(shtName).Protect password:=AdminPassword
End Select
Next iCounter
Note also it's good practice to explcitly qualify your Worksheets with a specific workbook. In this case it should be ThisWorkbook.Worksheets()
I have a problem with inserting data to the array. In the program I search all cells with "Data:" value. If this value appear I jump to the cell on the right and mark it. I want to collect all marked values (all of them are dates) in the array but with my code (enclosed below) I have an error. I have tried ReDim and setting an exact number of objects in the array. I would be grateful for a help.
Sub CheckData()
Dim FindIt As Range
Dim EndIt As Range
Dim StartAddress As String
With Range("A1:A100")
Set EndIt = .Cells(.Cells.Count)
End With
Set FindIt = Range("A1:A100").Find(what:="Data:", after:=EndIt)
If Not FindIt Is Nothing Then
StartAddress = FindIt.Address
End If
Dim Tabel() As Variant
Tabel = Array()
i = 0
Do Until FindIt Is Nothing
Set FindIt = Range("A1:A100").FindNext(after:=FindIt)
Data = FindIt.Address
Range(Data).Select
ActiveCell.Offset(0, 1).Select
ActiveCell.Interior.ColorIndex = 6
'Debug.Print ActiveCell.Value
Tabel(i) = ActiveCell.Value
i = i + 1
'Debug.Print i
If FindIt.Address = StartAddress Then
Exit Do
End If
Loop
End Sub
You never sized your array.
Dim Tabel() As Variant
Use ReDim to resize a dynamically-sized array.
ReDim Preserve Tabel(0 To UBound(Tabel)+1)
However that's a terribly inefficient thing to do in a loop (you're copying the same elements over and over and over and over again, at every single iteration).
Rule of thumb, if you don't know from the start how many elements you're going to need, it's probably best to use a Collection instead of an array.
Dim items As Collection
Set items = New Collection
'...
items.Add ActiveCell.Value
You could also use a for loop instead of a find (also using Mat's Mug idea about collections)
Sub CheckData1()
Dim ws As Worksheet
Dim searchRng As Range
Dim cell As Range
Dim tabel As Collection 'incorrectly spelt table?
Set ws = ActiveSheet
Set tabel = New Collection
Set searchRng = ws.Range("A1:A100")
For Each cell In searchRng.Cells
If cell.Value = "Data:" Then
tabel.Add cell.Offset(, 1)
cell.Offset(, 1).Interior.ColorIndex = 6 'If you still need it highlighted
End If
Next
End Sub
Common problem, I have worked through all answers I've found and finally got it almost working.
I have a list of discount options, let's call them named range F, down 1 column.
User filters out the discounts they don't want to apply.
I need to unfilter, do work, and refilter as the user selected.
I create an array with only visible cells, by loop and union of ranges. This works correctly, but generates a non-contiguous array usually.
When I run this, I don't get an error. However, the entry below the break in the contiguous array is not refiltered.
Just realised it's the transpose that doesn't like non-contiguous arrays - still need assistance and doubtless others have same issue so leaving as is
What's the easiest, most painless (it's nearly Friday), way to persuade Criteria1 to include the last elements in my non-contiguous array?
Sub Filters()
'Dimension variables
Dim Rng As Range
Dim i, Lim As Integer
Dim w As Worksheet
Dim Op As Variant
Set w = ActiveSheet
'Set Lim as total number of rows in named range "F" (only 1 cell in use but same effect)
Lim = Range("F").Rows.Count
'Data has header row so skip to row 2
i = 2
'Loop through i up to limit
Do While i <= Lim
'If the row is not hidden by the filters the user chose
If Range("F")(i, 1).EntireRow.Hidden = False Then
'Check if the range is nothing - if it is, union will not work to itself
'Union requires non-empty arguments
If Rng Is Nothing Then
'Set the Rng to include the current cell from "F"
Set Rng = Range("F")(i, 1)
Else
'If Rng has some value, add the current cell to it by Union
Set Rng = Application.Union(Rng, Range("F")(i, 1))
End If
End If
'Increment i
i = i + 1
Loop
If w.AutoFilter.Filters.Item(1).Operator <> False Then Op = w.AutoFilter.Filters.Item(1).Operator
'This gives the correct range, but most often non-contiguous
MsgBox Range("F").Address
'Remove AutoFilter
w.AutoFilterMode = False
'Insert Code Here
'Put filters back
'Check for Rng being non-empty (pointless running code if it is)
If Not IsEmpty(Rng) Then
'If there is an operator then use the array
If Op Then
'Found this option useful here - can transpose the array values which generates an array Criteria1 can use
'Always xlFilterValues as there will always be more than 2 options
'Also the options are taken from the worksheet live so won't change between times so specifying them precisely as strings is ok
Range("F").AutoFilter Field:=1, Criteria1:=Application.Transpose(Rng.Value), _
Operator:=xlFilterValues
Else
'Just filter the range but leave all options available
Range("F").AutoFilter Field:=1
End If
End If
End Sub
Answered by using a second counter to count successful entries that should be included as criteria, and writing them to a range in another worksheet.
Then set the range to be that new (contiguous) range in the new worksheet.
Now works like a charm at last. Only took me all day to find syntax that worked with Criteria, and figure that you can only use xlOr for up to 2 criteria, otherwise it's xlfiltervalues...
Final working code as generic as I can get it to be as helpful as possible:
Sub Filters()
'Dimension variables
Dim Rng As Range
Dim i, j, Lim As Integer
Dim w As Worksheet
Dim Op As Variant
Set w = ActiveSheet
'Set Lim as total number of rows in named range "F" (only 1 cell in use but same effect)
Lim = Range("F").Rows.Count
'Data has header row so skip to row 2
i = 2
'Loop through i up to limit
Do While i <= Lim
'If the row is not hidden by the filters the user chose
If Range("F")(i, 1).EntireRow.Hidden = False Then
'Check if the range is nothing - if it is, union will not work to itself
'Union requires non-empty arguments
If Rng Is Nothing Then
'Set the Rng to include the current cell from "F"
Set Rng = Range("F")(i, 1)
Sheets("Sheet2").Range("A75").Value = Range("F")(i, 1).Value
j = j + 1
Else
Sheets("Sheet2").Range("A1").Offset(j, 0).Value = Range("F")(i, 1).Value
j = j + 1
End If
End If
'Increment i
i = i + 1
Loop
'If there's an operator, save it as variable Op (if needed)
If w.AutoFilter.Filters.Item(1).Operator <> False Then Op = w.AutoFilter.Filters.Item(1).Operator
'Remove AutoFilter
w.AutoFilterMode = False
'Insert Code Here
'Pause between the two halves
MsgBox ""
'Put filters back
'Check for Rng being non-empty (pointless running code if it is)
If Not IsEmpty(Rng) Then
'If there is an operator then use the range
If Op Then
'Found this option useful here - can transpose the array values
'Always xlFilterValues as there will always be more than 2 options
'Also the options are taken from the worksheet live so won't change between times so specifying them precisely as strings is ok
Range("F").AutoFilter Field:=1, Criteria1:=Application.Transpose(Sheets("Sheet2").Range("A75").Resize(j, 1).Value), _
Operator:=xlFilterValues
Else
'Just filter the range but leave all options available
Range("F").AutoFilter Field:=1
End If
End If
End Sub
The problem is the following: I have an excel file with multiple worksheets and I needed to copy the G column from every worksheet to a single new worksheet ( the columns should be next to each other or with an empty column between the columns with data). I also wanted to ask if it is possible to put the name of each worksheet above the corresponding column.
Until now, I used this code:
Sub Copy_G_Columns()
Dim ws As Worksheet, i As Long
Application.ScreenUpdating = False
On Error Resume Next
Set ws = Sheets("Gee Columns")
If Err.Number <> 0 Then
ActiveWorkbook.Sheets.Add After:=ActiveWorkbook.Sheets(Sheets.Count): ActiveSheet.Name = "Gee Columns"
On Error GoTo 0
Else
Sheets("Gee Columns").Select
End If
For i = 1 To ActiveWorkbook.Sheets.Count - 1
With Sheets(i)
.Range("G1:G" & .Cells(.Rows.Count, 7).End(xlUp).Row).Copy Cells(2, i * 2 - 1)
Cells(1, i * 2 - 1) = Sheets(i).Name
End With
Next i
Application.ScreenUpdating = True
End Sub
It seems to almost work perfectly. The only problem is that in the new created sheet, the values in the columns have a #DIV/0 error. I think the problem is that the code is copying the formats and not the values.
Here is my interpretation of your code.
Option Explicit
Sub allGEE()
Dim w As Long, wsn As String, vGEEs As Variant
wsn = "Gee Columns"
For w = 1 To Worksheets.Count
With Worksheets(w)
On Error GoTo bm_NeedWorksheet
If .Name <> Worksheets(wsn).Name Then
On Error GoTo bm_Safe_Exit
vGEEs = .Range(.Cells(1, 7), .Cells(Rows.Count, 7).End(xlUp)).Value
vGEEs(1, 1) = .Name
With Worksheets(wsn).Cells(1, w * 2 - 1)
.Resize(UBound(vGEEs, 1), UBound(vGEEs, 2)) = vGEEs
End With
End If
End With
Next w
GoTo bm_Safe_Exit
bm_NeedWorksheet:
On Error GoTo 0
With Worksheets.Add(after:=Sheets(Sheets.Count))
.Name = wsn
End With
Resume
bm_Safe_Exit:
End Sub
I've retained the stagger in the destination cells. I strongly suspect that you were copying formulas across and needed the values only. Transferring values with a variant array (without the clipboard) is quicker. Direct value transfer is also possible but you wanted to put the origin worksheet name into the first cell(s).