VBA - Certain Data Only Visible After Second Run - excel

This code below does the following:
Copies specific rows from "source" sheet
Pastes the rows in "target" sheet
Does a count of the "types" (Column E) and inserts the count into Column J
The issue I have is by step 3. What the macro is supposed to do is:
Column I, Rows 3 - 5 --> Insert the Column Headings "Defect",
"System", "Script"
Perform a CountIf function of Column E on each of the criteria in
Column I
Output the value (counted number) in Column J, in the respective
rows alongside Column I
For example:
Column I, Row 3 --> Defect
Column J, Row 3 --> Count of the amount of times "Defect" occurs in
Column E
However, what seems to be happening is this:
Column I is populated with the correct criteria
CountIf is performed (what appears to be correctly) and inserts
the values in Column J
As the values are inserted, the criteria in Column I is erased
and all I have left are the number values in Column J
Now if I run the macro a second time, then it performs as expected and I cannot understand why.
Also, there are no "Defect" entries in Column E, so the value is 0. But on the first run, you don't see 0, it's just blank. On the second run, it shows the value 0.
Sub Copy()
Dim xRg As Range, xCell As Range
Dim i As Long, J As Long, K As Long, x As Long, count As Long
Dim y As Workbook
Dim ws1 As Worksheet
Dim element As Variant, myarray As Variant
myarray = Array("Defect", "System", "Script")
i = Worksheets("source").UsedRange.Rows.count
J = Worksheets("target").UsedRange.Rows.count
count = 3
Set y = Workbooks("myWKBK.xlsm")
Set ws1 = y.Sheets("target")
If J = 1 Then
If Application.WorksheetFunction.CountA(Worksheets("target").UsedRange) = 0 Then J = 0
End If
lngLastRow = Cells(Rows.count, "C").End(xlUp).Row
Set xRg = Worksheets("source").Range("E3:E" & lngLastRow & i)
On Error Resume Next
Application.ScreenUpdating = False
With ws1
'Assign name to columns where values will be pasted
.Range("$B$2").Value = "ID"
.Range("$C$2").Value = "Status"
.Range("$D$2").Value = "Description"
.Range("$E$2").Value = "Type"
.Range("$F$2").Value = "Folder"
.Range("$G$2").Value = "Defect ID"
.Range("$I$2").Value = "Type"
.Range("$I$3").Value = "Defect"
.Range("$I$4").Value = "System"
.Range("$I$5").Value = "Script"
.Range("$J$2").Value = "Count"
End With
For Each element In myarray
For K = 1 To xRg.count
If CStr(xRg(K).Value) = element Then
LastRow = ws1.Cells(Rows.count, "B").End(xlUp).Row + 1
xRg(K).EntireRow.Copy Destination:=ws1.Range("A" & LastRow)
J = J + 1
End If
Next
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
count = count + 1
Next element
ws1.Columns("B:J").AutoFit
Application.ScreenUpdating = True
End Sub
EDIT:
It's probably well worth mentioning that the below sub on its own works just fine:
Sub CountIf()
Dim element As Variant
Dim myarray As Variant
myarray = Array("Defect", "System", "Script")
Dim count As Long
count = 3
For Each element In myarray
Dim x As Long
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
count = count + 1
Next element
End Sub
This function only performs the CountIf on its own and works exactly as expected.

This is a really beautiful part of your code:
Set xRg = Worksheets("source").Range("E3:E" & lngLastRow & i)
It sets correctly the parent worksheet of the Range object, thus VBA knows where to look at. However, for some reasons, it is not always like this. Take a look at these lines:
lngLastRow = Cells(Rows.count, "C").End(xlUp).Row
x = Range("E" & Rows.count).End(xlUp).Row
Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
where the worksheet is not set. Thus, it takes either the ActiveSheet or the worksheet, in which the code is (if it is in a worksheet and not in a module). Try to rewrite it, following the beautiful part of your code, e.g., defining the worksheet:
With Worksheet("SomeName")
lngLastRow = .Cells(Rows.count, "C").End(xlUp).Row
x = .Range("E" & Rows.count).End(xlUp).Row
.Range("J" & count) = Application.WorksheetFunction.CountIf(Range("E3:E" & x), element)
End With
As a next step to debug, try to remove On Error Resume Next, because it ignores the errors in the applications and may provide false results due to this.

Related

Excel VBA: Update a cell based on conditions

I am not that much familiar in VBA code. I am looking to implement two scenarios using VBA code in excel.
Scenario 1: If the value in the "C" column contains specific text, then replace the corresponding values in the "A" column as below
If the value in C contains "abc" then A= "abc".
If the value in C contains "gec" then A= "GEC".
It should loop from the second row to last non-empty row
A
B
C
Two
abc-def
Thr
gec-vdg
Thr
abc-ghi
Expected Result:
A
B
C
abc
Two
abc-def
gec
Thr
gec-vdg
abc
Thr
abc-ghi
Scenario 2: If the value in the "B" column is "A", then replace all the "A" value in the B column as "Active". If the value in the "B" column is I", then replace all the I value in the B column as inactive.
It should loop from the second row to last non-empty row
A
B
C
abc
A
abc-def
gec
I
gec-vdg
abc
A
abc-ghi
Expected Result:
A
B
C
abc
Active
abc-def
gec
Inactive
gec-vdg
abc
Active
abc-ghi
I know that it is possible by using excel formulas. Wondering, how it can be implemented using vba code in excel.
Usually people on here won't just write code for you, this is more for helping you with your code when your stuck. However I've written one for you based on the information you have provided. I've assumed your cells in column C would always have the hyphen and you always want what's left of the hyphen. If there is no hyphen or the relevant cell in column C is empty then nothing will be put into the relevant cell in column A.
I've put in to turn off ScreenUpdating for the code as I don't know how many rows you have. If it's a lot and you have a lot going on, then we can also turn off Calculation and Events to speed it up more, or run it as an array if it's really slow but I suspect that it won't be an issue.
Paste this into your relevant sheet module and change the sheet name as well as the column that's finding the last row if C isn't the right one:
Sub UpdateCells()
Application.ScreenUpdating = False
Dim i As Long, lRow As Long, ws As Worksheet
Set ws = Sheets("Sheet1") 'Change Sheet1 to your sheet name
lRow = ws.Range("C" & Rows.Count).End(xlUp).Row 'Finds your last row using Column C
With ws
For i = 2 To lRow 'Loop from row 2 to last row
If .Range("B" & i) = "A" Then
.Range("B" & i) = "Active"
ElseIf .Range("B" & i) = "I" Then
.Range("B" & i) = "Inactive"
End If
If .Range("C" & i) <> "" Then
If InStr(.Range("C" & i), "-") > 0 Then 'If current row Column C contains hyphen
.Range("A" & i) = Left(.Range("C" & i), InStr(.Range("C" & i), "-") - 1)
End If
End If
Next i
End With
Application.ScreenUpdating = True
End Sub
Replace Values
Option Explicit
Sub replaceCustom()
' Define constants.
Const wsName As String = "Sheet1"
Const ColumnsAddress As String = "A:C"
Const FirstRow As Long = 2
Dim Contains As Variant: Contains = VBA.Array(3, 1) ' 0-read, 1-write
Const findContainsList As String = "abc,gec" ' read
Const replContainsList As String = "abc,gec" ' write
Dim Equals As Variant: Equals = VBA.Array(2, 2) ' 0-read, 1-write
Const findEqualsList As String = "A,I" ' read
Const replEqualsList As String = "Active,Inactive" ' write
Dim CompareMethod As VbCompareMethod: CompareMethod = vbTextCompare
' Define workbook.
Dim wb As Workbook: Set wb = ThisWorkbook ' Workbook containing this code.
' Define range.
Dim rng As Range
With wb.Worksheets(wsName).Columns(ColumnsAddress)
Set rng = .Resize(.Worksheet.Rows.Count - FirstRow + 1) _
.Offset(FirstRow - 1).Find( _
What:="*", _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious)
If rng Is Nothing Then
Exit Sub
End If
Set rng = .Resize(rng.Row - FirstRow + 1).Offset(FirstRow - 1)
End With
' Write values from range to array.
Dim Data As Variant: Data = rng.Value
' Write lists to arrays.
Dim findCo() As String: findCo = Split(findContainsList, ",")
Dim replCo() As String: replCo = Split(replContainsList, ",")
Dim findEq() As String: findEq = Split(findEqualsList, ",")
Dim replEq() As String: replEq = Split(replEqualsList, ",")
' Modify values in array.
Dim i As Long
Dim n As Long
For i = 1 To UBound(Data, 1)
For n = 0 To UBound(Contains)
If InStr(1, Data(i, Contains(0)), findCo(n), CompareMethod) > 0 Then
Data(i, Contains(1)) = replCo(n)
Exit For
End If
Next n
For n = 0 To UBound(Equals)
If StrComp(Data(i, Equals(0)), findEq(n), CompareMethod) = 0 Then
Data(i, Equals(1)) = replEq(n)
Exit For
End If
Next n
Next i
' Write values from array to range.
rng.Value = Data
End Sub

Slow search/filter algorithm

My problem is, my current code is pretty slow right now and i would like to make it faster, but don't know how.
I have data sets in rows, which looks like this:
I need to filter/search those values like numbers (for example show all >30). But some of the entries, like 30|32,89 are not numbers. Right now i am checking each value, if it needs to be split, like 30|32,89 in 30 and 32,89 and write all the values in a sheet. So i have a column, where all the values are numbers. With a second column, which saves the original row number, Like this:
After that i use advanced filter to get the data i need. I write it in another column. Using original row numbers to write values from the same original cell only ones, if several of the numbers in that cell meet the search criteria. And to do this, i save all the original data(20 columns and many rows) in a 2D array. Then i take only the values from that array, where the 1st index matches the original row number of the filtered data and write all of the values one buy one in another sheet in a row for each 1st index (this part causes the majority of the slowness). There are 20 values for each 1st index. So at the end i get all the corresponding data for the filtered items shown in one table.
Here is my code for that:
Public Sub numberSearch(srchCol As String, srchValue As String)
Dim sValues As Variant, wRange As Variant
'temp values
cRow = archSh.Range("A1").CurrentRegion.rowS.count
Dim srchCol As String
srchCol = "B"
Dim srchValue As String
srchValue = ">2005"
'------------------
'prepare sheet
shSearch.Cells.Clear
sValues = Application.Transpose(archSh.Range(srchCol & "2", srchCol & cRow))
wRange = archSh.Range("A1").CurrentRegion
shSearch.Range("A1").Value = archSh.Range(srchCol & "1").Value
shSearch.Range("B1").Value = "tst"
shSearch.Range("D1").Value = shSearch.Range("A1").Value
shSearch.Range("E1").Value = shSearch.Range("B1").Value
shSearch.Range("G1").Value = shSearch.Range("A1").Value
shSearch.Range("H1").Value = shSearch.Range("B1").Value
shSearch.Range("D2").Value = srchValue
'----------------------------
'spilt values, make all numeric
Dim i As Long, j As Long, k As Long
Dim tst As Variant, c As Variant
Dim s
i = 2
k = 2
For Each c In sValues
If IsNumeric(c) = True Then
ReDim tst(0 To 0)
tst(0) = c
Else
tst = Split(c, sepa)
End If
For j = 0 To UBound(tst)
shSearch.Range("A" & k + j).Value = tst(j)
shSearch.Range("B" & k + j).Value = i
Next j
i = i + 1
k = k + UBound(tst) - LBound(tst) + 1
Next
'--------------------------------
'filter data
Dim rgData As Range, rgCrit As Range, rgOut As Range
Set rgData = shSearch.Range("A1").CurrentRegion
Set rgCrit = shSearch.Range("D1").CurrentRegion
Set rgOut = shSearch.Range("G1").CurrentRegion
rgData.AdvancedFilter xlFilterCopy, rgCrit, rgOut
'---------------------------------
'write searched data
Dim searchColVal As Variant
searchColVal = Application.Transpose(shSearch.Range("H1:H" & shSearch.Cells(rowS.count, 8).End(xlUp).row))
Dim tempItem As Long
tempItem = 0
k = 4
tmpSh.Range("A4").CurrentRegion.Clear
archSh.Range("A1:T1").Copy tmpSh.Range("A4")
For i = 2 To UBound(searchColVal)
If tempItem <> searchColVal(i) Then
ReDim Preserve filterRow(1 To k - 3)
filterRow(k - 3) = searchColVal(i)
k = k + 1
tempItem = searchColVal(i)
For j = 1 To UBound(wRange, 2)
tmpSh.Cells(k, j).Value = wRange(searchColVal(i), j)
Next j
End If
Next i
'----------------------------------------
End Sub
Can anybody help me with speeding up this mess please? Ty in advance.
You can do this with the Advanced Filter and formula criteria.
We use FILTERXML (available in Excel 2013+) to split the text values.
We also is the ISNUMBER function to exclude the text values from being cast as TRUE by the comparison in the first formula.
And the Advanced Filter has an option to write the results elsewhere
For your example, the two formulas might be:
=AND(ISNUMBER(A9),A9>30)
=OR(FILTERXML("<t><s>" & SUBSTITUTE(A9,"|","</s><s>") & "</s></t>","//s")>30)
Before Filter
After Filter
Or, if you change the criteria in both formulas for >30 to <30
Depending on what you need, you could certainly use VBA to generate the relevant formulas.
This scans down the column, splits the cell value into an array then uses Evaluate to apply the search value.
Public Sub numberSearch2()
Const COL_FILTER = "B"
Const srchValue = ">2005"
Dim wb As Workbook, wsSource As Worksheet, WsTarget, t0 As Single
Dim iRow As Long, iLastRow As Long, iTargetRow As Long
Dim ar As Variant, i As Integer
t0 = timer
Set wb = ThisWorkbook
Set wsSource = wb.Sheets("Sheet2")
Set WsTarget = wb.Sheets("Sheet3")
WsTarget.Cells.Clear
wsSource.Rows(1).EntireRow.Copy WsTarget.Range("A1")
iTargetRow = 2
With wsSource
iLastRow = .Range(COL_FILTER & Rows.Count).End(xlUp).Row
For iRow = 2 To iLastRow
ar = Split(.Cells(iRow, COL_FILTER), "|")
For i = 0 To UBound(ar)
If Evaluate(ar(i) & srchValue) Then
wsSource.Rows(iRow).EntireRow.Copy WsTarget.Cells(iTargetRow, 1)
iTargetRow = iTargetRow + 1
i = UBound(ar) ' exit loop
End If
Next
Next
End With
MsgBox iLastRow - 1 & " rows read " & vbCr & _
iTargetRow - 2 & " rows written", vbInformation, "Completed in " & Int(timer - t0) & " secs"
End Sub

Excel VBA for Uniques

I am looking to extract Unique in the format given on the right side. I found the VBA code on one of the forum site, but this one does not suit me. Is there a way to modify the code or write something better. I do have a formula, but formula is quite resource intensive and a very large excel loads very slowly.
Sub FindDistinctValues()
Dim LastRowFrom As Long
Dim LastRowTo As Long
Dim i As Long, j As Long
Dim temp As Integer
Dim found As Boolean
'determines the last row that contains data in column A
LastRowFrom = Range("A" & Rows.Count).End(xlUp).Row
'Loop for each entry in column A
For i = 2 To LastRowFrom
'get the next value from column A
temp = Range("A" & i).Value
'Determine the last row with data in column B
LastRowTo = Range("B" & Rows.Count).End(xlUp).Row
'initialize j and found
j = 1
found = False
'Loop through "To List" until a match is found or the list has been searched
Do
'check if the value exists in B column
If temp = Range("B" & j).Value Then
found = True
End If
'increment j
j = j + 1
Loop Until found Or j = LastRowTo + 1
'if the value is not already in column B
If Not found Then
Range("B" & j).Value = temp
End If
Next i
End Sub
I didn't test it, but something like this:
Sub FindDistinctValues()
Dim dict As Object, cell As Range
Set dict = CreateObject("Scripting.Dictionary")
For Each cell in Range("A1").CurrentRegion.Resize(, 1)
If Not dict.Exists(cell & "")
cell(, 2) = "Unique"
dict.Add cell & "", 0
End If
Next
End Sub

Populate blank cells in a column until all populated from repeating list

I need to use VBA code to populate a list of filtered blank cells. I decided to make a picture with small example to explain it easier. Column D should be populated with names from col A repeating until each ID has a name.
I have absolutely no idea how to loop it to make it work - it's mind boggling! I have been searching the web for hours so I am now asking for help. Please note that column C and D are filtered with criteria blanks for column D.
Here is working code to populate blank cells of a filtered list with the same 3 names alternating.
Sub Macro1()
Dim last As Long
Dim counter As Integer
Dim nameRange As Range
Dim cell As Range
last = Range("A2").End(xlDown).Row
Set nameRange = Range("D2:D" & last).SpecialCells(xlCellTypeVisible)
counter = 1
For Each cell In nameRange
If counter = 1 Then
cell.Value = "Carrie"
counter = counter + 1
ElseIf counter = 2 Then
cell.Value = "Lisa"
counter = counter + 1
Else
cell.Value = "Bob"
counter = 1
End If
Next
End Sub
thanks for everyone's input - Hopefully, this will help someone else in the future.
This will do it without the need of filtering the data.
Sub foo()
Dim ws As Worksheet
Dim lastrowa As Long
Dim lastrowd As Long
Dim counta As Long
Dim rng As Range
counta = 2 'First row of name list in column A
Set ws = Sheets("Sheet1")
With ws
lastrowa = .Range("A" & .Rows.Count).End(xlUp).Row
lastrowd = .Range("D" & .Rows.Count).End(xlUp).Row
For Each rng In .Range(.Cells(2, 5), .Cells(lastrowd, 5))
If rng.Value = "" Then
rng.Value = .Cells(counta, 1).Value
If counta = lastrowa Then
counta = 2
Else
counta = counta + 1
End If
End If
Next rng
End With
End Sub
Range("D2:D4").Value = Range("A2:A4").Value
Range("D2:D4").AutoFill Destination:=Range("D2:D11")
If you don't know where column C ends that is easy enough to work out. Something like
Range("D2:D4").Value = Range("A2:A4").Value
Range("D2:D4").AutoFill Destination:=Range(Range("D2"), _
Range("C2").End(xlDown).Cells(1, 2))
If you don't know how far the data extends in column A:
Dim last As Integer
last = Range("A2").End(xlDown).Row
Range("D2:D" & last).Value = Range("A2:A" & last).Value
Range("D2:D" & last).AutoFill Destination:=Range(Range("D2"), _
Range("C2").End(xlDown).Cells(1, 2))
My example doesn't work perfectly, or even well... Its late :)
Create a named range that encapsulates all your "names" (called namesRange in my example).
In your "assigned" column put the following formula:
=INDEX(namesList,ROW()-((INT(ROW()/ROWS(namesList))*ROWS(namesList))),1)
Update...
Thought about it, and remembered how to excel a little more.. The following is what I was trying to do in my first example.
=INDEX(namesList,MOD(ROW()-1,ROWS(namesList)-1)+1,1)

Inserting VBA formula into a defined Range

I'm looping through a table (Sheet4) in a worksheet and inserting an empty column between each original column. In the new, empty columns, I want to insert a VLookup function.
I has successfully inserted the columns and I have successfully held the proper range (proper number of rows too) in a variable called FormulaRange.
I'm having problems with the VLookup. I don't know if it's the way I'm storing my variables or if I need to have a paste function after my Vlookup. Can someone take a look and give me a hand?
*note - I stored FormulaRange as String becuase As Range wouldn't let me pass my code into the variable. As a result I can't use the FormulaRange.Formula Method.
If I were to manually input the VLookup I would write it as =VLOOKUP(B1,RosettaStone_Employees!$A$1:$B$5,2,FALSE) and then copy down the range.
Sub InsertColumnsAndFormulasUntilEndOfProductivityTable()
Dim ActivityRange As Range
Dim UserName As String
Dim FormulaRange As String
Dim i As Integer
Dim x As Long
Dim y As Long
Dim Startrow As String
Dim Lastrow As String
Sheet6.Activate
Set ActivityRange = Range("A1", Range("B1").End(xlDown))
Sheet4.Activate
Range("A1").Select
y = Sheet4.Cells(Rows.Count, 1).End(xlUp).Row - 1
x = (Sheet4.Cells(1, Columns.Count).End(xlToLeft).Column) * 2
For i = 1 + 2 To x Step 2
Columns(i).EntireColumn.Insert
Startrow = 2
Lastrow = y
UserName = Cells(1, i - 1)
FormulaRange = Cells(Startrow, i).Address & ":" & Cells(Lastrow + 1, i).Address
FormulaRange = "=VLookup(UserName, ActivityRange, 2, False)"
Next
End Sub
Thanks
Jonathan
I changed your code a little to get rid of the sheet activates. Also I changed a few things to ranges and included with blocks.
This is untested:
Sub InsertColumnsAndFormulasUntilEndOfProductivityTable()
Dim ActivityRange As Range
Dim UserName As String
Dim FormulaRange As Range
Dim i As Long
Dim x As Long
Dim y As Long
Dim Startrow As Long
Dim Lastrow As Long
With Sheet6
Set ActivityRange = .Range("A1", .Range("B1").End(xlDown))
End With
With Sheet4
y = .Cells(.Rows.Count, 1).End(xlUp).Row - 1
x = (.Cells(1, .Columns.Count).End(xlToLeft).Column) * 2
For i = 1 + 2 To x Step 2
.Columns(i).EntireColumn.Insert
Startrow = 2
Lastrow = y
UserName = .Cells(1, i - 1)
Set FormulaRange = .Range(.Cells(Startrow, i), .Cells(Lastrow + 1, i))
FormulaRange.FormulaR1C1 = "=VLookup(R1C[-1],'" & ActivityRange.Parent.Name & "'!" & ActivityRange.Address(1, 1, xlR1C1) & ", 2, False)"
'If you do not want dynamic formulas and just want the value
'then comment out the above and use the below.
'FormulaRange.Value = Application.Vlookup(UserName,ActivityRange,2,False)
Next
End With
End Sub
The R1C1 is a relative nomenclature. When it fills the formulas into the columns it will return the cell relative to the one into which the formula will be filled.
For example, above I use R1C[-1]. This says get the first row of the column directly to the left. So if the formula was being entered into B2 it would return A$1.
The [] denotes the relative address. Without the [] eg R1C1 it would indicate an absolute address and would return $A$1. So R1C1:R4C2 would return a range of $A$1:$B$4.

Resources