VBA finding value and put it in specific column - vba

Hope you you can help me here. I have a repetitive task every week, which I could do the same way every single time through Excel formulas, but I am looking for a more automated way of going about this.
What I want to achieve is to set-up a dynamic range that will look for multiple key words such as in this case "OA" & "SNC" and if it matches it will return the value in the column G & H respectively. At the same time it has to skip blank rows. What is the best way to go about this?
I figured it shouldn't be too hard, but I cannot figure it out.
As per image above, I want to consolidate the charges per category (OA & SNC) in the designated columns ("G" & "H") on row level.

My approach to the task
Procedure finds data range, loops through it's values, adding unique values to the dictionary with sum for specific row and then loads all these values along with sums per row.
Option Explicit
Sub CountStuff()
Dim wb As Workbook, ws As Worksheet
Dim lColumn As Long, lRow As Long, lColTotal As Long
Dim i As Long, j As Long
Dim rngData As Range, iCell As Range
Dim dictVal As Object
Dim vArr(), vArrSub(), vArrEmpt()
'Your workbook
Set wb = ThisWorkbook
'Set wb = Workbooks("Workbook1")
'Your worksheet
Set ws = ActiveSheet
'Set ws = wb.Worksheets("Sheet1")
'Number of the first data range column
lColumn = ws.Rows(1).Find("1", , xlValues, xlWhole).Column
'Number of the last row of data range
lRow = ws.Cells(ws.Rows.Count, lColumn).End(xlUp).Row
'Total number of data range columns
lColTotal = ws.Cells(1, lColumn).End(xlToRight).Column - lColumn + 1
'Data range itself
Set rngData = ws.Cells(1, lColumn).Resize(lRow, lColTotal)
'Creating a dictionary
Set dictVal = CreateObject("Scripting.Dictionary")
'Data values -> array
vArr = rngData.Offset(1, 0).Resize(rngData.Rows.Count - 1, _
rngData.Columns.Count).Value
'Empty array
ReDim vArrEmpt(1 To UBound(vArr, 1))
'Loop through all values
For i = LBound(vArr, 1) To UBound(vArr, 1)
For j = LBound(vArr, 2) To UBound(vArr, 2)
'Value is not numeric and is not in dictionary
If Not IsNumeric(vArr(i, j)) And _
Not dictVal.Exists(vArr(i, j)) Then
'Add value to dictionary
dictVal.Add vArr(i, j), vArrEmpt
vArrSub = dictVal(vArr(i, j))
vArrSub(i) = vArr(i, j - 1)
dictVal(vArr(i, j)) = vArrSub
'Value is not numeric but already exists
ElseIf dictVal.Exists(vArr(i, j)) Then
vArrSub = dictVal(vArr(i, j))
vArrSub(i) = vArrSub(i) + vArr(i, j - 1)
dictVal(vArr(i, j)) = vArrSub
End If
Next j
Next i
'Define new range for results
Set rngData = ws.Cells(1, lColumn + lColTotal - 1). _
Offset(0, 2).Resize(1, dictVal.Count)
'Load results
rngData.Value = dictVal.Keys
For Each iCell In rngData.Cells
iCell.Offset(1, 0).Resize(lRow - 1).Value _
= Application.Transpose(dictVal(iCell.Value))
Next
End Sub

I've used a simple custom function, possibly overkill as this could be done with worksheet formulae, but given that your ranges can vary in either direction...
Function altsum(r As Range, v As Variant) As Variant
Dim c As Long
For c = 2 To r.Columns.Count Step 2
If r.Cells(c) = v Then altsum = altsum + r.Cells(c - 1)
Next c
If altsum = 0 Then altsum = vbNullString
End Function
Example below, copy and formula in F2 across and down (or apply it one go with another bit of code).

Related

Excel filter a column by the first letters for more than 2 values

I am very new at vba, and now fighting with one macro which will filter a Column by the first exact letters (for instance, I have a Column N - “City” and as a result I have to have all entries , starts for instance- “Vancouver”, “Vancouver. BC”, “Vancouver Canada” – so I want to sort this column by the first letters – VANCOU - to be sure, that I will not miss any info.
The code below does not work at all for 3 values – probably I choose a wrong way ., can you please advise – which function or operator will work at this case? All I find - work for 2 values (at that case I can use at list "begins with"). I have 5-6 values, and they might vary (I don't know which format of City name I will have next time) .
Thanks in advance!
Dim rng01 As Range
Set rng01 = [A1:Z5048]
rng01.Parent.AutoFilterMode = False
rng01.Columns(14).AutoFilter Field:=1, Criteria1:=Array("Vancou*", "Brampt*", "Halifa*"), Operator:= _
xlFilterValues
Upd:
Here is an adapted code , which is not working
Option Explicit
Sub AutoFilterWorkaround()
Dim sht As Worksheet
Dim filterarr As Variant, tofindarr As Variant
Dim lastrow As Long, i As Long, j As Long, k As Long
Set sht = ThisWorkbook.Worksheets("Sheet1")
lastrow = sht.Cells(sht.Rows.Count, "N").End(xlUp).Row
'List the parts of the words you need to find here
tofindarr = Array("Vancou", "Brampt", "Halifa")
ReDim filterarr(0 To 0)
j = 0
For k = 0 To UBound(tofindarr)
For i = 2 To lastrow
If InStr(sht.Cells(i, 14).Value, tofindarr(k)) > 0 Then
filterarr(j) = sht.Cells(i, 14).Value
j = j + 1
ReDim Preserve filterarr(0 To j)
End If
Next i
Next k
'Filter on array
sht.Range("$N$1:$N$" & lastrow).AutoFilter Field:=14, Criteria1:=Array(filterarr), Operator:=xlFilterValues
End Sub
Okay, so I rewrote the workaround - basically we avoid using wildcards by just finding each individual match case, loading that into an array, then filter on the entire array at the end.
This example works for column A - just change the A in lastrow to N, as well as changing the As to Ns in the last line. Also specify your sheet name on the Set sht line. Also Field:=1 needs to be changed to Field:=14 for column N in your case.
Option Explicit
Sub AutoFilterWorkaround()
Dim sht As Worksheet
Dim filterarr As Variant, tofindarr As Variant
Dim lastrow As Long, i As Long, j As Long, k As Long
Set sht = ThisWorkbook.Worksheets("Sheet1")
lastrow = sht.Cells(sht.Rows.Count, "A").End(xlUp).Row
'List the parts of the words you need to find here
tofindarr = Array("Vancou", "Brampt", "Halifa")
ReDim filterarr(0 To 0)
j = 0
For k = 0 To UBound(tofindarr)
For i = 2 To lastrow
If InStr(sht.Cells(i, 1).Value, tofindarr(k)) > 0 Then
filterarr(j) = sht.Cells(i, 1).Value
j = j + 1
ReDim Preserve filterarr(0 To j)
End If
Next i
Next k
'Filter on array
sht.Range("$A$1:$A$" & lastrow).AutoFilter Field:=1, Criteria1:=Array(filterarr), Operator:=xlFilterValues
End Sub

VBA Nested Do While Loop vs. Nested Do While If Loop

I'm not sure where I'm going wrong. I'm trying to compare values within a column ("B") to a cell referenced to ("A1"). If the values in Column "B" equal "A1" I want it to count up. When it gets to the end of Column "B" I'm trying to get it to loop back and compare values in column "B" with "A2", etc. For example:
So Far I've written two different codes one with a nested do while loop and a nested do while if loop but i cant get them to loop through the whole column
Sub CountDb()
Dim i As Long
Dim iRow As Long
Dim initial As Long
i = 1
iRow = 1
initial = 1
Do While Cells(iRow, "A").Value <> "" 'initial loop, whilst there are values in cell "A" continue the loop
Do While Cells(i, "B").Value = Cells(iRow, "A").Value 'nested while loop, comparing the first B1 and cell A1.
If True Then Cells(i, "C") = initial 'if they A1 and B1 are equal, print 1 in column C
initial = initial + 1 'and move on comparing A1 with B2
If False Then
i = i + 1 'if not satisfied, move on to cell B2 etc.
Loop
iRow = iRow + 1 'when you get to the end of column B, start again and compare values with A2 and B
Loop
End Sub
Sub CountDb()
Dim i As Long
Dim iRow As Long
Dim initial As Long
'same comments as above, just different methodology
i = 1
iRow = 1
initial = 1
Do While Cells(iRow, "A").Value <> ""
If Cells(i, "B").Value = Cells(iRow, "A").Value Then
Cells(i, "C") = initial
Else
initial = initial + 1
i = i + 1
End If
iRow = iRow + 1
Loop
End Sub
Any help would be appreciated. Thanks!
*EDIT - fixed up column references
**EDIT - applied comments to code
Try this instead:
Option Explicit
Sub test()
Dim sht As Worksheet
Dim lastrow As Long, i as integer, j as integer, initial as integer
Set sht = Workbooks("Book1").Worksheets("Sheet1") 'Don't forget to change this
lastrow = sht.Cells(sht.Rows.Count, "A").End(xlUp).Row
For i = 1 To lastrow
initial = 1
lastrow = sht.Cells(sht.Rows.Count, "B").End(xlUp).Row
For j = 1 To lastrow
If Workbooks("Book1").Worksheets("Sheet1").Range("A" & i).Value = Workbooks("Book1").Worksheets("Sheet1").Range("B" & j).Value Then
Workbooks("Book1").Worksheets("Sheet1").Range("C" & j).Value = initial
initial = initial + 1
End If
Next j
Next i
End Sub
I prefer using For loops as opposed to Whiles, just because I can see the ranges being looped through more easily. Here we use nested For loops, the first to loop through column A, the second to loop through column B. If our value in column A equals our value in column B, we place the initial number in column C using our variable from the nested loop.
Notice how to make this work, we re-initialize our lastrow variable to make the ranges for our loops.
It is useful to use countif.
Sub test()
Dim rngOrg As Range, rngDB As Range
Dim Wf As WorksheetFunction
Dim vR() As Variant
Dim i As Long, n As Long
Set Wf = WorksheetFunction
Set rngOrg = Range("a1", Range("a" & Rows.Count).End(xlUp))
Set rngDB = Range("b1", Range("b" & Rows.Count).End(xlUp))
n = rngDB.Rows.Count
ReDim vR(1 To n, 1 To 1)
For Each Rng In rngDB
i = i + 1
If Wf.CountIf(rngOrg, Rng) Then
vR(i, 1) = Wf.CountIf(Range("b1", Rng), Rng)
End If
Next Rng
Range("c1").Resize(n) = vR
End Sub
Here is another method, this time using Find. This is probably quicker than the looping method since it leverages the in-built find function to skip to the next match.
I've commented the code below for clarity, but basically we loop through values in column A (using a For loop because they're less prone to disguised infinite looping than While) and look for them in column B.
Note: This looks a bit longer, but that's mainly because (a) I've added lots of comments and (b) I've used a With statement to ensure the ranges are fully qualified.
Sub countdb()
Dim c As Range, fnd As Range, listrng As Range, cnt As Long, addr As String
' Use with so that our ranges are fully qualified
With ThisWorkbook.Sheets("Sheet1")
' Define the range to look up in (column B in this case)
Set listrng = .Range("B1", .Range("B1").End(xlDown))
' Loop over values in the index range (column
For Each c In .Range("A1", .Range("A1").End(xlDown))
cnt = 0
' Try and find the c value
Set fnd = listrng.Find(what:=c.Value, lookat:=xlWhole, LookIn:=xlValues, after:=listrng.Cells(listrng.Cells.Count))
If Not fnd Is Nothing Then
' Store the address of the first find so we can stop when we find it again!
addr = fnd.Address
' Loop over all other matches in the range. By using a "Do ... Loop While"
' style loop, we ensure that the loop is run at least once!
Do
' Increase count and assign value to next column
cnt = cnt + 1
fnd.Offset(0, 1).Value = cnt
' Find next match after current
Set fnd = listrng.Find(what:=c.Value, lookat:=xlWhole, LookIn:=xlValues, after:=fnd)
Loop While fnd.Address <> addr
End If
Next c
End With
End Sub
The trick is in making the declarations transparent. After that the programming is very easy.
Sub CountMatches()
Dim Rng As Range ' "count" range (= column "B")
Dim Itm As String ' item from the "items' column (= "A")
Dim Rla As Long, Rlb As Long ' last row in columns A and B
Dim Ra As Long, Rb As Long ' row counters
Dim Counter As Long ' count matches
With ActiveSheet
' look for the last used rows
Rla = .Cells(.Rows.Count, "A").End(xlUp).Row
Rlb = .Cells(.Rows.Count, "B").End(xlUp).Row
' start looking for matches from row 2
Set Rng = .Range(.Cells(2, "B"), .Cells(Rlb, "B"))
' start looping in row 2
For Ra = 2 To Rla
Itm = .Cells(Ra, "A").Value
If Len(Trim(Itm)) Then ' skip if blank
' start comparing from row 2
For Rb = 2 To Rlb
' compare not case sensitive
If StrComp(.Cells(Rb, "B").Value, Itm, vbTextCompare) = 0 Then
Counter = Counter + 1
End If
Next Rb
.Cells(Ra, "C").Value = Counter
Counter = 0
End If
Next Ra
End With
End Sub
Now the question is whether the transparency that workred for me appears transparent to you. I hope it does. :-)
This should be significantly faster.
Sub CountMatches_2()
Dim Rng As Range ' "count" range (= column "B")
Dim Itm As String ' item from the "items' column (= "A")
Dim Rla As Long, Rlb As Long ' last row in columns A and B
Dim Ra As Long, Rb As Long ' row counters
With ActiveSheet
' look for the last used rows
Rla = .Cells(.Rows.Count, "A").End(xlUp).Row
Rlb = .Cells(.Rows.Count, "B").End(xlUp).Row
' start looking for matches from row 2
Set Rng = .Range(.Cells(2, "B"), .Cells(Rlb, "B"))
' start looping in row 2
For Ra = 2 To Rla
Itm = .Cells(Ra, "A").Value
If Len(Trim(Itm)) Then ' skip if blank
.Cells(Ra, "C").Value = Application.CountIf(Rng, Itm)
End If
Next Ra
End With
End Sub
This code presumes that each item in column A is unique. If it is not duplicates will be created which, however, it would be easy to eliminate either before or after they are created.

Creating a dynamic range in VBA from unique texts in reference file and looping into dashboard

I have code here that takes data from a reference file and counts all of the unique text lines and places them in a list of their own in the reference file. It is then supposed to place each of the unique texts into columns shown in this picture if "Title" is in column 4. Right now though, it just takes the first unique line from the reference file "title1" and loops it into every box. What I want is the rest of the unique texts to loop through as well. So, it will be "title1", "title2", "title3", etc. I can't quite figure it out though.
Sub unique()
Dim wsRef As Worksheet
Dim wsDB As Worksheet
Set wsRef = Worksheets("reference1")
Set wsDB = Worksheets("Sheet1")
With wsRef
.Range("F1:F60").AdvancedFilter Action:=xlFilterCopy, CopyToRange:=.Range("I1"), unique:=True
Dim arrValues As Variant
arrValues = .Range("I2", .Range("I" & .Rows.Count).End(xlUp))
End With
For i = 1 To ActiveSheet.Cells(Rows.Count, 4).End(xlUp).Row
If Cells(i, 4) = "Title" Then
For j = 1 To (UBound(arrValues)) Step 1
With wsDB
.Range(.Cells(i, j * 4 + 2), .Cells(i, j * 4 + 4)).Value = Application.WorksheetFunction.Transpose(arrValues)
End With
Next j
End If
Next i
End Sub
It looks like you are trying to copy unique value form 1 worksheet and copy them into several columns, spaced 4 columns apart, on another worksheet. I achieve that here by adding unique values to dictionary as keys. Next I loop over the columns counting by 4 Step 4, pasting the values in the second row.
Sub unique2()
Dim j As Integer
Dim arrUniqueValues, arrValues, k
Dim wsRef As Worksheet
Dim wsDB As Worksheet
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Set wsRef = Worksheets("reference1")
Set wsDB = Worksheets("Sheet1")
With wsRef
arrValues = .Range("F1", .Range("F" & .Rows.Count).End(xlUp))
For Each k In arrValues
If Len(k) And Not dict.Exists(k) Then dict.Add k, ""
Next
End With
With wsDB
For j = 6 To 24 Step 4
.Cells(2, j).Resize(dict.Count).Value = WorksheetFunction.Transpose(dict.Keys)
Next j
End With
Set dict = Nothing
End Sub

Similar Grouping in vba

I have two for loops in vba that are iterating over column b and checking to see if the first word in the current cell is the same as the first word in any other cell and if so copying them into another column, therefore grouping similar items. But, when I go to copy and paste the matches it finds, it only copy and pastes the matches, not the original cells that it is comparing against. I would like to have the matches and the original cells as well in the grouping but I am unsure where to modify my code so it will do so. I am rather new to vba so any help would be greatly appreciated.
Sub FuzzySearch()
Dim WrdArray1() As String, WrdArray2() As String, i As Long, Count As Long, Rng1 As Range
Dim WS As Worksheet, positionx As Long, positiony As Long
Dim rng2 As Range
Set WS = ThisWorkbook.ActiveSheet
With WS
Set Rng1 = .Range("B2:B" & .Range("B" & .Rows.Count).End(xlUp).Row)
End With
For i = 1 To Rng1.Rows.Count
With Columns("B")
.Find(what:="*", after:=.Cells(1, 1), LookIn:=xlFormulas).Activate
End With
position = 1
For j = 1 To Rng1.Rows.Count
WrdArray1 = Split(ActiveCell.Value, " ")
ActiveCell.Offset(1).Activate
WrdArray2 = Split(ActiveCell.Value, " ")
If UBound(WrdArray2) < 0 Then
End
End If
If WrdArray1(0) = WrdArray2(0) Then
ActiveCell.Copy Destination:=ActiveSheet.Range("C" & position)
position = position + 1
Count = Count + 1
End If
Next j
Next i
End Sub
Given that you are using a mixture of arrays and Ranges it would probably be easier and less confusing to populate one of the arrays with the final output (including the comparator) within a loop and then transfer the array to the worksheet in a single command.
However, perhaps consider the following approach which lets Excel do all the 'heavy lifting'. It's the same number of code lines but I have annotated it for your information. This illustrates the filling of an array in a loop and transferring it to a Range. Change the various variables to suit your situation.
Sub grpAndCount()
Dim ws As Worksheet
Dim strow As Long, endrow As Long, stcol As Long
Dim coloffset As Long, r As Long
Dim newstr As String
Dim drng As Range
Dim strArr() As String
'Data start r/c
strow = 6 'Row 6
stcol = 2 'Col B
'Offset no of Cols from Data to place results
coloffset = 2
Set ws = Sheets("Sheet1")
With ws
'find last data row
endrow = Cells(Rows.Count, stcol).End(xlUp).Row
'for each data row
For r = strow To endrow
'get first word
newstr = Left(.Cells(r, stcol), InStr(.Cells(r, stcol), " ")-1)
'put string into array
ReDim Preserve strArr(r - strow)
strArr(r - strow) = newstr
Next r
'put array to worksheet
Set drng = .Range(.Cells(strow, stcol + coloffset), .Cells(endrow, stcol + coloffset))
drng = Application.Transpose(strArr)
'sort newly copied range
drng.Sort Key1:=.Cells(strow, stcol + coloffset), Order1:=xlAscending, Header:=xlNo
'provide a header row for SubTotal
.Cells(strow - 1, stcol + coloffset) = "Header"
'resize range to include header
drng.Offset(-1, 0).Resize(drng.Rows.Count + 1, 1).Select
'apply Excel SubTotal function
Application.DisplayAlerts = False
Selection.Subtotal GroupBy:=1, Function:=xlCount, Totallist:=Array(1)
Application.DisplayAlerts = True
'remove 'Header' legend
.Cells(strow - 1, stcol + coloffset) = ""
End With
End Sub

Run-time error 1004 Application-defined or object defined error

I have looked through the other posts about this and have tried adapted the strategies that were recommend by using Set ActiveWorkbook and Set Active Worksheet and I still get the same error. I hope another set of eyes can help out as I am still very new to VBA and I am not all that comfortable with it yet.
Basically the idea is to copy the cells from column f to column j as values as long as the cells of F do not match the cells of J. I get the row count of column E and use that as my count in the for loop.
Code is here:
Private Sub CalculateRewards_Click()
CopyPaste
End Sub
Sub CopyPaste()
Dim n As Integer
Dim i As Integer
n = Sheets("Calculate").Range("E:E").Cells.SpecialCells(xlCellTypeConstants).Count
i = n
For Counter = 1 To n
Set curCell = Sheets("Calculate").Range("F2:F" &i)
If "$F" &i <> "$J" &i Then
Sheets("Calculate").Range("$F:$F" &i).Copy
Sheets("Calculate").Range("$J:$J" &i).PasteSpecial (xlPasteValues)
Application.CutCopyMode = False
End If
i = i + 1
Next Counter
End Sub
Thanks for the help
Also Edit:
Link to Excel Sheet that has a before page, after first transaction sheet ,and a after second transaction sheet: https://www.dropbox.com/s/n2mn0zyrtoscjin/Rewards.xlsm
CHange this:
Set curCell = Sheets("Calculate").Range("F2:F" &i)
If "$F" &i <> "$J" &i Then
Sheets("Calculate").Range("$F:$F" &i).Copy
Sheets("Calculate").Range("$J:$J" &i).PasteSpecial (xlPasteValues)
Application.CutCopyMode = False
End If
To this:
Set curCell = Sheets("Calculate").Range("F2:F" & i)
If curCell <> Sheets("Calculate").Range("$J" & i) Then
Sheets("Calculate").Range("$J:$J" &i).Value = curCell.Value
End If
May need to do some more teaking as I notice you're working with SpecialCells which essentially filters the range, so iterating For i = 1 to n... probably does not work. Maybe something like:
Dim rngCalc as Range
Set rngCalc = Sheets("Calculate").Range("E:E").Cells.SpecialCells(xlCellTypeConstants)
For each curCell in rngCalc.Cells
If curCell <> curCell.Offset(0, 4) Then
curCell.Offset(0, 4).Value = curCell.Value
End If
Next
EDIT: this sub will calculate the points for the last transaction (identified as the furthest-right column containing transactions) and write them down in column C.
Option Explicit
Sub UpdateCurrentPurchase()
Dim CalcSheet As Worksheet
Dim LastTransRange As Range, TargetRange As Range
Dim LastTransCol As Long, LastTransRow As Long
Dim PurchaseArray() As Variant
Dim Points As Long, Index As Long
'set references up-front
Set CalcSheet = ThisWorkbook.Worksheets("Calculate")
With CalcSheet
LastTransCol = .Cells(2, .Columns.Count).End(xlToLeft).Column '<~ find the last column
LastTransRow = .Cells(.Rows.Count, LastTransCol).End(xlUp).Row
Set LastTransRange = .Range(.Cells(2, LastTransCol), .Cells(LastTransRow, LastTransCol))
Set TargetRange = .Range(.Cells(2, 6), .Cells(LastTransRow, 6)) '<~ column F is the Current Purchase Col
LastTransRange.Copy Destination:=TargetRange '<~ copy last transactions to Current Purchase Col
End With
'pull purchases into a variant array
PurchaseArray = TargetRange
'calculate points
For Index = 1 To LastTransRow
Points = Int(PurchaseArray(Index, 1) / 10) '<~ calculate points
CalcSheet.Cells(Index + 1, 3) = Points '<~ write out the points amount in col C
Next Index
End Sub
ORIGINAL RESPONSE: I think the below will get you where you're going. That being said, it seems like simply overwriting column J with column F (as values) might be the fastest way to an acceptable answer, so if that's the case we can re-work this code to be much quicker using Range objects.
Option Explicit
Private Sub CalculateRewards_Click()
CopyPaste
End Sub
Sub CopyPaste()
Dim LastRow As Long, Counter As Long
Dim cSheet As Worksheet '<~ add a worksheet reference to save some typing
'set references up front
Set cSheet = ThisWorkbook.Worksheets("Calculate")
With cSheet
LastRow = .Range("E" & .Rows.Count).End(xlUp).Row '<~ set loop boundary
'loop that compares the value in column 6 (F) to the value in
'column 10 (J) and writes the value from F to J if they are not equal
For Counter = 1 To LastRow
If .Cells(Counter, 6).Value <> .Cells(Counter, 10).Value Then
.Cells(Counter, 10) = .Cells(Counter, 6)
End If
Next Counter
End With
End Sub