Calculate duration of non-continuous overlapping time intervals - vba

I am trying to calculate the total duration of overlap between multiple events. Each event can overlap with multiple other events in any arrangement. I need to calculate the total amount of time any single event overlaps with any other event. The data I have looks like this.
event timeStart timeEnd
1 15:00 22:00
2 12:00 18:00
3 20:00 23:00
4 16:00 17:00
5 10:00 14:00
Output:
event timeOverlap
1 05:00 '03:00 (1,2) + 02:00 (1,3)
2 04:00 '03:00 (1,2) + 01:00 (2,4)
3 02:00 '02:00 (1,3)
4 01:00 '01:00 (2,4)
5 02:00 '02:00 (2,5)
I'm trying to do this in Excel VBA. My main problem right now is finding a way to sum up discontinuous overlaps, e.g. event 1 or event 2. Any help would be appreciated.
Edit: To clarify, I would like to avoid double counting, which is why I didn't include the overlap between (1,4) in the calculation for event 1. The output should show the sum of the overlaps that would result in the largest overlap duration.
Here's part of the code I'm using. Right now it calculates the longest continuous overlap between multiple events. It doesn't sum up discontinuous overlaps.
'DECLARE VARIABLES
Dim timeStart() As Date 'start times of cases
Dim timeEnd() As Date 'end times of cases
Dim ovlpStart() As Double 'start times of overlap regions for cases
Dim ovlpEnd() As Double 'end times of overlap regions for cases
Dim totalRows As Long 'total number of cases`
'RETRIEVE NUMBER OF ROWS
totalRows = WorksheetFunction.CountA(Columns(1))
'STORE COLUMN DATA FROM EXCEL SHEET INTO ARRAYS
ReDim timeStart(1 To totalRows)
ReDim timeEnd(1 To totalRows)
ReDim ovlpStart(1 To totalRows)
ReDim ovlpEnd(1 To totalRows)
'FILL IN ARRAYS WITH DATA FROM SPREADSHEET
For i = 2 To totalRows
timeStart(i) = Cells(i, 3).Value
timeEnd(i) = Cells(i, 4).Value
'Initialize ovlpStart and ovlpEnd
ovlpStart(i) = 1
ovlpEnd(i) = 0
Next
'FILL IN CONCURRENCE COLUMN WITH ALL ZEROS TO START
For i = 2 To totalRows
Cells(i, 6).Value = "0"
Next
'SEARCH FOR CONCURRENT TIME INTERVALS
For i = 2 To totalRows
For j = (i + 1) To totalRows
'Check if the times overlap b/w cases i and j
Dim diff1 As Double
Dim diff2 As Double
diff1 = timeEnd(j) - timeStart(i)
diff2 = timeEnd(i) - timeStart(j)
If diff1 > 0 And diff2 > 0 Then
'Mark cases i and j as concurrent in spreadsheet
Cells(i, 6).Value = "1"
Cells(j, 6).Value = "1"
'Determine overlap start and end b/w cases i and j, store as x and y
Dim x As Double
Dim y As Double
If timeStart(i) > timeStart(j) Then
x = timeStart(i)
Else
x = timeStart(j)
End If
If timeEnd(i) < timeEnd(j) Then
y = timeEnd(i)
Else
y = timeEnd(j)
End If
'Update ovlpStart and ovlpEnd values for cases i and j if overlap region has increased for either
If x < ovlpStart(i) Then
ovlpStart(i) = x
End If
If x < ovlpStart(j) Then
ovlpStart(j) = x
End If
If y > ovlpEnd(i) Then
ovlpEnd(i) = y
End If
If y > ovlpEnd(j) Then
ovlpEnd(j) = y
End If
End If
Next
Next
'DETERMINE DURATION OF OVERLAP, PRINT ONTO SPREADSHEET
Dim ovlpDuration As Double
For i = 2 To totalRows
ovlpDuration = ovlpEnd(i) - ovlpStart(i)
If Not ovlpDuration Then
Cells(i, 7).Value = ovlpDuration
Else
Cells(i, 7).Value = 0
End If
Next`

The Excel Application object has the Intersect method available. If you treat the hours as imaginary rows on an imaginary worksheet and calculate the rows.count of a possible intersection between them, you can use that integer as the hours interval in a TimeSerial function.
Loose Overlap with Intersect
Sub overlapHours()
Dim i As Long, j As Long, ohrs As Double
With Worksheets("Sheet7")
For i = 2 To .Cells(Rows.Count, "C").End(xlUp).Row
ohrs = 0
For j = 2 To .Cells(Rows.Count, "C").End(xlUp).Row
If j <> i And Not Intersect(Range(Hour(.Cells(i, "B").Value2) & ":" & Hour(.Cells(i, "C").Value2)), _
Range(Hour(.Cells(j, "B").Value2) & ":" & Hour(.Cells(j, "C").Value2))) Is Nothing Then
ohrs = ohrs + TimeSerial(Intersect(Range(Hour(.Cells(i, "B").Value2) & ":" & Hour(.Cells(i, "C").Value2)), _
Range(Hour(.Cells(j, "B").Value2) & ":" & Hour(.Cells(j, "C").Value2))).Rows.Count - 1, 0, 0)
End If
Next j
.Cells(i, 4).NumberFormat = "[hh]:mm"
.Cells(i, 4) = ohrs
Next i
End With
End Sub
To avoid repeating the overlap times from one time period to the next, build a Union of the intersects of the imaginary rows. Unions can be discontiguous ranges so we need to cycle through the Range.Areas property to achieve a correct count of the Range.Rows property.
Strict Overlap with Intersect and Union
Sub intersectHours()
Dim a As Long, i As Long, j As Long, rng As Range, ohrs As Double
With Worksheets("Sheet7")
For i = 2 To .Cells(Rows.Count, "C").End(xlUp).Row
ohrs = 0: Set rng = Nothing
For j = 2 To .Cells(Rows.Count, "C").End(xlUp).Row
If j <> i And Not Intersect(.Range(Hour(.Cells(i, "B").Value2) & ":" & Hour(.Cells(i, "C").Value2) - 1), _
.Range(Hour(.Cells(j, "B").Value2) & ":" & Hour(.Cells(j, "C").Value2) - 1)) Is Nothing Then
If rng Is Nothing Then
Set rng = Intersect(.Range(Hour(.Cells(i, "B").Value2) & ":" & Hour(.Cells(i, "C").Value2) - 1), _
.Range(Hour(.Cells(j, "B").Value2) & ":" & Hour(.Cells(j, "C").Value2) - 1))
Else
Set rng = Union(rng, Intersect(.Range(Hour(.Cells(i, "B").Value2) & ":" & Hour(.Cells(i, "C").Value2) - 1), _
.Range(Hour(.Cells(j, "B").Value2) & ":" & Hour(.Cells(j, "C").Value2) - 1)))
End If
End If
Next j
If Not rng Is Nothing Then
For a = 1 To rng.Areas.Count
ohrs = ohrs + TimeSerial(rng.Areas(a).Rows.Count, 0, 0)
Next a
End If
.Cells(i, 6).NumberFormat = "[hh]:mm"
.Cells(i, 6) = ohrs
Next i
End With
End Sub
      
My results differ from the ones you posted for event 2 but I have traced my logic backwards and forwards and cannot see an error.

I can't say I entirely follow your logic. For example, I don't see why 1 & 4 don't overlap.
However, it's looking as though you'd just take the later of the compared start times and the earlier of the compared end times and subtract the latter from the former. If the result is positive then there's an overlap so aggregate the result within a loop.
I'm assuming your time values are in the Time format (ie hh:mm) and therefore Doubles.
The code below hardcodes your ranges so you'll need to adjust that as suits, but at least you could see the logic to get you going:
Dim tStart As Double
Dim tEnd As Double
Dim tDiff As Double
Dim v As Variant
Dim i As Integer
Dim j As Integer
Dim output(1 To 5, 1 To 2) As Variant
v = Sheet1.Range("A2:C6").Value2
For i = 1 To 5
For j = i + 1 To 5
tStart = IIf(v(i, 2) > v(j, 2), v(i, 2), v(j, 2))
tEnd = IIf(v(i, 3) < v(j, 3), v(i, 3), v(j, 3))
tDiff = tEnd - tStart
If tDiff > 0 Then
output(i, 1) = output(i, 1) + tDiff
output(j, 1) = output(j, 1) + tDiff
output(i, 2) = output(i, 2) & i & "&" & j & " "
output(j, 2) = output(j, 2) & i & "&" & j & " "
End If
Next
Next
Sheet1.Range("B9:C13").Value = output

Related

Numbering every event which was splited from range to separate line

I found a VBA code online which does the hardest part of splitting absences data from ranges to separate line for each day. But one thing I cannot figure out how to do it is how to assign a number to each day that was requested. Could anyone help me? For better understanding see screenshot. Greatly appreciated!
Yellow and Green coloured lines separates events. Orange is the thing I am trying to accomplish. Absences
Sub One_Day_Per_Row()
Dim a, b
Dim rws As Long, sr As Long, i As Long, j As Long, k As Long, r As Long
a = Range("A2", Range("A" & Rows.Count).End(xlUp)).Resize(, 6).Value
rws = UBound(a, 1)
For r = 1 To rws
a(r, 6) = a(r, 5) - a(r, 4) + 1
k = k + a(r, 6)
Next r
If k < Rows.Count Then
ReDim b(1 To k, 1 To 4)
sr = 1
For r = 1 To rws
For i = 0 To a(r, 6) - 1
For j = 1 To 3
b(sr + i, j) = a(r, j)
Next j
b(sr + i, 4) = a(r, 4) + i
Next i
sr = sr + a(r, 6)
Next r
Range("G2").Resize(k, 4).Value = b
Range("G1:J1").Value = Array("emp number", "emp name", "absence code", "date")
Else
MsgBox "Too many rows"
End If
End Sub
Something like this should work:
Sub Tester()
Dim data, rw As Long, ws As Worksheet, dStart, dEnd, d, n As Long
Dim cOut As Range
Set ws = ActiveSheet 'or whatever
'read the data
data = ws.Range("A2:E" & ws.Cells(Rows.Count, "A").End(xlUp).Row).Value
Set cOut = ws.Range("G2") 'cell to begin output
For rw = 1 To UBound(data, 1)
dStart = data(rw, 4) 'start date
dEnd = data(rw, 5) 'end date
n = 1 'reset counter
For d = dStart To dEnd 'loop date range
cOut.Resize(1, 3).Value = Array(data(rw, 1), data(rw, 2), data(rw, 3))
cOut.Offset(0, 3).Value = d
cOut.Offset(0, 4).Value = n
n = n + 1
Set cOut = cOut.Offset(1, 0) 'next output row
Next d
Next rw
End Sub

Matching a number in a column with the same number in the other column using VBA Excel

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

Excel vlookup to get all occurrence cell details?

I am doing automation on matching Data form row Data1 to Data 2,
I was done by looping statement but the problem is take much time, when number of row increase
For that reason i planed do by vlookup, In vlookup only return first occurrence cell but i need to find all match cell and highlighted matched row ,which i show in figure.
Working with cells directly reduces the code performance. Try to set Data1 and Data2 to arrays and work with arrays.
Something like this:
With ActiveSheet
arr = .Range(.[A2], .Cells(.Rows.Count, "A").End(xlUp)).Value
arr2 = .Range(.[D2], .Cells(.Rows.Count, "D").End(xlUp)).Value
For i& = 1 To UBound(arr)
For j& = 1 To UBound(arr2)
If arr(i, 1) = arr2(j) Then
...
End If
Next j
Next i
End With
Hope you are looking for this
Sub testvlookup()
Dim lastrow, lastrowdata, incre, i, j As Long
lastrow = Range("A" & Rows.Count).End(xlUp).Row
lastrowdata = Range("D" & Rows.Count).End(xlUp).Row
incre = 6
For i = 2 To lastrow
For j = 2 To lastrowdata
If Range("A" & i).Value = Range("D" & j).Value Then
Range("D" & j, "G" & j).Interior.ColorIndex = incre
End If
Next j
incre = incre + 1
Next i
End Sub
I don't see the point why it should be to slow for many rows, it would be good to have more informations about that.
I would do it like the others, takes ~1 sec with 100000 comparisons.
Dim i As Integer
Dim b As Integer
i = 1
While i < 20000
Range("A1:A5").Copy Range(Cells(i, 4), Cells(i + 5, 4))
i = i + 5
Wend
MsgBox ("hi")
i = 1
While i < 7
b = 3
While b < 20000
If Cells(i, 1).Value = Cells(b, 4).Value Then
Cells(b, 4).Interior.ColorIndex = i
End If
b = b + 1
Wend
i = i + 1
Wend
End Sub

Excel VBA Loop through table and sum up values

I have this table about 50,000 rows long that I would like Excel to go through and assign a number or letter.
Basically I am trying to group rows of data based on their sum being greater than 1,000,000.
If cell A in that row is less than 1,000,000 it will go to the next row and add up the previous cell A to the current one, and so on. This continues until the sum of all rows >= 1,000,000. When that happens, a number is "assigned" (as in entered at the end of the rows).
Sample data:
Here is my current "pseudo" code:
For x = 2 to lastrow
y = 1
If Range("A" & x).value < 1000000 Then
'I know something needs to be entered here but I don't know what
Do while balance < 1000000
sumbalance = Range("A" & x) + Range("A" & x + 1)
'Until sumbalance >= 1000000 Then Range("A" & x).Offset(0, 2).value = y
Else
Range("A" & x).offset(0, 2).value = y + 1 '(?)
Next x
Can someone point me the in the right direction?
With 50K rows, you will likely appreciate moving the values into a variant array for processing then returning them to the worksheet en masse.
Dim i As Long, rws As Long, dTTL As Double, v As Long, vVALs As Variant
With Worksheets("Sheet2")
vVALs = .Range(.Cells(2, "A"), .Cells(.Cells(Rows.Count, "A").End(xlUp).Row, "B")).Value2
For v = LBound(vVALs, 1) To UBound(vVALs, 1)
dTTL = dTTL + vVALs(v, 1): rws = rws + 1
If dTTL >= 10 ^ 6 Then
For i = v - rws + 1 To v
vVALs(i, 2) = rws
Next i
dTTL = 0: rws = 0
End If
Next v
.Cells(2, "A").Resize(UBound(vVALs, 1), UBound(vVALs, 2)) = vVALs
End With
It isn't clear how you wanted to end the sequence if the last set of numbers do not reach the 1M mark.
I hope i am clear in my comments, let me know if the code does what you want.
Option Explicit
Sub balance()
Dim wb As Workbook
Dim ws As Worksheet
Dim x As Double, y As Integer
Dim lastrow As Long
Dim sumbalance As Double
Dim Reached As Boolean
Set wb = ThisWorkbook
Set ws = wb.Sheets("Sheet1") 'Change the name of the sheet to yours
lastrow = ws.Range("A" & Rows.Count).End(xlUp).Row 'Check the last Row
For x = 2 To lastrow
y = 1 ' Number 1 will be past in column C when sumblance >= 1'000'000
Reached = False
Do
If Range("A" & x).Value < 10 ^ 6 Then ' Value less than 1'000'000
If sumbalance = 0 Then 'Start the sum balance at 0
sumbalance = Range("A" & x)
Else
sumbalance = Range("A" & x) + sumbalance 'We add the previous amount to the new one
x = x + 1
End If
Else
Range("A" & x).Offset(0, 2).Value = y ' If the number is directly >= 1'000'000
Reached = True
End If
Loop Until sumbalance >= 10 ^ 6 Or x = lastrow Or Reached = True
Range("A" & x).Offset(0, 2).Value = y 'when the Sum Balance is >= 1'000'000 so 1 is paste in column c
sumbalance = 0 'Reinitialize the balance to 0
Next x
End Sub

VBA: How to transform a one column full dictionary into one column per letter?

I have a full dictionary. All the words (360 000) are in one column.
I'd like to have Column B with all words starting with "a", column C with all words starting with b...
I am trying to do a loop or something... but... It is just too long.
Any tips? Or did someone already do this vba macro?
Tks,
Stéphane.
If we start with:
Running this short macro:
Sub SeparateData()
Dim N As Long, i As Long, NewCol As Long
Dim M As Long
N = Cells(Rows.Count, 1).End(xlUp).Row
For i = 1 To N
NewCol = Asc(UCase(Left(Cells(i, 1).Value, 1))) - 63
If Cells(1, NewCol).Value = "" Then
M = 1
Else
M = Cells(Rows.Count, NewCol).End(xlUp).Row + 1
End If
Cells(M, NewCol).Value = Cells(i, 1).Value
Next i
End Sub
will produce:
NOTE:
You may want to add some error capture logic to the NewCol calculation line.
EDIT#1:
This version may be slightly faster:
Sub SeparateDataFaster()
Dim N As Long, i As Long, NewCol As Long
Dim M As Long, time1 As Date, time2 As Date
N = Cells(Rows.Count, 1).End(xlUp).Row
time1 = Now
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
For i = 1 To N
NewCol = Asc(UCase(Left(Cells(i, 1).Value, 1))) - 63
If Cells(1, NewCol).Value = "" Then
M = 1
Else
M = Cells(Rows.Count, NewCol).End(xlUp).Row + 1
End If
Cells(M, NewCol).Value = Cells(i, 1).Value
Next i
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
time2 = Now
MsgBox time1 & vbCrLf & time2
End Sub
You can try something like this.
For 360k records its take about 20sec.
To create tests data i use this sub:
Sub FillTestData()
Dim t As Long
Dim lng As Integer
Dim text As String
'Start = Timer
For t = 1 To 360000
text = vbNullString
lng = 5 * Rnd + 10
For i = 1 To lng
Randomize
text = text & Chr(Int((26 * Rnd) + 65))
Next i
Cells(t, 1) = text
Next t
'Debug.Print Timer - Start
End Sub
And for separate:
Sub sep()
'Start = Timer
Dim ArrWords() As Variant
Dim Row_ As Long
LastRow = Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row
ArrWords = Range("A1:A" & LastRow) 'all data from column A to array
For i = 65 To 90 ' from A to Z
Row_ = 1
For j = LBound(ArrWords, 1) To UBound(ArrWords, 1)
If Asc(UCase(ArrWords(j, 1))) = i Then
Cells(Row_, i - 63) = ArrWords(j, 1)
Row_ = Row_ + 1
End If
Next j
Next i
'other than a[A]-z[Z]
Row_ = 1
For j = LBound(ArrWords, 1) To UBound(ArrWords, 1)
If Asc(UCase(ArrWords(j, 1))) < 65 Or Asc(UCase(ArrWords(j, 1))) > 90 Then
Cells(Row_, 28) = ArrWords(j, 1)
Row_ = Row_ + 1
End If
Next j
'Debug.Print Timer - Start
End Sub
You could try:
For i = 1 To Cells(Rows.count, 1).End(xlUp).Row
Range(UCase(Left$(Cells(i, 1).Text, 1)) & Rows.count).Offset(0, 1).End(xlUp).Offset(IIf(Range(UCase(Left$(Cells(i, _
1).Text, 1)) & Rows.count).Offset(0, 1).End(xlUp).Row = 1, 0, 1), 0).Value = Cells(i, 1).Text
Next i
Which is just building the destination address using the first letter of the word by doing the following:
Loop through each cell in column A
Get the first letter of that cell and convert it to upper case
Find the last cell in the column starting with that letter
Move over 1 column to the right
Go up until we hit the last row of data
If the last row isn't row 1, move down another row (next blank cell)
Give this cell the same value as the cell in column A that we're assessing
You can enter the following formula:
For letter A in B Column:
=IF(UPPER(LEFT(A1,1))="A",A1,"")
For letter B in C Column:
=IF(UPPER(LEFT(A1,1))="B",A1,"")
Repeat the same for letter C, D and so on..