Related
I am writing a VBA code on excel using loops to go through 10000+ lines.
Here is an example of the table
And here is the code I wrote :
Sub Find_Matches()
Dim wb As Workbook
Dim xrow As Long
Set wb = ActiveWorkbook
wb.Worksheets("Data").Activate
tCnt = Sheets("Data").UsedRange.Rows.Count
Dim e, f, a, j, h As Range
xrow = 2
Application.ScreenUpdating = False
Application.Calculation = xlManual
For xrow = 2 To tCnt Step 1
Set e = Range("E" & xrow)
Set f = e.Offset(0, 1)
Set a = e.Offset(0, -4)
Set j = e.Offset(0, 5)
Set h = e.Offset(0, 3)
For Each Cell In Range("E2:E" & tCnt)
If Cell.Value = e.Value Then
If Cell.Offset(0, 1).Value = f.Value Then
If Cell.Offset(0, -4).Value = a.Value Then
If Cell.Offset(0, 5).Value = j.Value Then
If Cell.Offset(0, 3).Value = h.Value Then
If (e.Offset(0, 7).Value) + (Cell.Offset(0, 7).Value) = 0 Then
Cell.EntireRow.Interior.Color = vbYellow
e.EntireRow.Interior.Color = vbYellow
End If
End If
End If
End If
End If
End If
Next
Next
End Sub
As you can imagine, this is taking a lot of time to go through 10000+ lines and I would like to find a faster solution. There must be a method I don't think to avoid the over looping
Here are the condition :
For each line, if another line anywhere in the file has the exact same
:
Buyer ID (col. E)
`# purchased (col. F)
Product ID (col.A)
Payment (col. J)
Date purchased (col. H)
Then, if the SUM of the Amount (col. L) the those two matching line is
0, then color both rows in yellow.
Note that extra columns are present and not being compared (eg- col. B) but are still important for the document and cannot be deleted to ease the process.
Running the previous code, in my example, row 2 & 5 get highlighted :
This is using nested dictionaries and arrays to check all conditions
Timer with my test data: Rows: 100,001; Dupes: 70,000 - Time: 14.217 sec
Option Explicit
Public Sub FindMatches()
Const E = 5, F = 6, A = 1, J = 10, H = 8, L = 12
Dim ur As Range, x As Variant, ub As Long, d As Object, found As Object
Set ur = ThisWorkbook.Worksheets("Data").UsedRange
x = ur
Set d = CreateObject("Scripting.Dictionary")
Set found = CreateObject("Scripting.Dictionary")
Dim r As Long, rId As String, itm As Variant, dupeRows As Object
For r = ur.Row To ur.Rows.Count
rId = x(r, E) & x(r, F) & x(r, A) & x(r, J) & x(r, H)
If Not d.Exists(rId) Then
Set dupeRows = CreateObject("Scripting.Dictionary")
dupeRows(r) = 0
Set d(rId) = dupeRows
Else
For Each itm In d(rId)
If x(r, L) + x(itm, L) = 0 Then
found(r) = 0
found(itm) = 0
End If
Next
End If
Next
Application.ScreenUpdating = False
For Each itm In found
ur.Range("A" & itm).EntireRow.Interior.Color = vbYellow
Next
Application.ScreenUpdating = True
End Sub
Before
After
I suggest a different approach altogether: add a temporary column to your data that contains a concatenation of each cell in the row. This way, you have:
A|B|C|D|E
1|Mr. Smith|500|A|1Mr. Smith500A
Then use Excel's conditional formatting on the temporary column, highlighting duplicate values. There you have your duplicated rows. Now it's only a matter of using a filter to check which ones have amounts equal to zero.
You can use the CONCATENATE function; it requires you to specify each cell separately and you can't use a range, but in your case (comparing only some of the columns) it seems like a good fit.
Maciej's answer is easy to implement (if you can add columns to your data without interrupting anything), and I would recommend it if possible.
However, for the sake of answering your question, I will contribute a VBA solution as well. I tested it on dataset that is a bit smaller than yours, but I think it will work for you. Note that you might have to tweak it a little (which row you start on, table name, etc) to fit your workbook.
Most notably, the segment commented with "Helper column" is something you most likely will have to adjust - currently, it compares every cell between A and H for the current row, which is something you may or may not want.
I've tried to include a little commentary in the code, but it's not much. The primary change is that I'm using in-memory processing of an array rather than iterating over a worksheet range (which for larger datasets should be exponentially faster).
Option Base 1
Option Explicit
' Uses ref Microsoft Scripting Runtime
Sub Find_Matches()
Dim wb As Workbook, ws As Worksheet
Dim xrow As Long, tCnt As Long
Dim e As Range, f As Range, a As Range, j As Range, h As Range
Dim sheetArr() As Variant, arr() As Variant
Dim colorTheseYellow As New Dictionary, colorResults() As String, dictItem As Variant
Dim arrSize As Long, i As Long, k As Long
Dim c As Variant
Set wb = ThisWorkbook
Set ws = wb.Worksheets("Data")
ws.Activate
tCnt = ws.UsedRange.Rows.Count
xrow = 2
Application.ScreenUpdating = False
Application.Calculation = xlManual
' Read range into an array so we process in-memory
sheetArr = ws.Range("A2:H" & tCnt)
arrSize = UBound(sheetArr, 1)
' Build new arr with "helper column"
ReDim arr(1 To arrSize, 1 To 9)
For i = 1 To arrSize
For k = 1 To 8
arr(i, k) = sheetArr(i, k)
arr(i, 9) = CStr(arr(i, 9)) & CStr(arr(i, k)) ' "Helper column"
Next k
Next i
' Iterate over array & build collection to indicate yellow lines
For i = LBound(arr, 1) To UBound(arr, 1)
If Not colorTheseYellow.Exists(i) Then colorResults = Split(ReturnLines(arr(i, 9), arr), ";")
For Each c In colorResults
If Not colorTheseYellow.Exists(CLng(c)) Then colorTheseYellow.Add CLng(c), CLng(c)
Next c
Next i
' Enact row colors
For Each dictItem In colorTheseYellow
'Debug.Print "dict: "; dictItem
If dictItem <> 0 Then ws.ListObjects(1).ListRows(CLng(dictItem)).Range.Interior.Color = vbYellow
Next dictItem
End Sub
Function ReturnLines(ByVal s As String, ByRef arr() As Variant) As String
' Returns a "Index;Index" string indicating the index/indices where the second, third, etc. instance(s) of s was found
' Returns "0;0" if 1 or fewer matches
Dim i As Long
Dim j As Long
Dim tmp As String
ReturnLines = 0
j = 0
tmp = "0"
'Debug.Print "arg: " & s
For i = LBound(arr, 1) To UBound(arr, 1)
If arr(i, 9) = s Then
j = j + 1
'Debug.Print "arr: " & arr(i, 9)
'Debug.Print "ReturnLine: " & i
tmp = tmp & ";" & CStr(i)
End If
Next i
'If Left(tmp, 1) = ";" Then tmp = Mid(tmp, 2, Len(tmp) - 1)
'Debug.Print "tmp: " & tmp
If j >= 2 Then
ReturnLines = tmp
Else
ReturnLines = "0;0"
End If
End Function
On my simple dataset, it yields this result (marked excellently with freehand-drawn color indicators):
Thanks everybody for your answers,
Paul Bica's solution actually worked and I am using a version of this code now.
But, just to animate the debate, I think I also found another way around my first code, inspired by Maciej's idea of concatenating the cells and using CStr to compare the values and, of course Vegard's in-memory processing by using arrays instead of going through the workbook :
Sub Find_MatchesStr()
Dim AmountArr(300) As Variant
Dim rowArr(300) As Variant
Dim ws As Worksheet
Dim wb As Workbook
Set ws = ThisWorkbook.Sheets("Data")
ws.Activate
Range("A1").Select
rCnt = ws.Cells.SpecialCells(xlCellTypeLastCell).Row
For i = 2 To rCnt
If i = rCnt Then
Exit For
Else
intCnt = 0
strA = ws.Cells(i, 1).Value
strE = ws.Cells(i, 5).Value
strF = ws.Cells(i, 6).Value
strH = ws.Cells(i, 8).Value
strL = ws.Cells(i, 10).Value
For j = i To rCnt - 1
strSearchA = ws.Cells(j, 1).Value
strSearchE = ws.Cells(j, 5).Value
strSearchF = ws.Cells(j, 6).Value
strSearchH = ws.Cells(j, 8).Value
strSearchL = ws.Cells(j, 10).Value
If CStr(strE) = CStr(strSearchE) And CStr(strA) = CStr(strSearchA) And CStr(strF) = CStr(strSearchF) And CStr(strH) = CStr(strSearchH) And CStr(strL) = CStr(strSearchL) Then
AmountArr(k) = ws.Cells(j, 12).Value
rowArr(k) = j
intCnt = intCnt + 1
k = k + 1
Else
Exit For
End If
Next
strSum = 0
For s = 0 To UBound(AmountArr)
If AmountArr(s) <> "" Then
strSum = strSum + AmountArr(s)
Else
Exit For
End If
Next
strAppenRow = ""
For b = 0 To UBound(rowArr)
If rowArr(b) <> "" Then
strAppenRow = strAppenRow & "" & rowArr(b) & "," & AmountArr(b) & ","
Else
Exit For
End If
Next
If intCnt = 1 Then
Else
If strSum = 0 Then
For rn = 0 To UBound(rowArr)
If rowArr(rn) <> "" Then
Let rRange = rowArr(rn) & ":" & rowArr(rn)
Rows(rRange).Select
Selection.Interior.Color = vbYellow
Else
Exit For
End If
Next
Else
strvar = ""
strvar = Split(strAppenRow, ",")
For ik = 1 To UBound(strvar)
If strvar(ik) <> "" Then
strVal = CDbl(strvar(ik))
For ik1 = ik To UBound(strvar)
If strvar(ik1) <> "" Then
strVal1 = CDbl(strvar(ik1))
If strVal1 + strVal = 0 Then
Let sRange1 = strvar(ik - 1) & ":" & strvar(ik - 1)
Rows(sRange1).Select
Selection.Interior.Color = vbYellow
Let sRange = strvar(ik1 - 1) & ":" & strvar(ik1 - 1)
Rows(sRange).Select
Selection.Interior.Color = vbYellow
End If
Else
Exit For
End If
ik1 = ik1 + 1
Next
Else
Exit For
End If
ik = ik + 1
Next
End If
End If
i = i + (intCnt - 1)
k = 0
Erase AmountArr
Erase rowArr
End If
Next
Range("A1").Select
End Sub
I still have some mistakes (rows not higlighted when they should be), the above code is not perfect, but I thought it'd be OK to give you an idea of where I was going before Paul Bica's solution came in.
Thanks again !
If your data is only till column L, then use below code, I found it is taking less time to run....
Sub Duplicates()
Application.ScreenUpdating = False
Dim i As Long, lrow As Long
lrow = Cells(Rows.Count, 1).End(xlUp).Row
Range("O2") = "=A2&E2&F2&J2&L2"
Range("P2") = "=COUNTIF(O:O,O2)"
Range("O2:P" & lrow).FillDown
Range("O2:O" & lrow).Copy
Range("O2:O" & lrow).PasteSpecial xlPasteValues
Application.CutCopyMode = False
For i = 1 To lrow
If Cells(i, 16) = 2 Then
Cells(i, 16).EntireRow.Interior.Color = vbYellow
End If
Next
Application.ScreenUpdating = True
Range("O:P").Delete
Range("A1").Select
MsgBox "Done"
End Sub
I have a data like this :
A049
A050
A051
A053
A054
A055
A056
A062
A064
A065
A066
And I want the output like :
As you can see, I want the ranges which are in consecutive order
I am trying some thing like this:
Private Sub CommandButton1_Click()
Set wb = ThisWorkbook
lastRow = wb.Sheets("Sheet1").Range("A" & wb.Sheets("Sheet1").Rows.Count).End(xlUp).Row
For i = 2 To lastRow
r = wb.Sheets("Sheet1").Range("A" & i).Value
If wb.Sheets("Sheet1").Range("A" & i).Value = wb.Sheets("Sheet1").Range("A" & i+1).Value
Next i
End Sub
But not helping me
Am feeling charitable so have tried some code which should work. It assumes your starting values are in A1 down and puts results in C1 down.
Sub x()
Dim v1, v2(), i As Long, j As Long
v1 = Range("A1", Range("A" & Rows.Count).End(xlUp)).Value
ReDim v2(1 To UBound(v1, 1), 1 To 2)
For i = LBound(v1, 1) To UBound(v1, 1)
j = j + 1
v2(j, 1) = v1(i, 1)
If i <> UBound(v1, 1) Then
Do While Val(Right(v1(i + 1, 1), 3)) = Val(Right(v1(i, 1), 3)) + 1
i = i + 1
If i = UBound(v1, 1) Then
v2(j, 2) = v1(i, 1)
Exit Do
End If
Loop
End If
If v1(i, 1) <> v2(j, 1) Then v2(j, 2) = v1(i, 1)
Next i
Range("C1").Resize(j, 2) = v2
End Sub
Try the below code
Private Sub CommandButton1_Click()
Set wb = ThisWorkbook
lastRow = wb.Sheets("Sheet1").Range("A" & wb.Sheets("Sheet1").Rows.Count).End(xlUp).Row
Dim lastNum, Binsert As Integer
Dim firstCell, lastCell, currentCell As String
Binsert = 1
lastNum = getNum(wb.Sheets("Sheet1").Range("A1").Value)
firstCell = wb.Sheets("Sheet1").Range("A1").Value
For i = 2 To lastRow
activeNum = getNum(wb.Sheets("Sheet1").Range("A" & i).Value)
currentCell = wb.Sheets("Sheet1").Range("A" & i).Value
If (activeNum - lastNum) = 1 Then
'nothing
Else
lastCell = wb.Sheets("Sheet1").Range("A" & (i - 1)).Value
wb.Sheets("Sheet1").Range("B" & Binsert).FormulaR1C1() = firstCell
If (firstCell <> lastCell) Then
wb.Sheets("Sheet1").Range("C" & Binsert).FormulaR1C1() = lastCell
End If
Binsert = Binsert + 1
firstCell = wb.Sheets("Sheet1").Range("A" & i).Value
End If
lastNum = activeNum
Next i
'last entry
wb.Sheets("Sheet1").Range("B" & Binsert).FormulaR1C1() = firstCell
If (firstCell <> currentCell) Then
wb.Sheets("Sheet1").Range("C" & Binsert).FormulaR1C1() = currentCell
End If
End Sub
Public Function getNum(ByVal num As String) As Integer
getNum = Val(Mid(num, 2))
End Function
Another solution. It loops backwards from last row to first row.
Option Explicit
Public Sub FindConsecutiveValues()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim lRow As Long 'find last row
lRow = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
Dim lVal As String 'remember last value (stop value)
lVal = ws.Range("A" & lRow).Value
Const fRow As Long = 2 'define first data row
Dim i As Long
For i = lRow To fRow Step -1 'loop from last row to first row backwards
Dim iVal As Long
iVal = Val(Right(ws.Range("A" & i).Value, Len(ws.Range("A" & i).Value) - 1)) 'get value of row i without A so we can calculate
Dim bVal As Long
bVal = 0 'reset value
If i <> fRow Then 'if we are on the first row there is no value before
bVal = Val(Right(ws.Range("A" & i - 1).Value, Len(ws.Range("A" & i - 1).Value) - 1)) 'get value of row i-1 without A
End If
If iVal - 1 = bVal Then
ws.Rows(i).Delete 'delete current row
Else
If lVal <> ws.Range("A" & i).Value Then 'if start and stop value are not the same …
ws.Range("B" & i).Value = lVal 'write stop value in column B
End If
lVal = ws.Range("A" & i - 1).Value 'remember now stop value
End If
Next i
End Sub
Hoping for help form an Excel/VBA wizard on this problem. I have a possible vision of what i need, but lack the expertise to pull it off.
Essentially the problem combines the use of a countifs formula (with multiple criteria) along with counting unique strings in a column containing comma-delimited strings like this:
Criteria1 | Criteria2 |Names
A | X |Bob
B | Y |Cam;Bob
A | Y |Dan;Ava
A | Y |Ava;Cam
^In this super-simplified example, it would be like counting unique names where Criteria1 = A & criteria2 = Y. Answer = 3 (Cam, Dan, Ava)
So far, i've been able to find a VBA solution (from here)that counts unique strings in a given column like "names" above, but I don't know how to combine that with countifs-style criteria to only pass certain parts of the names range to that function.
I have created an xlsm spreadsheet that further elaborates on the problem with better sample data, expected results and the partial VBA solution I have so far:
xlsx
edit: I'm using Excel 2013
edit2: uploaded xlsx in addition to xlsm. VBA code i'm currently using is below. Note that I copied this form another source and I don't really understand how the scripting.dictionary stuff works :/
Function cntunq(ByVal rng As Range)
' http://www.mrexcel.com/forum/excel-questions/437952-counting-unique-values-seperate-comma.html
Dim cl As Range, i As Integer
Dim dic1, ar
ar = Split(Replace(Join(Application.Transpose(rng), ";"), vbLf, ""), ";")
Debug.Print Join(ar, ";")
Set dic1 = CreateObject("Scripting.Dictionary")
dic1.CompareMode = vbTextCompare
For i = 0 To UBound(ar)
dic1(ar(i)) = ""
Next i
cntunq = dic1.Count
End Function
Edit3: The above code just does the counting of unique values in a given range with ;-delimited strings. The part i don't know is how to modify this to take paramArray of conditions
Here it is in a UDF using a dictionary:
Function MyCount(critRng As Range, crit As String, critRng2 As Range, crit2 As String, cntRng As Range, delim As String) As Long
Dim critarr(), critarr2(), cntarr()
Set dict = CreateObject("Scripting.Dictionary")
critarr = critRng.Value
cntarr = cntRng.Value
critarr2 = critRng2.Value
If UBound(critarr, 1) <> UBound(cntarr, 1) Then Exit Function
For i = LBound(critarr, 1) To UBound(critarr, 1)
If critarr(i, 1) = crit And critarr2(i, 1) = crit2 Then
splt = Split(cntarr(i, 1), delim)
For j = LBound(splt) To UBound(splt)
On Error Resume Next
dict.Add splt(j), splt(j)
On Error GoTo 0
Next j
End If
Next i
MyCount = dict.Count
End Function
Put that in a module and you would call it like a formula:
=MyCount($A$2:$A$5,"A",$B$2:$B$5,"Y",$C$2:$C$5,";")
Edit as per Comments
This will allow an Array entry, which will allow many conditions:
Function MyCount2(delim As String, rsltArr()) As Long
Set dict = CreateObject("Scripting.Dictionary")
Dim splt() As String
Dim i&, j&
For i = LBound(rsltArr, 1) To UBound(rsltArr, 1)
If rsltArr(i, 1) <> "False" And rsltArr(i, 1) <> "" Then
splt = Split(rsltArr(i, 1), delim)
For j = LBound(splt) To UBound(splt)
On Error Resume Next
dict.Add splt(j), splt(j)
On Error GoTo 0
Next j
End If
Next i
MyCount2 = dict.Count
End Function
This then is entered as the following array formula:
=MyCount2(";",IF(($A$2:$A$5="A")*($B$2:$B$5="Y"),$C$2:$C$5))
Being an array formula it needs to be confirmed with Ctrl-Shift-Enter when exiting edit mode instead of Enter. If done correctly then Excel will put {} around the formula.
If you want more criteria, then add another Boolean multiply to the existing in the first criterion of the IF() statement. So if you wanted to test if column Z was greater than 0 you would add * ($Z$2:$Z$5>0) after the column B test.
Here is a non array formula that uses ParamArray.
Function MyCount3(cntrng As Range, delim As String, ParamArray t()) As Long
Set dict = CreateObject("Scripting.Dictionary")
Dim cntArr As Variant
cntArr = cntrng.Value
Dim tArr() As Boolean
Dim splt() As String
Dim I&, l&
Dim tpe As String
ReDim tArr(1 To t(0).Rows.Count)
For l = 1 To t(0).Rows.Count
For I = LBound(t) To UBound(t) Step 2
If Not tArr(l) Then
If InStr("<>=", Left(t(I + 1), 1)) = 0 Then t(I + 1) = "=" & t(I + 1)
If InStr("<>=", Mid(t(I + 1), 2, 1)) > 0 Then Z = 2 Else Z = 1
tArr(l) = Application.Evaluate("NOT(""" & t(I).Item(l).Value & """" & Left(t(I + 1), Z) & """" & Mid(t(I + 1), Z + 1) & """)")
End If
Next I
Next l
For l = 1 To UBound(tArr)
If Not tArr(l) Then
splt = Split(cntArr(l, 1), delim)
For j = LBound(splt) To UBound(splt)
On Error Resume Next
dict.Add splt(j), splt(j)
On Error GoTo 0
Next j
End If
Next l
MyCount3 = dict.Count
End Function
It is entered similar to SUMIFS,COUNTIFS.
The first criterion is the range that needs to be split and counted.
The second is the delimiter on which it should split.
Then the rest is entered in pairs.
=MyCount3($C$2:$C$5,";",$A$2:$A$5,"A",$B$2:$B$5,"Y")
Consider:
Sub poiuyt()
Dim N As Long, i As Long, c As Collection
Set c = New Collection
N = Cells(Rows.Count, 1).End(xlUp).Row
For i = 2 To N
If Cells(i, 1) = "A" And Cells(i, 2) = "Y" Then
arr = Split(Cells(i, 3), ";")
For Each a In arr
On Error Resume Next
c.Add a, CStr(a)
On Error GoTo 0
Next a
End If
Next i
MsgBox c.Count
End Sub
I took a different, possibly more complicated approach. You can specify the criteria directly on the sheet.
The function is UniqueNames(Range of Data, Range of Names, Range of Rules, Optional AndRules = True, Optional PrintNames = False)
Here is my sample sheet
I'm using the function 4 times in
- Range("E16") as UniqueNames(A1:F11,G1:G11,A13:B16,FALSE)
- Range("E17") as UniqueNames(A1:F11,G1:G11,A13:B16)
- Range("F16") as UniqueNames(A1:F11,G1:G11,A13:B16,FALSE,TRUE)
- Range("F17") as UniqueNames(A1:F11,G1:G11,A13:B16,,TRUE)
The following operators for conditions are acceptable =,<,>,<=,>=,!=
The operator must be followed by a single space and either
- a constant value e.g. Complete
- a function of a value, e.g. Status(Project#6)
An empty condition is invalid
Here's the code: Note: There is a private function as well
Public Function UniqueNames(DataSource As Range, ResultsSource As Range, RulesSource As Range, _
Optional AndRules As Boolean = True, Optional PrintNames As Boolean = False) As String
' Return N unique names and who
' Split Indexed Expressions
Dim iChar As Integer
' Expression to eval
Dim Expression() As String
Dim expr As Variant
' Results
Dim Results As Variant
' Get Data into variant array
Dim Data As Variant
' Get Rules into variant array of NRows x 2
Dim Rules As Variant
iChar = 0
Data = DataSource
If RulesSource.Columns.Count = 1 Then
Rules = Union(RulesSource, RulesSource.Offset(0, 1))
ElseIf RulesSource.Columns.Count > 2 Then
Rules = RulesSource.Resize(RulesSource.Rows.Count, 2)
Else
Rules = RulesSource
End If
Results = ResultsSource.Resize(ResultsSource.Rows.Count, UBound(Rules))
For i = LBound(Rules) + 1 To UBound(Rules)
For j = LBound(Data, 2) To UBound(Data, 2)
If Rules(i, 1) = Data(1, j) Then
' rules must be "operator condition"
Expression = Split(Rules(i, 2), " ", 2)
Expression(1) = Trim(Expression(1))
' determine which expression is this
' Convert expression when an item of something e.g. EndDate(10)
iChar = InStr(Expression(1), "(")
If iChar > 0 Then
expr = ExprToVal(Data, Left$(Expression(1), iChar - 1), _
Mid$(Expression(1), iChar + 1, Len(Expression(1)) - iChar - 1))
Else
expr = Expression(1)
End If
For k = LBound(Data, 1) + 1 To UBound(Data, 1)
Results(k, i) = False
Select Case (Expression(0))
Case "="
If Data(k, j) <> "" And LCase$(Data(k, j)) = LCase$(expr) Then Results(k, i) = True
Case "<"
If Data(k, j) <> "" And LCase$(Data(k, j)) < LCase$(expr) Then Results(k, i) = True
Case ">"
If Data(k, j) <> "" And LCase$(Data(k, j)) > LCase$(expr) Then Results(k, i) = True
Case "<="
If Data(k, j) <> "" And LCase$(Data(k, j)) <= LCase$(expr) Then Results(k, i) = True
Case ">="
If Data(k, j) <> "" And LCase$(Data(k, j)) >= LCase$(expr) Then Results(k, i) = True
Case "!="
If Data(k, j) <> "" And LCase$(Data(k, j)) <> LCase$(expr) Then Results(k, i) = True
End Select
Next k
End If
Next j
Next i
' create one list where all three rules are true
Data = Results
Set Results = Nothing
ReDim Results(LBound(Data, 1) + 1 To UBound(Data, 1), 1 To 2) As Variant
' results now has the names w/a number representing how many rules were met
For i = LBound(Data, 1) + 1 To UBound(Data, 1)
Results(i, 1) = Data(i, 1)
Results(i, 2) = 0
For j = LBound(Data, 2) + 1 To UBound(Data, 2)
If Data(i, j) Then Results(i, 2) = Results(i, 2) + 1
Next j
Next i
' put that back into data
Data = Results
Set Results = Nothing
Results = ""
For i = LBound(Data, 1) + 1 To UBound(Data, 1)
If Data(i, 2) = UBound(Rules, 1) - LBound(Rules, 1) Then
Results = Results & Data(i, 1) & ";"
ElseIf AndRules = False And Data(i, 2) > 0 Then
Results = Results & Data(i, 1) & ";"
End If
Next i
' split that into expression
Expression = Split(Results, ";")
For i = LBound(Expression) To UBound(Expression)
For j = i + 1 To UBound(Expression)
If Expression(i) = Expression(j) Then Expression(j) = ""
Next j
Next i
iChar = 0
Results = ""
For i = LBound(Expression) To UBound(Expression)
If Expression(i) <> "" Then
Results = Results & Expression(i) & ";"
iChar = iChar + 1
End If
Next i
UniqueNames = ""
If PrintNames Then
' prints number of unique names and the names
UniqueNames = Results
Else
' prints number of unique names
UniqueNames = CStr(iChar)
End If
End Function
Private Function ExprToVal(Data As Variant, expr As String, Index As String) As Variant
Dim Row As Integer
Dim Col As Integer
Dim sCol As Variant
' Get what type of data this is
For i = LBound(Data, 2) To UBound(Data, 2)
sCol = Replace(Index, Data(1, i), "", 1, 1, vbTextCompare)
If IsNumeric(sCol) Then
Col = i
Exit For
ElseIf LCase$(Left$(Index, Len(Data(1, i)))) = LCase$(Data(1, i)) Then
Col = i
Exit For
End If
Next i
' now find the row of the value
For i = LBound(Data, 1) + 1 To UBound(Data, 1)
If LCase$(Data(i, Col)) = LCase$(sCol) Then
Row = i
Exit For
End If
Next i
' find the column of the value
For i = LBound(Data, 2) To UBound(Data, 2)
If LCase$(Data(1, i)) = LCase$(expr) Then
Col = i
Exit For
End If
Next i
If Row >= LBound(Data, 1) And Row <= UBound(Data, 1) And _
Col >= LBound(Data, 2) And Col <= UBound(Data, 2) Then
ExprToVal = Data(Row, Col)
Else
ExprToVal = ""
End If
End Function
First, I'd like to apologize for this poor question and I hope it doesn't upset anyone here. Since I'm not that good at speaking English to convey my request, so please have a look to the cited links in order to get a clear explanation to this question.
I'm trying to find the solution to this question of mine. I started my attempt by searching for the same number in column A and column B (Debit and Credit). I used the looping-trough-array method to do it instead of employing the Find function like this question since I think it's faster.
Suppose that I have the following set data in Sheet1 and start from row 1 column A:
D e b i t Cr e d i t
20 13
14 13
13 14
14 17
19 19
11 20
17 14
20 12
19 19
20 15
20 12
13 11
12 19
13 20
19 19
20 11
11 16
10 16
19 19
20 11
Now, I'd like to process the data set above to something like this:
Basically, I need to find the same value of debit and credit in a specific row and match it with debit and credit in another row. Column C (Row) indicates the matched values. For example, the debit value in row 2 match with the credit value in row 15 and vice-versa. And numbers in column D (ID Match) are the label numbers to indicate the order of the matched data that's found first. This is my code in an attempt to implement the task:
Public i As Long, j As Long, k As Long, Last_Row As Long
Public DC, Row_Data, ID_Match
Sub Quick_Match()
T0 = Timer
k = 0
Last_Row = Cells(Rows.Count, "A").End(xlUp).Row
ReDim DC(1 To Last_Row - 1, 1 To 2)
ReDim Row_Data(1 To Last_Row - 1, 1 To 1)
ReDim ID_Match(1 To Last_Row - 1, 1 To 1)
DC = Range("A2:B" & Last_Row)
For i = 1 To Last_Row - 1
If DC(i, 1) <> "" Then
k = k + 1
For j = 1 To Last_Row - 1
If DC(i, 1) <> DC(i, 2) Then
If DC(i, 1) = DC(j, 2) And DC(i, 2) = DC(j, 1) Then
Call Row_Label
Exit For
Else
Row_Data(i, 1) = "No Match"
End If
Else
If i <> j Then
If DC(i, 1) = DC(j, 1) And DC(i, 2) = DC(j, 2) Then
Call Row_Label
Exit For
Else
Row_Data(i, 1) = "No Match"
End If
End If
End If
Next j
End If
If Row_Data(i, 1) = "No Match" Then
k = k - 1
End If
Next i
Range("C2:C" & Last_Row) = Row_Data
Range("D2:D" & Last_Row) = ID_Match
InputBox "The runtime of this program is ", "Runtime", Timer - T0
End Sub
Sub Row_Label()
Row_Data(i, 1) = j + 1
ID_Match(i, 1) = k
Row_Data(j, 1) = i + 1
ID_Match(j, 1) = k
DC(i, 1) = ""
DC(i, 2) = ""
DC(j, 1) = ""
DC(j, 2) = ""
End Sub
Though it's a bit slow on its performance, but it works fine. It completes in about 25 seconds on my machine for processing 10,000 rows of data (the data set file can be downloaded on this link for testing the running time of your code and mine). So I'm wondering if there is a more effective way for doing this. Could anyone come up with either a shorter version or a quicker version? Please do share your attempt.
Our ID's don't are different because I don't search ahead in the list for a match. I iterate over the list one time adding keys to a dictionary. If a find a key already exists that matches your criteria assign the new ID and row numbers.
Let me know if this meets your criteria.
Sub DebitCreditCrossMatch()
Dim dictKeys As Object, dictRows As Object
Dim DebitKey As String, CreditKey As String
Dim arrDebit, arrCredit, arrMatchRow, arrMatchID, items, keys
Dim ID As Long, rw As Long, x As Long, lastRow As Long
lastRow = Cells(Rows.count, "A").End(xlUp).Row
arrDebit = Range("A1", "A" & lastRow).Value
arrCredit = Range("B1", "B" & lastRow).Value
arrMatchRow = Range("C1", "C" & lastRow).Value
arrMatchID = Range("D1", "D" & lastRow).Value
Set dictKeys = CreateObject("Scripting.Dictionary")
For x = 2 To lastRow
arrMatchRow(x, 1) = "No Match"
arrMatchID(x, 1) = "No Match"
DebitKey = arrDebit(x, 1) & ":" & arrCredit(x, 1)
CreditKey = arrCredit(x, 1) & ":" & arrDebit(x, 1)
If dictKeys.Exists(CreditKey) Then
Set dictRows = dictKeys(CreditKey)
items = dictRows.items
keys = dictRows.keys
rw = CLng(items(0))
ID = ID + 1
arrMatchRow(x, 1) = rw
arrMatchRow(rw, 1) = x
arrMatchID(x, 1) = ID
arrMatchID(rw, 1) = ID
dictRows.Remove keys(0)
If dictRows.count = 0 Then dictKeys.Remove CreditKey
ElseIf dictKeys.Exists(DebitKey) Then
Set dictRows = dictKeys(DebitKey)
dictRows.Add x, x
Else
Set dictRows = CreateObject("Scripting.Dictionary")
dictRows.Add x, x
dictKeys.Add DebitKey, dictRows
End If
Next
Range("C1", "C" & lastRow).Value = arrMatchRow
Range("D1", "D" & lastRow).Value = arrMatchID
Set dictKeys = Nothing
Set dictRows = Nothing
End Sub
I reworked my previous answer introducing a second loop; so that our ID numbers will match.
Sub DebitCreditCrossMatch()
Dim dictKeys As Object, dictRows As Object
Dim DebitKey As String, CreditKey As String
Dim arrDebit, arrCredit, items, keys
Dim arrMatchRow(), arrMatchID()
Dim ID As Long, rw As Long, x As Long, lastRow As Long
lastRow = Cells(Rows.count, "A").End(xlUp).Row
arrDebit = Range("A1", "A" & lastRow).Value
arrCredit = Range("B1", "B" & lastRow).Value
ReDim arrMatchID(lastRow - 2)
ReDim arrMatchRow(lastRow - 2)
Set dictKeys = CreateObject("Scripting.Dictionary")
For x = 2 To lastRow
DebitKey = arrDebit(x, 1) & ":" & arrCredit(x, 1)
CreditKey = arrCredit(x, 1) & ":" & arrDebit(x, 1)
If dictKeys.Exists(CreditKey) Then
Set dictRows = dictKeys(CreditKey)
items = dictRows.items
keys = dictRows.keys
rw = CLng(items(0))
arrMatchRow(x - 2) = rw
arrMatchRow(rw - 2) = x
dictRows.Remove keys(0)
If dictRows.count = 0 Then dictKeys.Remove CreditKey
ElseIf dictKeys.Exists(DebitKey) Then
Set dictRows = dictKeys(DebitKey)
dictRows.Add x, x
Else
Set dictRows = CreateObject("Scripting.Dictionary")
dictRows.Add x, x
dictKeys.Add DebitKey, dictRows
End If
Next
For x = 0 To lastRow - 2
If Not IsEmpty(arrMatchRow(x)) And IsEmpty(arrMatchID(x)) Then
rw = arrMatchRow(x) - 2
arrMatchRow(rw) = x + 2
ID = ID + 1
arrMatchID(x) = ID
arrMatchID(rw) = ID
Else
If IsEmpty(arrMatchRow(x)) Then
arrMatchRow(x) = "No Match"
End If
End If
Next
Range("C2", "C" & lastRow).Value = WorksheetFunction.Transpose(arrMatchRow)
Range("D2", "D" & lastRow).Value = WorksheetFunction.Transpose(arrMatchID)
Set dictKeys = Nothing
Set dictRows = Nothing
End Sub
Assuming there are no duplicated in the Credit-Debit pairs you could use the following methods in a separate module calling matchCreditDebit() and adjusting the ranges in the initialization phase as needed:
Option Explicit
Public Sub matchCreditDebit()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim debit As Range, credit As Range, match As Range, rows As Long
rows = ws.UsedRange.rows.Count
Set credit = ws.Range("A1:A" & rows)
Set debit = ws.Range("B1:B" & rows)
Set match = ws.Range("C1:C" & rows)
match.Offset(1).Clear 'delete previous matched, start with clean slate, offset used to preserve header
Dim foundRanges As Collection, i As Long, r As Range
For i = 2 To rows
If Len(ws.Cells(i, match.Column).Value) = 0 _
And Len(ws.Cells(i, credit.Column).Value) > 0 Then 'check if match is already found and credit has value
Set foundRanges = FindAllInRange(debit, credit.Cells(i, 1).Value) 'first sift, find matching debit with a credit value
If Not foundRanges Is Nothing Then
For Each r In foundRanges
Debug.Print r.Address, ws.Cells(r.Row, credit.Column).Value
If ws.Cells(r.Row, credit.Column).Value = ws.Cells(i, debit.Column) Then 'second sift, match for found credit in debit
ws.Cells(r.Row, match.Column).Value = i
End If
Next r
End If
End If
Next i
End Sub
Public Function FindAllInRange( _
ByRef searchRange As Range, _
ByVal FindWhat As Variant _
) As Collection
Dim result As Collection
Set result = New Collection
Dim nextFound As Range
Set nextFound = searchRange.Cells(searchRange.rows.Count, 1)
Do
Set nextFound = searchRange.Find( _
What:=FindWhat, _
After:=nextFound, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows)
If nextFound Is Nothing Then Exit Do
If collectionContainsRange(result, nextFound) Then Exit Do
result.Add nextFound, nextFound.Address
Loop While True
Set FindAllInRange = result
End Function
Private Function collectionContainsRange(ByRef result As Collection, ByRef rng As Range) As Boolean
collectionContainsRange = False
Dim r As Range
For Each r In result
If StrComp(r.Address, rng.Address, vbTextCompare) = 0 Then
collectionContainsRange = True
Exit Function
End If
Next r
End Function
Generally what this does is find the ranges that match the first criteriaon and put them into a collection and from those sift out the ones that match the second criteria. However if there are duplicate pairs the last found reverse pair will be entered as ref.
Edit comments: Starting inner loop without lctrRow will not do back-checking. Reinstated original code.
Sub test()
'/ Assuming that on Sheet1 starting at A1, four headers are : Debit Credit Row ID Match
Dim lCtrRow As Long
Dim lCtrRow2 As Long
Dim lmatchCount As Long
Dim arrResult
arrResult = Sheet1.UsedRange
'/ Loop through first column Rows
For lCtrRow = LBound(arrResult) To UBound(arrResult)
lmatchCount = 0
arrResult(lCtrRow, 3) = "No Match"
'/ Re-Loop but this time match if A&B = B&A
For lCtrRow2 = LBound(arrResult) + 1 To UBound(arrResult)
If arrResult(lCtrRow, 1) & arrResult(lCtrRow, 2) = arrResult(lCtrRow2, 2) & arrResult(lCtrRow2, 1) Then
'/ If no match then only put down the row number. Avoids overwriting.
If arrResult(lCtrRow, 3) = "No Match" Then
arrResult(lCtrRow, 3) = lCtrRow2
End If
'/ Keep track of no. matches found.
lmatchCount = lmatchCount + 1
arrResult(lCtrRow, 4) = lmatchCount
End If
Next
Next
'/ Dump the processed result back on another sheet
Sheet2.Range("a1").Resize(UBound(arrResult), UBound(arrResult, 2)) = arrResult
End Sub
This worked for me:
Sub Matching()
Dim rng, arr, r1 As Long, r2 As Long, nR As Long
Dim sortId As Long, rwTrack(), s1, s2
'get the input range
With Range("a1").CurrentRegion 'assumes no blank columns/rows
Set rng = .Offset(1, 0).Resize(.Rows.Count - 1)
End With
arr = rng.Value
nR = UBound(arr, 1)
ReDim rwTrack(1 To nR) 'for matching row numbers to sortId
' (should be more like nR/2 but lazy...)
sortId = 1
For r1 = 1 To nR
For r2 = r1 + 1 To nR
If arr(r1, 1) = arr(r2, 2) And arr(r1, 2) = arr(r2, 1) Then
s1 = arr(r1, 4)
s2 = arr(r2, 4)
If Len(s1) = 0 And Len(s2) = 0 Then
'new match - assign new Id
arr(r1, 4) = sortId
arr(r2, 4) = sortId
rwTrack(sortId) = r1 & "," & r2 'capture the rows
sortId = sortId + 1
Else
'already matched: copy the existing Id and track rows
If Len(s1) > 0 And Len(s2) = 0 Then
arr(r2, 4) = s1
rwTrack(s1) = rwTrack(s1) & "," & r2
End If
If Len(s2) > 0 And Len(s1) = 0 Then
arr(r1, 4) = s2
rwTrack(s2) = rwTrack(s2) & "," & r1
End If
End If
End If
Next r2
Next r1
'populate all of the matched row numbers
For r1 = 1 To nR
If arr(r1, 4) <> "" Then arr(r1, 3) = rwTrack(arr(r1, 4))
Next r1
'dump the data back
Range("a1").Offset(1, 0).Resize(nR, UBound(arr, 2)).Value = arr
End Sub
Before and after:
To speed up the matching, can improve thru Algorithm.
Assuming your code are working fine.
1) We can sort Column A then Column B, therefore, your data will be like this
Row A B
2 20 13
3 20 12
4 20 11
.
.
.
998 13 20
999 12 20
1000 11 20
.
.
.
2) While looping Debit column to find the first value 20 within Credit column might have a very huge gap. We can then add in application.Match(20,Range("B:B"),0) to find out the row to start the loop.
Base on above assumption, we can reduce about 1000 times of loop. (in real case, it could be more/less). Application.Match() is lot more faster than looping one by one.
3) Exit the loop, when Credit value is less than Debit value, because we have sort the data in sequence, we can assume there have no possible match, when Credit < Debit.
4) While the use Application.ScreenUpdating = False, can increase the processing speed.
5) Without touch the original data, also can use Application.Match to reduce row by row loop.
Assume, you have 10K records,
first set searchRng as C1:C10000, then match to find the row of the first Debit value (20, base on yr photo),
then we found matched record on Row 7, check if the record match both Debit & Credit, if not reduce the resize of searchRng to C8:C10000 then keep repeating the logic
Sub Match ()
For nRow = 2 to lastRow 'Loop for each row in Column A
set searchRng = Range("C1:C10000")
debitVal = Cells(nRow, "B")
Do until searchRng is Nothing
If IsError(Application.Match(debitVal, searchRng, 0)) then
'No Match
Exit Do
Else
N = Application.Match(debitVal, searchRng, 0)
'Do something to check if Record match, and assign ID Match
If IsRecordMatched Then
'Assign ID
'Matching Range - Cells(nRow,"B")
'Matched Range - Cells(searchRng.Cells(1).Offset(N,0).Rows, "B")
Else
'Resize the searchRng
nSize = searchRng.Cells.Count - (N + 1)
if nSize < 1 then Exit Do
set searchRng = searchRng.Resize(nSize,1)
set searchRng = searchRng.Offset(N + 1,0)
End If
End If
Loop
Next nRow
End Sub
Above code not tested. Please take it as reference.
Improvement
The following code completes less than 2.4 seconds on average. It's twice faster than the previous one and also shorter.
Sub Quick_Match()
Dim i As Long, j As Long, k As Long, Last_Row As Long
Dim DC, Row_Data, ID_Match
Last_Row = Cells(Rows.Count, "A").End(xlUp).Row
ReDim DC(1 To Last_Row - 1, 1 To 2)
ReDim Row_Data(1 To Last_Row - 1, 1 To 1)
ReDim ID_Match(1 To Last_Row - 1, 1 To 1)
DC = Range("A2:B" & Last_Row).Value2
For i = 1 To Last_Row - 2
If DC(i, 1) <> vbNullString Then
k = k + 1
For j = i + 1 To Last_Row - 1
If DC(j, 2) <> vbNullString Then
If DC(i, 1) = DC(j, 2) And DC(i, 2) = DC(j, 1) Then
Row_Data(i, 1) = j + 1: ID_Match(i, 1) = k
Row_Data(j, 1) = i + 1: ID_Match(j, 1) = k
DC(i, 1) = vbNullString: DC(i, 2) = vbNullString
DC(j, 1) = vbNullString: DC(j, 2) = vbNullString
Exit For
End If
End If
Next j
End If
If Row_Data(i, 1) = vbNullString Then
Row_Data(i, 1) = "No Match": k = k - 1
End If
Next i
Range("C2:C" & Last_Row) = Row_Data
Range("D2:D" & Last_Row) = ID_Match
End Sub
[Old answer] Some progress. The following code completes less than 5.2 seconds on average for processing 10,000 rows of data on my machine. Not only is faster, but it's also a bit shorter than the previous one. I changed the looping algorithm to improve its performance. I also use some speedup tricks like using .Value2rather than the default property (.Value) makes Excel do less processing and assigning vbNullString instead of Zero Length String ("") to the elements of the array that has found its match or labeled "No Match" so that the loop procedure won't process it again.
Public i As Long, j As Long, k As Long, Last_Row As Long
Public DC, Row_Data, ID_Match
Sub Quick_Match()
T0 = Timer
k = 0
Last_Row = Cells(Rows.Count, "A").End(xlUp).Row
ReDim DC(1 To Last_Row - 1, 1 To 2)
ReDim Row_Data(1 To Last_Row - 1, 1 To 1)
ReDim ID_Match(1 To Last_Row - 1, 1 To 1)
DC = Range("A2:B" & Last_Row).Value2
For i = 1 To Last_Row - 1
If DC(i, 1) <> vbNullString Then
k = k + 1
For j = 1 To Last_Row - 1
If DC(j, 2) <> vbNullString Then
If DC(i, 1) <> DC(i, 2) Then
If DC(i, 1) = DC(j, 2) And DC(i, 2) = DC(j, 1) Then
Call Row_Label
Exit For
End If
Else
If i <> j Then
If DC(i, 1) = DC(j, 1) And DC(i, 2) = DC(j, 2) Then
Call Row_Label
Exit For
End If
End If
End If
End If
Next j
End If
If Row_Data(i, 1) = vbNullString Then
Row_Data(i, 1) = "No Match"
DC(i, 2) = vbNullString
k = k - 1
End If
Next i
Range("C2:C" & Last_Row) = Row_Data
Range("D2:D" & Last_Row) = ID_Match
InputBox "The runtime of this program is ", "Runtime", Timer - T0
End Sub
Sub Row_Label()
Row_Data(i, 1) = j + 1
ID_Match(i, 1) = k
Row_Data(j, 1) = i + 1
ID_Match(j, 1) = k
DC(i, 2) = vbNullString
DC(j, 1) = vbNullString
DC(j, 2) = vbNullString
End Sub
I need to sync up the values in the curly braces {} found in column C and put them against the user id in column F as seen below.
E.g. on the Emails sheet
becomes this on a new sheet
Sub CopyConditional()
Dim wshS As Worksheet
Dim WhichName As String
Set wshS = ActiveWorkbook.Sheets("Emails")
WhichName = "NewSheet"
Const NameCol = "C"
Const FirstRow = 1
Dim LastRow As Long
Dim SrcRow As Long
Dim TrgRow As Long
Dim wshT As Worksheet
Dim cpt As String
Dim user As String
Dim computers() As String
Dim computer As String
On Error Resume Next
Set wshT = Worksheets(WhichName)
If wshT Is Nothing Then
Set wshT = Worksheets.Add(After:=wshS)
wshT.Name = WhichName
End If
On Error GoTo 0
If wshT.Cells(1, NameCol).value = "" Then
TrgRow = 1
Else
TrgRow = wshT.Cells(wshT.Rows.Count, NameCol).End(xlUp).Row + 1
End If
LastRow = wshS.Cells(wshS.Rows.Count, NameCol).End(xlUp).Row
For SrcRow = FirstRow To LastRow
cpt = wshS.Range("C" & SrcRow).value
user = wshS.Range("F" & SrcRow).value
If InStr(cpt, ":") Then
cpt = Mid(cpt, InStr(1, cpt, ":") + 1, Len(cpt))
End If
If InStr(cpt, ";") Then
computers = Split(cpt, ";")
For i = 0 To UBound(computers)
If computers(i) <> "" Then
wshT.Range("A" & TrgRow).value = user
wshT.Range("B" & TrgRow).value = Mid(Left(computers(i), Len(computers(i)) - 1), 2)
TrgRow = TrgRow + 1
End If
Next
Else
computer = cpt
If computer <> "" Then
wshT.Range("A" & TrgRow).value = user
wshT.Range("B" & TrgRow).value = Mid(Left(computer, Len(computer) - 1), 2)
TrgRow = TrgRow + 1
End If
End If
Next SrcRow
End Sub
I managed to resolve it with the above code but there are 3 niggling issues:
1) The first curly brace is always copied, how do I omit this so something like {Computer1 looks like Computer 1
2) Where there are two computers in a row, then the output looks something like this:
when it should really be split into two different rows i.e.
User 1 | Computer 1
User 1 | Computer 2
3) If there is text after the last curly brace with text in it e.g. {Computer1};{Computer2};Request submitted then that text is added as a new row, I don't want this, I want it to be omitted e.g.
should just be:
User 1 | Computer 1
User 1 | Computer 2
How do I go about rectifying these issues?
Try this:
Sub Collapse()
Dim uRng As Range, cel As Range
Dim comps As Variant, comp As Variant, r As Variant, v As Variant
Dim d As Dictionary '~~> Early bind, for Late bind use commented line
'Dim d As Object
Dim a As String
With Sheet1 '~~> Sheet that contains your data
Set uRng = .Range("F1", .Range("F" & .Rows.Count).End(xlUp))
End With
Set d = CreateObject("Scripting.Dictionary")
With d
For Each cel In uRng
a = Replace(cel.Offset(0, -3), "{", "}")
comps = Split(a, "}")
Debug.Print UBound(comps)
For Each comp In comps
If InStr(comp, "Computer") <> 0 _
And Len(Trim(comp)) <= 10 Then '~~> I assumed max Comp# is 99
If Not .Exists(cel) Then
.Add cel, comp
Else
If IsArray(.Item(cel)) Then
r = .Item(cel)
ReDim Preserve r(UBound(r) + 1)
r(UBound(r)) = comp
.Item(cel) = r
Else
r = Array(.Item(cel), comp)
.Item(cel) = r
End If
End If
End If
Next
Next
End With
For Each v In d.Keys
With Sheet2 '~~> sheet you want to write your data to
If IsArray(d.Item(v)) Then
.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0) _
.Resize(UBound(d.Item(v)) + 1) = v
.Range("B" & .Rows.Count).End(xlUp).Offset(1, 0) _
.Resize(UBound(d.Item(v)) + 1) = Application.Transpose(d.Item(v))
Else
.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0) = v
.Range("B" & .Rows.Count).End(xlUp).Offset(1, 0) = d.Item(v)
End If
End With
Next
Set d = Nothing
End Sub
Above code uses Replace and Split Function to pass your string to array.
a = Replace(cel.Offset(0, -3), "{", "}") '~~> standardize delimiter
comps = Split(a, "}") '~~> split using standard delimiter
Then information are passed to dictionary object using User as key and computers as items.
We filter the items passed to dictionary using Instr and Len Function
If InStr(comp, "Computer") <> 0 _
And Len(Trim(comp)) <= 10 Then
As I've commented, I assumed your max computer number is 99.
Else change 10 to whatever length you need to check.
Finally we return the dictionary information to the target worksheet.
Note: You need to add reference to Microsoft Scripting Runtime if you prefer early bind
Result: I tried it on a small sample data patterned on how I see it in you SS.
So assuming you have this data in Sheet1:
Will output data in Sheet2 like this:
I use a custom parse function for this type of operation:
Sub CopyConditional()
' some detail left out
Dim iRow&, Usern$, Computer$, Computers$
For iRow = ' firstrow To lastrow
Usern = Sheets("Emails").Cells(iRow, "F")
Computers = Sheets("Emails").Cells(iRow, "C")
Do
Computer = zParse(Computers) ' gets one computer
If Computer = "" Then Exit Do
' Store Computer and Usern
Loop
Next iRow
End Sub
Function zParse$(Haystack$) ' find all {..}
Static iPosL& '
Dim iPosR&
If iPosL = 0 Then iPosL = 1
iPosL = InStr(iPosL, Haystack, "{") ' Left
If iPosL = 0 Then Exit Function ' no more
iPosR = InStr(iPosL, Haystack, "}") ' Right
If iPosR = 0 Then MsgBox "No matching }": Stop
zParse = Mid$(Haystack, iPosL + 1, iPosR - iPosL - 1)
iPosL = iPosR
End Function
1) Use the Mid function to drop the first character:
str = "{Computer1"
str = Mid(str,2)
now str = "Computer1"
2) You can use the Split function to separate these out and combine with the Mid function above
str = "{Computer1}{Computer2}"
splt = Split(str,"}")
for a = 0 to Ubound(splt)
result = Mid(splt(a),2)
next a
3) Add a conditional statement to the above loop
str = "{Computer1}{Computer2}"
splt = Split(str,"}")
for a = 0 to Ubound(splt)
if Left(splt(a),1) = "{" then result = Mid(splt(a),2)
next a
Use this loop and send each result to the desired cell (in the for-next loop) and you should be good to go.